Skip to content

Commit 1af23b6

Browse files
authored
Merge 3dcfdeb into ed2aa23
2 parents ed2aa23 + 3dcfdeb commit 1af23b6

32 files changed

Lines changed: 2055 additions & 0 deletions

docs/server-api.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# DeepCode Server API
2+
3+
[English](./server-api_en.md) · 中文
4+
5+
```bash
6+
deepcode-server --port 8787
7+
```
8+
9+
默认开启 token 鉴权。stdout 会打印类似:
10+
11+
```text
12+
deepcode server listening on http://127.0.0.1:8787 token=<token>
13+
```
14+
15+
通过以下任一方式传递:
16+
17+
- `?token=<token>`
18+
- `x-deepcode-token: <token>`
19+
- `Authorization: Bearer <token>`
20+
21+
## 范围
22+
23+
Server 暴露现有 DeepCode runtime / CLI-TUI 已有能力作为 http api。斜杠命令含义参考 [README 的“斜杠命令与按键功能”](../README.md#斜杠命令与按键功能)

docs/server-api_en.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# DeepCode Server API
2+
3+
English · [中文](./server-api.md)
4+
5+
```bash
6+
deepcode-server --port 8787
7+
```
8+
9+
Token authentication is enabled by default. stdout prints a line similar to:
10+
11+
```text
12+
deepcode server listening on http://127.0.0.1:8787 token=<token>
13+
```
14+
15+
Pass the token with any of:
16+
17+
- `?token=<token>`
18+
- `x-deepcode-token: <token>`
19+
- `Authorization: Bearer <token>`
20+
21+
## Scope
22+
23+
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).

package-lock.json

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/server/package.json

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"name": "@vegamo/deepcode-server",
3+
"version": "0.1.31",
4+
"description": "Deep Code local HTTP/SSE server runtime host",
5+
"license": "MIT",
6+
"type": "module",
7+
"repository": {
8+
"type": "git",
9+
"url": "git+https://github.com/lessweb/deepcode-cli.git"
10+
},
11+
"homepage": "https://deepcode.vegamo.cn",
12+
"bin": {
13+
"deepcode-server": "./dist/server.js",
14+
"deepcode-headless-server": "./dist/server.js"
15+
},
16+
"main": "./dist/index.js",
17+
"types": "./dist/index.d.ts",
18+
"files": [
19+
"dist/**",
20+
"README.md",
21+
"LICENSE"
22+
],
23+
"engines": {
24+
"node": ">=22"
25+
},
26+
"scripts": {
27+
"typecheck": "tsc -p ./ --noEmit",
28+
"build": "tsc -p ./ && node scripts/rewrite-esm-imports.js && node -e \"require('fs').chmodSync('dist/server.js', 0o755)\"",
29+
"test": "tsx --test src/tests/*.test.ts",
30+
"prepublishOnly": "npm run build",
31+
"format": "prettier --write ."
32+
},
33+
"dependencies": {
34+
"@vegamo/deepcode-core": "file:../core"
35+
}
36+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/**
2+
* Post-build script: rewrites extensionless relative imports in the server
3+
* package's dist/ output to include explicit ".js" extensions.
4+
*
5+
* Keep this package-local so the server feature does not change the existing
6+
* root-level core rewrite script.
7+
*/
8+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
9+
import { dirname, join } from "node:path";
10+
import { fileURLToPath } from "node:url";
11+
import { globSync } from "glob";
12+
13+
const __dirname = dirname(fileURLToPath(import.meta.url));
14+
const packageRoot = join(__dirname, "..");
15+
const distDir = join(packageRoot, "dist");
16+
17+
if (!existsSync(distDir)) {
18+
throw new Error(`Cannot rewrite server ESM imports because dist directory does not exist: ${distDir}`);
19+
}
20+
21+
const files = globSync("**/*.js", { cwd: distDir, absolute: true });
22+
23+
// Match: from "./anything" or from "../anything"
24+
// Negative lookahead: skip if already ends with .js, .json, .node, or is a bare specifier
25+
const IMPORT_RE = /(from\s+["'])(\.\.?\/[^"']+?)(?<!\.[a-zA-Z0-9]{1,4})(["'])/g;
26+
27+
let totalRewrites = 0;
28+
29+
for (const filePath of files) {
30+
const original = readFileSync(filePath, "utf8");
31+
let rewrites = 0;
32+
33+
const updated = original.replace(IMPORT_RE, (_match, prefix, specifier, quote) => {
34+
rewrites++;
35+
return `${prefix}${specifier}.js${quote}`;
36+
});
37+
38+
if (rewrites > 0) {
39+
writeFileSync(filePath, updated, "utf8");
40+
totalRewrites += rewrites;
41+
}
42+
}
43+
44+
console.log(`\n✅ Rewrote ${totalRewrites} imports across ${files.length} files in server/dist/\n`);

packages/server/src/command-map.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/**
2+
* Server slash-command route metadata.
3+
*
4+
* Summary:
5+
* Maps supported DeepCode runtime slash commands to HTTP route metadata for the
6+
* local server. This module is intentionally independent from the CLI slash
7+
* command table so server behavior does not depend on terminal UI code.
8+
*
9+
* Exports:
10+
* - buildHeadlessCommandRoutes(): HeadlessCommandRoute[]
11+
* - findHeadlessCommandRoute(pathname: string): HeadlessCommandRoute | null
12+
*/
13+
export type HeadlessCommandRoute = {
14+
name: string;
15+
label: string;
16+
description: string;
17+
method: "GET" | "POST";
18+
path: string;
19+
aliases: string[];
20+
implemented: boolean;
21+
};
22+
23+
type RuntimeCommand = {
24+
name: string;
25+
label: string;
26+
description: string;
27+
};
28+
29+
const RUNTIME_COMMANDS: RuntimeCommand[] = [
30+
{ name: "skills", label: "/skills", description: "List available skills" },
31+
{ name: "model", label: "/model", description: "Select model, thinking mode and effort control" },
32+
{ name: "new", label: "/new", description: "Start a fresh conversation" },
33+
{ name: "init", label: "/init", description: "Initialize an AGENTS.md file with instructions for LLM" },
34+
{ name: "resume", label: "/resume", description: "Pick a previous conversation to continue" },
35+
{ name: "continue", label: "/continue", description: "Continue the active conversation or pick one to resume" },
36+
{ name: "undo", label: "/undo", description: "Restore code and/or conversation to a previous point" },
37+
{ name: "mcp", label: "/mcp", description: "Show MCP server status and available tools" },
38+
{ name: "raw", label: "/raw", description: "CLI display mode command; no backend raw display state is exposed" },
39+
{ name: "exit", label: "/exit", description: "Quit Deep Code server" },
40+
];
41+
42+
const READ_ONLY_COMMANDS = new Set(["skills", "resume", "mcp", "model"]);
43+
const IMPLEMENTED_COMMANDS = new Set(["skills", "new", "init", "resume", "continue", "undo", "mcp", "model", "exit"]);
44+
45+
function commandMethod(command: RuntimeCommand): "GET" | "POST" {
46+
return READ_ONLY_COMMANDS.has(command.name) ? "GET" : "POST";
47+
}
48+
49+
function commandAliases(command: RuntimeCommand): string[] {
50+
if (command.name === "model") {
51+
return ["/model"];
52+
}
53+
return [`/${command.name}`, `/api/${command.name}`];
54+
}
55+
56+
export function buildHeadlessCommandRoutes(): HeadlessCommandRoute[] {
57+
return RUNTIME_COMMANDS.map((command) => ({
58+
name: command.name,
59+
label: command.label,
60+
description: command.description,
61+
method: commandMethod(command),
62+
path: `/${command.name}`,
63+
aliases: commandAliases(command),
64+
implemented: IMPLEMENTED_COMMANDS.has(command.name),
65+
}));
66+
}
67+
68+
export function findHeadlessCommandRoute(pathname: string): HeadlessCommandRoute | null {
69+
const normalized = pathname.replace(/\/+$/u, "") || "/";
70+
return buildHeadlessCommandRoutes().find((route) => route.aliases.includes(normalized)) ?? null;
71+
}

packages/server/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { runHeadlessHttp } from "./services/http-server";
2+
export type { HeadlessOptions } from "./services/http-server";
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* Server model option constants.
3+
*
4+
* Summary:
5+
* Defines the model and thinking-mode choices exposed by the local HTTP/SSE
6+
* server contract. This module contains data only and has no CLI, UI, Ink, or
7+
* React dependency.
8+
*
9+
* Exports:
10+
* - MODEL_COMMAND_MODELS: readonly model id list exposed by GET /model.
11+
* - MODEL_COMMAND_THINKING_OPTIONS: thinking-mode choices exposed by GET /model.
12+
*/
13+
import type { ReasoningEffort } from "@vegamo/deepcode-core";
14+
15+
export type ThinkingModeOption = {
16+
label: string;
17+
thinkingEnabled: boolean;
18+
reasoningEffort?: ReasoningEffort;
19+
};
20+
21+
export const MODEL_COMMAND_MODELS = ["deepseek-v4-pro", "deepseek-v4-flash"] as const;
22+
23+
export const MODEL_COMMAND_THINKING_OPTIONS: ThinkingModeOption[] = [
24+
{ label: "Thinking mode [max]", thinkingEnabled: true, reasoningEffort: "max" },
25+
{ label: "Thinking mode [high]", thinkingEnabled: true, reasoningEffort: "high" },
26+
{ label: "No thinking", thinkingEnabled: false },
27+
];

packages/server/src/server.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
#!/usr/bin/env node
2+
import * as path from "node:path";
3+
import { createRequire } from "node:module";
4+
import { setShellIfWindows } from "@vegamo/deepcode-core";
5+
import { runHeadlessHttp } from "./index";
6+
import { readArgValue } from "./services/server-options";
7+
8+
const require = createRequire(import.meta.url);
9+
10+
type PackageInfo = {
11+
version?: unknown;
12+
};
13+
14+
const args = process.argv.slice(2);
15+
const version = readVersion();
16+
17+
if (args.includes("--version") || args.includes("-v")) {
18+
process.stdout.write(`${version}\n`);
19+
process.exit(0);
20+
}
21+
22+
if (args.includes("--help") || args.includes("-h")) {
23+
process.stdout.write(buildHelpText());
24+
process.exit(0);
25+
}
26+
27+
const projectRoot = path.resolve(readArgValue(args, "--project-root") ?? process.cwd());
28+
configureWindowsShell();
29+
30+
try {
31+
await runHeadlessHttp({ args, projectRoot, version });
32+
} catch (error) {
33+
const message = error instanceof Error ? error.message : String(error);
34+
process.stderr.write(`deepcode server failed: ${message}\n`);
35+
process.exit(1);
36+
}
37+
38+
function configureWindowsShell(): void {
39+
process.env.NoDefaultCurrentDirectoryInExePath = "1";
40+
try {
41+
setShellIfWindows();
42+
} catch (error) {
43+
const message = error instanceof Error ? error.message : String(error);
44+
process.stderr.write(`deepcode server: ${message}\n`);
45+
process.exit(1);
46+
}
47+
}
48+
49+
function readVersion(): string {
50+
try {
51+
const pkg = require("../package.json") as PackageInfo;
52+
return typeof pkg.version === "string" ? pkg.version : "unknown";
53+
} catch {
54+
return "unknown";
55+
}
56+
}
57+
58+
function buildHelpText(): string {
59+
return [
60+
"deepcode-server - Deep Code local HTTP/SSE runtime server",
61+
"",
62+
"Usage:",
63+
" deepcode-server [--host <host>] [--port <port>] [--project-root <path>]",
64+
" deepcode-server --version",
65+
" deepcode-server --help",
66+
"",
67+
"Options:",
68+
" --host <host> Bind address. Defaults to 127.0.0.1.",
69+
" --port <port> Bind port. Defaults to 8787.",
70+
" --project-root <path> Deep Code project root. Defaults to current working directory.",
71+
" --no-auth Turn off local token auth for trusted local development.",
72+
" --unsafe-bind Allow non-local bind addresses.",
73+
" --version, -v Print the version.",
74+
" --help, -h Show this help.",
75+
"",
76+
].join("\n");
77+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
* Request authorization helpers.
3+
*
4+
* Summary:
5+
* Contains token matching logic for local HTTP requests. This is separated from
6+
* route execution so auth policy can be audited independently.
7+
*
8+
* Exports:
9+
* - isAuthorized(request: IncomingMessage, token: string): boolean
10+
*/
11+
import type { IncomingMessage } from "node:http";
12+
13+
export function isAuthorized(request: IncomingMessage, token: string): boolean {
14+
const url = new URL(request.url ?? "/", "http://127.0.0.1");
15+
if (url.searchParams.get("token") === token) {
16+
return true;
17+
}
18+
19+
const tokenHeader = firstHeaderValue(request.headers["x-deepcode-token"]);
20+
if (tokenHeader === token) {
21+
return true;
22+
}
23+
24+
const authorization = firstHeaderValue(request.headers.authorization);
25+
if (!authorization) {
26+
return false;
27+
}
28+
29+
const [scheme, ...rest] = authorization.trim().split(/\s+/u);
30+
return scheme.toLowerCase() === "bearer" && rest.join(" ") === token;
31+
}
32+
33+
function firstHeaderValue(value: string | string[] | undefined): string {
34+
return Array.isArray(value) ? value[0] ?? "" : value ?? "";
35+
}

0 commit comments

Comments
 (0)