napbox 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,208 @@
1
+ const OPERATOR_CHARACTERS = new Set([";", "&", "|", "\n", "(", ")", "`"]);
2
+ function tokenize(command) {
3
+ const tokens = [];
4
+ let word;
5
+ const flush = () => {
6
+ if (word !== undefined)
7
+ tokens.push({ word });
8
+ word = undefined;
9
+ };
10
+ for (let i = 0; i < command.length; i++) {
11
+ const c = command[i];
12
+ if (c === "\\") {
13
+ i++;
14
+ if (command[i] !== "\n")
15
+ word = (word ?? "") + (command[i] ?? "");
16
+ }
17
+ else if (c === "'") {
18
+ const end = command.indexOf("'", i + 1);
19
+ const stop = end < 0 ? command.length : end;
20
+ word = (word ?? "") + command.slice(i + 1, stop);
21
+ i = stop;
22
+ }
23
+ else if (c === '"') {
24
+ word ??= "";
25
+ for (i++; i < command.length && command[i] !== '"'; i++) {
26
+ if (command[i] === "\\" && i + 1 < command.length)
27
+ i++;
28
+ word += command[i];
29
+ }
30
+ }
31
+ else if (c === "$" && command[i + 1] === "(") {
32
+ flush();
33
+ tokens.push({ operator: true });
34
+ i++;
35
+ }
36
+ else if (OPERATOR_CHARACTERS.has(c)) {
37
+ flush();
38
+ tokens.push({ operator: true });
39
+ }
40
+ else if (c === " " || c === "\t") {
41
+ flush();
42
+ }
43
+ else if (c === "#" && word === undefined) {
44
+ while (i + 1 < command.length && command[i + 1] !== "\n")
45
+ i++;
46
+ }
47
+ else {
48
+ word = (word ?? "") + c;
49
+ }
50
+ }
51
+ flush();
52
+ return tokens;
53
+ }
54
+ /** 这些命令后面跟的才是真正运行的命令;值是它们自己带参数的选项 */
55
+ const WRAPPERS = new Map([
56
+ ["sudo", new Set(["-u", "-g", "-h", "-p", "-C", "-T", "-U"])],
57
+ ["env", new Set(["-u", "-C", "-S"])],
58
+ ["command", new Set()],
59
+ ["exec", new Set(["-a"])],
60
+ ["nohup", new Set()],
61
+ ["time", new Set()],
62
+ ["nice", new Set(["-n"])],
63
+ ]);
64
+ const SHELLS = new Set(["sh", "bash", "zsh", "dash"]);
65
+ const baseName = (word) => word.slice(word.lastIndexOf("/") + 1);
66
+ const isAssignment = (word) => /^[A-Za-z_][A-Za-z0-9_]*=/.test(word);
67
+ /** 去掉变量赋值和 sudo、env 这类前缀,剩下真正运行的命令 */
68
+ function unwrap(argv) {
69
+ let rest = argv;
70
+ for (;;) {
71
+ while (rest[0] !== undefined && isAssignment(rest[0]))
72
+ rest = rest.slice(1);
73
+ const name = rest[0] === undefined ? undefined : baseName(rest[0]);
74
+ if (name === "timeout") {
75
+ let i = 1;
76
+ while (rest[i]?.startsWith("-"))
77
+ i++;
78
+ rest = rest.slice(i + 1); // 跳过时长
79
+ continue;
80
+ }
81
+ const options = name === undefined ? undefined : WRAPPERS.get(name);
82
+ if (!options)
83
+ return rest;
84
+ let i = 1;
85
+ while (rest[i]?.startsWith("-"))
86
+ i += options.has(rest[i]) ? 2 : 1;
87
+ rest = rest.slice(i);
88
+ }
89
+ }
90
+ /** 拆成简单命令。sh -c '...' 里的内容也拆开看 */
91
+ export function simpleCommands(command, depth = 0) {
92
+ const commands = [];
93
+ const cds = [];
94
+ let argv = [];
95
+ const finish = () => {
96
+ const words = unwrap(argv);
97
+ argv = [];
98
+ const name = words[0] === undefined ? undefined : baseName(words[0]);
99
+ if (name === undefined)
100
+ return;
101
+ if (name === "cd") {
102
+ const target = words[1];
103
+ if (target && target !== "-" && !target.includes("$"))
104
+ cds.push(target);
105
+ return;
106
+ }
107
+ const script = SHELLS.has(name) ? shellScript(words) : undefined;
108
+ if (script !== undefined && depth < 3) {
109
+ for (const inner of simpleCommands(script, depth + 1))
110
+ commands.push({ argv: inner.argv, cds: [...cds, ...inner.cds] });
111
+ return;
112
+ }
113
+ commands.push({ argv: words, cds: [...cds] });
114
+ };
115
+ for (const token of tokenize(command)) {
116
+ if ("word" in token)
117
+ argv.push(token.word);
118
+ else
119
+ finish();
120
+ }
121
+ finish();
122
+ return commands;
123
+ }
124
+ /** sh -c、bash -lc 这类写法里 -c 后面的脚本 */
125
+ function shellScript(words) {
126
+ for (let i = 1; i < words.length; i++) {
127
+ const word = words[i];
128
+ if (!word.startsWith("-"))
129
+ return undefined;
130
+ if (!word.startsWith("--") && word.includes("c"))
131
+ return words[i + 1];
132
+ }
133
+ return undefined;
134
+ }
135
+ /** docker 和 docker compose 自己带值的选项,子命令在它们后面 */
136
+ const DOCKER_OPTIONS = new Set(["--config", "-c", "--context", "-H", "--host", "-l", "--log-level"]);
137
+ const COMPOSE_OPTIONS = new Set([
138
+ "--ansi",
139
+ "--env-file",
140
+ "-f",
141
+ "--file",
142
+ "--parallel",
143
+ "--profile",
144
+ "--progress",
145
+ "--project-directory",
146
+ "-p",
147
+ "--project-name",
148
+ ]);
149
+ function firstWordAfterOptions(words, options) {
150
+ let i = 0;
151
+ while (words[i]?.startsWith("-"))
152
+ i += options.has(words[i]) ? 2 : 1;
153
+ return i;
154
+ }
155
+ /** docker 自己的选项之后的词,第一个是子命令;不是 docker 命令时是 undefined */
156
+ function dockerWords(argv) {
157
+ if (argv[0] === undefined || baseName(argv[0]) !== "docker")
158
+ return undefined;
159
+ const rest = argv.slice(1);
160
+ return rest.slice(firstWordAfterOptions(rest, DOCKER_OPTIONS));
161
+ }
162
+ /** docker compose 的子命令,不是 docker compose 时是 undefined */
163
+ export function composeSubcommand(argv) {
164
+ let rest;
165
+ if (argv[0] !== undefined && baseName(argv[0]) === "docker-compose") {
166
+ rest = argv.slice(1);
167
+ }
168
+ else {
169
+ const words = dockerWords(argv);
170
+ if (words?.[0] !== "compose")
171
+ return undefined;
172
+ rest = words.slice(1);
173
+ }
174
+ const i = firstWordAfterOptions(rest, COMPOSE_OPTIONS);
175
+ if (rest.includes("--help") || rest.includes("-h"))
176
+ return undefined;
177
+ return rest[i];
178
+ }
179
+ /** 是不是 docker run(或者同一件事的另一种写法 docker container run):不靠 compose 直接起一个容器 */
180
+ export function isDockerRun(argv) {
181
+ const words = dockerWords(argv);
182
+ if (words === undefined || words.includes("--help"))
183
+ return false;
184
+ return words[0] === "run" || (words[0] === "container" && words[1] === "run");
185
+ }
186
+ /** 自己在宿主机上起数据库这类服务的命令,返回一句话描述;不是时返回 undefined */
187
+ export function startsOwnService(argv) {
188
+ const name = argv[0] === undefined ? undefined : baseName(argv[0]);
189
+ switch (name) {
190
+ case "postgres":
191
+ case "mysqld":
192
+ case "mysqld_safe":
193
+ case "mongod":
194
+ case "redis-server":
195
+ return argv.includes("--version") || argv.includes("--help") ? undefined : name;
196
+ case "pg_ctl":
197
+ case "mysql.server":
198
+ return argv.includes("start") || argv.includes("restart") ? name : undefined;
199
+ case "minio":
200
+ return argv[1] === "server" ? "minio server" : undefined;
201
+ case "brew":
202
+ return argv[1] === "services" && (argv[2] === "start" || argv[2] === "run" || argv[2] === "restart")
203
+ ? "brew services"
204
+ : undefined;
205
+ default:
206
+ return undefined;
207
+ }
208
+ }
@@ -0,0 +1,43 @@
1
+ import { z } from "zod";
2
+ import type { NapboxClient } from "../client.ts";
3
+ export interface ToolContext {
4
+ client: NapboxClient;
5
+ /** 没传 directory 时用的目录:会话工作的地方 */
6
+ sessionDirectory(): Promise<string>;
7
+ signal: AbortSignal;
8
+ }
9
+ export interface ToolResult {
10
+ value: unknown;
11
+ isError: boolean;
12
+ }
13
+ interface Tool<S extends z.ZodObject = z.ZodObject> {
14
+ name: string;
15
+ title: string;
16
+ description: string;
17
+ input: S;
18
+ annotations: {
19
+ readOnlyHint: boolean;
20
+ destructiveHint: boolean;
21
+ idempotentHint: boolean;
22
+ };
23
+ run(args: z.output<S>, context: ToolContext): Promise<unknown>;
24
+ }
25
+ export declare const tools: readonly Tool[];
26
+ export declare function listTools(): {
27
+ name: string;
28
+ title: string;
29
+ description: string;
30
+ inputSchema: {
31
+ [x: string]: unknown;
32
+ };
33
+ annotations: {
34
+ readOnlyHint: boolean;
35
+ destructiveHint: boolean;
36
+ idempotentHint: boolean;
37
+ title: string;
38
+ openWorldHint: boolean;
39
+ };
40
+ }[];
41
+ /** 工具的错误都放进结果(isError),不当 JSON-RPC 错误:agent 能看到原因 */
42
+ export declare function callTool(name: string, rawArguments: unknown, context: ToolContext): Promise<ToolResult>;
43
+ export {};
@@ -0,0 +1,295 @@
1
+ // MCP 工具,和命令行一一对应(DESIGN.md 第 4 节)。出错时的内容和命令行一样是 {"error": {"code", "message"}}。
2
+ import path from "node:path";
3
+ import { z } from "zod";
4
+ import { NapboxError } from "../client.js";
5
+ import { EgressMode } from "../protocol.js";
6
+ import { setup } from "./setup.js";
7
+ /** 一次结果里每种输出最多留这么多字节(留最后的部分):太长的结果 agent 也读不了,要全部内容就重定向到文件 */
8
+ const OUTPUT_LIMIT = 48 * 1024;
9
+ const DEFAULT_EXEC_SECONDS = 120;
10
+ /** run 的时间从起环境算起,第一次还要拉镜像、构建 */
11
+ const DEFAULT_RUN_SECONDS = 600;
12
+ const directory = z
13
+ .string()
14
+ .optional()
15
+ .describe("在哪个目录操作(环境按所在 worktree 识别)。不写就是会话工作的目录;会话换了 worktree 时要写。");
16
+ /** 操作临时环境的工具共用;up 的说明另写(它是起,不是找) */
17
+ const temp = z
18
+ .string()
19
+ .optional()
20
+ .describe("操作这个 worktree 里叫这个名字的临时环境(up 传 temp 起的)。不写是开发环境。");
21
+ const READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true };
22
+ const WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: true };
23
+ const DESTROY = { readOnlyHint: false, destructiveHint: true, idempotentHint: false };
24
+ const define = (tool) => tool;
25
+ async function target(args, context) {
26
+ const base = await context.sessionDirectory();
27
+ return { cwd: args.directory ? path.resolve(base, args.directory) : base, temp: args.temp };
28
+ }
29
+ /** compose 变量替换用 MCP 服务自己的环境变量,和命令行用它自己的一样 */
30
+ const processEnvironment = () => Object.fromEntries(Object.entries(process.env).filter((entry) => entry[1] !== undefined));
31
+ const timeoutSeconds = (fallback, from) => z.int().min(1).max(3600).optional().describe(`超时后结束命令,默认 ${fallback} 秒${from}`);
32
+ /** 只留最后 OUTPUT_LIMIT 字节 */
33
+ class TailBuffer {
34
+ kept = Buffer.alloc(0);
35
+ total = 0;
36
+ add(data) {
37
+ this.total += data.length;
38
+ this.kept = Buffer.concat([this.kept, data]).subarray(-OUTPUT_LIMIT);
39
+ }
40
+ get text() {
41
+ return this.kept.toString("utf8");
42
+ }
43
+ get droppedBytes() {
44
+ return this.total - this.kept.length;
45
+ }
46
+ }
47
+ /** exec、run 共用:收下输出,到时间就取消。超时不抛,返回 timedOut 和已经收到的输出 */
48
+ async function capture(seconds, context, start) {
49
+ const stdout = new TailBuffer();
50
+ const stderr = new TailBuffer();
51
+ const timeout = AbortSignal.timeout(seconds * 1000);
52
+ const captured = () => ({
53
+ stdout: stdout.text,
54
+ stderr: stderr.text,
55
+ droppedBytes: { stdout: stdout.droppedBytes, stderr: stderr.droppedBytes },
56
+ });
57
+ try {
58
+ const { exitCode } = await start(({ stream, data }) => (stream === "stdout" ? stdout : stderr).add(data), AbortSignal.any([context.signal, timeout]));
59
+ return { exitCode, ...captured() };
60
+ }
61
+ catch (error) {
62
+ if (!timeout.aborted)
63
+ throw error;
64
+ return { timedOut: true, ...captured() };
65
+ }
66
+ }
67
+ export const tools = [
68
+ define({
69
+ name: "up",
70
+ title: "启动环境",
71
+ description: "用项目的 compose 文件启动当前 worktree 的开发环境(数据库、对象存储、后端等),不用 Docker,每个 worktree 一套,端口不冲突。等服务都就绪后返回端口和变量。同一个环境的端口固定不变(休眠、唤醒、再次 up、换 profile 都一样),down 之后才释放。已在运行时马上返回。第一次可能要拉镜像、构建镜像,比较慢。测试要一套用完就扔的服务(比如空的数据库)时传 temp,代替 docker run。",
72
+ input: z.strictObject({
73
+ directory,
74
+ files: z.array(z.string()).optional().describe("compose 文件,按顺序合并。不写就自动查找。"),
75
+ profiles: z.array(z.string()).optional().describe("启用的 profile"),
76
+ build: z
77
+ .boolean()
78
+ .optional()
79
+ .describe("写了 build 的服务都重新构建。不传时只在镜像还没有时构建;改了 Dockerfile 或打进镜像的文件后要传 true。"),
80
+ temp: z
81
+ .string()
82
+ .optional()
83
+ .describe("起一个叫这个名字的临时环境,不动开发环境:有自己的端口和数据,可以同时起好几个。名字小写字母或数字开头,只用小写字母、数字和 . _ -,不超过 40 个字符。用完调 down 传同一个名字,数据直接删掉;它不休眠,闲置了自己销毁。"),
84
+ }),
85
+ annotations: WRITE,
86
+ async run(args, context) {
87
+ return context.client.up({
88
+ ...(await target(args, context)),
89
+ files: args.files,
90
+ profiles: args.profiles,
91
+ environment: processEnvironment(),
92
+ build: args.build,
93
+ });
94
+ },
95
+ }),
96
+ define({
97
+ name: "run",
98
+ title: "单独跑一次某个服务",
99
+ description: `临时起一个环境,在前台跑 compose 里的一个服务(测试、检查、一次性脚本),跑完整个销毁,代替 docker compose run 和 docker run --rm。它依赖的服务先起好;和开发环境互不影响,数据不保留。stdout、stderr 各留最后 ${OUTPUT_LIMIT / 1024}KB;不支持交互。服务失败看 exitCode。`,
100
+ input: z.strictObject({
101
+ directory,
102
+ service: z.string().describe("服务名"),
103
+ command: z
104
+ .array(z.string())
105
+ .min(1)
106
+ .optional()
107
+ .describe('命令和参数,换掉服务的 command,比如 ["npm", "test"];不写就用 compose 里写的'),
108
+ files: z.array(z.string()).optional().describe("compose 文件,按顺序合并。不写就自动查找。"),
109
+ profiles: z.array(z.string()).optional().describe("启用的 profile。要跑的服务自己的 profile 不用写。"),
110
+ build: z.boolean().optional().describe("写了 build 的服务都重新构建。不传时只在镜像还没有时构建。"),
111
+ noDeps: z.boolean().optional().describe("不起它 depends_on 的服务"),
112
+ egress: EgressMode.optional().describe("出网模式,默认 open;none 是一起来就只能访问这个环境自己的服务"),
113
+ stdin: z.string().optional().describe("给服务的标准输入"),
114
+ timeoutSeconds: timeoutSeconds(DEFAULT_RUN_SECONDS, ",从起环境算起"),
115
+ }),
116
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
117
+ async run(args, context) {
118
+ const { cwd } = await target(args, context);
119
+ return capture(args.timeoutSeconds ?? DEFAULT_RUN_SECONDS, context, (onOutput, signal) => context.client.run({
120
+ cwd,
121
+ files: args.files,
122
+ profiles: args.profiles,
123
+ environment: processEnvironment(),
124
+ build: args.build,
125
+ egress: args.egress,
126
+ service: args.service,
127
+ command: args.command,
128
+ noDeps: args.noDeps,
129
+ stdin: args.stdin,
130
+ onOutput,
131
+ signal,
132
+ }));
133
+ },
134
+ }),
135
+ define({
136
+ name: "env",
137
+ title: "查看环境",
138
+ description: "当前 worktree 环境的状态、端口和变量(比如 NAPBOX_API_3100=127.0.0.1:41217)。不唤醒环境。代码连服务时用这里的地址。",
139
+ input: z.strictObject({ directory, temp }),
140
+ annotations: READ,
141
+ async run(args, context) {
142
+ return context.client.environment(await target(args, context));
143
+ },
144
+ }),
145
+ define({
146
+ name: "status",
147
+ title: "所有环境",
148
+ description: "napboxd 的版本和这台机器上所有环境的状态、端口。",
149
+ input: z.strictObject({}),
150
+ annotations: READ,
151
+ run: (_args, context) => context.client.status(),
152
+ }),
153
+ define({
154
+ name: "logs",
155
+ title: "查看日志",
156
+ description: "服务的日志,默认最后 200 行。不唤醒环境。每次启动前有一行分隔。",
157
+ input: z.strictObject({
158
+ directory,
159
+ temp,
160
+ service: z.string().describe("服务名"),
161
+ tail: z.int().min(0).optional().describe("最后几行,默认 200"),
162
+ }),
163
+ annotations: READ,
164
+ async run(args, context) {
165
+ const output = new TailBuffer();
166
+ await context.client.logs({
167
+ target: await target(args, context),
168
+ service: args.service,
169
+ tail: args.tail ?? 200,
170
+ onOutput: ({ data }) => output.add(data),
171
+ signal: context.signal,
172
+ });
173
+ return { logs: output.text, droppedBytes: output.droppedBytes };
174
+ },
175
+ }),
176
+ define({
177
+ name: "exec",
178
+ title: "在服务里执行命令",
179
+ description: `在服务的容器里执行命令,比如跑数据库迁移或 psql。休眠时先唤醒。沿用容器的环境变量、工作目录和用户。stdout、stderr 各留最后 ${OUTPUT_LIMIT / 1024}KB;不支持交互。命令失败看 exitCode。`,
180
+ input: z.strictObject({
181
+ directory,
182
+ temp,
183
+ service: z.string().describe("服务名"),
184
+ command: z.array(z.string()).min(1).describe('命令和参数,比如 ["sh", "-c", "ls /app | wc -l"]'),
185
+ stdin: z.string().optional().describe("给命令的标准输入"),
186
+ timeoutSeconds: timeoutSeconds(DEFAULT_EXEC_SECONDS, ""),
187
+ }),
188
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false },
189
+ async run(args, context) {
190
+ const environmentTarget = await target(args, context);
191
+ return capture(args.timeoutSeconds ?? DEFAULT_EXEC_SECONDS, context, (onOutput, signal) => context.client.exec({
192
+ target: environmentTarget,
193
+ service: args.service,
194
+ command: args.command,
195
+ stdin: args.stdin,
196
+ onOutput,
197
+ signal,
198
+ }));
199
+ },
200
+ }),
201
+ define({
202
+ name: "reset",
203
+ title: "清空数据卷",
204
+ description: "把一个数据卷恢复成空的,再重新起环境(一次性服务会重跑)。其他数据卷不动。旧数据保留 7 天。",
205
+ input: z.strictObject({ directory, volume: z.string().describe("数据卷名") }),
206
+ annotations: DESTROY,
207
+ async run(args, context) {
208
+ return context.client.reset(await target(args, context), args.volume);
209
+ },
210
+ }),
211
+ define({
212
+ name: "sleep",
213
+ title: "休眠环境",
214
+ description: "让当前 worktree 的环境休眠,释放内存,断开现有连接。一连接就会自动唤醒。闲置 30 分钟也会自动休眠。",
215
+ input: z.strictObject({ directory }),
216
+ annotations: WRITE,
217
+ async run(args, context) {
218
+ return context.client.sleep(await target(args, context));
219
+ },
220
+ }),
221
+ define({
222
+ name: "down",
223
+ title: "回收环境",
224
+ description: "回收当前 worktree 的环境:停掉服务、释放端口,数据保留 7 天后删除。只是不用了不需要回收,它会自己休眠。传 temp 是销毁那个临时环境,它的数据直接删掉。",
225
+ input: z.strictObject({ directory, temp }),
226
+ annotations: DESTROY,
227
+ async run(args, context) {
228
+ return context.client.down(await target(args, context));
229
+ },
230
+ }),
231
+ define({
232
+ name: "egress",
233
+ title: "切换出网模式",
234
+ description: "open:服务能上外网;none:只能访问环境自己的服务。都访问不到宿主机和局域网。在运行的马上生效。",
235
+ input: z.strictObject({ directory, temp, mode: EgressMode }),
236
+ annotations: WRITE,
237
+ async run(args, context) {
238
+ return context.client.egress(await target(args, context), args.mode);
239
+ },
240
+ }),
241
+ define({
242
+ name: "image_import",
243
+ title: "从 Docker 导入镜像",
244
+ description: "把本机 Docker 里的镜像导入 napbox,给 Docker Hub 上已经拉不到的旧版本用。只执行 docker save,不碰 Docker 的容器和数据。",
245
+ input: z.strictObject({ reference: z.string().describe("镜像名") }),
246
+ annotations: WRITE,
247
+ run: (args, context) => context.client.importImage(args.reference),
248
+ }),
249
+ define({
250
+ name: "setup",
251
+ title: "首次准备",
252
+ description: "第一次使用前的准备,或工具报 daemon_unavailable 时调用:下载约 700MB 的内核包,把后台程序 napboxd 注册成登录项并启动。已经准备好的不重做。有环境在运行时不会换版本,除非 restart 为 true。",
253
+ input: z.strictObject({
254
+ restart: z.boolean().optional().describe("有环境在运行时也更新后台程序,运行中的环境会停止,连接会断开"),
255
+ }),
256
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
257
+ run: (args) => setup({ restart: args.restart ?? false, log: () => { } }),
258
+ }),
259
+ ];
260
+ export function listTools() {
261
+ return tools.map(({ name, title, description, input, annotations }) => {
262
+ const { $schema: _, ...inputSchema } = z.toJSONSchema(input);
263
+ return {
264
+ name,
265
+ title,
266
+ description,
267
+ inputSchema,
268
+ annotations: { title, ...annotations, openWorldHint: false },
269
+ };
270
+ });
271
+ }
272
+ /** 工具的错误都放进结果(isError),不当 JSON-RPC 错误:agent 能看到原因 */
273
+ export async function callTool(name, rawArguments, context) {
274
+ const tool = tools.find((candidate) => candidate.name === name);
275
+ if (!tool)
276
+ return failure("usage_error", `没有工具 ${name}`);
277
+ const args = tool.input.safeParse(rawArguments ?? {});
278
+ if (!args.success)
279
+ return failure("usage_error", z.prettifyError(args.error));
280
+ try {
281
+ const value = await tool.run(args.data, context);
282
+ const timedOut = value.timedOut === true;
283
+ return { value, isError: timedOut };
284
+ }
285
+ catch (error) {
286
+ if (!(error instanceof NapboxError))
287
+ throw error;
288
+ return failure(error.code, error.code === "daemon_unavailable"
289
+ ? `${error.message} 调用 setup 工具准备(要用户同意:下载约 700MB、注册登录项)。`
290
+ : error.message);
291
+ }
292
+ }
293
+ function failure(code, message) {
294
+ return { value: { error: { code, message } }, isError: true };
295
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+ // npm 包的 napbox 命令。setup、mcp、hook 是 TypeScript 写的;其余都交给 Swift 写的命令行,
3
+ // 用 execve 直接换成它:标准输入输出、信号和退出码都不用转发。
4
+ import path from "node:path";
5
+ import { NapboxError } from "./client.js";
6
+ import { runHook } from "./agent/hooks.js";
7
+ import { nativeDirectory } from "./agent/locations.js";
8
+ import { serveMcp } from "./agent/mcp.js";
9
+ import { sweepCachedVersions } from "./agent/cache.js";
10
+ import { registerMcp, setup } from "./agent/setup.js";
11
+ const [command, ...rest] = process.argv.slice(2);
12
+ async function runSetup(args) {
13
+ const restart = args.includes("--restart");
14
+ const others = args.filter((arg) => arg !== "--restart");
15
+ const [agent, extra] = others;
16
+ if (extra !== undefined || (agent !== undefined && agent !== "claude" && agent !== "codex")) {
17
+ throw new NapboxError("usage_error", "用法:napbox setup [claude|codex] [--restart]");
18
+ }
19
+ const result = await setup({ restart, log: (line) => process.stderr.write(`${line}\n`) });
20
+ const mcp = agent ? await registerMcp(agent) : undefined;
21
+ process.stdout.write(`${JSON.stringify({ ...result, mcp })}\n`);
22
+ }
23
+ async function readStdin() {
24
+ let text = "";
25
+ process.stdin.setEncoding("utf8");
26
+ for await (const chunk of process.stdin)
27
+ text += chunk;
28
+ return text;
29
+ }
30
+ try {
31
+ switch (command) {
32
+ case "setup":
33
+ await runSetup(rest);
34
+ break;
35
+ case "mcp":
36
+ // 只是清理,失败了下次再清,不影响服务
37
+ void sweepCachedVersions().catch(() => { });
38
+ await serveMcp();
39
+ process.exit(0);
40
+ break;
41
+ case "hook": {
42
+ const output = await runHook(JSON.parse(await readStdin()));
43
+ if (output)
44
+ process.stdout.write(`${JSON.stringify(output)}\n`);
45
+ break;
46
+ }
47
+ default: {
48
+ const native = path.join(nativeDirectory(), "napbox");
49
+ // engines 要求的 Node 都有它,类型里写成可选是因为 Windows 没有
50
+ process.execve(native, [native, ...process.argv.slice(2)]);
51
+ }
52
+ }
53
+ }
54
+ catch (error) {
55
+ // 和命令行一样的错误格式(DESIGN.md 第 9 节)
56
+ const { code, message } = error instanceof NapboxError ? error : { code: "internal_error", message: String(error) };
57
+ process.stdout.write(`${JSON.stringify({ error: { code, message } })}\n`);
58
+ process.exit(1);
59
+ }