@tcb-sandbox/cli 0.3.7 → 0.3.8

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.
package/README.md CHANGED
@@ -1,67 +1,87 @@
1
1
  # @tcb-sandbox/cli
2
2
 
3
- 面向 **TRW(tcb-remote-workspace)** 的命令行工具:**一种安装,两种用法**。
4
-
5
- > **目录位置:** `clis/sandbox-cli/`
6
-
7
- | 用法 | 说明 |
8
- |------|------|
9
- | **本机起服务** | `tcb-sandbox serve`(或 `local`)在本地跑内置 TRW,默认 `http://127.0.0.1:9000`。 |
10
- | **只当客户端** | 其它子命令都是 **HTTP 薄客户端**,用 `--endpoint` / `--session-id` 连已有 TRW(远端或本机均可)。 |
11
-
12
- **文档(建议按顺序看):** [快速上手](./docs/quick-start.md) → [薄客户端](./docs/thin-client.md) → [本地 serve](./docs/local-mode.md) → [文档索引](./docs/README.md) → [变更记录](./CHANGELOG.md)
13
-
14
- ---
3
+ TRW 的官方命令行工具:**一种安装,两种用法**。
15
4
 
16
5
  ## 安装
17
6
 
18
- 任选一种全局安装方式:
19
-
20
7
  ```bash
21
8
  npm install -g @tcb-sandbox/cli
22
- ```
23
-
24
- ```bash
9
+ # 或
25
10
  pnpm add -g @tcb-sandbox/cli
26
11
  ```
27
12
 
28
- 安装后可用命令名:**`tcb-sandbox`**(主名)、**`tcb-sandbox`**(别名)。
13
+ ## 两种用法
29
14
 
30
- ---
15
+ | 用法 | 命令 | 说明 |
16
+ |------|------|------|
17
+ | **本地起服务** | `tcb-sandbox serve` | 启动内置 TRW,默认 `http://127.0.0.1:9000` |
18
+ | **只当客户端** | `tcb-sandbox <tool>` | 调用已有 TRW(远端或本地) |
31
19
 
32
- ## 最短路径
20
+ ## 快速开始
33
21
 
34
22
  ```bash
23
+ # 本地模式
35
24
  tcb-sandbox serve
25
+
36
26
  # 另开终端
37
27
  tcb-sandbox --endpoint http://127.0.0.1:9000 --session-id my-session health
28
+ tcb-sandbox --session-id my-session write --path hello.txt --content "world"
29
+ tcb-sandbox --session-id my-session read --path hello.txt
38
30
  ```
39
31
 
40
- serve 之后 TRW **六大门面**(HTTP API、`/mcp`、`/mcp_user_define`、CLI、Skills、E2B)及探活/文档/预览,见 **[docs/quick-start.md](./docs/quick-start.md)**。
32
+ ## 环境变量
41
33
 
42
- ---
34
+ ```bash
35
+ export TRW_ENDPOINT=https://your-gateway.com
36
+ export TRW_SESSION_ID=your-session-id
43
37
 
44
- ## 使用前提(读一遍即可)
38
+ # 然后可直接使用
39
+ tcb-sandbox health
40
+ tcb-sandbox bash --command "ls -la"
41
+ ```
45
42
 
46
- - **TRW 没有单独发 npm 包**;本地模式用的是本包自带的 **`dist/trw-embedded.js`**(esbuild 打好的服务)。
47
- - 需要 **node-pty**、**@mongodb-js/zstd**:已写在包的 `dependencies` 里,给内置 bundle 动态加载用。
48
- - 想用**自己编出来的 TRW** 替代内置文件:设环境变量 **`TCB_SANDBOX_TRW_ENTRY`** 指向你的 `dist/index.js` 等入口(见 [local-mode.md](./docs/local-mode.md))。
49
- - 公开展示的相关仓库还有 **@tcb-sandbox/e2b-sandbox-adapter** 等;**npm 上搜不到名叫 TRW 的包**是正常的。
50
- - 连远端 TRW 时**不会**在 CLI 里写死密钥;工具列表优先走 **`GET /api/docs`**(离线时 CLI 可能用内置文档快照)。
43
+ ## 命令列表
51
44
 
52
- ---
45
+ | 命令 | 说明 |
46
+ |------|------|
47
+ | `serve` / `local` | 启动本地 TRW |
48
+ | `health` | 健康检查 |
49
+ | `read` / `write` / `edit` | 文件操作 |
50
+ | `bash` | 执行命令 |
51
+ | `grep` / `glob` / `ls` | 搜索 |
52
+ | `batch` | 批量执行 |
53
+ | `git-push` | Git 提交 |
54
+ | `add-mcp-servers` / `remove-mcp-servers` / `list-mcp-servers` | MCP 管理 |
55
+ | `mcporter` | mcporter CLI |
56
+ | `capability-*` | 能力管理(6个) |
57
+ | `secrets` | 密钥管理 |
58
+ | `files` | 文件传输 |
59
+ | `preview` | 预览服务 |
60
+ | `pty` | PTY 终端 |
61
+ | `snapshot` | 快照管理 |
62
+
63
+ ## 本地模式详情
53
64
 
54
- ## 仓库内维护 / 发布
65
+ ```bash
66
+ # 默认端口 9000
67
+ tcb-sandbox serve
55
68
 
56
- 以下在 **tcb-remote-workspace** monorepo **根目录**执行(发布前请再跑一遍 test):
69
+ # 指定端口
70
+ tcb-sandbox serve --port 8080
57
71
 
58
- ```bash
59
- pnpm --filter @tcb-sandbox/cli run build
60
- pnpm --filter @tcb-sandbox/cli run test
61
- pnpm --filter @tcb-sandbox/cli run pack:dry-run
62
- pnpm --filter @tcb-sandbox/cli run pack:safety # 拒绝把 .env、src、tests、*.map 打进包
63
- pnpm --filter @tcb-sandbox/cli version patch # 或 minor / major
64
- pnpm --filter @tcb-sandbox/cli publish
72
+ # 使用自定义 TRW 入口(开发用)
73
+ export TCB_SANDBOX_TRW_ENTRY=/path/to/your/dist/index.js
74
+ tcb-sandbox serve
65
75
  ```
66
76
 
67
- 可选:包内 **live 测试** 读 `cli/.env`(参考 `.env.example`)。
77
+ ## 限制
78
+
79
+ - 除 `serve` 外所有命令都是纯 HTTP 客户端
80
+ - 需要有效的 `endpoint` 和 `session-id`
81
+ - 文件传输受 6MB 限制
82
+
83
+ ## 更多文档
84
+
85
+ - `docs/quick-start.md` — 快速上手
86
+ - `docs/local-mode.md` — 本地模式详解
87
+ - `docs/thin-client.md` — 薄客户端说明
@@ -87,10 +87,7 @@ export const BUNDLED_API_DOCS = {
87
87
  additionalProperties: false,
88
88
  properties: {
89
89
  serverNames: {
90
- oneOf: [
91
- { type: "array", items: { type: "string" } },
92
- { type: "string" },
93
- ],
90
+ oneOf: [{ type: "array", items: { type: "string" } }, { type: "string" }],
94
91
  },
95
92
  serverName: { type: "string" },
96
93
  name: { type: "string" },
@@ -161,11 +158,17 @@ export const BUNDLED_API_DOCS = {
161
158
  type: "object",
162
159
  required: ["command"],
163
160
  properties: {
164
- command: { type: "string", description: "Mcporter subcommand (without 'mcporter' prefix)" },
161
+ command: {
162
+ type: "string",
163
+ description: "Mcporter subcommand (without 'mcporter' prefix)",
164
+ },
165
165
  timeout: { type: "integer", description: "Timeout in ms (default 300000)" },
166
166
  },
167
167
  },
168
- limits: { default_timeout_ms: 300_000, allowed_subcommands: ["help", "list", "call", "config"] },
168
+ limits: {
169
+ default_timeout_ms: 300_000,
170
+ allowed_subcommands: ["help", "list", "call", "config"],
171
+ },
169
172
  notes: ["Bundled fallback docs snapshot."],
170
173
  },
171
174
  {
package/dist/cli.js CHANGED
@@ -2,10 +2,10 @@
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import process from "node:process";
5
+ import { TcbSandboxClient } from "@tcb-sandbox/sdk-js";
5
6
  import { Command } from "commander";
6
7
  import { BUNDLED_API_DOCS } from "./bundled-docs.js";
7
8
  import { defaultWorkspaceRoot, runServe } from "./serve.js";
8
- import { TcbSandboxClient } from "@tcb-sandbox/sdk-js";
9
9
  const DEFAULT_TIMEOUT = 600_000;
10
10
  const DEFAULT_ACCEPT_HEADER = "application/problem+json, application/json;q=0.9, text/markdown;q=0.8, */*;q=0.1";
11
11
  const LOG_PRIORITIES = {
@@ -130,24 +130,6 @@ function logMessage(ctx, level, message) {
130
130
  }
131
131
  process.stderr.write(`[${level.toUpperCase()}] ${message}\n`);
132
132
  }
133
- function parseKeyValue(input) {
134
- const idx = input.indexOf("=");
135
- if (idx <= 0)
136
- throw new Error(`Invalid --param "${input}". Expected key=value.`);
137
- const key = input.slice(0, idx).trim();
138
- const raw = input.slice(idx + 1);
139
- if (!key)
140
- throw new Error(`Invalid --param "${input}". Key is empty.`);
141
- if (raw === "true")
142
- return [key, true];
143
- if (raw === "false")
144
- return [key, false];
145
- if (raw === "null")
146
- return [key, null];
147
- if (raw !== "" && /^-?\d+(\.\d+)?$/.test(raw))
148
- return [key, Number(raw)];
149
- return [key, raw];
150
- }
151
133
  function normalizeBashModeValue(input) {
152
134
  if (typeof input !== "string") {
153
135
  throw new Error('Invalid bash mode. Supported values: "execute", "dry_run", "dryrun".');
@@ -203,112 +185,6 @@ function createSdkClientFromContext(ctx) {
203
185
  timeoutInSeconds: ctx.timeout > 0 ? Math.floor(ctx.timeout / 1000) : DEFAULT_TIMEOUT / 1000,
204
186
  });
205
187
  }
206
- async function request(ctx, method, route, options) {
207
- const controller = new AbortController();
208
- const startedAt = Date.now();
209
- const timer = setTimeout(() => controller.abort(), ctx.timeout);
210
- const headers = { ...ctx.headers };
211
- if (options?.accept) {
212
- headers.Accept = options.accept;
213
- }
214
- else if (!Object.keys(headers).some((key) => key.toLowerCase() === "accept")) {
215
- headers.Accept = DEFAULT_ACCEPT_HEADER;
216
- }
217
- let body;
218
- if (options?.json !== undefined) {
219
- headers["Content-Type"] = "application/json";
220
- body = JSON.stringify(options.json);
221
- }
222
- else if (options?.body) {
223
- headers["Content-Type"] = options.contentType ?? "application/octet-stream";
224
- body = new Uint8Array(options.body);
225
- }
226
- logMessage(ctx, "info", `${method} ${route}`);
227
- if (shouldLog(ctx, "debug")) {
228
- const maskedHeaders = {};
229
- for (const [k, v] of Object.entries(headers)) {
230
- maskedHeaders[k] = maskHeaderValue(k, v);
231
- }
232
- logMessage(ctx, "debug", `request headers: ${JSON.stringify(maskedHeaders)}`);
233
- if (options?.json !== undefined) {
234
- logMessage(ctx, "debug", `request json: ${JSON.stringify(redactJsonForDebug(options.json))}`);
235
- }
236
- else if (options?.body) {
237
- logMessage(ctx, "debug", `request body bytes: ${options.body.byteLength}`);
238
- }
239
- }
240
- try {
241
- const res = await fetch(`${ctx.endpoint}${route}`, {
242
- method,
243
- headers,
244
- body,
245
- signal: controller.signal,
246
- });
247
- const elapsed = Date.now() - startedAt;
248
- logMessage(ctx, "info", `${method} ${route} -> ${res.status} (${elapsed}ms)`);
249
- return res;
250
- }
251
- catch (err) {
252
- const elapsed = Date.now() - startedAt;
253
- logMessage(ctx, "error", `${method} ${route} failed after ${elapsed}ms: ${err instanceof Error ? err.message : String(err)}`);
254
- throw err;
255
- }
256
- finally {
257
- clearTimeout(timer);
258
- }
259
- }
260
- function safeParseJson(input) {
261
- try {
262
- return JSON.parse(input);
263
- }
264
- catch {
265
- return undefined;
266
- }
267
- }
268
- function extractErrorMessage(payload) {
269
- if (!payload || typeof payload !== "object")
270
- return undefined;
271
- const maybe = payload;
272
- if (typeof maybe.detail === "string" && maybe.detail.trim())
273
- return maybe.detail;
274
- if (typeof maybe.error === "string" && maybe.error.trim())
275
- return maybe.error;
276
- if (typeof maybe.message === "string" && maybe.message.trim())
277
- return maybe.message;
278
- if (typeof maybe.title === "string" && maybe.title.trim())
279
- return maybe.title;
280
- return undefined;
281
- }
282
- async function throwHttpError(res, requestLabel) {
283
- const text = await res.text();
284
- const payload = safeParseJson(text);
285
- const contentType = res.headers.get("content-type") ?? "";
286
- const message = extractErrorMessage(payload);
287
- if (payload && typeof payload === "object" && contentType.includes("application/problem+json")) {
288
- const problem = payload;
289
- const retryHint = problem.retryable && typeof problem.retry_after === "number"
290
- ? `; retry after ${problem.retry_after}s`
291
- : problem.retryable
292
- ? "; retryable"
293
- : "";
294
- const codeHint = problem.error_code ? ` [${problem.error_code}]` : "";
295
- throw new Error(`${requestLabel} failed (${res.status})${codeHint}: ${message ?? "Unknown problem response"}${retryHint}`);
296
- }
297
- throw new Error(`${requestLabel} failed (${res.status}): ${message ?? (text || "Unknown error response")}`);
298
- }
299
- async function readJsonResponse(res, requestLabel) {
300
- if (!res.ok) {
301
- return throwHttpError(res, requestLabel);
302
- }
303
- let body;
304
- try {
305
- body = await res.json();
306
- }
307
- catch (err) {
308
- throw new Error(`${requestLabel} returned non-JSON response: ${err instanceof Error ? err.message : String(err)}`);
309
- }
310
- return body;
311
- }
312
188
  function printJson(data) {
313
189
  process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
314
190
  }
@@ -316,19 +192,6 @@ function printPretty(title, data) {
316
192
  process.stdout.write(`${title}\n`);
317
193
  printJson(data);
318
194
  }
319
- async function readApiDocs(ctx) {
320
- const res = await request(ctx, "GET", "/api/docs");
321
- return readJsonResponse(res, "GET /api/docs");
322
- }
323
- async function getDocs(ctx) {
324
- try {
325
- return await readApiDocs(ctx);
326
- }
327
- catch {
328
- logMessage(ctx, "warn", "GET /api/docs unavailable, using bundled fallback docs");
329
- return BUNDLED_API_DOCS;
330
- }
331
- }
332
195
  async function readFromStdin() {
333
196
  const chunks = [];
334
197
  for await (const chunk of process.stdin) {
@@ -336,35 +199,6 @@ async function readFromStdin() {
336
199
  }
337
200
  return Buffer.concat(chunks).toString("utf8");
338
201
  }
339
- function renderSchema(schema) {
340
- const properties = (schema.properties ?? {});
341
- const required = new Set((schema.required ?? []));
342
- const lines = [];
343
- const keys = Object.keys(properties);
344
- if (keys.length === 0) {
345
- lines.push(" (no fields)");
346
- return lines;
347
- }
348
- for (const key of keys) {
349
- const spec = properties[key];
350
- const type = spec.type ?? "unknown";
351
- const desc = spec.description ?? "";
352
- const mark = required.has(key) ? "required" : "optional";
353
- const constraint = typeof spec.minimum === "number"
354
- ? `, min=${spec.minimum}`
355
- : typeof spec.pattern === "string"
356
- ? `, pattern=${spec.pattern}`
357
- : "";
358
- lines.push(` - ${key}: ${type} (${mark}${constraint})${desc ? ` — ${desc}` : ""}`);
359
- }
360
- return lines;
361
- }
362
- function renderRecord(record) {
363
- const entries = Object.entries(record);
364
- if (entries.length === 0)
365
- return [" (none)"];
366
- return entries.map(([k, v]) => ` - ${k}: ${typeof v === "string" ? v : JSON.stringify(v)}`);
367
- }
368
202
  async function getPreviewUrl(ctx, port) {
369
203
  ensureSessionId(ctx);
370
204
  const client = createSdkClientFromContext(ctx);
@@ -382,12 +216,6 @@ async function getPreviewUrl(ctx, port) {
382
216
  }
383
217
  return previewUrl;
384
218
  }
385
- async function callTopLevelTool(ctx, tool, payload, label) {
386
- const res = await request(ctx, "POST", `/api/tools/${encodeURIComponent(tool)}`, {
387
- json: payload,
388
- });
389
- return readJsonResponse(res, label);
390
- }
391
219
  async function openUrl(url) {
392
220
  const { spawn } = await import("node:child_process");
393
221
  const platform = process.platform;
@@ -569,7 +397,9 @@ Error Handling
569
397
  command,
570
398
  timeout: options.timeout,
571
399
  cwd: options.cwd,
572
- mode: options.mode ? normalizeBashModeValue(options.mode) : undefined,
400
+ mode: options.mode
401
+ ? normalizeBashModeValue(options.mode)
402
+ : undefined,
573
403
  };
574
404
  const response = await client.execution.bash(bashRequest);
575
405
  const result = "data" in response ? response.data : response;
@@ -657,7 +487,8 @@ Error Handling
657
487
  const payload = options.data ? parseJsonObject(options.data, "--data") : { tool_calls: [] };
658
488
  logMessage(ctx, "info", "POST /api/tools/batch");
659
489
  const batchRequest = {
660
- tool_calls: payload.tool_calls || [],
490
+ tool_calls: payload.tool_calls ||
491
+ [],
661
492
  };
662
493
  const response = await client.execution.batch(batchRequest);
663
494
  const result = "data" in response ? response.data : response;
@@ -787,7 +618,9 @@ Error Handling
787
618
  ensureSessionId(ctx);
788
619
  const client = createSdkClientFromContext(ctx);
789
620
  logMessage(ctx, "info", "POST /api/tools/capability_list");
790
- const response = await client.capabilitySystem.capabilityList({ revealPaths: options.revealPaths });
621
+ const response = await client.capabilitySystem.capabilityList({
622
+ revealPaths: options.revealPaths,
623
+ });
791
624
  const result = "data" in response ? response.data : response;
792
625
  if (ctx.output === "json")
793
626
  return printJson(result);
@@ -0,0 +1,86 @@
1
+ import { execFileSync, spawn } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ export function isMacOSSandboxAvailable() {
6
+ if (process.platform !== "darwin")
7
+ return false;
8
+ try {
9
+ execFileSync("/usr/bin/sandbox-exec", ["-p", "(version 1)(allow default)", "/usr/bin/true"], {
10
+ timeout: 5000,
11
+ stdio: "ignore",
12
+ });
13
+ return true;
14
+ }
15
+ catch {
16
+ return false;
17
+ }
18
+ }
19
+ export function generateProfile(writablePaths) {
20
+ const lines = [
21
+ ";; Auto-generated by @tcb-sandbox/cli — macOS sandbox profile",
22
+ ";; Restricts file-write to workspace + system temp directories.",
23
+ "(version 1)",
24
+ "",
25
+ ";; Allow everything by default (read, network, process, IPC, …)",
26
+ "(allow default)",
27
+ "",
28
+ ";; Deny all file writes globally",
29
+ "(deny file-write*)",
30
+ "",
31
+ ";; Re-allow writes to specific paths",
32
+ ];
33
+ for (const p of writablePaths) {
34
+ const abs = path.resolve(p);
35
+ lines.push(`(allow file-write* (subpath "${abs}"))`);
36
+ }
37
+ lines.push("");
38
+ return lines.join("\n");
39
+ }
40
+ export function resolveWritablePaths(workspaceRoot) {
41
+ const paths = [
42
+ path.resolve(workspaceRoot),
43
+ "/tmp",
44
+ "/private/tmp",
45
+ "/dev",
46
+ "/private/var/folders",
47
+ ];
48
+ const tmpDir = os.tmpdir();
49
+ if (tmpDir && !paths.some((p) => tmpDir.startsWith(p))) {
50
+ paths.push(tmpDir);
51
+ }
52
+ return paths;
53
+ }
54
+ export function writeProfileFile(workspaceRoot, profile) {
55
+ const sandboxDir = path.join(path.dirname(path.resolve(workspaceRoot)), "sandbox");
56
+ fs.mkdirSync(sandboxDir, { recursive: true });
57
+ const profilePath = path.join(sandboxDir, "serve.sb");
58
+ fs.writeFileSync(profilePath, profile, "utf8");
59
+ return profilePath;
60
+ }
61
+ export function spawnSandboxed(trwMain, env, workspaceRoot) {
62
+ const available = isMacOSSandboxAvailable();
63
+ if (!available) {
64
+ const child = spawn(process.execPath, [trwMain], {
65
+ stdio: "inherit",
66
+ env,
67
+ });
68
+ return {
69
+ enabled: false,
70
+ status: "unavailable (not macOS or sandbox-exec missing)",
71
+ child,
72
+ };
73
+ }
74
+ const writablePaths = resolveWritablePaths(workspaceRoot);
75
+ const profile = generateProfile(writablePaths);
76
+ const profilePath = writeProfileFile(workspaceRoot, profile);
77
+ const child = spawn("/usr/bin/sandbox-exec", ["-f", profilePath, process.execPath, trwMain], {
78
+ stdio: "inherit",
79
+ env,
80
+ });
81
+ return {
82
+ enabled: true,
83
+ status: `active (writes restricted to ${workspaceRoot}, /tmp)`,
84
+ child,
85
+ };
86
+ }