@0dai-dev/cli 4.2.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 (48) hide show
  1. package/README.md +30 -5
  2. package/bin/0dai.js +289 -60
  3. package/lib/commands/audit.js +13 -0
  4. package/lib/commands/auth.js +341 -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 +20 -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 +440 -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 +44 -4
  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 +26 -1
  30. package/lib/commands/swarm.js +97 -4
  31. package/lib/commands/tui.js +81 -13
  32. package/lib/commands/usage.js +87 -0
  33. package/lib/commands/vault.js +246 -0
  34. package/lib/onboarding.js +9 -3
  35. package/lib/shared.js +29 -14
  36. package/lib/tui/index.mjs +571 -187
  37. package/lib/utils/auth.js +1 -0
  38. package/lib/utils/canonical-counts.js +54 -0
  39. package/lib/utils/diff-preview.js +192 -0
  40. package/lib/utils/identity.js +76 -18
  41. package/lib/utils/mcp-auth.js +607 -0
  42. package/lib/utils/plan.js +37 -2
  43. package/lib/vault/cipher.js +125 -0
  44. package/lib/vault/identity.js +122 -0
  45. package/lib/vault/index.js +184 -0
  46. package/lib/vault/storage.js +84 -0
  47. package/lib/wizard.js +19 -12
  48. package/package.json +2 -2
@@ -0,0 +1,173 @@
1
+ "use strict";
2
+ // Play Book CLI — curated plays come from the server-side registry
3
+ // (/v1/plays, gated by plan). Local plays live under ai/plays/ inside
4
+ // the project and support fork/rank/preview via scripts/play_book.py.
5
+ //
6
+ // Subcommands:
7
+ // 0dai play list # curated plays (server) + local (disk)
8
+ // 0dai play get <slug> # show details of one play
9
+ // 0dai play search <query…> # substring search over local + server
10
+ // 0dai play fork <slug> --as <new> --author <handle>
11
+ // 0dai play run <slug> [--var K=V …]
12
+ // 0dai play rank # local-only ranking (uses ai/outcomes if present)
13
+ const shared = require("../shared");
14
+ const { log, T, R, D, apiCall } = shared;
15
+ const { spawnSync } = require("child_process");
16
+ const path = require("path");
17
+ const fs = require("fs");
18
+
19
+ function findRepoScript(target, name) {
20
+ const candidates = [
21
+ path.join(target, "scripts", name),
22
+ path.join(__dirname, "../../../../scripts", name),
23
+ path.join("/root/0dai/scripts", name),
24
+ ];
25
+ for (const c of candidates) {
26
+ try { if (fs.existsSync(c)) return c; } catch {}
27
+ }
28
+ return null;
29
+ }
30
+
31
+ function runLocal(target, args) {
32
+ const script = findRepoScript(target, "play_book.py");
33
+ if (!script) return { ok: false, error: "play_book.py not found locally" };
34
+ const res = spawnSync("python3", [script, "--root", target, ...args], {
35
+ encoding: "utf8",
36
+ stdio: ["ignore", "pipe", "pipe"],
37
+ });
38
+ if (res.status !== 0) {
39
+ return { ok: false, error: (res.stderr || res.stdout || "").trim() };
40
+ }
41
+ try { return { ok: true, data: JSON.parse(res.stdout || "[]") }; }
42
+ catch { return { ok: true, data: res.stdout }; }
43
+ }
44
+
45
+ function fmtTags(tags) {
46
+ if (!tags || !tags.length) return `${D}—${R}`;
47
+ return tags.slice(0, 6).map(t => `${D}#${t}${R}`).join(" ");
48
+ }
49
+
50
+ async function cmdList(target) {
51
+ log("curated (server):");
52
+ const remote = await apiCall("/v1/plays");
53
+ if (remote && Array.isArray(remote.plays) && remote.plays.length) {
54
+ for (const p of remote.plays) {
55
+ console.log(` ${T}${(p.slug || "").padEnd(28)}${R} ${p.title || ""} ${fmtTags(p.stacks)}`);
56
+ }
57
+ if (remote.plan && remote.plan !== "pro" && remote.plan !== "team") {
58
+ console.log(` ${D}plan: ${remote.plan} — some plays locked; upgrade at /pricing${R}`);
59
+ }
60
+ } else {
61
+ log(`no curated plays available (plan: ${remote && remote.plan || "?"})`);
62
+ }
63
+
64
+ const local = runLocal(target, ["list"]);
65
+ if (local.ok && Array.isArray(local.data) && local.data.length) {
66
+ console.log("");
67
+ log("local (this repo):");
68
+ for (const p of local.data) {
69
+ const origin = p.parent ? `← ${p.parent}` : "original";
70
+ console.log(` ${T}${(p.slug || "").padEnd(28)}${R} ${p.title || ""} ${D}${origin}${R}`);
71
+ }
72
+ }
73
+ }
74
+
75
+ async function cmdGet(target, slug) {
76
+ if (!slug) { log("usage: 0dai play get <slug>"); return; }
77
+ // Try remote first (curated), fall back to local
78
+ const remote = await apiCall(`/v1/plays/${encodeURIComponent(slug)}`);
79
+ if (remote && !remote.error) {
80
+ console.log(JSON.stringify(remote, null, 2));
81
+ return;
82
+ }
83
+ const local = runLocal(target, ["get", slug]);
84
+ if (local.ok) {
85
+ console.log(typeof local.data === "string" ? local.data : JSON.stringify(local.data, null, 2));
86
+ return;
87
+ }
88
+ log(`not found on server or locally: ${slug}`);
89
+ process.exit(1);
90
+ }
91
+
92
+ async function cmdSearch(target, query) {
93
+ if (!query) { log("usage: 0dai play search <query>"); return; }
94
+ const remote = await apiCall("/v1/plays");
95
+ const q = query.toLowerCase().split(/\s+/).filter(Boolean);
96
+ const hits = [];
97
+ if (remote && Array.isArray(remote.plays)) {
98
+ for (const p of remote.plays) {
99
+ const hay = [p.slug, p.title, p.description, ...(p.stacks || [])].join(" ").toLowerCase();
100
+ if (q.every(t => hay.includes(t))) hits.push({ origin: "server", ...p });
101
+ }
102
+ }
103
+ const local = runLocal(target, ["search", ...query.split(/\s+/)]);
104
+ if (local.ok && Array.isArray(local.data)) {
105
+ for (const p of local.data) hits.push({ origin: "local", ...p });
106
+ }
107
+ if (!hits.length) { log("no matches"); return; }
108
+ for (const h of hits) {
109
+ console.log(` ${T}[${h.origin}]${R} ${(h.slug || "").padEnd(26)} ${h.title || ""}`);
110
+ }
111
+ }
112
+
113
+ function cmdFork(target, slug, args) {
114
+ if (!slug) { log("usage: 0dai play fork <slug> --as <new-slug> --author <handle>"); return; }
115
+ const newSlug = argAfter(args, "--as");
116
+ const author = argAfter(args, "--author");
117
+ if (!newSlug || !author) { log("fork requires --as <new-slug> --author <handle>"); return; }
118
+ const res = runLocal(target, ["fork", slug, "--as", newSlug, "--author", author]);
119
+ if (!res.ok) { log(`fork failed: ${res.error}`); process.exit(1); }
120
+ console.log(typeof res.data === "string" ? res.data : JSON.stringify(res.data, null, 2));
121
+ }
122
+
123
+ function cmdRun(target, slug, args) {
124
+ if (!slug) { log("usage: 0dai play run <slug> [--var K=V …]"); return; }
125
+ const fwdArgs = ["run", slug];
126
+ for (let i = 0; i < args.length; i++) {
127
+ if (args[i] === "--var" && args[i + 1]) { fwdArgs.push("--var", args[i + 1]); i++; }
128
+ }
129
+ const res = runLocal(target, fwdArgs);
130
+ if (!res.ok) { log(`run failed: ${res.error}`); process.exit(1); }
131
+ console.log(typeof res.data === "string" ? res.data : JSON.stringify(res.data, null, 2));
132
+ }
133
+
134
+ function cmdRank(target, args) {
135
+ const fwdArgs = ["rank"];
136
+ const outcomes = argAfter(args, "--outcomes");
137
+ if (outcomes) fwdArgs.push("--outcomes", outcomes);
138
+ const res = runLocal(target, fwdArgs);
139
+ if (!res.ok) { log(`rank failed: ${res.error}`); process.exit(1); }
140
+ console.log(typeof res.data === "string" ? res.data : JSON.stringify(res.data, null, 2));
141
+ }
142
+
143
+ function argAfter(args, flag) {
144
+ const i = args.indexOf(flag);
145
+ return i >= 0 && i + 1 < args.length ? args[i + 1] : null;
146
+ }
147
+
148
+ async function cmdPlay(target, sub, args) {
149
+ const remaining = args.slice(2);
150
+ if (!sub || sub === "help") {
151
+ console.log("Usage: 0dai play <command>");
152
+ console.log(" list Show curated + local plays");
153
+ console.log(" get <slug> Show one play");
154
+ console.log(" search <query…> Search curated + local");
155
+ console.log(" fork <slug> --as <new> --author <you> Fork into local");
156
+ console.log(" run <slug> [--var K=V …] Preview rendered steps");
157
+ console.log(" rank [--outcomes FILE] Rank local plays by success rate");
158
+ return;
159
+ }
160
+ switch (sub) {
161
+ case "list": return cmdList(target);
162
+ case "get": return cmdGet(target, args[1]);
163
+ case "search": return cmdSearch(target, args.slice(1).join(" "));
164
+ case "fork": return cmdFork(target, args[1], remaining);
165
+ case "run": return cmdRun(target, args[1], remaining);
166
+ case "rank": return cmdRank(target, remaining);
167
+ default:
168
+ log(`unknown subcommand: ${sub}`);
169
+ return cmdPlay(target, "help", []);
170
+ }
171
+ }
172
+
173
+ module.exports = { cmdPlay };
@@ -4,6 +4,75 @@ const shared = require("../shared");
4
4
  const { log, spawnSync, findRepoScript } = shared;
5
5
 
6
6
  function cmdProvider(target, args) {
7
+ // Check for per-repo provider commands first
8
+ const subcommand = args[0] || "";
9
+
10
+ if (subcommand === "list" || subcommand === "switch" || subcommand === "clear") {
11
+ // New per-repo provider profile commands — use provider_registry.py
12
+ const script = findRepoScript(target, "provider_registry_cli.py");
13
+ if (!script) {
14
+ // Fallback: inline Python call to provider_registry module
15
+ const pyCode = `
16
+ import sys
17
+ import json
18
+ import pathlib
19
+ sys.path.insert(0, '${target}/scripts')
20
+ import provider_registry as pr
21
+
22
+ target = pathlib.Path('${target}')
23
+ args = ${JSON.stringify(args)}
24
+ subcmd = args[0] if args else 'list'
25
+
26
+ try:
27
+ if subcmd == 'list':
28
+ providers = pr.list_providers_for_repo(target)
29
+ print('Provider Status for', target.name)
30
+ print('=' * 60)
31
+ for p in providers:
32
+ active = '→' if p['active'] else ' '
33
+ source = f"[{p['source']}]" if p['source'] else ''
34
+ creds = '✓' if p['has_credentials'] else '✗'
35
+ print(f" {active} {p['name']:<12} {creds} creds {source:<10} {p['base_url']}")
36
+ print()
37
+ try:
38
+ resolved = pr.resolve_for_repo(target)
39
+ print(f"Active: {resolved['provider_name']} (source={resolved['source']})")
40
+ except pr.ProviderRegistryError as e:
41
+ print(f"No active provider: {e}")
42
+ elif subcmd == 'switch':
43
+ provider_name = ''
44
+ for i, a in enumerate(args):
45
+ if a == '--provider' and i + 1 < len(args):
46
+ provider_name = args[i + 1]
47
+ if not provider_name:
48
+ print('Usage: 0dai provider switch --target . --provider NAME')
49
+ sys.exit(1)
50
+ result = pr.switch_provider(target, provider_name)
51
+ print(f"Switched {target.name} to provider: {result['provider_name']}")
52
+ print(f" base_url: {result['base_url']}")
53
+ print(f" audit log: ~/.0dai/audit/provider-changes.jsonl")
54
+ elif subcmd == 'clear':
55
+ cleared = pr.clear_repo_profile(target)
56
+ if cleared:
57
+ print(f"Cleared per-repo profile for {target.name}")
58
+ print(f"Will fall back to global default or env vars")
59
+ else:
60
+ print(f"No per-repo profile to clear for {target.name}")
61
+ except pr.ProviderRegistryError as e:
62
+ print(f"Error: {e}")
63
+ sys.exit(1)
64
+ `;
65
+ const result = spawnSync("python3", ["-c", pyCode], { stdio: "inherit" });
66
+ if (typeof result.status === "number") process.exit(result.status);
67
+ process.exit(1);
68
+ }
69
+ const forwarded = [script, ...args];
70
+ const result = spawnSync("python3", forwarded, { stdio: "inherit" });
71
+ if (typeof result.status === "number") process.exit(result.status);
72
+ process.exit(1);
73
+ }
74
+
75
+ // Legacy provider_profiles.py commands
7
76
  const script = findRepoScript(target, "provider_profiles.py");
8
77
  if (!script) {
9
78
  log("provider profiles unavailable in this environment");
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+
3
+ const shared = require("../shared");
4
+ const { log, T, R, D, E, W, G, fs, path, spawnSync, findRepoScript } = shared;
5
+
6
+ function _color(pct) {
7
+ if (pct == null) return D;
8
+ if (pct >= 95) return E;
9
+ if (pct >= 80) return W;
10
+ return G;
11
+ }
12
+
13
+ function _bar(pct, width = 20) {
14
+ if (pct == null) return D + "—".repeat(width) + R;
15
+ const filled = Math.min(width, Math.max(0, Math.round((pct / 100) * width)));
16
+ const empty = width - filled;
17
+ return _color(pct) + "█".repeat(filled) + R + D + "░".repeat(empty) + R;
18
+ }
19
+
20
+ function _pad(s, n) { s = String(s); return s + " ".repeat(Math.max(0, n - s.length)); }
21
+
22
+ function _loadEnvelope(target, opts = {}) {
23
+ const cachePath = path.join(target, "ai", "manifest", "agent-quotas.json");
24
+ const force = !!opts.refresh;
25
+ const script = findRepoScript(target, "agent_quotas.py");
26
+ if (script) {
27
+ const args = [script, "--target", target, "--json"];
28
+ if (force) args.push("--refresh");
29
+ const r = spawnSync("python3", args, {
30
+ encoding: "utf8",
31
+ timeout: 30000,
32
+ stdio: ["ignore", "pipe", "pipe"],
33
+ });
34
+ if (r.status === 0 && r.stdout) {
35
+ try { return JSON.parse(r.stdout); } catch {}
36
+ }
37
+ }
38
+ if (fs.existsSync(cachePath)) {
39
+ try { return JSON.parse(fs.readFileSync(cachePath, "utf8")); } catch {}
40
+ }
41
+ return { fetched_at: 0, fetched_at_iso: null, ttl_seconds: 600, agents: {} };
42
+ }
43
+
44
+ function cmdQuota(target, args = []) {
45
+ const opts = {
46
+ refresh: args.includes("--refresh") || args.includes("-r"),
47
+ json: args.includes("--json"),
48
+ };
49
+ const envelope = _loadEnvelope(target, opts);
50
+ if (opts.json) {
51
+ console.log(JSON.stringify(envelope, null, 2));
52
+ return envelope;
53
+ }
54
+ const agents = envelope.agents || {};
55
+ const known = ["codex", "qoder", "claude", "opencode", "gemini", "capi"];
56
+ log(`Agent quotas (cached ${envelope.fetched_at_iso || "never"}, ttl=${envelope.ttl_seconds || 600}s)`);
57
+ console.log("");
58
+ console.log(
59
+ ` ${_pad("AGENT", 10)} ${_pad("PLAN", 14)} ${_pad("WEEK%", 7)} ${_pad("HOUR%", 7)} ${_pad("BAR (weekly)", 22)} SOURCE`,
60
+ );
61
+ for (const a of known) {
62
+ const rec = agents[a] || { plan: "unknown", weekly_pct_used: null, hourly_pct_used: null, source: null, error: "no data" };
63
+ const weekly = rec.weekly_pct_used;
64
+ const hourly = rec.hourly_pct_used;
65
+ const bar = _bar(weekly);
66
+ const wstr = weekly == null ? `${D}—${R}` : `${_color(weekly)}${weekly}%${R}`;
67
+ const hstr = hourly == null ? `${D}—${R}` : `${_color(hourly)}${hourly}%${R}`;
68
+ const src = rec.source || rec.error || "—";
69
+ console.log(` ${_pad(a, 10)} ${_pad(rec.plan || "unknown", 14)} ${_pad(wstr, 7)} ${_pad(hstr, 7)} ${bar} ${D}${src}${R}`);
70
+ }
71
+ console.log("");
72
+ console.log(` ${D}Yellow >80% · Red >95% (skipped by dispatcher) · '0dai quota --refresh' forces re-poll${R}`);
73
+ return envelope;
74
+ }
75
+
76
+ module.exports = { cmdQuota };
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ /**
3
+ * `0dai receipt` — shareable PNG receipt for a 0dai session.
4
+ *
5
+ * Reads session telemetry from ai/sessions + ai/experience/events via
6
+ * scripts/receipt_png.py, saves a 1200×630 OG image to ~/.0dai/receipts/,
7
+ * and copies it to the system clipboard (pbcopy / xclip / wl-copy).
8
+ *
9
+ * Issue: #529 (The Bill)
10
+ */
11
+ const shared = require("../shared");
12
+ const { log, D, R, findRepoScript, spawnSync } = shared;
13
+
14
+ function _findArg(args, flag) {
15
+ const idx = args.indexOf(flag);
16
+ if (idx < 0 || !args[idx + 1]) return null;
17
+ return args[idx + 1];
18
+ }
19
+
20
+ function cmdReceipt(target, args) {
21
+ const script = findRepoScript(target, "receipt_png.py");
22
+ if (!script) {
23
+ log("receipt generator unavailable — missing scripts/receipt_png.py");
24
+ console.log(` ${D}Run from a 0dai-initialized project root.${R}`);
25
+ return;
26
+ }
27
+
28
+ const fwd = [script, "--target", target];
29
+
30
+ const sessionId = _findArg(args, "--session");
31
+ if (sessionId) {
32
+ fwd.push("--session", sessionId);
33
+ } else if (args.includes("--active")) {
34
+ fwd.push("--active");
35
+ } else {
36
+ // Default: --last (matches the acceptance criterion in #529).
37
+ fwd.push("--last");
38
+ }
39
+
40
+ const output = _findArg(args, "--output");
41
+ if (output) fwd.push("--output", output);
42
+
43
+ if (args.includes("--no-clipboard")) fwd.push("--no-clipboard");
44
+ if (args.includes("--json")) fwd.push("--json");
45
+ if (args.includes("--stats-only")) fwd.push("--stats-only");
46
+
47
+ const result = spawnSync("python3", fwd, { stdio: "inherit" });
48
+ if (typeof result.status === "number" && result.status !== 0) {
49
+ process.exit(result.status);
50
+ }
51
+ }
52
+
53
+ module.exports = { cmdReceipt };
@@ -1,8 +1,9 @@
1
1
  "use strict";
2
2
  const shared = require("../shared");
3
- const { log, D, R, spawnSync, findRepoScript, VERSION } = shared;
3
+ const { log, D, R, T, spawnSync, findRepoScript, VERSION } = shared;
4
+ const { confirmOrExit, shouldAutoYes } = require("../utils/diff-preview");
4
5
 
5
- function cmdReport(target, sub, args) {
6
+ async function cmdReport(target, sub, args) {
6
7
  const reportScript = findRepoScript(target, "report_manager.py");
7
8
  if (!reportScript) {
8
9
  log("report manager unavailable in this environment");
@@ -11,6 +12,32 @@ function cmdReport(target, sub, args) {
11
12
  }
12
13
 
13
14
  const command = sub || "preview";
15
+
16
+ if (command === "push") {
17
+ const skipPreview = args.includes("--no-preview");
18
+ const dryRun = args.includes("--dry-run");
19
+ if (!skipPreview) {
20
+ const previewArgs = [reportScript, "preview", "--target", target, "--channel", "npm", "--cli-version", VERSION];
21
+ log(`${T}preview of report payload before push:${R}`);
22
+ const preview = spawnSync("python3", previewArgs, { stdio: "inherit" });
23
+ if (typeof preview.status === "number" && preview.status !== 0) {
24
+ log("preview failed; aborting push");
25
+ process.exit(preview.status);
26
+ }
27
+ }
28
+ if (dryRun) {
29
+ log(`${D}dry-run: would push the report shown above. Re-run without --dry-run to send.${R}`);
30
+ return;
31
+ }
32
+ if (!shouldAutoYes(args)) {
33
+ const ok = await confirmOrExit({ args, message: "Push this report to 0dai cloud?", defaultYes: false });
34
+ if (!ok) {
35
+ log("push aborted by user");
36
+ return;
37
+ }
38
+ }
39
+ }
40
+
14
41
  const forwarded = [reportScript, command, "--target", target, "--channel", "npm", "--cli-version", VERSION];
15
42
 
16
43
  if (args.includes("--json")) forwarded.push("--json");
@@ -4,7 +4,7 @@ const { log, T, R, D, fs, path, apiCall } = shared;
4
4
 
5
5
  async function cmdRun(goal, target, args = []) {
6
6
  if (!goal) {
7
- console.log(`Usage: 0dai run <goal> [--dry-run] [--agent claude|codex|gemini]`);
7
+ console.log(`Usage: 0dai run <goal> [--dry-run] [--agent claude|codex|gemini] [--provider deepseek|gemini-direct|codex|claude-opus]`);
8
8
  console.log(` Example: 0dai run "add dark mode to settings page"`);
9
9
  return;
10
10
  }
@@ -12,6 +12,15 @@ async function cmdRun(goal, target, args = []) {
12
12
  const dryRun = args.includes("--dry-run");
13
13
  const agentIdx = args.indexOf("--agent");
14
14
  const agentOverride = agentIdx >= 0 ? args[agentIdx + 1] : null;
15
+ const providerIdx = args.indexOf("--provider");
16
+ const providerOverride = providerIdx >= 0 ? normalizeProvider(args[providerIdx + 1] || "") : null;
17
+ const providerModelIdx = args.indexOf("--provider-model");
18
+ const providerModel = providerModelIdx >= 0 ? (args[providerModelIdx + 1] || "") : "";
19
+
20
+ if (providerIdx >= 0 && !providerOverride) {
21
+ log("error: unsupported provider. Supported: deepseek, gemini-direct, codex, claude-opus, claude-sonnet");
22
+ return;
23
+ }
15
24
 
16
25
  // Read project context
17
26
  let stack = "generic", agents = ["claude"], commands = {};
@@ -35,9 +44,10 @@ async function cmdRun(goal, target, args = []) {
35
44
 
36
45
  for (let i = 0; i < tasks.length; i++) {
37
46
  const t = tasks[i];
38
- const agent = agentOverride || t.assigned_to;
47
+ const agent = agentOverride || providerAgent(providerOverride) || t.assigned_to;
39
48
  console.log(` ${T}${i + 1}.${R} ${t.title}`);
40
- console.log(` ${D}→ ${agent} [${t.model_tier}]${t.description ? " " + t.description : ""}${R}`);
49
+ const providerText = providerOverride ? ` via ${providerOverride}` : "";
50
+ console.log(` ${D}→ ${agent}${providerText} [${t.model_tier}]${t.description ? " " + t.description : ""}${R}`);
41
51
  }
42
52
 
43
53
  if (dryRun) {
@@ -58,12 +68,18 @@ async function cmdRun(goal, target, args = []) {
58
68
  id,
59
69
  title: t.title,
60
70
  description: t.description || "",
61
- assigned_to: agentOverride || t.assigned_to,
71
+ assigned_to: agentOverride || providerAgent(providerOverride) || t.assigned_to,
62
72
  model_tier: t.model_tier,
63
73
  created_by: "run",
64
74
  created_at: new Date().toISOString(),
65
75
  context: { goal },
66
76
  };
77
+ if (providerOverride) {
78
+ entry.provider_override = providerOverride;
79
+ entry.provider = providerOverride;
80
+ entry.provider_source = "cli_override";
81
+ if (providerModel) entry.provider_model = providerModel;
82
+ }
67
83
  try {
68
84
  fs.writeFileSync(path.join(queueDir, `${id}.json`), JSON.stringify(entry, null, 2));
69
85
  created.push(id);
@@ -74,4 +90,28 @@ async function cmdRun(goal, target, args = []) {
74
90
  console.log(` ${D}Monitor: 0dai watch | Queue: 0dai swarm status${R}\n`);
75
91
  }
76
92
 
93
+ function normalizeProvider(provider) {
94
+ const key = String(provider || "").trim().toLowerCase().replace(/_/g, "-");
95
+ const aliases = {
96
+ "deepseek": "deepseek",
97
+ "gemini": "gemini-direct",
98
+ "gemini-direct": "gemini-direct",
99
+ "gemini-1.5": "gemini-direct",
100
+ "codex": "codex",
101
+ "claude-opus": "claude-opus",
102
+ "opus": "claude-opus",
103
+ "claude-sonnet": "claude-sonnet",
104
+ "sonnet": "claude-sonnet",
105
+ };
106
+ return aliases[key] || "";
107
+ }
108
+
109
+ function providerAgent(provider) {
110
+ if (provider === "deepseek") return "deepseek";
111
+ if (provider === "gemini-direct") return "gemini";
112
+ if (provider === "codex") return "codex";
113
+ if (provider === "claude-opus" || provider === "claude-sonnet") return "claude";
114
+ return null;
115
+ }
116
+
77
117
  module.exports = { cmdRun };