tracelet-cli 0.1.0 → 0.1.1

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/dist/bin.js CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/program.ts
4
- import { confirm, select } from "@inquirer/prompts";
4
+ import { confirm as confirm2, select as select2 } from "@inquirer/prompts";
5
+ import { readFileSync } from "fs";
5
6
  import { Command, Option } from "commander";
6
7
 
7
8
  // src/agents.ts
@@ -19,16 +20,38 @@ function launchEnv(env = process.env) {
19
20
  return { ...env, NO_PROXY: noProxy, no_proxy: noProxy };
20
21
  }
21
22
  async function hasBin(command) {
22
- const paths = process.env.PATH?.split(delimiter) ?? [];
23
+ const paths = command.includes("/") || command.includes("\\") ? [""] : process.env.PATH?.split(delimiter) ?? [];
24
+ const names = process.platform === "win32" && !/\.[^\\/]+$/.test(command) ? [command, ...(process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").map((ext) => `${command}${ext}`)] : [command];
23
25
  for (const path of paths) {
24
- try {
25
- await access(join(path, command), constants.X_OK);
26
- return true;
27
- } catch {
26
+ for (const name of names) {
27
+ try {
28
+ await access(join(path, name), constants.X_OK);
29
+ return true;
30
+ } catch {
31
+ }
28
32
  }
29
33
  }
30
34
  return false;
31
35
  }
36
+ function customAdapter(id, config) {
37
+ return {
38
+ id,
39
+ label: config.label,
40
+ command: config.command,
41
+ protocol: config.protocol,
42
+ detect: () => hasBin(config.command),
43
+ upstream: async () => config.upstream,
44
+ launch: (proxyUrl2, args) => {
45
+ const env = launchEnv();
46
+ const inject = config.inject.type === "env" ? [] : config.inject.args.map((arg) => arg.replaceAll("{baseUrl}", proxyUrl2));
47
+ if (config.inject.type === "env") {
48
+ const name = config.inject.name ?? (config.protocol === "anthropic" ? "ANTHROPIC_BASE_URL" : "OPENAI_BASE_URL");
49
+ env[name] = proxyUrl2;
50
+ }
51
+ return { command: config.command, args: [...inject, ...config.args, ...args], env };
52
+ }
53
+ };
54
+ }
32
55
  async function usesChatGpt() {
33
56
  try {
34
57
  const result = await exec("codex", ["login", "status"]);
@@ -138,6 +161,13 @@ async function clearData(root) {
138
161
  await mkdir(runs, { recursive: true });
139
162
  }
140
163
 
164
+ // src/custom-menu.ts
165
+ import { confirm, input, select } from "@inquirer/prompts";
166
+
167
+ // src/settings.ts
168
+ import { mkdir as mkdir2, readFile as readFile2, writeFile } from "fs/promises";
169
+ import { dirname } from "path";
170
+
141
171
  // src/paths.ts
142
172
  import { existsSync } from "fs";
143
173
  import { homedir as homedir3 } from "os";
@@ -155,6 +185,340 @@ function dashboardDir() {
155
185
  return existsSync(bundled) ? bundled : workspace;
156
186
  }
157
187
 
188
+ // src/settings.ts
189
+ function readAgent(value, id) {
190
+ if (!/^custom-[a-z0-9]+(?:-[a-z0-9]+)*$/.test(id) || !isObject(value)) {
191
+ throw new Error(`Invalid custom agent: ${id}`);
192
+ }
193
+ const { label, command, args, protocol, upstream, inject } = value;
194
+ if (typeof label !== "string" || !label.trim() || typeof command !== "string" || !command.trim() || !Array.isArray(args) || !args.every((arg) => typeof arg === "string") || protocol !== "anthropic" && protocol !== "openai" || typeof upstream !== "string" || !isObject(inject) || inject.type !== "env" && inject.type !== "args") {
195
+ throw new Error(`Invalid custom agent: ${id}`);
196
+ }
197
+ try {
198
+ if (!["http:", "https:"].includes(new URL(upstream).protocol)) {
199
+ throw new Error("Unsupported protocol");
200
+ }
201
+ } catch {
202
+ throw new Error(`Invalid upstream URL for custom agent: ${id}`);
203
+ }
204
+ if (inject.type === "env") {
205
+ if (inject.name !== void 0 && (typeof inject.name !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(inject.name))) {
206
+ throw new Error(`Invalid environment variable for custom agent: ${id}`);
207
+ }
208
+ return { label: label.trim(), command: command.trim(), args, protocol, upstream, inject: {
209
+ type: "env",
210
+ ...inject.name ? { name: inject.name } : {}
211
+ } };
212
+ }
213
+ if (!Array.isArray(inject.args) || !inject.args.every((arg) => typeof arg === "string") || !inject.args.some((arg) => arg.includes("{baseUrl}"))) {
214
+ throw new Error(`Invalid Base URL argument template for custom agent: ${id}`);
215
+ }
216
+ return {
217
+ label: label.trim(),
218
+ command: command.trim(),
219
+ args,
220
+ protocol,
221
+ upstream,
222
+ inject: { type: "args", args: inject.args }
223
+ };
224
+ }
225
+ function isObject(value) {
226
+ return value !== null && typeof value === "object" && !Array.isArray(value);
227
+ }
228
+ async function readRaw(path) {
229
+ try {
230
+ const value = JSON.parse(await readFile2(path, "utf8"));
231
+ if (!isObject(value)) {
232
+ throw new Error("Settings root must be an object");
233
+ }
234
+ return value;
235
+ } catch (error) {
236
+ if (error?.code === "ENOENT") {
237
+ return {};
238
+ }
239
+ throw new Error(`Failed to read Tracelet settings: ${path}`, { cause: error });
240
+ }
241
+ }
242
+ function readProxy(value, name) {
243
+ if (!isObject(value) || value.mode !== "direct" && value.mode !== "system") {
244
+ throw new Error(`Invalid proxy mode: ${name}`);
245
+ }
246
+ return { mode: value.mode };
247
+ }
248
+ function parseSettings(value, path) {
249
+ if (value.customAgents !== void 0 && !isObject(value.customAgents)) {
250
+ throw new Error(`Invalid custom agents: ${path}`);
251
+ }
252
+ const customAgents = {};
253
+ if (isObject(value.customAgents)) {
254
+ for (const [id, config] of Object.entries(value.customAgents)) {
255
+ customAgents[id] = readAgent(config, id);
256
+ }
257
+ }
258
+ if (value.proxy !== void 0 && !isObject(value.proxy)) {
259
+ throw new Error(`Invalid proxy settings: ${path}`);
260
+ }
261
+ const proxy = isObject(value.proxy) ? value.proxy : {};
262
+ const defaultConfig = proxy.default === void 0 ? { mode: "direct" } : readProxy(proxy.default, "default");
263
+ if (proxy.agents !== void 0 && !isObject(proxy.agents)) {
264
+ throw new Error(`Invalid agent proxy settings: ${path}`);
265
+ }
266
+ const agents2 = {};
267
+ if (isObject(proxy.agents)) {
268
+ for (const [agentId, config] of Object.entries(proxy.agents)) {
269
+ agents2[agentId] = readProxy(config, agentId);
270
+ }
271
+ }
272
+ return { customAgents, proxy: { default: defaultConfig, agents: agents2 } };
273
+ }
274
+ async function writeSettings(path, value) {
275
+ await mkdir2(dirname(path), { recursive: true });
276
+ await writeFile(path, `${JSON.stringify(value, null, 2)}
277
+ `, { encoding: "utf8", mode: 384 });
278
+ }
279
+ async function loadSettings(path = settingsFile()) {
280
+ return parseSettings(await readRaw(path), path);
281
+ }
282
+ function proxyMode(settings, agentId) {
283
+ return settings.proxy.agents[agentId]?.mode ?? settings.proxy.default.mode;
284
+ }
285
+ async function saveProxy(target, mode, path = settingsFile()) {
286
+ const value = await readRaw(path);
287
+ const current = parseSettings(value, path);
288
+ const agents2 = { ...current.proxy.agents };
289
+ let defaultConfig = current.proxy.default;
290
+ if (target === "all") {
291
+ defaultConfig = { mode };
292
+ for (const agentId of Object.keys(agents2)) {
293
+ delete agents2[agentId];
294
+ }
295
+ } else if (mode === defaultConfig.mode) {
296
+ delete agents2[target];
297
+ } else {
298
+ agents2[target] = { mode };
299
+ }
300
+ const next = { ...value, proxy: { default: defaultConfig, agents: agents2 } };
301
+ await writeSettings(path, next);
302
+ }
303
+ async function saveAgent(id, agent, path = settingsFile()) {
304
+ const value = await readRaw(path);
305
+ const current = parseSettings(value, path);
306
+ const customAgents = { ...current.customAgents, [id]: readAgent(agent, id) };
307
+ await writeSettings(path, { ...value, customAgents });
308
+ }
309
+ async function removeAgent(id, path = settingsFile()) {
310
+ const value = await readRaw(path);
311
+ const current = parseSettings(value, path);
312
+ const customAgents = { ...current.customAgents };
313
+ const proxyAgents = { ...current.proxy.agents };
314
+ delete customAgents[id];
315
+ delete proxyAgents[id];
316
+ await writeSettings(path, {
317
+ ...value,
318
+ customAgents,
319
+ proxy: { default: current.proxy.default, agents: proxyAgents }
320
+ });
321
+ }
322
+
323
+ // src/custom-menu.ts
324
+ function baseEnv(protocol) {
325
+ return protocol === "anthropic" ? "ANTHROPIC_BASE_URL" : "OPENAI_BASE_URL";
326
+ }
327
+ function required(value) {
328
+ return value.trim() ? true : "This field is required.";
329
+ }
330
+ function parseArgs(value) {
331
+ const args = JSON.parse(value);
332
+ if (!Array.isArray(args) || !args.every((arg) => typeof arg === "string")) {
333
+ throw new Error("Enter a JSON array of strings.");
334
+ }
335
+ return args;
336
+ }
337
+ async function askArgs(message, args, needsUrl = false) {
338
+ const value = await input({
339
+ message,
340
+ default: JSON.stringify(args),
341
+ validate: (text) => {
342
+ try {
343
+ const parsed = parseArgs(text);
344
+ if (needsUrl && !parsed.some((arg) => arg.includes("{baseUrl}"))) {
345
+ return "Include {baseUrl} in one argument.";
346
+ }
347
+ return true;
348
+ } catch {
349
+ return 'Enter a JSON array of strings, for example ["--profile","work"].';
350
+ }
351
+ }
352
+ });
353
+ return parseArgs(value);
354
+ }
355
+ function validUrl(value) {
356
+ try {
357
+ return ["http:", "https:"].includes(new URL(value).protocol) ? true : "Use an http:// or https:// URL.";
358
+ } catch {
359
+ return "Enter a valid upstream URL.";
360
+ }
361
+ }
362
+ async function askProtocol(current) {
363
+ return select({
364
+ message: "Protocol",
365
+ choices: [
366
+ { name: "Anthropic Messages", value: "anthropic" },
367
+ { name: "OpenAI Responses", value: "openai" }
368
+ ],
369
+ default: current
370
+ });
371
+ }
372
+ async function askInject(protocol, current) {
373
+ const type = await select({
374
+ message: "How should Tracelet pass its local Base URL?",
375
+ choices: [
376
+ { name: "Environment variable (recommended)", value: "env" },
377
+ { name: "Command arguments", value: "args" }
378
+ ],
379
+ default: current?.type ?? "env"
380
+ });
381
+ if (type === "env") {
382
+ const fallback = baseEnv(protocol);
383
+ const name = await input({
384
+ message: `Environment variable (blank uses ${fallback})`,
385
+ default: current?.type === "env" ? current.name ?? "" : "",
386
+ validate: (value) => !value || /^[A-Za-z_][A-Za-z0-9_]*$/.test(value) ? true : "Enter a valid environment variable name."
387
+ });
388
+ return { type: "env", ...name ? { name } : {} };
389
+ }
390
+ const args = await askArgs(
391
+ "Base URL arguments (JSON array containing {baseUrl})",
392
+ current?.type === "args" ? current.args : ["--base-url", "{baseUrl}"],
393
+ true
394
+ );
395
+ return { type: "args", args };
396
+ }
397
+ function nextId(label, used) {
398
+ const slug = label.toLowerCase().normalize("NFKD").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "agent";
399
+ const base = `custom-${slug}`;
400
+ let id = base;
401
+ let count = 2;
402
+ while (used[id]) {
403
+ id = `${base}-${count++}`;
404
+ }
405
+ return id;
406
+ }
407
+ function showAgent(id, agent) {
408
+ console.log(`
409
+ ${agent.label} (${id})`);
410
+ console.log(` Command: ${JSON.stringify([agent.command, ...agent.args])}`);
411
+ console.log(` Protocol: ${agent.protocol}`);
412
+ console.log(` Upstream: ${agent.upstream}`);
413
+ console.log(` Base URL: ${agent.inject.type === "env" ? `env ${agent.inject.name ?? baseEnv(agent.protocol)}` : `args ${JSON.stringify(agent.inject.args)}`}
414
+ `);
415
+ }
416
+ async function addAgent() {
417
+ const settings = await loadSettings();
418
+ const label = await input({ message: "Agent name", validate: required });
419
+ const id = nextId(label, settings.customAgents);
420
+ const command = await input({ message: "Executable command", validate: required });
421
+ const args = await askArgs("Default arguments (JSON array)", []);
422
+ const protocol = await askProtocol();
423
+ const inject = await askInject(protocol);
424
+ const envName = inject.type === "env" ? inject.name ?? baseEnv(protocol) : baseEnv(protocol);
425
+ const upstream = await input({
426
+ message: "Original upstream URL",
427
+ default: process.env[envName] ?? (protocol === "anthropic" ? "https://api.anthropic.com" : "https://api.openai.com/v1"),
428
+ validate: validUrl
429
+ });
430
+ const agent = { label, command, args, protocol, upstream, inject };
431
+ showAgent(id, agent);
432
+ if (!await confirm({ message: "Save this agent?", default: true })) {
433
+ return void 0;
434
+ }
435
+ await saveAgent(id, agent);
436
+ console.log(`Agent saved: ${id}`);
437
+ return await confirm({ message: "Launch it now?", default: false }) ? id : void 0;
438
+ }
439
+ async function chooseAgent(message) {
440
+ const entries = Object.entries((await loadSettings()).customAgents);
441
+ if (entries.length === 0) {
442
+ console.log("No custom agents configured.");
443
+ return void 0;
444
+ }
445
+ return select({
446
+ message,
447
+ choices: [
448
+ ...entries.map(([id, agent]) => ({ name: `${agent.label} (${id})`, value: id })),
449
+ { name: "Back", value: "back" }
450
+ ]
451
+ });
452
+ }
453
+ async function editAgent() {
454
+ const id = await chooseAgent("Select an agent to edit");
455
+ if (!id || id === "back") return;
456
+ const draft = structuredClone((await loadSettings()).customAgents[id]);
457
+ showAgent(id, draft);
458
+ for (; ; ) {
459
+ const field = await select({
460
+ message: "Select a field to edit",
461
+ // 一次展示全部字段,并在首尾停止方向键导航。
462
+ pageSize: 8,
463
+ loop: false,
464
+ choices: [
465
+ { name: `Name: ${draft.label}`, value: "label" },
466
+ { name: `Command: ${draft.command}`, value: "command" },
467
+ { name: `Default arguments: ${JSON.stringify(draft.args)}`, value: "args" },
468
+ { name: `Protocol: ${draft.protocol}`, value: "protocol" },
469
+ { name: `Upstream: ${draft.upstream}`, value: "upstream" },
470
+ { name: "Base URL injection", value: "inject" },
471
+ { name: "Save changes", value: "save" },
472
+ { name: "Cancel", value: "cancel" }
473
+ ]
474
+ });
475
+ if (field === "cancel") return;
476
+ if (field === "save") {
477
+ await saveAgent(id, draft);
478
+ console.log(`Agent updated: ${id}`);
479
+ return;
480
+ }
481
+ if (field === "label") draft.label = await input({ message: "Agent name", default: draft.label, validate: required });
482
+ if (field === "command") draft.command = await input({ message: "Executable command", default: draft.command, validate: required });
483
+ if (field === "args") draft.args = await askArgs("Default arguments (JSON array)", draft.args);
484
+ if (field === "protocol") draft.protocol = await askProtocol(draft.protocol);
485
+ if (field === "upstream") draft.upstream = await input({ message: "Original upstream URL", default: draft.upstream, validate: validUrl });
486
+ if (field === "inject") draft.inject = await askInject(draft.protocol, draft.inject);
487
+ }
488
+ }
489
+ async function deleteAgent() {
490
+ const id = await chooseAgent("Select an agent to delete");
491
+ if (!id || id === "back") return;
492
+ const agent = (await loadSettings()).customAgents[id];
493
+ if (!await confirm({ message: `Delete ${agent.label} (${id})? Recorded history will be kept.`, default: false })) {
494
+ console.log("Delete cancelled.");
495
+ return;
496
+ }
497
+ await removeAgent(id);
498
+ console.log(`Agent deleted: ${id}. Recorded history was kept.`);
499
+ }
500
+ async function manageAgents() {
501
+ if (!process.stdin.isTTY) throw new Error("The agent command requires an interactive terminal.");
502
+ for (; ; ) {
503
+ const action = await select({
504
+ message: "Manage custom agents",
505
+ choices: [
506
+ { name: "Add agent", value: "add" },
507
+ { name: "Edit agent", value: "edit" },
508
+ { name: "Delete agent", value: "delete" },
509
+ { name: "Back", value: "back" }
510
+ ]
511
+ });
512
+ if (action === "back") return void 0;
513
+ if (action === "add") {
514
+ const id = await addAgent();
515
+ if (id) return id;
516
+ }
517
+ if (action === "edit") await editAgent();
518
+ if (action === "delete") await deleteAgent();
519
+ }
520
+ }
521
+
158
522
  // ../../packages/shared/src/headers.ts
159
523
  var secrets = /* @__PURE__ */ new Set([
160
524
  "authorization",
@@ -229,8 +593,8 @@ function responseHeaders(headers) {
229
593
  }
230
594
 
231
595
  // ../../packages/proxy/src/target.ts
232
- function targetUrl(route, input) {
233
- const local = new URL(input, "http://127.0.0.1");
596
+ function targetUrl(route, input2) {
597
+ const local = new URL(input2, "http://127.0.0.1");
234
598
  const target = new URL(route.upstream);
235
599
  const suffix = local.pathname.slice(route.prefix.length).replace(/^\//, "");
236
600
  const basePath = target.pathname.replace(/\/$/, "");
@@ -1685,11 +2049,11 @@ function parseOutputFormat(params, content) {
1685
2049
  }
1686
2050
 
1687
2051
  // ../../node_modules/.pnpm/@anthropic-ai+sdk@0.125.0/node_modules/@anthropic-ai/sdk/_vendor/partial-json-parser/parser.mjs
1688
- var tokenize = (input) => {
2052
+ var tokenize = (input2) => {
1689
2053
  let current = 0;
1690
2054
  let tokens = [];
1691
- while (current < input.length) {
1692
- let char = input[current];
2055
+ while (current < input2.length) {
2056
+ let char = input2[current];
1693
2057
  if (char === "\\") {
1694
2058
  current++;
1695
2059
  continue;
@@ -1745,26 +2109,26 @@ var tokenize = (input) => {
1745
2109
  if (char === '"') {
1746
2110
  let value = "";
1747
2111
  let danglingQuote = false;
1748
- char = input[++current];
2112
+ char = input2[++current];
1749
2113
  while (char !== '"') {
1750
- if (current === input.length) {
2114
+ if (current === input2.length) {
1751
2115
  danglingQuote = true;
1752
2116
  break;
1753
2117
  }
1754
2118
  if (char === "\\") {
1755
2119
  current++;
1756
- if (current === input.length) {
2120
+ if (current === input2.length) {
1757
2121
  danglingQuote = true;
1758
2122
  break;
1759
2123
  }
1760
- value += char + input[current];
1761
- char = input[++current];
2124
+ value += char + input2[current];
2125
+ char = input2[++current];
1762
2126
  } else {
1763
2127
  value += char;
1764
- char = input[++current];
2128
+ char = input2[++current];
1765
2129
  }
1766
2130
  }
1767
- char = input[++current];
2131
+ char = input2[++current];
1768
2132
  if (!danglingQuote) {
1769
2133
  tokens.push({
1770
2134
  type: "string",
@@ -1783,13 +2147,13 @@ var tokenize = (input) => {
1783
2147
  let value = "";
1784
2148
  if (char === "-") {
1785
2149
  value += char;
1786
- char = input[++current];
2150
+ char = input2[++current];
1787
2151
  }
1788
2152
  while (char && (NUMBERS.test(char) || char === "." || // exponent marker, e.g. `1e10` or `1.5E-9`
1789
2153
  char === "e" || char === "E" || // exponent sign, only valid immediately after the exponent marker
1790
2154
  (char === "-" || char === "+") && (value[value.length - 1] === "e" || value[value.length - 1] === "E"))) {
1791
2155
  value += char;
1792
- char = input[++current];
2156
+ char = input2[++current];
1793
2157
  }
1794
2158
  tokens.push({
1795
2159
  type: "number",
@@ -1801,11 +2165,11 @@ var tokenize = (input) => {
1801
2165
  if (char && LETTERS.test(char)) {
1802
2166
  let value = "";
1803
2167
  while (char && LETTERS.test(char)) {
1804
- if (current === input.length) {
2168
+ if (current === input2.length) {
1805
2169
  break;
1806
2170
  }
1807
2171
  value += char;
1808
- char = input[++current];
2172
+ char = input2[++current];
1809
2173
  }
1810
2174
  if (value == "true" || value == "false" || value === "null") {
1811
2175
  tokens.push({
@@ -1904,7 +2268,7 @@ var generate = (tokens) => {
1904
2268
  });
1905
2269
  return output;
1906
2270
  };
1907
- var partialParse = (input) => JSON.parse(generate(unstrip(strip(tokenize(input)))));
2271
+ var partialParse = (input2) => JSON.parse(generate(unstrip(strip(tokenize(input2)))));
1908
2272
 
1909
2273
  // ../../node_modules/.pnpm/@anthropic-ai+sdk@0.125.0/node_modules/@anthropic-ai/sdk/internal/message-stream-utils.mjs
1910
2274
  var JSON_BUF_PROPERTY = "__json_buf";
@@ -1915,17 +2279,17 @@ function withLazyInput(prev, jsonBuf) {
1915
2279
  next[key] = prev[key];
1916
2280
  }
1917
2281
  Object.defineProperty(next, JSON_BUF_PROPERTY, { value: jsonBuf, enumerable: false, writable: true });
1918
- let input;
2282
+ let input2;
1919
2283
  let parsed = false;
1920
2284
  Object.defineProperty(next, "input", {
1921
2285
  enumerable: true,
1922
2286
  configurable: true,
1923
2287
  get() {
1924
2288
  if (!parsed) {
1925
- input = jsonBuf ? partialParse(jsonBuf) : {};
2289
+ input2 = jsonBuf ? partialParse(jsonBuf) : {};
1926
2290
  parsed = true;
1927
2291
  }
1928
- return input;
2292
+ return input2;
1929
2293
  }
1930
2294
  });
1931
2295
  return next;
@@ -6716,17 +7080,17 @@ var Recorder = class {
6716
7080
  this.onChange = onChange;
6717
7081
  }
6718
7082
  /** 开始捕获一次模型 HTTP 请求。 */
6719
- async start(input) {
7083
+ async start(input2) {
6720
7084
  const meta = {
6721
7085
  id: makeId("ex"),
6722
- runId: input.runId,
6723
- sessionId: `run:${input.runId}`,
7086
+ runId: input2.runId,
7087
+ sessionId: `run:${input2.runId}`,
6724
7088
  sessionSource: "run",
6725
- protocol: input.protocol,
6726
- method: input.method,
6727
- path: input.path,
7089
+ protocol: input2.protocol,
7090
+ method: input2.method,
7091
+ path: input2.path,
6728
7092
  stream: false,
6729
- requestHeaders: redactHeaders(input.headers),
7093
+ requestHeaders: redactHeaders(input2.headers),
6730
7094
  startedAt: nowIso(),
6731
7095
  requestBytes: 0,
6732
7096
  responseBytes: 0,
@@ -6748,12 +7112,12 @@ var Recorder = class {
6748
7112
  };
6749
7113
 
6750
7114
  // ../../packages/storage/src/reader.ts
6751
- import { readFile as readFile3, readdir } from "fs/promises";
7115
+ import { readFile as readFile4, readdir } from "fs/promises";
6752
7116
  import { join as join4 } from "path";
6753
7117
 
6754
7118
  // ../../packages/storage/src/files.ts
6755
- import { mkdir as mkdir2, readFile as readFile2, rename, writeFile } from "fs/promises";
6756
- import { dirname, join as join3 } from "path";
7119
+ import { mkdir as mkdir3, readFile as readFile3, rename, writeFile as writeFile2 } from "fs/promises";
7120
+ import { dirname as dirname2, join as join3 } from "path";
6757
7121
  function dateKey(iso) {
6758
7122
  return iso.slice(0, 10);
6759
7123
  }
@@ -6762,18 +7126,18 @@ function getRunDir(root, iso, runId) {
6762
7126
  }
6763
7127
  async function writeJson(path, value) {
6764
7128
  const temp = `${path}.tmp`;
6765
- await mkdir2(dirname(path), { recursive: true });
6766
- await writeFile(temp, `${JSON.stringify(value, null, 2)}
7129
+ await mkdir3(dirname2(path), { recursive: true });
7130
+ await writeFile2(temp, `${JSON.stringify(value, null, 2)}
6767
7131
  `, "utf8");
6768
7132
  await rename(temp, path);
6769
7133
  }
6770
7134
  async function readJson(path) {
6771
- return JSON.parse(await readFile2(path, "utf8"));
7135
+ return JSON.parse(await readFile3(path, "utf8"));
6772
7136
  }
6773
7137
  async function readJsonl(path) {
6774
7138
  let text = "";
6775
7139
  try {
6776
- text = await readFile2(path, "utf8");
7140
+ text = await readFile3(path, "utf8");
6777
7141
  } catch {
6778
7142
  return [];
6779
7143
  }
@@ -6835,6 +7199,28 @@ function contentText(value) {
6835
7199
  const parts = array(value).map((item) => record(item)).filter((item) => item?.type === "text" || item?.type === "input_text" || item?.type === "output_text").map((item) => string(item?.text)).filter((text) => typeof text === "string" && !isContext(text));
6836
7200
  return string(parts.join("\n\n"));
6837
7201
  }
7202
+ function promptText(value) {
7203
+ if (typeof value === "string") {
7204
+ return string(value);
7205
+ }
7206
+ const parts = array(value).map((item) => record(item)).filter((item) => item?.type === "text" || item?.type === "input_text" || item?.type === "output_text").map((item) => string(item?.text)).filter((text) => Boolean(text));
7207
+ return string(parts.join("\n\n"));
7208
+ }
7209
+ function systemText(exchange) {
7210
+ const body = record(exchange?.request);
7211
+ const parts = [promptText(body?.system), promptText(body?.instructions)].filter((text) => Boolean(text));
7212
+ for (const raw of array(body?.input)) {
7213
+ const item = record(raw);
7214
+ if (item?.type !== "message" || item?.role !== "system" && item?.role !== "developer") {
7215
+ continue;
7216
+ }
7217
+ const text = promptText(item?.content);
7218
+ if (text) {
7219
+ parts.push(text);
7220
+ }
7221
+ }
7222
+ return string(parts.join("\n\n"));
7223
+ }
6838
7224
  function reasoningText(value) {
6839
7225
  const summary = array(value?.summary).map((item) => string(record(item)?.text)).filter((text) => Boolean(text));
6840
7226
  return string(summary.join("\n\n")) ?? string(value?.thinking) ?? contentText(value?.content);
@@ -6931,13 +7317,13 @@ function codexItem(value) {
6931
7317
  }
6932
7318
  if (type.endsWith("_call")) {
6933
7319
  const callId = string(item?.call_id) ?? string(item?.id);
6934
- const input = item?.arguments ?? item?.input ?? item?.action;
7320
+ const input2 = item?.arguments ?? item?.input ?? item?.action;
6935
7321
  return [{
6936
7322
  ...callId ? { sourceId: callId, callId } : {},
6937
7323
  kind: "tool",
6938
7324
  role: "assistant",
6939
7325
  name: string(item?.name) ?? type.replace(/_call$/, ""),
6940
- input: json(input),
7326
+ input: json(input2),
6941
7327
  status: "pending"
6942
7328
  }];
6943
7329
  }
@@ -6964,10 +7350,10 @@ function turnMeta(exchange) {
6964
7350
  const metadata = record(json(encoded));
6965
7351
  const source = string(client?.thread_source) ?? string(metadata?.thread_source);
6966
7352
  const id = string(client?.turn_id) ?? string(metadata?.turn_id);
6967
- const texts = requestItems(exchange).filter((item) => item?.kind === "message").map((item) => item?.text ?? "").join("\n");
7353
+ const claudeInternal = exchange?.meta?.protocol === "anthropic" && isInternalText(requestItems(exchange).filter((item) => item?.kind === "message").map((item) => item?.text ?? "").join("\n"));
6968
7354
  return {
6969
7355
  ...id ? { id } : {},
6970
- internal: source === "system" || isInternalText(texts)
7356
+ internal: source === "system" || claudeInternal
6971
7357
  };
6972
7358
  }
6973
7359
  function fingerprint(item) {
@@ -7007,6 +7393,7 @@ var Builder = class {
7007
7393
  ids = /* @__PURE__ */ new Set();
7008
7394
  seen = /* @__PURE__ */ new Set();
7009
7395
  max = /* @__PURE__ */ new Map();
7396
+ systemPrompt;
7010
7397
  current;
7011
7398
  turnSeq = 0;
7012
7399
  itemSeq = 0;
@@ -7016,10 +7403,16 @@ var Builder = class {
7016
7403
  }
7017
7404
  /** 将一次 Exchange 合并到当前会话。 */
7018
7405
  add(exchange) {
7406
+ const prompt = systemText(exchange);
7407
+ if (prompt && !this.systemPrompt) {
7408
+ this.systemPrompt = { text: prompt, exchangeIds: [exchange.meta.id] };
7409
+ } else if (prompt && this.systemPrompt?.text === prompt) {
7410
+ this.link(this.systemPrompt.exchangeIds, exchange.meta.id);
7411
+ }
7019
7412
  const meta = turnMeta(exchange);
7020
- const input = this.snapshot(requestItems(exchange));
7021
- const userIndex = lastUser(input);
7022
- input.forEach((item, index) => {
7413
+ const input2 = this.snapshot(requestItems(exchange));
7414
+ const userIndex = lastUser(input2);
7415
+ input2.forEach((item, index) => {
7023
7416
  const newTurn = item?.kind === "message" && item?.role === "user";
7024
7417
  const explicit = newTurn && index === userIndex ? meta.id : void 0;
7025
7418
  const turn = newTurn ? this.turn(explicit, meta.internal, exchange, true) : this.turn(meta.id, meta.internal, exchange, false);
@@ -7040,6 +7433,7 @@ var Builder = class {
7040
7433
  sessionId: this.session.id,
7041
7434
  protocol: this.session.protocol,
7042
7435
  ...model ? { model } : {},
7436
+ ...this.systemPrompt ? { systemPrompt: this.systemPrompt } : {},
7043
7437
  startedAt: this.session.startedAt,
7044
7438
  ...this.session.endedAt ? { endedAt: this.session.endedAt } : {},
7045
7439
  exchangeIds: this.session.exchanges.map((item) => item.id),
@@ -7219,12 +7613,19 @@ async function scan(root) {
7219
7613
  for (const date of await readDirs(runsRoot)) {
7220
7614
  const dateDir = join4(runsRoot, date);
7221
7615
  for (const run of await readDirs(dateDir)) {
7616
+ const runDir = join4(dateDir, run);
7617
+ let agentLabel;
7618
+ try {
7619
+ const info = await readJson(join4(runDir, "run.json"));
7620
+ agentLabel = info.agentLabel ?? (info.agent === "claude" ? "Claude Code" : info.agent === "codex" ? "Codex" : info.agent);
7621
+ } catch {
7622
+ }
7222
7623
  const exchangesDir = join4(dateDir, run, "exchanges");
7223
7624
  for (const exchange of await readDirs(exchangesDir)) {
7224
7625
  const dir = join4(exchangesDir, exchange);
7225
7626
  try {
7226
7627
  const meta = await readJson(join4(dir, "meta.json"));
7227
- result.push({ dir, meta: normalizeMeta(meta) });
7628
+ result.push({ dir, meta: normalizeMeta(meta), ...agentLabel ? { agentLabel } : {} });
7228
7629
  } catch {
7229
7630
  }
7230
7631
  }
@@ -7266,6 +7667,7 @@ async function listSessions(root) {
7266
7667
  groups.set(item.meta.sessionId, {
7267
7668
  id: item.meta.sessionId,
7268
7669
  protocol: item.meta.protocol,
7670
+ ...item.agentLabel ? { agentLabel: item.agentLabel } : {},
7269
7671
  startedAt: item.meta.startedAt,
7270
7672
  ...item.meta.completedAt ? { endedAt: item.meta.completedAt } : {},
7271
7673
  ...isInternal(item.meta) ? { internal: true } : {},
@@ -7276,8 +7678,8 @@ async function listSessions(root) {
7276
7678
  }
7277
7679
  async function readExchange(stored) {
7278
7680
  const [requestBody, responseBody, chunkRows, events] = await Promise.all([
7279
- readFile3(join4(stored.dir, "request.bin")),
7280
- readFile3(join4(stored.dir, "response.bin")),
7681
+ readFile4(join4(stored.dir, "request.bin")),
7682
+ readFile4(join4(stored.dir, "response.bin")),
7281
7683
  readJsonl(join4(stored.dir, "response-chunks.jsonl")),
7282
7684
  readJsonl(join4(stored.dir, "sse-events.jsonl"))
7283
7685
  ]);
@@ -7343,11 +7745,11 @@ async function getConversation(root, sessionId) {
7343
7745
  }
7344
7746
 
7345
7747
  // ../../packages/storage/src/store.ts
7346
- import { mkdir as mkdir4 } from "fs/promises";
7748
+ import { mkdir as mkdir5 } from "fs/promises";
7347
7749
  import { join as join6 } from "path";
7348
7750
 
7349
7751
  // ../../packages/storage/src/writer.ts
7350
- import { mkdir as mkdir3, open } from "fs/promises";
7752
+ import { mkdir as mkdir4, open } from "fs/promises";
7351
7753
  import { join as join5 } from "path";
7352
7754
  var ExchangeWriter = class {
7353
7755
  dir;
@@ -7370,7 +7772,7 @@ var ExchangeWriter = class {
7370
7772
  }
7371
7773
  /** 创建目录并打开全部追加文件。 */
7372
7774
  async init() {
7373
- await mkdir3(this.dir, { recursive: true });
7775
+ await mkdir4(this.dir, { recursive: true });
7374
7776
  [this.req, this.reqIndex, this.res, this.resIndex, this.eventFile] = await Promise.all([
7375
7777
  open(join5(this.dir, "request.bin"), "a"),
7376
7778
  open(join5(this.dir, "request-chunks.jsonl"), "a"),
@@ -7463,7 +7865,7 @@ var FileStore = class {
7463
7865
  }
7464
7866
  /** 初始化存储根目录。 */
7465
7867
  async init() {
7466
- await mkdir4(join6(this.root, "runs"), { recursive: true });
7868
+ await mkdir5(join6(this.root, "runs"), { recursive: true });
7467
7869
  }
7468
7870
  /** 创建一次运行记录并返回其目录。 */
7469
7871
  async startRun(run) {
@@ -7559,7 +7961,7 @@ data: ${JSON.stringify(data)}
7559
7961
  };
7560
7962
 
7561
7963
  // ../../packages/server/src/static.ts
7562
- import { readFile as readFile4, stat } from "fs/promises";
7964
+ import { readFile as readFile5, stat } from "fs/promises";
7563
7965
  import { extname, resolve as resolve3, sep } from "path";
7564
7966
  var contentTypes = {
7565
7967
  ".css": "text/css; charset=utf-8",
@@ -7588,7 +7990,7 @@ async function serveStatic(root, pathname, res) {
7588
7990
  target = resolve3(base, "index.html");
7589
7991
  }
7590
7992
  try {
7591
- const body = await readFile4(target);
7993
+ const body = await readFile5(target);
7592
7994
  res.writeHead(200, {
7593
7995
  "content-type": contentTypes?.[extname(target)] ?? "application/octet-stream",
7594
7996
  "cache-control": target.endsWith("index.html") ? "no-cache" : "public, max-age=31536000, immutable"
@@ -7702,53 +8104,12 @@ var TraceletServer = class {
7702
8104
  }
7703
8105
  };
7704
8106
 
7705
- // src/settings.ts
7706
- import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile2 } from "fs/promises";
7707
- import { dirname as dirname2 } from "path";
7708
- function isObject(value) {
7709
- return value !== null && typeof value === "object" && !Array.isArray(value);
7710
- }
7711
- async function readRaw(path) {
7712
- try {
7713
- const value = JSON.parse(await readFile5(path, "utf8"));
7714
- if (!isObject(value)) {
7715
- throw new Error("Settings root must be an object");
7716
- }
7717
- return value;
7718
- } catch (error) {
7719
- if (error?.code === "ENOENT") {
7720
- return {};
7721
- }
7722
- throw new Error(`Failed to read Tracelet settings: ${path}`, { cause: error });
7723
- }
7724
- }
7725
- async function loadSettings(path = settingsFile()) {
7726
- const value = await readRaw(path);
7727
- const proxy = value.proxy;
7728
- if (proxy !== void 0 && !isObject(proxy)) {
7729
- throw new Error(`Invalid proxy settings: ${path}`);
7730
- }
7731
- const mode = isObject(proxy) ? proxy.mode : void 0;
7732
- if (mode !== void 0 && mode !== "direct" && mode !== "system") {
7733
- throw new Error(`Invalid proxy mode: ${String(mode)}`);
7734
- }
7735
- return { proxy: { mode: mode ?? "direct" } };
7736
- }
7737
- async function saveProxy(mode, path = settingsFile()) {
7738
- const value = await readRaw(path);
7739
- const proxy = isObject(value.proxy) ? value.proxy : {};
7740
- const next = { ...value, proxy: { ...proxy, mode } };
7741
- await mkdir5(dirname2(path), { recursive: true });
7742
- await writeFile2(path, `${JSON.stringify(next, null, 2)}
7743
- `, { encoding: "utf8", mode: 384 });
7744
- }
7745
-
7746
8107
  // src/spawn.ts
7747
8108
  import { spawn } from "child_process";
7748
- async function spawnAgent(input) {
8109
+ async function spawnAgent(input2) {
7749
8110
  return new Promise((resolve4, reject) => {
7750
- const child = spawn(input.command, input.args, {
7751
- env: input.env,
8111
+ const child = spawn(input2.command, input2.args, {
8112
+ env: input2.env,
7752
8113
  stdio: "inherit"
7753
8114
  });
7754
8115
  function onSigint() {
@@ -7783,12 +8144,12 @@ function lineValue(output, key) {
7783
8144
  return output.match(new RegExp(`^\\s*${key}\\s*:\\s*(.+?)\\s*$`, "m"))?.[1];
7784
8145
  }
7785
8146
  function proxyUrl(value) {
7786
- const input = value?.trim();
7787
- if (!input) {
8147
+ const input2 = value?.trim();
8148
+ if (!input2) {
7788
8149
  return void 0;
7789
8150
  }
7790
8151
  try {
7791
- const url = new URL(/^[a-z]+:\/\//i.test(input) ? input : `http://${input}`);
8152
+ const url = new URL(/^[a-z]+:\/\//i.test(input2) ? input2 : `http://${input2}`);
7792
8153
  return url.protocol === "http:" || url.protocol === "https:" ? url.toString().replace(/\/$/, "") : void 0;
7793
8154
  } catch {
7794
8155
  return void 0;
@@ -7844,13 +8205,17 @@ async function systemProxy(platform = process.platform) {
7844
8205
 
7845
8206
  // src/run.ts
7846
8207
  async function runAgent(agentId, args, options) {
7847
- const adapter = agents[agentId];
8208
+ const settings = await loadSettings();
8209
+ const adapter = agentId === "claude" || agentId === "codex" ? agents[agentId] : settings.customAgents[agentId] ? customAdapter(agentId, settings.customAgents[agentId]) : void 0;
8210
+ if (!adapter) {
8211
+ throw new Error(`Unknown agent: ${agentId}`);
8212
+ }
7848
8213
  if (!await adapter.detect()) {
7849
8214
  throw new Error(`${adapter.label} command not found: ${adapter.command}`);
7850
8215
  }
7851
- const settings = await loadSettings();
7852
- const proxy = settings.proxy.mode === "system" ? await systemProxy() : void 0;
7853
- if (settings.proxy.mode === "system" && !proxy) {
8216
+ const mode = proxyMode(settings, agentId);
8217
+ const proxy = mode === "system" ? await systemProxy() : void 0;
8218
+ if (mode === "system" && !proxy) {
7854
8219
  console.warn("System proxy not found. Using a direct connection.");
7855
8220
  }
7856
8221
  const server = new TraceletServer({
@@ -7861,6 +8226,7 @@ async function runAgent(agentId, args, options) {
7861
8226
  const run = {
7862
8227
  id: makeId("run"),
7863
8228
  agent: adapter.id,
8229
+ agentLabel: adapter.label,
7864
8230
  cwd: process.cwd(),
7865
8231
  command: adapter.command,
7866
8232
  startedAt: nowIso()
@@ -7895,6 +8261,9 @@ async function runDashboard(options) {
7895
8261
  }
7896
8262
 
7897
8263
  // src/program.ts
8264
+ var { version } = JSON.parse(
8265
+ readFileSync(new URL("../package.json", import.meta.url), "utf8")
8266
+ );
7898
8267
  function parsePort(value) {
7899
8268
  const port = Number.parseInt(value, 10);
7900
8269
  if (!Number.isInteger(port) || port < 0 || port > 65535) {
@@ -7911,28 +8280,47 @@ function getOptions(program) {
7911
8280
  }
7912
8281
  async function interactive(program) {
7913
8282
  if (!process.stdin.isTTY) {
7914
- throw new Error("Use tracelet claude or tracelet codex in a non-interactive environment.");
8283
+ throw new Error("Use tracelet claude, tracelet codex, or tracelet run <id> in a non-interactive environment.");
7915
8284
  }
7916
- const [claudeReady, codexReady] = await Promise.all([
7917
- agents.claude.detect(),
7918
- agents.codex.detect()
7919
- ]);
7920
- const agent = await select({
7921
- message: "Select an agent to trace",
7922
- choices: [
7923
- {
7924
- name: "Claude Code",
7925
- value: "claude",
7926
- ...!claudeReady ? { disabled: "claude command not found" } : {}
7927
- },
7928
- {
7929
- name: "Codex",
7930
- value: "codex",
7931
- ...!codexReady ? { disabled: "codex command not found" } : {}
8285
+ for (; ; ) {
8286
+ const settings = await loadSettings();
8287
+ const [claudeReady, codexReady] = await Promise.all([
8288
+ agents.claude.detect(),
8289
+ agents.codex.detect()
8290
+ ]);
8291
+ const custom = await Promise.all(Object.entries(settings.customAgents).map(async ([id, config]) => ({
8292
+ name: config.label,
8293
+ value: id,
8294
+ ...!await customAdapter(id, config).detect() ? { disabled: `${config.command} command not found` } : {}
8295
+ })));
8296
+ const agent = await select2({
8297
+ message: "Select an agent to trace",
8298
+ choices: [
8299
+ {
8300
+ name: "Claude Code",
8301
+ value: "claude",
8302
+ ...!claudeReady ? { disabled: "claude command not found" } : {}
8303
+ },
8304
+ {
8305
+ name: "Codex",
8306
+ value: "codex",
8307
+ ...!codexReady ? { disabled: "codex command not found" } : {}
8308
+ },
8309
+ ...custom,
8310
+ { name: "Manage custom agents...", value: "manage" }
8311
+ ]
8312
+ });
8313
+ if (agent === "manage") {
8314
+ const launch = await manageAgents();
8315
+ if (launch) {
8316
+ process.exitCode = await runAgent(launch, [], getOptions(program));
8317
+ return;
7932
8318
  }
7933
- ]
7934
- });
7935
- process.exitCode = await runAgent(agent, [], getOptions(program));
8319
+ continue;
8320
+ }
8321
+ process.exitCode = await runAgent(agent, [], getOptions(program));
8322
+ return;
8323
+ }
7936
8324
  }
7937
8325
  async function clearRecords(program, yes) {
7938
8326
  const root = dataDir(getOptions(program).dataDir);
@@ -7940,7 +8328,7 @@ async function clearRecords(program, yes) {
7940
8328
  if (!process.stdin.isTTY) {
7941
8329
  throw new Error("Use tracelet clear --yes in a non-interactive environment.");
7942
8330
  }
7943
- const accepted = await confirm({
8331
+ const accepted = await confirm2({
7944
8332
  message: `This will permanently delete all Tracelet records in ${root}. Continue?`,
7945
8333
  default: false
7946
8334
  });
@@ -7956,8 +8344,27 @@ async function configureProxy() {
7956
8344
  if (!process.stdin.isTTY) {
7957
8345
  throw new Error("The proxy command requires an interactive terminal.");
7958
8346
  }
7959
- const current = (await loadSettings()).proxy.mode;
7960
- const mode = await select({
8347
+ const settings = await loadSettings();
8348
+ const target = await select2({
8349
+ message: "Select an agent to configure",
8350
+ choices: [
8351
+ {
8352
+ name: `Claude Code (${proxyMode(settings, "claude") === "system" ? "On" : "Off"})`,
8353
+ value: "claude"
8354
+ },
8355
+ {
8356
+ name: `Codex (${proxyMode(settings, "codex") === "system" ? "On" : "Off"})`,
8357
+ value: "codex"
8358
+ },
8359
+ { name: "All agents", value: "all" },
8360
+ ...Object.entries(settings.customAgents).map(([id, agent]) => ({
8361
+ name: `${agent.label} (${proxyMode(settings, id) === "system" ? "On" : "Off"})`,
8362
+ value: id
8363
+ }))
8364
+ ]
8365
+ });
8366
+ const current = target === "all" ? settings.proxy.default.mode : proxyMode(settings, target);
8367
+ const mode = await select2({
7961
8368
  message: "Use the system proxy for upstream requests?",
7962
8369
  choices: [
7963
8370
  { name: "On", value: "system" },
@@ -7965,17 +8372,25 @@ async function configureProxy() {
7965
8372
  ],
7966
8373
  default: current
7967
8374
  });
7968
- await saveProxy(mode);
7969
- console.log(mode === "system" ? "System proxy enabled." : "System proxy disabled.");
8375
+ await saveProxy(target, mode);
8376
+ const label = target === "all" ? "all agents" : target === "claude" || target === "codex" ? agents[target].label : settings.customAgents[target]?.label ?? target;
8377
+ console.log(`System proxy ${mode === "system" ? "enabled" : "disabled"} for ${label}.`);
7970
8378
  }
7971
8379
  function createProgram() {
7972
8380
  const program = new Command();
7973
- program.name("tracelet").description("Record LLM requests and streaming responses from Claude Code and Codex").version("0.1.0").enablePositionalOptions().addOption(new Option("-p, --port <port>", "Local server port").default(4318).argParser(parsePort)).option("--data-dir <path>", "Local trace directory").action(() => interactive(program));
8381
+ program.name("tracelet").description("Record LLM requests and streaming responses from built-in and custom agents").version(version).enablePositionalOptions().addOption(new Option("-p, --port <port>", "Local server port").default(4318).argParser(parsePort)).option("--data-dir <path>", "Local trace directory").action(() => interactive(program));
7974
8382
  for (const agentId of ["claude", "codex"]) {
7975
8383
  program.command(`${agentId} [args...]`).description(`Start and trace ${agents[agentId].label}`).allowUnknownOption().passThroughOptions().action(async (args) => {
7976
8384
  process.exitCode = await runAgent(agentId, args, getOptions(program));
7977
8385
  });
7978
8386
  }
8387
+ program.command("agent").description("Add, edit, or delete custom agents").action(async () => {
8388
+ const launch = await manageAgents();
8389
+ if (launch) process.exitCode = await runAgent(launch, [], getOptions(program));
8390
+ });
8391
+ program.command("run <id> [args...]").description("Start and trace a custom agent by ID").allowUnknownOption().passThroughOptions().action(async (id, args) => {
8392
+ process.exitCode = await runAgent(id, args, getOptions(program));
8393
+ });
7979
8394
  program.command("dashboard").description("View local Tracelet history").action(() => runDashboard(getOptions(program)));
7980
8395
  program.command("proxy").description("Configure system proxy usage").action(configureProxy);
7981
8396
  const clear = program.command("clear").description("Clear all local Tracelet history").option("-y, --yes", "Skip confirmation");