0sec-cli 0.22.1 → 0.22.4

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.
Files changed (107) hide show
  1. package/0.js +56 -45
  2. package/README.md +12 -0
  3. package/chunks/{adapt-loop-J6QCZZTB.js → adapt-loop-3CCOWE43.js} +4 -4
  4. package/chunks/{artifact-scraper-MFKT6OI4.js → artifact-scraper-Z4UUIOH3.js} +2 -2
  5. package/chunks/{assumption-mining-646LYSJY.js → assumption-mining-2UAKQB6V.js} +18 -14
  6. package/chunks/{chunk-OHHCGDJ2.js → chunk-2WWAPKAW.js} +126 -1089
  7. package/chunks/{chunk-XDKB2P5X.js → chunk-3BPX75OR.js} +1 -1
  8. package/chunks/chunk-4ZYYX5GY.js +74 -0
  9. package/chunks/{chunk-3E2KALZ5.js → chunk-5JNTMWPK.js} +2 -2
  10. package/chunks/{chunk-CPWA6RWE.js → chunk-5WAJGLCW.js} +3 -3
  11. package/chunks/{chunk-KAU6NLGP.js → chunk-6KFB6AYN.js} +515 -20
  12. package/chunks/{chunk-XA2O4WXA.js → chunk-6WQDXWQC.js} +169 -15
  13. package/chunks/chunk-6YZE552K.js +5603 -0
  14. package/chunks/chunk-BGX53365.js +309 -0
  15. package/chunks/{chunk-X7LAEEUW.js → chunk-C6JPA32H.js} +1 -1
  16. package/chunks/chunk-CF3MNS3O.js +990 -0
  17. package/chunks/{chunk-4VGLO2YS.js → chunk-CPCHLL6A.js} +5 -2
  18. package/chunks/{chunk-ZKQ2I3S2.js → chunk-DPUXXMJW.js} +1 -1
  19. package/chunks/{chunk-XCISCI4T.js → chunk-E3OOBVOK.js} +30 -12
  20. package/chunks/{chunk-4G7OVYJX.js → chunk-E74LXQFY.js} +1 -1
  21. package/chunks/{chunk-OSBPGWTE.js → chunk-ELI3BVIK.js} +3 -3
  22. package/chunks/chunk-FFHRANYC.js +547 -0
  23. package/chunks/{chunk-WSDDGFA6.js → chunk-G2BE2FC7.js} +1 -1
  24. package/chunks/chunk-GQ3DGHWE.js +1659 -0
  25. package/chunks/{chunk-V36HVTUL.js → chunk-GVY3S5E5.js} +21 -18
  26. package/chunks/{chunk-5BTJMINS.js → chunk-H4DH7COV.js} +192 -40
  27. package/chunks/{chunk-HHPWRY2M.js → chunk-H6Q2QAIY.js} +23475 -22747
  28. package/chunks/{chunk-7XCKYNLO.js → chunk-IHPTOTLL.js} +1 -1
  29. package/chunks/chunk-IXAHQICR.js +31 -0
  30. package/chunks/{chunk-TH2LW437.js → chunk-KCA35IL3.js} +2 -190
  31. package/chunks/{chunk-VWITLWXB.js → chunk-L2CCO4KE.js} +282 -34
  32. package/chunks/{chunk-6YNKRUMC.js → chunk-NJLKFC66.js} +321 -5
  33. package/chunks/{chunk-W6UWZFKL.js → chunk-O6KPVYOI.js} +100 -5
  34. package/chunks/{chunk-LGCASF2C.js → chunk-OAS5SAVZ.js} +6767 -2100
  35. package/chunks/chunk-PS5E7LUQ.js +277 -0
  36. package/chunks/{chunk-AV2LG2L7.js → chunk-QVIRWJ45.js} +257 -145
  37. package/chunks/{chunk-TV27OWIW.js → chunk-QWK4R3DB.js} +9 -9
  38. package/chunks/{chunk-WZRVS7SB.js → chunk-RCRTSKQN.js} +1 -1
  39. package/chunks/chunk-RS4MAN3Q.js +73 -0
  40. package/chunks/{chunk-V6GHX34G.js → chunk-RTBZ36BD.js} +8 -274
  41. package/chunks/{chunk-YL6YAGLL.js → chunk-SCWWLABR.js} +1 -1
  42. package/chunks/chunk-VDOWARMJ.js +63 -0
  43. package/chunks/{chunk-IFME676H.js → chunk-WBRGXCU2.js} +79 -18
  44. package/chunks/{chunk-IVQV574Y.js → chunk-WHUAUFSX.js} +1 -1
  45. package/chunks/{chunk-GOEA3NW6.js → chunk-XODDLNBI.js} +1534 -1305
  46. package/chunks/chunk-YAZARMJY.js +355 -0
  47. package/chunks/{chunk-NLH4LMWH.js → chunk-YED25PA4.js} +1 -1
  48. package/chunks/{codex-models-NHQW45SO.js → codex-models-Z43V3427.js} +9 -3
  49. package/chunks/{commands-N6GN5SBF.js → commands-EUGF4FUZ.js} +5842 -4093
  50. package/chunks/{db-5IWEJUYV.js → db-NVAW4WWR.js} +3 -3
  51. package/chunks/{dist-L3XJWWZ6.js → dist-27BNG6WD.js} +193 -112
  52. package/chunks/{dist-FSS7MBDF.js → dist-7RUOKPRJ.js} +12 -2
  53. package/chunks/{dist-KEULE5IQ.js → dist-LNXGATR2.js} +2 -2
  54. package/chunks/{dist-XZB53PS5.js → dist-M2X367VW.js} +2 -2
  55. package/chunks/{dist-BS4CY4RG.js → dist-NG2HT5XU.js} +27 -1
  56. package/chunks/{eval-runner-EMEGF24P.js → eval-runner-6Q3H52SP.js} +13 -9
  57. package/chunks/{exploit-agent-KW4EB7BE.js → exploit-agent-FCCR2HYX.js} +1 -1
  58. package/chunks/{exploit-autoclimb-CXJ2ERRO.js → exploit-autoclimb-X5ZS5FAO.js} +1 -1
  59. package/chunks/{exploit-climb-TJWLY4XC.js → exploit-climb-3JHGEO36.js} +2 -2
  60. package/chunks/{fix-UUKPXCFB.js → fix-NSNPU33B.js} +12 -6
  61. package/chunks/{harness-C5HOKIEV.js → harness-KXQOVIDQ.js} +3 -3
  62. package/chunks/{hunt-scan-KZZ444V2.js → hunt-scan-ELTKGKC4.js} +16 -12
  63. package/chunks/{kernel-primitive-Z77IR7YS.js → kernel-primitive-MT7YZSIG.js} +2 -2
  64. package/chunks/{kernel-vm-runner-JW77P276.js → kernel-vm-runner-YFRHYC6G.js} +2 -2
  65. package/chunks/{native-loop-P6P5GYVJ.js → native-loop-OFJDRHPI.js} +18 -8
  66. package/chunks/orchestrate-DRZ2SVCP.js +62 -0
  67. package/chunks/{prepare-KYHGFWCJ.js → prepare-72GWTRVI.js} +6 -2
  68. package/chunks/{process-N245QIQG.js → process-MJYJ2K5E.js} +2 -1
  69. package/chunks/{run-GAFKBCTN.js → run-WTJCZZHV.js} +1901 -4816
  70. package/chunks/{runtime-H7YBR5IZ.js → runtime-FCKOUB7V.js} +10 -4
  71. package/chunks/{runtime-XFWJTBUO.js → runtime-LCJ4XSKE.js} +5 -1
  72. package/chunks/{scan-stream-BMOCPKQX.js → scan-stream-GT6GKQ3U.js} +7 -1
  73. package/chunks/{scope-QWY4VQZ7.js → scope-E5QZUOOH.js} +2 -2
  74. package/chunks/service-plugins-MSK2EQP6.js +62 -0
  75. package/chunks/{session-store-XIDFGG7K.js → session-store-H3Q7KCYG.js} +6 -4
  76. package/chunks/{variant-candidates-7D4F5E4H.js → variant-candidates-SZKDCOLK.js} +9 -3
  77. package/chunks/{web-recon-prepass-VXXBPDAV.js → web-recon-prepass-IO3GXQMW.js} +11 -6
  78. package/dashboard/apple-touch-icon.png +0 -0
  79. package/dashboard/assets/operations-BvB5Qpmk.css +1 -0
  80. package/dashboard/assets/operations-D8Ii01SV.js +114 -0
  81. package/dashboard/assets/zero-peek-Dzm3MT3X.png +0 -0
  82. package/dashboard/favicon.ico +0 -0
  83. package/dashboard/favicon.svg +10 -0
  84. package/dashboard/index.html +5 -3
  85. package/package.json +1 -1
  86. package/chunks/chunk-6GHSR47F.js +0 -147
  87. package/chunks/chunk-BCSRRQG5.js +0 -3514
  88. package/chunks/chunk-BOUGKEKA.js +0 -193
  89. package/chunks/chunk-GHWODZMR.js +0 -16
  90. package/chunks/orchestrate-DWWUT5RC.js +0 -58
  91. package/dashboard/assets/bot-Bg1Tp9_d.js +0 -1
  92. package/dashboard/assets/dist-B_dDCPbu.js +0 -1
  93. package/dashboard/assets/findings-page-mayGhNx6.js +0 -5
  94. package/dashboard/assets/format--susmHX7.js +0 -1
  95. package/dashboard/assets/input-TuVPR3Zl.js +0 -1
  96. package/dashboard/assets/jsx-runtime-MGseXjj5.js +0 -3
  97. package/dashboard/assets/live-page-HWWHX99U.js +0 -1
  98. package/dashboard/assets/meta-tile-ckHo-w7k.js +0 -1
  99. package/dashboard/assets/operations-BxZ0OoXO.js +0 -10
  100. package/dashboard/assets/operations-CMFVrXEe.css +0 -2
  101. package/dashboard/assets/operations-app-D3d35rgo.js +0 -46
  102. package/dashboard/assets/overview-page-W_4IteEr.js +0 -1
  103. package/dashboard/assets/page-header-Bw9AJgBV.js +0 -1
  104. package/dashboard/assets/scans-page-CgzVXHnn.js +0 -1
  105. package/dashboard/assets/siren-KQj8QAnJ.js +0 -1
  106. package/dashboard/assets/table-8EDklulN.js +0 -1
  107. package/dashboard/assets/tabs-CFHPOwg8.js +0 -1
@@ -1,122 +1,987 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire as __0CreateRequire } from "node:module";
3
3
  const require = __0CreateRequire(import.meta.url);
4
+ import {
5
+ createConsoleRuntime,
6
+ createWorkbenchProviderBroker,
7
+ runSmolvmWorkbench,
8
+ snapshotSmolvmWorkspace
9
+ } from "./chunk-OAS5SAVZ.js";
10
+ import {
11
+ TOOL_DEFINITIONS
12
+ } from "./chunk-H6Q2QAIY.js";
4
13
  import {
5
14
  ANALYTICS_LEVEL_ENV,
6
- analyticsPipeline,
15
+ getSmolvmWorkbenchStatus,
16
+ isAdmittedSmolvmWorkbench,
7
17
  levelAtLeast,
8
18
  loadCloudCredentials,
9
19
  resolveAnalyticsLevel
10
- } from "./chunk-OHHCGDJ2.js";
20
+ } from "./chunk-2WWAPKAW.js";
21
+ import {
22
+ ScopePolicy,
23
+ aggregateCapabilities,
24
+ emptyEnablement,
25
+ getScopeEnforcementState,
26
+ listInstalledPluginIds,
27
+ loadableIds,
28
+ pluginsRootDir,
29
+ readEnablement,
30
+ readInstalledPlugin,
31
+ reconcile,
32
+ writeEnablement
33
+ } from "./chunk-C6JPA32H.js";
11
34
  import {
12
35
  DEFAULT_ALLOW_MODEL_SELF_EXTENSION,
13
- createPresentationEvent,
36
+ SERVICE_PLUGIN_CATALOG,
37
+ VERSION,
38
+ findingSchema,
14
39
  homeStateDir
15
- } from "./chunk-KAU6NLGP.js";
40
+ } from "./chunk-6KFB6AYN.js";
16
41
 
17
- // packages/cli/src/tui/settings-store.ts
18
- import { useSyncExternalStore } from "react";
42
+ // packages/cli/src/workbench.ts
43
+ import { chmodSync, existsSync as existsSync2, lstatSync as lstatSync2, mkdirSync as mkdirSync4, readFileSync as readFileSync3, realpathSync as realpathSync3, renameSync as renameSync2, writeFileSync as writeFileSync3 } from "node:fs";
44
+ import { homedir as homedir2 } from "node:os";
45
+ import { dirname as dirname5, isAbsolute as isAbsolute2, join as join8 } from "node:path";
19
46
 
20
- // packages/cli/src/tui/settings.ts
21
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
47
+ // packages/cli/src/workbench-assets.ts
48
+ import { existsSync, lstatSync, realpathSync } from "node:fs";
49
+ import { dirname, join } from "node:path";
50
+ import { fileURLToPath } from "node:url";
51
+ function currentWorkbenchAssets(moduleUrl = import.meta.url) {
52
+ let directory = dirname(fileURLToPath(moduleUrl));
53
+ for (let depth = 0; depth < 5; depth++) {
54
+ for (const candidate of [directory, join(directory, "dist")]) {
55
+ const entry = join(candidate, "0.js");
56
+ if (existsSync(entry) && lstatSync(entry).isFile() && !lstatSync(entry).isSymbolicLink()) return { cliDist: realpathSync(candidate) };
57
+ }
58
+ const parent = dirname(directory);
59
+ if (parent === directory) break;
60
+ directory = parent;
61
+ }
62
+ throw new Error("Current workbench CLI distribution is unavailable. Build/install the current CLI bundle before isolated execution; an older image CLI is not used.");
63
+ }
64
+
65
+ // packages/cli/src/workbench-console-protocol.ts
66
+ import { isAbsolute, relative, resolve, sep } from "node:path";
67
+ var WORKBENCH_FRAME_BYTES = 8 * 1024 * 1024;
68
+ var WORKBENCH_MAX_PENDING = 64;
69
+ function encodeWorkbenchFrame(frame) {
70
+ const encoded = JSON.stringify(frame);
71
+ if (Buffer.byteLength(encoded) > WORKBENCH_FRAME_BYTES) throw new Error("Workbench protocol frame exceeds its byte limit");
72
+ return encoded + "\n";
73
+ }
74
+ var WorkbenchFrameReader = class {
75
+ #pending = "";
76
+ push(data, receive) {
77
+ this.#pending += data;
78
+ let newline;
79
+ while ((newline = this.#pending.indexOf("\n")) !== -1) {
80
+ const line = this.#pending.slice(0, newline);
81
+ this.#pending = this.#pending.slice(newline + 1);
82
+ if (!line || Buffer.byteLength(line) > WORKBENCH_FRAME_BYTES) throw new Error("Invalid workbench protocol frame size");
83
+ const frame = JSON.parse(line);
84
+ if (!frame || typeof frame !== "object" || Array.isArray(frame) || typeof frame.type !== "string") throw new Error("Invalid workbench protocol frame");
85
+ const parsed = frame;
86
+ if (parsed.id !== void 0 && (typeof parsed.id !== "string" || !/^[A-Za-z0-9_-]{1,80}$/.test(parsed.id))) throw new Error("Invalid workbench protocol request id");
87
+ receive(parsed);
88
+ }
89
+ if (Buffer.byteLength(this.#pending) > WORKBENCH_FRAME_BYTES) throw new Error("Workbench protocol input exceeds its byte limit");
90
+ }
91
+ };
92
+ function guestWorkspacePath(path, workspace) {
93
+ if (path === "/workspace" || path.startsWith("/workspace/")) {
94
+ const normalized = resolve(path);
95
+ if (normalized !== "/workspace" && !normalized.startsWith("/workspace/")) throw new Error("Invalid guest workspace path");
96
+ return normalized;
97
+ }
98
+ if (!isAbsolute(path)) throw new Error("Workbench scope requires an absolute host path");
99
+ const suffix = relative(workspace, path);
100
+ if (isAbsolute(suffix) || suffix === ".." || suffix.startsWith(`..${sep}`)) throw new Error("Requested host path is outside the granted workbench workspace");
101
+ return suffix ? `/workspace/${suffix.split(sep).join("/")}` : "/workspace";
102
+ }
103
+ function hostWorkspacePath(path, workspace) {
104
+ if (path === "/workspace" || path.startsWith("/workspace/")) {
105
+ const normalized = resolve(path);
106
+ if (normalized !== "/workspace" && !normalized.startsWith("/workspace/")) throw new Error("Invalid guest workspace path");
107
+ return resolve(workspace, relative("/workspace", normalized));
108
+ }
109
+ throw new Error("Guest local scope is outside the granted workspace");
110
+ }
111
+ function mapWorkbenchTarget(target, workspace, host = false) {
112
+ const map = host ? hostWorkspacePath : guestWorkspacePath;
113
+ if (isAbsolute(target)) return map(target, workspace);
114
+ if (target.startsWith("source:") && isAbsolute(target.slice(7))) return `source:${map(target.slice(7), workspace)}`;
115
+ return target;
116
+ }
117
+ function mapWorkbenchCheckpoint(checkpoint, workspace, host) {
118
+ const result = structuredClone(checkpoint);
119
+ const map = host ? hostWorkspacePath : guestWorkspacePath;
120
+ result.workspaceRoot = map(result.workspaceRoot, workspace);
121
+ result.target = mapWorkbenchTarget(result.target, workspace, host);
122
+ if (result.localScopePath) result.localScopePath = map(result.localScopePath, workspace);
123
+ result.deniedLocalPaths = result.deniedLocalPaths.map((path) => {
124
+ try {
125
+ return map(path, workspace);
126
+ } catch {
127
+ return path;
128
+ }
129
+ });
130
+ if (result.harnessRoot) {
131
+ throw new Error("Workbench checkpoint handoff with live executable resources is not supported");
132
+ }
133
+ return result;
134
+ }
135
+ function serializeWorkbenchConfig(config, workspace) {
136
+ for (const key of ["pluginHost", "mcpHost", "jevRuntime", "contribution", "developmentSourceRoot", "executablePlugins", "executableEvolutionProfiles"]) {
137
+ if (config[key] !== void 0) throw new Error(`Host ${key} cannot execute through the workbench controller`);
138
+ }
139
+ const result = { workspaceRoot: "/workspace", target: mapWorkbenchTarget(config.target ?? "", workspace) };
140
+ for (const key of ["role", "tools", "scanId", "maxToolIterations", "allowScanners", "systemPrompt", "autonomyMode", "refineObjective", "allowModelSelfExtension", "costModel", "contextWindowTokens", "compaction", "initialMessages"]) {
141
+ if (config[key] !== void 0) result[key] = structuredClone(config[key]);
142
+ }
143
+ if (config.maxTurnTokens !== void 0 && Number.isFinite(config.maxTurnTokens)) result.maxTurnTokens = config.maxTurnTokens;
144
+ if (config.scope) result.scope = config.scope.raw;
145
+ if (config.initialCheckpoint) result.initialCheckpoint = mapWorkbenchCheckpoint(config.initialCheckpoint, workspace, false);
146
+ if (config.agentMessaging && typeof config.agentMessaging === "object") {
147
+ const messaging = config.agentMessaging;
148
+ result.agentMessaging = {
149
+ selfId: messaging.selfId,
150
+ selfRole: messaging.selfRole,
151
+ parentId: messaging.parentId,
152
+ operatorId: messaging.operatorId,
153
+ siblingPrefix: messaging.siblingPrefix,
154
+ siblingChannelEnabled: messaging.siblingChannelEnabled,
155
+ operatorChannelEnabled: messaging.operatorChannelEnabled,
156
+ projectPath: "/workspace",
157
+ homeDir: "/home/zero"
158
+ };
159
+ }
160
+ return result;
161
+ }
162
+ function mapWorkbenchCliArguments(args, workspace) {
163
+ const targetFlags = /* @__PURE__ */ new Set(["--target", "-t", "--repo"]);
164
+ const pathFlags = /* @__PURE__ */ new Set(["--scope", "--db-path", "--workspace", "--workspace-root", "--cwd", "--output", "-o", "--config"]);
165
+ const result = [...args];
166
+ for (let index = 0; index < result.length; index++) {
167
+ const argument = result[index];
168
+ const equal = argument.indexOf("=");
169
+ const flag = equal < 0 ? argument : argument.slice(0, equal);
170
+ if (!targetFlags.has(flag) && !pathFlags.has(flag)) continue;
171
+ const valueIndex = equal < 0 ? index + 1 : index;
172
+ const value = equal < 0 ? result[valueIndex] : argument.slice(equal + 1);
173
+ if (value === void 0) throw new Error(`Missing workbench argument for ${flag}`);
174
+ const mapped = targetFlags.has(flag) ? mapWorkbenchTarget(value, workspace) : isAbsolute(value) ? guestWorkspacePath(value, workspace) : value;
175
+ result[valueIndex] = equal < 0 ? mapped : `${flag}=${mapped}`;
176
+ if (equal < 0) index++;
177
+ }
178
+ return result;
179
+ }
180
+
181
+ // packages/cli/src/workbench-console-session.ts
22
182
  import { randomUUID } from "node:crypto";
23
- import { dirname as dirname2, join as join3 } from "node:path";
183
+ import { realpathSync as realpathSync2 } from "node:fs";
184
+ import { mkdir, mkdtemp as mkdtemp2, realpath as realpath2 } from "node:fs/promises";
185
+ import { join as join3 } from "node:path";
24
186
 
25
- // packages/cli/src/tui/keybindings.ts
26
- var KEYBINDINGS = [
27
- // ── Composer ──────────────────────────────────────────────────────────────
28
- {
29
- id: "composer.send",
30
- keys: "Enter",
31
- defaultChords: ["return"],
32
- rebindable: false,
33
- description: "Send the message or run the highlighted slash command; queues the line when a turn is already in flight.",
34
- category: "Composer",
35
- lockReason: "text entry",
36
- handler: 'if (key.name === "return") { \u2026 send / queue / dequeue }'
37
- },
187
+ // packages/cli/src/workbench-plugins.ts
188
+ import { lstat, mkdtemp, realpath, rm } from "node:fs/promises";
189
+ import { join as join2 } from "node:path";
190
+ var GUEST_PLUGIN_ASSETS = "/opt/0-approved-plugins";
191
+ var reservedToolNames = Object.values(TOOL_DEFINITIONS).map((tool) => tool.name);
192
+ var bounds = { maxBytes: 64 * 1024 * 1024, maxFiles: 4096 };
193
+ async function prepareWorkbenchPlugins(project, homeDir) {
194
+ const root = pluginsRootDir(homeDir);
195
+ const record = readEnablement(project, homeDir);
196
+ const views = listInstalledPluginIds(root).flatMap((id) => {
197
+ const discovered = readInstalledPlugin(root, id, { reservedToolNames });
198
+ return discovered.ok ? [{ id, version: discovered.plugin.manifest.version, capabilities: aggregateCapabilities(discovered.plugin.manifest) }] : [];
199
+ });
200
+ const ids = loadableIds(reconcile(record, views));
201
+ const approvals = emptyEnablement("/workspace");
202
+ if (!ids.length) return { approvals, cleanup: async () => {
203
+ } };
204
+ const directory = await realpath(await mkdtemp("/tmp/0-guest-plugins-"));
205
+ const cleanup = () => rm(directory, { recursive: true, force: true });
206
+ try {
207
+ let bytes = 0, files = 0;
208
+ for (const id of ids) {
209
+ if ((await lstat(join2(root, id))).isSymbolicLink()) throw new Error("Workbench plugins cannot use linked source directories");
210
+ const snapshot = await snapshotSmolvmWorkspace(join2(root, id), join2(directory, id), { maxBytes: Math.max(1, bounds.maxBytes - bytes), maxFiles: Math.max(1, bounds.maxFiles - files) }, void 0, false, false);
211
+ bytes += snapshot.bytes;
212
+ files += snapshot.files.length;
213
+ if (bytes > bounds.maxBytes || files > bounds.maxFiles) throw new Error("Approved plugins exceed the workbench plugin limits");
214
+ const copied = readInstalledPlugin(directory, id, { reservedToolNames });
215
+ if (!copied.ok || !loadableIds(reconcile(record, [{ id, version: copied.plugin.manifest.version, capabilities: aggregateCapabilities(copied.plugin.manifest) }])).includes(id)) throw new Error(`Plugin ${id} changed during workbench preparation; approve its current permissions first`);
216
+ approvals.enabled[id] = record.enabled[id];
217
+ }
218
+ return { directory, approvals, cleanup };
219
+ } catch (error) {
220
+ await cleanup();
221
+ throw error;
222
+ }
223
+ }
224
+ async function installWorkbenchPlugins(approvals) {
225
+ if (!isAdmittedSmolvmWorkbench()) throw new Error("Plugin installation requires admitted VM execution");
226
+ const ids = Object.keys(approvals.enabled);
227
+ for (const id of ids) {
228
+ const discovered = readInstalledPlugin(GUEST_PLUGIN_ASSETS, id, { reservedToolNames });
229
+ if (!discovered.ok || !loadableIds(reconcile(approvals, [{ id, version: discovered.plugin.manifest.version, capabilities: aggregateCapabilities(discovered.plugin.manifest) }])).includes(id)) throw new Error(`Invalid approved guest plugin: ${id}`);
230
+ await snapshotSmolvmWorkspace(join2(GUEST_PLUGIN_ASSETS, id), join2(pluginsRootDir(), id), bounds, void 0, false, false);
231
+ }
232
+ if (ids.length && !writeEnablement("/workspace", { ...approvals, project: "/workspace" })) throw new Error("Could not save guest plugin approvals");
233
+ }
234
+
235
+ // packages/cli/src/workbench-service-plugins.ts
236
+ function validateGuestServicePluginConnections(raw, networkGranted) {
237
+ if (raw === void 0) return [];
238
+ if (!Array.isArray(raw) || raw.length > SERVICE_PLUGIN_CATALOG.length) throw new Error("Invalid guest service plugin grants");
239
+ const ids = /* @__PURE__ */ new Set();
240
+ const connections = raw.map((value) => {
241
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid guest service plugin grant");
242
+ const row = value;
243
+ const plugin = SERVICE_PLUGIN_CATALOG.find((item) => item.id === row.id);
244
+ if (!plugin || ids.has(plugin.id) || row.enabled !== true || !row.fields || typeof row.fields !== "object" || Array.isArray(row.fields)) throw new Error("Invalid guest service plugin grant");
245
+ ids.add(plugin.id);
246
+ const fields = row.fields;
247
+ if (Object.keys(fields).some((key) => !plugin.fields.some((field) => field.key === key))) throw new Error("Invalid guest service plugin field");
248
+ const output = {};
249
+ for (const field of plugin.fields) {
250
+ const value2 = fields[field.key];
251
+ if (value2 === void 0 && !field.required) continue;
252
+ if (typeof value2 !== "string" || !value2.trim() || value2.length > 8192 || /[\x00-\x1f\x7f]/.test(value2)) throw new Error("Invalid guest service plugin field");
253
+ if (field.type === "url") {
254
+ let url;
255
+ try {
256
+ url = new URL(value2);
257
+ } catch {
258
+ throw new Error("Invalid guest service plugin URL");
259
+ }
260
+ if (url.username || url.password || url.search || url.hash || url.protocol !== "https:" && !(url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname))) throw new Error("Invalid guest service plugin URL");
261
+ }
262
+ output[field.key] = value2;
263
+ }
264
+ return { id: plugin.id, enabled: true, fields: output };
265
+ });
266
+ if (connections.length && networkGranted !== true) throw new Error("Service plugins require the existing SmolVM network grant. Enable workbench networking before using connected plugins.");
267
+ return connections;
268
+ }
269
+ function servicePluginSecretRedactor(connections) {
270
+ const secrets = connections.flatMap((connection) => {
271
+ const plugin = SERVICE_PLUGIN_CATALOG.find((item) => item.id === connection.id);
272
+ const values = plugin.fields.filter((field) => field.type === "secret").map((field) => connection.fields[field.key]).filter((value) => !!value);
273
+ if (connection.id === "jira" && connection.fields.email && connection.fields.token) values.push(Buffer.from(`${connection.fields.email}:${connection.fields.token}`).toString("base64"));
274
+ return values.flatMap((value) => [value, encodeURIComponent(value), JSON.stringify(value).slice(1, -1)]);
275
+ }).sort((a, b) => b.length - a.length);
276
+ return (value) => secrets.reduce((text, secret) => text.split(secret).join("[redacted]"), value);
277
+ }
278
+
279
+ // packages/cli/src/workbench-console-session.ts
280
+ var EVENTS = /* @__PURE__ */ new Set(["onHarnessUpdate", "onAssistantDelta", "onReasoningDelta", "onToolStart", "onToolResult", "onUsage", "onNotice", "onCompaction"]);
281
+ var DECISIONS = /* @__PURE__ */ new Set(["requestScope", "requestLocalScope", "approveTool", "escalateScopedAudit", "askOperator", "historyList", "historyRead"]);
282
+ function validateOptions(options) {
283
+ if (options.provider.provider !== "chatgpt-codex" || options.selection.provider && options.selection.provider !== "chatgpt-codex" || !options.provider.models.includes(options.selection.model)) throw new Error("Workbench requires an explicit host provider/model grant");
284
+ for (const model of Object.values(options.selection.agentModels ?? {})) if (model !== "auto" && !options.provider.models.includes(model)) throw new Error("Workbench worker model is outside the host provider grant");
285
+ for (const [name, value, max] of [["lifetimeMs", options.lifetimeMs ?? 30 * 6e4, 60 * 6e4], ["idleMs", options.idleMs ?? 5 * 6e4, 15 * 6e4]]) if (!Number.isSafeInteger(value) || value < 1e3 || value > max) throw new Error(`Invalid workbench ${name}`);
286
+ }
287
+ function guestCommand(options, cli) {
288
+ if (options.guestCommand) return { command: options.guestCommand };
289
+ const entry = cli ? "run-agent" : "console-agent";
290
+ if (!options.assets) return { command: ["/usr/local/bin/0", "--workbench-inner", "workbench", entry] };
291
+ const dependencies = options.assets.dependencies ? "/opt/0-controller-deps/node_modules" : "/opt/0/node_modules";
292
+ const script = `const fs=require('node:fs');const path='/tmp/0-controller-cli';fs.cpSync('/opt/0-controller-cli',path,{recursive:true});fs.symlinkSync(${JSON.stringify(dependencies)},path+'/node_modules');const child=require('node:child_process').spawn(process.execPath,[path+'/0.js','--workbench-inner','workbench',${JSON.stringify(entry)}],{stdio:'inherit',env:process.env});child.once('error',e=>{console.error(e.message);process.exit(1)});child.once('exit',code=>process.exit(code??1));`;
293
+ return { command: ["/usr/local/bin/node", "--eval", script], mounts: [{ source: options.assets.cliDist, target: "/opt/0-controller-cli" }, ...options.assets.dependencies ? [{ source: options.assets.dependencies, target: "/opt/0-controller-deps/node_modules" }] : []] };
294
+ }
295
+ var Controller = class {
296
+ constructor(options, init, cli) {
297
+ this.options = options;
298
+ this.init = init;
299
+ this.cli = cli;
300
+ validateOptions(options);
301
+ this.workspace = realpathSync2(options.workbench.workspaceRoot ?? process.cwd());
302
+ this.execution = { backend: "smolvm", status: "pending", runId: this.runId, workspacePath: this.workspace, guestWorkspacePath: "/workspace", imageDigest: options.workbench.imageDigest, cpus: options.workbench.cpus, memoryMb: options.workbench.memoryMb };
303
+ this.ready = new Promise((resolve2, reject) => {
304
+ this.resolveReady = resolve2;
305
+ this.rejectReady = reject;
306
+ });
307
+ void this.ready.catch(() => {
308
+ });
309
+ }
310
+ options;
311
+ init;
312
+ cli;
313
+ abort = new AbortController();
314
+ runId = randomUUID();
315
+ workspace;
316
+ execution;
317
+ ready;
318
+ done;
319
+ resolveReady;
320
+ rejectReady;
321
+ input;
322
+ pending = /* @__PURE__ */ new Map();
323
+ providers = /* @__PURE__ */ new Map();
324
+ idle;
325
+ lifetime;
326
+ started = false;
327
+ guestReady = false;
328
+ startupStderr = "";
329
+ redact = (value) => value;
330
+ receive = () => {
331
+ };
332
+ decision = async () => null;
333
+ status(status, message) {
334
+ const { message: _previous, ...snapshot } = this.execution;
335
+ this.execution = { ...snapshot, status, ...message ? { message } : {} };
336
+ this.options.onExecution?.(this.execution);
337
+ }
338
+ write(frame) {
339
+ if (!this.input || this.abort.signal.aborted) throw new Error("Workbench VM is unavailable");
340
+ this.input.write(encodeWorkbenchFrame(frame));
341
+ }
342
+ touch() {
343
+ clearTimeout(this.idle);
344
+ this.idle = setTimeout(() => {
345
+ this.abort.abort(new Error("Workbench idle deadline expired"));
346
+ }, this.options.idleMs ?? 5 * 6e4);
347
+ this.idle.unref();
348
+ }
349
+ async start() {
350
+ if (this.started) return this.ready;
351
+ this.started = true;
352
+ this.status("pending", "Starting isolated workbench");
353
+ let stagedPlugins;
354
+ this.lifetime = setTimeout(() => this.abort.abort(new Error("Workbench lifetime expired")), this.options.lifetimeMs ?? 30 * 6e4);
355
+ this.lifetime.unref();
356
+ try {
357
+ const root = join3(this.options.workbench.stateRoot, "controller-results");
358
+ await mkdir(root, { recursive: true, mode: 448 });
359
+ const artifacts = this.options.artifactDirectory ?? await realpath2(await mkdtemp2(join3(root, "run-")));
360
+ const reader = new WorkbenchFrameReader();
361
+ const launch = guestCommand(this.options, this.cli);
362
+ const connections = this.cli ? [] : (await import("./service-plugins-MSK2EQP6.js")).loadServicePluginConnections(this.options.pluginHomeDir).filter((item) => item.enabled);
363
+ const servicePluginConnections = validateGuestServicePluginConnections(connections, this.options.network);
364
+ this.redact = servicePluginSecretRedactor(servicePluginConnections);
365
+ const plugins = stagedPlugins = await prepareWorkbenchPlugins(this.workspace, this.options.pluginHomeDir);
366
+ this.done = runSmolvmWorkbench({
367
+ image: this.options.workbench.image,
368
+ stateRoot: this.options.workbench.stateRoot,
369
+ workspaceRoot: this.workspace,
370
+ command: launch.command,
371
+ environment: { ZERO_PROVIDER: "chatgpt-codex", ZERO_NO_TELEMETRY: "1", DO_NOT_TRACK: "1" },
372
+ network: this.options.network,
373
+ tty: false,
374
+ cpus: this.options.workbench.cpus,
375
+ memoryMb: this.options.workbench.memoryMb,
376
+ storageGb: this.options.workbench.storageGb,
377
+ approvedImages: this.options.workbench.approvedImages,
378
+ signal: this.abort.signal,
379
+ workspaceMode: "snapshot",
380
+ artifactDirectory: artifacts,
381
+ readOnlyMounts: [...launch.mounts ?? [], ...plugins.directory ? [{ source: plugins.directory, target: GUEST_PLUGIN_ASSETS }] : []],
382
+ transport: { initialInput: encodeWorkbenchFrame({ type: "init", ...this.init, servicePluginConnections, servicePluginNetworkGranted: this.options.network, pluginApprovals: plugins.approvals, guestSettings: this.options.guestSettings, selection: { ...this.options.selection, provider: "chatgpt-codex" } }), onReady: (input) => {
383
+ this.input = input;
384
+ }, onStdout: (data) => reader.push(data, (frame) => this.dispatch(frame)), onStderr: (data) => {
385
+ this.startupStderr = this.redact(this.startupStderr + data).slice(-4096);
386
+ } }
387
+ });
388
+ this.done = this.done.finally(() => plugins.cleanup());
389
+ void this.done.then(async (result) => {
390
+ const error = new Error(this.redact(result.error ?? "Workbench VM exited") + (!this.guestReady && this.startupStderr.trim() ? `: ${this.startupStderr.trim()}` : ""));
391
+ if (!this.guestReady) this.rejectReady(error);
392
+ for (const pending of this.pending.values()) pending.reject(error);
393
+ this.pending.clear();
394
+ clearTimeout(this.idle);
395
+ clearTimeout(this.lifetime);
396
+ for (const pending of this.providers.values()) pending.abort();
397
+ this.providers.clear();
398
+ await this.options.provider.close?.();
399
+ this.status(result.cleanupFailed || result.exitCode !== 0 && !this.abort.signal.aborted ? "failed" : "stopped", result.error ? this.redact(result.error) : void 0);
400
+ });
401
+ this.touch();
402
+ await this.ready;
403
+ } catch (error) {
404
+ if (!this.done) {
405
+ await stagedPlugins?.cleanup();
406
+ await this.options.provider.close?.();
407
+ }
408
+ clearTimeout(this.idle);
409
+ clearTimeout(this.lifetime);
410
+ this.abort.abort();
411
+ this.rejectReady(new Error(this.redact(error instanceof Error ? error.message : "Workbench startup failed")));
412
+ this.status("failed", this.redact(error instanceof Error ? error.message : "Workbench startup failed"));
413
+ throw new Error(this.redact(error instanceof Error ? error.message : "Workbench startup failed"));
414
+ }
415
+ }
416
+ dispatch(frame) {
417
+ if (frame.type === "ready") {
418
+ if (this.guestReady || frame.platform !== "linux" || frame.workspace !== "/workspace") throw new Error("Invalid workbench guest readiness");
419
+ this.guestReady = true;
420
+ this.status("ready");
421
+ this.resolveReady();
422
+ return;
423
+ }
424
+ if (frame.type === "provider") {
425
+ void this.provider(frame);
426
+ return;
427
+ }
428
+ if (frame.type === "provider-cancel") {
429
+ this.providers.get(frame.id)?.abort();
430
+ return;
431
+ }
432
+ if (frame.type === "decision") {
433
+ if (!DECISIONS.has(String(frame.name)) || !Array.isArray(frame.args)) throw new Error("Invalid guest decision callback");
434
+ void this.decision(String(frame.name), frame.args).then((value) => this.write({ type: "decision-result", id: frame.id, value }), () => this.write({ type: "decision-result", id: frame.id, error: "Host decision unavailable" })).catch(() => {
435
+ });
436
+ return;
437
+ }
438
+ if (frame.type === "result" || frame.type === "error") {
439
+ const pending = this.pending.get(frame.id);
440
+ this.pending.delete(frame.id);
441
+ if (frame.type === "error") {
442
+ const error = new Error(typeof frame.error === "string" ? this.redact(frame.error) : "Guest operation failed");
443
+ if (!this.guestReady) this.rejectReady(error);
444
+ pending?.reject(error);
445
+ } else pending?.resolve(frame.value);
446
+ return;
447
+ }
448
+ this.receive(frame);
449
+ }
450
+ async provider(frame) {
451
+ if (!frame.id || this.providers.size >= 4 || this.providers.has(frame.id)) {
452
+ this.abort.abort(new Error("Invalid guest provider request"));
453
+ return;
454
+ }
455
+ const envelope = frame.envelope;
456
+ const abort = new AbortController();
457
+ this.providers.set(frame.id, abort);
458
+ const cancel = () => abort.abort();
459
+ this.abort.signal.addEventListener("abort", cancel, { once: true });
460
+ try {
461
+ if (!envelope || envelope.provider !== "chatgpt-codex" || !this.options.provider.models.includes(envelope.model) || typeof envelope.body !== "string") throw new Error("Invalid guest provider envelope");
462
+ const response = await this.options.provider.request({ provider: envelope.provider, model: envelope.model, body: envelope.body }, abort.signal);
463
+ this.write({ type: "provider-headers", id: frame.id, status: response.status, contentType: response.headers.get("content-type") ?? "text/event-stream" });
464
+ let bytes = 0;
465
+ if (response.body) {
466
+ const reader = response.body.getReader();
467
+ try {
468
+ while (true) {
469
+ const next = await reader.read();
470
+ if (next.done) break;
471
+ const chunk = next.value;
472
+ bytes += chunk.length;
473
+ if (bytes > 16 * 1024 * 1024) throw new Error("Provider response exceeds controller limit");
474
+ for (let offset = 0; offset < chunk.length; offset += 49152) this.write({ type: "provider-chunk", id: frame.id, data: Buffer.from(chunk.subarray(offset, offset + 49152)).toString("base64") });
475
+ }
476
+ } finally {
477
+ await reader.cancel().catch(() => {
478
+ });
479
+ reader.releaseLock();
480
+ }
481
+ }
482
+ this.write({ type: "provider-end", id: frame.id });
483
+ } catch {
484
+ try {
485
+ this.write({ type: "provider-error", id: frame.id });
486
+ } catch {
487
+ }
488
+ } finally {
489
+ this.providers.delete(frame.id);
490
+ this.abort.signal.removeEventListener("abort", cancel);
491
+ }
492
+ }
493
+ async request(op, values = {}) {
494
+ await this.start();
495
+ if (this.pending.size >= WORKBENCH_MAX_PENDING) throw new Error("Workbench request limit reached");
496
+ this.touch();
497
+ const id = randomUUID();
498
+ const result = new Promise((resolve2, reject) => this.pending.set(id, { resolve: resolve2, reject }));
499
+ try {
500
+ this.write({ type: "request", id, op, ...values });
501
+ } catch (error) {
502
+ this.pending.delete(id);
503
+ throw error;
504
+ }
505
+ return result;
506
+ }
507
+ async close() {
508
+ if (!this.started) {
509
+ await this.options.provider.close?.();
510
+ return;
511
+ }
512
+ let exited = false;
513
+ try {
514
+ await Promise.race([this.request("close"), new Promise((resolve2) => setTimeout(resolve2, 1e3))]);
515
+ await Promise.race([this.done?.then(() => {
516
+ exited = true;
517
+ }), new Promise((resolve2) => setTimeout(resolve2, 1e3))]);
518
+ } catch {
519
+ } finally {
520
+ if (!exited) this.abort.abort();
521
+ const result = await this.done;
522
+ if (result?.cleanupFailed) throw new Error(result.error ?? "Workbench teardown could not be confirmed");
523
+ }
524
+ }
525
+ };
526
+ function createWorkbenchConsoleSession(options) {
527
+ const workspace = realpathSync2(options.workbench.workspaceRoot ?? options.config.workspaceRoot ?? process.cwd());
528
+ const config = { ...options.config, scanId: options.config.scanId ?? `console-${randomUUID()}` };
529
+ const serial = serializeWorkbenchConfig(config, workspace);
530
+ const callbacks = [...DECISIONS].filter((name) => name === "historyList" || name === "historyRead" ? !!config.conversationHistory : typeof config[name] === "function");
531
+ const scopeEnforcement = getScopeEnforcementState(workspace);
532
+ const controller = new Controller({ ...options, workbench: { ...options.workbench, workspaceRoot: workspace } }, { config: serial, callbacks, scopeEnforcement }, false);
533
+ let messages = structuredClone(config.initialMessages ?? config.initialCheckpoint?.messages ?? []);
534
+ let checkpoint = config.initialCheckpoint;
535
+ let target = config.target ?? "";
536
+ let scope = config.scope;
537
+ let localScopePath = config.initialCheckpoint?.localScopePath ?? void 0;
538
+ let autonomyMode = config.autonomyMode ?? "standard";
539
+ let systemPrompt = config.systemPrompt ?? "";
540
+ let tools = config.tools ?? [];
541
+ let render;
542
+ let active = false;
543
+ let findings = [];
544
+ let engineWorkStarted = false;
545
+ let lastOutcome;
546
+ let cleanupPromise;
547
+ const deferred = [];
548
+ const queue = (op, value) => {
549
+ if (!controller.done) {
550
+ serial[op === "autonomy" ? "autonomyMode" : op] = value;
551
+ return;
552
+ }
553
+ const pending = controller.request(op, { value });
554
+ deferred.push(pending);
555
+ void pending.catch(() => {
556
+ });
557
+ };
558
+ controller.decision = async (name, args) => {
559
+ if (name === "historyList") return config.conversationHistory.list(...args);
560
+ if (name === "historyRead") return config.conversationHistory.read(...args);
561
+ const callback = config[name];
562
+ if (!callback || !callbacks.includes(name)) return null;
563
+ if (name === "requestLocalScope") {
564
+ const request = structuredClone(args[0]);
565
+ for (const key of ["requestedPath", "currentScopePath"]) if (typeof request[key] === "string") request[key] = hostWorkspacePath(request[key], workspace);
566
+ const value = await callback(request);
567
+ if (!value) return null;
568
+ const resolution = value;
569
+ return { scopePath: guestWorkspacePath(resolution.scopePath, workspace) };
570
+ }
571
+ if (name === "requestScope") {
572
+ const request = args[0];
573
+ const value = await callback({ ...request, target: mapWorkbenchTarget(String(request.target ?? ""), workspace, true), ...request.currentScope ? { currentScope: new ScopePolicy(request.currentScope.raw) } : {} });
574
+ if (!value) return null;
575
+ const resolution = value;
576
+ return { target: mapWorkbenchTarget(resolution.target, workspace), scope: resolution.scope.raw };
577
+ }
578
+ return callback(...args);
579
+ };
580
+ controller.receive = (frame) => {
581
+ if (frame.type === "state") {
582
+ const state = frame.snapshot;
583
+ if (!state || state.scanId !== config.scanId || !Array.isArray(state.messages) || state.messages.length > 1e4 || !Array.isArray(state.tools)) throw new Error("Invalid guest session state");
584
+ messages = state.messages;
585
+ systemPrompt = String(state.systemPrompt ?? "");
586
+ tools = state.tools;
587
+ target = mapWorkbenchTarget(String(state.target ?? ""), workspace, true);
588
+ scope = state.scope ? new ScopePolicy(state.scope) : void 0;
589
+ localScopePath = state.localScopePath ? hostWorkspacePath(String(state.localScopePath), workspace) : void 0;
590
+ autonomyMode = state.autonomyMode;
591
+ checkpoint = state.checkpoint ? mapWorkbenchCheckpoint(state.checkpoint, workspace, true) : void 0;
592
+ } else if (frame.type === "event") {
593
+ if (!EVENTS.has(String(frame.name)) || !Array.isArray(frame.args)) throw new Error("Invalid guest render callback");
594
+ const callback = render?.[frame.name];
595
+ callback?.(...frame.args);
596
+ } else if (frame.type === "findings") {
597
+ if (!Array.isArray(frame.findings) || frame.findings.length > 1e3) throw new Error("Invalid guest findings");
598
+ findings = frame.findings.map((finding) => findingSchema.parse(finding));
599
+ } else throw new Error("Unsupported guest controller event");
600
+ };
601
+ return {
602
+ scanId: config.scanId,
603
+ ready: Promise.resolve(),
604
+ get execution() {
605
+ return controller.execution;
606
+ },
607
+ get systemPrompt() {
608
+ return systemPrompt;
609
+ },
610
+ get tools() {
611
+ return tools;
612
+ },
613
+ get messages() {
614
+ return messages;
615
+ },
616
+ get target() {
617
+ return target;
618
+ },
619
+ get scope() {
620
+ return scope;
621
+ },
622
+ get localScopePath() {
623
+ return localScopePath;
624
+ },
625
+ get autonomyMode() {
626
+ return autonomyMode;
627
+ },
628
+ scopeEnforcement,
629
+ setAutonomyMode(mode) {
630
+ autonomyMode = mode;
631
+ queue("autonomy", mode);
632
+ },
633
+ configureEngagement(selection) {
634
+ if (selection.target !== void 0) target = selection.target;
635
+ if (selection.scope !== void 0) scope = selection.scope ?? void 0;
636
+ const value = { ...selection.target === void 0 ? {} : { target: mapWorkbenchTarget(selection.target, workspace) }, ...selection.scope === void 0 ? {} : { scope: selection.scope?.raw ?? null } };
637
+ if (!controller.done) Object.assign(serial, value);
638
+ else queue("configure", value);
639
+ },
640
+ configureWorkspace(path) {
641
+ if (realpathSync2(path) !== workspace) throw new Error("The SmolVM workspace grant is fixed for this chat; start a new chat to change it");
642
+ },
643
+ reconfigureRuntime(selection) {
644
+ if (selection.env !== void 0 || selection.provider && selection.provider !== "chatgpt-codex" || selection.model && !options.provider.models.includes(selection.model) || Object.values(selection.agentModels ?? {}).some((model) => model !== "auto" && !options.provider.models.includes(model))) throw new Error("Runtime selection is outside workbench provider grant");
645
+ Object.assign(options.selection, selection);
646
+ if (controller.done) queue("reconfigure", selection);
647
+ },
648
+ clearConversation() {
649
+ messages = [];
650
+ checkpoint = void 0;
651
+ if (!controller.done) {
652
+ serial.initialMessages = [];
653
+ delete serial.initialCheckpoint;
654
+ } else queue("clear");
655
+ },
656
+ async send(text, callbacks2, sendOptions) {
657
+ if (active) throw new Error("Workbench turn already active");
658
+ active = true;
659
+ render = callbacks2;
660
+ let cancelTimer;
661
+ const cancel = () => {
662
+ try {
663
+ controller.write({ type: "cancel" });
664
+ } catch {
665
+ }
666
+ cancelTimer = setTimeout(() => controller.abort.abort(new Error("Operator cancelled workbench turn")), 2e3);
667
+ };
668
+ sendOptions?.signal?.addEventListener("abort", cancel, { once: true });
669
+ try {
670
+ await Promise.all(deferred.splice(0));
671
+ await controller.start();
672
+ if (sendOptions?.signal?.aborted) cancel();
673
+ controller.status("running");
674
+ engineWorkStarted = true;
675
+ lastOutcome = await controller.request("send", { text, generateTitle: sendOptions?.generateTitle });
676
+ return lastOutcome;
677
+ } finally {
678
+ clearTimeout(cancelTimer);
679
+ active = false;
680
+ render = void 0;
681
+ sendOptions?.signal?.removeEventListener("abort", cancel);
682
+ if (!controller.abort.signal.aborted && controller.execution.status !== "stopped" && controller.execution.status !== "failed") controller.status("ready");
683
+ }
684
+ },
685
+ async stopPersistentAgent(agentId) {
686
+ return await controller.request("stopWorker", { agentId });
687
+ },
688
+ async stopPersistentAgents() {
689
+ if (controller.done) await controller.request("stopWorkers");
690
+ },
691
+ exportCheckpoint() {
692
+ if (active || !checkpoint) throw new Error("No quiescent VM checkpoint is available");
693
+ return structuredClone(checkpoint);
694
+ },
695
+ async prepareHandoff() {
696
+ if (active) throw new Error("Cannot hand off active VM turn");
697
+ if (!controller.done) return {};
698
+ return await controller.request("handoff");
699
+ },
700
+ cleanup() {
701
+ return cleanupPromise ??= (async () => {
702
+ await controller.close();
703
+ if (engineWorkStarted && controller.done) await options.onFindings?.(findings, { outcome: lastOutcome });
704
+ })();
705
+ }
706
+ };
707
+ }
708
+ async function runWorkbenchCli(options) {
709
+ if (options.args.length > 512 || options.args.some((arg) => typeof arg !== "string" || arg.includes("\0"))) throw new Error("Invalid workbench CLI argv");
710
+ const controller = new Controller(options, { args: options.args }, true);
711
+ let exitCode;
712
+ controller.receive = (frame) => {
713
+ if ((frame.type === "stdout" || frame.type === "stderr") && typeof frame.data === "string") (frame.type === "stdout" ? options.onStdout : options.onStderr)?.(Buffer.from(frame.data, "base64").toString("utf8"));
714
+ else if (frame.type === "exit" && Number.isSafeInteger(frame.code)) {
715
+ exitCode = frame.code;
716
+ } else throw new Error("Invalid CLI guest event");
717
+ };
718
+ const cancel = () => controller.abort.abort();
719
+ options.signal?.addEventListener("abort", cancel, { once: true });
720
+ try {
721
+ if (options.signal?.aborted) cancel();
722
+ await controller.start();
723
+ controller.status("running");
724
+ const result = await controller.done;
725
+ return { ...result, exitCode: exitCode ?? result.exitCode };
726
+ } finally {
727
+ options.signal?.removeEventListener("abort", cancel);
728
+ }
729
+ }
730
+
731
+ // packages/cli/src/tui/provider-status.ts
732
+ var PROVIDER_DEFS = [
38
733
  {
39
- id: "composer.newline",
40
- keys: "Shift+Enter",
41
- defaultChords: ["shift+return"],
42
- rebindable: false,
43
- description: "Insert a newline instead of sending (terminals without the kitty protocol fall through to send).",
44
- category: "Composer",
45
- lockReason: "text entry",
46
- handler: 'if (key.name === "return" && key.shift) setComposerText(`${\u2026}\\n`)'
734
+ id: "chatgpt-codex",
735
+ label: "ChatGPT Codex",
736
+ methods: ["oauth"],
737
+ // OAuth, not an API key. Both tokens are accepted and the access token is
738
+ // read first (llm-api.ts L874-875, L1386-1394), so it leads the list.
739
+ envVars: ["ZERO_CHATGPT_ACCESS_TOKEN", "ZERO_CHATGPT_OAUTH_REFRESH_TOKEN"],
740
+ fileSource: "~/.codex/auth.json (override with ZERO_CHATGPT_AUTH_FILE)",
741
+ hint: "run `codex login` to write ~/.codex/auth.json, or invoke 0 with env ZERO_CHATGPT_OAUTH_REFRESH_TOKEN=..."
47
742
  },
48
743
  {
49
- id: "composer.complete-command",
50
- keys: "Tab",
51
- defaultChords: ["tab"],
52
- rebindable: false,
53
- description: "Complete the highlighted slash command in the command menu.",
54
- category: "Composer",
55
- lockReason: "text entry",
56
- handler: 'if (key.name === "tab") setComposerText(completionFor(selectedSlashCommand, \u2026))'
744
+ id: "deepseek",
745
+ label: "DeepSeek",
746
+ methods: ["api-key"],
747
+ envVars: ["DEEPSEEK_API_KEY"],
748
+ hint: "set DEEPSEEK_API_KEY (endpoint override: DEEPSEEK_BASE_URL)"
57
749
  },
58
750
  {
59
- id: "composer.edit-queued",
60
- keys: "Ctrl+Y",
61
- defaultChords: ["ctrl+y"],
62
- rebindable: true,
63
- description: "Pull the most recently queued message back into the composer to edit, re-send, or drop.",
64
- category: "Composer",
65
- handler: 'if (matchesBinding(key, "composer.edit-queued", \u2026) && queuedRef.current.length > 0) \u2026'
751
+ id: "openrouter",
752
+ label: "OpenRouter",
753
+ // OAuth (PKCE browser sign-in) is preferred; a pasted OPENROUTER_API_KEY
754
+ // remains a secondary path. The browser flow provisions a durable
755
+ // `sk-or-...` key and stores it as an api_key record written to
756
+ // OPENROUTER_API_KEY (envVars[0], which llm-api already reads), so — unlike
757
+ // the xai/kimi device flows — the credential is a key, not OAuth tokens.
758
+ methods: ["oauth", "api-key"],
759
+ envVars: ["OPENROUTER_API_KEY"],
760
+ hint: "sign in with your OpenRouter account, or set OPENROUTER_API_KEY=sk-or-... from openrouter.ai/keys"
66
761
  },
67
762
  {
68
- id: "composer.delete-word",
69
- keys: "Ctrl+W / Alt+Backspace",
70
- defaultChords: ["ctrl+w", "option+backspace"],
71
- rebindable: false,
72
- description: "Delete the word before the cursor.",
73
- category: "Composer",
74
- lockReason: "text entry",
75
- handler: 'if (key.ctrl && key.name === "w") / if (key.name === "backspace" && (key.meta || key.option || key.ctrl)) \u2192 deletePreviousWord'
763
+ id: "azure",
764
+ label: "Azure OpenAI",
765
+ methods: ["api-key"],
766
+ // Only the Azure key authenticates; the deployment URL comes from
767
+ // AZURE_OPENAI_BASE_URL / OPENAI_BASE_URL / ~/.codex/config.toml
768
+ // (L1435-1445). A key with no reachable base URL still counts as
769
+ // configured here because detectProvider selects azure on the key alone.
770
+ envVars: ["AZURE_OPENAI_API_KEY"],
771
+ hint: "set AZURE_OPENAI_API_KEY plus AZURE_OPENAI_BASE_URL (or [model_providers.azure] in ~/.codex/config.toml)"
76
772
  },
77
773
  {
78
- id: "composer.delete-to-start",
79
- keys: "Ctrl+U",
80
- defaultChords: ["ctrl+u"],
81
- rebindable: false,
82
- description: "Delete from the cursor to the start of the line.",
83
- category: "Composer",
84
- lockReason: "text entry",
85
- handler: 'if (key.ctrl && key.name === "u") \u2192 deleteToLineStart'
774
+ id: "openai",
775
+ label: "OpenAI",
776
+ methods: ["api-key"],
777
+ envVars: ["OPENAI_API_KEY"],
778
+ hint: "set OPENAI_API_KEY=sk-... from platform.openai.com/api-keys"
86
779
  },
87
780
  {
88
- id: "composer.accept-suggestion",
89
- keys: "Right",
90
- defaultChords: ["right"],
91
- rebindable: false,
92
- description: "Accept the inline autosuggestion, filling the ghost suffix into the draft without sending.",
93
- category: "Composer",
94
- lockReason: "text entry",
95
- handler: 'if (key.name === "right") { const suffix = suggestCompletion(...); if (suffix) setComposerText(`${\u2026}${suffix}`) }'
781
+ id: "z-ai",
782
+ label: "Z.ai GLM",
783
+ methods: ["api-key"],
784
+ envVars: ["Z_AI_API_KEY"],
785
+ hint: "set Z_AI_API_KEY from your Z.ai Coding Plan (endpoint override: Z_AI_BASE_URL)"
96
786
  },
97
- // ── Navigation ──────────────────────────────────────────────────────────────
98
787
  {
99
- id: "nav.palette",
100
- keys: "Ctrl+P / Ctrl+K",
101
- defaultChords: ["ctrl+p", "ctrl+k"],
102
- rebindable: true,
103
- description: "Open the slash-command palette.",
104
- category: "Navigation",
105
- handler: 'if (matchesBinding(key, "nav.palette", \u2026)) setComposerText("/")'
788
+ id: "kimi",
789
+ label: "Moonshot Kimi",
790
+ // OAuth (device sign-in) is preferred; a pasted KIMI_API_KEY remains a
791
+ // secondary path. The OAuth access token is written to KIMI_API_KEY as a
792
+ // Bearer (llm-api.ts reads it there, no change needed), and the refresh
793
+ // token to the 0-owned var so the store can round-trip it.
794
+ methods: ["oauth", "api-key"],
795
+ envVars: ["KIMI_API_KEY", "ZERO_KIMI_OAUTH_REFRESH_TOKEN"],
796
+ hint: "sign in with your Kimi account, or set KIMI_API_KEY from your Kimi coding plan (endpoint override: KIMI_BASE_URL)"
106
797
  },
107
798
  {
108
- id: "nav.history-prev",
109
- keys: "Up",
110
- defaultChords: ["up"],
111
- rebindable: false,
112
- description: "Recall the previous submitted message into the composer (also moves the selection in menus and overlays).",
113
- category: "Navigation",
114
- lockReason: "modal",
115
- handler: 'if (key.name === "up") recallComposerHistory("up")'
799
+ id: "qwen",
800
+ label: "Alibaba Qwen",
801
+ methods: ["api-key"],
802
+ envVars: ["QWEN_API_KEY"],
803
+ hint: "set QWEN_API_KEY from Alibaba Model Studio (endpoint override: QWEN_BASE_URL)"
116
804
  },
117
805
  {
118
- id: "nav.history-next",
119
- keys: "Down",
806
+ id: "xai",
807
+ label: "xAI Grok",
808
+ // OAuth (device sign-in) is preferred; a pasted XAI_API_KEY remains a
809
+ // secondary path. The OAuth access token is written to XAI_API_KEY as a
810
+ // Bearer (llm-api.ts reads it there, no change needed), and the refresh
811
+ // token to the 0-owned var so the store can round-trip it.
812
+ methods: ["oauth", "api-key"],
813
+ envVars: ["XAI_API_KEY", "ZERO_XAI_OAUTH_REFRESH_TOKEN"],
814
+ hint: "sign in with your xAI account, or set XAI_API_KEY from console.x.ai (endpoint override: XAI_BASE_URL)"
815
+ },
816
+ {
817
+ id: "opencode",
818
+ label: "OpenCode Zen",
819
+ methods: ["api-key"],
820
+ envVars: ["OPENCODE_API_KEY"],
821
+ hint: "set OPENCODE_API_KEY from opencode.ai/auth (endpoint override: OPENCODE_BASE_URL)"
822
+ },
823
+ {
824
+ id: "copilot",
825
+ label: "GitHub Copilot",
826
+ // OAuth only (device-code sign-in). The GitHub device-flow access token is
827
+ // written to ZERO_COPILOT_GITHUB_TOKEN (envVars[0]) and sent directly as a
828
+ // Bearer to api.githubcopilot.com — no secondary exchange, no refresh, so
829
+ // there is no refresh-token env var.
830
+ methods: ["oauth"],
831
+ envVars: ["ZERO_COPILOT_GITHUB_TOKEN"],
832
+ hint: "sign in with your GitHub Copilot account (device sign-in), or set ZERO_COPILOT_GITHUB_TOKEN=... (endpoint override: COPILOT_BASE_URL)"
833
+ },
834
+ {
835
+ id: "google",
836
+ label: "Google Gemini (Code Assist)",
837
+ // OAuth only (PKCE browser sign-in — the Gemini CLI flow). The minted
838
+ // access token is written to ZERO_GEMINI_ACCESS_TOKEN (envVars[0]) and the
839
+ // refresh token to ZERO_GEMINI_OAUTH_REFRESH_TOKEN (the /REFRESH/i var); the
840
+ // runtime refreshes on demand against oauth2.googleapis.com. There is no
841
+ // pasted-key equivalent — Code Assist authenticates only via OAuth.
842
+ methods: ["oauth"],
843
+ envVars: ["ZERO_GEMINI_ACCESS_TOKEN", "ZERO_GEMINI_OAUTH_REFRESH_TOKEN"],
844
+ hint: "sign in with your Google account (browser sign-in), or set ZERO_GEMINI_OAUTH_REFRESH_TOKEN=... for Gemini Code Assist (project override: GOOGLE_CLOUD_PROJECT / ZERO_GEMINI_PROJECT)"
845
+ },
846
+ {
847
+ id: "anthropic",
848
+ label: "Anthropic",
849
+ // API key only for now. The schema supports OAuth, but Anthropic/Claude
850
+ // subscription OAuth is deliberately deferred — do not add "oauth" here
851
+ // until that workstream lands.
852
+ methods: ["api-key"],
853
+ envVars: ["ANTHROPIC_API_KEY"],
854
+ hint: "set ANTHROPIC_API_KEY=sk-ant-... from console.anthropic.com"
855
+ }
856
+ ];
857
+ function primaryMethod(methods) {
858
+ return methods[0] ?? "api-key";
859
+ }
860
+ var PROVIDERS = PROVIDER_DEFS.map((def) => ({
861
+ ...def,
862
+ auth: primaryMethod(def.methods)
863
+ }));
864
+ function providerSupportsMethod(providerId, method) {
865
+ const info = PROVIDERS.find((candidate) => candidate.id === providerId);
866
+ return info !== void 0 && info.methods.includes(method);
867
+ }
868
+ function hasCredential(value) {
869
+ return typeof value === "string" && value.trim().length > 0;
870
+ }
871
+ function satisfyingVar(info, env) {
872
+ return info.envVars.find((name) => hasCredential(env[name]));
873
+ }
874
+ function providerStates(env) {
875
+ return PROVIDERS.map((info) => {
876
+ const via = satisfyingVar(info, env);
877
+ return via === void 0 ? { ...info, configured: false } : { ...info, configured: true, via };
878
+ });
879
+ }
880
+ function isProviderConfigured(providerId, env) {
881
+ const info = PROVIDERS.find((candidate) => candidate.id === providerId);
882
+ return info !== void 0 && satisfyingVar(info, env) !== void 0;
883
+ }
884
+
885
+ // packages/cli/src/tui/settings.ts
886
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
887
+ import { randomUUID as randomUUID2 } from "node:crypto";
888
+ import { dirname as dirname3, join as join6 } from "node:path";
889
+
890
+ // packages/cli/src/tui/keybindings.ts
891
+ var KEYBINDINGS = [
892
+ // ── Composer ──────────────────────────────────────────────────────────────
893
+ {
894
+ id: "composer.send",
895
+ keys: "Enter",
896
+ defaultChords: ["return"],
897
+ rebindable: false,
898
+ description: "Send the message or run the highlighted slash command; queues the line when a turn is already in flight.",
899
+ category: "Composer",
900
+ lockReason: "text entry",
901
+ handler: 'if (key.name === "return") { \u2026 send / queue / dequeue }'
902
+ },
903
+ {
904
+ id: "composer.newline",
905
+ keys: "Shift+Enter",
906
+ defaultChords: ["shift+return"],
907
+ rebindable: false,
908
+ description: "Insert a newline instead of sending (terminals without the kitty protocol fall through to send).",
909
+ category: "Composer",
910
+ lockReason: "text entry",
911
+ handler: 'if (key.name === "return" && key.shift) setComposerText(`${\u2026}\\n`)'
912
+ },
913
+ {
914
+ id: "composer.complete-command",
915
+ keys: "Tab",
916
+ defaultChords: ["tab"],
917
+ rebindable: false,
918
+ description: "Complete the highlighted slash command in the command menu.",
919
+ category: "Composer",
920
+ lockReason: "text entry",
921
+ handler: 'if (key.name === "tab") setComposerText(completionFor(selectedSlashCommand, \u2026))'
922
+ },
923
+ {
924
+ id: "composer.edit-queued",
925
+ keys: "Ctrl+Y",
926
+ defaultChords: ["ctrl+y"],
927
+ rebindable: true,
928
+ description: "Pull the most recently queued message back into the composer to edit, re-send, or drop.",
929
+ category: "Composer",
930
+ handler: 'if (matchesBinding(key, "composer.edit-queued", \u2026) && queuedRef.current.length > 0) \u2026'
931
+ },
932
+ {
933
+ id: "composer.delete-word",
934
+ keys: "Ctrl+W / Alt+Backspace",
935
+ defaultChords: ["ctrl+w", "option+backspace"],
936
+ rebindable: false,
937
+ description: "Delete the word before the cursor.",
938
+ category: "Composer",
939
+ lockReason: "text entry",
940
+ handler: 'if (key.ctrl && key.name === "w") / if (key.name === "backspace" && (key.meta || key.option || key.ctrl)) \u2192 deletePreviousWord'
941
+ },
942
+ {
943
+ id: "composer.delete-to-start",
944
+ keys: "Ctrl+U",
945
+ defaultChords: ["ctrl+u"],
946
+ rebindable: false,
947
+ description: "Delete from the cursor to the start of the line.",
948
+ category: "Composer",
949
+ lockReason: "text entry",
950
+ handler: 'if (key.ctrl && key.name === "u") \u2192 deleteToLineStart'
951
+ },
952
+ {
953
+ id: "composer.accept-suggestion",
954
+ keys: "Right",
955
+ defaultChords: ["right"],
956
+ rebindable: false,
957
+ description: "Accept the inline autosuggestion, filling the ghost suffix into the draft without sending.",
958
+ category: "Composer",
959
+ lockReason: "text entry",
960
+ handler: 'if (key.name === "right") { const suffix = suggestCompletion(...); if (suffix) setComposerText(`${\u2026}${suffix}`) }'
961
+ },
962
+ // ── Navigation ──────────────────────────────────────────────────────────────
963
+ {
964
+ id: "nav.palette",
965
+ keys: "Ctrl+P / Ctrl+K",
966
+ defaultChords: ["ctrl+p", "ctrl+k"],
967
+ rebindable: true,
968
+ description: "Open the slash-command palette.",
969
+ category: "Navigation",
970
+ handler: 'if (matchesBinding(key, "nav.palette", \u2026)) setComposerText("/")'
971
+ },
972
+ {
973
+ id: "nav.history-prev",
974
+ keys: "Up",
975
+ defaultChords: ["up"],
976
+ rebindable: false,
977
+ description: "Recall the previous submitted message into the composer (also moves the selection in menus and overlays).",
978
+ category: "Navigation",
979
+ lockReason: "modal",
980
+ handler: 'if (key.name === "up") recallComposerHistory("up")'
981
+ },
982
+ {
983
+ id: "nav.history-next",
984
+ keys: "Down",
120
985
  defaultChords: ["down"],
121
986
  rebindable: false,
122
987
  description: "Recall the next message, or \u2014 on an empty composer with workers running \u2014 drop into the active-subagents list.",
@@ -416,11 +1281,11 @@ function sanitizeKeybindingOverrides(raw, bindings = KEYBINDINGS) {
416
1281
 
417
1282
  // packages/cli/src/tui/themes.ts
418
1283
  import { mkdirSync, rmSync, writeFileSync } from "node:fs";
419
- import { dirname, join as join2 } from "node:path";
1284
+ import { dirname as dirname2, join as join5 } from "node:path";
420
1285
 
421
1286
  // packages/cli/src/tui/user-themes.ts
422
1287
  import { readdirSync, readFileSync } from "node:fs";
423
- import { join } from "node:path";
1288
+ import { join as join4 } from "node:path";
424
1289
  function coerceUserTheme(id, raw) {
425
1290
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
426
1291
  return { ok: false, reason: "file is not a JSON object" };
@@ -468,7 +1333,7 @@ function loadUserThemes(dir) {
468
1333
  }
469
1334
  let parsed;
470
1335
  try {
471
- parsed = JSON.parse(readFileSync(join(dir, file), "utf8"));
1336
+ parsed = JSON.parse(readFileSync(join4(dir, file), "utf8"));
472
1337
  } catch (err) {
473
1338
  rejected.push({
474
1339
  name: id,
@@ -1221,11 +2086,11 @@ function degradePalette(palette, depth) {
1221
2086
  return out;
1222
2087
  }
1223
2088
  var INSTALLED_THEMES_DIRNAME = "themes";
1224
- function installedThemesDir(homeDir2) {
1225
- return join2(homeStateDir(homeDir2), INSTALLED_THEMES_DIRNAME);
2089
+ function installedThemesDir(homeDir) {
2090
+ return join5(homeStateDir(homeDir), INSTALLED_THEMES_DIRNAME);
1226
2091
  }
1227
- function installedThemeFilePath(id, homeDir2) {
1228
- return join2(installedThemesDir(homeDir2), `${id}.json`);
2092
+ function installedThemeFilePath(id, homeDir) {
2093
+ return join5(installedThemesDir(homeDir), `${id}.json`);
1229
2094
  }
1230
2095
  var THEME_ID_RE = /^[a-z][a-z0-9]*([._-][a-z0-9]+)*$/;
1231
2096
  var THEME_ID_MAX = 64;
@@ -1234,36 +2099,36 @@ function isSafeThemeId(id) {
1234
2099
  }
1235
2100
  var installedThemes = null;
1236
2101
  var installedFromHome;
1237
- function readInstalledThemes(homeDir2) {
2102
+ function readInstalledThemes(homeDir) {
1238
2103
  const out = /* @__PURE__ */ new Map();
1239
- for (const entry of loadUserThemes(installedThemesDir(homeDir2)).valid) {
2104
+ for (const entry of loadUserThemes(installedThemesDir(homeDir)).valid) {
1240
2105
  out.set(entry.name, entry);
1241
2106
  }
1242
2107
  return out;
1243
2108
  }
1244
- function ensureInstalledThemesLoaded(homeDir2) {
1245
- if (installedThemes === null || installedFromHome !== homeDir2) {
1246
- installedThemes = readInstalledThemes(homeDir2);
1247
- installedFromHome = homeDir2;
2109
+ function ensureInstalledThemesLoaded(homeDir) {
2110
+ if (installedThemes === null || installedFromHome !== homeDir) {
2111
+ installedThemes = readInstalledThemes(homeDir);
2112
+ installedFromHome = homeDir;
1248
2113
  }
1249
2114
  return installedThemes;
1250
2115
  }
1251
- function reloadInstalledThemes(homeDir2) {
1252
- installedThemes = readInstalledThemes(homeDir2);
1253
- installedFromHome = homeDir2;
2116
+ function reloadInstalledThemes(homeDir) {
2117
+ installedThemes = readInstalledThemes(homeDir);
2118
+ installedFromHome = homeDir;
1254
2119
  return installedThemes;
1255
2120
  }
1256
- function installedThemeEntries(homeDir2) {
1257
- return [...ensureInstalledThemesLoaded(homeDir2).values()];
2121
+ function installedThemeEntries(homeDir) {
2122
+ return [...ensureInstalledThemesLoaded(homeDir).values()];
1258
2123
  }
1259
- function allThemeNames(homeDir2) {
1260
- return [...THEME_NAMES, ...ensureInstalledThemesLoaded(homeDir2).keys()];
2124
+ function allThemeNames(homeDir) {
2125
+ return [...THEME_NAMES, ...ensureInstalledThemesLoaded(homeDir).keys()];
1261
2126
  }
1262
2127
  function isKnownTheme(value) {
1263
2128
  if (isThemeName(value)) return true;
1264
2129
  return typeof value === "string" && installedThemes !== null && installedThemes.has(value);
1265
2130
  }
1266
- function writeInstalledTheme(file, homeDir2) {
2131
+ function writeInstalledTheme(file, homeDir) {
1267
2132
  if (!isSafeThemeId(file.id)) return { ok: false, error: `unsafe theme id ${JSON.stringify(file.id)}` };
1268
2133
  if (isThemeName(file.id)) return { ok: false, error: `"${file.id}" is a built-in theme name and cannot be overridden` };
1269
2134
  const issues = validateTheme(file.palette);
@@ -1271,8 +2136,8 @@ function writeInstalledTheme(file, homeDir2) {
1271
2136
  return { ok: false, error: `invalid palette: ${issues.map((i) => i.message).join("; ")}` };
1272
2137
  }
1273
2138
  try {
1274
- const path = installedThemeFilePath(file.id, homeDir2);
1275
- mkdirSync(dirname(path), { recursive: true });
2139
+ const path = installedThemeFilePath(file.id, homeDir);
2140
+ mkdirSync(dirname2(path), { recursive: true });
1276
2141
  const body = {
1277
2142
  id: file.id,
1278
2143
  ...file.label !== void 0 ? { label: file.label } : {},
@@ -1287,13 +2152,13 @@ function writeInstalledTheme(file, homeDir2) {
1287
2152
  return { ok: false, error: err instanceof Error ? err.message : String(err) };
1288
2153
  }
1289
2154
  }
1290
- function removeInstalledTheme(id, homeDir2) {
2155
+ function removeInstalledTheme(id, homeDir) {
1291
2156
  if (!isSafeThemeId(id)) return { ok: false, error: `unsafe theme id ${JSON.stringify(id)}` };
1292
2157
  if (isThemeName(id)) return { ok: false, error: `"${id}" is a built-in theme and cannot be removed` };
1293
- const cache = ensureInstalledThemesLoaded(homeDir2);
2158
+ const cache = ensureInstalledThemesLoaded(homeDir);
1294
2159
  if (!cache.has(id)) return { ok: false, error: `theme "${id}" is not installed` };
1295
2160
  try {
1296
- rmSync(installedThemeFilePath(id, homeDir2), { force: true });
2161
+ rmSync(installedThemeFilePath(id, homeDir), { force: true });
1297
2162
  return { ok: true };
1298
2163
  } catch (err) {
1299
2164
  return { ok: false, error: err instanceof Error ? err.message : String(err) };
@@ -1302,15 +2167,15 @@ function removeInstalledTheme(id, homeDir2) {
1302
2167
 
1303
2168
  // packages/cli/src/tui/settings.ts
1304
2169
  var THEME_CHOICES = [...THEME_NAMES];
1305
- function syncThemeChoices(homeDir2) {
1306
- ensureInstalledThemesLoaded(homeDir2);
1307
- THEME_CHOICES.splice(0, THEME_CHOICES.length, ...allThemeNames(homeDir2));
2170
+ function syncThemeChoices(homeDir) {
2171
+ ensureInstalledThemesLoaded(homeDir);
2172
+ THEME_CHOICES.splice(0, THEME_CHOICES.length, ...allThemeNames(homeDir));
1308
2173
  }
1309
2174
  var DEFS = [
1310
2175
  {
1311
2176
  key: "showStatusBar",
1312
2177
  label: "Status bar",
1313
- description: "Optional bottom-bar telemetry: model, working directory, git state and token counters. Permission mode and the turn timer share that row and stay visible with this off.",
2178
+ description: "Bottom bar with model, folder, git and token counts. Permission mode and turn timer always show.",
1314
2179
  kind: "boolean",
1315
2180
  default: true,
1316
2181
  group: "Display"
@@ -1326,7 +2191,7 @@ var DEFS = [
1326
2191
  {
1327
2192
  key: "composerSuggestions",
1328
2193
  label: "Composer autosuggestions",
1329
- description: "Inline ghost-text continuation from your submitted-message history, accepted with the Right arrow at end-of-input.",
2194
+ description: "Ghost-text suggestions from your past messages. Right arrow accepts.",
1330
2195
  kind: "boolean",
1331
2196
  default: true,
1332
2197
  group: "Display"
@@ -1334,7 +2199,7 @@ var DEFS = [
1334
2199
  {
1335
2200
  key: "mouseSupport",
1336
2201
  label: "Mouse support",
1337
- description: "Scroll-wheel scrolling, click-to-select on list rows, and clickable hints. Turn off for a keyboard-only console where the terminal's own text selection works.",
2202
+ description: "Scroll, click rows and hints. Turn off to use your terminal's own text selection.",
1338
2203
  kind: "boolean",
1339
2204
  default: true,
1340
2205
  group: "Display"
@@ -1350,7 +2215,7 @@ var DEFS = [
1350
2215
  {
1351
2216
  key: "showRuntimeNotices",
1352
2217
  label: "Runtime notices",
1353
- description: "Surface runtime stdout/stderr as transcript notices.",
2218
+ description: "Show engine output (stdout/stderr) in the conversation.",
1354
2219
  kind: "boolean",
1355
2220
  default: true,
1356
2221
  group: "Transcript"
@@ -1382,7 +2247,7 @@ var DEFS = [
1382
2247
  {
1383
2248
  key: "showObjective",
1384
2249
  label: "Objective",
1385
- description: `Bottom-bar "objective" pill: a short "what am I working on" title derived from the session's first message.`,
2250
+ description: "Short title for the current session in the bottom bar.",
1386
2251
  kind: "boolean",
1387
2252
  default: true,
1388
2253
  group: "Display"
@@ -1390,7 +2255,7 @@ var DEFS = [
1390
2255
  {
1391
2256
  key: "showScope",
1392
2257
  label: "Scope",
1393
- description: "Show the configured multi-host scope and exclusions. An absent scope is distinct from an explicit empty scope.",
2258
+ description: "Show the target scope and exclusions. No scope is different from an empty scope.",
1394
2259
  kind: "boolean",
1395
2260
  default: true,
1396
2261
  group: "Display"
@@ -1441,7 +2306,7 @@ var DEFS = [
1441
2306
  {
1442
2307
  key: "transcriptStyle",
1443
2308
  label: "Transcript style",
1444
- description: "Minimal (the default) drops every bubble and box: your turns carry a thin coloured accent rail and the answer flows as plain body text, the OpenCode / oh-my-pi flat look. Bubble right-aligns your messages against left-aligned answers and titles each card on its border. Balanced: only your own messages are bubbled; everything else is flat. Rail, plain, compact and document offer alternative transcript layouts.",
2309
+ description: "How the conversation looks. Minimal (default) is flat text; Bubble and Balanced add message bubbles; Rail, plain, compact and document are alternatives.",
1445
2310
  kind: "enum",
1446
2311
  default: "minimal",
1447
2312
  choices: ["minimal", "rail", "bubble", "balanced", "plain", "compact", "document"],
@@ -1450,7 +2315,7 @@ var DEFS = [
1450
2315
  {
1451
2316
  key: "roleLabelStyle",
1452
2317
  label: "Role label",
1453
- description: 'Labels your messages as "You"; assistant replies stay unlabeled. Full and short include the optional age, glyph shows the name alone, and off hides the label.',
2318
+ description: 'Label your messages as "You". Full and short add the age, glyph shows the name only, off hides it.',
1454
2319
  kind: "enum",
1455
2320
  default: "off",
1456
2321
  choices: ["full", "short", "glyph", "off"],
@@ -1468,7 +2333,7 @@ var DEFS = [
1468
2333
  {
1469
2334
  key: "richToolCards",
1470
2335
  label: "Rich tool cards",
1471
- description: "Draw bash/run_command output and apply_patch edits as bordered cards (command + output + wall/exit footer, or an edit header + diff) instead of a plain line.",
2336
+ description: "Show command output and file edits as cards instead of one line.",
1472
2337
  kind: "boolean",
1473
2338
  default: true,
1474
2339
  group: "Transcript"
@@ -1476,7 +2341,7 @@ var DEFS = [
1476
2341
  {
1477
2342
  key: "transcriptDetail",
1478
2343
  label: "Transcript detail",
1479
- description: "Fold tool and reasoning steps. Output previews stay within 20 lines; click a card for retained detail.",
2344
+ description: "Fold tool and reasoning steps. Click a card to see more.",
1480
2345
  kind: "enum",
1481
2346
  default: "collapsed",
1482
2347
  choices: ["collapsed", "expanded"],
@@ -1485,7 +2350,7 @@ var DEFS = [
1485
2350
  {
1486
2351
  key: "autoCompaction",
1487
2352
  label: "Auto-compaction",
1488
- description: "Automatically summarise older turns when the conversation nears the model's context window.",
2353
+ description: "Summarise older turns when the context window gets full.",
1489
2354
  kind: "boolean",
1490
2355
  default: true,
1491
2356
  group: "Context"
@@ -1502,7 +2367,7 @@ var DEFS = [
1502
2367
  {
1503
2368
  key: "theme",
1504
2369
  label: "Theme",
1505
- description: "Colour palette. Slate (neutral grey, default) and Midnight (deep blue-black) and Carbon (warm dark), Standard/Paper (light), plus Contrast, Mono Dim and ANSI 16 for 16-colour terminals. Drop validated palettes in ~/.0/themes to add your own.",
2370
+ description: "Colour theme. Contrast, Mono Dim and ANSI 16 suit 16-colour terminals. Add your own in ~/.0/themes.",
1506
2371
  kind: "enum",
1507
2372
  default: DEFAULT_THEME_NAME,
1508
2373
  choices: THEME_CHOICES,
@@ -1511,7 +2376,7 @@ var DEFS = [
1511
2376
  {
1512
2377
  key: "allowModelSelfExtension",
1513
2378
  label: "Model self-extension",
1514
- description: "Allow new sessions to add sandboxed tools and live harness generations. Existing disabled sessions stay disabled. Trusted harness code requires separate workspace trust; development engine updates are controlled independently.",
2379
+ description: "Let new sessions add sandboxed tools. Sessions started with this off stay off. Trusted tool code also needs workspace trust; engine updates are a separate setting.",
1515
2380
  kind: "boolean",
1516
2381
  default: DEFAULT_ALLOW_MODEL_SELF_EXTENSION,
1517
2382
  group: "Security"
@@ -1519,7 +2384,7 @@ var DEFS = [
1519
2384
  {
1520
2385
  key: "allowDevSourceUpdates",
1521
2386
  label: "Development engine updates",
1522
- description: "0dev only. Reload changed core engine code between turns without losing the session. Runs with host privileges, including credential access; independent of sandboxed tool extension.",
2387
+ description: "0dev only. Reload changed engine code between turns. Runs with full host access, including your credentials; separate from sandboxed tools.",
1523
2388
  kind: "boolean",
1524
2389
  default: false,
1525
2390
  group: "Security"
@@ -1527,7 +2392,7 @@ var DEFS = [
1527
2392
  {
1528
2393
  key: "autoEvolveFinderLenses",
1529
2394
  label: "Auto-evolve finder lenses",
1530
- description: "Start the TUI watcher for ~/.0/lens-synthesis/miss-input.json (or OSEC_TUI_LENS_SYNTH_INPUT) so each new curated revision can invoke the configured model.",
2395
+ description: "Watch ~/.0/lens-synthesis/miss-input.json (or OSEC_TUI_LENS_SYNTH_INPUT) and let the model draft new finder lenses.",
1531
2396
  kind: "boolean",
1532
2397
  default: false,
1533
2398
  group: "Security"
@@ -1535,7 +2400,7 @@ var DEFS = [
1535
2400
  {
1536
2401
  key: "autoPromoteFinderLenses",
1537
2402
  label: "Auto-promote validated lenses",
1538
- description: "Permit the TUI watcher to persist a candidate only after its positive and negative-control corpus gate passes, otherwise automatic evaluation stays dry-run.",
2403
+ description: "Save a drafted lens only after it passes its positive and negative test cases. Otherwise evaluation is dry-run.",
1539
2404
  kind: "boolean",
1540
2405
  default: false,
1541
2406
  group: "Security"
@@ -1576,7 +2441,7 @@ var DEFS = [
1576
2441
  {
1577
2442
  key: "elapsedTimer",
1578
2443
  label: "Elapsed timer",
1579
- description: "Show the running-turn elapsed time as a compact clock-glyphed pill next to the left status icon, or hide it.",
2444
+ description: "Show how long the current turn has been running.",
1580
2445
  kind: "enum",
1581
2446
  default: "left",
1582
2447
  choices: ["left", "off"],
@@ -1585,7 +2450,7 @@ var DEFS = [
1585
2450
  {
1586
2451
  key: "logoAnimation",
1587
2452
  label: "Logo animation",
1588
- description: 'Intro animation for the "0.SECURITY" wordmark: glitch (a neon-flecked scramble that resolves \u2014 the default), rainbow (a looping hue sweep), matrix (a green matrix-rain cascade), wave (a rippling cyan wavefront), neon (a neon-sign warm-up flicker), shimmer (a bright comet with a gradient tail), pulse (the slash breathes), strike (an orange slash strikes through the 0), draw (letters draw in behind a pen tip), fade (a centre-out bloom), typein (per-cell reveal), sweep (a bright bar wipes across) or off (static).',
2453
+ description: "Intro animation for the 0 logo, or off.",
1589
2454
  kind: "enum",
1590
2455
  default: "glitch",
1591
2456
  choices: [
@@ -1609,7 +2474,7 @@ var DEFS = [
1609
2474
  {
1610
2475
  key: "reduceMotion",
1611
2476
  label: "Reduce motion",
1612
- description: "Master reduce-motion: the console keeps essential feedback but disables decorative animations like the logo intro, shimmers and sweeps.",
2477
+ description: "Turn off decorative animations (logo intro, shimmers, sweeps).",
1613
2478
  kind: "boolean",
1614
2479
  default: false,
1615
2480
  group: "Motion"
@@ -1617,16 +2482,16 @@ var DEFS = [
1617
2482
  {
1618
2483
  key: "analyticsLevel",
1619
2484
  label: "Data sharing",
1620
- description: "Usage sends counts, timing, cost and error categories. Commands adds tool inputs/outputs and code; Full adds scope and findings. Credentials are scrubbed, but identifying content may remain. Uploads require an endpoint and are not anonymous. Environment opt-outs take precedence. Applies to this computer.",
2485
+ description: "Usage shares pseudonymous counts, timing, cost and error types, never code or tool content. Off sends nothing. Environment opt-outs win. Applies to this computer.",
1621
2486
  kind: "enum",
1622
2487
  default: "off",
1623
- choices: ["off", "usage", "commands", "full"],
2488
+ choices: ["off", "usage"],
1624
2489
  group: "Privacy"
1625
2490
  },
1626
2491
  {
1627
2492
  key: "diagnosticReporting",
1628
2493
  label: "Problem reports",
1629
- description: "Review diagnostics before sending, send automatically, or keep reports local. Requires a configured endpoint. Off/Usage sharing sends categories only; Commands/Full can include scrubbed errors, stacks and failure output. Applies to this computer.",
2494
+ description: "Review error reports before sending, send automatically, or keep them local. Needs a configured destination; separate from usage sharing. Applies to this computer.",
1630
2495
  kind: "enum",
1631
2496
  default: "ask",
1632
2497
  choices: ["off", "ask", "automatic"],
@@ -1635,7 +2500,7 @@ var DEFS = [
1635
2500
  {
1636
2501
  key: "updatePolicy",
1637
2502
  label: "Updates",
1638
- description: "Off, notify about releases, or install updates before the console starts. Applies to this computer; project settings cannot enable installation.",
2503
+ description: "Off, notify about new releases, or install updates at startup. Applies to this computer; project settings can't turn on installing.",
1639
2504
  kind: "enum",
1640
2505
  default: "automatic",
1641
2506
  choices: ["off", "notify", "automatic"],
@@ -1644,7 +2509,7 @@ var DEFS = [
1644
2509
  {
1645
2510
  key: "executionProfile",
1646
2511
  label: "Execution profile",
1647
- description: "SmolVM runs the whole console, agents, browser and shell in the online Kali workbench. Prepare its approved image and explicit credential grants with 0 workbench setup. Applies to this computer on the next launch; failures never fall back to host execution.",
2512
+ description: "SmolVM runs everything (agents, browser, shell) in the online Kali workbench. Set it up with 0 workbench setup. Applies to this computer on next launch; failures never fall back to running on your machine.",
1648
2513
  kind: "enum",
1649
2514
  default: "local",
1650
2515
  choices: ["local", "smolvm"],
@@ -1653,7 +2518,7 @@ var DEFS = [
1653
2518
  {
1654
2519
  key: "symbolPreset",
1655
2520
  label: "Symbols",
1656
- description: "Glyph set for icons, status marks and checkboxes: Unicode (default, works everywhere), Nerd Font (crisp patched-font icons \u2014 requires a Nerd Font terminal) or ASCII (plain, single-cell).",
2521
+ description: "Icon set: Unicode (default), Nerd Font (needs a Nerd Font terminal) or ASCII.",
1657
2522
  kind: "enum",
1658
2523
  default: "unicode",
1659
2524
  choices: ["unicode", "nerd", "ascii"],
@@ -1662,7 +2527,7 @@ var DEFS = [
1662
2527
  {
1663
2528
  key: "rosterSort",
1664
2529
  label: "Roster order",
1665
- description: "Order the agent herd and comms fleet by attention (blocked and working agents float to the top \u2014 the default) or by fixed lifecycle status.",
2530
+ description: "Sort agents by attention (blocked and working first, default) or by status.",
1666
2531
  kind: "enum",
1667
2532
  default: "attention",
1668
2533
  choices: ["attention", "status"],
@@ -1671,7 +2536,7 @@ var DEFS = [
1671
2536
  {
1672
2537
  key: "leaderKey",
1673
2538
  label: "Leader key",
1674
- description: "Optional tmux-style prefix chord for the roster views: press it, then a number to focus that agent, or n/p to step. Off disables it (the direct number keys work regardless).",
2539
+ description: "Optional prefix key for agent views: press it, then a number to focus an agent or n/p to step. Number keys work either way.",
1675
2540
  kind: "enum",
1676
2541
  default: "off",
1677
2542
  choices: ["off", "ctrl+a", "ctrl+b", "ctrl+space"],
@@ -1706,1068 +2571,246 @@ var DEFAULT_SETTINGS = {
1706
2571
  transcriptDetail: "collapsed",
1707
2572
  autoCompaction: true,
1708
2573
  compactionThreshold: "80%",
1709
- theme: DEFAULT_THEME_NAME,
1710
- allowModelSelfExtension: DEFAULT_ALLOW_MODEL_SELF_EXTENSION,
1711
- allowDevSourceUpdates: false,
1712
- autoEvolveFinderLenses: false,
1713
- autoPromoteFinderLenses: false,
1714
- showTokenUsage: false,
1715
- showCost: false,
1716
- showContextMeter: true,
1717
- modelDisplay: "statusbar",
1718
- elapsedTimer: "left",
1719
- logoAnimation: "glitch",
1720
- reduceMotion: false,
1721
- analyticsLevel: "off",
1722
- diagnosticReporting: "ask",
1723
- diagnosticReportingPrompted: false,
1724
- updatePolicy: "automatic",
1725
- symbolPreset: "unicode",
1726
- rosterSort: "attention",
1727
- leaderKey: "off",
1728
- keybindings: {},
1729
- modelPreference: null
1730
- };
1731
- var SETTINGS_FILENAME = "tui-settings.json";
1732
- function settingsFilePath(homeDir2) {
1733
- return join3(homeStateDir(homeDir2), SETTINGS_FILENAME);
1734
- }
1735
- function projectStateDir(projectDir2 = process.cwd()) {
1736
- return join3(projectDir2, ".0");
1737
- }
1738
- function projectSettingsFilePath(projectDir2 = process.cwd()) {
1739
- return join3(projectStateDir(projectDir2), SETTINGS_FILENAME);
1740
- }
1741
- function readRawSettingsFile(path) {
1742
- try {
1743
- return JSON.parse(readFileSync2(path, "utf8"));
1744
- } catch {
1745
- return void 0;
1746
- }
1747
- }
1748
- function projectSettingsExist(projectDir2 = process.cwd()) {
1749
- const raw = readRawSettingsFile(projectSettingsFilePath(projectDir2));
1750
- return typeof raw === "object" && raw !== null && !Array.isArray(raw);
1751
- }
1752
- function strictValueAt(raw, key) {
1753
- const value = rawValue(raw, key);
1754
- if (value === void 0) return void 0;
1755
- if (key === "onboardingCompleted" || key === "diagnosticReportingPrompted") {
1756
- return typeof value === "boolean" ? value : void 0;
1757
- }
1758
- if (value === false && (key === "diagnosticReporting" || key === "updatePolicy")) {
1759
- return "off";
1760
- }
1761
- if (key === "theme") {
1762
- return typeof value === "string" && isKnownTheme(value) ? value : void 0;
1763
- }
1764
- if (key === "keybindings") {
1765
- return typeof value === "object" && value !== null && !Array.isArray(value) ? sanitizeKeybindingOverrides(value) : void 0;
1766
- }
1767
- if (key === "modelPreference") {
1768
- return normalizeModelPreference(value);
1769
- }
1770
- const def = DEF_BY_KEY.get(key);
1771
- if (def?.kind === "boolean") {
1772
- return typeof value === "boolean" ? value : void 0;
1773
- }
1774
- const choices = def?.choices ?? [];
1775
- return typeof value === "string" && choices.includes(value) ? value : void 0;
1776
- }
1777
- function isOperatorSetting(key) {
1778
- return key === "onboardingCompleted" || key === "analyticsLevel" || key === "diagnosticReporting" || key === "diagnosticReportingPrompted" || key === "updatePolicy" || key === "allowDevSourceUpdates" || key === "executionProfile" || key === "modelPreference";
1779
- }
1780
- function resolveLayeredSettings(globalRaw, projectRaw) {
1781
- const settings = {};
1782
- const sources = {};
1783
- for (const key of Object.keys(DEFAULT_SETTINGS)) {
1784
- const projectValue = isOperatorSetting(key) ? void 0 : strictValueAt(projectRaw, key);
1785
- if (projectValue !== void 0) {
1786
- settings[key] = projectValue;
1787
- sources[key] = "project";
1788
- continue;
1789
- }
1790
- const globalValue = strictValueAt(globalRaw, key);
1791
- if (globalValue !== void 0) {
1792
- settings[key] = globalValue;
1793
- sources[key] = "global";
1794
- continue;
1795
- }
1796
- settings[key] = DEFAULT_SETTINGS[key];
1797
- sources[key] = "default";
1798
- }
1799
- return { settings, sources };
1800
- }
1801
- function loadLayeredSettings(opts = {}) {
1802
- syncThemeChoices(opts.homeDir);
1803
- const globalRaw = readRawSettingsFile(settingsFilePath(opts.homeDir));
1804
- const projectRaw = readRawSettingsFile(projectSettingsFilePath(opts.projectDir));
1805
- return resolveLayeredSettings(globalRaw, projectRaw);
1806
- }
1807
- function rawValue(raw, key) {
1808
- if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return void 0;
1809
- const value = raw[key];
1810
- return key === "transcriptStyle" && value === "messenger" ? "bubble" : value;
1811
- }
1812
- function booleanAt(raw, key) {
1813
- const value = rawValue(raw, key);
1814
- return typeof value === "boolean" ? value : DEFAULT_SETTINGS[key];
1815
- }
1816
- function enumAt(raw, key) {
1817
- const def = DEF_BY_KEY.get(key);
1818
- const value = rawValue(raw, key);
1819
- const choices = def?.choices ?? [];
1820
- return typeof value === "string" && choices.includes(value) ? value : DEFAULT_SETTINGS[key];
1821
- }
1822
- function themeAt(raw) {
1823
- const value = rawValue(raw, "theme");
1824
- return typeof value === "string" && isKnownTheme(value) ? value : DEFAULT_SETTINGS.theme;
1825
- }
1826
- function keybindingsAt(raw) {
1827
- return sanitizeKeybindingOverrides(rawValue(raw, "keybindings"));
1828
- }
1829
- function normalizeModelPreference(raw) {
1830
- if (raw === null) return null;
1831
- if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return void 0;
1832
- const value = raw;
1833
- if (typeof value.providerId !== "string" || !value.providerId.trim() || typeof value.model !== "string" || !value.model.trim() || typeof value.connectionIdentity !== "string" || !/^[a-f0-9]{64}$/.test(value.connectionIdentity)) return void 0;
1834
- return { providerId: value.providerId, model: value.model, connectionIdentity: value.connectionIdentity };
1835
- }
1836
- function normalizeSettings(raw) {
1837
- return {
1838
- showStatusBar: booleanAt(raw, "showStatusBar"),
1839
- showComposerHints: booleanAt(raw, "showComposerHints"),
1840
- composerSuggestions: booleanAt(raw, "composerSuggestions"),
1841
- mouseSupport: booleanAt(raw, "mouseSupport"),
1842
- showLogo: booleanAt(raw, "showLogo"),
1843
- showRuntimeNotices: booleanAt(raw, "showRuntimeNotices"),
1844
- showTurnSummary: booleanAt(raw, "showTurnSummary"),
1845
- showSubagents: booleanAt(raw, "showSubagents"),
1846
- showTimestamps: booleanAt(raw, "showTimestamps"),
1847
- showObjective: booleanAt(raw, "showObjective"),
1848
- showScope: booleanAt(raw, "showScope"),
1849
- density: enumAt(raw, "density"),
1850
- composerStyle: enumAt(raw, "composerStyle"),
1851
- busyInputMode: enumAt(raw, "busyInputMode"),
1852
- onboardingCompleted: booleanAt(raw, "onboardingCompleted"),
1853
- executionProfile: enumAt(raw, "executionProfile"),
1854
- allowSubagentPeerMessaging: booleanAt(raw, "allowSubagentPeerMessaging"),
1855
- allowSubagentOperatorMessaging: booleanAt(raw, "allowSubagentOperatorMessaging"),
1856
- transcriptStyle: enumAt(raw, "transcriptStyle"),
1857
- roleLabelStyle: enumAt(raw, "roleLabelStyle"),
1858
- toolCardStyle: enumAt(raw, "toolCardStyle"),
1859
- richToolCards: booleanAt(raw, "richToolCards"),
1860
- transcriptDetail: enumAt(raw, "transcriptDetail"),
1861
- autoCompaction: booleanAt(raw, "autoCompaction"),
1862
- compactionThreshold: enumAt(raw, "compactionThreshold"),
1863
- theme: themeAt(raw),
1864
- allowModelSelfExtension: booleanAt(raw, "allowModelSelfExtension"),
1865
- allowDevSourceUpdates: booleanAt(raw, "allowDevSourceUpdates"),
1866
- autoEvolveFinderLenses: booleanAt(raw, "autoEvolveFinderLenses"),
1867
- autoPromoteFinderLenses: booleanAt(raw, "autoPromoteFinderLenses"),
1868
- showTokenUsage: booleanAt(raw, "showTokenUsage"),
1869
- showCost: booleanAt(raw, "showCost"),
1870
- showContextMeter: booleanAt(raw, "showContextMeter"),
1871
- modelDisplay: enumAt(raw, "modelDisplay"),
1872
- elapsedTimer: enumAt(raw, "elapsedTimer"),
1873
- logoAnimation: enumAt(raw, "logoAnimation"),
1874
- reduceMotion: booleanAt(raw, "reduceMotion"),
1875
- analyticsLevel: enumAt(raw, "analyticsLevel"),
1876
- diagnosticReporting: strictValueAt(raw, "diagnosticReporting") ?? DEFAULT_SETTINGS.diagnosticReporting,
1877
- diagnosticReportingPrompted: booleanAt(raw, "diagnosticReportingPrompted"),
1878
- updatePolicy: strictValueAt(raw, "updatePolicy") ?? DEFAULT_SETTINGS.updatePolicy,
1879
- symbolPreset: enumAt(raw, "symbolPreset"),
1880
- rosterSort: enumAt(raw, "rosterSort"),
1881
- leaderKey: enumAt(raw, "leaderKey"),
1882
- keybindings: keybindingsAt(raw),
1883
- modelPreference: normalizeModelPreference(rawValue(raw, "modelPreference")) ?? null
1884
- };
1885
- }
1886
- function loadSettings(homeDir2, projectDir2) {
1887
- return loadLayeredSettings({ homeDir: homeDir2, projectDir: projectDir2 }).settings;
1888
- }
1889
- function loadGlobalSettings(homeDir2, options = {}) {
1890
- syncThemeChoices(homeDir2);
1891
- const raw = readRawSettingsFile(settingsFilePath(homeDir2));
1892
- if (options.requireExecutionProfile && strictValueAt(raw, "executionProfile") === void 0) {
1893
- throw new Error("Configured workbench execution profile is missing or invalid. Run 0 workbench setup or 0 workbench disable to make an explicit operator choice; host fallback is refused.");
1894
- }
1895
- return normalizeSettings(raw);
1896
- }
1897
- function saveSettings(settings, homeDir2) {
1898
- const path = settingsFilePath(homeDir2);
1899
- const temporary = `${path}.${randomUUID()}.tmp`;
1900
- try {
1901
- mkdirSync2(dirname2(path), { recursive: true });
1902
- writeFileSync2(temporary, `${JSON.stringify(normalizeSettings(settings), null, 2)}
1903
- `, { encoding: "utf8", flag: "wx", mode: 384 });
1904
- renameSync(temporary, path);
1905
- return true;
1906
- } catch {
1907
- return false;
1908
- } finally {
1909
- try {
1910
- rmSync2(temporary, { force: true });
1911
- } catch {
1912
- }
1913
- }
1914
- }
1915
- function sanitizeOverrides(raw) {
1916
- const out = {};
1917
- for (const key of Object.keys(DEFAULT_SETTINGS)) {
1918
- if (isOperatorSetting(key)) continue;
1919
- const value = strictValueAt(raw, key);
1920
- if (value !== void 0) out[key] = value;
1921
- }
1922
- return out;
1923
- }
1924
- function readProjectOverrides(projectDir2) {
1925
- return sanitizeOverrides(readRawSettingsFile(projectSettingsFilePath(projectDir2)));
1926
- }
1927
- function saveProjectOverrides(patch, projectDir2) {
1928
- try {
1929
- const path = projectSettingsFilePath(projectDir2);
1930
- mkdirSync2(dirname2(path), { recursive: true });
1931
- writeFileSync2(path, `${JSON.stringify(sanitizeOverrides(patch), null, 2)}
1932
- `, "utf8");
1933
- return true;
1934
- } catch {
1935
- return false;
1936
- }
1937
- }
1938
-
1939
- // packages/cli/src/tui/settings-store.ts
1940
- var cached = null;
1941
- var globalDraft = null;
1942
- var projectDraft = null;
1943
- var homeDir;
1944
- var projectDir;
1945
- var subscribers = /* @__PURE__ */ new Set();
1946
- function notify(settings) {
1947
- bridgeAnalyticsLevel(settings);
1948
- for (const fn of [...subscribers]) {
1949
- try {
1950
- fn(settings);
1951
- } catch {
1952
- }
1953
- }
1954
- }
1955
- function loadLayered() {
1956
- return globalDraft === null && projectDraft === null ? loadLayeredSettings({ homeDir, projectDir }) : resolveLayeredSettings(globalDraft ?? loadGlobalSettings(homeDir), projectDraft ?? readProjectOverrides(projectDir));
1957
- }
1958
- function getSettings() {
1959
- if (cached === null) {
1960
- cached = loadLayered();
1961
- bridgeAnalyticsLevel(cached.settings);
1962
- }
1963
- return cached.settings;
1964
- }
1965
- function getSettingSources() {
1966
- getSettings();
1967
- return cached.sources;
1968
- }
1969
- function setSettings(next) {
1970
- const value = normalizeSettings(next);
1971
- const saved = saveSettings(value, homeDir);
1972
- globalDraft = saved ? null : value;
1973
- cached = loadLayered();
1974
- notify(cached.settings);
1975
- return saved;
1976
- }
1977
- function defaultWriteLayer() {
1978
- return projectSettingsExist(projectDir) ? "project" : "global";
1979
- }
1980
- function updateSetting(key, value, opts = {}) {
1981
- const operatorOnly = isOperatorSetting(key);
1982
- if (operatorOnly && opts.scope === "project") return false;
1983
- const layer = operatorOnly ? "global" : opts.scope ?? defaultWriteLayer();
1984
- if (layer === "global") {
1985
- return setSettings({ ...globalDraft ?? loadGlobalSettings(homeDir), [key]: value });
1986
- }
1987
- const patch = { ...projectDraft ?? readProjectOverrides(projectDir), [key]: value };
1988
- const saved = saveProjectOverrides(patch, projectDir);
1989
- projectDraft = saved ? null : patch;
1990
- cached = loadLayered();
1991
- notify(cached.settings);
1992
- return saved;
1993
- }
1994
- function resetSettings(next, keys) {
1995
- const selected = keys.filter((key) => SETTING_DEFS.some((def) => def.key === key));
1996
- if (selected.length === 0) return true;
1997
- const global = { ...globalDraft ?? loadGlobalSettings(homeDir) };
1998
- const project = { ...projectDraft ?? readProjectOverrides(projectDir) };
1999
- let projectChanged = false;
2000
- for (const key of selected) {
2001
- global[key] = next[key];
2002
- if (Object.hasOwn(project, key)) {
2003
- delete project[key];
2004
- projectChanged = true;
2005
- }
2006
- }
2007
- const value = normalizeSettings(global);
2008
- const globalSaved = saveSettings(value, homeDir);
2009
- globalDraft = globalSaved ? null : value;
2010
- const projectSaved = !projectChanged || saveProjectOverrides(project, projectDir);
2011
- if (projectChanged) projectDraft = projectSaved ? null : project;
2012
- cached = loadLayered();
2013
- notify(cached.settings);
2014
- return globalSaved && projectSaved;
2015
- }
2016
- function previewSetting(key, value) {
2017
- if (cached === null) cached = loadLayered();
2018
- cached = { settings: { ...cached.settings, [key]: value }, sources: cached.sources };
2019
- notify(cached.settings);
2020
- }
2021
- function subscribeSettings(fn) {
2022
- subscribers.add(fn);
2023
- return () => {
2024
- subscribers.delete(fn);
2025
- };
2026
- }
2027
- var bridgedAnalyticsEnv;
2028
- function bridgeAnalyticsLevel(settings) {
2029
- try {
2030
- const current = process.env[ANALYTICS_LEVEL_ENV];
2031
- if (current === void 0 || current === bridgedAnalyticsEnv) {
2032
- process.env[ANALYTICS_LEVEL_ENV] = settings.analyticsLevel;
2033
- bridgedAnalyticsEnv = settings.analyticsLevel;
2034
- } else {
2035
- bridgedAnalyticsEnv = void 0;
2036
- }
2037
- const environmentLevel = resolveAnalyticsLevel();
2038
- analyticsPipeline.setLevel(levelAtLeast(environmentLevel, settings.analyticsLevel) ? settings.analyticsLevel : environmentLevel);
2039
- } catch {
2040
- }
2041
- }
2042
- function reloadSettings() {
2043
- globalDraft = null;
2044
- projectDraft = null;
2045
- cached = loadLayered();
2046
- notify(cached.settings);
2047
- return cached.settings;
2048
- }
2049
- function configureSettingsStore(options) {
2050
- homeDir = options.homeDir;
2051
- projectDir = options.projectDir;
2052
- reloadSettings();
2053
- }
2054
- function useSettings() {
2055
- return useSyncExternalStore(subscribeSettings, getSettings, getSettings);
2056
- }
2057
-
2058
- // packages/cli/src/codex-auth.ts
2059
- import { readFileSync as readFileSync3, existsSync } from "node:fs";
2060
- import { homedir } from "node:os";
2061
- import { join as join4 } from "node:path";
2062
- var AUTH_FILE_ENV = "ZERO_CHATGPT_AUTH_FILE";
2063
- var LEGACY_AUTH_FILE_ENV = "ZERO_CODEX_AUTH_JSON_PATH";
2064
- function maybeLoadCodexAuth(options = {}) {
2065
- const env = options.env ?? process.env;
2066
- if (!options.force && (env["ZERO_CHATGPT_ACCESS_TOKEN"] || env["ZERO_CHATGPT_OAUTH_REFRESH_TOKEN"])) {
2067
- return;
2068
- }
2069
- const home = options.home ?? homedir();
2070
- const override = nonEmpty(env[AUTH_FILE_ENV]) ?? nonEmpty(env[LEGACY_AUTH_FILE_ENV]);
2071
- const path = override ?? join4(home, ".codex", "auth.json");
2072
- if (!existsSync(path)) return;
2073
- try {
2074
- const raw = JSON.parse(readFileSync3(path, "utf8"));
2075
- const accessToken = nonEmpty(asString(raw.tokens?.access_token));
2076
- const refreshToken = nonEmpty(asString(raw.tokens?.refresh_token));
2077
- if (!accessToken && !refreshToken) return;
2078
- if (options.force) {
2079
- delete env["ZERO_CHATGPT_ACCESS_TOKEN"];
2080
- delete env["ZERO_CHATGPT_OAUTH_REFRESH_TOKEN"];
2081
- }
2082
- if (accessToken) {
2083
- env["ZERO_CHATGPT_ACCESS_TOKEN"] = accessToken;
2084
- }
2085
- if (refreshToken) {
2086
- env["ZERO_CHATGPT_OAUTH_REFRESH_TOKEN"] = refreshToken;
2087
- }
2088
- } catch {
2089
- }
2090
- }
2091
- function nonEmpty(value) {
2092
- return typeof value === "string" && value.length > 0 ? value : void 0;
2093
- }
2094
- function asString(value) {
2095
- return typeof value === "string" ? value : void 0;
2096
- }
2097
-
2098
- // packages/cli/src/presentation/event-bus.ts
2099
- var PresentationEventBus = class {
2100
- listeners = [];
2101
- subscribe(listener) {
2102
- this.listeners.push(listener);
2103
- return () => {
2104
- const index = this.listeners.indexOf(listener);
2105
- if (index >= 0) this.listeners.splice(index, 1);
2106
- };
2107
- }
2108
- emit(event) {
2109
- for (const listener of [...this.listeners]) {
2110
- try {
2111
- listener.emit(event);
2112
- } catch {
2113
- }
2114
- }
2115
- }
2116
- get size() {
2117
- return this.listeners.length;
2118
- }
2119
- };
2120
- var presentationEventBus = new PresentationEventBus();
2121
- function createPresentationEmitter(options = {}) {
2122
- const source = options.source ?? "cli";
2123
- const now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
2124
- const bus = options.bus ?? presentationEventBus;
2125
- let sequence = 0;
2126
- return {
2127
- emit(eventType, payload, correlation = {}) {
2128
- const event = createPresentationEvent({
2129
- source,
2130
- sequence: ++sequence,
2131
- at: now(),
2132
- eventType,
2133
- payload,
2134
- ...correlation.scanId ? { scanId: correlation.scanId } : {},
2135
- ...correlation.sessionId ? { sessionId: correlation.sessionId } : {}
2136
- });
2137
- bus.emit(event);
2138
- return event;
2139
- }
2140
- };
2141
- }
2142
-
2143
- // packages/cli/src/presentation/process-output.ts
2144
- import { format } from "node:util";
2145
- var streamBridgeSuppressionDepth = 0;
2146
- var activeStreamBridge = null;
2147
- function currentPresentationTimestamp() {
2148
- return (/* @__PURE__ */ new Date()).toISOString();
2149
- }
2150
- function createProcessPresentationOutput(options = {}) {
2151
- const stdout = options.stdout ?? { write: process.stdout.write.bind(process.stdout) };
2152
- const stderr = options.stderr ?? { write: process.stderr.write.bind(process.stderr) };
2153
- const source = options.source ?? "cli";
2154
- const now = options.now ?? currentPresentationTimestamp;
2155
- let sequence = 0;
2156
- const write = (channel, target, text, eventType) => {
2157
- const event = createPresentationEvent({
2158
- source,
2159
- sequence: ++sequence,
2160
- at: now(),
2161
- eventType,
2162
- payload: { channel, text }
2163
- });
2164
- streamBridgeSuppressionDepth += 1;
2165
- try {
2166
- target.write(text);
2167
- } finally {
2168
- streamBridgeSuppressionDepth -= 1;
2169
- }
2170
- options.onEvent?.(event);
2171
- return event;
2172
- };
2173
- return {
2174
- stdout(text, eventType = "output.stdout") {
2175
- return write("stdout", stdout, text, eventType);
2176
- },
2177
- stderr(text, eventType = "output.stderr") {
2178
- return write("stderr", stderr, text, eventType);
2179
- }
2180
- };
2181
- }
2182
- function createConsolePresentationOutput(options = {}) {
2183
- const source = options.source ?? "cli";
2184
- const now = options.now ?? currentPresentationTimestamp;
2185
- let sequence = 0;
2186
- const emit = (channel, text, eventType) => {
2187
- const event = createPresentationEvent({
2188
- source,
2189
- sequence: ++sequence,
2190
- at: now(),
2191
- eventType,
2192
- payload: { channel, text }
2193
- });
2194
- streamBridgeSuppressionDepth += 1;
2195
- try {
2196
- if (channel === "stdout") {
2197
- (options.log ?? console.log)(text);
2198
- } else {
2199
- (options.error ?? console.error)(text);
2200
- }
2201
- } finally {
2202
- streamBridgeSuppressionDepth -= 1;
2203
- }
2204
- options.onEvent?.(event);
2205
- return event;
2206
- };
2207
- return {
2208
- stdout(text, eventType = "output.stdout") {
2209
- return emit("stdout", text, eventType);
2210
- },
2211
- stderr(text, eventType = "output.stderr") {
2212
- return emit("stderr", text, eventType);
2213
- }
2214
- };
2215
- }
2216
- var consolePresentationOutput = createConsolePresentationOutput({
2217
- onEvent(event) {
2218
- presentationEventBus.emit(event);
2219
- }
2220
- });
2221
- function installConsolePresentationBridge(options = {}) {
2222
- const target = options.target ?? console;
2223
- const originals = {
2224
- log: target.log,
2225
- info: target.info,
2226
- warn: target.warn,
2227
- error: target.error
2228
- };
2229
- const source = options.source ?? "cli";
2230
- const now = options.now ?? currentPresentationTimestamp;
2231
- let sequence = 0;
2232
- const emit = (method, channel, args, eventType) => {
2233
- const text = format(...args);
2234
- if (streamBridgeSuppressionDepth > 0) {
2235
- originals[method].call(target, text);
2236
- return;
2237
- }
2238
- const event = createPresentationEvent({
2239
- source,
2240
- sequence: ++sequence,
2241
- at: now(),
2242
- eventType,
2243
- payload: { channel, text }
2244
- });
2245
- streamBridgeSuppressionDepth += 1;
2246
- try {
2247
- originals[method].call(target, text);
2248
- } finally {
2249
- streamBridgeSuppressionDepth -= 1;
2250
- }
2251
- presentationEventBus.emit(event);
2252
- options.onEvent?.(event);
2253
- };
2254
- const bridgedLog = (...args) => emit("log", "stdout", args, "output.console.log");
2255
- const bridgedInfo = (...args) => emit("info", "stdout", args, "output.console.info");
2256
- const bridgedWarn = (...args) => emit("warn", "stderr", args, "output.console.warn");
2257
- const bridgedError = (...args) => emit("error", "stderr", args, "output.console.error");
2258
- target.log = bridgedLog;
2259
- target.info = bridgedInfo;
2260
- target.warn = bridgedWarn;
2261
- target.error = bridgedError;
2262
- return {
2263
- restore() {
2264
- if (target.log === bridgedLog) target.log = originals.log;
2265
- if (target.info === bridgedInfo) target.info = originals.info;
2266
- if (target.warn === bridgedWarn) target.warn = originals.warn;
2267
- if (target.error === bridgedError) target.error = originals.error;
2268
- }
2269
- };
2270
- }
2271
- function installProcessPresentationStreamBridge(options = {}) {
2272
- if (activeStreamBridge) return activeStreamBridge;
2273
- const source = options.source ?? "cli";
2274
- const now = options.now ?? currentPresentationTimestamp;
2275
- let sequence = 0;
2276
- let suspended = false;
2277
- const streams = [];
2278
- const emit = (channel, chunk, encoding) => {
2279
- const text = typeof chunk === "string" ? chunk : chunk instanceof Uint8Array ? Buffer.from(chunk).toString(typeof encoding === "string" ? encoding : "utf8") : String(chunk ?? "");
2280
- if (channel === "stdout" && text.startsWith("ZERO_EVENT_")) return;
2281
- const event = createPresentationEvent({
2282
- source,
2283
- sequence: ++sequence,
2284
- at: now(),
2285
- eventType: channel === "stdout" ? "output.stream.stdout" : "output.stream.stderr",
2286
- payload: { channel, text }
2287
- });
2288
- presentationEventBus.emit(event);
2289
- options.onEvent?.(event);
2290
- };
2291
- for (const [channel, stream] of [
2292
- ["stdout", options.stdout ?? process.stdout],
2293
- ["stderr", options.stderr ?? process.stderr]
2294
- ]) {
2295
- const original = stream.write;
2296
- const patched = ((chunk, encoding, callback) => {
2297
- const result = original.call(stream, chunk, encoding, callback);
2298
- if (!suspended && streamBridgeSuppressionDepth === 0) emit(channel, chunk, encoding);
2299
- return result;
2300
- });
2301
- streams.push({ channel, stream, original, patched });
2302
- }
2303
- const bridge = {
2304
- suspend() {
2305
- if (suspended) return;
2306
- suspended = true;
2307
- for (const item of streams) {
2308
- if (item.stream.write === item.patched) item.stream.write = item.original;
2309
- }
2310
- },
2311
- resume() {
2312
- if (!suspended) return;
2313
- suspended = false;
2314
- for (const item of streams) {
2315
- if (item.stream.write === item.original) item.stream.write = item.patched;
2316
- }
2317
- },
2318
- restore() {
2319
- for (const item of streams) {
2320
- if (item.stream.write === item.patched) item.stream.write = item.original;
2321
- }
2322
- if (activeStreamBridge === bridge) activeStreamBridge = null;
2323
- }
2324
- };
2325
- for (const item of streams) item.stream.write = item.patched;
2326
- activeStreamBridge = bridge;
2327
- return bridge;
2328
- }
2329
- function suspendProcessPresentationStreamBridge() {
2330
- activeStreamBridge?.suspend();
2331
- }
2332
- function resumeProcessPresentationStreamBridge() {
2333
- activeStreamBridge?.resume();
2334
- }
2335
- var processPresentationOutput = createProcessPresentationOutput({
2336
- onEvent(event) {
2337
- presentationEventBus.emit(event);
2338
- }
2339
- });
2340
- function writePresentationLine(text, eventType = "output.stdout") {
2341
- return processPresentationOutput.stdout(`${text}
2342
- `, eventType);
2343
- }
2344
-
2345
- // packages/cli/src/tui/provider-status.ts
2346
- var PROVIDER_DEFS = [
2347
- {
2348
- id: "chatgpt-codex",
2349
- label: "ChatGPT Codex",
2350
- methods: ["oauth"],
2351
- // OAuth, not an API key. Both tokens are accepted and the access token is
2352
- // read first (llm-api.ts L874-875, L1386-1394), so it leads the list.
2353
- envVars: ["ZERO_CHATGPT_ACCESS_TOKEN", "ZERO_CHATGPT_OAUTH_REFRESH_TOKEN"],
2354
- fileSource: "~/.codex/auth.json (override with ZERO_CHATGPT_AUTH_FILE)",
2355
- hint: "run `codex login` to write ~/.codex/auth.json, or invoke 0 with env ZERO_CHATGPT_OAUTH_REFRESH_TOKEN=..."
2356
- },
2357
- {
2358
- id: "deepseek",
2359
- label: "DeepSeek",
2360
- methods: ["api-key"],
2361
- envVars: ["DEEPSEEK_API_KEY"],
2362
- hint: "set DEEPSEEK_API_KEY (endpoint override: DEEPSEEK_BASE_URL)"
2363
- },
2364
- {
2365
- id: "openrouter",
2366
- label: "OpenRouter",
2367
- // OAuth (PKCE browser sign-in) is preferred; a pasted OPENROUTER_API_KEY
2368
- // remains a secondary path. The browser flow provisions a durable
2369
- // `sk-or-...` key and stores it as an api_key record written to
2370
- // OPENROUTER_API_KEY (envVars[0], which llm-api already reads), so — unlike
2371
- // the xai/kimi device flows — the credential is a key, not OAuth tokens.
2372
- methods: ["oauth", "api-key"],
2373
- envVars: ["OPENROUTER_API_KEY"],
2374
- hint: "sign in with your OpenRouter account, or set OPENROUTER_API_KEY=sk-or-... from openrouter.ai/keys"
2375
- },
2376
- {
2377
- id: "azure",
2378
- label: "Azure OpenAI",
2379
- methods: ["api-key"],
2380
- // Only the Azure key authenticates; the deployment URL comes from
2381
- // AZURE_OPENAI_BASE_URL / OPENAI_BASE_URL / ~/.codex/config.toml
2382
- // (L1435-1445). A key with no reachable base URL still counts as
2383
- // configured here because detectProvider selects azure on the key alone.
2384
- envVars: ["AZURE_OPENAI_API_KEY"],
2385
- hint: "set AZURE_OPENAI_API_KEY plus AZURE_OPENAI_BASE_URL (or [model_providers.azure] in ~/.codex/config.toml)"
2386
- },
2387
- {
2388
- id: "openai",
2389
- label: "OpenAI",
2390
- methods: ["api-key"],
2391
- envVars: ["OPENAI_API_KEY"],
2392
- hint: "set OPENAI_API_KEY=sk-... from platform.openai.com/api-keys"
2393
- },
2394
- {
2395
- id: "z-ai",
2396
- label: "Z.ai GLM",
2397
- methods: ["api-key"],
2398
- envVars: ["Z_AI_API_KEY"],
2399
- hint: "set Z_AI_API_KEY from your Z.ai Coding Plan (endpoint override: Z_AI_BASE_URL)"
2400
- },
2401
- {
2402
- id: "kimi",
2403
- label: "Moonshot Kimi",
2404
- // OAuth (device sign-in) is preferred; a pasted KIMI_API_KEY remains a
2405
- // secondary path. The OAuth access token is written to KIMI_API_KEY as a
2406
- // Bearer (llm-api.ts reads it there, no change needed), and the refresh
2407
- // token to the 0-owned var so the store can round-trip it.
2408
- methods: ["oauth", "api-key"],
2409
- envVars: ["KIMI_API_KEY", "ZERO_KIMI_OAUTH_REFRESH_TOKEN"],
2410
- hint: "sign in with your Kimi account, or set KIMI_API_KEY from your Kimi coding plan (endpoint override: KIMI_BASE_URL)"
2411
- },
2412
- {
2413
- id: "qwen",
2414
- label: "Alibaba Qwen",
2415
- methods: ["api-key"],
2416
- envVars: ["QWEN_API_KEY"],
2417
- hint: "set QWEN_API_KEY from Alibaba Model Studio (endpoint override: QWEN_BASE_URL)"
2418
- },
2419
- {
2420
- id: "xai",
2421
- label: "xAI Grok",
2422
- // OAuth (device sign-in) is preferred; a pasted XAI_API_KEY remains a
2423
- // secondary path. The OAuth access token is written to XAI_API_KEY as a
2424
- // Bearer (llm-api.ts reads it there, no change needed), and the refresh
2425
- // token to the 0-owned var so the store can round-trip it.
2426
- methods: ["oauth", "api-key"],
2427
- envVars: ["XAI_API_KEY", "ZERO_XAI_OAUTH_REFRESH_TOKEN"],
2428
- hint: "sign in with your xAI account, or set XAI_API_KEY from console.x.ai (endpoint override: XAI_BASE_URL)"
2429
- },
2430
- {
2431
- id: "opencode",
2432
- label: "OpenCode Zen",
2433
- methods: ["api-key"],
2434
- envVars: ["OPENCODE_API_KEY"],
2435
- hint: "set OPENCODE_API_KEY from opencode.ai/auth (endpoint override: OPENCODE_BASE_URL)"
2436
- },
2437
- {
2438
- id: "copilot",
2439
- label: "GitHub Copilot",
2440
- // OAuth only (device-code sign-in). The GitHub device-flow access token is
2441
- // written to ZERO_COPILOT_GITHUB_TOKEN (envVars[0]) and sent directly as a
2442
- // Bearer to api.githubcopilot.com — no secondary exchange, no refresh, so
2443
- // there is no refresh-token env var.
2444
- methods: ["oauth"],
2445
- envVars: ["ZERO_COPILOT_GITHUB_TOKEN"],
2446
- hint: "sign in with your GitHub Copilot account (device sign-in), or set ZERO_COPILOT_GITHUB_TOKEN=... (endpoint override: COPILOT_BASE_URL)"
2447
- },
2448
- {
2449
- id: "google",
2450
- label: "Google Gemini (Code Assist)",
2451
- // OAuth only (PKCE browser sign-in — the Gemini CLI flow). The minted
2452
- // access token is written to ZERO_GEMINI_ACCESS_TOKEN (envVars[0]) and the
2453
- // refresh token to ZERO_GEMINI_OAUTH_REFRESH_TOKEN (the /REFRESH/i var); the
2454
- // runtime refreshes on demand against oauth2.googleapis.com. There is no
2455
- // pasted-key equivalent — Code Assist authenticates only via OAuth.
2456
- methods: ["oauth"],
2457
- envVars: ["ZERO_GEMINI_ACCESS_TOKEN", "ZERO_GEMINI_OAUTH_REFRESH_TOKEN"],
2458
- hint: "sign in with your Google account (browser sign-in), or set ZERO_GEMINI_OAUTH_REFRESH_TOKEN=... for Gemini Code Assist (project override: GOOGLE_CLOUD_PROJECT / ZERO_GEMINI_PROJECT)"
2459
- },
2460
- {
2461
- id: "anthropic",
2462
- label: "Anthropic",
2463
- // API key only for now. The schema supports OAuth, but Anthropic/Claude
2464
- // subscription OAuth is deliberately deferred — do not add "oauth" here
2465
- // until that workstream lands.
2466
- methods: ["api-key"],
2467
- envVars: ["ANTHROPIC_API_KEY"],
2468
- hint: "set ANTHROPIC_API_KEY=sk-ant-... from console.anthropic.com"
2469
- }
2470
- ];
2471
- function primaryMethod(methods) {
2472
- return methods[0] ?? "api-key";
2473
- }
2474
- var PROVIDERS = PROVIDER_DEFS.map((def) => ({
2475
- ...def,
2476
- auth: primaryMethod(def.methods)
2477
- }));
2478
- function providerSupportsMethod(providerId, method) {
2479
- const info = PROVIDERS.find((candidate) => candidate.id === providerId);
2480
- return info !== void 0 && info.methods.includes(method);
2481
- }
2482
- function hasCredential(value) {
2483
- return typeof value === "string" && value.trim().length > 0;
2484
- }
2485
- function satisfyingVar(info, env) {
2486
- return info.envVars.find((name) => hasCredential(env[name]));
2487
- }
2488
- function providerStates(env) {
2489
- return PROVIDERS.map((info) => {
2490
- const via = satisfyingVar(info, env);
2491
- return via === void 0 ? { ...info, configured: false } : { ...info, configured: true, via };
2492
- });
2574
+ theme: DEFAULT_THEME_NAME,
2575
+ allowModelSelfExtension: DEFAULT_ALLOW_MODEL_SELF_EXTENSION,
2576
+ allowDevSourceUpdates: false,
2577
+ autoEvolveFinderLenses: false,
2578
+ autoPromoteFinderLenses: false,
2579
+ showTokenUsage: false,
2580
+ showCost: false,
2581
+ showContextMeter: true,
2582
+ modelDisplay: "statusbar",
2583
+ elapsedTimer: "left",
2584
+ logoAnimation: "glitch",
2585
+ reduceMotion: false,
2586
+ analyticsLevel: "off",
2587
+ diagnosticReporting: "ask",
2588
+ diagnosticReportingPrompted: false,
2589
+ updatePolicy: "automatic",
2590
+ symbolPreset: "unicode",
2591
+ rosterSort: "attention",
2592
+ leaderKey: "off",
2593
+ keybindings: {},
2594
+ modelPreference: null
2595
+ };
2596
+ var SETTINGS_FILENAME = "tui-settings.json";
2597
+ function settingsFilePath(homeDir) {
2598
+ return join6(homeStateDir(homeDir), SETTINGS_FILENAME);
2493
2599
  }
2494
- function isProviderConfigured(providerId, env) {
2495
- const info = PROVIDERS.find((candidate) => candidate.id === providerId);
2496
- return info !== void 0 && satisfyingVar(info, env) !== void 0;
2600
+ function projectStateDir(projectDir = process.cwd()) {
2601
+ return join6(projectDir, ".0");
2497
2602
  }
2498
-
2499
- // packages/cli/src/tui/credential-store.ts
2500
- import { chmodSync, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
2501
- import { dirname as dirname3, join as join5 } from "node:path";
2502
- var CREDENTIALS_FILENAME = "credentials.json";
2503
- var FILE_MODE = 384;
2504
- var DIR_MODE = 448;
2505
- function credentialsFilePath(homeDir2) {
2506
- return join5(homeStateDir(homeDir2), CREDENTIALS_FILENAME);
2507
- }
2508
- var STORABLE_PROVIDER_IDS = new Set(
2509
- PROVIDERS.filter((provider) => provider.methods.includes("api-key")).map((provider) => provider.id)
2510
- );
2511
- function hasCredential2(value) {
2512
- return typeof value === "string" && value.trim().length > 0;
2603
+ function projectSettingsFilePath(projectDir = process.cwd()) {
2604
+ return join6(projectStateDir(projectDir), SETTINGS_FILENAME);
2513
2605
  }
2514
- function normalizeCredentials(raw) {
2515
- const out = {};
2516
- if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return out;
2517
- for (const [providerId, value] of Object.entries(raw)) {
2518
- if (!STORABLE_PROVIDER_IDS.has(providerId)) continue;
2519
- if (typeof value !== "string") continue;
2520
- const secret = value.trim();
2521
- if (secret.length === 0) continue;
2522
- out[providerId] = secret;
2606
+ function readRawSettingsFile(path) {
2607
+ try {
2608
+ return JSON.parse(readFileSync2(path, "utf8"));
2609
+ } catch {
2610
+ return void 0;
2523
2611
  }
2524
- return out;
2525
2612
  }
2526
- function loadCredentials(homeDir2) {
2527
- return projectApiKeys(loadAccountStore(homeDir2));
2528
- }
2529
- function saveCredentials(creds, homeDir2) {
2530
- const store = loadAccountStore(homeDir2);
2531
- const desired = normalizeCredentials(creds);
2532
- return saveAccountStore(replaceApiKeyLayer(store, desired), homeDir2);
2613
+ function projectSettingsExist(projectDir = process.cwd()) {
2614
+ const raw = readRawSettingsFile(projectSettingsFilePath(projectDir));
2615
+ return typeof raw === "object" && raw !== null && !Array.isArray(raw);
2533
2616
  }
2534
- function projectApiKeys(store) {
2535
- const out = {};
2536
- for (const providerId of Object.keys(store.providers)) {
2537
- const record = getActiveAccount(store, providerId);
2538
- if (record?.kind === "api_key") out[providerId] = record.secret;
2617
+ function strictValueAt(raw, key) {
2618
+ const value = rawValue(raw, key);
2619
+ if (value === void 0) return void 0;
2620
+ if (key === "onboardingCompleted" || key === "diagnosticReportingPrompted") {
2621
+ return typeof value === "boolean" ? value : void 0;
2539
2622
  }
2540
- return out;
2623
+ if (value === false && (key === "diagnosticReporting" || key === "updatePolicy")) {
2624
+ return "off";
2625
+ }
2626
+ if (key === "theme") {
2627
+ return typeof value === "string" && isKnownTheme(value) ? value : void 0;
2628
+ }
2629
+ if (key === "keybindings") {
2630
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? sanitizeKeybindingOverrides(value) : void 0;
2631
+ }
2632
+ if (key === "modelPreference") {
2633
+ return normalizeModelPreference(value);
2634
+ }
2635
+ const def = DEF_BY_KEY.get(key);
2636
+ if (def?.kind === "boolean") {
2637
+ return typeof value === "boolean" ? value : void 0;
2638
+ }
2639
+ const choices = def?.choices ?? [];
2640
+ return typeof value === "string" && choices.includes(value) ? value : void 0;
2641
+ }
2642
+ function isOperatorSetting(key) {
2643
+ return key === "onboardingCompleted" || key === "analyticsLevel" || key === "diagnosticReporting" || key === "diagnosticReportingPrompted" || key === "updatePolicy" || key === "allowDevSourceUpdates" || key === "executionProfile" || key === "modelPreference";
2541
2644
  }
2542
- function replaceApiKeyLayer(store, desired) {
2543
- const providers = {};
2544
- for (const [providerId, entry] of Object.entries(store.providers)) {
2545
- const kept = {};
2546
- for (const [accountId, record] of Object.entries(entry.accounts)) {
2547
- if (record.kind !== "api_key") kept[accountId] = record;
2645
+ function resolveLayeredSettings(globalRaw, projectRaw) {
2646
+ const settings = {};
2647
+ const sources = {};
2648
+ for (const key of Object.keys(DEFAULT_SETTINGS)) {
2649
+ const projectValue = isOperatorSetting(key) ? void 0 : strictValueAt(projectRaw, key);
2650
+ if (projectValue !== void 0) {
2651
+ settings[key] = projectValue;
2652
+ sources[key] = "project";
2653
+ continue;
2548
2654
  }
2549
- const keptIds = Object.keys(kept);
2550
- if (keptIds.length === 0) continue;
2551
- const active = entry.activeAccountId !== void 0 && kept[entry.activeAccountId] !== void 0 ? entry.activeAccountId : keptIds[0];
2552
- providers[providerId] = { activeAccountId: active, accounts: kept };
2553
- }
2554
- for (const [providerId, secret] of Object.entries(desired)) {
2555
- const existing = providers[providerId];
2556
- const accounts = existing ? { ...existing.accounts } : {};
2557
- accounts[DEFAULT_ACCOUNT_ID] = { kind: "api_key", secret };
2558
- providers[providerId] = { activeAccountId: DEFAULT_ACCOUNT_ID, accounts };
2559
- }
2560
- return { version: STORE_VERSION, providers };
2561
- }
2562
- function credentialEnvPatch(creds, env) {
2563
- const stored = normalizeCredentials(creds);
2564
- const patch = {};
2565
- for (const info of PROVIDERS) {
2566
- const secret = stored[info.id];
2567
- if (secret === void 0) continue;
2568
- if (info.envVars.some((name) => hasCredential2(env[name]))) continue;
2569
- const target = info.envVars[0];
2570
- if (target === void 0) continue;
2571
- patch[target] = secret;
2572
- }
2573
- return patch;
2574
- }
2575
- var MASK = "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
2576
- var TAIL = 4;
2577
- var PREFIX = 6;
2578
- var MIN_LENGTH_FOR_TAIL = 12;
2579
- var MIN_LENGTH_FOR_PREFIX = 20;
2580
- function redactSecret(secret) {
2581
- const value = typeof secret === "string" ? secret.trim() : "";
2582
- if (value.length < MIN_LENGTH_FOR_TAIL) return MASK;
2583
- const tail = value.slice(-TAIL);
2584
- if (value.length < MIN_LENGTH_FOR_PREFIX) return `${MASK}${tail}`;
2585
- return `${value.slice(0, PREFIX)}\u2026${tail}`;
2586
- }
2587
- var STORE_VERSION = 2;
2588
- var DEFAULT_ACCOUNT_ID = "default";
2589
- var KNOWN_PROVIDER_IDS = new Set(PROVIDERS.map((provider) => provider.id));
2590
- function emptyStore() {
2591
- return { version: STORE_VERSION, providers: {} };
2592
- }
2593
- function isPlainObject(value) {
2594
- return typeof value === "object" && value !== null && !Array.isArray(value);
2595
- }
2596
- function isSafeKey(key) {
2597
- return key.length > 0 && key !== "__proto__" && key !== "constructor" && key !== "prototype";
2598
- }
2599
- function trimmedString(value) {
2600
- if (typeof value !== "string") return void 0;
2601
- const trimmed = value.trim();
2602
- return trimmed.length > 0 ? trimmed : void 0;
2603
- }
2604
- function normalizeTokens(raw) {
2605
- if (!isPlainObject(raw)) return void 0;
2606
- const tokens = {};
2607
- const accessToken = trimmedString(raw.accessToken);
2608
- const refreshToken = trimmedString(raw.refreshToken);
2609
- if (accessToken !== void 0) tokens.accessToken = accessToken;
2610
- if (refreshToken !== void 0) tokens.refreshToken = refreshToken;
2611
- if (tokens.accessToken === void 0 && tokens.refreshToken === void 0) return void 0;
2612
- if (typeof raw.expiresAt === "number" && Number.isFinite(raw.expiresAt)) tokens.expiresAt = raw.expiresAt;
2613
- const tokenType = trimmedString(raw.tokenType);
2614
- if (tokenType !== void 0) tokens.tokenType = tokenType;
2615
- const scope = trimmedString(raw.scope);
2616
- if (scope !== void 0) tokens.scope = scope;
2617
- return tokens;
2618
- }
2619
- function normalizeIdentity(raw) {
2620
- if (!isPlainObject(raw)) return void 0;
2621
- const identity = {};
2622
- const label = trimmedString(raw.label);
2623
- const accountId = trimmedString(raw.accountId);
2624
- const email = trimmedString(raw.email);
2625
- if (label !== void 0) identity.label = label;
2626
- if (accountId !== void 0) identity.accountId = accountId;
2627
- if (email !== void 0) identity.email = email;
2628
- return Object.keys(identity).length > 0 ? identity : void 0;
2629
- }
2630
- function normalizeRecord(providerId, raw) {
2631
- if (!isPlainObject(raw)) return void 0;
2632
- if (raw.kind === "api_key") {
2633
- if (!providerSupportsMethod(providerId, "api-key")) return void 0;
2634
- const secret = trimmedString(raw.secret);
2635
- return secret === void 0 ? void 0 : { kind: "api_key", secret };
2636
- }
2637
- if (raw.kind === "oauth") {
2638
- if (!providerSupportsMethod(providerId, "oauth")) return void 0;
2639
- const tokens = normalizeTokens(raw.tokens);
2640
- if (tokens === void 0) return void 0;
2641
- const identity = normalizeIdentity(raw.identity);
2642
- return identity === void 0 ? { kind: "oauth", tokens } : { kind: "oauth", tokens, identity };
2643
- }
2644
- return void 0;
2645
- }
2646
- function normalizeProviderAccounts(providerId, raw) {
2647
- if (!isPlainObject(raw)) return void 0;
2648
- const accountsRaw = isPlainObject(raw.accounts) ? raw.accounts : {};
2649
- const accounts = {};
2650
- for (const [accountId, recordRaw] of Object.entries(accountsRaw)) {
2651
- if (!isSafeKey(accountId)) continue;
2652
- const record = normalizeRecord(providerId, recordRaw);
2653
- if (record !== void 0) accounts[accountId] = record;
2654
- }
2655
- const ids = Object.keys(accounts);
2656
- if (ids.length === 0) return void 0;
2657
- const active = typeof raw.activeAccountId === "string" && accounts[raw.activeAccountId] !== void 0 ? raw.activeAccountId : ids[0];
2658
- return { activeAccountId: active, accounts };
2659
- }
2660
- function normalizeAccountStore(raw) {
2661
- if (!isPlainObject(raw)) return emptyStore();
2662
- const looksV2 = isPlainObject(raw.providers) || raw.version !== void 0;
2663
- const store = emptyStore();
2664
- if (looksV2) {
2665
- const providersRaw = isPlainObject(raw.providers) ? raw.providers : {};
2666
- for (const [providerId, entry] of Object.entries(providersRaw)) {
2667
- if (!isSafeKey(providerId) || !KNOWN_PROVIDER_IDS.has(providerId)) continue;
2668
- const accounts = normalizeProviderAccounts(providerId, entry);
2669
- if (accounts !== void 0) store.providers[providerId] = accounts;
2655
+ const globalValue = strictValueAt(globalRaw, key);
2656
+ if (globalValue !== void 0) {
2657
+ settings[key] = globalValue;
2658
+ sources[key] = "global";
2659
+ continue;
2670
2660
  }
2671
- return store;
2661
+ settings[key] = DEFAULT_SETTINGS[key];
2662
+ sources[key] = "default";
2672
2663
  }
2673
- for (const [providerId, secret] of Object.entries(normalizeCredentials(raw))) {
2674
- store.providers[providerId] = {
2675
- activeAccountId: DEFAULT_ACCOUNT_ID,
2676
- accounts: { [DEFAULT_ACCOUNT_ID]: { kind: "api_key", secret } }
2677
- };
2664
+ return { settings, sources };
2665
+ }
2666
+ function loadLayeredSettings(opts = {}) {
2667
+ syncThemeChoices(opts.homeDir);
2668
+ const globalRaw = readRawSettingsFile(settingsFilePath(opts.homeDir));
2669
+ const projectRaw = readRawSettingsFile(projectSettingsFilePath(opts.projectDir));
2670
+ return resolveLayeredSettings(globalRaw, projectRaw);
2671
+ }
2672
+ function rawValue(raw, key) {
2673
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return void 0;
2674
+ const value = raw[key];
2675
+ if (key === "analyticsLevel" && value !== void 0) {
2676
+ return value === "usage" || value === "commands" || value === "full" ? "usage" : "off";
2678
2677
  }
2679
- return store;
2678
+ return key === "transcriptStyle" && value === "messenger" ? "bubble" : value;
2680
2679
  }
2681
- function loadAccountStore(homeDir2) {
2682
- try {
2683
- const text = readFileSync4(credentialsFilePath(homeDir2), "utf8");
2684
- return normalizeAccountStore(JSON.parse(text));
2685
- } catch {
2686
- return emptyStore();
2680
+ function booleanAt(raw, key) {
2681
+ const value = rawValue(raw, key);
2682
+ return typeof value === "boolean" ? value : DEFAULT_SETTINGS[key];
2683
+ }
2684
+ function enumAt(raw, key) {
2685
+ const def = DEF_BY_KEY.get(key);
2686
+ const value = rawValue(raw, key);
2687
+ const choices = def?.choices ?? [];
2688
+ return typeof value === "string" && choices.includes(value) ? value : DEFAULT_SETTINGS[key];
2689
+ }
2690
+ function themeAt(raw) {
2691
+ const value = rawValue(raw, "theme");
2692
+ return typeof value === "string" && isKnownTheme(value) ? value : DEFAULT_SETTINGS.theme;
2693
+ }
2694
+ function keybindingsAt(raw) {
2695
+ return sanitizeKeybindingOverrides(rawValue(raw, "keybindings"));
2696
+ }
2697
+ function normalizeModelPreference(raw) {
2698
+ if (raw === null) return null;
2699
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return void 0;
2700
+ const value = raw;
2701
+ if (typeof value.providerId !== "string" || !value.providerId.trim() || typeof value.model !== "string" || !value.model.trim() || typeof value.connectionIdentity !== "string" || !/^[a-f0-9]{64}$/.test(value.connectionIdentity)) return void 0;
2702
+ return { providerId: value.providerId, model: value.model, connectionIdentity: value.connectionIdentity };
2703
+ }
2704
+ function normalizeSettings(raw) {
2705
+ return {
2706
+ showStatusBar: booleanAt(raw, "showStatusBar"),
2707
+ showComposerHints: booleanAt(raw, "showComposerHints"),
2708
+ composerSuggestions: booleanAt(raw, "composerSuggestions"),
2709
+ mouseSupport: booleanAt(raw, "mouseSupport"),
2710
+ showLogo: booleanAt(raw, "showLogo"),
2711
+ showRuntimeNotices: booleanAt(raw, "showRuntimeNotices"),
2712
+ showTurnSummary: booleanAt(raw, "showTurnSummary"),
2713
+ showSubagents: booleanAt(raw, "showSubagents"),
2714
+ showTimestamps: booleanAt(raw, "showTimestamps"),
2715
+ showObjective: booleanAt(raw, "showObjective"),
2716
+ showScope: booleanAt(raw, "showScope"),
2717
+ density: enumAt(raw, "density"),
2718
+ composerStyle: enumAt(raw, "composerStyle"),
2719
+ busyInputMode: enumAt(raw, "busyInputMode"),
2720
+ onboardingCompleted: booleanAt(raw, "onboardingCompleted"),
2721
+ executionProfile: enumAt(raw, "executionProfile"),
2722
+ allowSubagentPeerMessaging: booleanAt(raw, "allowSubagentPeerMessaging"),
2723
+ allowSubagentOperatorMessaging: booleanAt(raw, "allowSubagentOperatorMessaging"),
2724
+ transcriptStyle: enumAt(raw, "transcriptStyle"),
2725
+ roleLabelStyle: enumAt(raw, "roleLabelStyle"),
2726
+ toolCardStyle: enumAt(raw, "toolCardStyle"),
2727
+ richToolCards: booleanAt(raw, "richToolCards"),
2728
+ transcriptDetail: enumAt(raw, "transcriptDetail"),
2729
+ autoCompaction: booleanAt(raw, "autoCompaction"),
2730
+ compactionThreshold: enumAt(raw, "compactionThreshold"),
2731
+ theme: themeAt(raw),
2732
+ allowModelSelfExtension: booleanAt(raw, "allowModelSelfExtension"),
2733
+ allowDevSourceUpdates: booleanAt(raw, "allowDevSourceUpdates"),
2734
+ autoEvolveFinderLenses: booleanAt(raw, "autoEvolveFinderLenses"),
2735
+ autoPromoteFinderLenses: booleanAt(raw, "autoPromoteFinderLenses"),
2736
+ showTokenUsage: booleanAt(raw, "showTokenUsage"),
2737
+ showCost: booleanAt(raw, "showCost"),
2738
+ showContextMeter: booleanAt(raw, "showContextMeter"),
2739
+ modelDisplay: enumAt(raw, "modelDisplay"),
2740
+ elapsedTimer: enumAt(raw, "elapsedTimer"),
2741
+ logoAnimation: enumAt(raw, "logoAnimation"),
2742
+ reduceMotion: booleanAt(raw, "reduceMotion"),
2743
+ analyticsLevel: enumAt(raw, "analyticsLevel"),
2744
+ diagnosticReporting: strictValueAt(raw, "diagnosticReporting") ?? DEFAULT_SETTINGS.diagnosticReporting,
2745
+ diagnosticReportingPrompted: booleanAt(raw, "diagnosticReportingPrompted"),
2746
+ updatePolicy: strictValueAt(raw, "updatePolicy") ?? DEFAULT_SETTINGS.updatePolicy,
2747
+ symbolPreset: enumAt(raw, "symbolPreset"),
2748
+ rosterSort: enumAt(raw, "rosterSort"),
2749
+ leaderKey: enumAt(raw, "leaderKey"),
2750
+ keybindings: keybindingsAt(raw),
2751
+ modelPreference: normalizeModelPreference(rawValue(raw, "modelPreference")) ?? null
2752
+ };
2753
+ }
2754
+ function loadSettings(homeDir, projectDir) {
2755
+ return loadLayeredSettings({ homeDir, projectDir }).settings;
2756
+ }
2757
+ function loadGlobalSettings(homeDir, options = {}) {
2758
+ syncThemeChoices(homeDir);
2759
+ const raw = readRawSettingsFile(settingsFilePath(homeDir));
2760
+ if (options.requireExecutionProfile && strictValueAt(raw, "executionProfile") === void 0) {
2761
+ throw new Error("Configured workbench execution profile is missing or invalid. Run 0 workbench setup or 0 workbench disable to make an explicit operator choice; host fallback is refused.");
2687
2762
  }
2763
+ return normalizeSettings(raw);
2688
2764
  }
2689
- function saveAccountStore(store, homeDir2) {
2765
+ function saveSettings(settings, homeDir) {
2766
+ const path = settingsFilePath(homeDir);
2767
+ const temporary = `${path}.${randomUUID2()}.tmp`;
2690
2768
  try {
2691
- const path = credentialsFilePath(homeDir2);
2692
- const dir = dirname3(path);
2693
- mkdirSync3(dir, { recursive: true, mode: DIR_MODE });
2694
- chmodSync(dir, DIR_MODE);
2695
- writeFileSync3(path, `${JSON.stringify(normalizeAccountStore(store), null, 2)}
2696
- `, {
2697
- encoding: "utf8",
2698
- mode: FILE_MODE
2699
- });
2700
- chmodSync(path, FILE_MODE);
2769
+ mkdirSync2(dirname3(path), { recursive: true });
2770
+ writeFileSync2(temporary, `${JSON.stringify(normalizeSettings(settings), null, 2)}
2771
+ `, { encoding: "utf8", flag: "wx", mode: 384 });
2772
+ renameSync(temporary, path);
2701
2773
  return true;
2702
2774
  } catch {
2703
2775
  return false;
2776
+ } finally {
2777
+ try {
2778
+ rmSync2(temporary, { force: true });
2779
+ } catch {
2780
+ }
2704
2781
  }
2705
2782
  }
2706
- function getActiveAccount(store, providerId) {
2707
- const entry = store.providers[providerId];
2708
- if (entry === void 0) return void 0;
2709
- const id = entry.activeAccountId;
2710
- if (id !== void 0 && entry.accounts[id] !== void 0) return entry.accounts[id];
2711
- const first = Object.keys(entry.accounts)[0];
2712
- return first === void 0 ? void 0 : entry.accounts[first];
2713
- }
2714
- function chooseAccountId(record, requested) {
2715
- if (requested !== void 0 && isSafeKey(requested)) return requested;
2716
- if (record.kind === "oauth" && record.identity?.accountId !== void 0 && isSafeKey(record.identity.accountId)) {
2717
- return record.identity.accountId;
2718
- }
2719
- if (record.kind === "api_key") return DEFAULT_ACCOUNT_ID;
2720
- return `account-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
2721
- }
2722
- function addAccount(store, providerId, record, options) {
2723
- if (!KNOWN_PROVIDER_IDS.has(providerId)) {
2724
- throw new Error(`unknown provider "${providerId}"`);
2725
- }
2726
- const method = record.kind === "api_key" ? "api-key" : "oauth";
2727
- if (!providerSupportsMethod(providerId, method)) {
2728
- throw new Error(`provider "${providerId}" does not support ${record.kind} accounts`);
2729
- }
2730
- const accountId = chooseAccountId(record, options?.accountId);
2731
- const existing = store.providers[providerId];
2732
- const accounts = { ...existing?.accounts ?? {} };
2733
- accounts[accountId] = record;
2734
- const makeActive = options?.makeActive ?? true;
2735
- const activeAccountId = makeActive ? accountId : existing?.activeAccountId !== void 0 && accounts[existing.activeAccountId] !== void 0 ? existing.activeAccountId : accountId;
2736
- return {
2737
- store: { version: STORE_VERSION, providers: { ...store.providers, [providerId]: { activeAccountId, accounts } } },
2738
- accountId
2739
- };
2783
+ function sanitizeOverrides(raw) {
2784
+ const out = {};
2785
+ for (const key of Object.keys(DEFAULT_SETTINGS)) {
2786
+ if (isOperatorSetting(key)) continue;
2787
+ const value = strictValueAt(raw, key);
2788
+ if (value !== void 0) out[key] = value;
2789
+ }
2790
+ return out;
2740
2791
  }
2741
- function accountEnvPatch(store, env) {
2742
- const normalized = normalizeAccountStore(store);
2743
- const patch = {};
2744
- for (const info of PROVIDERS) {
2745
- const record = getActiveAccount(normalized, info.id);
2746
- if (record === void 0) continue;
2747
- if (info.envVars.some((name) => hasCredential2(env[name]))) continue;
2748
- if (record.kind === "api_key") {
2749
- const target = info.envVars[0];
2750
- if (target !== void 0) patch[target] = record.secret;
2751
- continue;
2752
- }
2753
- const accessVar = info.envVars[0];
2754
- if (accessVar !== void 0 && record.tokens.accessToken !== void 0) {
2755
- patch[accessVar] = record.tokens.accessToken;
2756
- }
2757
- if (record.tokens.refreshToken !== void 0) {
2758
- const refreshVar = info.envVars.find((name) => /REFRESH/i.test(name));
2759
- if (refreshVar !== void 0) patch[refreshVar] = record.tokens.refreshToken;
2760
- }
2792
+ function readProjectOverrides(projectDir) {
2793
+ return sanitizeOverrides(readRawSettingsFile(projectSettingsFilePath(projectDir)));
2794
+ }
2795
+ function saveProjectOverrides(patch, projectDir) {
2796
+ try {
2797
+ const path = projectSettingsFilePath(projectDir);
2798
+ mkdirSync2(dirname3(path), { recursive: true });
2799
+ writeFileSync2(path, `${JSON.stringify(sanitizeOverrides(patch), null, 2)}
2800
+ `, "utf8");
2801
+ return true;
2802
+ } catch {
2803
+ return false;
2761
2804
  }
2762
- return patch;
2763
2805
  }
2764
2806
 
2765
2807
  // packages/cli/src/tui/feedback.ts
2766
- import { appendFileSync, mkdirSync as mkdirSync4 } from "node:fs";
2767
- import { dirname as dirname4, join as join6 } from "node:path";
2768
- import { homedir as homedir2 } from "node:os";
2769
- function feedbackFilePath(homeDir2) {
2770
- return join6(homeDir2 ?? homedir2(), ".0", "feedback.md");
2808
+ import { appendFileSync, mkdirSync as mkdirSync3 } from "node:fs";
2809
+ import { randomUUID as randomUUID3 } from "node:crypto";
2810
+ import { dirname as dirname4, join as join7 } from "node:path";
2811
+ import { homedir } from "node:os";
2812
+ function feedbackFilePath(homeDir) {
2813
+ return join7(homeDir ?? homedir(), ".0", "feedback.md");
2771
2814
  }
2772
2815
  function formatFeedbackEntry(entry) {
2773
2816
  const context = [
@@ -2781,10 +2824,10 @@ function formatFeedbackEntry(entry) {
2781
2824
  return `${lines.join("\n")}
2782
2825
  `;
2783
2826
  }
2784
- function appendFeedback(entry, homeDir2) {
2785
- const path = feedbackFilePath(homeDir2);
2827
+ function appendFeedback(entry, homeDir) {
2828
+ const path = feedbackFilePath(homeDir);
2786
2829
  try {
2787
- mkdirSync4(dirname4(path), { recursive: true });
2830
+ mkdirSync3(dirname4(path), { recursive: true });
2788
2831
  appendFileSync(path, formatFeedbackEntry(entry), "utf8");
2789
2832
  return { ok: true, path };
2790
2833
  } catch (error) {
@@ -2808,6 +2851,7 @@ function parseFeedbackCommand(raw) {
2808
2851
  }
2809
2852
  var DEFAULT_FEEDBACK_URL = "";
2810
2853
  var FEEDBACK_URL_ENV = "ZERO_FEEDBACK_URL";
2854
+ var SENTRY_DSN_ENV = "ZERO_SENTRY_DSN";
2811
2855
  var CLOUD_FEEDBACK_PATH = "/api/cli-feedback";
2812
2856
  function defaultCloudCredentials(env) {
2813
2857
  try {
@@ -2833,6 +2877,24 @@ function cloudFeedbackUrl(host) {
2833
2877
  return null;
2834
2878
  }
2835
2879
  }
2880
+ function resolveSentryTarget(dsn) {
2881
+ try {
2882
+ const parsed = new URL(dsn);
2883
+ const key = parsed.username;
2884
+ const segments = parsed.pathname.split("/").filter(Boolean);
2885
+ const project = segments.pop();
2886
+ if (parsed.protocol !== "https:" || parsed.password || parsed.search || parsed.hash || !/^[A-Za-z0-9_-]+$/.test(key) || !project || !/^\d+$/.test(project) || segments.some((segment) => !/^[A-Za-z0-9_-]+$/.test(segment))) return { url: "" };
2887
+ parsed.username = "";
2888
+ parsed.pathname = `/${[...segments, "api", project, "envelope"].join("/")}/`;
2889
+ return { url: parsed.toString(), sentryKey: key };
2890
+ } catch {
2891
+ return { url: "" };
2892
+ }
2893
+ }
2894
+ function resolveSubmissionTarget(payload, env, options) {
2895
+ const dsn = env[SENTRY_DSN_ENV]?.trim();
2896
+ return payload.diagnostic && dsn ? resolveSentryTarget(dsn) : resolveFeedbackTarget(env, options);
2897
+ }
2836
2898
  function resolveFeedbackTarget(env = process.env, options = {}) {
2837
2899
  const configured = env[FEEDBACK_URL_ENV]?.trim();
2838
2900
  if (configured) return { url: configured };
@@ -2852,11 +2914,11 @@ function isOptOutSet(value) {
2852
2914
  if (normalized === "") return false;
2853
2915
  return normalized !== "0" && normalized !== "false" && normalized !== "no";
2854
2916
  }
2855
- function submissionBlockedReason(env = process.env, options = {}) {
2917
+ function submissionBlockedReason(env = process.env, options = {}, payload) {
2856
2918
  for (const name of FEEDBACK_OPT_OUT_ENV) {
2857
2919
  if (isOptOutSet(env[name])) return "opt-out";
2858
2920
  }
2859
- return targetBlockedReason(resolveFeedbackTarget(env, options));
2921
+ return targetBlockedReason(payload ? resolveSubmissionTarget(payload, env, options) : resolveFeedbackTarget(env, options));
2860
2922
  }
2861
2923
  function targetBlockedReason(target) {
2862
2924
  if (target === null) return "no-endpoint";
@@ -2866,7 +2928,7 @@ function targetBlockedReason(target) {
2866
2928
  } catch {
2867
2929
  return "insecure-endpoint";
2868
2930
  }
2869
- if (parsed.protocol !== "https:") return "insecure-endpoint";
2931
+ if (parsed.protocol !== "https:" || parsed.username || parsed.password || parsed.hash) return "insecure-endpoint";
2870
2932
  return null;
2871
2933
  }
2872
2934
  function describeSkip(reason) {
@@ -2876,7 +2938,9 @@ function describeSkip(reason) {
2876
2938
  case "no-endpoint":
2877
2939
  return `No feedback endpoint configured (set ${FEEDBACK_URL_ENV}). Saved locally only.`;
2878
2940
  case "insecure-endpoint":
2879
- return `Refusing a non-HTTPS ${FEEDBACK_URL_ENV}. Saved locally only.`;
2941
+ return "Refusing an invalid or non-HTTPS telemetry endpoint. Saved locally only.";
2942
+ case "no-consent":
2943
+ return "Problem report requires confirmation or an automatic-reporting preference. Saved locally only.";
2880
2944
  }
2881
2945
  }
2882
2946
  var SECRET_RULES = [
@@ -2924,30 +2988,38 @@ function serializePayload(payload) {
2924
2988
  }
2925
2989
  function requestHeaders(target, redactAuthorization = false) {
2926
2990
  const headers = { "content-type": "application/json" };
2991
+ if (target.sentryKey) {
2992
+ headers["content-type"] = "application/x-sentry-envelope";
2993
+ headers["x-sentry-auth"] = `Sentry sentry_version=7,sentry_key=${redactAuthorization ? "<redacted>" : target.sentryKey},sentry_client=0-cli/${VERSION}`;
2994
+ }
2927
2995
  if (target.authorization) {
2928
2996
  headers.authorization = redactAuthorization ? "Bearer <redacted>" : target.authorization;
2929
2997
  }
2930
2998
  return headers;
2931
2999
  }
2932
3000
  function buildSubmitPreview(payload, env = process.env, options = {}) {
2933
- if (submissionBlockedReason(env, options) !== null) return null;
2934
- const target = resolveFeedbackTarget(env, options);
2935
- if (target === null) return null;
3001
+ if (FEEDBACK_OPT_OUT_ENV.some((name) => isOptOutSet(env[name]))) return null;
3002
+ const target = resolveSubmissionTarget(payload, env, options);
3003
+ if (target === null || targetBlockedReason(target) !== null) return null;
2936
3004
  return {
2937
3005
  url: target.url,
2938
- body: serializePayload(payload),
3006
+ body: serializeSubmission(payload, target, env),
2939
3007
  headers: requestHeaders(target, true),
2940
- warnings: scanForSecrets(payload.message)
3008
+ warnings: target.sentryKey ? [] : scanForSecrets(payload.message)
2941
3009
  };
2942
3010
  }
2943
3011
  async function submitFeedback(payload, env = process.env, opts = {}) {
2944
- const blocked = submissionBlockedReason(env, opts);
2945
- if (blocked !== null) return { ok: false, skipped: blocked, error: describeSkip(blocked) };
2946
- const target = resolveFeedbackTarget(env, opts);
3012
+ if (FEEDBACK_OPT_OUT_ENV.some((name) => isOptOutSet(env[name]))) {
3013
+ return { ok: false, skipped: "opt-out", error: describeSkip("opt-out") };
3014
+ }
3015
+ if (payload.diagnostic && opts.diagnosticConsent?.confirmed !== true && opts.diagnosticConsent?.policy !== "automatic") {
3016
+ return { ok: false, skipped: "no-consent", error: describeSkip("no-consent") };
3017
+ }
3018
+ const target = resolveSubmissionTarget(payload, env, opts);
2947
3019
  const targetBlocked = targetBlockedReason(target);
2948
3020
  if (targetBlocked !== null) return { ok: false, skipped: targetBlocked, error: describeSkip(targetBlocked) };
2949
3021
  if (target === null) return { ok: false, skipped: "no-endpoint", error: describeSkip("no-endpoint") };
2950
- const body = serializePayload(payload);
3022
+ const body = serializeSubmission(payload, target, env);
2951
3023
  if (Buffer.byteLength(body, "utf8") > MAX_BODY_BYTES) {
2952
3024
  return { ok: false, error: `Message too large to submit (limit ${MAX_BODY_BYTES} bytes).` };
2953
3025
  }
@@ -2957,7 +3029,7 @@ async function submitFeedback(payload, env = process.env, opts = {}) {
2957
3029
  if (target.url !== reviewed.url || body !== reviewed.body || Object.keys(currentHeaders).length !== Object.keys(reviewed.headers).length || !Object.keys(currentHeaders).every((k) => currentHeaders[k] === reviewed.headers[k])) {
2958
3030
  return {
2959
3031
  ok: false,
2960
- error: "Reviewed feedback destination has changed. Preview the updated submission before sending again."
3032
+ error: "Reviewed telemetry destination has changed. Preview the updated submission before sending again."
2961
3033
  };
2962
3034
  }
2963
3035
  }
@@ -2965,10 +3037,10 @@ async function submitFeedback(payload, env = process.env, opts = {}) {
2965
3037
  const timeoutMs = opts.timeoutMs ?? FEEDBACK_TIMEOUT_MS;
2966
3038
  const controller = new AbortController();
2967
3039
  let timer;
2968
- const timeout = new Promise((resolve) => {
3040
+ const timeout = new Promise((resolve2) => {
2969
3041
  timer = setTimeout(() => {
2970
3042
  controller.abort();
2971
- resolve({ ok: false, error: `Feedback submission timed out after ${timeoutMs}ms.` });
3043
+ resolve2({ ok: false, error: `Feedback submission timed out after ${timeoutMs}ms.` });
2972
3044
  }, timeoutMs);
2973
3045
  timer.unref?.();
2974
3046
  });
@@ -2982,8 +3054,8 @@ async function submitFeedback(payload, env = process.env, opts = {}) {
2982
3054
  redirect: "error"
2983
3055
  });
2984
3056
  return response.ok ? { ok: true, status: response.status } : { ok: false, status: response.status, error: `Endpoint returned ${response.status}.` };
2985
- } catch (error) {
2986
- return { ok: false, error: error instanceof Error ? error.message : String(error) };
3057
+ } catch {
3058
+ return { ok: false, error: "Feedback submission failed." };
2987
3059
  }
2988
3060
  })();
2989
3061
  try {
@@ -3031,7 +3103,7 @@ function diagnosticVersion(raw) {
3031
3103
  if (typeof raw !== "string" || raw.length > 128) return "unknown";
3032
3104
  return /^v?(\d{1,6}\.\d{1,6}\.\d{1,6})(?:[-+][a-zA-Z0-9.+-]+)?$/.exec(raw)?.[1] ?? "unknown";
3033
3105
  }
3034
- function buildDiagnosticFeedback(info, env = process.env) {
3106
+ function buildDiagnosticFeedback(info) {
3035
3107
  const kind = info.kind === "tool" ? "tool" : info.kind === "runtime" ? "runtime" : "unknown";
3036
3108
  const platform = DIAGNOSTIC_PLATFORMS.has(info.platform) ? info.platform : "unknown";
3037
3109
  const arch = DIAGNOSTIC_ARCHS.has(info.arch) ? info.arch : "unknown";
@@ -3039,90 +3111,251 @@ function buildDiagnosticFeedback(info, env = process.env) {
3039
3111
  const runtimeVersion = diagnosticVersion(info.runtimeVersion);
3040
3112
  const version = diagnosticVersion(info.version);
3041
3113
  const timestamp = typeof info.timestamp === "string" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(info.timestamp) && Number.isFinite(Date.parse(info.timestamp)) ? info.timestamp : (/* @__PURE__ */ new Date()).toISOString();
3042
- let message = `Diagnostic: ${kind} error \u2014 ${platform}/${arch} on ${runtime} ${runtimeVersion}
3114
+ const category = diagnosticError(info.error);
3115
+ const message = `Diagnostic: ${kind} error \u2014 ${platform}/${arch} on ${runtime} ${runtimeVersion}
3043
3116
  Version: ${version}
3044
- Error: ${diagnosticError(info.error)}`;
3045
- if (diagnosticDetailAllowed(env)) {
3046
- const detail = redactDiagnosticDetail(buildDiagnosticDetailText(info));
3047
- if (detail) message = capUtf8Bytes(`${message}
3048
-
3049
- ${detail}`, MAX_DIAGNOSTIC_DETAIL_MESSAGE_BYTES);
3050
- }
3051
- return { message, timestamp, version };
3052
- }
3053
- var MAX_DIAGNOSTIC_DETAIL_MESSAGE_BYTES = 8192;
3054
- var MAX_DIAGNOSTIC_DETAIL_CHARS = 6e3;
3055
- function diagnosticDetailAllowed(env) {
3056
- const level = (env["ZERO_ANALYTICS_LEVEL"] ?? "").trim().toLowerCase();
3057
- return level === "commands" || level === "full";
3117
+ Error: ${category}`;
3118
+ return {
3119
+ message,
3120
+ timestamp,
3121
+ version,
3122
+ diagnostic: {
3123
+ eventId: randomUUID3().replaceAll("-", ""),
3124
+ kind,
3125
+ category,
3126
+ platform,
3127
+ arch,
3128
+ runtime,
3129
+ runtimeVersion,
3130
+ frames: diagnosticFrames(info.error)
3131
+ }
3132
+ };
3058
3133
  }
3059
- function buildDiagnosticDetailText(info) {
3060
- const parts = [];
3061
- if (info.toolName) parts.push(`Tool: ${info.toolName}`);
3062
- const err = info.error;
3063
- if (err instanceof Error) {
3064
- parts.push(`${err.name}: ${err.message || "(no message)"}`);
3065
- if (typeof err.stack === "string" && err.stack.trim()) parts.push(err.stack.trim());
3066
- } else {
3067
- const text = diagnosticErrorText(err);
3068
- if (text) parts.push(text);
3069
- }
3070
- const exitOutput = typeof info.exitOutput === "string" ? info.exitOutput.trim() : "";
3071
- if (exitOutput && !parts.some((p) => p.includes(exitOutput))) parts.push(exitOutput);
3072
- let detail = parts.join("\n").trim();
3073
- if (detail.length > MAX_DIAGNOSTIC_DETAIL_CHARS) detail = detail.slice(0, MAX_DIAGNOSTIC_DETAIL_CHARS - 1) + "\u2026";
3074
- return detail;
3075
- }
3076
- var DETAIL_REDACTIONS = [
3077
- { re: /-----BEGIN (?:[A-Z0-9 ]+ )?PRIVATE KEY-----[\s\S]*?-----END (?:[A-Z0-9 ]+ )?PRIVATE KEY-----/g, to: "\u2039redacted:private-key\u203A" },
3078
- { re: /\bsk-[A-Za-z0-9_-]{16,}/g, to: "\u2039redacted:key\u203A" },
3079
- { re: /\bgh[pousr]_[A-Za-z0-9]{20,}/g, to: "\u2039redacted:gh-token\u203A" },
3080
- { re: /\b(?:AKIA|ASIA|ABIA|ACCA)[0-9A-Z]{16}\b/g, to: "\u2039redacted:aws-key\u203A" },
3081
- { re: /\bAIza[0-9A-Za-z_-]{35}\b/g, to: "\u2039redacted:gcp-key\u203A" },
3082
- { re: /\bxox[abprs]-[A-Za-z0-9-]{10,}/g, to: "\u2039redacted:slack-token\u203A" },
3083
- { re: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]+/g, to: "\u2039redacted:jwt\u203A" },
3084
- { re: /authorization\s*[:=]\s*(?:bearer|basic|token|digest)\s+\S+/gi, to: "authorization: \u2039redacted\u203A" },
3085
- { re: /\b((?:pass(?:word|wd)?|api[_-]?key|secret|token|credentials?)\s*[:=]\s*)\S{6,}/gi, to: "$1\u2039redacted\u203A" },
3086
- // Connection strings: keep the scheme/host shape, drop the inline credentials.
3087
- // MUST run before the email rule so `user:pass@host` is not mis-read as an
3088
- // email (which would swallow the host too).
3089
- { re: /([a-z][a-z0-9+.-]*:\/\/)[^\s/@:]+:[^\s/@]+@/gi, to: "$1\u2039redacted\u203A@" },
3090
- { re: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g, to: "\u2039redacted:email\u203A" },
3091
- // Home-dir usernames in file paths — keep the path shape, anonymise the user.
3092
- { re: /(\/home\/)[^/\s]+/g, to: "$1\u2039user\u203A" },
3093
- { re: /(\/Users\/)[^/\s]+/g, to: "$1\u2039user\u203A" },
3094
- { re: /([A-Za-z]:\\Users\\)[^\\\s]+/g, to: "$1\u2039user\u203A" }
3095
- ];
3096
- function redactDiagnosticDetail(text) {
3097
- if (typeof text !== "string" || text.length === 0) return "";
3134
+ function diagnosticFrames(error) {
3098
3135
  try {
3099
- let out = text;
3100
- for (const { re, to } of DETAIL_REDACTIONS) out = out.replace(re, to);
3101
- return out;
3136
+ if (!(error instanceof Error) || typeof error.stack !== "string") return [];
3137
+ const frames = [];
3138
+ for (const line of error.stack.slice(0, 16384).split("\n").slice(1, 33)) {
3139
+ const match = /(?:^|[/\\])(packages[/\\](?:cli|core|shared)[/\\](?:src|dist)[/\\][A-Za-z0-9_./\\-]+\.[cm]?[jt]sx?):(\d{1,7}):(\d{1,7})\)?$/.exec(line.trim());
3140
+ if (!match || match[1].split(/[/\\]/).some((part) => part === "..")) continue;
3141
+ const lineno = Number(match[2]);
3142
+ const colno = Number(match[3]);
3143
+ if (lineno > 0 && colno > 0) {
3144
+ frames.push({ filename: `app:///${match[1].replaceAll("\\", "/")}`, lineno, colno });
3145
+ }
3146
+ }
3147
+ return frames.reverse();
3102
3148
  } catch {
3103
- return "";
3104
- }
3149
+ return [];
3150
+ }
3151
+ }
3152
+ function serializeSubmission(payload, target, env) {
3153
+ if (!target.sentryKey || !payload.diagnostic) return serializePayload(payload);
3154
+ const diagnostic = payload.diagnostic;
3155
+ const cliVersion = VERSION;
3156
+ const commit = /^[0-9a-f]{40}$/i.test("531d93617a529a487a9d94e6c20886903ffc40ec") ? "531d93617a529a487a9d94e6c20886903ffc40ec".toLowerCase() : void 0;
3157
+ const environment = env["NODE_ENV"] === "development" ? "development" : env["NODE_ENV"] === "production" || true ? "production" : "development";
3158
+ const event = JSON.stringify({
3159
+ event_id: diagnostic.eventId,
3160
+ timestamp: Date.parse(payload.timestamp) / 1e3,
3161
+ platform: "node",
3162
+ level: "error",
3163
+ logger: "0.cli",
3164
+ release: `0-cli@${cliVersion}${commit ? `+${commit}` : ""}`,
3165
+ environment,
3166
+ tags: {
3167
+ cli_version: cliVersion,
3168
+ ...commit ? { build_commit: commit } : {},
3169
+ kind: diagnostic.kind,
3170
+ platform: diagnostic.platform,
3171
+ arch: diagnostic.arch,
3172
+ runtime: diagnostic.runtime
3173
+ },
3174
+ contexts: { runtime: { name: diagnostic.runtime, version: diagnostic.runtimeVersion } },
3175
+ exception: {
3176
+ values: [{
3177
+ type: "CLIError",
3178
+ value: diagnostic.category,
3179
+ ...diagnostic.frames.length ? { stacktrace: { frames: diagnostic.frames } } : {}
3180
+ }]
3181
+ }
3182
+ });
3183
+ return `${JSON.stringify({ event_id: diagnostic.eventId })}
3184
+ ${JSON.stringify({ type: "event", length: Buffer.byteLength(event, "utf8") })}
3185
+ ${event}
3186
+ `;
3105
3187
  }
3106
- function capUtf8Bytes(text, maxBytes) {
3107
- if (Buffer.byteLength(text, "utf8") <= maxBytes) return text;
3108
- let lo = 0, hi = text.length;
3109
- while (lo < hi) {
3110
- const mid = Math.ceil((lo + hi) / 2);
3111
- if (Buffer.byteLength(text.slice(0, mid), "utf8") <= maxBytes - 1) lo = mid;
3112
- else hi = mid - 1;
3188
+
3189
+ // packages/cli/src/workbench.ts
3190
+ var WORKBENCH_INNER_ARGUMENT = "--workbench-inner";
3191
+ function workbenchConfigPath(home = process.env.HOME || homedir2()) {
3192
+ return join8(homeStateDir(home), "workbench.json");
3193
+ }
3194
+ function defaultWorkbenchStateRoot(home = process.env.HOME || homedir2()) {
3195
+ return join8(homeStateDir(home), "workbench");
3196
+ }
3197
+ function positiveInteger(value, maximum) {
3198
+ return typeof value === "number" && Number.isInteger(value) && value > 0 && value <= maximum;
3199
+ }
3200
+ function isPinnedWorkbenchImageReference(value) {
3201
+ return typeof value === "string" && /^[a-z0-9][a-z0-9._:/-]*@sha256:[a-f0-9]{64}$/.test(value) && !value.slice(0, value.indexOf("@")).slice(value.lastIndexOf("/") + 1).includes(":");
3202
+ }
3203
+ function normalizeWorkbenchConfig(raw) {
3204
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) throw new Error("Invalid workbench operator choices.");
3205
+ const config = raw;
3206
+ if (config.github === true) throw new Error("GitHub credential forwarding is not supported by the isolated workbench. Revoke the GitHub grant before launch.");
3207
+ if (Array.isArray(config.providers) && config.providers.some((provider) => provider !== "chatgpt-codex")) throw new Error("The isolated workbench currently supports only host-brokered chatgpt-codex grants.");
3208
+ const approvedImages = [];
3209
+ if (config.approvedImages !== void 0) {
3210
+ if (!Array.isArray(config.approvedImages)) throw new Error("Invalid workbench sandbox image catalog.");
3211
+ const references = /* @__PURE__ */ new Set();
3212
+ for (const rawImage of config.approvedImages) {
3213
+ if (typeof rawImage !== "object" || rawImage === null || Array.isArray(rawImage)) throw new Error("Invalid workbench sandbox image catalog.");
3214
+ const image = rawImage;
3215
+ if (!isPinnedWorkbenchImageReference(image.reference) || typeof image.archive !== "string" || !isAbsolute2(image.archive) || typeof image.digest !== "string" || !/^sha256:[a-f0-9]{64}$/.test(image.digest) || references.has(image.reference)) throw new Error("Sandbox image grants require unique immutable repository@sha256 references and approved local archives.");
3216
+ references.add(image.reference);
3217
+ approvedImages.push({ reference: image.reference, archive: image.archive, digest: image.digest });
3218
+ }
3113
3219
  }
3114
- return text.slice(0, lo) + "\u2026";
3220
+ if (config.schemaVersion !== 1 || typeof config.image !== "string" || !isAbsolute2(config.image) || typeof config.imageDigest !== "string" || !/^sha256:[a-f0-9]{64}$/.test(config.imageDigest) || typeof config.stateRoot !== "string" || !isAbsolute2(config.stateRoot) || config.workspaceRoot !== void 0 && (typeof config.workspaceRoot !== "string" || !isAbsolute2(config.workspaceRoot)) || !Array.isArray(config.providers) || config.providers.some((id) => typeof id !== "string" || !PROVIDERS.some((provider) => provider.id === id)) || typeof config.github !== "boolean" || !positiveInteger(config.cpus, 64) || !positiveInteger(config.memoryMb, 262144) || !positiveInteger(config.storageGb, 1024)) {
3221
+ throw new Error("Invalid workbench operator choices.");
3222
+ }
3223
+ return {
3224
+ schemaVersion: 1,
3225
+ image: config.image,
3226
+ imageDigest: config.imageDigest,
3227
+ stateRoot: config.stateRoot,
3228
+ ...config.workspaceRoot === void 0 ? {} : { workspaceRoot: config.workspaceRoot },
3229
+ providers: [...new Set(config.providers)],
3230
+ github: config.github,
3231
+ cpus: config.cpus,
3232
+ memoryMb: config.memoryMb,
3233
+ storageGb: config.storageGb,
3234
+ ...approvedImages.length ? { approvedImages } : {}
3235
+ };
3115
3236
  }
3116
- function diagnosticErrorText(error) {
3117
- if (typeof error === "string") return error.trim();
3118
- if (error instanceof Error) return (error.message || error.name || "Error").trim();
3119
- if (error === null || error === void 0) return "";
3237
+ function loadWorkbenchConfig(home) {
3238
+ const file = workbenchConfigPath(home);
3239
+ if (!existsSync2(file)) return void 0;
3120
3240
  try {
3121
- const text = String(error).trim();
3122
- return text === "[object Object]" ? "" : text;
3123
- } catch {
3124
- return "";
3241
+ const stat = lstatSync2(file);
3242
+ if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 63) !== 0 || stat.uid !== process.getuid?.()) {
3243
+ throw new Error("Workbench configuration must be an operator-owned, private regular file (mode 0600).");
3244
+ }
3245
+ return normalizeWorkbenchConfig(JSON.parse(readFileSync3(file, "utf8")));
3246
+ } catch (error) {
3247
+ if (error instanceof Error && error.message.startsWith("Workbench configuration must")) throw error;
3248
+ throw new Error(`Unreadable or invalid workbench configuration at ${file}. Repair the operator choices before setup or launch; no profile fallback is allowed.`);
3249
+ }
3250
+ }
3251
+ function saveWorkbenchConfig(config, home) {
3252
+ const path = workbenchConfigPath(home);
3253
+ mkdirSync4(dirname5(path), { recursive: true, mode: 448 });
3254
+ const temporary = `${path}.${process.pid}.tmp`;
3255
+ writeFileSync3(temporary, `${JSON.stringify(normalizeWorkbenchConfig(config), null, 2)}
3256
+ `, { encoding: "utf8", mode: 384, flag: "wx" });
3257
+ renameSync2(temporary, path);
3258
+ chmodSync(path, 384);
3259
+ }
3260
+ function resolveWorkbenchGuestSettings(settings, env = process.env) {
3261
+ const privacy = { [ANALYTICS_LEVEL_ENV]: env[ANALYTICS_LEVEL_ENV] ?? settings.analyticsLevel };
3262
+ for (const name of FEEDBACK_OPT_OUT_ENV) privacy[name] = env[name];
3263
+ const environmentLevel = resolveAnalyticsLevel(privacy);
3264
+ return normalizeSettings({
3265
+ ...settings,
3266
+ executionProfile: "local",
3267
+ updatePolicy: "off",
3268
+ analyticsLevel: levelAtLeast(environmentLevel, settings.analyticsLevel) ? settings.analyticsLevel : environmentLevel,
3269
+ diagnosticReporting: submissionBlockedReason(privacy, { allowCloud: false }) === "opt-out" ? "off" : settings.diagnosticReporting
3270
+ });
3271
+ }
3272
+ function workbenchNetworkEnabled(env = process.env) {
3273
+ return submissionBlockedReason({ ZERO_OFFLINE: env.ZERO_OFFLINE }, { allowCloud: false }) !== "opt-out";
3274
+ }
3275
+ async function launchConfiguredWorkbench(args) {
3276
+ const home = process.env.HOME || homedir2();
3277
+ if (args[0] === WORKBENCH_INNER_ARGUMENT) {
3278
+ if (process.platform !== "linux" || !isAdmittedSmolvmWorkbench()) {
3279
+ throw new Error("Missing authenticated SmolVM broker admission; refusing internal invocation.");
3280
+ }
3281
+ process.argv = [process.argv[0], process.argv[1], ...args.slice(1)];
3282
+ return void 0;
3283
+ }
3284
+ if (process.platform === "linux" && isAdmittedSmolvmWorkbench()) return void 0;
3285
+ if (["workbench", "config", "web", "dashboard", "console", "tui", "doctor", "help", "--help", "-h", "--version", "-v"].includes(args[0] ?? "") || args.length === 0) return void 0;
3286
+ const hostSettings = loadGlobalSettings(home, { requireExecutionProfile: existsSync2(workbenchConfigPath(home)) });
3287
+ if (hostSettings.executionProfile !== "smolvm") return void 0;
3288
+ const config = loadWorkbenchConfig(home);
3289
+ if (!config) throw new Error("SmolVM profile is selected but no workbench is configured. Run 0 workbench setup --image <approved-archive>.");
3290
+ const workspaceRoot = realpathSync3(config.workspaceRoot ?? process.cwd());
3291
+ if (!lstatSync2(workspaceRoot).isDirectory()) throw new Error("The configured workbench workspace is not a directory.");
3292
+ const status = await getSmolvmWorkbenchStatus({ stateRoot: config.stateRoot, image: config.image });
3293
+ if (!status.platformSupported || !status.runtimeReady || !status.imageApproved) {
3294
+ throw new Error(status.error ?? "SmolVM runtime or approved image is unavailable. Run 0 workbench setup; host fallback is refused.");
3295
+ }
3296
+ const modelIndex = args.findIndex((argument) => argument === "--model" || argument === "-m");
3297
+ const modelArgument = modelIndex >= 0 ? args[modelIndex + 1] : args.find((argument) => argument.startsWith("--model="))?.slice("--model=".length);
3298
+ const runtime = createConsoleRuntime({ model: modelArgument });
3299
+ const providerId = runtime.resolvedProvider();
3300
+ if (providerId !== "chatgpt-codex" || !config.providers.includes(providerId)) throw new Error("Isolated CLI execution requires an explicitly granted brokered chatgpt-codex provider. Host fallback is refused.");
3301
+ const model = runtime.resolvedModel();
3302
+ const routing = runtime.modelSelection();
3303
+ const broker = createWorkbenchProviderBroker({
3304
+ provider: providerId,
3305
+ models: [.../* @__PURE__ */ new Set([model, ...Object.values(routing.agentModels).filter((value) => value !== "auto")])],
3306
+ resolveCredentials: runtime.workbenchCredentialResolver()
3307
+ });
3308
+ const network = workbenchNetworkEnabled();
3309
+ const controller = new AbortController();
3310
+ let signalCode;
3311
+ const interrupt = () => {
3312
+ signalCode = 130;
3313
+ controller.abort();
3314
+ };
3315
+ const terminate = () => {
3316
+ signalCode = 143;
3317
+ controller.abort();
3318
+ };
3319
+ const hangup = () => {
3320
+ signalCode = 129;
3321
+ controller.abort();
3322
+ };
3323
+ process.once("SIGINT", interrupt);
3324
+ process.once("SIGTERM", terminate);
3325
+ process.once("SIGHUP", hangup);
3326
+ let result;
3327
+ try {
3328
+ result = await runWorkbenchCli({
3329
+ workbench: config,
3330
+ selection: { provider: providerId, model, ...routing },
3331
+ provider: { ...broker.grant, request: broker.request, close: broker.close },
3332
+ assets: currentWorkbenchAssets(),
3333
+ guestSettings: resolveWorkbenchGuestSettings(hostSettings),
3334
+ network,
3335
+ args: mapWorkbenchCliArguments(args, workspaceRoot),
3336
+ signal: controller.signal,
3337
+ onStdout: (data) => process.stdout.write(data),
3338
+ onStderr: (data) => process.stderr.write(data)
3339
+ });
3340
+ } finally {
3341
+ await broker.close();
3342
+ process.removeListener("SIGINT", interrupt);
3343
+ process.removeListener("SIGTERM", terminate);
3344
+ process.removeListener("SIGHUP", hangup);
3125
3345
  }
3346
+ if (result.cleanupFailed) {
3347
+ process.stderr.write("[0] SmolVM cleanup could not be proven. Workbench admission/state is retained; host execution is refused.\n");
3348
+ return 125;
3349
+ }
3350
+ if (result.artifacts) process.stderr.write(`[0] Guest workspace and results saved at ${result.artifacts.directory}
3351
+ `);
3352
+ if (result.error) process.stderr.write(`[0] ${result.error}
3353
+ `);
3354
+ if (result.timedOut) return 124;
3355
+ return signalCode ?? result.exitCode ?? 125;
3356
+ }
3357
+ function selectWorkbenchProfile(profile, home) {
3358
+ if (!saveSettings({ ...loadGlobalSettings(home), executionProfile: profile }, home)) throw new Error("Could not persist the operator execution profile.");
3126
3359
  }
3127
3360
 
3128
3361
  export {
@@ -3150,13 +3383,18 @@ export {
3150
3383
  isSafeThemeId,
3151
3384
  reloadInstalledThemes,
3152
3385
  installedThemeEntries,
3386
+ allThemeNames,
3153
3387
  isKnownTheme,
3154
3388
  writeInstalledTheme,
3155
3389
  removeInstalledTheme,
3390
+ syncThemeChoices,
3156
3391
  SETTING_DEFS,
3157
3392
  DEFAULT_SETTINGS,
3158
3393
  settingsFilePath,
3159
3394
  projectSettingsFilePath,
3395
+ projectSettingsExist,
3396
+ isOperatorSetting,
3397
+ resolveLayeredSettings,
3160
3398
  loadLayeredSettings,
3161
3399
  normalizeSettings,
3162
3400
  loadSettings,
@@ -3165,42 +3403,33 @@ export {
3165
3403
  sanitizeOverrides,
3166
3404
  readProjectOverrides,
3167
3405
  saveProjectOverrides,
3168
- getSettings,
3169
- getSettingSources,
3170
- updateSetting,
3171
- resetSettings,
3172
- previewSetting,
3173
- subscribeSettings,
3174
- reloadSettings,
3175
- configureSettingsStore,
3176
- useSettings,
3177
- maybeLoadCodexAuth,
3178
- presentationEventBus,
3179
- createPresentationEmitter,
3180
- consolePresentationOutput,
3181
- installConsolePresentationBridge,
3182
- installProcessPresentationStreamBridge,
3183
- suspendProcessPresentationStreamBridge,
3184
- resumeProcessPresentationStreamBridge,
3185
- processPresentationOutput,
3186
- writePresentationLine,
3406
+ currentWorkbenchAssets,
3407
+ WORKBENCH_FRAME_BYTES,
3408
+ WORKBENCH_MAX_PENDING,
3409
+ encodeWorkbenchFrame,
3410
+ WorkbenchFrameReader,
3411
+ installWorkbenchPlugins,
3412
+ validateGuestServicePluginConnections,
3413
+ servicePluginSecretRedactor,
3414
+ createWorkbenchConsoleSession,
3187
3415
  PROVIDERS,
3416
+ providerSupportsMethod,
3188
3417
  providerStates,
3189
3418
  isProviderConfigured,
3190
- loadCredentials,
3191
- saveCredentials,
3192
- credentialEnvPatch,
3193
- redactSecret,
3194
- loadAccountStore,
3195
- saveAccountStore,
3196
- addAccount,
3197
- accountEnvPatch,
3198
3419
  appendFeedback,
3199
3420
  parseFeedbackCommand,
3200
- FEEDBACK_OPT_OUT_ENV,
3201
3421
  submissionBlockedReason,
3202
3422
  describeSkip,
3203
3423
  buildSubmitPreview,
3204
3424
  submitFeedback,
3205
- buildDiagnosticFeedback
3425
+ buildDiagnosticFeedback,
3426
+ workbenchConfigPath,
3427
+ defaultWorkbenchStateRoot,
3428
+ isPinnedWorkbenchImageReference,
3429
+ loadWorkbenchConfig,
3430
+ saveWorkbenchConfig,
3431
+ resolveWorkbenchGuestSettings,
3432
+ workbenchNetworkEnabled,
3433
+ launchConfiguredWorkbench,
3434
+ selectWorkbenchProfile
3206
3435
  };