@0dai-dev/cli 4.2.0 → 4.3.5

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 (52) hide show
  1. package/README.md +98 -10
  2. package/bin/0dai.js +298 -60
  3. package/lib/commands/audit.js +13 -0
  4. package/lib/commands/auth.js +344 -98
  5. package/lib/commands/boneyard.js +44 -0
  6. package/lib/commands/ci.js +329 -0
  7. package/lib/commands/compliance.js +20 -0
  8. package/lib/commands/doctor.js +39 -1
  9. package/lib/commands/experience.js +5 -1
  10. package/lib/commands/feedback.js +92 -5
  11. package/lib/commands/gh.js +506 -0
  12. package/lib/commands/graph.js +78 -10
  13. package/lib/commands/heatmap.js +17 -0
  14. package/lib/commands/import_claude_code_agents.js +367 -0
  15. package/lib/commands/init.js +504 -28
  16. package/lib/commands/loop.js +108 -0
  17. package/lib/commands/mcp.js +410 -0
  18. package/lib/commands/models.js +27 -3
  19. package/lib/commands/paste.js +114 -0
  20. package/lib/commands/play.js +173 -0
  21. package/lib/commands/provider.js +69 -0
  22. package/lib/commands/quota.js +76 -0
  23. package/lib/commands/receipt.js +53 -0
  24. package/lib/commands/report.js +29 -2
  25. package/lib/commands/run.js +104 -7
  26. package/lib/commands/runner.js +527 -0
  27. package/lib/commands/session.js +1 -7
  28. package/lib/commands/standup.js +40 -0
  29. package/lib/commands/status.js +30 -1
  30. package/lib/commands/swarm.js +97 -4
  31. package/lib/commands/tui.js +81 -13
  32. package/lib/commands/upgrade.js +58 -0
  33. package/lib/commands/usage.js +87 -0
  34. package/lib/commands/vault.js +246 -0
  35. package/lib/onboarding.js +9 -3
  36. package/lib/shared.js +29 -14
  37. package/lib/utils/activation_telemetry.js +156 -0
  38. package/lib/utils/auth.js +1 -0
  39. package/lib/utils/canonical-counts.js +54 -0
  40. package/lib/utils/constants.js +7 -0
  41. package/lib/utils/diff-preview.js +192 -0
  42. package/lib/utils/identity.js +76 -18
  43. package/lib/utils/mcp-auth.js +607 -0
  44. package/lib/utils/plan.js +47 -2
  45. package/lib/utils/run_cost.js +91 -0
  46. package/lib/vault/cipher.js +125 -0
  47. package/lib/vault/identity.js +122 -0
  48. package/lib/vault/index.js +184 -0
  49. package/lib/vault/storage.js +84 -0
  50. package/lib/wizard.js +19 -12
  51. package/package.json +8 -4
  52. package/lib/tui/index.mjs +0 -34610
@@ -1,11 +1,15 @@
1
1
  "use strict";
2
2
  const shared = require("../shared");
3
- const { log, T, R, D, fs, path, https, requirePlan } = shared;
3
+ const { log, T, R, D, fs, path, https, requirePlan, spawnSync, findRepoScript } = shared;
4
4
 
5
5
  function cmdSwarm(target, sub, args) {
6
6
  const swarmDir = path.join(target, "ai", "swarm");
7
7
  const queueDir = path.join(swarmDir, "queue");
8
8
 
9
+ if (sub === "swarm-run") {
10
+ cmdSwarmRun(target, args.slice(2));
11
+ return;
12
+ }
9
13
  if (sub === "status") {
10
14
  const count = (d) => { try { return fs.readdirSync(d).filter(f => f.endsWith(".json")).length; } catch { return 0; } };
11
15
  const q = count(path.join(swarmDir, "queue"));
@@ -192,8 +196,97 @@ function cmdSwarm(target, sub, args) {
192
196
  if (typeof result.status === "number" && result.status !== 0) process.exit(result.status);
193
197
  return;
194
198
  }
195
- console.log("Usage: 0dai swarm [status|add|delegate|budget|estimate|quality] [--task '...'] [--to agent]");
196
- console.log(" quality Show quality scores for recent tasks (--last N / --details TASK_ID)");
199
+ if (sub === "sessions") {
200
+ const script = findRepoScript(target, "swarm_session_registry.py");
201
+ if (!script) { log("swarm session registry unavailable"); return; }
202
+ const fwd = [script, "rebuild", "--stale-after", "600"];
203
+ const asJson = args.includes("--json");
204
+ if (!asJson) fwd.push("--write", path.join(swarmDir, "session_registry.json"));
205
+ const r = spawnSync("python3", fwd, { encoding: "utf8", timeout: 15000 });
206
+ if (r.error) { log(`error: ${r.error.message}`); return; }
207
+ if (r.status !== 0 && r.status !== null) { process.exit(r.status); }
208
+ if (asJson) {
209
+ console.log(r.stdout);
210
+ return;
211
+ }
212
+ const snapFile = path.join(swarmDir, "session_registry.json");
213
+ if (!fs.existsSync(snapFile)) {
214
+ log("no session data yet — run a few tasks first");
215
+ return;
216
+ }
217
+ let snap;
218
+ try { snap = JSON.parse(fs.readFileSync(snapFile, "utf8")); } catch { log("could not read registry snapshot"); return; }
219
+ const sessions = snap.sessions || {};
220
+ if (Object.keys(sessions).length === 0) {
221
+ console.log("\n No sessions in registry.\n");
222
+ return;
223
+ }
224
+ const B = process.stdout.isTTY ? "\x1b[1m" : "";
225
+ const R_ = process.stdout.isTTY ? "\x1b[0m" : "";
226
+ const W2 = process.stdout.isTTY ? "\x1b[33m" : "";
227
+ const G2 = process.stdout.isTTY ? "\x1b[32m" : "";
228
+ const M2 = process.stdout.isTTY ? "\x1b[35m" : "";
229
+ const dim = process.stdout.isTTY ? "\x1b[2m" : "";
230
+ const reset = process.stdout.isTTY ? "\x1b[0m" : "";
231
+ console.log(`\n ${B}Swarm Sessions${R_} (updated ${snap.updated_at || "?"})`);
232
+ console.log(` ${dim}${"─".repeat(80)}${reset}`);
233
+ const rows = Object.entries(sessions).sort(([a], [b]) => a.localeCompare(b));
234
+ for (const [name, s] of rows) {
235
+ const status = s.current_status || "???";
236
+ const statusCol = status === "idle" ? G2 : status === "busy" ? W2 : status === "stale" ? M2 : "";
237
+ const task = s.current_task || "-";
238
+ const branch = s.current_branch || "-";
239
+ const claimed = s.claimed_at
240
+ ? new Date(s.claimed_at).toISOString().replace("T", " ").slice(0, 16)
241
+ : "-";
242
+ console.log(
243
+ ` ${(statusCol + status + reset).padEnd(8)} ${(name || "").padEnd(22)} ${(s.agent_family || "").padEnd(8)} ${(task || "").padEnd(20)} ${(branch || "").padEnd(25)} ${claimed}`
244
+ );
245
+ }
246
+ console.log(` ${dim}${"─".repeat(80)}${reset}\n`);
247
+ const idle = rows.filter(([, s]) => s.current_status === "idle").length;
248
+ const busy = rows.filter(([, s]) => s.current_status === "busy").length;
249
+ const stale = rows.filter(([, s]) => s.current_status === "stale").length;
250
+ console.log(` ${G2}idle${reset}: ${idle} ${W2}busy${reset}: ${busy} ${M2}stale${reset}: ${stale} total: ${rows.length}\n`);
251
+ return;
252
+ }
253
+ console.log("Usage: 0dai swarm [status|add|delegate|budget|estimate|quality|sessions|swarm-run] [--task '...'] [--to agent]");
254
+ console.log(" sessions Show live session registry table (idle/busy/stale per session) [--json]");
255
+ console.log(" swarm-run Add, dispatch, and wait for one swarm task as JSON");
256
+ }
257
+
258
+ function cmdSwarmRun(target, args) {
259
+ // #2143 hardening:
260
+ // 1. `ODAI_SWARM_RUN_SCRIPT` env override lets tests inject a deterministic
261
+ // script path, bypassing the `findRepoScript` heuristic that walks up
262
+ // multiple parent dirs and can drift on CI runners with stale layouts.
263
+ // 2. `ODAI_SWARM_RUN_PYTHON` env override picks the interpreter (defaults to
264
+ // `python3`). Lets a runner with `python3.12` aliased differently still
265
+ // invoke the helper without modifying the test.
266
+ // 3. When the helper cannot be located, log every candidate that was probed
267
+ // so CI failures point at the right runner-layout issue instead of a
268
+ // generic "unavailable" line.
269
+ let script = process.env.ODAI_SWARM_RUN_SCRIPT || "";
270
+ if (script && !fs.existsSync(script)) {
271
+ log(`swarm-run helper unavailable: ODAI_SWARM_RUN_SCRIPT=${script} does not exist`);
272
+ process.exit(1);
273
+ }
274
+ if (!script) script = findRepoScript(target, "swarm_run.py");
275
+ if (!script) {
276
+ const cwd = process.cwd();
277
+ log("swarm-run helper unavailable");
278
+ log(` probed: ${path.join(target, "scripts", "swarm_run.py")}`);
279
+ log(` probed: ${path.join(cwd, "scripts", "swarm_run.py")}`);
280
+ log(` hint: set ODAI_SWARM_RUN_SCRIPT to override the lookup`);
281
+ process.exit(1);
282
+ }
283
+ const python = process.env.ODAI_SWARM_RUN_PYTHON || "python3";
284
+ const result = spawnSync(python, [script, "--target", target, ...args], { stdio: "inherit" });
285
+ if (result.error) {
286
+ log(`swarm-run failed: ${result.error.message}`);
287
+ process.exit(1);
288
+ }
289
+ if (typeof result.status === "number" && result.status !== 0) process.exit(result.status);
197
290
  }
198
291
 
199
- module.exports = { cmdSwarm };
292
+ module.exports = { cmdSwarm, cmdSwarmRun };
@@ -1,27 +1,95 @@
1
1
  "use strict";
2
2
  const path = require("path");
3
+ const fs = require("fs");
3
4
  const shared = require("../shared");
4
- const { log, D, R, VERSION } = shared;
5
-
6
- /**
7
- * `0dai tui` — read-only dashboard (issue #373).
8
- *
9
- * Lazy-loads lib/tui/index.js (pre-built by scripts/build-tui.js at prepack
10
- * time). Degrades gracefully if the bundle / peer deps aren't available so
11
- * non-TUI users on minimal installs still get a useful message.
12
- */
5
+ const { log, D, R, VERSION, spawnSync } = shared;
6
+
7
+ function findRepoScript(target, name) {
8
+ const candidates = [
9
+ path.join(target, "scripts", name),
10
+ path.join(__dirname, "../../../../scripts", name),
11
+ path.join("/root/0dai/scripts", name),
12
+ ];
13
+ for (const c of candidates) {
14
+ try { if (fs.existsSync(c)) return c; } catch {}
15
+ }
16
+ return null;
17
+ }
18
+
19
+ function argAfter(args, flag) {
20
+ const i = args.indexOf(flag);
21
+ return i >= 0 && i + 1 < args.length ? args[i + 1] : null;
22
+ }
23
+
24
+ function cmdTuiLayout(target, args = []) {
25
+ const layout = argAfter(args, "--layout");
26
+ if (layout !== "launchpad") {
27
+ console.error(`Unknown --layout: ${layout || "(missing)"}. Available: launchpad`);
28
+ process.exit(2);
29
+ }
30
+ const script = findRepoScript(target, "tui.py");
31
+ if (!script) {
32
+ console.error("tui.py not found locally. Is the 0dai project set up?");
33
+ process.exit(1);
34
+ }
35
+ const fwd = [script, "--target", target, "--layout", "launchpad"];
36
+ const exportPng = argAfter(args, "--export-png");
37
+ if (exportPng) fwd.push("--export-png", exportPng);
38
+ if (args.includes("--no-color") || !process.stdout.isTTY) fwd.push("--no-color");
39
+ const res = spawnSync("python3", fwd, { stdio: "inherit" });
40
+ process.exit(typeof res.status === "number" ? res.status : 1);
41
+ }
42
+
13
43
  async function cmdTui(target, args = []) {
44
+ // Short-circuit for `--layout launchpad` — one-shot render, no TTY needed.
45
+ if (args.includes("--layout")) {
46
+ return cmdTuiLayout(target, args);
47
+ }
48
+
14
49
  if (!process.stdout.isTTY) {
15
50
  console.error("0dai tui requires an interactive TTY. Try: 0dai status");
16
51
  process.exit(2);
17
52
  }
18
53
 
19
- // Ink 5.x is ESM; bundle is .mjs so we load it via dynamic import
20
- // from this CJS entry.
54
+ // SPEC-026 Phase 3: --remote <url> + --project <id> flip hooks from
55
+ // local fs to HTTP. Env vars ODAI_TUI_REMOTE_URL + ODAI_TUI_PROJECT
56
+ // act as fallbacks so PC / mobile launchers can set them once.
57
+ const remoteUrl =
58
+ argAfter(args, "--remote") || process.env.ODAI_TUI_REMOTE_URL || undefined;
59
+ const projectId =
60
+ argAfter(args, "--project") || process.env.ODAI_TUI_PROJECT || undefined;
61
+ if (remoteUrl && !projectId) {
62
+ console.error(
63
+ "0dai tui --remote requires --project <id> (or ODAI_TUI_PROJECT env)",
64
+ );
65
+ process.exit(2);
66
+ }
67
+ if (remoteUrl) {
68
+ log(`${D}TUI remote-mode: ${remoteUrl} (project=${projectId})${R}`);
69
+ }
70
+
71
+ const writeFlag = args.includes("--write");
72
+ const writeEnv = process.env.ODAI_TUI_WRITE === "1";
73
+ const writeMode = writeFlag || writeEnv;
74
+ if (writeFlag && !writeEnv) {
75
+ log(`${D}TUI write-mode requested via --write. Set ODAI_TUI_WRITE=1 to persist this preference.${R}`);
76
+ }
77
+ if (writeMode) {
78
+ log(`${D}TUI write-mode ENABLED — destructive actions will require diff preview + confirmation.${R}`);
79
+ }
80
+
81
+ const standupScript = findRepoScript(target, "standup.py");
82
+ if (standupScript) {
83
+ spawnSync(
84
+ "python3",
85
+ [standupScript, "--target", target, "--if-stale", "--launch-hook"],
86
+ { stdio: "inherit" },
87
+ );
88
+ }
89
+
21
90
  const bundlePath = path.join(__dirname, "..", "tui", "index.mjs");
22
91
  let tui;
23
92
  try {
24
- // Use the file:// URL form so Windows paths with drive letters load too.
25
93
  const url = require("url").pathToFileURL(bundlePath).href;
26
94
  tui = await import(url);
27
95
  } catch (err) {
@@ -43,7 +111,7 @@ async function cmdTui(target, args = []) {
43
111
  // keep default plan
44
112
  }
45
113
 
46
- await tui.run({ target, version: VERSION, plan });
114
+ await tui.run({ target, version: VERSION, plan, writeMode, remoteUrl, projectId });
47
115
  }
48
116
 
49
117
  module.exports = { cmdTui };
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+
3
+ const { spawnSync } = require("child_process");
4
+ const { buildUpgradeUrl } = require("../utils/plan");
5
+
6
+ function isInteractive() {
7
+ return !!(process.stdin && process.stdin.isTTY && process.stdout && process.stdout.isTTY);
8
+ }
9
+
10
+ function isBrowserDisabled(env = process.env) {
11
+ const browser = env.BROWSER;
12
+ if (browser == null) return false;
13
+ const normalized = String(browser).trim().toLowerCase();
14
+ return normalized === "" || normalized === "0" || normalized === "false" || normalized === "none";
15
+ }
16
+
17
+ function openBrowser(url, deps = {}) {
18
+ const spawn = deps.spawnSync || spawnSync;
19
+ const platform = deps.platform || process.platform;
20
+ let command;
21
+ let args;
22
+ if (platform === "darwin") {
23
+ command = "open";
24
+ args = [url];
25
+ } else if (platform === "win32") {
26
+ command = "cmd";
27
+ args = ["/c", "start", "", url];
28
+ } else {
29
+ command = "xdg-open";
30
+ args = [url];
31
+ }
32
+ const result = spawn(command, args, { stdio: "ignore", timeout: 5000 });
33
+ return result.status === 0;
34
+ }
35
+
36
+ function cmdUpgrade(options = {}) {
37
+ const writeLine = typeof options.writeLine === "function" ? options.writeLine : (msg) => console.log(msg);
38
+ const url = buildUpgradeUrl(options.params);
39
+ const interactive = typeof options.isInteractive === "function"
40
+ ? options.isInteractive()
41
+ : isInteractive();
42
+ const browserDisabled = isBrowserDisabled(options.env || process.env);
43
+
44
+ if (interactive && !browserDisabled) {
45
+ const opened = openBrowser(url, options);
46
+ if (!opened) writeLine(url);
47
+ return;
48
+ }
49
+
50
+ writeLine(url);
51
+ }
52
+
53
+ module.exports = {
54
+ cmdUpgrade,
55
+ openBrowser,
56
+ isInteractive,
57
+ isBrowserDisabled,
58
+ };
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+
3
+ const shared = require("../shared");
4
+ const { log, D, R, spawnSync, findRepoScript } = shared;
5
+
6
+ const SUBCOMMANDS = new Set(["status", "daily", "monthly"]);
7
+
8
+ function printUsageHelp() {
9
+ console.log("Usage:");
10
+ console.log(" 0dai usage [status] [--plan free|pro|team|enterprise] [--json] [--target PATH]");
11
+ console.log(" 0dai usage daily [--date YYYY-MM-DD] [--json] [--target PATH]");
12
+ console.log(" 0dai usage monthly [--month YYYY-MM] [--json] [--target PATH]");
13
+ console.log("");
14
+ console.log("Shows token, task, and USD usage from the local usage ledger.");
15
+ }
16
+
17
+ function _forwardArgs(subcommand, args) {
18
+ const forwarded = [subcommand];
19
+ const allowedFlags = new Set(["--json"]);
20
+ const valueFlags = new Set(
21
+ subcommand === "daily" ? ["--date"] :
22
+ subcommand === "monthly" ? ["--month"] :
23
+ ["--plan"],
24
+ );
25
+
26
+ for (let i = 0; i < args.length; i += 1) {
27
+ const arg = args[i];
28
+ if (allowedFlags.has(arg)) {
29
+ forwarded.push(arg);
30
+ continue;
31
+ }
32
+ if (valueFlags.has(arg)) {
33
+ if (!args[i + 1] || args[i + 1].startsWith("-")) {
34
+ throw new Error(`${arg} requires a value`);
35
+ }
36
+ forwarded.push(arg, args[i + 1]);
37
+ i += 1;
38
+ continue;
39
+ }
40
+ throw new Error(`unknown usage option: ${arg}`);
41
+ }
42
+
43
+ return forwarded;
44
+ }
45
+
46
+ function cmdUsage(target, rawArgs = []) {
47
+ const args = rawArgs.slice();
48
+ if (args.includes("--help") || args.includes("-h")) {
49
+ printUsageHelp();
50
+ return;
51
+ }
52
+
53
+ const first = args[0] || "";
54
+ const subcommand = first && !first.startsWith("-") ? args.shift() : "status";
55
+ if (!SUBCOMMANDS.has(subcommand)) {
56
+ log(`unknown usage subcommand: ${subcommand}`);
57
+ printUsageHelp();
58
+ process.exit(1);
59
+ }
60
+
61
+ let forwarded;
62
+ try {
63
+ forwarded = _forwardArgs(subcommand, args);
64
+ } catch (err) {
65
+ log(err.message);
66
+ printUsageHelp();
67
+ process.exit(1);
68
+ }
69
+
70
+ const script = findRepoScript(target, "usage_ledger.py");
71
+ if (!script) {
72
+ log("usage ledger unavailable");
73
+ console.log(` ${D}Expected scripts/usage_ledger.py in this project${R}`);
74
+ process.exit(1);
75
+ }
76
+
77
+ const result = spawnSync("python3", [script, ...forwarded], {
78
+ cwd: target,
79
+ stdio: "inherit",
80
+ timeout: 15000,
81
+ });
82
+ if (typeof result.status === "number" && result.status !== 0) {
83
+ process.exit(result.status);
84
+ }
85
+ }
86
+
87
+ module.exports = { cmdUsage, _forwardArgs };
@@ -0,0 +1,246 @@
1
+ /**
2
+ * 0dai vault CLI — Phase 1 SCAFFOLD (Task #104, F6 G2).
3
+ *
4
+ * Mirrors the shape of ci.js (#2696): tiny dispatcher, JSON or human
5
+ * output, sub-commands forwarded to ../vault/index.js. Phase 1 implements
6
+ * only `init`; all other sub-commands print a "P2 deferred" message and
7
+ * exit with status 2 so scripts can branch on it.
8
+ *
9
+ * See docs/runbooks/0dai-vault.md for the phase plan and SPEC pointer.
10
+ */
11
+
12
+ "use strict";
13
+
14
+ const shared = require("../shared");
15
+ const { T, R, D, log } = shared;
16
+
17
+ const vault = require("../vault");
18
+
19
+ // Phase 2a ships `add` + `get`. The rest stay P2-gated until operator
20
+ // approves Phase 2b (list/inject/rotate scope).
21
+ const P2_GATED = new Set(["list", "inject", "rotate"]);
22
+
23
+ function hasFlag(args, name) {
24
+ return args.includes(name);
25
+ }
26
+
27
+ function printUsage() {
28
+ console.log("Usage: 0dai vault <init|add|get|list|inject|rotate> [--json]");
29
+ console.log(" Phase 1+2a ships `init`, `add`, `get`. `list`/`inject`/`rotate` gated on operator P2b approval.");
30
+ console.log(" add: 0dai vault add <scope> <name> <value> (or value via --stdin)");
31
+ console.log(" get: 0dai vault get <scope> <name>");
32
+ console.log(" Runbook: docs/runbooks/0dai-vault.md");
33
+ }
34
+
35
+ function printInitHuman(result) {
36
+ console.log(`\n ${T}0dai vault${R} — Phase 1 init`);
37
+ console.log(` vault dir : ${result.vaultDir}`);
38
+ console.log(` identity : ${result.identityPath}`);
39
+ console.log(` public key : ${result.publicKey || "(missing — re-run init)"}`);
40
+ if (result.created) {
41
+ console.log(` ${T}new keypair generated${R} — back up ${result.identityPath} offline`);
42
+ } else {
43
+ console.log(` ${D}existing keypair detected — no changes${R}`);
44
+ }
45
+ console.log(` ${D}Phase 2 (get/add/list/inject/rotate) gated on operator P2 approval${R}`);
46
+ console.log(` ${D}runbook: docs/runbooks/0dai-vault.md${R}\n`);
47
+ }
48
+
49
+ function cmdVaultInit(args) {
50
+ const asJson = hasFlag(args, "--json");
51
+ try {
52
+ const result = vault.init();
53
+ if (asJson) {
54
+ console.log(JSON.stringify({
55
+ schema_version: 1,
56
+ command: "vault init",
57
+ phase: 1,
58
+ ...result,
59
+ }, null, 2));
60
+ return;
61
+ }
62
+ printInitHuman(result);
63
+ } catch (err) {
64
+ if (asJson) {
65
+ console.log(JSON.stringify({
66
+ schema_version: 1,
67
+ command: "vault init",
68
+ error: err.message,
69
+ code: err.code || "VAULT_INIT_FAILED",
70
+ }, null, 2));
71
+ } else {
72
+ log(`vault init failed: ${err.message}`);
73
+ }
74
+ process.exitCode = 1;
75
+ }
76
+ }
77
+
78
+ function _readStdinSync() {
79
+ const fs = require("node:fs");
80
+ try {
81
+ return fs.readFileSync(0, "utf8");
82
+ } catch (e) {
83
+ return "";
84
+ }
85
+ }
86
+
87
+ function cmdVaultAdd(args) {
88
+ const asJson = hasFlag(args, "--json");
89
+ const useStdin = hasFlag(args, "--stdin");
90
+ // positional args after stripping flags
91
+ const positional = args.filter((a) => !a.startsWith("--"));
92
+ const [scope, name, value] = positional;
93
+ if (!scope || !name) {
94
+ log("usage: 0dai vault add <scope> <name> <value>|--stdin");
95
+ process.exitCode = 1;
96
+ return;
97
+ }
98
+ let payload = value;
99
+ if (useStdin) {
100
+ payload = _readStdinSync();
101
+ if (!payload) {
102
+ log("vault add --stdin: no input on stdin");
103
+ process.exitCode = 1;
104
+ return;
105
+ }
106
+ }
107
+ if (payload == null || payload === "") {
108
+ log("vault add: <value> required (or --stdin)");
109
+ process.exitCode = 1;
110
+ return;
111
+ }
112
+ try {
113
+ const result = vault.add(scope, name, payload);
114
+ if (asJson) {
115
+ console.log(JSON.stringify({
116
+ schema_version: 1,
117
+ command: "vault add",
118
+ phase: "2a",
119
+ ...result,
120
+ }, null, 2));
121
+ } else {
122
+ console.log(`\n ${T}0dai vault add${R}`);
123
+ console.log(` scope/name : ${scope}/${name}`);
124
+ console.log(` path : ${result.path}`);
125
+ console.log(` bytes : ${result.bytes}`);
126
+ console.log(` ${result.overwritten ? T + "overwritten" + R : D + "new" + R}\n`);
127
+ }
128
+ } catch (err) {
129
+ if (asJson) {
130
+ console.log(JSON.stringify({
131
+ schema_version: 1,
132
+ command: "vault add",
133
+ error: err.message,
134
+ code: err.code || "VAULT_ADD_FAILED",
135
+ }, null, 2));
136
+ } else {
137
+ log(`vault add failed: ${err.message}`);
138
+ }
139
+ process.exitCode = err.code === "VAULT_INVALID_NAME" ? 2 : 1;
140
+ }
141
+ }
142
+
143
+ function cmdVaultGet(args) {
144
+ const asJson = hasFlag(args, "--json");
145
+ const positional = args.filter((a) => !a.startsWith("--"));
146
+ const [scope, name] = positional;
147
+ if (!scope || !name) {
148
+ log("usage: 0dai vault get <scope> <name>");
149
+ process.exitCode = 1;
150
+ return;
151
+ }
152
+ try {
153
+ const result = vault.get(scope, name);
154
+ if (asJson) {
155
+ console.log(JSON.stringify({
156
+ schema_version: 1,
157
+ command: "vault get",
158
+ phase: "2a",
159
+ scope: result.scope,
160
+ name: result.name,
161
+ value: result.value,
162
+ path: result.path,
163
+ }, null, 2));
164
+ } else {
165
+ // Human mode: print value to stdout unwrapped so shell pipelines work.
166
+ process.stdout.write(result.value);
167
+ if (!result.value.endsWith("\n")) process.stdout.write("\n");
168
+ }
169
+ } catch (err) {
170
+ if (asJson) {
171
+ console.log(JSON.stringify({
172
+ schema_version: 1,
173
+ command: "vault get",
174
+ error: err.message,
175
+ code: err.code || "VAULT_GET_FAILED",
176
+ }, null, 2));
177
+ } else {
178
+ log(`vault get failed: ${err.message}`);
179
+ }
180
+ if (err.code === "VAULT_SECRET_NOT_FOUND") {
181
+ process.exitCode = 4; // distinct: "no such secret"
182
+ } else if (err.code === "VAULT_INVALID_NAME") {
183
+ process.exitCode = 2;
184
+ } else {
185
+ process.exitCode = 1;
186
+ }
187
+ }
188
+ }
189
+
190
+ function cmdVaultDeferred(sub, args) {
191
+ const asJson = hasFlag(args, "--json");
192
+ const message = `vault ${sub} is gated on operator P2 approval — see docs/runbooks/0dai-vault.md`;
193
+ if (asJson) {
194
+ console.log(JSON.stringify({
195
+ schema_version: 1,
196
+ command: `vault ${sub}`,
197
+ phase: 2,
198
+ deferred: true,
199
+ message,
200
+ }, null, 2));
201
+ } else {
202
+ console.log(`\n ${T}0dai vault ${sub}${R}`);
203
+ console.log(` ${D}${message}${R}\n`);
204
+ }
205
+ // Distinct exit code so wrapper scripts can detect "not implemented yet"
206
+ // vs a real failure (1).
207
+ process.exitCode = 2;
208
+ }
209
+
210
+ function cmdVault(_target, sub, args) {
211
+ const command = sub && !sub.startsWith("-") ? sub : "";
212
+ const forwarded = command === sub ? args.slice(2) : args.slice(1);
213
+
214
+ if (!command || command === "help" || command === "-h" || command === "--help") {
215
+ printUsage();
216
+ return;
217
+ }
218
+
219
+ if (command === "init") {
220
+ cmdVaultInit(forwarded);
221
+ return;
222
+ }
223
+
224
+ if (command === "add") {
225
+ cmdVaultAdd(forwarded);
226
+ return;
227
+ }
228
+
229
+ if (command === "get") {
230
+ cmdVaultGet(forwarded);
231
+ return;
232
+ }
233
+
234
+ if (P2_GATED.has(command)) {
235
+ cmdVaultDeferred(command, forwarded);
236
+ return;
237
+ }
238
+
239
+ log(`unknown vault sub-command: ${command}`);
240
+ printUsage();
241
+ process.exitCode = 1;
242
+ }
243
+
244
+ module.exports = {
245
+ cmdVault,
246
+ };
package/lib/onboarding.js CHANGED
@@ -23,12 +23,16 @@ function _loadState(target) {
23
23
  }
24
24
  }
25
25
 
26
- function _saveState(target, state) {
26
+ function _saveState(target, state, options = {}) {
27
+ const createAiDir = options.createAiDir !== false;
27
28
  try {
28
29
  const dir = path.join(target, "ai");
30
+ if (!fs.existsSync(dir) && !createAiDir) return false;
29
31
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
30
32
  fs.writeFileSync(_statePath(target), JSON.stringify(state, null, 2) + "\n");
33
+ return true;
31
34
  } catch {}
35
+ return false;
32
36
  }
33
37
 
34
38
  // ---------------------------------------------------------------------------
@@ -73,14 +77,16 @@ function trackFirstRun(target) {
73
77
  const state = _loadState(target);
74
78
  if (!state.first_run_at) {
75
79
  state.first_run_at = new Date().toISOString();
76
- _saveState(target, state);
80
+ _saveState(target, state, { createAiDir: false });
77
81
  }
78
82
  }
79
83
 
80
84
  function trackFirstInit(target) {
81
85
  const state = _loadState(target);
82
86
  if (!state.first_init_at) {
83
- state.first_init_at = new Date().toISOString();
87
+ const now = new Date().toISOString();
88
+ if (!state.first_run_at) state.first_run_at = now;
89
+ state.first_init_at = now;
84
90
  _saveState(target, state);
85
91
  }
86
92
  }