@tcb-sandbox/cli 0.3.7
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/CHANGELOG.md +19 -0
- package/README.md +67 -0
- package/dist/bundled-docs.d.ts +769 -0
- package/dist/bundled-docs.js +528 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +1234 -0
- package/dist/sdk-client.js +50 -0
- package/dist/serve.d.ts +13 -0
- package/dist/serve.js +197 -0
- package/dist/trw-embedded.js +2719 -0
- package/dist/trw-version.json +1 -0
- package/docs/README.md +13 -0
- package/docs/local-mode.md +76 -0
- package/docs/quick-start.md +82 -0
- package/docs/thin-client.md +100 -0
- package/package.json +49 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1234 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
import { BUNDLED_API_DOCS } from "./bundled-docs.js";
|
|
7
|
+
import { defaultWorkspaceRoot, runServe } from "./serve.js";
|
|
8
|
+
import { TcbSandboxClient } from "@tcb-sandbox/sdk-js";
|
|
9
|
+
const DEFAULT_TIMEOUT = 600_000;
|
|
10
|
+
const DEFAULT_ACCEPT_HEADER = "application/problem+json, application/json;q=0.9, text/markdown;q=0.8, */*;q=0.1";
|
|
11
|
+
const LOG_PRIORITIES = {
|
|
12
|
+
debug: 10,
|
|
13
|
+
info: 20,
|
|
14
|
+
warn: 30,
|
|
15
|
+
error: 40,
|
|
16
|
+
};
|
|
17
|
+
const DEFAULT_LOG_LEVEL = "warn";
|
|
18
|
+
function asInt(value) {
|
|
19
|
+
const parsed = Number.parseInt(value, 10);
|
|
20
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
21
|
+
throw new Error(`Invalid integer value: ${value}`);
|
|
22
|
+
}
|
|
23
|
+
return parsed;
|
|
24
|
+
}
|
|
25
|
+
function asNonNegativeInt(value) {
|
|
26
|
+
const parsed = Number.parseInt(value, 10);
|
|
27
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
28
|
+
throw new Error(`Invalid non-negative integer value: ${value}`);
|
|
29
|
+
}
|
|
30
|
+
return parsed;
|
|
31
|
+
}
|
|
32
|
+
function normalizeEndpoint(endpoint) {
|
|
33
|
+
return endpoint.replace(/\/+$/, "");
|
|
34
|
+
}
|
|
35
|
+
function parseHeaderValue(input) {
|
|
36
|
+
const idx = input.indexOf(":");
|
|
37
|
+
if (idx <= 0) {
|
|
38
|
+
throw new Error(`Invalid --header value "${input}". Expected "Key: Value".`);
|
|
39
|
+
}
|
|
40
|
+
const key = input.slice(0, idx).trim();
|
|
41
|
+
const value = input.slice(idx + 1).trim();
|
|
42
|
+
if (!key)
|
|
43
|
+
throw new Error(`Invalid --header value "${input}". Header key is empty.`);
|
|
44
|
+
return [key, value];
|
|
45
|
+
}
|
|
46
|
+
function parseJsonObject(input, label) {
|
|
47
|
+
let parsed;
|
|
48
|
+
try {
|
|
49
|
+
parsed = JSON.parse(input);
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
throw new Error(`${label} must be valid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
53
|
+
}
|
|
54
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
55
|
+
throw new Error(`${label} must be a JSON object`);
|
|
56
|
+
}
|
|
57
|
+
return parsed;
|
|
58
|
+
}
|
|
59
|
+
function parseJsonRecord(input, label) {
|
|
60
|
+
const obj = parseJsonObject(input, label);
|
|
61
|
+
const out = {};
|
|
62
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
63
|
+
if (typeof v !== "string") {
|
|
64
|
+
throw new Error(`${label} must contain string values only (invalid key: ${k})`);
|
|
65
|
+
}
|
|
66
|
+
out[k] = v;
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
function isLogLevel(value) {
|
|
71
|
+
return value === "debug" || value === "info" || value === "warn" || value === "error";
|
|
72
|
+
}
|
|
73
|
+
function isLogFormat(value) {
|
|
74
|
+
return value === "text" || value === "json";
|
|
75
|
+
}
|
|
76
|
+
function resolveLogLevel(opts) {
|
|
77
|
+
if (opts.debug)
|
|
78
|
+
return "debug";
|
|
79
|
+
if (opts.verbose)
|
|
80
|
+
return "info";
|
|
81
|
+
if (opts.quiet)
|
|
82
|
+
return "error";
|
|
83
|
+
if (opts.logLevel && isLogLevel(opts.logLevel))
|
|
84
|
+
return opts.logLevel;
|
|
85
|
+
return DEFAULT_LOG_LEVEL;
|
|
86
|
+
}
|
|
87
|
+
function resolveLogFormat(opts) {
|
|
88
|
+
if (opts.logFormat && isLogFormat(opts.logFormat))
|
|
89
|
+
return opts.logFormat;
|
|
90
|
+
return "text";
|
|
91
|
+
}
|
|
92
|
+
function shouldLog(ctx, level) {
|
|
93
|
+
return LOG_PRIORITIES[level] >= LOG_PRIORITIES[ctx.logLevel];
|
|
94
|
+
}
|
|
95
|
+
function maskHeaderValue(key, value) {
|
|
96
|
+
const lower = key.toLowerCase();
|
|
97
|
+
if (lower.includes("authorization") ||
|
|
98
|
+
lower.includes("api-key") ||
|
|
99
|
+
lower.includes("token") ||
|
|
100
|
+
lower.includes("cookie") ||
|
|
101
|
+
lower.includes("session")) {
|
|
102
|
+
return "***";
|
|
103
|
+
}
|
|
104
|
+
return value;
|
|
105
|
+
}
|
|
106
|
+
const SENSITIVE_JSON_KEY_RE = /(authorization|api[_-]?key|token|secret|password|cookie|session|value)/i;
|
|
107
|
+
function redactJsonForDebug(input) {
|
|
108
|
+
if (Array.isArray(input)) {
|
|
109
|
+
return input.map((item) => redactJsonForDebug(item));
|
|
110
|
+
}
|
|
111
|
+
if (!input || typeof input !== "object") {
|
|
112
|
+
return input;
|
|
113
|
+
}
|
|
114
|
+
const out = {};
|
|
115
|
+
for (const [key, value] of Object.entries(input)) {
|
|
116
|
+
if (SENSITIVE_JSON_KEY_RE.test(key)) {
|
|
117
|
+
out[key] = "***";
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
out[key] = redactJsonForDebug(value);
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
function logMessage(ctx, level, message) {
|
|
125
|
+
if (!shouldLog(ctx, level))
|
|
126
|
+
return;
|
|
127
|
+
if (ctx.logFormat === "json") {
|
|
128
|
+
process.stderr.write(`${JSON.stringify({ ts: new Date().toISOString(), level, component: "@tcb-sandbox/cli", message })}\n`);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
process.stderr.write(`[${level.toUpperCase()}] ${message}\n`);
|
|
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
|
+
function normalizeBashModeValue(input) {
|
|
152
|
+
if (typeof input !== "string") {
|
|
153
|
+
throw new Error('Invalid bash mode. Supported values: "execute", "dry_run", "dryrun".');
|
|
154
|
+
}
|
|
155
|
+
const value = input.trim().toLowerCase();
|
|
156
|
+
if (value === "execute")
|
|
157
|
+
return "execute";
|
|
158
|
+
if (value === "dry_run" || value === "dryrun")
|
|
159
|
+
return "dry_run";
|
|
160
|
+
throw new Error('Invalid bash mode. Supported values: "execute", "dry_run", "dryrun".');
|
|
161
|
+
}
|
|
162
|
+
function ensureSessionId(ctx) {
|
|
163
|
+
if (!ctx.sessionId) {
|
|
164
|
+
throw new Error("session-id is required for this command. Pass --session-id or set TCB_SANDBOX_SESSION_ID.");
|
|
165
|
+
}
|
|
166
|
+
return ctx.sessionId;
|
|
167
|
+
}
|
|
168
|
+
function buildContext(cmd) {
|
|
169
|
+
const opts = cmd.optsWithGlobals();
|
|
170
|
+
const endpoint = opts.endpoint ?? process.env.TCB_SANDBOX_ENDPOINT;
|
|
171
|
+
if (!endpoint) {
|
|
172
|
+
throw new Error("endpoint is required. Pass --endpoint or set TCB_SANDBOX_ENDPOINT.");
|
|
173
|
+
}
|
|
174
|
+
const sessionId = opts.sessionId ?? process.env.TCB_SANDBOX_SESSION_ID;
|
|
175
|
+
const headers = {};
|
|
176
|
+
const headersFromEnv = process.env.TCB_SANDBOX_HEADERS_JSON;
|
|
177
|
+
if (headersFromEnv) {
|
|
178
|
+
Object.assign(headers, parseJsonRecord(headersFromEnv, "TCB_SANDBOX_HEADERS_JSON"));
|
|
179
|
+
}
|
|
180
|
+
for (const raw of opts.header ?? []) {
|
|
181
|
+
const [key, value] = parseHeaderValue(raw);
|
|
182
|
+
headers[key] = value;
|
|
183
|
+
}
|
|
184
|
+
if (sessionId &&
|
|
185
|
+
!Object.keys(headers).some((k) => k.toLowerCase() === "x-cloudbase-session-id")) {
|
|
186
|
+
headers["X-Cloudbase-Session-Id"] = sessionId;
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
endpoint: normalizeEndpoint(endpoint ?? ""),
|
|
190
|
+
sessionId,
|
|
191
|
+
headers,
|
|
192
|
+
output: opts.output ?? "pretty",
|
|
193
|
+
timeout: opts.timeout ?? DEFAULT_TIMEOUT,
|
|
194
|
+
logLevel: resolveLogLevel(opts),
|
|
195
|
+
logFormat: resolveLogFormat(opts),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function createSdkClientFromContext(ctx) {
|
|
199
|
+
return new TcbSandboxClient({
|
|
200
|
+
baseUrl: ctx.endpoint,
|
|
201
|
+
cloudbaseSessionId: ctx.sessionId,
|
|
202
|
+
headers: ctx.headers,
|
|
203
|
+
timeoutInSeconds: ctx.timeout > 0 ? Math.floor(ctx.timeout / 1000) : DEFAULT_TIMEOUT / 1000,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
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
|
+
function printJson(data) {
|
|
313
|
+
process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
|
|
314
|
+
}
|
|
315
|
+
function printPretty(title, data) {
|
|
316
|
+
process.stdout.write(`${title}\n`);
|
|
317
|
+
printJson(data);
|
|
318
|
+
}
|
|
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
|
+
async function readFromStdin() {
|
|
333
|
+
const chunks = [];
|
|
334
|
+
for await (const chunk of process.stdin) {
|
|
335
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
336
|
+
}
|
|
337
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
338
|
+
}
|
|
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
|
+
async function getPreviewUrl(ctx, port) {
|
|
369
|
+
ensureSessionId(ctx);
|
|
370
|
+
const client = createSdkClientFromContext(ctx);
|
|
371
|
+
const response = await client.preview.url({
|
|
372
|
+
port,
|
|
373
|
+
endpoint: ctx.endpoint,
|
|
374
|
+
includeSessionQuery: Boolean(ctx.sessionId),
|
|
375
|
+
});
|
|
376
|
+
const result = "data" in response ? response.data : response;
|
|
377
|
+
const resultRecord = result;
|
|
378
|
+
const innerResult = resultRecord?.result;
|
|
379
|
+
const previewUrl = typeof innerResult?.previewUrl === "string" ? innerResult.previewUrl : "";
|
|
380
|
+
if (!previewUrl) {
|
|
381
|
+
throw new Error("preview-service.url returned invalid previewUrl");
|
|
382
|
+
}
|
|
383
|
+
return previewUrl;
|
|
384
|
+
}
|
|
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
|
+
async function openUrl(url) {
|
|
392
|
+
const { spawn } = await import("node:child_process");
|
|
393
|
+
const platform = process.platform;
|
|
394
|
+
if (platform === "darwin") {
|
|
395
|
+
await new Promise((resolve, reject) => {
|
|
396
|
+
const child = spawn("open", [url], { stdio: "ignore" });
|
|
397
|
+
child.once("error", reject);
|
|
398
|
+
child.once("close", (code) => code === 0 ? resolve() : reject(new Error(`open exited ${code}`)));
|
|
399
|
+
});
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
if (platform === "win32") {
|
|
403
|
+
await new Promise((resolve, reject) => {
|
|
404
|
+
const child = spawn("cmd", ["/c", "start", "", url], { stdio: "ignore" });
|
|
405
|
+
child.once("error", reject);
|
|
406
|
+
child.once("close", (code) => code === 0 ? resolve() : reject(new Error(`cmd start exited ${code}`)));
|
|
407
|
+
});
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
await new Promise((resolve, reject) => {
|
|
411
|
+
const child = spawn("xdg-open", [url], { stdio: "ignore" });
|
|
412
|
+
child.once("error", reject);
|
|
413
|
+
child.once("close", (code) => code === 0 ? resolve() : reject(new Error(`xdg-open exited ${code}`)));
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
async function main() {
|
|
417
|
+
const program = new Command();
|
|
418
|
+
program
|
|
419
|
+
.name("tcb-sandbox")
|
|
420
|
+
.description("Thin CLI client for tcb-remote-workspace HTTP API.")
|
|
421
|
+
.option("-e, --endpoint <url>", "TRW endpoint URL. Env: TCB_SANDBOX_ENDPOINT")
|
|
422
|
+
.option("-s, --session-id <id>", "Session id. Env: TCB_SANDBOX_SESSION_ID")
|
|
423
|
+
.option("-H, --header <header...>", 'Extra HTTP header, repeatable. Example: -H "X-Trace-Id: 1"')
|
|
424
|
+
.option("-o, --output <format>", "Output format: json | pretty", "pretty")
|
|
425
|
+
.option("--timeout <ms>", "HTTP timeout in milliseconds", asInt, DEFAULT_TIMEOUT)
|
|
426
|
+
.option("--log-level <level>", "debug | info | warn | error", DEFAULT_LOG_LEVEL)
|
|
427
|
+
.option("--log-format <format>", "text | json", "text")
|
|
428
|
+
.option("--verbose", "Enable info logs (stderr)")
|
|
429
|
+
.option("--debug", "Enable debug logs (stderr)")
|
|
430
|
+
.option("--quiet", "Only show errors (stderr)")
|
|
431
|
+
.showHelpAfterError();
|
|
432
|
+
program.addHelpText("after", `
|
|
433
|
+
Quick Start
|
|
434
|
+
tcb-sandbox serve # alias: local — embedded TRW on loopback
|
|
435
|
+
tcb-sandbox --endpoint http://127.0.0.1:9000 health
|
|
436
|
+
tcb-sandbox --endpoint http://127.0.0.1:9000 docs
|
|
437
|
+
tcb-sandbox --endpoint http://127.0.0.1:9000 --session-id demo tools call read --param path=README.md
|
|
438
|
+
|
|
439
|
+
Error Handling
|
|
440
|
+
HTTP failures are parsed as RFC 9457 problem details when available.
|
|
441
|
+
Use --debug to inspect request/response metadata.
|
|
442
|
+
`);
|
|
443
|
+
program
|
|
444
|
+
.command("serve")
|
|
445
|
+
.alias("local")
|
|
446
|
+
.description("Run embedded tcb-remote-workspace locally (multi-session; X-Cloudbase-Session-Id; no auth).")
|
|
447
|
+
.option("--port <n>", "Listen port", "9000")
|
|
448
|
+
.option("--host <addr>", "Bind address (use 0.0.0.0 for all interfaces)", "127.0.0.1")
|
|
449
|
+
.option("--workspace-root <path>", "Parent directory; each session is a subdirectory by session id", defaultWorkspaceRoot())
|
|
450
|
+
.action(async function (opts) {
|
|
451
|
+
const port = asInt(opts.port);
|
|
452
|
+
const workspaceRoot = path.resolve(opts.workspaceRoot);
|
|
453
|
+
const code = await runServe({
|
|
454
|
+
host: opts.host,
|
|
455
|
+
port,
|
|
456
|
+
workspaceRoot,
|
|
457
|
+
});
|
|
458
|
+
process.exit(code === null ? 0 : code);
|
|
459
|
+
});
|
|
460
|
+
program
|
|
461
|
+
.command("health")
|
|
462
|
+
.description("Call GET /health")
|
|
463
|
+
.action(async function () {
|
|
464
|
+
const ctx = buildContext(this);
|
|
465
|
+
logMessage(ctx, "info", "GET /health");
|
|
466
|
+
const client = createSdkClientFromContext(ctx);
|
|
467
|
+
const body = await client.system.health();
|
|
468
|
+
const result = "data" in body ? body.data : body;
|
|
469
|
+
if (ctx.output === "json")
|
|
470
|
+
return printJson(result);
|
|
471
|
+
printPretty("Health", result);
|
|
472
|
+
});
|
|
473
|
+
program
|
|
474
|
+
.command("docs")
|
|
475
|
+
.description("Call GET /api/docs (machine-readable API spec)")
|
|
476
|
+
.action(async function () {
|
|
477
|
+
const ctx = buildContext(this);
|
|
478
|
+
logMessage(ctx, "info", "GET /api/docs");
|
|
479
|
+
const client = createSdkClientFromContext(ctx);
|
|
480
|
+
let docs;
|
|
481
|
+
try {
|
|
482
|
+
const response = await client.system.apiDocs();
|
|
483
|
+
docs = ("data" in response ? response.data : response);
|
|
484
|
+
}
|
|
485
|
+
catch (err) {
|
|
486
|
+
logMessage(ctx, "warn", "GET /api/docs unavailable, using bundled fallback docs");
|
|
487
|
+
docs = BUNDLED_API_DOCS;
|
|
488
|
+
}
|
|
489
|
+
if (ctx.output === "json")
|
|
490
|
+
return printJson(docs);
|
|
491
|
+
printPretty("API Docs", docs);
|
|
492
|
+
});
|
|
493
|
+
program
|
|
494
|
+
.command("read <path>")
|
|
495
|
+
.description("Read file contents")
|
|
496
|
+
.option("--offset <n>", "Start line (1-indexed)", asInt)
|
|
497
|
+
.option("--limit <n>", "Max lines", asInt)
|
|
498
|
+
.action(async function (path, options) {
|
|
499
|
+
const ctx = buildContext(this);
|
|
500
|
+
ensureSessionId(ctx);
|
|
501
|
+
const client = createSdkClientFromContext(ctx);
|
|
502
|
+
logMessage(ctx, "info", "POST /api/tools/read");
|
|
503
|
+
const response = await client.fileOperations.read({
|
|
504
|
+
path,
|
|
505
|
+
offset: options.offset,
|
|
506
|
+
limit: options.limit,
|
|
507
|
+
});
|
|
508
|
+
const result = "data" in response ? response.data : response;
|
|
509
|
+
if (ctx.output === "json")
|
|
510
|
+
return printJson(result);
|
|
511
|
+
printPretty("Read", result);
|
|
512
|
+
});
|
|
513
|
+
program
|
|
514
|
+
.command("write <path>")
|
|
515
|
+
.description("Create or overwrite a file")
|
|
516
|
+
.option("--content <text>", "File content")
|
|
517
|
+
.option("--content-stdin", "Read content from stdin")
|
|
518
|
+
.action(async function (path, options) {
|
|
519
|
+
const ctx = buildContext(this);
|
|
520
|
+
ensureSessionId(ctx);
|
|
521
|
+
const client = createSdkClientFromContext(ctx);
|
|
522
|
+
const content = options.contentStdin
|
|
523
|
+
? (await readFromStdin()).trimEnd()
|
|
524
|
+
: (options.content ?? "");
|
|
525
|
+
logMessage(ctx, "info", "POST /api/tools/write");
|
|
526
|
+
const response = await client.fileOperations.write({ path, content });
|
|
527
|
+
const result = "data" in response ? response.data : response;
|
|
528
|
+
if (ctx.output === "json")
|
|
529
|
+
return printJson(result);
|
|
530
|
+
printPretty("Write", result);
|
|
531
|
+
});
|
|
532
|
+
program
|
|
533
|
+
.command("edit <path>")
|
|
534
|
+
.description("Apply a targeted string replacement in a file")
|
|
535
|
+
.option("--old <text>", "String to replace")
|
|
536
|
+
.option("--new <text>", "Replacement string")
|
|
537
|
+
.option("--replace-all", "Replace all matches", false)
|
|
538
|
+
.action(async function (path, options) {
|
|
539
|
+
const ctx = buildContext(this);
|
|
540
|
+
ensureSessionId(ctx);
|
|
541
|
+
if (options.old === undefined || options.new === undefined) {
|
|
542
|
+
throw new Error("--old and --new are required");
|
|
543
|
+
}
|
|
544
|
+
const client = createSdkClientFromContext(ctx);
|
|
545
|
+
logMessage(ctx, "info", "POST /api/tools/edit");
|
|
546
|
+
const response = await client.fileOperations.edit({
|
|
547
|
+
path,
|
|
548
|
+
oldString: options.old,
|
|
549
|
+
newString: options.new,
|
|
550
|
+
replaceAll: options.replaceAll,
|
|
551
|
+
});
|
|
552
|
+
const result = "data" in response ? response.data : response;
|
|
553
|
+
if (ctx.output === "json")
|
|
554
|
+
return printJson(result);
|
|
555
|
+
printPretty("Edit", result);
|
|
556
|
+
});
|
|
557
|
+
program
|
|
558
|
+
.command("bash <command>")
|
|
559
|
+
.description("Execute a shell command in the session workspace")
|
|
560
|
+
.option("--timeout <ms>", "Execution timeout in milliseconds", asInt)
|
|
561
|
+
.option("--cwd <dir>", "Working directory")
|
|
562
|
+
.option("--mode <mode>", "execute | dry_run")
|
|
563
|
+
.action(async function (command, options) {
|
|
564
|
+
const ctx = buildContext(this);
|
|
565
|
+
ensureSessionId(ctx);
|
|
566
|
+
const client = createSdkClientFromContext(ctx);
|
|
567
|
+
logMessage(ctx, "info", "POST /api/tools/bash");
|
|
568
|
+
const bashRequest = {
|
|
569
|
+
command,
|
|
570
|
+
timeout: options.timeout,
|
|
571
|
+
cwd: options.cwd,
|
|
572
|
+
mode: options.mode ? normalizeBashModeValue(options.mode) : undefined,
|
|
573
|
+
};
|
|
574
|
+
const response = await client.execution.bash(bashRequest);
|
|
575
|
+
const result = "data" in response ? response.data : response;
|
|
576
|
+
if (ctx.output === "json")
|
|
577
|
+
return printJson(result);
|
|
578
|
+
printPretty("Bash", result);
|
|
579
|
+
});
|
|
580
|
+
program
|
|
581
|
+
.command("grep <pattern>")
|
|
582
|
+
.description("Search file contents by regex pattern")
|
|
583
|
+
.option("--path <dir>", "Directory to search in")
|
|
584
|
+
.option("--include <glob>", "File glob filter, e.g. '*.ts'")
|
|
585
|
+
.option("--glob <glob>", "Alias for --include")
|
|
586
|
+
.option("--case-sensitive", "Enable case-sensitive matching", false)
|
|
587
|
+
.option("--limit <n>", "Max matches", asInt)
|
|
588
|
+
.option("--offset <n>", "Skip first N file results", asNonNegativeInt)
|
|
589
|
+
.action(async function (pattern, options) {
|
|
590
|
+
const ctx = buildContext(this);
|
|
591
|
+
ensureSessionId(ctx);
|
|
592
|
+
const client = createSdkClientFromContext(ctx);
|
|
593
|
+
const effectiveGlob = options.include ?? options.glob;
|
|
594
|
+
logMessage(ctx, "info", "POST /api/tools/grep");
|
|
595
|
+
const grepRequest = {
|
|
596
|
+
pattern,
|
|
597
|
+
path: options.path,
|
|
598
|
+
include: effectiveGlob,
|
|
599
|
+
caseSensitive: options.caseSensitive,
|
|
600
|
+
limit: options.limit,
|
|
601
|
+
offset: options.offset,
|
|
602
|
+
};
|
|
603
|
+
const response = await client.search.grep(grepRequest);
|
|
604
|
+
const result = "data" in response ? response.data : response;
|
|
605
|
+
if (ctx.output === "json")
|
|
606
|
+
return printJson(result);
|
|
607
|
+
printPretty("Grep", result);
|
|
608
|
+
});
|
|
609
|
+
program
|
|
610
|
+
.command("glob <pattern>")
|
|
611
|
+
.description("Find files by name/path glob pattern")
|
|
612
|
+
.option("--path <dir>", "Search root path")
|
|
613
|
+
.option("--ignore <pattern...>", "Ignore patterns, repeatable")
|
|
614
|
+
.action(async function (pattern, options) {
|
|
615
|
+
const ctx = buildContext(this);
|
|
616
|
+
ensureSessionId(ctx);
|
|
617
|
+
const client = createSdkClientFromContext(ctx);
|
|
618
|
+
logMessage(ctx, "info", "POST /api/tools/glob");
|
|
619
|
+
const globRequest = {
|
|
620
|
+
pattern,
|
|
621
|
+
path: options.path,
|
|
622
|
+
ignore: options.ignore,
|
|
623
|
+
};
|
|
624
|
+
const response = await client.search.glob(globRequest);
|
|
625
|
+
const result = "data" in response ? response.data : response;
|
|
626
|
+
if (ctx.output === "json")
|
|
627
|
+
return printJson(result);
|
|
628
|
+
printPretty("Glob", result);
|
|
629
|
+
});
|
|
630
|
+
program
|
|
631
|
+
.command("ls [path]")
|
|
632
|
+
.description("List directory contents")
|
|
633
|
+
.option("--ignore <pattern...>", "Ignore patterns, repeatable")
|
|
634
|
+
.action(async function (path, options) {
|
|
635
|
+
const ctx = buildContext(this);
|
|
636
|
+
ensureSessionId(ctx);
|
|
637
|
+
const client = createSdkClientFromContext(ctx);
|
|
638
|
+
logMessage(ctx, "info", "POST /api/tools/ls");
|
|
639
|
+
const response = await client.search.ls({
|
|
640
|
+
path,
|
|
641
|
+
ignore: options.ignore,
|
|
642
|
+
});
|
|
643
|
+
const result = "data" in response ? response.data : response;
|
|
644
|
+
if (ctx.output === "json")
|
|
645
|
+
return printJson(result);
|
|
646
|
+
printPretty("Ls", result);
|
|
647
|
+
});
|
|
648
|
+
program
|
|
649
|
+
.command("batch")
|
|
650
|
+
.description("Execute multiple tool calls in one request")
|
|
651
|
+
.option("--data <json>", "JSON array of tool calls")
|
|
652
|
+
.option("--param <key=value...>", "Key/value entry, repeatable (merged into each call)")
|
|
653
|
+
.action(async function (options) {
|
|
654
|
+
const ctx = buildContext(this);
|
|
655
|
+
ensureSessionId(ctx);
|
|
656
|
+
const client = createSdkClientFromContext(ctx);
|
|
657
|
+
const payload = options.data ? parseJsonObject(options.data, "--data") : { tool_calls: [] };
|
|
658
|
+
logMessage(ctx, "info", "POST /api/tools/batch");
|
|
659
|
+
const batchRequest = {
|
|
660
|
+
tool_calls: payload.tool_calls || [],
|
|
661
|
+
};
|
|
662
|
+
const response = await client.execution.batch(batchRequest);
|
|
663
|
+
const result = "data" in response ? response.data : response;
|
|
664
|
+
if (ctx.output === "json")
|
|
665
|
+
return printJson(result);
|
|
666
|
+
printPretty("Batch", result);
|
|
667
|
+
});
|
|
668
|
+
program
|
|
669
|
+
.command("git-push <message>")
|
|
670
|
+
.description("Push a workspace snapshot to a remote git repository")
|
|
671
|
+
.action(async function (message) {
|
|
672
|
+
const ctx = buildContext(this);
|
|
673
|
+
ensureSessionId(ctx);
|
|
674
|
+
const client = createSdkClientFromContext(ctx);
|
|
675
|
+
logMessage(ctx, "info", "POST /api/tools/git_push");
|
|
676
|
+
const response = await client.git.push({ message });
|
|
677
|
+
const result = "data" in response ? response.data : response;
|
|
678
|
+
if (ctx.output === "json")
|
|
679
|
+
return printJson(result);
|
|
680
|
+
printPretty("Git Push", result);
|
|
681
|
+
});
|
|
682
|
+
program
|
|
683
|
+
.command("mcporter <command...>")
|
|
684
|
+
.description("mcporter CLI — pass full command line as arguments")
|
|
685
|
+
.action(async function (command, _options) {
|
|
686
|
+
const ctx = buildContext(this);
|
|
687
|
+
ensureSessionId(ctx);
|
|
688
|
+
const client = createSdkClientFromContext(ctx);
|
|
689
|
+
const cmdStr = command.join(" ");
|
|
690
|
+
logMessage(ctx, "info", "POST /api/tools/mcporter_cli");
|
|
691
|
+
const response = await client.mcpServerManagement.mcporterCli({ command: cmdStr });
|
|
692
|
+
const result = "data" in response ? response.data : response;
|
|
693
|
+
if (ctx.output === "json")
|
|
694
|
+
return printJson(result);
|
|
695
|
+
printPretty("Mcporter CLI", result);
|
|
696
|
+
});
|
|
697
|
+
program
|
|
698
|
+
.command("add-mcp-servers")
|
|
699
|
+
.description("Add one or more stdio MCP servers to session mcporter config (idempotent)")
|
|
700
|
+
.option("--data <json>", "Full JSON payload (supports mcpServers object, or bare fields)")
|
|
701
|
+
.option("--server-name <name>", "Server name")
|
|
702
|
+
.option("--command <cmd>", "Executable path or command")
|
|
703
|
+
.option("--args <args...>", "Command arguments, repeatable")
|
|
704
|
+
.option("--env <key=val...>", "Environment variables, repeatable")
|
|
705
|
+
.action(async function (options) {
|
|
706
|
+
const ctx = buildContext(this);
|
|
707
|
+
ensureSessionId(ctx);
|
|
708
|
+
let payload;
|
|
709
|
+
if (options.data) {
|
|
710
|
+
payload = parseJsonObject(options.data, "--data");
|
|
711
|
+
}
|
|
712
|
+
else if (options.serverName && options.command) {
|
|
713
|
+
payload = { serverName: options.serverName, command: options.command };
|
|
714
|
+
if (options.args)
|
|
715
|
+
payload.args = options.args;
|
|
716
|
+
if (options.env) {
|
|
717
|
+
const envMap = {};
|
|
718
|
+
for (const e of options.env) {
|
|
719
|
+
const idx = e.indexOf("=");
|
|
720
|
+
if (idx <= 0)
|
|
721
|
+
throw new Error(`Invalid --env "${e}". Expected KEY=VALUE.`);
|
|
722
|
+
envMap[e.slice(0, idx)] = e.slice(idx + 1);
|
|
723
|
+
}
|
|
724
|
+
payload.env = envMap;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
else {
|
|
728
|
+
throw new Error("--data or (--server-name + --command) is required");
|
|
729
|
+
}
|
|
730
|
+
const client = createSdkClientFromContext(ctx);
|
|
731
|
+
const response = await client.mcpServerManagement.addMcpServers(payload);
|
|
732
|
+
const result = "data" in response ? response.data : response;
|
|
733
|
+
if (ctx.output === "json")
|
|
734
|
+
return printJson(result);
|
|
735
|
+
printPretty("Add MCP Servers", result);
|
|
736
|
+
});
|
|
737
|
+
program
|
|
738
|
+
.command("remove-mcp-servers")
|
|
739
|
+
.description("Remove one or more MCP servers from session mcporter config")
|
|
740
|
+
.option("--data <json>", "Full JSON payload (serverNames array or bare serverName)")
|
|
741
|
+
.option("--server-name <name>", "Single server name to remove")
|
|
742
|
+
.option("--server-names <names...>", "Multiple server names to remove, repeatable")
|
|
743
|
+
.action(async function (options) {
|
|
744
|
+
const ctx = buildContext(this);
|
|
745
|
+
ensureSessionId(ctx);
|
|
746
|
+
const client = createSdkClientFromContext(ctx);
|
|
747
|
+
let payload;
|
|
748
|
+
if (options.data) {
|
|
749
|
+
payload = parseJsonObject(options.data, "--data");
|
|
750
|
+
}
|
|
751
|
+
else if (options.serverNames && options.serverNames.length > 0) {
|
|
752
|
+
payload = { serverNames: options.serverNames };
|
|
753
|
+
}
|
|
754
|
+
else if (options.serverName) {
|
|
755
|
+
payload = { serverName: options.serverName };
|
|
756
|
+
}
|
|
757
|
+
else {
|
|
758
|
+
throw new Error("--data, --server-name, or --server-names is required");
|
|
759
|
+
}
|
|
760
|
+
logMessage(ctx, "info", "POST /api/tools/remove_mcp_servers");
|
|
761
|
+
const response = await client.mcpServerManagement.removeMcpServers(payload);
|
|
762
|
+
const result = "data" in response ? response.data : response;
|
|
763
|
+
if (ctx.output === "json")
|
|
764
|
+
return printJson(result);
|
|
765
|
+
printPretty("Remove MCP Servers", result);
|
|
766
|
+
});
|
|
767
|
+
program
|
|
768
|
+
.command("list-mcp-servers")
|
|
769
|
+
.description("List configured MCP server names in session mcporter")
|
|
770
|
+
.action(async function () {
|
|
771
|
+
const ctx = buildContext(this);
|
|
772
|
+
ensureSessionId(ctx);
|
|
773
|
+
const client = createSdkClientFromContext(ctx);
|
|
774
|
+
logMessage(ctx, "info", "POST /api/tools/list_mcp_servers");
|
|
775
|
+
const response = await client.mcpServerManagement.listMcpServers();
|
|
776
|
+
const result = "data" in response ? response.data : response;
|
|
777
|
+
if (ctx.output === "json")
|
|
778
|
+
return printJson(result);
|
|
779
|
+
printPretty("List MCP Servers", result);
|
|
780
|
+
});
|
|
781
|
+
program
|
|
782
|
+
.command("capability-list")
|
|
783
|
+
.description("List all capabilities in the current session")
|
|
784
|
+
.option("--reveal-paths", "Include absolute paths for skill packages", false)
|
|
785
|
+
.action(async function (options) {
|
|
786
|
+
const ctx = buildContext(this);
|
|
787
|
+
ensureSessionId(ctx);
|
|
788
|
+
const client = createSdkClientFromContext(ctx);
|
|
789
|
+
logMessage(ctx, "info", "POST /api/tools/capability_list");
|
|
790
|
+
const response = await client.capabilitySystem.capabilityList({ revealPaths: options.revealPaths });
|
|
791
|
+
const result = "data" in response ? response.data : response;
|
|
792
|
+
if (ctx.output === "json")
|
|
793
|
+
return printJson(result);
|
|
794
|
+
printPretty("Capability List", result);
|
|
795
|
+
});
|
|
796
|
+
program
|
|
797
|
+
.command("capability-describe <id>")
|
|
798
|
+
.description("Describe a single capability (executionBy / executionVia / actions / schema)")
|
|
799
|
+
.action(async function (id) {
|
|
800
|
+
const ctx = buildContext(this);
|
|
801
|
+
ensureSessionId(ctx);
|
|
802
|
+
const client = createSdkClientFromContext(ctx);
|
|
803
|
+
logMessage(ctx, "info", "POST /api/tools/capability_describe");
|
|
804
|
+
const response = await client.capabilitySystem.capabilityDescribe({ id });
|
|
805
|
+
const result = "data" in response ? response.data : response;
|
|
806
|
+
if (ctx.output === "json")
|
|
807
|
+
return printJson(result);
|
|
808
|
+
printPretty("Capability Describe", result);
|
|
809
|
+
});
|
|
810
|
+
program
|
|
811
|
+
.command("capability-install <id>")
|
|
812
|
+
.description("Get installation guidance for a capability (instruction-only, no registry change)")
|
|
813
|
+
.option("--kind <kind>", "Capability kind: mcp | cli | skill")
|
|
814
|
+
.option("--server-name <name>", "MCP server name (required when kind=mcp)")
|
|
815
|
+
.option("--command <cmd>", "Executable path")
|
|
816
|
+
.option("--package-path <path>", "Skill package path")
|
|
817
|
+
.option("--entry-file <file>", "Skill entry file, default SKILL.md")
|
|
818
|
+
.action(async function (id, options) {
|
|
819
|
+
const ctx = buildContext(this);
|
|
820
|
+
ensureSessionId(ctx);
|
|
821
|
+
const client = createSdkClientFromContext(ctx);
|
|
822
|
+
logMessage(ctx, "info", "POST /api/tools/capability_install");
|
|
823
|
+
const installRequest = {
|
|
824
|
+
id,
|
|
825
|
+
kind: options.kind,
|
|
826
|
+
serverName: options.serverName,
|
|
827
|
+
command: options.command,
|
|
828
|
+
packagePath: options.packagePath,
|
|
829
|
+
entryFile: options.entryFile,
|
|
830
|
+
};
|
|
831
|
+
const response = await client.capabilitySystem.capabilityInstall(installRequest);
|
|
832
|
+
const result = "data" in response ? response.data : response;
|
|
833
|
+
if (ctx.output === "json")
|
|
834
|
+
return printJson(result);
|
|
835
|
+
printPretty("Capability Install", result);
|
|
836
|
+
});
|
|
837
|
+
program
|
|
838
|
+
.command("capability-register <id>")
|
|
839
|
+
.description("Register a capability into the session (after external installation)")
|
|
840
|
+
.option("--kind <kind>", "Capability kind: mcp | cli | skill")
|
|
841
|
+
.option("--server-name <name>", "MCP server name (required when kind=mcp)")
|
|
842
|
+
.option("--command <cmd>", "Executable path")
|
|
843
|
+
.option("--args <args...>", "Command arguments, repeatable")
|
|
844
|
+
.option("--package-path <path>", "Skill package path")
|
|
845
|
+
.option("--entry-file <file>", "Skill entry file, default SKILL.md")
|
|
846
|
+
.action(async function (id, options) {
|
|
847
|
+
const ctx = buildContext(this);
|
|
848
|
+
ensureSessionId(ctx);
|
|
849
|
+
const client = createSdkClientFromContext(ctx);
|
|
850
|
+
logMessage(ctx, "info", "POST /api/tools/capability_register");
|
|
851
|
+
if (!options.kind) {
|
|
852
|
+
throw new Error("--kind is required (mcp | cli | skill)");
|
|
853
|
+
}
|
|
854
|
+
const registerRequest = {
|
|
855
|
+
id,
|
|
856
|
+
kind: options.kind,
|
|
857
|
+
serverName: options.serverName,
|
|
858
|
+
command: options.command,
|
|
859
|
+
args: options.args,
|
|
860
|
+
packagePath: options.packagePath,
|
|
861
|
+
entryFile: options.entryFile,
|
|
862
|
+
};
|
|
863
|
+
const response = await client.capabilitySystem.capabilityRegister(registerRequest);
|
|
864
|
+
const result = "data" in response ? response.data : response;
|
|
865
|
+
if (ctx.output === "json")
|
|
866
|
+
return printJson(result);
|
|
867
|
+
printPretty("Capability Register", result);
|
|
868
|
+
});
|
|
869
|
+
program
|
|
870
|
+
.command("capability-remove <id>")
|
|
871
|
+
.description("Remove a registered capability (built-in capabilities cannot be removed)")
|
|
872
|
+
.action(async function (id) {
|
|
873
|
+
const ctx = buildContext(this);
|
|
874
|
+
ensureSessionId(ctx);
|
|
875
|
+
const client = createSdkClientFromContext(ctx);
|
|
876
|
+
logMessage(ctx, "info", "POST /api/tools/capability_remove");
|
|
877
|
+
const response = await client.capabilitySystem.capabilityRemove({ id });
|
|
878
|
+
const result = "data" in response ? response.data : response;
|
|
879
|
+
if (ctx.output === "json")
|
|
880
|
+
return printJson(result);
|
|
881
|
+
printPretty("Capability Remove", result);
|
|
882
|
+
});
|
|
883
|
+
program
|
|
884
|
+
.command("capability-invoke <id> <action>")
|
|
885
|
+
.description("Invoke a native capability action (secrets-store / files-transfer / preview-service / pty-service / git-archive)")
|
|
886
|
+
.option("--parameters <json>", "Action parameters as JSON string")
|
|
887
|
+
.action(async function (id, action, options) {
|
|
888
|
+
const ctx = buildContext(this);
|
|
889
|
+
ensureSessionId(ctx);
|
|
890
|
+
const client = createSdkClientFromContext(ctx);
|
|
891
|
+
const parameters = options.parameters
|
|
892
|
+
? parseJsonObject(options.parameters, "--parameters")
|
|
893
|
+
: {};
|
|
894
|
+
logMessage(ctx, "info", "POST /api/tools/capability_invoke");
|
|
895
|
+
const response = await client.capabilitySystem.capabilityInvoke({ id, action, parameters });
|
|
896
|
+
const result = "data" in response ? response.data : response;
|
|
897
|
+
if (ctx.output === "json")
|
|
898
|
+
return printJson(result);
|
|
899
|
+
printPretty("Capability Invoke", result);
|
|
900
|
+
});
|
|
901
|
+
const snapshot = program
|
|
902
|
+
.command("snapshot")
|
|
903
|
+
.description("Workspace snapshot and restore (S3-backed)");
|
|
904
|
+
snapshot
|
|
905
|
+
.command("create")
|
|
906
|
+
.description("Create async workspace snapshot and upload to S3")
|
|
907
|
+
.option("--exclude <patterns...>", "Additional exclude patterns")
|
|
908
|
+
.option("--include-all", "Skip default excludes (only sensitive files still excluded)")
|
|
909
|
+
.action(async function (options) {
|
|
910
|
+
const ctx = buildContext(this);
|
|
911
|
+
ensureSessionId(ctx);
|
|
912
|
+
const client = createSdkClientFromContext(ctx);
|
|
913
|
+
logMessage(ctx, "info", "POST /api/tools/workspace_snapshot");
|
|
914
|
+
const response = await client.other.workspaceSnapshot({
|
|
915
|
+
exclude: options.exclude,
|
|
916
|
+
include_all: options.includeAll,
|
|
917
|
+
});
|
|
918
|
+
const result = "data" in response ? response.data : response;
|
|
919
|
+
if (ctx.output === "json")
|
|
920
|
+
return printJson(result);
|
|
921
|
+
printPretty("Snapshot Create", result);
|
|
922
|
+
});
|
|
923
|
+
snapshot
|
|
924
|
+
.command("status [snapshotId]")
|
|
925
|
+
.description("Check snapshot status (single or all session tasks)")
|
|
926
|
+
.action(async function (snapshotId) {
|
|
927
|
+
const ctx = buildContext(this);
|
|
928
|
+
ensureSessionId(ctx);
|
|
929
|
+
const client = createSdkClientFromContext(ctx);
|
|
930
|
+
logMessage(ctx, "info", "POST /api/tools/workspace_snapshot_status");
|
|
931
|
+
const response = await client.other.workspaceSnapshotStatus({
|
|
932
|
+
snapshot_id: snapshotId,
|
|
933
|
+
});
|
|
934
|
+
const result = "data" in response ? response.data : response;
|
|
935
|
+
if (ctx.output === "json")
|
|
936
|
+
return printJson(result);
|
|
937
|
+
printPretty("Snapshot Status", result);
|
|
938
|
+
});
|
|
939
|
+
snapshot
|
|
940
|
+
.command("restore <snapshotId>")
|
|
941
|
+
.description("Restore workspace from snapshot (supports cross-session)")
|
|
942
|
+
.option("--mode <mode>", 'Restore mode: "merge" (default) or "replace"', "merge")
|
|
943
|
+
.action(async function (snapshotId, options) {
|
|
944
|
+
const ctx = buildContext(this);
|
|
945
|
+
ensureSessionId(ctx);
|
|
946
|
+
const client = createSdkClientFromContext(ctx);
|
|
947
|
+
logMessage(ctx, "info", "POST /api/tools/workspace_restore");
|
|
948
|
+
const response = await client.other.workspaceRestore({
|
|
949
|
+
snapshot_id: snapshotId,
|
|
950
|
+
mode: options.mode,
|
|
951
|
+
});
|
|
952
|
+
const result = "data" in response ? response.data : response;
|
|
953
|
+
if (ctx.output === "json")
|
|
954
|
+
return printJson(result);
|
|
955
|
+
printPretty("Snapshot Restore", result);
|
|
956
|
+
});
|
|
957
|
+
const secrets = program.command("secrets").description("Secrets helpers (session-scoped)");
|
|
958
|
+
secrets
|
|
959
|
+
.command("set <key>")
|
|
960
|
+
.description("Set or overwrite a secret")
|
|
961
|
+
.option("--value <value>", "Secret value")
|
|
962
|
+
.option("--value-stdin", "Read secret value from stdin")
|
|
963
|
+
.action(async function (key, options) {
|
|
964
|
+
const ctx = buildContext(this);
|
|
965
|
+
ensureSessionId(ctx);
|
|
966
|
+
const client = createSdkClientFromContext(ctx);
|
|
967
|
+
const byArg = options.value;
|
|
968
|
+
const byStdin = options.valueStdin ? (await readFromStdin()).trimEnd() : undefined;
|
|
969
|
+
const value = byStdin ?? byArg;
|
|
970
|
+
if (value === undefined) {
|
|
971
|
+
throw new Error("Missing value. Use --value <value> or --value-stdin.");
|
|
972
|
+
}
|
|
973
|
+
logMessage(ctx, "info", "POST /api/tools/secrets_set");
|
|
974
|
+
const secretsRequest = { key, value };
|
|
975
|
+
if (shouldLog(ctx, "debug")) {
|
|
976
|
+
logMessage(ctx, "debug", `request json: ${JSON.stringify(redactJsonForDebug(secretsRequest))}`);
|
|
977
|
+
}
|
|
978
|
+
const response = await client.secrets.set(secretsRequest);
|
|
979
|
+
const result = "data" in response ? response.data : response;
|
|
980
|
+
if (ctx.output === "json")
|
|
981
|
+
return printJson(result);
|
|
982
|
+
printPretty("Secret Set", result);
|
|
983
|
+
});
|
|
984
|
+
secrets
|
|
985
|
+
.command("get <key>")
|
|
986
|
+
.description("Get plaintext secret value")
|
|
987
|
+
.action(async function (key) {
|
|
988
|
+
const ctx = buildContext(this);
|
|
989
|
+
ensureSessionId(ctx);
|
|
990
|
+
const client = createSdkClientFromContext(ctx);
|
|
991
|
+
logMessage(ctx, "info", "POST /api/tools/secrets_get");
|
|
992
|
+
const response = await client.secrets.get({ key });
|
|
993
|
+
const result = "data" in response ? response.data : response;
|
|
994
|
+
if (ctx.output === "json")
|
|
995
|
+
return printJson(result);
|
|
996
|
+
printPretty("Secret Get", result);
|
|
997
|
+
});
|
|
998
|
+
secrets
|
|
999
|
+
.command("list")
|
|
1000
|
+
.description("List secret keys (without plaintext values)")
|
|
1001
|
+
.action(async function () {
|
|
1002
|
+
const ctx = buildContext(this);
|
|
1003
|
+
ensureSessionId(ctx);
|
|
1004
|
+
const client = createSdkClientFromContext(ctx);
|
|
1005
|
+
logMessage(ctx, "info", "POST /api/tools/secrets_list");
|
|
1006
|
+
const response = await client.secrets.list();
|
|
1007
|
+
const result = "data" in response ? response.data : response;
|
|
1008
|
+
if (ctx.output === "json")
|
|
1009
|
+
return printJson(result);
|
|
1010
|
+
printPretty("Secret List", result);
|
|
1011
|
+
});
|
|
1012
|
+
secrets
|
|
1013
|
+
.command("delete <key>")
|
|
1014
|
+
.description("Delete a secret by key")
|
|
1015
|
+
.action(async function (key) {
|
|
1016
|
+
const ctx = buildContext(this);
|
|
1017
|
+
ensureSessionId(ctx);
|
|
1018
|
+
const client = createSdkClientFromContext(ctx);
|
|
1019
|
+
logMessage(ctx, "info", "POST /api/tools/secrets_delete");
|
|
1020
|
+
const response = await client.secrets.delete({ key });
|
|
1021
|
+
const result = "data" in response ? response.data : response;
|
|
1022
|
+
if (ctx.output === "json")
|
|
1023
|
+
return printJson(result);
|
|
1024
|
+
printPretty("Secret Delete", result);
|
|
1025
|
+
});
|
|
1026
|
+
const files = program.command("files").description("Binary file upload/download");
|
|
1027
|
+
files
|
|
1028
|
+
.command("upload <localPath> <remotePath>")
|
|
1029
|
+
.description("Upload local file via top-level files_upload tool")
|
|
1030
|
+
.action(async function (localPath, remotePath) {
|
|
1031
|
+
const ctx = buildContext(this);
|
|
1032
|
+
ensureSessionId(ctx);
|
|
1033
|
+
const client = createSdkClientFromContext(ctx);
|
|
1034
|
+
const full = path.resolve(localPath);
|
|
1035
|
+
const content = await fs.readFile(full);
|
|
1036
|
+
logMessage(ctx, "info", "POST /api/tools/files_upload");
|
|
1037
|
+
const response = await client.fileOperations.filesUpload({
|
|
1038
|
+
path: remotePath,
|
|
1039
|
+
contentBase64: content.toString("base64"),
|
|
1040
|
+
});
|
|
1041
|
+
const result = "data" in response ? response.data : response;
|
|
1042
|
+
if (ctx.output === "json")
|
|
1043
|
+
return printJson(result);
|
|
1044
|
+
printPretty("File Upload", result);
|
|
1045
|
+
});
|
|
1046
|
+
files
|
|
1047
|
+
.command("download <remotePath> <localPath>")
|
|
1048
|
+
.description("Download remote file via top-level files_download tool")
|
|
1049
|
+
.action(async function (remotePath, localPath) {
|
|
1050
|
+
const ctx = buildContext(this);
|
|
1051
|
+
ensureSessionId(ctx);
|
|
1052
|
+
const client = createSdkClientFromContext(ctx);
|
|
1053
|
+
logMessage(ctx, "info", "POST /api/tools/files_download");
|
|
1054
|
+
const response = await client.fileOperations.filesDownload({ path: remotePath });
|
|
1055
|
+
const result = "data" in response ? response.data : response;
|
|
1056
|
+
const resultRecord = result;
|
|
1057
|
+
const innerResult = resultRecord?.result;
|
|
1058
|
+
const contentBase64 = typeof innerResult?.contentBase64 === "string" ? innerResult.contentBase64 : "";
|
|
1059
|
+
if (!contentBase64) {
|
|
1060
|
+
throw new Error("files-transfer.download returned empty contentBase64");
|
|
1061
|
+
}
|
|
1062
|
+
const buffer = Buffer.from(contentBase64, "base64");
|
|
1063
|
+
const full = path.resolve(localPath);
|
|
1064
|
+
await fs.mkdir(path.dirname(full), { recursive: true });
|
|
1065
|
+
await fs.writeFile(full, buffer);
|
|
1066
|
+
if (ctx.output === "json") {
|
|
1067
|
+
return printJson({ success: true, path: full, bytes: buffer.byteLength });
|
|
1068
|
+
}
|
|
1069
|
+
process.stdout.write(`Downloaded ${buffer.byteLength} bytes -> ${full}\n`);
|
|
1070
|
+
});
|
|
1071
|
+
const preview = program.command("preview").description("Preview endpoints");
|
|
1072
|
+
preview
|
|
1073
|
+
.command("ports")
|
|
1074
|
+
.description("List previewable ports via top-level preview_ports tool")
|
|
1075
|
+
.action(async function () {
|
|
1076
|
+
const ctx = buildContext(this);
|
|
1077
|
+
ensureSessionId(ctx);
|
|
1078
|
+
const client = createSdkClientFromContext(ctx);
|
|
1079
|
+
logMessage(ctx, "info", "POST /api/tools/preview_ports");
|
|
1080
|
+
const response = await client.preview.ports({
|
|
1081
|
+
endpoint: ctx.endpoint,
|
|
1082
|
+
includeSessionQuery: Boolean(ctx.sessionId),
|
|
1083
|
+
});
|
|
1084
|
+
const result = "data" in response ? response.data : response;
|
|
1085
|
+
if (ctx.output === "json")
|
|
1086
|
+
return printJson(result);
|
|
1087
|
+
printPretty("Preview Ports", result);
|
|
1088
|
+
});
|
|
1089
|
+
preview
|
|
1090
|
+
.command("url <port>")
|
|
1091
|
+
.description("Build preview URL for a port")
|
|
1092
|
+
.action(async function (portRaw) {
|
|
1093
|
+
const ctx = buildContext(this);
|
|
1094
|
+
const port = asInt(portRaw);
|
|
1095
|
+
const url = await getPreviewUrl(ctx, port);
|
|
1096
|
+
if (ctx.output === "json") {
|
|
1097
|
+
return printJson({ url, port, sessionIdIncluded: Boolean(ctx.sessionId) });
|
|
1098
|
+
}
|
|
1099
|
+
process.stdout.write(`${url}\n`);
|
|
1100
|
+
});
|
|
1101
|
+
preview
|
|
1102
|
+
.command("open <port>")
|
|
1103
|
+
.description("Open preview URL in default browser")
|
|
1104
|
+
.option("--print-only", "Print URL only, do not open browser")
|
|
1105
|
+
.action(async function (portRaw, options) {
|
|
1106
|
+
const ctx = buildContext(this);
|
|
1107
|
+
const port = asInt(portRaw);
|
|
1108
|
+
const url = await getPreviewUrl(ctx, port);
|
|
1109
|
+
if (options.printOnly) {
|
|
1110
|
+
process.stdout.write(`${url}\n`);
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
await openUrl(url);
|
|
1114
|
+
if (ctx.output === "json") {
|
|
1115
|
+
return printJson({ opened: true, url, port, sessionIdIncluded: Boolean(ctx.sessionId) });
|
|
1116
|
+
}
|
|
1117
|
+
process.stdout.write(`Opened ${url}\n`);
|
|
1118
|
+
});
|
|
1119
|
+
const pty = program.command("pty").description("PTY process helpers");
|
|
1120
|
+
pty
|
|
1121
|
+
.command("create")
|
|
1122
|
+
.description("Create PTY process via top-level pty_create tool")
|
|
1123
|
+
.option("--command <command>", "Command to run")
|
|
1124
|
+
.option("--args <args...>", "Command arguments")
|
|
1125
|
+
.option("--cwd <cwd>", "Working directory (workspace relative or absolute)")
|
|
1126
|
+
.option("--cols <cols>", "Terminal columns")
|
|
1127
|
+
.option("--rows <rows>", "Terminal rows")
|
|
1128
|
+
.action(async function (options) {
|
|
1129
|
+
const ctx = buildContext(this);
|
|
1130
|
+
ensureSessionId(ctx);
|
|
1131
|
+
const client = createSdkClientFromContext(ctx);
|
|
1132
|
+
logMessage(ctx, "info", "POST /api/tools/pty_create");
|
|
1133
|
+
const response = await client.ptyTerminal.ptyCreate({
|
|
1134
|
+
cmd: options.command,
|
|
1135
|
+
args: options.args && options.args.length > 0 ? options.args : undefined,
|
|
1136
|
+
cwd: options.cwd,
|
|
1137
|
+
cols: options.cols !== undefined ? asInt(options.cols) : undefined,
|
|
1138
|
+
rows: options.rows !== undefined ? asInt(options.rows) : undefined,
|
|
1139
|
+
});
|
|
1140
|
+
const result = "data" in response ? response.data : response;
|
|
1141
|
+
if (ctx.output === "json")
|
|
1142
|
+
return printJson(result);
|
|
1143
|
+
printPretty("PTY Create", result);
|
|
1144
|
+
});
|
|
1145
|
+
pty
|
|
1146
|
+
.command("send-input <pid>")
|
|
1147
|
+
.description("Send stdin payload to PTY process via top-level pty_send_input tool")
|
|
1148
|
+
.option("--text <text>", "Plain text to send")
|
|
1149
|
+
.option("--text-stdin", "Read plain text from stdin")
|
|
1150
|
+
.option("--base64 <content>", "Base64 payload to send directly")
|
|
1151
|
+
.action(async function (pidRaw, options) {
|
|
1152
|
+
const ctx = buildContext(this);
|
|
1153
|
+
ensureSessionId(ctx);
|
|
1154
|
+
const client = createSdkClientFromContext(ctx);
|
|
1155
|
+
const pid = asInt(pidRaw);
|
|
1156
|
+
const textFromStdin = options.textStdin ? await readFromStdin() : undefined;
|
|
1157
|
+
const base64Payload = options.base64 ??
|
|
1158
|
+
(textFromStdin !== undefined
|
|
1159
|
+
? Buffer.from(textFromStdin, "utf8").toString("base64")
|
|
1160
|
+
: options.text !== undefined
|
|
1161
|
+
? Buffer.from(options.text, "utf8").toString("base64")
|
|
1162
|
+
: undefined);
|
|
1163
|
+
if (!base64Payload) {
|
|
1164
|
+
throw new Error("Missing input payload. Use --text, --text-stdin, or --base64.");
|
|
1165
|
+
}
|
|
1166
|
+
logMessage(ctx, "info", "POST /api/tools/pty_send_input");
|
|
1167
|
+
const response = await client.ptyTerminal.ptySendInput({ pid, inputBase64: base64Payload });
|
|
1168
|
+
const result = "data" in response ? response.data : response;
|
|
1169
|
+
if (ctx.output === "json")
|
|
1170
|
+
return printJson(result);
|
|
1171
|
+
printPretty("PTY Send Input", result);
|
|
1172
|
+
});
|
|
1173
|
+
pty
|
|
1174
|
+
.command("kill <pid>")
|
|
1175
|
+
.description("Kill PTY process via top-level pty_kill tool")
|
|
1176
|
+
.option("--signal <signal>", "Signal to use (default: SIGTERM)")
|
|
1177
|
+
.action(async function (pidRaw, options) {
|
|
1178
|
+
const ctx = buildContext(this);
|
|
1179
|
+
ensureSessionId(ctx);
|
|
1180
|
+
const client = createSdkClientFromContext(ctx);
|
|
1181
|
+
const pid = asInt(pidRaw);
|
|
1182
|
+
logMessage(ctx, "info", "POST /api/tools/pty_kill");
|
|
1183
|
+
const response = await client.ptyTerminal.ptyKill({ pid, signal: options.signal });
|
|
1184
|
+
const result = "data" in response ? response.data : response;
|
|
1185
|
+
if (ctx.output === "json")
|
|
1186
|
+
return printJson(result);
|
|
1187
|
+
printPretty("PTY Kill", result);
|
|
1188
|
+
});
|
|
1189
|
+
pty
|
|
1190
|
+
.command("resize <pid>")
|
|
1191
|
+
.description("Resize PTY process via top-level pty_resize tool")
|
|
1192
|
+
.requiredOption("--cols <cols>", "Terminal columns")
|
|
1193
|
+
.requiredOption("--rows <rows>", "Terminal rows")
|
|
1194
|
+
.action(async function (pidRaw, options) {
|
|
1195
|
+
const ctx = buildContext(this);
|
|
1196
|
+
ensureSessionId(ctx);
|
|
1197
|
+
const client = createSdkClientFromContext(ctx);
|
|
1198
|
+
const pid = asInt(pidRaw);
|
|
1199
|
+
const cols = asInt(options.cols);
|
|
1200
|
+
const rows = asInt(options.rows);
|
|
1201
|
+
logMessage(ctx, "info", "POST /api/tools/pty_resize");
|
|
1202
|
+
const response = await client.ptyTerminal.ptyResize({ pid, cols, rows });
|
|
1203
|
+
const result = "data" in response ? response.data : response;
|
|
1204
|
+
if (ctx.output === "json")
|
|
1205
|
+
return printJson(result);
|
|
1206
|
+
printPretty("PTY Resize", result);
|
|
1207
|
+
});
|
|
1208
|
+
pty
|
|
1209
|
+
.command("read-output <pid>")
|
|
1210
|
+
.description("Read buffered PTY output via top-level pty_read_output tool")
|
|
1211
|
+
.option("--after-seq <seq>", "Read events strictly after this sequence")
|
|
1212
|
+
.option("--limit <limit>", "Max number of events to read")
|
|
1213
|
+
.action(async function (pidRaw, options) {
|
|
1214
|
+
const ctx = buildContext(this);
|
|
1215
|
+
ensureSessionId(ctx);
|
|
1216
|
+
const client = createSdkClientFromContext(ctx);
|
|
1217
|
+
const pid = asInt(pidRaw);
|
|
1218
|
+
logMessage(ctx, "info", "POST /api/tools/pty_read_output");
|
|
1219
|
+
const response = await client.ptyTerminal.ptyReadOutput({
|
|
1220
|
+
pid,
|
|
1221
|
+
afterSeq: options.afterSeq !== undefined ? asNonNegativeInt(options.afterSeq) : undefined,
|
|
1222
|
+
limit: options.limit !== undefined ? asInt(options.limit) : undefined,
|
|
1223
|
+
});
|
|
1224
|
+
const result = "data" in response ? response.data : response;
|
|
1225
|
+
if (ctx.output === "json")
|
|
1226
|
+
return printJson(result);
|
|
1227
|
+
printPretty("PTY Read Output", result);
|
|
1228
|
+
});
|
|
1229
|
+
await program.parseAsync(process.argv);
|
|
1230
|
+
}
|
|
1231
|
+
main().catch((err) => {
|
|
1232
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
1233
|
+
process.exit(1);
|
|
1234
|
+
});
|