Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions docs/server-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# DeepCode Server API

[English](./server-api_en.md) · 中文

```bash
deepcode-server --port 8787
```

默认开启 token 鉴权。stdout 会打印类似:

```text
deepcode server listening on http://127.0.0.1:8787 token=<token>
```

通过以下任一方式传递:

- `?token=<token>`
- `x-deepcode-token: <token>`
- `Authorization: Bearer <token>`

## 范围

Server 暴露现有 DeepCode runtime / CLI-TUI 已有能力作为 http api。斜杠命令含义参考 [README 的“斜杠命令与按键功能”](../README.md#斜杠命令与按键功能)。
23 changes: 23 additions & 0 deletions docs/server-api_en.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# DeepCode Server API

English · [中文](./server-api.md)

```bash
deepcode-server --port 8787
```

Token authentication is enabled by default. stdout prints a line similar to:

```text
deepcode server listening on http://127.0.0.1:8787 token=<token>
```

Pass the token with any of:

- `?token=<token>`
- `x-deepcode-token: <token>`
- `Authorization: Bearer <token>`

## Scope

The server exposes existing DeepCode runtime / CLI-TUI capabilities as HTTP APIs. For slash command meanings, see [README “Slash Commands & Keyboard Shortcuts”](../README-en.md#slash-commands--keyboard-shortcuts).
19 changes: 19 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 36 additions & 0 deletions packages/server/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"name": "@vegamo/deepcode-server",
"version": "0.1.31",
"description": "Deep Code local HTTP/SSE server runtime host",
"license": "MIT",
"type": "module",
"repository": {
"type": "git",
"url": "git+https://github.com/lessweb/deepcode-cli.git"
},
"homepage": "https://deepcode.vegamo.cn",
"bin": {
"deepcode-server": "./dist/server.js",
"deepcode-headless-server": "./dist/server.js"
},
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": [
"dist/**",
"README.md",
"LICENSE"
],
"engines": {
"node": ">=22"
},
"scripts": {
"typecheck": "tsc -p ./ --noEmit",
"build": "tsc -p ./ && node scripts/rewrite-esm-imports.js && node -e \"require('fs').chmodSync('dist/server.js', 0o755)\"",
"test": "tsx --test src/tests/*.test.ts",
"prepublishOnly": "npm run build",
"format": "prettier --write ."
},
"dependencies": {
"@vegamo/deepcode-core": "file:../core"
}
}
44 changes: 44 additions & 0 deletions packages/server/scripts/rewrite-esm-imports.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* Post-build script: rewrites extensionless relative imports in the server
* package's dist/ output to include explicit ".js" extensions.
*
* Keep this package-local so the server feature does not change the existing
* root-level core rewrite script.
*/
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { globSync } from "glob";

const __dirname = dirname(fileURLToPath(import.meta.url));
const packageRoot = join(__dirname, "..");
const distDir = join(packageRoot, "dist");

if (!existsSync(distDir)) {
throw new Error(`Cannot rewrite server ESM imports because dist directory does not exist: ${distDir}`);
}

const files = globSync("**/*.js", { cwd: distDir, absolute: true });

// Match: from "./anything" or from "../anything"
// Negative lookahead: skip if already ends with .js, .json, .node, or is a bare specifier
const IMPORT_RE = /(from\s+["'])(\.\.?\/[^"']+?)(?<!\.[a-zA-Z0-9]{1,4})(["'])/g;

let totalRewrites = 0;

for (const filePath of files) {
const original = readFileSync(filePath, "utf8");
let rewrites = 0;

const updated = original.replace(IMPORT_RE, (_match, prefix, specifier, quote) => {
rewrites++;
return `${prefix}${specifier}.js${quote}`;
});

if (rewrites > 0) {
writeFileSync(filePath, updated, "utf8");
totalRewrites += rewrites;
}
}

console.log(`\n✅ Rewrote ${totalRewrites} imports across ${files.length} files in server/dist/\n`);
71 changes: 71 additions & 0 deletions packages/server/src/command-map.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* Server slash-command route metadata.
*
* Summary:
* Maps supported DeepCode runtime slash commands to HTTP route metadata for the
* local server. This module is intentionally independent from the CLI slash
* command table so server behavior does not depend on terminal UI code.
*
* Exports:
* - buildHeadlessCommandRoutes(): HeadlessCommandRoute[]
* - findHeadlessCommandRoute(pathname: string): HeadlessCommandRoute | null
*/
export type HeadlessCommandRoute = {
name: string;
label: string;
description: string;
method: "GET" | "POST";
path: string;
aliases: string[];
implemented: boolean;
};

type RuntimeCommand = {
name: string;
label: string;
description: string;
};

const RUNTIME_COMMANDS: RuntimeCommand[] = [
{ name: "skills", label: "/skills", description: "List available skills" },
{ name: "model", label: "/model", description: "Select model, thinking mode and effort control" },
{ name: "new", label: "/new", description: "Start a fresh conversation" },
{ name: "init", label: "/init", description: "Initialize an AGENTS.md file with instructions for LLM" },
{ name: "resume", label: "/resume", description: "Pick a previous conversation to continue" },
{ name: "continue", label: "/continue", description: "Continue the active conversation or pick one to resume" },
{ name: "undo", label: "/undo", description: "Restore code and/or conversation to a previous point" },
{ name: "mcp", label: "/mcp", description: "Show MCP server status and available tools" },
{ name: "raw", label: "/raw", description: "CLI display mode command; no backend raw display state is exposed" },
{ name: "exit", label: "/exit", description: "Quit Deep Code server" },
];

const READ_ONLY_COMMANDS = new Set(["skills", "resume", "mcp", "model"]);
const IMPLEMENTED_COMMANDS = new Set(["skills", "new", "init", "resume", "continue", "undo", "mcp", "model", "exit"]);

function commandMethod(command: RuntimeCommand): "GET" | "POST" {
return READ_ONLY_COMMANDS.has(command.name) ? "GET" : "POST";
}

function commandAliases(command: RuntimeCommand): string[] {
if (command.name === "model") {
return ["/model"];
}
return [`/${command.name}`, `/api/${command.name}`];
}

export function buildHeadlessCommandRoutes(): HeadlessCommandRoute[] {
return RUNTIME_COMMANDS.map((command) => ({
name: command.name,
label: command.label,
description: command.description,
method: commandMethod(command),
path: `/${command.name}`,
aliases: commandAliases(command),
implemented: IMPLEMENTED_COMMANDS.has(command.name),
}));
}

export function findHeadlessCommandRoute(pathname: string): HeadlessCommandRoute | null {
const normalized = pathname.replace(/\/+$/u, "") || "/";
return buildHeadlessCommandRoutes().find((route) => route.aliases.includes(normalized)) ?? null;
}
2 changes: 2 additions & 0 deletions packages/server/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { runHeadlessHttp } from "./services/http-server";
export type { HeadlessOptions } from "./services/http-server";
27 changes: 27 additions & 0 deletions packages/server/src/model-options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* Server model option constants.
*
* Summary:
* Defines the model and thinking-mode choices exposed by the local HTTP/SSE
* server contract. This module contains data only and has no CLI, UI, Ink, or
* React dependency.
*
* Exports:
* - MODEL_COMMAND_MODELS: readonly model id list exposed by GET /model.
* - MODEL_COMMAND_THINKING_OPTIONS: thinking-mode choices exposed by GET /model.
*/
import type { ReasoningEffort } from "@vegamo/deepcode-core";

export type ThinkingModeOption = {
label: string;
thinkingEnabled: boolean;
reasoningEffort?: ReasoningEffort;
};

export const MODEL_COMMAND_MODELS = ["deepseek-v4-pro", "deepseek-v4-flash"] as const;

export const MODEL_COMMAND_THINKING_OPTIONS: ThinkingModeOption[] = [
{ label: "Thinking mode [max]", thinkingEnabled: true, reasoningEffort: "max" },
{ label: "Thinking mode [high]", thinkingEnabled: true, reasoningEffort: "high" },
{ label: "No thinking", thinkingEnabled: false },
];
77 changes: 77 additions & 0 deletions packages/server/src/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#!/usr/bin/env node
import * as path from "node:path";
import { createRequire } from "node:module";
import { setShellIfWindows } from "@vegamo/deepcode-core";
import { runHeadlessHttp } from "./index";
import { readArgValue } from "./services/server-options";

const require = createRequire(import.meta.url);

type PackageInfo = {
version?: unknown;
};

const args = process.argv.slice(2);
const version = readVersion();

if (args.includes("--version") || args.includes("-v")) {
process.stdout.write(`${version}\n`);
process.exit(0);
}

if (args.includes("--help") || args.includes("-h")) {
process.stdout.write(buildHelpText());
process.exit(0);
}

const projectRoot = path.resolve(readArgValue(args, "--project-root") ?? process.cwd());
configureWindowsShell();

try {
await runHeadlessHttp({ args, projectRoot, version });
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
process.stderr.write(`deepcode server failed: ${message}\n`);
process.exit(1);
}

function configureWindowsShell(): void {
process.env.NoDefaultCurrentDirectoryInExePath = "1";
try {
setShellIfWindows();
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
process.stderr.write(`deepcode server: ${message}\n`);
process.exit(1);
}
}

function readVersion(): string {
try {
const pkg = require("../package.json") as PackageInfo;
return typeof pkg.version === "string" ? pkg.version : "unknown";
} catch {
return "unknown";
}
}

function buildHelpText(): string {
return [
"deepcode-server - Deep Code local HTTP/SSE runtime server",
"",
"Usage:",
" deepcode-server [--host <host>] [--port <port>] [--project-root <path>]",
" deepcode-server --version",
" deepcode-server --help",
"",
"Options:",
" --host <host> Bind address. Defaults to 127.0.0.1.",
" --port <port> Bind port. Defaults to 8787.",
" --project-root <path> Deep Code project root. Defaults to current working directory.",
" --no-auth Turn off local token auth for trusted local development.",
" --unsafe-bind Allow non-local bind addresses.",
" --version, -v Print the version.",
" --help, -h Show this help.",
"",
].join("\n");
}
35 changes: 35 additions & 0 deletions packages/server/src/services/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* Request authorization helpers.
*
* Summary:
* Contains token matching logic for local HTTP requests. This is separated from
* route execution so auth policy can be audited independently.
*
* Exports:
* - isAuthorized(request: IncomingMessage, token: string): boolean
*/
import type { IncomingMessage } from "node:http";

export function isAuthorized(request: IncomingMessage, token: string): boolean {
const url = new URL(request.url ?? "/", "http://127.0.0.1");
if (url.searchParams.get("token") === token) {
return true;
}

const tokenHeader = firstHeaderValue(request.headers["x-deepcode-token"]);
if (tokenHeader === token) {
return true;
}

const authorization = firstHeaderValue(request.headers.authorization);
if (!authorization) {
return false;
}

const [scheme, ...rest] = authorization.trim().split(/\s+/u);
return scheme.toLowerCase() === "bearer" && rest.join(" ") === token;
}

function firstHeaderValue(value: string | string[] | undefined): string {
return Array.isArray(value) ? value[0] ?? "" : value ?? "";
}
17 changes: 17 additions & 0 deletions packages/server/src/services/events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* Server event envelope types.
*
* Summary:
* Defines the structured event envelope used by the HTTP/SSE server. Event
* production remains in the runtime service until the runtime class is split.
*
* Exports:
* - type HeadlessEvent
*/
export type HeadlessEvent = {
type: string;
requestId?: string;
sequence?: number;
timestamp?: string;
[key: string]: unknown;
};
Loading