@0dai-dev/cli 4.0.0 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/0dai.js CHANGED
@@ -32,6 +32,11 @@ const { cmdFeedback, cmdFeedbackPush } = require("../lib/commands/feedback");
32
32
  const { cmdGraph } = require("../lib/commands/graph");
33
33
  const { cmdReport } = require("../lib/commands/report");
34
34
  const { cmdExperience } = require("../lib/commands/experience");
35
+ const { cmdWorkspace } = require("../lib/commands/workspace");
36
+ const { cmdSsh } = require("../lib/commands/ssh");
37
+ const { cmdTui } = require("../lib/commands/tui");
38
+ const { cmdPersonaSimulate } = require("../lib/commands/persona-simulate");
39
+ const { cmdProvider } = require("../lib/commands/provider");
35
40
 
36
41
  async function main() {
37
42
  const args = process.argv.slice(2);
@@ -67,6 +72,29 @@ async function main() {
67
72
  case "watch": cmdWatch(target, args.slice(1)); break;
68
73
  case "audit": cmdAudit(target); break;
69
74
  case "security": {
75
+ const subSec = args[1] || "";
76
+ if (subSec === "install-hook") {
77
+ const hooksDir = path.join(target, ".git", "hooks");
78
+ if (!fs.existsSync(hooksDir)) { log("not a git repo"); break; }
79
+ const hookPath = path.join(hooksDir, "pre-commit");
80
+ const hookSource = path.join(__dirname, "..", "..", "..", "scripts", "hooks", "pre-commit.sh");
81
+ if (fs.existsSync(hookSource)) {
82
+ fs.copyFileSync(hookSource, hookPath);
83
+ fs.chmodSync(hookPath, 0o755);
84
+ log("pre-commit hook installed: " + hookPath);
85
+ } else {
86
+ // Inline install: copy from repo scripts if available
87
+ const repoHook = findRepoScript(target, "hooks/pre-commit.sh");
88
+ if (repoHook) {
89
+ fs.copyFileSync(repoHook, hookPath);
90
+ fs.chmodSync(hookPath, 0o755);
91
+ log("pre-commit hook installed: " + hookPath);
92
+ } else {
93
+ log("hook source not found — run: 0dai security install-hook from repo root");
94
+ }
95
+ }
96
+ break;
97
+ }
70
98
  const secScript = findRepoScript(target, "scan_secrets.py");
71
99
  if (!secScript) { log("secret scanner unavailable"); break; }
72
100
  const fwd = [secScript, "--target", target];
@@ -79,13 +107,21 @@ async function main() {
79
107
  case "init": await cmdInit(target, args); break;
80
108
  case "sync": await cmdSync(target, args); break;
81
109
  case "detect": await cmdDetect(target); break;
82
- case "doctor":
83
- cmdDoctor(target);
110
+ case "doctor": {
111
+ const driftMode = args.includes("--drift");
112
+ cmdDoctor(target, { drift: driftMode });
84
113
  if (args.includes("--drift")) {
85
114
  const ds = findRepoScript(target, "drift_detector.py");
86
- if (ds) spawnSync("python3", [ds, "report", "--target", target], { stdio: "inherit" });
115
+ console.log("\n drift report:");
116
+ if (ds) {
117
+ const result = spawnSync("python3", [ds, "report", "--target", target], { stdio: "inherit" });
118
+ if (typeof result.status === "number" && result.status !== 0) process.exit(result.status);
119
+ } else {
120
+ console.log(` ${D}drift detector unavailable in this environment${R}`);
121
+ }
87
122
  }
88
123
  break;
124
+ }
89
125
  case "drift": {
90
126
  const ds = findRepoScript(target, "drift_detector.py");
91
127
  if (!ds) { log("drift detector unavailable"); break; }
@@ -103,7 +139,7 @@ async function main() {
103
139
  case "update": cmdUpdate(args); break;
104
140
  case "metrics": cmdMetrics(target); break;
105
141
  case "portfolio": cmdPortfolio(); break;
106
- case "status": cmdStatus(target); break;
142
+ case "status": cmdStatus(target, { json: args.includes("--json") }); break;
107
143
  case "auth":
108
144
  if (sub === "login") await cmdAuthLogin();
109
145
  else if (sub === "logout") cmdAuthLogout();
@@ -117,11 +153,42 @@ async function main() {
117
153
  break;
118
154
  case "session": cmdSession(target, sub, args); break;
119
155
  case "swarm": cmdSwarm(target, sub, args); break;
156
+ case "workspace": cmdWorkspace(target, sub, args.slice(2)); break;
157
+ case "ssh": await cmdSsh(target, sub, args); break;
158
+ case "provider": cmdProvider(target, args.slice(1)); break;
159
+ case "tui": case "dashboard": await cmdTui(target, args.slice(1)); break;
120
160
  case "feedback": await cmdFeedback(target, sub, args); break;
121
161
  case "report": cmdReport(target, sub, args); break;
122
162
  case "experience": cmdExperience(target, sub, args); break;
163
+ case "persona-simulate": cmdPersonaSimulate(target, args.slice(1)); break;
123
164
  case "graph": await cmdGraph(target, sub, args); break;
124
165
  case "models": cmdModels(sub || args[1]); break;
166
+ case "delegate": case "delegation": {
167
+ const deScript = findRepoScript(target, "delegation_engine.py");
168
+ if (!deScript) { log("delegation engine unavailable"); break; }
169
+ const deCmd = cmd === "delegate" ? "delegate" : (sub || "show");
170
+ const fwd = [deScript, deCmd, "--target", target];
171
+ if (deCmd === "delegate") {
172
+ // Find goal: first non-flag arg after delegate, or --goal value
173
+ let goal = "";
174
+ for (let i = 0; i < args.length; i++) {
175
+ if (args[i] === "--goal" && args[i + 1]) { goal = args[i + 1]; i++; }
176
+ else if (!args[i].startsWith("-") && !goal) goal = args[i];
177
+ }
178
+ if (sub && !sub.startsWith("-")) goal = goal || sub;
179
+ if (goal) fwd.push(goal);
180
+ for (let i = 0; i < args.length; i++) {
181
+ if (args[i] === "--agent" && args[i + 1]) { fwd.push("--agent", args[i + 1]); i++; }
182
+ else if (args[i] === "--model" && args[i + 1]) { fwd.push("--model", args[i + 1]); i++; }
183
+ else if (args[i] === "--task-type" && args[i + 1]) { fwd.push("--task-type", args[i + 1]); i++; }
184
+ }
185
+ if (args.includes("--dry-run")) fwd.push("--dry-run");
186
+ }
187
+ if (args.includes("--json")) fwd.push("--json");
188
+ const result = spawnSync("python3", fwd, { stdio: "inherit", timeout: 15000 });
189
+ if (typeof result.status === "number" && result.status !== 0) process.exit(result.status);
190
+ break;
191
+ }
125
192
  case "redeem": await cmdRedeem(sub || args[1]); break;
126
193
  case "terminal": case "term":
127
194
  try {
@@ -192,30 +259,38 @@ async function main() {
192
259
  console.log(" watch Live task monitor: queue, active, recently done [--interval N]");
193
260
  console.log(" audit Scan ai/ and agent configs for leaked secrets");
194
261
  console.log(" init Initialize ai/ layer (via API) [--dry-run] [--minimal]");
195
- console.log(" sync Update ai/ layer (via API) [--dry-run] [--quiet]");
262
+ console.log(" sync Update ai/ layer (via API) [--dry-run] [--quiet] [--force]");
196
263
  console.log(" detect Show detected stack");
197
- console.log(" doctor Check health + credentials checklist");
264
+ console.log(" doctor Check health + credentials checklist [--drift]");
198
265
  console.log(" update Update all installed agent CLIs to latest [--dry-run]");
199
266
  console.log(" validate Validate ai/ layer completeness");
200
267
  console.log(" reflect Session reflection: delivered, delegation rate, blockers");
201
268
  console.log(" metrics Effectiveness score: adoption funnel, sessions, delegation");
202
269
  console.log(" portfolio All tracked projects: score, sessions, agents, last activity");
203
- console.log(" status Show maturity, swarm, session");
270
+ console.log(" status Show maturity, swarm, session [--json]");
204
271
  console.log(" session save Save session for roaming");
205
272
  console.log(" swarm status Task queue & delegation");
206
273
  console.log(" swarm webhook add Register webhook (fires on task done/failed)");
207
274
  console.log(" swarm webhook list Show registered webhooks");
275
+ console.log(" ssh Manage SSH keys, hosts, grants, and host-side sync");
276
+ console.log(" provider Local provider profiles, bindings, and direct invoke");
208
277
  console.log(" swarm webhook test Send test ping to a webhook URL");
278
+ console.log(" workspace init Create tmux workspace config (auto-detect services)");
279
+ console.log(" workspace up Start all workspace sessions");
280
+ console.log(" workspace status Show session status table");
209
281
  console.log(" feedback push Send feedback to 0dai");
210
282
  console.log(" report preview Preview privacy-safe project report");
211
283
  console.log(" report push Send report to 0dai (with offline queue)");
212
284
  console.log(" report status Show last report, queue, and auto-report status");
285
+ console.log(" persona-simulate Produce a focus-group report and optional issue drafts");
213
286
  console.log(" experience list Show recent structured experience events");
214
287
  console.log(" experience stats Show success and cost stats by agent/model/type");
215
288
  console.log(" graph push Upload local graph to server (Pro: edges, Free: nodes)");
216
289
  console.log(" graph pull Download server graph and merge locally");
217
290
  console.log(" graph status Show local graph stats and sync state");
218
291
  console.log(" models Show model ratings (--fast/--balanced/--deep/--available)");
292
+ console.log(" delegate Auto-route task to best agent/model (0dai delegate 'goal')");
293
+ console.log(" delegation show Show current delegation policy");
219
294
  console.log(" terminal Launch interactive agent session");
220
295
  console.log(" auth login Authenticate (device code flow)");
221
296
  console.log(" auth logout Remove credentials");
@@ -8,6 +8,40 @@ const {
8
8
  makeEnsureAuthenticated, ensureLicenseActivation,
9
9
  } = shared;
10
10
 
11
+ async function resolveSessionState() {
12
+ const auth = loadAuthState();
13
+ const hasCachedToken = !!(auth && (auth.api_key || auth.access_token || auth.token));
14
+ if (!hasCachedToken) {
15
+ return {
16
+ ok: false,
17
+ degraded: false,
18
+ auth: null,
19
+ status: null,
20
+ message: "Not logged in. Run: 0dai auth login",
21
+ };
22
+ }
23
+
24
+ const status = await fetchAuthStatus();
25
+ if (!status || status.error || !status.email) {
26
+ const email = auth.email || auth.user || "unknown";
27
+ return {
28
+ ok: false,
29
+ degraded: true,
30
+ auth,
31
+ status,
32
+ message: `Saved session for ${email}, but cloud validation failed. Run: 0dai auth login`,
33
+ };
34
+ }
35
+
36
+ return {
37
+ ok: true,
38
+ degraded: false,
39
+ auth,
40
+ status,
41
+ message: "",
42
+ };
43
+ }
44
+
11
45
  async function cmdAuthLogin() {
12
46
  const isTTY = process.stdout.isTTY && process.stdin.isTTY;
13
47
 
@@ -131,8 +165,8 @@ async function cmdAuthLogin() {
131
165
  process.exit(1);
132
166
  }
133
167
  }
134
- s.stop("Timed out");
135
- p.log.error("Try again.");
168
+ s.stop("Device code expired");
169
+ p.log.error("The code expired after 10 minutes. Run '0dai auth login' to get a new code.");
136
170
  process.exit(1);
137
171
 
138
172
  } else {
@@ -158,7 +192,7 @@ async function cmdAuthLogin() {
158
192
  return;
159
193
  }
160
194
  }
161
- log("Timed out");
195
+ log("Device code expired after 10 minutes. Run '0dai auth login' again.");
162
196
  process.exit(1);
163
197
  }
164
198
  }
@@ -196,26 +230,25 @@ async function cmdRedeem(code) {
196
230
  }
197
231
 
198
232
  async function cmdAuthStatus() {
199
- try {
200
- const auth = loadAuthState();
201
- if (!auth) throw new Error("missing auth");
202
- // Backwards compat: old auth.json used `user`, new uses `email`
203
- const email = auth.email || auth.user || "unknown";
204
- log(`${email} (${auth.plan || "free"} plan)`);
205
- // Get usage from API
206
- const status = await fetchAuthStatus();
207
- if (status.usage_today) {
208
- console.log(" Usage today:");
209
- for (const [k, v] of Object.entries(status.usage_today))
210
- console.log(` ${k}: ${v} / ${status.limits[k]}`);
211
- }
212
- const license = status.license || auth.license || { status: "inactive" };
213
- console.log(` Activation: ${license.status || "inactive"}${license.activation_id ? ` (${license.activation_id})` : ""}`);
214
- if (status.projects && status.projects.length) {
215
- console.log(` Projects bound: ${status.projects.length} / ${status.project_limit || "?"}`);
216
- }
217
- } catch {
218
- log("Not logged in. Run: 0dai auth login");
233
+ const session = await resolveSessionState();
234
+ if (!session.ok) {
235
+ log(session.message);
236
+ process.exitCode = 1;
237
+ return;
238
+ }
239
+
240
+ const { auth, status } = session;
241
+ const email = status.email || auth.email || auth.user || "unknown";
242
+ log(`${email} (${status.plan || auth.plan || "free"} plan)`);
243
+ if (status.usage_today) {
244
+ console.log(" Usage today:");
245
+ for (const [k, v] of Object.entries(status.usage_today))
246
+ console.log(` ${k}: ${v} / ${status.limits[k]}`);
247
+ }
248
+ const license = status.license || auth.license || { status: "inactive" };
249
+ console.log(` Activation: ${license.status || "inactive"}${license.activation_id ? ` (${license.activation_id})` : ""}`);
250
+ if (status.projects && status.projects.length) {
251
+ console.log(` Projects bound: ${status.projects.length} / ${status.project_limit || "?"}`);
219
252
  }
220
253
  }
221
254
 
@@ -229,13 +262,19 @@ async function cmdActivateFree() {
229
262
  }
230
263
 
231
264
  async function cmdActivateStatus() {
232
- const ensureAuthenticated = makeEnsureAuthenticated(cmdAuthLogin);
233
- const status = await ensureAuthenticated("activation status");
234
- const license = status.license || (await apiCall("/v1/licenses/status")).license || { status: "inactive" };
265
+ const session = await resolveSessionState();
266
+ if (!session.ok) {
267
+ log(session.message);
268
+ process.exitCode = 1;
269
+ return;
270
+ }
271
+
272
+ const licenseResult = await apiCall("/v1/licenses/status");
273
+ const license = licenseResult.license || session.status.license || session.auth.license || { status: "inactive" };
235
274
  updateAuthState({ license });
236
275
  log(`license ${license.status || "inactive"}`);
237
276
  if (license.activation_id) console.log(` activation id: ${license.activation_id}`);
238
- console.log(` plan: ${license.plan || status.plan || "free"}`);
277
+ console.log(` plan: ${license.plan || session.status.plan || session.auth.plan || "free"}`);
239
278
  }
240
279
 
241
- module.exports = { cmdAuthLogin, cmdAuthLogout, cmdRedeem, cmdAuthStatus, cmdActivateFree, cmdActivateStatus };
280
+ module.exports = { cmdAuthLogin, cmdAuthLogout, cmdRedeem, cmdAuthStatus, cmdActivateFree, cmdActivateStatus, resolveSessionState };
@@ -2,7 +2,7 @@
2
2
  const shared = require("../shared");
3
3
  const { log, T, R, D, fs, path, spawnSync, findRepoScript, SUPPORTED_CLIS, recordExperienceEvent } = shared;
4
4
 
5
- function cmdDoctor(target) {
5
+ function cmdDoctor(target, options = {}) {
6
6
  const ai = path.join(target, "ai");
7
7
  if (!fs.existsSync(ai)) { log("No 0dai config found. Run: 0dai init"); return; }
8
8
  let v = "?", stack = "generic";
@@ -108,6 +108,36 @@ function cmdDoctor(target) {
108
108
  console.log(` ${mark.padEnd(22)} ${c.name}${hint}`);
109
109
  }
110
110
 
111
+ console.log("\n provider profiles:");
112
+ try {
113
+ const providerScript = findRepoScript(target, "provider_profiles.py");
114
+ if (!providerScript) {
115
+ console.log(` ${D}—${R2} unavailable in this environment`);
116
+ } else {
117
+ const pr = spawnSync(
118
+ "python3",
119
+ [providerScript, "status", "--target", target, "--json"],
120
+ { stdio: ["ignore", "pipe", "ignore"], encoding: "utf8", timeout: 5000 },
121
+ );
122
+ if (pr.stdout) {
123
+ const payload = JSON.parse(pr.stdout);
124
+ const bindings = payload.bindings && typeof payload.bindings === "object" ? payload.bindings : {};
125
+ const resolved = Object.entries(bindings).filter(([, profileId]) => Boolean(profileId));
126
+ if (!resolved.length) {
127
+ console.log(` ${D}—${R2} no provider profiles bound`);
128
+ } else {
129
+ for (const [agent, profileId] of resolved.sort((a, b) => a[0].localeCompare(b[0]))) {
130
+ console.log(` ${G}ok${R2} ${agent} ${D}→ ${profileId}${R2}`);
131
+ }
132
+ }
133
+ } else {
134
+ console.log(` ${D}—${R2} no provider profiles bound`);
135
+ }
136
+ }
137
+ } catch {
138
+ console.log(` ${D}—${R2} unable to resolve provider profile status`);
139
+ }
140
+
111
141
  // --- agent CLIs check ---
112
142
  const { execFileSync: _ef2 } = require("child_process");
113
143
  let updatesAvailable = 0;
@@ -172,23 +202,39 @@ function cmdDoctor(target) {
172
202
  review_needed: warnings > 0,
173
203
  },
174
204
  });
175
- if (errors) process.exitCode = 1;
205
+ if (errors && !options.suppressExitCode) process.exitCode = 1;
176
206
 
177
207
  // Drift summary (lightweight — full report via --drift flag)
178
- try {
179
- const ds = findRepoScript(target, "drift_detector.py");
180
- if (ds) {
181
- const dr = spawnSync("python3", [ds, "report", "--target", target],
182
- { stdio: ["ignore", "pipe", "ignore"], encoding: "utf8", timeout: 5000 });
183
- if (dr.stdout && dr.stdout.includes("MODIFIED")) {
184
- const lines = dr.stdout.trim().split("\n");
185
- const driftCount = lines.filter(l => l.includes("MODIFIED") || l.includes("CONTRADICTS")).length;
186
- if (driftCount > 0) {
187
- console.log(`\n config drift: ${driftCount} issue(s) — run: 0dai doctor --drift`);
208
+ if (!options.drift) {
209
+ try {
210
+ const ds = findRepoScript(target, "drift_detector.py");
211
+ if (ds) {
212
+ const dr = spawnSync("python3", [ds, "report", "--target", target],
213
+ { stdio: ["ignore", "pipe", "ignore"], encoding: "utf8", timeout: 5000 });
214
+ if (dr.stdout && dr.stdout.includes("MODIFIED")) {
215
+ const lines = dr.stdout.trim().split("\n");
216
+ const driftCount = lines.filter(l => l.includes("MODIFIED") || l.includes("CONTRADICTS")).length;
217
+ if (driftCount > 0) {
218
+ console.log(`\n config drift: ${driftCount} issue(s) — run: 0dai doctor --drift`);
219
+ }
188
220
  }
189
221
  }
222
+ } catch {}
223
+ }
224
+
225
+ // --fix: auto-repair by running sync
226
+ if (process.argv.includes("--fix") && errors) {
227
+ console.log(`\n ${G}auto-fix:${R2} running 0dai sync to regenerate missing files...`);
228
+ try {
229
+ const { cmdSync } = require("./init");
230
+ cmdSync(target, ["--quiet"]);
231
+ } catch (e) {
232
+ console.log(` ${E}auto-fix failed:${R2} ${e.message}`);
233
+ console.log(` ${D}Run manually: 0dai sync${R2}`);
190
234
  }
191
- } catch {}
235
+ } else if (errors) {
236
+ console.log(`\n ${D}Tip: run 0dai doctor --fix to auto-repair${R2}`);
237
+ }
192
238
  }
193
239
 
194
240
  module.exports = { cmdDoctor };
@@ -162,10 +162,109 @@ async function cmdGraph(target, sub, args) {
162
162
  return;
163
163
  }
164
164
 
165
- console.log("Usage: 0dai graph [push|pull|status]");
166
- console.log(" push Upload local graph to server (Pro: edges, Free: nodes only)");
167
- console.log(" pull Download server graph and merge locally");
168
- console.log(" status Show local graph stats and sync state");
165
+ if (sub === "history") {
166
+ const authFile = path.join(require("os").homedir(), ".0dai", "auth.json");
167
+ let auth = {};
168
+ try { auth = JSON.parse(fs.readFileSync(authFile, "utf8")); } catch {}
169
+ const plan = (auth.plan || "free").toLowerCase();
170
+ const limitIdx = args.indexOf("--limit");
171
+ const limit = limitIdx >= 0 && args[limitIdx + 1] ? parseInt(args[limitIdx + 1], 10) || 50 : 50;
172
+ const sinceIdx = args.indexOf("--since");
173
+ const since = sinceIdx >= 0 ? args[sinceIdx + 1] : null;
174
+ const nodeIdx = args.indexOf("--node");
175
+ const nodeId = nodeIdx >= 0 ? args[nodeIdx + 1] : null;
176
+
177
+ if (["pro", "team", "enterprise"].includes(plan) && auth.access_token) {
178
+ const identity = buildProjectIdentity(target, collectMetadata(target));
179
+ const params = new URLSearchParams({
180
+ project_id: identity.project_id,
181
+ limit: String(Math.max(1, limit)),
182
+ });
183
+ if (since) params.set("since", since);
184
+ if (nodeId) params.set("target_id", nodeId);
185
+ const result = await apiCall(`/v1/graph/history?${params.toString()}`);
186
+ if (result.error) {
187
+ log(`Error: ${result.error}`);
188
+ if (result.hint) log(result.hint);
189
+ return;
190
+ }
191
+ const entries = result.mutations || [];
192
+ if (!entries.length) { console.log(" No graph history yet."); return; }
193
+ for (const entry of entries) {
194
+ const actor = (entry.agent || "unknown").padEnd(7);
195
+ const context = entry.context ? ` | "${entry.context}"` : "";
196
+ console.log(` ${entry.timestamp || "?"} | ${actor} | ${entry.action.padEnd(13)} | ${entry.target_id}${context}`);
197
+ }
198
+ return;
199
+ }
200
+ log(`${D}Graph history requires Pro plan. Upgrade: 0dai upgrade${R}`);
201
+ return;
202
+ }
203
+
204
+ if (sub === "context") {
205
+ if (!fs.existsSync(graphFile)) {
206
+ log("No local graph found. Run: 0dai graph init or create ai/manifest/project_graph.json");
207
+ return;
208
+ }
209
+ const slicerScript = shared.findRepoScript(target, "graph_slicer_cli.py");
210
+ if (!slicerScript) {
211
+ log("graph slicer unavailable in this environment");
212
+ console.log(` ${D}Expected scripts/graph_slicer_cli.py in repo checkout${R}`);
213
+ return;
214
+ }
215
+ const forwarded = [slicerScript, "--target", target];
216
+ let i = 0;
217
+ while (i < args.length) {
218
+ if (args[i] === "--scope" && args[i + 1]) { forwarded.push("--scope", args[i + 1]); i += 2; }
219
+ else if (args[i] === "--task" && args[i + 1]) { forwarded.push("--task", args[i + 1]); i += 2; }
220
+ else if (args[i] === "--depth" && args[i + 1]) { forwarded.push("--depth", args[i + 1]); i += 2; }
221
+ else if (args[i] === "--budget" && args[i + 1]) { forwarded.push("--budget", args[i + 1]); i += 2; }
222
+ else if (args[i] === "--json") { forwarded.push("--json"); i += 1; }
223
+ else { i += 1; }
224
+ }
225
+ const result = shared.spawnSync("python3", forwarded, { stdio: "inherit" });
226
+ if (typeof result.status === "number" && result.status !== 0) process.exit(result.status);
227
+ return;
228
+ }
229
+
230
+ if (sub === "outcomes") {
231
+ const authFile = path.join(require("os").homedir(), ".0dai", "auth.json");
232
+ let auth = {};
233
+ try { auth = JSON.parse(fs.readFileSync(authFile, "utf8")); } catch {}
234
+ const plan = (auth.plan || "free").toLowerCase();
235
+ if (!["pro", "team", "enterprise"].includes(plan)) {
236
+ log(`${D}Outcomes require Pro plan. Upgrade: 0dai upgrade${R}`);
237
+ return;
238
+ }
239
+ if (!auth.access_token) {
240
+ log("Graph outcomes requires an account. Run: 0dai auth login");
241
+ return;
242
+ }
243
+ const identity = buildProjectIdentity(target, collectMetadata(target));
244
+ const params = new URLSearchParams({ project_id: identity.project_id });
245
+ const limitIdx = args.indexOf("--limit");
246
+ if (limitIdx >= 0 && args[limitIdx + 1]) params.set("limit", args[limitIdx + 1]);
247
+ const sinceIdx = args.indexOf("--since");
248
+ if (sinceIdx >= 0 && args[sinceIdx + 1]) params.set("since", args[sinceIdx + 1]);
249
+ const result = await apiCall(`/v1/outcomes?${params.toString()}`);
250
+ if (result.error) { log(`Error: ${result.error}`); return; }
251
+ const stats = result.stats || {};
252
+ log(`Outcomes: ${stats.total || 0} total, ${stats.success_rate || 0}% success rate`);
253
+ if (result.outcomes && result.outcomes.length) {
254
+ for (const o of result.outcomes.slice(0, 10)) {
255
+ console.log(` ${o.result || "?"} | ${o.agent || "?"} | ${o.title || o.task_id || "?"}`);
256
+ }
257
+ }
258
+ return;
259
+ }
260
+
261
+ console.log("Usage: 0dai graph [push|pull|status|history|context|outcomes]");
262
+ console.log(" push Upload local graph to server (Pro: edges, Free: nodes only)");
263
+ console.log(" pull Download server graph and merge locally");
264
+ console.log(" status Show local graph stats and sync state");
265
+ console.log(" history Show graph mutation timeline [--since 7d] [--node ID] [--limit 50]");
266
+ console.log(" context Get relevant graph context for a task [--scope FILE] [--task TYPE]");
267
+ console.log(" outcomes Show outcome analytics (Pro only)");
169
268
  }
170
269
 
171
270
  module.exports = { cmdGraph };