@0dai-dev/cli 4.1.0 → 4.3.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 (53) hide show
  1. package/README.md +30 -5
  2. package/bin/0dai.js +308 -60
  3. package/lib/commands/audit.js +13 -0
  4. package/lib/commands/auth.js +404 -122
  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 +79 -14
  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 +553 -53
  16. package/lib/commands/loop.js +108 -0
  17. package/lib/commands/mcp.js +410 -0
  18. package/lib/commands/models.js +42 -12
  19. package/lib/commands/paste.js +114 -0
  20. package/lib/commands/persona-simulate.js +19 -0
  21. package/lib/commands/play.js +173 -0
  22. package/lib/commands/provider.js +87 -0
  23. package/lib/commands/quota.js +76 -0
  24. package/lib/commands/receipt.js +53 -0
  25. package/lib/commands/report.js +29 -2
  26. package/lib/commands/run.js +44 -4
  27. package/lib/commands/runner.js +527 -0
  28. package/lib/commands/session.js +1 -7
  29. package/lib/commands/ssh.js +416 -0
  30. package/lib/commands/standup.js +40 -0
  31. package/lib/commands/status.js +131 -36
  32. package/lib/commands/swarm.js +97 -4
  33. package/lib/commands/tui.js +117 -0
  34. package/lib/commands/usage.js +87 -0
  35. package/lib/commands/vault.js +246 -0
  36. package/lib/commands/workspace.js +1 -0
  37. package/lib/onboarding.js +30 -10
  38. package/lib/shared.js +153 -96
  39. package/lib/tui/index.mjs +34994 -0
  40. package/lib/utils/auth.js +1 -0
  41. package/lib/utils/canonical-counts.js +54 -0
  42. package/lib/utils/diff-preview.js +192 -0
  43. package/lib/utils/identity.js +76 -18
  44. package/lib/utils/mcp-auth.js +607 -0
  45. package/lib/utils/model_ratings.js +77 -0
  46. package/lib/utils/plan.js +37 -2
  47. package/lib/vault/cipher.js +125 -0
  48. package/lib/vault/identity.js +122 -0
  49. package/lib/vault/index.js +184 -0
  50. package/lib/vault/storage.js +84 -0
  51. package/lib/wizard.js +19 -12
  52. package/package.json +13 -5
  53. package/scripts/build-tui.js +77 -0
@@ -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 };
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+ const path = require("path");
3
+ const fs = require("fs");
4
+ const shared = require("../shared");
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
+
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
+
49
+ if (!process.stdout.isTTY) {
50
+ console.error("0dai tui requires an interactive TTY. Try: 0dai status");
51
+ process.exit(2);
52
+ }
53
+
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
+
90
+ const bundlePath = path.join(__dirname, "..", "tui", "index.mjs");
91
+ let tui;
92
+ try {
93
+ const url = require("url").pathToFileURL(bundlePath).href;
94
+ tui = await import(url);
95
+ } catch (err) {
96
+ const code = err && err.code;
97
+ const msg = String(err && err.message || "");
98
+ if (code === "ERR_MODULE_NOT_FOUND" || msg.includes("Cannot find module")) {
99
+ log("TUI bundle missing. Rebuild with:");
100
+ console.log(` ${D}cd $(npm root -g)/@0dai-dev/cli && node scripts/build-tui.js${R}`);
101
+ process.exit(1);
102
+ }
103
+ throw err;
104
+ }
105
+
106
+ let plan = "free";
107
+ try {
108
+ const auth = shared.loadAuthState();
109
+ if (auth && auth.plan) plan = auth.plan;
110
+ } catch {
111
+ // keep default plan
112
+ }
113
+
114
+ await tui.run({ target, version: VERSION, plan, writeMode, remoteUrl, projectId });
115
+ }
116
+
117
+ module.exports = { cmdTui };
@@ -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
+ };
@@ -108,6 +108,7 @@ function detectSessions(target) {
108
108
  function cmdWorkspaceInit(target, args) {
109
109
  const globalFlag = args.includes("--global");
110
110
 
111
+ const sessions = detectSessions(target);
111
112
  if (sessions.length === 0) {
112
113
  log("no services detected. Create workspace config manually with: 0dai workspace add");
113
114
  return;
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
  }
@@ -104,20 +110,28 @@ async function cmdQuickstart(target, { cmdDoctor, cmdStatus, cmdInit, log, ensur
104
110
 
105
111
  // Step 1: Auth
106
112
  console.log(`\n [1/${steps}] Checking authentication...`);
113
+ let signedIn = false;
107
114
  let authInfo = "not signed in";
108
115
  try {
109
116
  const auth = JSON.parse(fs.readFileSync(path.join(require("os").homedir(), ".0dai", "auth.json"), "utf8"));
110
117
  if (auth.access_token) {
118
+ signedIn = true;
111
119
  authInfo = `signed in (${auth.plan || "free"} plan)`;
112
120
  }
113
121
  } catch {}
114
- console.log(` \u2713 ${authInfo}`);
122
+ if (signedIn) {
123
+ console.log(` \u2713 ${authInfo}`);
124
+ } else {
125
+ console.log(` \u26a0 ${authInfo} \u2192 run 0dai auth login`);
126
+ }
115
127
 
116
128
  // Step 2: Init
117
129
  console.log(` [2/${steps}] Checking project config...`);
118
130
  const aiExists = fs.existsSync(path.join(target, "ai", "VERSION"));
119
131
  if (aiExists) {
120
132
  console.log(" \u2713 ai/ layer found");
133
+ } else if (!signedIn) {
134
+ console.log(" \u26a0 skipped \u2192 sign in before first init");
121
135
  } else {
122
136
  console.log(" initializing...");
123
137
  try {
@@ -132,9 +146,9 @@ async function cmdQuickstart(target, { cmdDoctor, cmdStatus, cmdInit, log, ensur
132
146
 
133
147
  // Step 3: Doctor
134
148
  console.log(` [3/${steps}] Running health check...`);
135
- if (fs.existsSync(path.join(target, "ai"))) {
149
+ if (fs.existsSync(path.join(target, "ai", "VERSION"))) {
136
150
  try {
137
- cmdDoctor(target);
151
+ cmdDoctor(target, { suppressExitCode: true });
138
152
  } catch {}
139
153
  console.log(" \u2713 health check complete");
140
154
  } else {
@@ -143,7 +157,7 @@ async function cmdQuickstart(target, { cmdDoctor, cmdStatus, cmdInit, log, ensur
143
157
 
144
158
  // Step 4: Status
145
159
  console.log(` [4/${steps}] Project status:`);
146
- if (fs.existsSync(path.join(target, "ai"))) {
160
+ if (fs.existsSync(path.join(target, "ai", "VERSION"))) {
147
161
  try {
148
162
  cmdStatus(target);
149
163
  } catch {}
@@ -155,9 +169,15 @@ async function cmdQuickstart(target, { cmdDoctor, cmdStatus, cmdInit, log, ensur
155
169
  const elapsed = ((Date.now() - start) / 1000).toFixed(1);
156
170
  console.log(` [5/${steps}] Ready! (${elapsed}s)`);
157
171
  console.log("");
158
- console.log(" Your project is set up. Try:");
159
- console.log(" 0dai swarm run --goal \"add auth\" (Pro)");
160
- console.log(" 0dai graph push (Pro)");
172
+ if (!signedIn && !aiExists) {
173
+ console.log(" Next:");
174
+ console.log(" 0dai auth login Sign in to unlock init and sync");
175
+ console.log(" 0dai quickstart Re-run after sign-in");
176
+ } else {
177
+ console.log(" Your project is set up. Try:");
178
+ console.log(" 0dai swarm run --goal \"add auth\" (Pro)");
179
+ console.log(" 0dai graph push (Pro)");
180
+ }
161
181
  console.log("");
162
182
  }
163
183