@0dai-dev/cli 3.10.1 → 4.0.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/README.md +4 -2
- package/bin/0dai.js +96 -2153
- package/lib/commands/audit.js +129 -0
- package/lib/commands/auth.js +241 -0
- package/lib/commands/detect.js +31 -0
- package/lib/commands/doctor.js +194 -0
- package/lib/commands/experience.js +65 -0
- package/lib/commands/feedback.js +92 -0
- package/lib/commands/graph.js +171 -0
- package/lib/commands/init.js +282 -0
- package/lib/commands/metrics.js +145 -0
- package/lib/commands/models.js +57 -0
- package/lib/commands/portfolio.js +96 -0
- package/lib/commands/reflect.js +211 -0
- package/lib/commands/report.js +29 -0
- package/lib/commands/run.js +77 -0
- package/lib/commands/session.js +61 -0
- package/lib/commands/status.js +69 -0
- package/lib/commands/swarm.js +161 -0
- package/lib/commands/update.js +69 -0
- package/lib/commands/validate.js +71 -0
- package/lib/commands/watch.js +118 -0
- package/lib/onboarding.js +171 -0
- package/lib/shared.js +283 -0
- package/lib/utils/auth.js +142 -0
- package/lib/utils/constants.js +76 -0
- package/lib/utils/identity.js +147 -0
- package/lib/utils/plan.js +73 -0
- package/lib/wizard.js +311 -0
- package/package.json +5 -1
- package/scripts/postinstall.js +29 -0
package/bin/0dai.js
CHANGED
|
@@ -1,2153 +1,37 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
const CONFIG_DIR = path.join(os.homedir(), ".0dai");
|
|
36
|
-
const AUTH_FILE = path.join(CONFIG_DIR, "auth.json");
|
|
37
|
-
const VERSION_CHECK_FILE = path.join(CONFIG_DIR, ".version_check");
|
|
38
|
-
const PROJECTS_FILE = path.join(CONFIG_DIR, "projects.json");
|
|
39
|
-
|
|
40
|
-
const MANIFEST_FILES = [
|
|
41
|
-
// Node ecosystem
|
|
42
|
-
"package.json", "tsconfig.json",
|
|
43
|
-
"next.config.js", "next.config.mjs", "next.config.ts",
|
|
44
|
-
"vite.config.js", "vite.config.ts", "vite.config.mjs",
|
|
45
|
-
"vue.config.js", "nuxt.config.js", "nuxt.config.ts",
|
|
46
|
-
"svelte.config.js", "astro.config.mjs", "astro.config.ts",
|
|
47
|
-
"remix.config.js", "angular.json",
|
|
48
|
-
// Other languages
|
|
49
|
-
"go.mod", "pyproject.toml", "requirements.txt", "setup.py",
|
|
50
|
-
"pubspec.yaml", "Cargo.toml", "pom.xml", "build.gradle",
|
|
51
|
-
"Gemfile", "composer.json",
|
|
52
|
-
// Build/deploy
|
|
53
|
-
"Makefile", "docker-compose.yml", "Dockerfile",
|
|
54
|
-
"pnpm-workspace.yaml", "lerna.json", "turbo.json", "nx.json",
|
|
55
|
-
];
|
|
56
|
-
|
|
57
|
-
const PROBE_DIRS = [
|
|
58
|
-
"src", "lib", "app", "apps", "packages", "services",
|
|
59
|
-
"cmd", "internal", "web", "frontend", "backend",
|
|
60
|
-
"tests", "test", "spec", "__tests__",
|
|
61
|
-
"infra", "deploy", "docker", ".github",
|
|
62
|
-
"android", "ios", "linux", "macos", "windows",
|
|
63
|
-
];
|
|
64
|
-
|
|
65
|
-
// Single source of truth for all supported agent CLIs.
|
|
66
|
-
// Adding a new CLI here automatically wires detection, doctor, validate,
|
|
67
|
-
// and update — no other changes needed in this file. Keep in sync with
|
|
68
|
-
// bootstrap/common.sh's detect_available_clis() and scripts/swarm.py's
|
|
69
|
-
// AGENT_STRENGTHS dict (those are the bash + python equivalents).
|
|
70
|
-
//
|
|
71
|
-
// Fields:
|
|
72
|
-
// name logical name used in discovery.json + swarm
|
|
73
|
-
// bin actual binary name in $PATH (only differs from name for qoder)
|
|
74
|
-
// pkg package name for install/update (null = no auto-update)
|
|
75
|
-
// pkgType "npm" | "pip" | "go" — drives update install command
|
|
76
|
-
// install human-readable install hint shown in doctor output
|
|
77
|
-
// altAuth optional alternative auth hint (subscription, etc.)
|
|
78
|
-
// agentFiles files that must exist when this CLI is selected (cmdValidate)
|
|
79
|
-
const SUPPORTED_CLIS = [
|
|
80
|
-
{
|
|
81
|
-
name: "claude", bin: "claude",
|
|
82
|
-
pkg: "@anthropic-ai/claude-code", pkgType: "npm",
|
|
83
|
-
install: "npm i -g @anthropic-ai/claude-code",
|
|
84
|
-
altAuth: "Pro/Team subscription",
|
|
85
|
-
agentFiles: [".claude/settings.json", ".claude/CLAUDE.md", ".mcp.json"],
|
|
86
|
-
},
|
|
87
|
-
{
|
|
88
|
-
name: "codex", bin: "codex",
|
|
89
|
-
pkg: "@openai/codex", pkgType: "npm",
|
|
90
|
-
install: "npm i -g @openai/codex",
|
|
91
|
-
altAuth: "ChatGPT Pro subscription",
|
|
92
|
-
agentFiles: ["AGENTS.md", ".codex/config.toml"],
|
|
93
|
-
},
|
|
94
|
-
{
|
|
95
|
-
name: "opencode", bin: "opencode",
|
|
96
|
-
pkg: null, pkgType: "go",
|
|
97
|
-
install: "go install github.com/nichochar/opencode@latest",
|
|
98
|
-
altAuth: null,
|
|
99
|
-
agentFiles: ["opencode.json"],
|
|
100
|
-
},
|
|
101
|
-
{
|
|
102
|
-
name: "gemini", bin: "gemini",
|
|
103
|
-
pkg: "@google/gemini-cli", pkgType: "npm",
|
|
104
|
-
install: "npm i -g @google/gemini-cli",
|
|
105
|
-
altAuth: null,
|
|
106
|
-
agentFiles: null,
|
|
107
|
-
},
|
|
108
|
-
{
|
|
109
|
-
name: "aider", bin: "aider",
|
|
110
|
-
pkg: "aider-chat", pkgType: "pip",
|
|
111
|
-
install: "pip install aider-chat",
|
|
112
|
-
altAuth: null,
|
|
113
|
-
agentFiles: null,
|
|
114
|
-
},
|
|
115
|
-
{
|
|
116
|
-
name: "qoder", bin: "qodercli",
|
|
117
|
-
pkg: "@qoder-ai/qodercli", pkgType: "npm",
|
|
118
|
-
install: "npm i -g @qoder-ai/qodercli",
|
|
119
|
-
altAuth: null,
|
|
120
|
-
agentFiles: [".qoder/settings.json"],
|
|
121
|
-
},
|
|
122
|
-
];
|
|
123
|
-
|
|
124
|
-
function deviceFingerprint() {
|
|
125
|
-
const crypto = require("crypto");
|
|
126
|
-
const parts = [
|
|
127
|
-
os.hostname(),
|
|
128
|
-
os.userInfo().username,
|
|
129
|
-
os.platform(),
|
|
130
|
-
os.arch(),
|
|
131
|
-
os.cpus().length.toString(),
|
|
132
|
-
os.totalmem().toString(),
|
|
133
|
-
];
|
|
134
|
-
// Try machine-id
|
|
135
|
-
try {
|
|
136
|
-
if (os.platform() === "linux") parts.push(fs.readFileSync("/etc/machine-id", "utf8").trim());
|
|
137
|
-
else if (os.platform() === "darwin") {
|
|
138
|
-
const { execSync } = require("child_process");
|
|
139
|
-
parts.push(execSync("ioreg -rd1 -c IOPlatformExpertDevice | awk '/IOPlatformUUID/'", { encoding: "utf8" }).trim());
|
|
140
|
-
}
|
|
141
|
-
} catch {}
|
|
142
|
-
return crypto.createHash("sha256").update(parts.join(":")).digest("hex").slice(0, 32);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function registerProject(projectPath, name, stack) {
|
|
146
|
-
try {
|
|
147
|
-
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
148
|
-
let projects = [];
|
|
149
|
-
try { projects = JSON.parse(fs.readFileSync(PROJECTS_FILE, "utf8")).projects || []; } catch {}
|
|
150
|
-
const abs = path.resolve(projectPath);
|
|
151
|
-
const idx = projects.findIndex(p => p.path === abs);
|
|
152
|
-
const entry = { path: abs, name: name || path.basename(abs), stack: stack || "?", last_seen: new Date().toISOString() };
|
|
153
|
-
if (idx >= 0) projects[idx] = entry;
|
|
154
|
-
else projects.unshift(entry);
|
|
155
|
-
fs.writeFileSync(PROJECTS_FILE, JSON.stringify({ projects: projects.slice(0, 50) }, null, 2));
|
|
156
|
-
} catch {}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function cmdPortfolio() {
|
|
160
|
-
let projects = [];
|
|
161
|
-
try { projects = JSON.parse(fs.readFileSync(PROJECTS_FILE, "utf8")).projects || []; } catch {}
|
|
162
|
-
|
|
163
|
-
if (!projects.length) {
|
|
164
|
-
log(`no projects registered yet`);
|
|
165
|
-
console.log(` Run ${D}0dai init${R} in a project to start tracking it.`);
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const rows = [];
|
|
170
|
-
let totalSessions = 0, totalScore = 0, scored = 0;
|
|
171
|
-
|
|
172
|
-
for (const p of projects) {
|
|
173
|
-
if (!fs.existsSync(p.path)) continue;
|
|
174
|
-
|
|
175
|
-
let sessions = 0, lastSession = null, agentMap = {};
|
|
176
|
-
try {
|
|
177
|
-
const stats = JSON.parse(fs.readFileSync(path.join(p.path, "ai", "feedback", ".usage_stats.json"), "utf8"));
|
|
178
|
-
sessions = stats.total_sessions || 0;
|
|
179
|
-
lastSession = stats.last_session || null;
|
|
180
|
-
agentMap = stats.agents || {};
|
|
181
|
-
} catch {}
|
|
182
|
-
|
|
183
|
-
let name = p.name, stack = p.stack;
|
|
184
|
-
try {
|
|
185
|
-
const disc = JSON.parse(fs.readFileSync(path.join(p.path, "ai", "manifest", "discovery.json"), "utf8"));
|
|
186
|
-
name = disc.project_name || name;
|
|
187
|
-
stack = disc.stack || stack;
|
|
188
|
-
} catch {}
|
|
189
|
-
|
|
190
|
-
// Effectiveness score (mirrors metrics command)
|
|
191
|
-
let score = 0;
|
|
192
|
-
if (sessions > 0) {
|
|
193
|
-
score += Math.min(sessions * 5, 35);
|
|
194
|
-
let done = 0;
|
|
195
|
-
try { done = fs.readdirSync(path.join(p.path, "ai", "swarm", "done")).filter(f => f.endsWith(".json")).length; } catch {}
|
|
196
|
-
if (done > 0) score += Math.min(done * 6, 30);
|
|
197
|
-
try {
|
|
198
|
-
const hasFb = fs.readdirSync(path.join(p.path, "ai", "feedback")).some(f => f.endsWith("-report.json"));
|
|
199
|
-
if (hasFb) score += 20;
|
|
200
|
-
} catch {}
|
|
201
|
-
const layerFiles = ["ai/manifest/discovery.json", "ai/manifest/commands.yaml", "ai/playbooks/quick-start.md"];
|
|
202
|
-
score += Math.round(layerFiles.filter(f => fs.existsSync(path.join(p.path, f))).length / layerFiles.length * 15);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
totalSessions += sessions;
|
|
206
|
-
if (sessions > 0) { totalScore += score; scored++; }
|
|
207
|
-
|
|
208
|
-
const agentList = Object.keys(agentMap).join("·") || "—";
|
|
209
|
-
|
|
210
|
-
let ago = "never";
|
|
211
|
-
if (lastSession) {
|
|
212
|
-
const h = Math.floor((Date.now() - new Date(lastSession).getTime()) / 3600000);
|
|
213
|
-
const d = Math.floor(h / 24);
|
|
214
|
-
if (h < 1) ago = "<1h ago";
|
|
215
|
-
else if (h < 24) ago = `${h}h ago`;
|
|
216
|
-
else if (d < 7) ago = `${d}d ago`;
|
|
217
|
-
else ago = `${Math.floor(d / 7)}w ago`;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
rows.push({ name, stack, score: sessions > 0 ? score : null, sessions, agents: agentList, ago });
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
if (!rows.length) {
|
|
224
|
-
log("no projects found (paths may have moved)");
|
|
225
|
-
return;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
const nameW = Math.min(Math.max(...rows.map(r => r.name.length), 4), 28);
|
|
229
|
-
const stackW = Math.min(Math.max(...rows.map(r => r.stack.length), 5), 16);
|
|
230
|
-
|
|
231
|
-
console.log(`\n ${T}Portfolio${R} — ${rows.length} project${rows.length === 1 ? "" : "s"}\n`);
|
|
232
|
-
for (const r of rows) {
|
|
233
|
-
const nm = r.name.slice(0, nameW).padEnd(nameW);
|
|
234
|
-
const st = r.stack.slice(0, stackW).padEnd(stackW);
|
|
235
|
-
const sc = r.score !== null ? `score ${String(r.score).padStart(3)}` : " ";
|
|
236
|
-
const se = `${String(r.sessions).padStart(2)} session${r.sessions === 1 ? " " : "s"}`;
|
|
237
|
-
console.log(` ${T}${nm}${R} ${D}${st}${R} ${sc} ${se} ${r.agents.padEnd(14)} ${D}${r.ago}${R}`);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
if (totalSessions > 0) {
|
|
241
|
-
const avg = scored > 0 ? Math.round(totalScore / scored) : 0;
|
|
242
|
-
const stacks = [...new Set(rows.map(r => r.stack))].length;
|
|
243
|
-
console.log(`\n ${D}${"─".repeat(70)}`);
|
|
244
|
-
console.log(` Total: ${totalSessions} sessions ${stacks} stack${stacks === 1 ? "" : "s"} avg effectiveness: ${avg}${R}\n`);
|
|
245
|
-
} else {
|
|
246
|
-
console.log(`\n ${D}Tip: run '0dai init' in your projects to start tracking sessions.${R}\n`);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
async function cmdRun(goal, target, args = []) {
|
|
251
|
-
if (!goal) {
|
|
252
|
-
console.log(`Usage: 0dai run <goal> [--dry-run] [--agent claude|codex|gemini]`);
|
|
253
|
-
console.log(` Example: 0dai run "add dark mode to settings page"`);
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
const dryRun = args.includes("--dry-run");
|
|
258
|
-
const agentIdx = args.indexOf("--agent");
|
|
259
|
-
const agentOverride = agentIdx >= 0 ? args[agentIdx + 1] : null;
|
|
260
|
-
|
|
261
|
-
// Read project context
|
|
262
|
-
let stack = "generic", agents = ["claude"], commands = {};
|
|
263
|
-
try {
|
|
264
|
-
const disc = JSON.parse(fs.readFileSync(path.join(target, "ai", "manifest", "discovery.json"), "utf8"));
|
|
265
|
-
stack = disc.stack || "generic";
|
|
266
|
-
agents = disc.selected_agents || ["claude"];
|
|
267
|
-
} catch {}
|
|
268
|
-
|
|
269
|
-
if (!dryRun) process.stdout.write(`${T}[0dai]${R} decomposing goal...`);
|
|
270
|
-
const result = await apiCall("/v1/run", { goal, context: { stack, agents, commands } });
|
|
271
|
-
if (!dryRun) process.stdout.write("\r" + " ".repeat(40) + "\r");
|
|
272
|
-
|
|
273
|
-
if (result.error) { log(`error: ${result.error}`); return; }
|
|
274
|
-
|
|
275
|
-
const tasks = result.tasks || [];
|
|
276
|
-
if (!tasks.length) { log("no tasks returned"); return; }
|
|
277
|
-
|
|
278
|
-
console.log(`\n ${T}Goal:${R} ${goal}`);
|
|
279
|
-
console.log(` Decomposed into ${tasks.length} task${tasks.length === 1 ? "" : "s"}:\n`);
|
|
280
|
-
|
|
281
|
-
for (let i = 0; i < tasks.length; i++) {
|
|
282
|
-
const t = tasks[i];
|
|
283
|
-
const agent = agentOverride || t.assigned_to;
|
|
284
|
-
console.log(` ${T}${i + 1}.${R} ${t.title}`);
|
|
285
|
-
console.log(` ${D}→ ${agent} [${t.model_tier}]${t.description ? " " + t.description : ""}${R}`);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
if (dryRun) {
|
|
289
|
-
console.log(`\n ${D}[dry-run] would create ${tasks.length} task(s) in swarm queue${R}\n`);
|
|
290
|
-
return;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// Create queue entries
|
|
294
|
-
const queueDir = path.join(target, "ai", "swarm", "queue");
|
|
295
|
-
try { fs.mkdirSync(queueDir, { recursive: true }); } catch {}
|
|
296
|
-
|
|
297
|
-
const created = [];
|
|
298
|
-
for (const t of tasks) {
|
|
299
|
-
const ts = Date.now();
|
|
300
|
-
const rand = Math.random().toString(36).slice(2, 6);
|
|
301
|
-
const id = `run-${ts}-${rand}`;
|
|
302
|
-
const entry = {
|
|
303
|
-
id,
|
|
304
|
-
title: t.title,
|
|
305
|
-
description: t.description || "",
|
|
306
|
-
assigned_to: agentOverride || t.assigned_to,
|
|
307
|
-
model_tier: t.model_tier,
|
|
308
|
-
created_by: "run",
|
|
309
|
-
created_at: new Date().toISOString(),
|
|
310
|
-
context: { goal },
|
|
311
|
-
};
|
|
312
|
-
try {
|
|
313
|
-
fs.writeFileSync(path.join(queueDir, `${id}.json`), JSON.stringify(entry, null, 2));
|
|
314
|
-
created.push(id);
|
|
315
|
-
} catch (e) { log(`warn: could not write task ${id}: ${e.message}`); }
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
console.log(`\n ${T}✓${R} ${created.length} task${created.length === 1 ? "" : "s"} added to swarm queue`);
|
|
319
|
-
console.log(` ${D}Monitor: 0dai watch | Queue: 0dai swarm status${R}\n`);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
function apiCall(endpoint, data) {
|
|
323
|
-
return new Promise((resolve, reject) => {
|
|
324
|
-
const url = new URL(endpoint, API_URL);
|
|
325
|
-
const mod = url.protocol === "https:" ? https : http;
|
|
326
|
-
const body = data ? JSON.stringify(data) : null;
|
|
327
|
-
const headers = { "Content-Type": "application/json", "X-Device-ID": deviceFingerprint(), "X-CLI-Version": VERSION };
|
|
328
|
-
|
|
329
|
-
try {
|
|
330
|
-
const auth = JSON.parse(fs.readFileSync(AUTH_FILE, "utf8"));
|
|
331
|
-
const token = auth.api_key || auth.access_token || auth.token;
|
|
332
|
-
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
333
|
-
} catch {}
|
|
334
|
-
|
|
335
|
-
const opts = {
|
|
336
|
-
hostname: url.hostname,
|
|
337
|
-
port: url.port || (url.protocol === "https:" ? 443 : 80),
|
|
338
|
-
path: url.pathname,
|
|
339
|
-
method: body ? "POST" : "GET",
|
|
340
|
-
headers,
|
|
341
|
-
timeout: 60000,
|
|
342
|
-
};
|
|
343
|
-
if (body) opts.headers["Content-Length"] = Buffer.byteLength(body);
|
|
344
|
-
|
|
345
|
-
const req = mod.request(opts, (res) => {
|
|
346
|
-
let chunks = [];
|
|
347
|
-
res.on("data", (c) => chunks.push(c));
|
|
348
|
-
res.on("end", () => {
|
|
349
|
-
try {
|
|
350
|
-
resolve(JSON.parse(Buffer.concat(chunks).toString()));
|
|
351
|
-
} catch {
|
|
352
|
-
resolve({ error: `HTTP ${res.statusCode}` });
|
|
353
|
-
}
|
|
354
|
-
});
|
|
355
|
-
});
|
|
356
|
-
req.on("error", (e) => resolve({ error: `${e.message}. Is ${API_URL} reachable?` }));
|
|
357
|
-
req.on("timeout", () => { req.destroy(); resolve({ error: "timeout" }); });
|
|
358
|
-
if (body) req.write(body);
|
|
359
|
-
req.end();
|
|
360
|
-
});
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
function collectMetadata(target) {
|
|
364
|
-
const projectFiles = [];
|
|
365
|
-
const fileContents = {};
|
|
366
|
-
|
|
367
|
-
for (const d of PROBE_DIRS) {
|
|
368
|
-
try { if (fs.statSync(path.join(target, d)).isDirectory()) projectFiles.push(d + "/"); } catch {}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
for (const f of MANIFEST_FILES) {
|
|
372
|
-
const p = path.join(target, f);
|
|
373
|
-
try {
|
|
374
|
-
const stat = fs.statSync(p);
|
|
375
|
-
if (stat.isFile()) {
|
|
376
|
-
projectFiles.push(f);
|
|
377
|
-
const content = fs.readFileSync(p, "utf8");
|
|
378
|
-
if (content.length < 10000) fileContents[f] = content;
|
|
379
|
-
}
|
|
380
|
-
} catch {}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
const clis = [];
|
|
384
|
-
const { execSync } = require("child_process");
|
|
385
|
-
for (const cli of SUPPORTED_CLIS) {
|
|
386
|
-
try {
|
|
387
|
-
execSync(`command -v ${cli.bin}`, { stdio: "ignore", shell: "/bin/sh", env: process.env });
|
|
388
|
-
clis.push(cli.name);
|
|
389
|
-
} catch {}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
return { projectFiles, fileContents, clis };
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
// Fields in settings.json that belong to the user, not to 0dai
|
|
396
|
-
const SETTINGS_PRESERVE_FIELDS = ["model", "permissionMode", "effortLevel"];
|
|
397
|
-
|
|
398
|
-
function mergeSettingsJson(existing, incoming) {
|
|
399
|
-
try {
|
|
400
|
-
const base = JSON.parse(incoming);
|
|
401
|
-
const user = JSON.parse(existing);
|
|
402
|
-
// Preserve user-owned fields
|
|
403
|
-
for (const field of SETTINGS_PRESERVE_FIELDS) {
|
|
404
|
-
if (field in user && user[field] !== base[field]) {
|
|
405
|
-
base[field] = user[field];
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
return JSON.stringify(base, null, 2) + "\n";
|
|
409
|
-
} catch { return incoming; }
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
function writeFiles(target, files) {
|
|
413
|
-
let created = 0, updated = 0, unchanged = 0, merged = 0, skipped = 0;
|
|
414
|
-
const targetResolved = path.resolve(target);
|
|
415
|
-
for (const [rel, content] of Object.entries(files)) {
|
|
416
|
-
// HIGH: path traversal protection — reject absolute paths and `..` escapes
|
|
417
|
-
if (typeof rel !== "string" || !rel || path.isAbsolute(rel) || rel.split(/[/\\]/).includes("..")) {
|
|
418
|
-
skipped++;
|
|
419
|
-
continue;
|
|
420
|
-
}
|
|
421
|
-
const p = path.resolve(targetResolved, rel);
|
|
422
|
-
if (!p.startsWith(targetResolved + path.sep) && p !== targetResolved) {
|
|
423
|
-
skipped++;
|
|
424
|
-
continue;
|
|
425
|
-
}
|
|
426
|
-
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
427
|
-
|
|
428
|
-
let finalContent = content;
|
|
429
|
-
|
|
430
|
-
// Smart merge for specific files
|
|
431
|
-
if (fs.existsSync(p)) {
|
|
432
|
-
const existing = fs.readFileSync(p, "utf8");
|
|
433
|
-
if (existing === content) { unchanged++; continue; }
|
|
434
|
-
|
|
435
|
-
if (rel.endsWith("settings.json")) {
|
|
436
|
-
finalContent = mergeSettingsJson(existing, content);
|
|
437
|
-
merged++;
|
|
438
|
-
} else if (rel === "AGENTS.md") {
|
|
439
|
-
if (existing.includes("managed: false")) {
|
|
440
|
-
unchanged++; // User owns this file, skip
|
|
441
|
-
continue;
|
|
442
|
-
}
|
|
443
|
-
// Backup existing AGENTS.md before overwrite
|
|
444
|
-
const backupDir = path.join(target, "ai", ".backups");
|
|
445
|
-
fs.mkdirSync(backupDir, { recursive: true });
|
|
446
|
-
fs.writeFileSync(path.join(backupDir, "AGENTS.md.bak"), existing, "utf8");
|
|
447
|
-
updated++;
|
|
448
|
-
} else {
|
|
449
|
-
updated++;
|
|
450
|
-
}
|
|
451
|
-
} else {
|
|
452
|
-
created++;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
fs.writeFileSync(p, finalContent, "utf8");
|
|
456
|
-
}
|
|
457
|
-
const parts = [`${created} created`, `${updated} updated`, `${unchanged} unchanged`];
|
|
458
|
-
if (merged) parts.push(`${merged} merged`);
|
|
459
|
-
if (skipped) parts.push(`${skipped} skipped (unsafe path)`);
|
|
460
|
-
log(parts.join(", "));
|
|
461
|
-
return created + updated;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
async function cmdInit(target, args = []) {
|
|
465
|
-
const dryRun = args.includes("--dry-run");
|
|
466
|
-
const minimal = args.includes("--minimal");
|
|
467
|
-
|
|
468
|
-
if (fs.existsSync(path.join(target, "ai", "VERSION"))) {
|
|
469
|
-
const v = fs.readFileSync(path.join(target, "ai", "VERSION"), "utf8").trim();
|
|
470
|
-
log(`ai/ layer already exists (v${v}). Run '0dai sync' to update.`);
|
|
471
|
-
return;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
const isTTY = process.stdout.isTTY;
|
|
475
|
-
let spinner = null;
|
|
476
|
-
if (isTTY) {
|
|
477
|
-
try { spinner = require("@clack/prompts").spinner(); } catch {}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
const { projectFiles, fileContents, clis } = collectMetadata(target);
|
|
481
|
-
if (dryRun) log(`${D}dry-run: would generate ai/ layer (${projectFiles.length} files, ${clis.length} CLIs)${R}`);
|
|
482
|
-
if (spinner) spinner.start(`${dryRun ? "[dry-run] " : ""}Generating ai/ layer (${projectFiles.length} files, ${clis.length} CLIs)...`);
|
|
483
|
-
else if (!dryRun) log(`sending to API (${projectFiles.length} files, ${clis.length} CLIs)...`);
|
|
484
|
-
const result = await apiCall("/v1/init", {
|
|
485
|
-
project_files: projectFiles,
|
|
486
|
-
file_contents: fileContents,
|
|
487
|
-
available_clis: clis,
|
|
488
|
-
dry_run: dryRun,
|
|
489
|
-
minimal: minimal,
|
|
490
|
-
});
|
|
491
|
-
|
|
492
|
-
if (result.error) {
|
|
493
|
-
if (result.error.includes("authentication")) {
|
|
494
|
-
log(`authentication required`);
|
|
495
|
-
console.log(` 0dai auth is separate from your agent CLIs (Claude Code, Codex).`);
|
|
496
|
-
console.log(` It tracks projects, usage limits, and team features.\n`);
|
|
497
|
-
console.log(` ${D}Run: 0dai auth login${R}`);
|
|
498
|
-
} else if (result.hint) {
|
|
499
|
-
log(`${result.message || result.error}`);
|
|
500
|
-
console.log(` ${result.hint}\n`);
|
|
501
|
-
} else {
|
|
502
|
-
log(`error: ${result.error}`);
|
|
503
|
-
}
|
|
504
|
-
process.exit(1);
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
if (spinner) spinner.stop(`${dryRun ? "[dry-run] " : ""}Detected: ${result.stack || "?"}`);
|
|
508
|
-
else log(`detected: ${result.stack || "?"}`);
|
|
509
|
-
if (dryRun) {
|
|
510
|
-
const files = Object.keys(result.files || {});
|
|
511
|
-
log(`${D}dry-run: would write ${files.length} files:${R}`);
|
|
512
|
-
for (const f of files.slice(0, 20)) console.log(` ${D}+ ${f}${R}`);
|
|
513
|
-
if (files.length > 20) console.log(` ${D}… and ${files.length - 20} more${R}`);
|
|
514
|
-
return;
|
|
515
|
-
}
|
|
516
|
-
writeFiles(target, result.files || {});
|
|
517
|
-
|
|
518
|
-
// Ensure ai/VERSION matches CLI version
|
|
519
|
-
const versionFile = path.join(target, "ai", "VERSION");
|
|
520
|
-
fs.mkdirSync(path.dirname(versionFile), { recursive: true });
|
|
521
|
-
fs.writeFileSync(versionFile, VERSION + "\n", "utf8");
|
|
522
|
-
|
|
523
|
-
// Add to .gitignore
|
|
524
|
-
const gi = path.join(target, ".gitignore");
|
|
525
|
-
try {
|
|
526
|
-
const text = fs.existsSync(gi) ? fs.readFileSync(gi, "utf8") : "";
|
|
527
|
-
if (!text.includes(".0dai")) fs.appendFileSync(gi, "\n.0dai/\n");
|
|
528
|
-
} catch {}
|
|
529
|
-
|
|
530
|
-
// Register in global portfolio
|
|
531
|
-
registerProject(target, path.basename(target), result.stack);
|
|
532
|
-
|
|
533
|
-
log(`initialized (${result.file_count || "?"} files)`);
|
|
534
|
-
console.log(" skills: /build /review /status /feedback /bugfix /delegate");
|
|
535
|
-
|
|
536
|
-
// Detect agent auth status for smart onboarding hints
|
|
537
|
-
const { execFileSync: _ef } = require("child_process");
|
|
538
|
-
const agents = [];
|
|
539
|
-
try { _ef("claude", ["--version"], { timeout: 8000 }); agents.push("claude"); } catch {}
|
|
540
|
-
try { _ef("codex", ["--version"], { timeout: 8000 }); agents.push("codex"); } catch {}
|
|
541
|
-
try { _ef("gemini", ["--version"], { timeout: 8000 }); agents.push("gemini"); } catch {}
|
|
542
|
-
|
|
543
|
-
// Next steps — guide user to first value
|
|
544
|
-
console.log(`\n ${T}Next steps:${R}`);
|
|
545
|
-
console.log(` ${D}1.${R} Check health: ${D}0dai doctor${R}`);
|
|
546
|
-
if (agents.length > 0) {
|
|
547
|
-
const a = agents[0];
|
|
548
|
-
console.log(` ${D}2.${R} Try delegation: ${D}0dai run "write tests for auth"${R}`);
|
|
549
|
-
console.log(` ${D}(${agents.join(", ")} detected — delegation will use ${a} by default)${R}`);
|
|
550
|
-
} else {
|
|
551
|
-
console.log(` ${D}2.${R} Install an agent CLI to enable delegation:`);
|
|
552
|
-
console.log(` ${D}claude:${R} npm i -g @anthropic-ai/claude-code ${D}(or Pro subscription)${R}`);
|
|
553
|
-
console.log(` ${D}codex:${R} npm i -g @openai/codex ${D}(or ChatGPT Pro)${R}`);
|
|
554
|
-
}
|
|
555
|
-
console.log(` ${D}3.${R} Open dashboard: ${D}https://0dai.dev/dashboard${R}`);
|
|
556
|
-
|
|
557
|
-
// Send anonymous usage ping
|
|
558
|
-
apiCall("/v1/feedback", { report: {
|
|
559
|
-
stack_detected: result.stack || "?", _auto: true, _plan: result.plan || "trial",
|
|
560
|
-
_cli_version: VERSION, _files_generated: result.file_count || 0,
|
|
561
|
-
}}).catch(() => {});
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
async function cmdSync(target, args = []) {
|
|
565
|
-
const dryRun = args.includes("--dry-run");
|
|
566
|
-
const quiet = args.includes("--quiet") || args.includes("-q");
|
|
567
|
-
|
|
568
|
-
// Quick local check: skip API if already at current version (unless dry-run)
|
|
569
|
-
let version = "unknown";
|
|
570
|
-
try { version = fs.readFileSync(path.join(target, "ai", "VERSION"), "utf8").trim(); } catch {}
|
|
571
|
-
if (!dryRun && version === VERSION) {
|
|
572
|
-
log("already up to date (v" + version + ")");
|
|
573
|
-
return;
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
const { projectFiles, fileContents, clis } = collectMetadata(target);
|
|
577
|
-
let stack = "generic", agents = [];
|
|
578
|
-
try {
|
|
579
|
-
const d = JSON.parse(fs.readFileSync(path.join(target, "ai", "manifest", "discovery.json"), "utf8"));
|
|
580
|
-
stack = d.stack || "generic";
|
|
581
|
-
agents = d.selected_agents || [];
|
|
582
|
-
} catch {}
|
|
583
|
-
|
|
584
|
-
// Collect current ai/ files
|
|
585
|
-
const currentFiles = {};
|
|
586
|
-
const aiDir = path.join(target, "ai");
|
|
587
|
-
if (fs.existsSync(aiDir)) {
|
|
588
|
-
const walk = (dir) => {
|
|
589
|
-
for (const f of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
590
|
-
const p = path.join(dir, f.name);
|
|
591
|
-
if (f.isDirectory()) walk(p);
|
|
592
|
-
else {
|
|
593
|
-
try {
|
|
594
|
-
const stat = fs.statSync(p);
|
|
595
|
-
if (stat.size < 10000) currentFiles[path.relative(target, p)] = fs.readFileSync(p, "utf8");
|
|
596
|
-
} catch {}
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
};
|
|
600
|
-
walk(aiDir);
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
if (dryRun) log(`${D}dry-run: checking what sync would change...${R}`);
|
|
604
|
-
|
|
605
|
-
const result = await apiCall("/v1/sync", {
|
|
606
|
-
ai_version: version, stack, agents: agents.length ? agents : clis,
|
|
607
|
-
current_files: currentFiles, file_contents: fileContents,
|
|
608
|
-
dry_run: dryRun, quiet,
|
|
609
|
-
});
|
|
610
|
-
|
|
611
|
-
if (result.error) {
|
|
612
|
-
if (result.error.includes("authentication")) {
|
|
613
|
-
log(`authentication required`);
|
|
614
|
-
console.log(` 0dai auth is separate from your agent CLIs (Claude Code, Codex).`);
|
|
615
|
-
console.log(` It tracks projects, usage limits, and team features.\n`);
|
|
616
|
-
console.log(` ${D}Run: 0dai auth login${R}`);
|
|
617
|
-
} else {
|
|
618
|
-
log(`error: ${result.error}`);
|
|
619
|
-
}
|
|
620
|
-
process.exit(1);
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
const updated = result.files_updated || {};
|
|
624
|
-
if (dryRun) {
|
|
625
|
-
const files = Object.keys(updated);
|
|
626
|
-
if (files.length) {
|
|
627
|
-
log(`${D}dry-run: would update ${files.length} file(s):${R}`);
|
|
628
|
-
for (const f of files) console.log(` ${D}~ ${f}${R}`);
|
|
629
|
-
} else {
|
|
630
|
-
log(`${D}dry-run: nothing to update${R}`);
|
|
631
|
-
}
|
|
632
|
-
return;
|
|
633
|
-
}
|
|
634
|
-
const changedCount = Object.keys(updated).length;
|
|
635
|
-
if (changedCount) {
|
|
636
|
-
writeFiles(target, updated);
|
|
637
|
-
if (!quiet) {
|
|
638
|
-
for (const f of Object.keys(updated)) console.log(` ~ ${f}`);
|
|
639
|
-
}
|
|
640
|
-
log(`sync: ${changedCount} file(s) updated`);
|
|
641
|
-
console.log(` ${D}Run: 0dai doctor to verify project health${R}`);
|
|
642
|
-
} else {
|
|
643
|
-
log("already up to date");
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
// Ensure ai/VERSION matches CLI version after successful sync
|
|
647
|
-
const versionFile = path.join(target, "ai", "VERSION");
|
|
648
|
-
try {
|
|
649
|
-
const current = fs.existsSync(versionFile) ? fs.readFileSync(versionFile, "utf8").trim() : "";
|
|
650
|
-
if (current !== VERSION) {
|
|
651
|
-
fs.writeFileSync(versionFile, VERSION + "\n", "utf8");
|
|
652
|
-
}
|
|
653
|
-
} catch {}
|
|
654
|
-
|
|
655
|
-
// Update portfolio registry
|
|
656
|
-
registerProject(target, path.basename(target), stack);
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
async function cmdDetect(target) {
|
|
660
|
-
const OPTIONAL_CLIS = ["gemini", "aider", "opencode"];
|
|
661
|
-
const { projectFiles, fileContents, clis: localClis } = collectMetadata(target);
|
|
662
|
-
// Send file contents AND local CLI inventory so server can do content-based detection
|
|
663
|
-
const result = await apiCall("/v1/detect", {
|
|
664
|
-
project_files: projectFiles,
|
|
665
|
-
file_contents: fileContents,
|
|
666
|
-
available_clis: localClis,
|
|
667
|
-
});
|
|
668
|
-
if (result.error) { log(`error: ${result.error}`); return; }
|
|
669
|
-
console.log(`stack: ${result.stack || "?"}`);
|
|
670
|
-
// Use local CLIs if server didn't return any (server can't detect locally installed binaries)
|
|
671
|
-
const clis = (result.available_clis && result.available_clis.length && result.available_clis[0]) ? result.available_clis : localClis;
|
|
672
|
-
if (clis.length) {
|
|
673
|
-
console.log(`clis: ${clis.join(", ")}`);
|
|
674
|
-
} else {
|
|
675
|
-
console.log(`clis: none detected`);
|
|
676
|
-
console.log(` ${D}install claude, codex, or opencode to use 0dai${R}`);
|
|
677
|
-
}
|
|
678
|
-
// Explain optional CLIs so missing doesn't alarm users
|
|
679
|
-
const missing = OPTIONAL_CLIS.filter(c => !clis.includes(c));
|
|
680
|
-
if (missing.length && clis.length) {
|
|
681
|
-
console.log(` ${D}optional (not installed): ${missing.join(", ")}${R}`);
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
function cmdAudit(target) {
|
|
686
|
-
const W = process.stdout.isTTY ? "\x1b[33m" : ""; // yellow
|
|
687
|
-
const RE = process.stdout.isTTY ? "\x1b[31m" : ""; // red
|
|
688
|
-
const G = process.stdout.isTTY ? "\x1b[32m" : ""; // green
|
|
689
|
-
|
|
690
|
-
// Secret patterns: [label, regex, severity]
|
|
691
|
-
const PATTERNS = [
|
|
692
|
-
["Anthropic API key", /sk-ant-api[0-9A-Za-z_-]{20,}/g, "critical"],
|
|
693
|
-
["OpenAI API key", /sk-[A-Za-z0-9]{20,}/g, "critical"],
|
|
694
|
-
["GitHub PAT (ghp)", /ghp_[A-Za-z0-9]{36}/g, "critical"],
|
|
695
|
-
["GitHub PAT (gho)", /gho_[A-Za-z0-9]{36}/g, "critical"],
|
|
696
|
-
["GitHub fine-grained",/github_pat_[A-Za-z0-9_]{59}/g, "critical"],
|
|
697
|
-
["AWS access key", /AKIA[0-9A-Z]{16}/g, "critical"],
|
|
698
|
-
["AWS secret key", /aws_secret_access_key\s*[=:]\s*\S{20,}/gi,"critical"],
|
|
699
|
-
["Google API key", /AIza[0-9A-Za-z_-]{35}/g, "high"],
|
|
700
|
-
["Bearer token", /Bearer\s+[A-Za-z0-9_-]{32,}/g, "high"],
|
|
701
|
-
["Private key block", /-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY/g, "critical"],
|
|
702
|
-
["Generic secret var", /(?:secret|password|passwd|pwd)\s*[=:]\s*["']?[A-Za-z0-9+/=_-]{12,}["']?/gi, "medium"],
|
|
703
|
-
];
|
|
704
|
-
|
|
705
|
-
// Files to scan
|
|
706
|
-
const SCAN_FILES = [
|
|
707
|
-
"CLAUDE.md", "AGENTS.md", "GEMINI.md", ".cursorrules",
|
|
708
|
-
".codex/config.md", ".codex/instructions.md",
|
|
709
|
-
"opencode.json", ".mcp.json",
|
|
710
|
-
];
|
|
711
|
-
|
|
712
|
-
// Walk a directory recursively, return all file paths
|
|
713
|
-
function walk(dir, maxDepth = 6, _depth = 0) {
|
|
714
|
-
if (_depth > maxDepth) return [];
|
|
715
|
-
let results = [];
|
|
716
|
-
try {
|
|
717
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
718
|
-
if (entry.name.startsWith(".git") || entry.name === "node_modules") continue;
|
|
719
|
-
const full = path.join(dir, entry.name);
|
|
720
|
-
if (entry.isDirectory()) results = results.concat(walk(full, maxDepth, _depth + 1));
|
|
721
|
-
else results.push(full);
|
|
722
|
-
}
|
|
723
|
-
} catch { /* skip unreadable */ }
|
|
724
|
-
return results;
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
const findings = []; // {file, line, label, severity, excerpt}
|
|
728
|
-
|
|
729
|
-
function scanContent(filePath, content) {
|
|
730
|
-
const lines = content.split("\n");
|
|
731
|
-
for (const [label, regex, severity] of PATTERNS) {
|
|
732
|
-
regex.lastIndex = 0;
|
|
733
|
-
for (let i = 0; i < lines.length; i++) {
|
|
734
|
-
let m;
|
|
735
|
-
regex.lastIndex = 0;
|
|
736
|
-
while ((m = regex.exec(lines[i])) !== null) {
|
|
737
|
-
const val = m[0];
|
|
738
|
-
// Redact: show first 6 + ... + last 4 chars
|
|
739
|
-
const excerpt = val.length > 14 ? val.slice(0, 6) + "..." + val.slice(-4) : val.slice(0, 4) + "...";
|
|
740
|
-
findings.push({ file: filePath, line: i + 1, label, severity, excerpt });
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
// Collect files to scan
|
|
747
|
-
const toScan = new Set();
|
|
748
|
-
|
|
749
|
-
for (const rel of SCAN_FILES) {
|
|
750
|
-
const p = path.join(target, rel);
|
|
751
|
-
if (fs.existsSync(p)) toScan.add(p);
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
const aiDir = path.join(target, "ai");
|
|
755
|
-
if (fs.existsSync(aiDir)) {
|
|
756
|
-
for (const f of walk(aiDir)) {
|
|
757
|
-
if (/\.(md|json|yaml|yml|txt|toml)$/.test(f)) toScan.add(f);
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
// Warn about .env files (don't scan content, just flag existence)
|
|
762
|
-
const envFiles = [".env", ".env.local", ".env.production", ".env.development"];
|
|
763
|
-
const foundEnv = envFiles.filter(e => fs.existsSync(path.join(target, e)));
|
|
764
|
-
|
|
765
|
-
console.log(`\n ${T}0dai audit${R} — scanning for leaked secrets\n`);
|
|
766
|
-
console.log(` ${D}target: ${target}${R}`);
|
|
767
|
-
console.log(` ${D}files: ${toScan.size} scanned${R}\n`);
|
|
768
|
-
|
|
769
|
-
for (const filePath of toScan) {
|
|
770
|
-
try {
|
|
771
|
-
const content = fs.readFileSync(filePath, "utf8");
|
|
772
|
-
scanContent(filePath, content);
|
|
773
|
-
} catch { /* skip */ }
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
if (foundEnv.length > 0) {
|
|
777
|
-
console.log(` ${W}WARN${R} .env files detected — ensure they are in .gitignore`);
|
|
778
|
-
for (const e of foundEnv) console.log(` ${D}${e}${R}`);
|
|
779
|
-
console.log();
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
if (findings.length === 0) {
|
|
783
|
-
console.log(` ${G}✓ No secrets found${R} in scanned files\n`);
|
|
784
|
-
return;
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
const critical = findings.filter(f => f.severity === "critical");
|
|
788
|
-
const high = findings.filter(f => f.severity === "high");
|
|
789
|
-
const medium = findings.filter(f => f.severity === "medium");
|
|
790
|
-
|
|
791
|
-
const colorFor = (s) => s === "critical" ? RE : s === "high" ? W : D;
|
|
792
|
-
|
|
793
|
-
for (const f of findings) {
|
|
794
|
-
const c = colorFor(f.severity);
|
|
795
|
-
const rel = path.relative(target, f.file);
|
|
796
|
-
console.log(` ${c}${f.severity.toUpperCase().padEnd(8)}${R} ${rel}:${f.line}`);
|
|
797
|
-
console.log(` ${D}${f.label}: ${f.excerpt}${R}`);
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
console.log();
|
|
801
|
-
if (critical.length > 0) console.log(` ${RE}${critical.length} critical${R} · ${W}${high.length} high${R} · ${D}${medium.length} medium${R}\n`);
|
|
802
|
-
else console.log(` ${W}${high.length} high${R} · ${D}${medium.length} medium${R}\n`);
|
|
803
|
-
|
|
804
|
-
console.log(` ${D}Tip: add secrets to .gitignore or use env vars, not plaintext files${R}\n`);
|
|
805
|
-
|
|
806
|
-
if (critical.length > 0) process.exit(1);
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
function cmdDoctor(target) {
|
|
810
|
-
const ai = path.join(target, "ai");
|
|
811
|
-
if (!fs.existsSync(ai)) { log("no ai/ layer. Run '0dai init' first."); return; }
|
|
812
|
-
let v = "?", stack = "generic";
|
|
813
|
-
try { v = fs.readFileSync(path.join(ai, "VERSION"), "utf8").trim(); } catch {}
|
|
814
|
-
try { stack = JSON.parse(fs.readFileSync(path.join(ai, "manifest", "discovery.json"), "utf8")).stack || "generic"; } catch {}
|
|
815
|
-
|
|
816
|
-
const W = process.stdout.isTTY ? "\x1b[33m" : ""; // yellow
|
|
817
|
-
const E = process.stdout.isTTY ? "\x1b[31m" : ""; // red
|
|
818
|
-
const G = process.stdout.isTTY ? "\x1b[32m" : ""; // green
|
|
819
|
-
const R2 = process.stdout.isTTY ? "\x1b[0m" : "";
|
|
820
|
-
|
|
821
|
-
// --- ai/ layer checks ---
|
|
822
|
-
const layerChecks = {
|
|
823
|
-
"ai/VERSION": { path: path.join(ai, "VERSION"), sev: "error" },
|
|
824
|
-
"ai/manifest/project.yaml": { path: path.join(ai, "manifest", "project.yaml"), sev: "error" },
|
|
825
|
-
"ai/manifest/commands.yaml": { path: path.join(ai, "manifest", "commands.yaml"), sev: "warn" },
|
|
826
|
-
"ai/manifest/discovery.json": { path: path.join(ai, "manifest", "discovery.json"),sev: "warn" },
|
|
827
|
-
".claude/settings.json": { path: path.join(target, ".claude", "settings.json"), sev: "warn" },
|
|
828
|
-
"AGENTS.md": { path: path.join(target, "AGENTS.md"), sev: "warn" },
|
|
829
|
-
};
|
|
830
|
-
|
|
831
|
-
// --- credentials checklist ---
|
|
832
|
-
// Detect subscription-based auth (not just env API keys)
|
|
833
|
-
const { execFileSync: _execFile } = require("child_process");
|
|
834
|
-
function cliAuthed(cli) {
|
|
835
|
-
try {
|
|
836
|
-
if (cli === "claude") {
|
|
837
|
-
const out = _execFile("claude", ["auth", "status"], { timeout: 5000 }).toString();
|
|
838
|
-
try { return JSON.parse(out).loggedIn === true; } catch {}
|
|
839
|
-
return out.includes("loggedIn");
|
|
840
|
-
}
|
|
841
|
-
_execFile("which", [cli], { timeout: 2000 });
|
|
842
|
-
return true;
|
|
843
|
-
} catch { return false; }
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
const claudeAuth = cliAuthed("claude");
|
|
847
|
-
const codexAuth = cliAuthed("codex");
|
|
848
|
-
|
|
849
|
-
const credChecks = [
|
|
850
|
-
{
|
|
851
|
-
name: "Claude Code",
|
|
852
|
-
present: claudeAuth || !!process.env.ANTHROPIC_API_KEY,
|
|
853
|
-
sev: (claudeAuth || process.env.ANTHROPIC_API_KEY) ? "ok" : "warn",
|
|
854
|
-
hint: claudeAuth ? "authenticated via subscription" : "run: claude auth login (or set ANTHROPIC_API_KEY)",
|
|
855
|
-
},
|
|
856
|
-
{
|
|
857
|
-
name: "Codex CLI",
|
|
858
|
-
present: codexAuth || !!process.env.OPENAI_API_KEY,
|
|
859
|
-
sev: (codexAuth || process.env.OPENAI_API_KEY) ? "ok" : "warn",
|
|
860
|
-
hint: codexAuth ? "installed (uses ChatGPT subscription)" : "run: npm i -g @openai/codex (or set OPENAI_API_KEY)",
|
|
861
|
-
},
|
|
862
|
-
{
|
|
863
|
-
name: "GITHUB_TOKEN",
|
|
864
|
-
present: !!process.env.GITHUB_TOKEN,
|
|
865
|
-
sev: process.env.GITHUB_TOKEN ? "ok" : "info",
|
|
866
|
-
hint: "Optional — for gh CLI, PR creation",
|
|
867
|
-
},
|
|
868
|
-
];
|
|
869
|
-
|
|
870
|
-
// Stack-specific creds
|
|
871
|
-
if (stack.includes("vercel") || stack.includes("next")) {
|
|
872
|
-
credChecks.push({ name: "VERCEL_TOKEN", present: !!process.env.VERCEL_TOKEN, sev: process.env.VERCEL_TOKEN ? "ok" : "info", hint: "Optional — for Vercel deployments" });
|
|
873
|
-
}
|
|
874
|
-
if (stack.includes("aws") || stack.includes("lambda") || stack.includes("cdk")) {
|
|
875
|
-
credChecks.push({ name: "AWS_ACCESS_KEY_ID", present: !!process.env.AWS_ACCESS_KEY_ID, sev: process.env.AWS_ACCESS_KEY_ID ? "ok" : "info", hint: "Optional — for AWS deployments" });
|
|
876
|
-
}
|
|
877
|
-
if (stack.includes("gcp") || stack.includes("firebase") || stack.includes("flutter")) {
|
|
878
|
-
credChecks.push({ name: "GCP_CREDENTIALS", present: !!process.env.GOOGLE_APPLICATION_CREDENTIALS, sev: process.env.GOOGLE_APPLICATION_CREDENTIALS ? "ok" : "info", hint: "Optional — for GCP/Firebase" });
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
// --- run checks ---
|
|
882
|
-
let errors = 0, warnings = 0;
|
|
883
|
-
log(`v${v} | stack: ${stack}\n`);
|
|
884
|
-
|
|
885
|
-
const missingConfigs = [];
|
|
886
|
-
console.log(" ai/ layer:");
|
|
887
|
-
for (const [name, { path: p, sev }] of Object.entries(layerChecks)) {
|
|
888
|
-
const exists = fs.existsSync(p);
|
|
889
|
-
if (!exists) {
|
|
890
|
-
sev === "error" ? errors++ : warnings++;
|
|
891
|
-
if (sev === "warn") missingConfigs.push(name);
|
|
892
|
-
}
|
|
893
|
-
const mark = exists ? `${G}ok${R2}` : sev === "error" ? `${E}MISSING${R2}` : `${W}missing${R2}`;
|
|
894
|
-
console.log(` ${mark.padEnd(22)} ${name}`);
|
|
895
|
-
}
|
|
896
|
-
// Explain WHY native configs are missing and what to do
|
|
897
|
-
if (missingConfigs.length > 0) {
|
|
898
|
-
const hasDiscovery = fs.existsSync(path.join(ai, "manifest", "discovery.json"));
|
|
899
|
-
if (hasDiscovery) {
|
|
900
|
-
console.log(`\n ${W}→ Native configs not generated yet.${R2}`);
|
|
901
|
-
console.log(` ${D}Run: 0dai sync --target .${R2}`);
|
|
902
|
-
} else {
|
|
903
|
-
console.log(`\n ${W}→ ai/ layer incomplete — run '0dai init' first.${R2}`);
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
console.log("\n credentials:");
|
|
908
|
-
for (const c of credChecks) {
|
|
909
|
-
if (!c.present && c.sev === "warn") warnings++;
|
|
910
|
-
const mark = c.present ? `${G}ok${R2}` : c.sev === "warn" ? `${W}not set${R2}` : `${D}not set${R2}`;
|
|
911
|
-
const hint = c.present && c.hint.includes("subscription") ? ` ${D}(${c.hint})${R2}` : (!c.present ? `\n ${D}→ ${c.hint}${R2}` : "");
|
|
912
|
-
console.log(` ${mark.padEnd(22)} ${c.name}${hint}`);
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
// --- agent CLIs check ---
|
|
916
|
-
const { execFileSync: _ef2 } = require("child_process");
|
|
917
|
-
let updatesAvailable = 0;
|
|
918
|
-
console.log("\n agent CLIs:");
|
|
919
|
-
for (const cli of SUPPORTED_CLIS) {
|
|
920
|
-
let installed = false, ver = null;
|
|
921
|
-
try {
|
|
922
|
-
const out = _ef2(cli.bin, ["--version"], { timeout: 8000 }).toString().trim();
|
|
923
|
-
installed = true;
|
|
924
|
-
const m = out.match(/(\d+\.\d+\.\d+)/);
|
|
925
|
-
if (m) ver = m[1];
|
|
926
|
-
} catch {}
|
|
927
|
-
|
|
928
|
-
if (installed) {
|
|
929
|
-
let latest = null;
|
|
930
|
-
if (cli.pkg) {
|
|
931
|
-
try {
|
|
932
|
-
const npmOut = _ef2("npm", ["view", cli.pkg, "version"], { timeout: 5000 }).toString().trim();
|
|
933
|
-
if (npmOut.match(/^\d+\.\d+\.\d+$/)) latest = npmOut;
|
|
934
|
-
} catch {}
|
|
935
|
-
}
|
|
936
|
-
if (latest && ver && latest !== ver) {
|
|
937
|
-
updatesAvailable++;
|
|
938
|
-
console.log(` ${W}update${R2} ${cli.name} ${D}${ver} → ${latest}${R2}`);
|
|
939
|
-
console.log(` ${D}→ ${cli.install}${R2}`);
|
|
940
|
-
} else {
|
|
941
|
-
console.log(` ${G}ok${R2} ${cli.name}${ver ? ` ${D}v${ver}${R2}` : ""}`);
|
|
942
|
-
}
|
|
943
|
-
} else {
|
|
944
|
-
console.log(` ${D}—${R2} ${cli.name} ${D}not installed${R2}`);
|
|
945
|
-
console.log(` ${D}→ ${cli.install}${cli.altAuth ? ` (or ${cli.altAuth})` : ""}${R2}`);
|
|
946
|
-
}
|
|
947
|
-
}
|
|
948
|
-
if (updatesAvailable) {
|
|
949
|
-
console.log(`\n ${D}Run: 0dai update${R2} to update all`);
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
// --- swarm check ---
|
|
953
|
-
const swarmDir = path.join(ai, "swarm");
|
|
954
|
-
const countDir = (d) => { try { return fs.readdirSync(d).filter(f => f.endsWith(".json")).length; } catch { return 0; } };
|
|
955
|
-
const qCount = countDir(path.join(swarmDir, "queue"));
|
|
956
|
-
const dCount = countDir(path.join(swarmDir, "done"));
|
|
957
|
-
if (qCount || dCount) {
|
|
958
|
-
console.log(`\n swarm: ${qCount} queued, ${dCount} done`);
|
|
959
|
-
if (qCount) console.log(` ${W}→ run '0dai reflect' to review pending tasks${R2}`);
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
const summary = errors ? `${E}${errors} error(s)${R2}` : warnings ? `${W}${warnings} warning(s)${R2}` : `${G}healthy${R2}`;
|
|
963
|
-
console.log(`\n status: ${summary}`);
|
|
964
|
-
if (errors) process.exitCode = 1;
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
function cmdValidate(target) {
|
|
968
|
-
const ai = path.join(target, "ai");
|
|
969
|
-
if (!fs.existsSync(ai)) {
|
|
970
|
-
log("no ai/ layer. Run '0dai init' first.");
|
|
971
|
-
process.exitCode = 1;
|
|
972
|
-
return;
|
|
973
|
-
}
|
|
974
|
-
const E = process.stdout.isTTY ? "\x1b[31m" : "";
|
|
975
|
-
const G = process.stdout.isTTY ? "\x1b[32m" : "";
|
|
976
|
-
const D2 = process.stdout.isTTY ? "\x1b[2m" : "";
|
|
977
|
-
const R2 = process.stdout.isTTY ? "\x1b[0m" : "";
|
|
978
|
-
|
|
979
|
-
const required = [
|
|
980
|
-
"ai/VERSION", "ai/VERSION_SCHEMA",
|
|
981
|
-
"ai/manifest/project.yaml", "ai/manifest/discovery.json",
|
|
982
|
-
"ai/manifest/applied-lock.json", "ai/manifest/environment.yaml",
|
|
983
|
-
"ai/manifest/commands.yaml",
|
|
984
|
-
];
|
|
985
|
-
|
|
986
|
-
let agents = [];
|
|
987
|
-
try {
|
|
988
|
-
agents = JSON.parse(fs.readFileSync(path.join(ai, "manifest", "discovery.json"), "utf8")).selected_agents || [];
|
|
989
|
-
} catch {}
|
|
990
|
-
|
|
991
|
-
const agentFiles = Object.fromEntries(
|
|
992
|
-
SUPPORTED_CLIS
|
|
993
|
-
.filter((c) => c.agentFiles && c.agentFiles.length > 0)
|
|
994
|
-
.map((c) => [c.name, c.agentFiles])
|
|
995
|
-
);
|
|
996
|
-
for (const agent of agents) {
|
|
997
|
-
for (const f of agentFiles[agent] || []) required.push(f);
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
const FIX_HINTS = {
|
|
1001
|
-
"ai/VERSION": "run: 0dai init",
|
|
1002
|
-
"ai/VERSION_SCHEMA": "run: 0dai sync",
|
|
1003
|
-
"ai/manifest/project.yaml": "run: 0dai init",
|
|
1004
|
-
"ai/manifest/discovery.json": "run: 0dai init",
|
|
1005
|
-
"ai/manifest/applied-lock.json": "run: 0dai sync",
|
|
1006
|
-
"ai/manifest/environment.yaml": "run: 0dai sync",
|
|
1007
|
-
"ai/manifest/commands.yaml": "run: 0dai sync",
|
|
1008
|
-
"AGENTS.md": "run: 0dai sync",
|
|
1009
|
-
".claude/settings.json": "run: 0dai sync",
|
|
1010
|
-
".claude/CLAUDE.md": "run: 0dai sync",
|
|
1011
|
-
".mcp.json": "run: 0dai sync",
|
|
1012
|
-
".codex/config.toml": "install codex, then: 0dai sync",
|
|
1013
|
-
"opencode.json": "install opencode, then: 0dai sync",
|
|
1014
|
-
};
|
|
1015
|
-
|
|
1016
|
-
const present = required.filter(f => fs.existsSync(path.join(target, f)));
|
|
1017
|
-
const missing = required.filter(f => !fs.existsSync(path.join(target, f)));
|
|
1018
|
-
|
|
1019
|
-
for (const f of present) console.log(` ${G}✓${R2} ${f}`);
|
|
1020
|
-
for (const f of missing) {
|
|
1021
|
-
const hint = FIX_HINTS[f] || "run: 0dai sync";
|
|
1022
|
-
console.log(` ${E}✗${R2} ${f} ${D2}— ${hint}${R2}`);
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
if (missing.length) {
|
|
1026
|
-
console.log(`\n${E}${missing.length} missing${R2} / ${present.length + missing.length} total`);
|
|
1027
|
-
process.exitCode = 1;
|
|
1028
|
-
} else {
|
|
1029
|
-
log(`${G}validate ok${R2} — all ${present.length} required files present`);
|
|
1030
|
-
}
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
// --- Update agent CLIs ---
|
|
1034
|
-
function cmdUpdate(args) {
|
|
1035
|
-
const { execFileSync: _ef3, execSync } = require("child_process");
|
|
1036
|
-
const dryRun = args.includes("--dry-run");
|
|
1037
|
-
|
|
1038
|
-
// 0dai self-update entry, then all supported agent CLIs.
|
|
1039
|
-
const CLIS = [
|
|
1040
|
-
{ name: "0dai", pkg: "@0dai-dev/cli", bin: "0dai", pkgType: "npm" },
|
|
1041
|
-
...SUPPORTED_CLIS,
|
|
1042
|
-
];
|
|
1043
|
-
|
|
1044
|
-
let updated = 0;
|
|
1045
|
-
for (const cli of CLIS) {
|
|
1046
|
-
let installed = false, ver = null;
|
|
1047
|
-
try {
|
|
1048
|
-
const out = _ef3(cli.bin, ["--version"], { timeout: 8000 }).toString().trim();
|
|
1049
|
-
installed = true;
|
|
1050
|
-
const m = out.match(/(\d+\.\d+\.\d+)/);
|
|
1051
|
-
if (m) ver = m[1];
|
|
1052
|
-
} catch {}
|
|
1053
|
-
|
|
1054
|
-
if (!installed) continue;
|
|
1055
|
-
|
|
1056
|
-
let latest = null;
|
|
1057
|
-
if (cli.pkgType === "npm" && cli.pkg) {
|
|
1058
|
-
try { latest = _ef3("npm", ["view", cli.pkg, "version"], { timeout: 5000 }).toString().trim(); } catch {}
|
|
1059
|
-
} else if (cli.pkgType === "pip" && cli.pkg) {
|
|
1060
|
-
try {
|
|
1061
|
-
const out = _ef3("pip", ["index", "versions", cli.pkg], { timeout: 8000, encoding: "utf8" });
|
|
1062
|
-
const m = out.match(/LATEST:\s*(\d+\.\d+\.\d+)/i) || out.match(/(\d+\.\d+\.\d+)/);
|
|
1063
|
-
if (m) latest = m[1];
|
|
1064
|
-
} catch {}
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
if (!latest || latest === ver) {
|
|
1068
|
-
console.log(` ${cli.name} ${ver || ""} — up to date`);
|
|
1069
|
-
continue;
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
console.log(` ${cli.name} ${ver} → ${latest}`);
|
|
1073
|
-
if (dryRun) { updated++; continue; }
|
|
1074
|
-
|
|
1075
|
-
try {
|
|
1076
|
-
if (cli.pkgType === "npm") {
|
|
1077
|
-
log(`updating ${cli.name}...`);
|
|
1078
|
-
execSync(`npm install -g ${cli.pkg}@latest`, { timeout: 60000, stdio: "pipe" });
|
|
1079
|
-
log(`${cli.name} updated to ${latest}`);
|
|
1080
|
-
updated++;
|
|
1081
|
-
} else if (cli.pkgType === "pip") {
|
|
1082
|
-
log(`updating ${cli.name}...`);
|
|
1083
|
-
execSync(`pip install --upgrade ${cli.pkg}`, { timeout: 60000, stdio: "pipe" });
|
|
1084
|
-
log(`${cli.name} updated to ${latest}`);
|
|
1085
|
-
updated++;
|
|
1086
|
-
}
|
|
1087
|
-
} catch (e) {
|
|
1088
|
-
log(`failed to update ${cli.name}: ${e.message.split("\n")[0]}`);
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1091
|
-
if (updated) {
|
|
1092
|
-
log(`${dryRun ? "would update" : "updated"} ${updated} CLI(s)`);
|
|
1093
|
-
} else {
|
|
1094
|
-
log("all CLIs are up to date");
|
|
1095
|
-
}
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
// --- Session reflection --- (dogfood feedback #36)
|
|
1099
|
-
function cmdReflect(target, args) {
|
|
1100
|
-
const ai = path.join(target, "ai");
|
|
1101
|
-
const W = process.stdout.isTTY ? "\x1b[33m" : "";
|
|
1102
|
-
const G = process.stdout.isTTY ? "\x1b[32m" : "";
|
|
1103
|
-
const R2 = process.stdout.isTTY ? "\x1b[0m" : "";
|
|
1104
|
-
const B = process.stdout.isTTY ? "\x1b[1m" : "";
|
|
1105
|
-
|
|
1106
|
-
// Collect swarm done tasks
|
|
1107
|
-
const doneDir = path.join(ai, "swarm", "done");
|
|
1108
|
-
const queueDir = path.join(ai, "swarm", "queue");
|
|
1109
|
-
const activeDir = path.join(ai, "swarm", "active");
|
|
1110
|
-
const doneTasks = [], queueTasks = [];
|
|
1111
|
-
|
|
1112
|
-
// -- how many days to look back (default 7)
|
|
1113
|
-
const daysArg = args.find((_, i) => args[i - 1] === "--days");
|
|
1114
|
-
const days = parseInt(daysArg || "7");
|
|
1115
|
-
const since = Date.now() - days * 24 * 60 * 60 * 1000;
|
|
1116
|
-
|
|
1117
|
-
const readJsonDir = (dir) => {
|
|
1118
|
-
const out = [];
|
|
1119
|
-
try {
|
|
1120
|
-
for (const f of fs.readdirSync(dir).filter(f => f.endsWith(".json"))) {
|
|
1121
|
-
try { out.push(JSON.parse(fs.readFileSync(path.join(dir, f), "utf8"))); } catch {}
|
|
1122
|
-
}
|
|
1123
|
-
} catch {}
|
|
1124
|
-
return out;
|
|
1125
|
-
};
|
|
1126
|
-
|
|
1127
|
-
const allDone = readJsonDir(doneDir).filter(t => {
|
|
1128
|
-
const ts = t.completed_at || t.created_at;
|
|
1129
|
-
return !ts || new Date(ts).getTime() >= since;
|
|
1130
|
-
});
|
|
1131
|
-
const allQueue = readJsonDir(queueDir);
|
|
1132
|
-
const allActive = readJsonDir(activeDir);
|
|
1133
|
-
|
|
1134
|
-
// Aggregate by agent
|
|
1135
|
-
const byAgent = {};
|
|
1136
|
-
for (const t of allDone) {
|
|
1137
|
-
const a = t.assigned_to || t.agent || "unknown";
|
|
1138
|
-
if (!byAgent[a]) byAgent[a] = { done: 0, tasks: [] };
|
|
1139
|
-
byAgent[a].done++;
|
|
1140
|
-
byAgent[a].tasks.push(t.title || t.id || "?");
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
// Session data
|
|
1144
|
-
let sessionGoal = "?";
|
|
1145
|
-
try {
|
|
1146
|
-
const active = JSON.parse(fs.readFileSync(path.join(ai, "sessions", "active.json"), "utf8"));
|
|
1147
|
-
sessionGoal = (active.task || {}).goal || "?";
|
|
1148
|
-
} catch {}
|
|
1149
|
-
|
|
1150
|
-
console.log(`\n ${B}${T}0dai reflect${R2}${R} — last ${days} days\n`);
|
|
1151
|
-
|
|
1152
|
-
// Goal
|
|
1153
|
-
if (sessionGoal !== "?") console.log(` ${B}Goal${R2} ${sessionGoal}`);
|
|
1154
|
-
|
|
1155
|
-
// Delegation stats
|
|
1156
|
-
const totalDone = allDone.length;
|
|
1157
|
-
const totalPending = allQueue.length + allActive.length;
|
|
1158
|
-
const successRate = totalDone + totalPending > 0
|
|
1159
|
-
? Math.round((totalDone / (totalDone + totalPending)) * 100)
|
|
1160
|
-
: null;
|
|
1161
|
-
|
|
1162
|
-
console.log(` ${B}Delivered${R2} ${G}${totalDone}${R2} tasks completed`);
|
|
1163
|
-
if (totalPending) console.log(` ${B}Remaining${R2} ${W}${totalPending}${R2} tasks still pending`);
|
|
1164
|
-
if (successRate !== null) console.log(` ${B}Rate${R2} ${successRate >= 80 ? G : W}${successRate}%${R2} delegation success rate`);
|
|
1165
|
-
|
|
1166
|
-
// By agent breakdown with per-agent completion rate
|
|
1167
|
-
const allPendingByAgent = {};
|
|
1168
|
-
for (const t of [...allQueue, ...allActive]) {
|
|
1169
|
-
const a = t.assigned_to || "unknown";
|
|
1170
|
-
allPendingByAgent[a] = (allPendingByAgent[a] || 0) + 1;
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
if (Object.keys(byAgent).length) {
|
|
1174
|
-
console.log(`\n ${B}By agent:${R2}`);
|
|
1175
|
-
for (const [agent, data] of Object.entries(byAgent).sort((a, b) => b[1].done - a[1].done)) {
|
|
1176
|
-
const pending = allPendingByAgent[agent] || 0;
|
|
1177
|
-
const total = data.done + pending;
|
|
1178
|
-
const rate = total > 0 ? Math.round((data.done / total) * 100) : 100;
|
|
1179
|
-
const bar = "█".repeat(Math.min(data.done, 20));
|
|
1180
|
-
const rateStr = total > 1 ? ` (${rate >= 80 ? G : W}${rate}%${R2})` : "";
|
|
1181
|
-
console.log(` ${(agent + " ").padEnd(14)} ${G}${bar}${R2} ${data.done}/${total}${rateStr}`);
|
|
1182
|
-
}
|
|
1183
|
-
// Agents with only pending tasks (never completed anything in window)
|
|
1184
|
-
for (const [agent, count] of Object.entries(allPendingByAgent)) {
|
|
1185
|
-
if (!byAgent[agent]) {
|
|
1186
|
-
console.log(` ${(agent + " ").padEnd(14)} ${W}${"░".repeat(Math.min(count, 20))}${R2} 0/${count} ${W}(pending)${R2}`);
|
|
1187
|
-
}
|
|
1188
|
-
}
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
// Budget summary from budget.json
|
|
1192
|
-
const budgetFile = path.join(ai, "swarm", "budget.json");
|
|
1193
|
-
try {
|
|
1194
|
-
const budget = JSON.parse(fs.readFileSync(budgetFile, "utf8"));
|
|
1195
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
1196
|
-
const sessionKey = process.env.ODAI_SESSION_ID ||
|
|
1197
|
-
new Date().toISOString().slice(0, 13).replace("T", "-"); // YYYY-MM-DD-HH
|
|
1198
|
-
const dailySpent = budget.daily?.[today] || 0;
|
|
1199
|
-
const totalSpent = budget.total_spent || 0;
|
|
1200
|
-
const sess = budget.sessions?.[sessionKey];
|
|
1201
|
-
|
|
1202
|
-
// Tier distribution across all tasks
|
|
1203
|
-
const tierCount = { fast: 0, balanced: 0, deep: 0 };
|
|
1204
|
-
for (const t of Object.values(budget.tasks || {})) {
|
|
1205
|
-
if (t.tier && tierCount[t.tier] !== undefined) tierCount[t.tier]++;
|
|
1206
|
-
}
|
|
1207
|
-
const tieredTotal = tierCount.fast + tierCount.balanced + tierCount.deep;
|
|
1208
|
-
|
|
1209
|
-
if (dailySpent > 0 || totalSpent > 0 || sess) {
|
|
1210
|
-
console.log(`\n ${B}Budget:${R2}`);
|
|
1211
|
-
if (sess && sess.total_cost > 0) {
|
|
1212
|
-
const taskCount = (sess.tasks || []).length;
|
|
1213
|
-
const avgCost = taskCount > 0 ? (sess.total_cost / taskCount).toFixed(4) : "0";
|
|
1214
|
-
console.log(` ${B}This session${R2} $${sess.total_cost.toFixed(4)} · ${taskCount} tasks · avg $${avgCost}/task`);
|
|
1215
|
-
if (sess.tiers) {
|
|
1216
|
-
const tiers = Object.entries(sess.tiers).filter(([, n]) => n > 0).map(([t, n]) => `${n}×${t}`).join(" ");
|
|
1217
|
-
if (tiers) console.log(` ${D}Tiers ${tiers}${R2}`);
|
|
1218
|
-
}
|
|
1219
|
-
}
|
|
1220
|
-
if (dailySpent > 0) {
|
|
1221
|
-
const dailyLimit = parseFloat(process.env.ODAI_DAILY_BUDGET || "5");
|
|
1222
|
-
const pct = Math.round((dailySpent / dailyLimit) * 100);
|
|
1223
|
-
const bar = "█".repeat(Math.round(pct / 5)).padEnd(20, "░");
|
|
1224
|
-
const col = pct < 50 ? G : pct < 80 ? W : "\x1b[31m";
|
|
1225
|
-
console.log(` ${B}Today${R2} ${col}$${dailySpent.toFixed(4)}${R2} / $${dailyLimit.toFixed(2)} ${D}${bar} ${pct}%${R2}`);
|
|
1226
|
-
}
|
|
1227
|
-
if (totalSpent > 0) {
|
|
1228
|
-
console.log(` ${B}All time${R2} ${D}$${totalSpent.toFixed(4)}${R2}`);
|
|
1229
|
-
}
|
|
1230
|
-
if (tieredTotal > 0) {
|
|
1231
|
-
const fastPct = Math.round((tierCount.fast / tieredTotal) * 100);
|
|
1232
|
-
console.log(` ${D}Model routing ${tierCount.fast}×fast ${tierCount.balanced}×balanced ${tierCount.deep}×deep (${fastPct}% cheap)${R2}`);
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
|
-
} catch {}
|
|
1236
|
-
|
|
1237
|
-
// Insights — learn from patterns
|
|
1238
|
-
if (totalDone >= 3) {
|
|
1239
|
-
const insights = [];
|
|
1240
|
-
// Best agent by completion rate
|
|
1241
|
-
const agentEntries = Object.entries(byAgent).map(([a, d]) => {
|
|
1242
|
-
const pending = allPendingByAgent[a] || 0;
|
|
1243
|
-
const total = d.done + pending;
|
|
1244
|
-
return { agent: a, done: d.done, total, rate: total > 0 ? d.done / total : 1 };
|
|
1245
|
-
});
|
|
1246
|
-
const best = agentEntries.sort((a, b) => b.rate - a.rate)[0];
|
|
1247
|
-
if (best && agentEntries.length > 1) {
|
|
1248
|
-
insights.push(`${best.agent} has the highest success rate (${Math.round(best.rate * 100)}%)`);
|
|
1249
|
-
}
|
|
1250
|
-
// Fastest agent by avg elapsed
|
|
1251
|
-
try {
|
|
1252
|
-
const budget = JSON.parse(fs.readFileSync(path.join(ai, "swarm", "budget.json"), "utf8"));
|
|
1253
|
-
const agentElapsed = {};
|
|
1254
|
-
const agentCounts = {};
|
|
1255
|
-
for (const t of Object.values(budget.tasks || {})) {
|
|
1256
|
-
if (t.elapsed > 0) {
|
|
1257
|
-
agentElapsed[t.agent] = (agentElapsed[t.agent] || 0) + t.elapsed;
|
|
1258
|
-
agentCounts[t.agent] = (agentCounts[t.agent] || 0) + 1;
|
|
1259
|
-
}
|
|
1260
|
-
}
|
|
1261
|
-
let fastest = null, fastestAvg = Infinity;
|
|
1262
|
-
for (const [a, total] of Object.entries(agentElapsed)) {
|
|
1263
|
-
const avg = total / agentCounts[a];
|
|
1264
|
-
if (avg < fastestAvg) { fastest = a; fastestAvg = avg; }
|
|
1265
|
-
}
|
|
1266
|
-
if (fastest && Object.keys(agentElapsed).length > 1) {
|
|
1267
|
-
insights.push(`${fastest} is fastest (avg ${Math.round(fastestAvg)}s per task)`);
|
|
1268
|
-
}
|
|
1269
|
-
} catch {}
|
|
1270
|
-
// Cost efficiency
|
|
1271
|
-
if (allDone.length > 0) {
|
|
1272
|
-
try {
|
|
1273
|
-
const budget = JSON.parse(fs.readFileSync(path.join(ai, "swarm", "budget.json"), "utf8"));
|
|
1274
|
-
if (budget.total_spent > 0) {
|
|
1275
|
-
const costPerTask = budget.total_spent / allDone.length;
|
|
1276
|
-
insights.push(`avg cost: $${costPerTask.toFixed(4)}/task`);
|
|
1277
|
-
}
|
|
1278
|
-
} catch {}
|
|
1279
|
-
}
|
|
1280
|
-
if (insights.length) {
|
|
1281
|
-
console.log(`\n ${B}Insights:${R2}`);
|
|
1282
|
-
for (const i of insights) console.log(` ${G}→${R2} ${i}`);
|
|
1283
|
-
}
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
// Remaining blockers
|
|
1287
|
-
if (allQueue.length) {
|
|
1288
|
-
console.log(`\n ${B}Blockers / queue:${R2}`);
|
|
1289
|
-
for (const t of allQueue.slice(0, 8)) {
|
|
1290
|
-
const agent = t.assigned_to ? ` → ${t.assigned_to}` : "";
|
|
1291
|
-
console.log(` ${W}•${R2} ${(t.title || t.id || "?").slice(0, 60)}${agent}`);
|
|
1292
|
-
}
|
|
1293
|
-
if (allQueue.length > 8) console.log(` ${D}… and ${allQueue.length - 8} more${R2}`);
|
|
1294
|
-
}
|
|
1295
|
-
|
|
1296
|
-
// No data
|
|
1297
|
-
if (!totalDone && !totalPending) {
|
|
1298
|
-
console.log(` ${D}No swarm tasks found in the last ${days} days.${R2}`);
|
|
1299
|
-
console.log(` ${D}Use '0dai swarm add --task "..." --to codex' to delegate tasks.${R2}`);
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
|
-
console.log();
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
|
-
function cmdMetrics(target) {
|
|
1306
|
-
const ai = path.join(target, "ai");
|
|
1307
|
-
const G = "\x1b[32m", W = "\x1b[33m", R2 = "\x1b[0m", D = "\x1b[2m",
|
|
1308
|
-
B = "\x1b[34m", T = "\x1b[36m", M = "\x1b[35m";
|
|
1309
|
-
|
|
1310
|
-
// --- Data sources ---
|
|
1311
|
-
let stats = {}, budget = {}, discovery = {};
|
|
1312
|
-
try { stats = JSON.parse(fs.readFileSync(path.join(ai, "feedback", ".usage_stats.json"), "utf8")); } catch {}
|
|
1313
|
-
try { budget = JSON.parse(fs.readFileSync(path.join(ai, "swarm", "budget.json"), "utf8")); } catch {}
|
|
1314
|
-
try { discovery = JSON.parse(fs.readFileSync(path.join(ai, "manifest", "discovery.json"), "utf8")); } catch {}
|
|
1315
|
-
|
|
1316
|
-
const projectName = discovery.project_name || path.basename(target);
|
|
1317
|
-
const stack = discovery.stack || "?";
|
|
1318
|
-
const totalSessions = stats.total_sessions || 0;
|
|
1319
|
-
const agentBreakdown = stats.agents || {};
|
|
1320
|
-
const layerInfo = stats.layer || {};
|
|
1321
|
-
const lastSession = stats.last_session ? new Date(stats.last_session) : null;
|
|
1322
|
-
const layerVersion = stats.version || "?";
|
|
1323
|
-
|
|
1324
|
-
// Swarm tasks: count done files
|
|
1325
|
-
let tasksDone = 0, tasksQueue = 0;
|
|
1326
|
-
const doneDir = path.join(ai, "swarm", "done");
|
|
1327
|
-
const queueDir = path.join(ai, "swarm", "queue");
|
|
1328
|
-
try { tasksDone = fs.readdirSync(doneDir).filter(f => f.endsWith(".json")).length; } catch {}
|
|
1329
|
-
try { tasksQueue = fs.readdirSync(queueDir).filter(f => f.endsWith(".json")).length; } catch {}
|
|
1330
|
-
|
|
1331
|
-
// Activity: count events from activity.jsonl
|
|
1332
|
-
let activityEvents = 0;
|
|
1333
|
-
try {
|
|
1334
|
-
const lines = fs.readFileSync(path.join(ai, "swarm", "activity.jsonl"), "utf8").trim().split("\n").filter(Boolean);
|
|
1335
|
-
activityEvents = lines.length;
|
|
1336
|
-
} catch {}
|
|
1337
|
-
|
|
1338
|
-
// Feedback submissions
|
|
1339
|
-
let feedbackCount = layerInfo.feedback_reports || 0;
|
|
1340
|
-
|
|
1341
|
-
// Budget totals
|
|
1342
|
-
const totalSpent = budget.total_spent || 0;
|
|
1343
|
-
const sessionsWithBudget = Object.keys(budget.sessions || {}).length;
|
|
1344
|
-
|
|
1345
|
-
// --- Effectiveness score (0-100) ---
|
|
1346
|
-
let score = 0, scoreNotes = [];
|
|
1347
|
-
|
|
1348
|
-
// Sessions depth: 1 = tried, 3 = habit forming, 7 = regular use
|
|
1349
|
-
const sessionScore = Math.min(Math.floor((totalSessions / 7) * 35), 35);
|
|
1350
|
-
score += sessionScore;
|
|
1351
|
-
if (totalSessions === 0) scoreNotes.push("not started");
|
|
1352
|
-
else if (totalSessions === 1) scoreNotes.push("first session");
|
|
1353
|
-
else if (totalSessions < 3) scoreNotes.push("early");
|
|
1354
|
-
else if (totalSessions < 7) scoreNotes.push("habit forming");
|
|
1355
|
-
else scoreNotes.push("regular use");
|
|
1356
|
-
|
|
1357
|
-
// Delegation: did they delegate to swarm?
|
|
1358
|
-
const delegationScore = tasksDone > 0 ? Math.min(Math.floor((tasksDone / 5) * 30), 30) : 0;
|
|
1359
|
-
score += delegationScore;
|
|
1360
|
-
if (tasksDone > 0) scoreNotes.push(`${tasksDone} tasks delegated`);
|
|
1361
|
-
|
|
1362
|
-
// Feedback: submitted = trust signal
|
|
1363
|
-
const feedbackScore = feedbackCount > 0 ? 20 : 0;
|
|
1364
|
-
score += feedbackScore;
|
|
1365
|
-
if (feedbackCount > 0) scoreNotes.push("feedback submitted");
|
|
1366
|
-
|
|
1367
|
-
// Layer completeness: has playbooks and commands?
|
|
1368
|
-
const layerScore = (layerInfo.playbooks && layerInfo.commands) ? 15 : (layerInfo.commands ? 8 : 0);
|
|
1369
|
-
score += layerScore;
|
|
1370
|
-
|
|
1371
|
-
const scoreColor = score >= 70 ? G : score >= 40 ? W : "\x1b[31m";
|
|
1372
|
-
const bar = "█".repeat(Math.round(score / 5)).padEnd(20, "░");
|
|
1373
|
-
|
|
1374
|
-
// --- Output ---
|
|
1375
|
-
console.log(`\n ${T}Metrics${R2} ${D}${projectName} · ${stack} · ai v${layerVersion}${R2}\n`);
|
|
1376
|
-
|
|
1377
|
-
// Effectiveness score
|
|
1378
|
-
console.log(` ${B}Effectiveness${R2}`);
|
|
1379
|
-
console.log(` ${scoreColor}${score}/100${R2} ${D}${bar}${R2}`);
|
|
1380
|
-
if (scoreNotes.length) console.log(` ${D}${scoreNotes.join(" · ")}${R2}`);
|
|
1381
|
-
|
|
1382
|
-
// Adoption funnel
|
|
1383
|
-
console.log(`\n ${B}Adoption funnel${R2}`);
|
|
1384
|
-
const funnelStep = (label, value, done, hint) => {
|
|
1385
|
-
const icon = done ? `${G}✓${R2}` : `${D}○${R2}`;
|
|
1386
|
-
const val = value !== null ? ` ${D}${value}${R2}` : "";
|
|
1387
|
-
const h = !done && hint ? ` ${D}← ${hint}${R2}` : "";
|
|
1388
|
-
console.log(` ${icon} ${label}${val}${h}`);
|
|
1389
|
-
};
|
|
1390
|
-
funnelStep("Initialized", `ai/ v${layerVersion}`, true);
|
|
1391
|
-
funnelStep("Returned (>1 session)", `${totalSessions} total`, totalSessions > 1, "run 0dai reflect after each session");
|
|
1392
|
-
funnelStep("Used swarm delegation", tasksDone > 0 ? `${tasksDone} tasks done` : null, tasksDone > 0, "try: 0dai swarm add --task '...' --to codex");
|
|
1393
|
-
funnelStep("Submitted feedback", feedbackCount > 0 ? `${feedbackCount} reports` : null, feedbackCount > 0, "0dai feedback log + push");
|
|
1394
|
-
|
|
1395
|
-
// Session stats
|
|
1396
|
-
if (totalSessions > 0) {
|
|
1397
|
-
console.log(`\n ${B}Sessions${R2}`);
|
|
1398
|
-
console.log(` Total ${totalSessions}`);
|
|
1399
|
-
if (lastSession) {
|
|
1400
|
-
const daysAgo = Math.floor((Date.now() - lastSession.getTime()) / 86400000);
|
|
1401
|
-
const when = daysAgo === 0 ? "today" : daysAgo === 1 ? "yesterday" : `${daysAgo}d ago`;
|
|
1402
|
-
console.log(` Last ${when}`);
|
|
1403
|
-
}
|
|
1404
|
-
const agentEntries = Object.entries(agentBreakdown).sort((a, b) => b[1] - a[1]);
|
|
1405
|
-
if (agentEntries.length) {
|
|
1406
|
-
console.log(` Agents ${agentEntries.map(([a, n]) => `${a}: ${n}`).join(" ")}`);
|
|
1407
|
-
}
|
|
1408
|
-
if (sessionsWithBudget > 0 && totalSpent > 0) {
|
|
1409
|
-
console.log(` Cost $${totalSpent.toFixed(4)} total ${D}(${sessionsWithBudget} sessions tracked)${R2}`);
|
|
1410
|
-
}
|
|
1411
|
-
}
|
|
1412
|
-
|
|
1413
|
-
// Delegation stats
|
|
1414
|
-
if (tasksDone > 0 || tasksQueue > 0) {
|
|
1415
|
-
console.log(`\n ${B}Delegation${R2}`);
|
|
1416
|
-
if (tasksDone > 0) console.log(` Done ${G}${tasksDone}${R2}`);
|
|
1417
|
-
if (tasksQueue > 0) console.log(` Queue ${W}${tasksQueue}${R2}`);
|
|
1418
|
-
if (activityEvents > 0) console.log(` Events ${activityEvents}`);
|
|
1419
|
-
}
|
|
1420
|
-
|
|
1421
|
-
// Layer health
|
|
1422
|
-
console.log(`\n ${B}ai/ layer${R2}`);
|
|
1423
|
-
const checks = [
|
|
1424
|
-
["commands.yaml", layerInfo.commands],
|
|
1425
|
-
["playbooks", layerInfo.playbooks],
|
|
1426
|
-
["personas", layerInfo.personas],
|
|
1427
|
-
["session roaming", layerInfo.session_active],
|
|
1428
|
-
["swarm queue", (layerInfo.swarm_queue || 0) > 0],
|
|
1429
|
-
];
|
|
1430
|
-
for (const [label, ok] of checks) {
|
|
1431
|
-
const icon = ok ? `${G}✓${R2}` : `${D}—${R2}`;
|
|
1432
|
-
console.log(` ${icon} ${label}`);
|
|
1433
|
-
}
|
|
1434
|
-
|
|
1435
|
-
// Next suggested action
|
|
1436
|
-
console.log(`\n ${B}Next${R2}`);
|
|
1437
|
-
if (totalSessions === 0) console.log(` ${D}Start a Claude Code session — session_start hook will print project context${R2}`);
|
|
1438
|
-
else if (tasksDone === 0) console.log(` ${D}Try delegating a task: 0dai swarm add --task "write tests for auth module" --to codex${R2}`);
|
|
1439
|
-
else if (feedbackCount === 0) console.log(` ${D}Submit feedback: 0dai feedback log --type positive --detail "what worked"${R2}`);
|
|
1440
|
-
else console.log(` ${D}Score ${score}/100 — keep delegating and submitting feedback${R2}`);
|
|
1441
|
-
|
|
1442
|
-
console.log();
|
|
1443
|
-
}
|
|
1444
|
-
|
|
1445
|
-
function cmdStatus(target) {
|
|
1446
|
-
const ai = path.join(target, "ai");
|
|
1447
|
-
let v = "?", stack = "?";
|
|
1448
|
-
try { v = fs.readFileSync(path.join(ai, "VERSION"), "utf8").trim(); } catch {}
|
|
1449
|
-
try { stack = JSON.parse(fs.readFileSync(path.join(ai, "manifest", "discovery.json"), "utf8")).stack || "?"; } catch {}
|
|
1450
|
-
log(`v${v} | stack: ${stack}`);
|
|
1451
|
-
|
|
1452
|
-
const count = (dir) => { try { return fs.readdirSync(dir).filter(f => f.endsWith(".json")).length; } catch { return 0; } };
|
|
1453
|
-
const q = count(path.join(ai, "swarm", "queue"));
|
|
1454
|
-
const a = count(path.join(ai, "swarm", "active"));
|
|
1455
|
-
const d = count(path.join(ai, "swarm", "done"));
|
|
1456
|
-
if (q || a || d) console.log(` swarm: ${q} queued, ${a} active, ${d} done`);
|
|
1457
|
-
|
|
1458
|
-
try {
|
|
1459
|
-
const s = JSON.parse(fs.readFileSync(path.join(ai, "sessions", "active.json"), "utf8"));
|
|
1460
|
-
console.log(` session: ${(s.task || {}).goal || "?"} (agent: ${s.current_agent || "?"})`);
|
|
1461
|
-
} catch {}
|
|
1462
|
-
}
|
|
1463
|
-
|
|
1464
|
-
async function checkVersion() {
|
|
1465
|
-
try {
|
|
1466
|
-
// Check interval: 1 hour during debug, configurable via env
|
|
1467
|
-
const intervalSec = parseInt(process.env.ODAI_UPDATE_CHECK_INTERVAL || "3600");
|
|
1468
|
-
let lastCheck = 0;
|
|
1469
|
-
try { lastCheck = parseFloat(fs.readFileSync(VERSION_CHECK_FILE, "utf8")); } catch {}
|
|
1470
|
-
if (Date.now() / 1000 - lastCheck < intervalSec) return;
|
|
1471
|
-
|
|
1472
|
-
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
1473
|
-
fs.writeFileSync(VERSION_CHECK_FILE, String(Date.now() / 1000));
|
|
1474
|
-
|
|
1475
|
-
const result = await apiCall("/v1/version");
|
|
1476
|
-
if (result.version && result.version !== VERSION) {
|
|
1477
|
-
// Only suggest update if remote version is actually newer
|
|
1478
|
-
const cmp = (a, b) => { const [a1,a2,a3] = a.split(".").map(Number); const [b1,b2,b3] = b.split(".").map(Number); return a1 - b1 || a2 - b2 || a3 - b3; };
|
|
1479
|
-
if (cmp(result.version, VERSION) > 0) {
|
|
1480
|
-
log(`Update available: ${VERSION} → ${result.version}`);
|
|
1481
|
-
console.log(` Run: npm update -g @0dai-dev/cli\n`);
|
|
1482
|
-
}
|
|
1483
|
-
}
|
|
1484
|
-
} catch {}
|
|
1485
|
-
}
|
|
1486
|
-
|
|
1487
|
-
async function cmdAuthLogin() {
|
|
1488
|
-
const isTTY = process.stdout.isTTY && process.stdin.isTTY;
|
|
1489
|
-
|
|
1490
|
-
// Check if already authenticated
|
|
1491
|
-
try {
|
|
1492
|
-
const existing = JSON.parse(fs.readFileSync(AUTH_FILE, "utf8"));
|
|
1493
|
-
if (existing.access_token || existing.email) {
|
|
1494
|
-
if (isTTY) {
|
|
1495
|
-
const p = require("@clack/prompts");
|
|
1496
|
-
p.intro(`${T}0dai${R} authentication`);
|
|
1497
|
-
p.log.success(`Already logged in as ${T}${existing.email || "unknown"}${R} (${existing.plan || "free"} plan)`);
|
|
1498
|
-
const reauth = await p.confirm({ message: "Sign in with a different account?" });
|
|
1499
|
-
if (p.isCancel(reauth) || !reauth) {
|
|
1500
|
-
p.outro("Current session kept");
|
|
1501
|
-
return;
|
|
1502
|
-
}
|
|
1503
|
-
} else {
|
|
1504
|
-
log(`Already logged in as ${existing.email || "unknown"} (${existing.plan || "free"} plan)`);
|
|
1505
|
-
log("To switch accounts, delete ~/.0dai/auth.json and run again");
|
|
1506
|
-
return;
|
|
1507
|
-
}
|
|
1508
|
-
}
|
|
1509
|
-
} catch {}
|
|
1510
|
-
|
|
1511
|
-
if (isTTY) {
|
|
1512
|
-
// Interactive TUI flow
|
|
1513
|
-
const p = require("@clack/prompts");
|
|
1514
|
-
if (!p._intro_shown) p.intro(`${T}0dai${R} authentication`);
|
|
1515
|
-
|
|
1516
|
-
p.note(
|
|
1517
|
-
"0dai auth is separate from agent CLIs (Claude Code, Codex).\n" +
|
|
1518
|
-
"It tracks your projects, usage limits, and team features.\n" +
|
|
1519
|
-
"Your agent CLIs keep their own auth (subscription/API key).",
|
|
1520
|
-
"Why sign in?"
|
|
1521
|
-
);
|
|
1522
|
-
|
|
1523
|
-
const method = await p.select({
|
|
1524
|
-
message: "How would you like to sign in?",
|
|
1525
|
-
options: [
|
|
1526
|
-
{ value: "github", label: "GitHub", hint: "recommended" },
|
|
1527
|
-
{ value: "google", label: "Google" },
|
|
1528
|
-
{ value: "device", label: "Device code", hint: "no browser needed" },
|
|
1529
|
-
],
|
|
1530
|
-
});
|
|
1531
|
-
if (p.isCancel(method)) { p.cancel("Cancelled"); process.exit(0); }
|
|
1532
|
-
|
|
1533
|
-
if (method === "github" || method === "google") {
|
|
1534
|
-
const url = `${API_URL}/v1/auth/${method}?cli=true`;
|
|
1535
|
-
p.log.info(`Opening browser: ${url}`);
|
|
1536
|
-
try {
|
|
1537
|
-
const { execFileSync } = require("child_process");
|
|
1538
|
-
const cmd = os.platform() === "darwin" ? "open" : os.platform() === "win32" ? "start" : "xdg-open";
|
|
1539
|
-
// MED: use execFileSync to avoid shell injection via URL metacharacters
|
|
1540
|
-
execFileSync(cmd, [url], { stdio: "ignore" });
|
|
1541
|
-
} catch {
|
|
1542
|
-
p.log.warn(`Could not open browser. Visit manually:\n ${url}`);
|
|
1543
|
-
}
|
|
1544
|
-
|
|
1545
|
-
const s = p.spinner();
|
|
1546
|
-
s.start("Waiting for browser confirmation...");
|
|
1547
|
-
|
|
1548
|
-
// Poll auth/status until we get a new token (check every 3s, 5min timeout)
|
|
1549
|
-
// For now, ask user to paste token from success page
|
|
1550
|
-
s.stop("Browser opened");
|
|
1551
|
-
const token = await p.text({
|
|
1552
|
-
message: "Paste your token from the success page (or press Enter to skip):",
|
|
1553
|
-
placeholder: "0dai_at_...",
|
|
1554
|
-
});
|
|
1555
|
-
if (token && !p.isCancel(token) && token.startsWith("0dai_at_")) {
|
|
1556
|
-
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
1557
|
-
fs.writeFileSync(AUTH_FILE, JSON.stringify({
|
|
1558
|
-
access_token: token,
|
|
1559
|
-
authenticated_at: new Date().toISOString(),
|
|
1560
|
-
}, null, 2) + "\n", { mode: 0o600 });
|
|
1561
|
-
// Fetch profile
|
|
1562
|
-
const status = await apiCall("/v1/auth/status");
|
|
1563
|
-
if (status.email) {
|
|
1564
|
-
const auth = JSON.parse(fs.readFileSync(AUTH_FILE, "utf8"));
|
|
1565
|
-
auth.email = status.email;
|
|
1566
|
-
auth.plan = status.plan;
|
|
1567
|
-
auth.name = status.name;
|
|
1568
|
-
fs.writeFileSync(AUTH_FILE, JSON.stringify(auth, null, 2) + "\n", { mode: 0o600 });
|
|
1569
|
-
p.outro(`${T}Logged in${R} as ${status.email} (${status.plan} plan)`);
|
|
1570
|
-
} else {
|
|
1571
|
-
p.outro(`${T}Token saved${R}`);
|
|
1572
|
-
}
|
|
1573
|
-
return;
|
|
1574
|
-
}
|
|
1575
|
-
p.log.info("Skipped. You can also use device code flow:");
|
|
1576
|
-
}
|
|
1577
|
-
|
|
1578
|
-
// Device code fallback
|
|
1579
|
-
const result = await apiCall("/v1/auth/device", { client_id: "cli" });
|
|
1580
|
-
if (result.error) { p.log.error(result.error); process.exit(1); }
|
|
1581
|
-
|
|
1582
|
-
p.log.step(`Open: ${result.verification_uri}`);
|
|
1583
|
-
p.log.step(`Code: ${T}${result.user_code}${R}`);
|
|
1584
|
-
|
|
1585
|
-
const s = p.spinner();
|
|
1586
|
-
s.start("Waiting for confirmation...");
|
|
1587
|
-
|
|
1588
|
-
const interval = (result.interval || 5) * 1000;
|
|
1589
|
-
const deadline = Date.now() + (result.expires_in || 600) * 1000;
|
|
1590
|
-
while (Date.now() < deadline) {
|
|
1591
|
-
await new Promise(r => setTimeout(r, interval));
|
|
1592
|
-
const poll = await apiCall("/v1/auth/token", { device_code: result.device_code });
|
|
1593
|
-
if (poll.access_token) {
|
|
1594
|
-
s.stop("Authorized!");
|
|
1595
|
-
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
1596
|
-
fs.writeFileSync(AUTH_FILE, JSON.stringify({
|
|
1597
|
-
access_token: poll.access_token, email: poll.email,
|
|
1598
|
-
plan: poll.plan || "free", authenticated_at: new Date().toISOString(),
|
|
1599
|
-
expires_at: poll.expires_at,
|
|
1600
|
-
}, null, 2) + "\n", { mode: 0o600 });
|
|
1601
|
-
p.outro(`${T}Logged in${R} as ${poll.email} (${poll.plan} plan)`);
|
|
1602
|
-
return;
|
|
1603
|
-
}
|
|
1604
|
-
if (poll.error && poll.error !== "authorization_pending") {
|
|
1605
|
-
s.stop("Failed");
|
|
1606
|
-
p.log.error(poll.error);
|
|
1607
|
-
process.exit(1);
|
|
1608
|
-
}
|
|
1609
|
-
}
|
|
1610
|
-
s.stop("Timed out");
|
|
1611
|
-
p.log.error("Try again.");
|
|
1612
|
-
process.exit(1);
|
|
1613
|
-
|
|
1614
|
-
} else {
|
|
1615
|
-
// Non-interactive: device code only
|
|
1616
|
-
log("0dai auth is separate from agent CLIs. It tracks projects, limits, and team features.");
|
|
1617
|
-
const result = await apiCall("/v1/auth/device", { client_id: "cli" });
|
|
1618
|
-
if (result.error) { log(`error: ${result.error}`); process.exit(1); }
|
|
1619
|
-
log(`Open: ${result.verification_uri}`);
|
|
1620
|
-
log(`Code: ${result.user_code}`);
|
|
1621
|
-
log("Waiting...");
|
|
1622
|
-
const interval = (result.interval || 5) * 1000;
|
|
1623
|
-
const deadline = Date.now() + (result.expires_in || 600) * 1000;
|
|
1624
|
-
while (Date.now() < deadline) {
|
|
1625
|
-
await new Promise(r => setTimeout(r, interval));
|
|
1626
|
-
const poll = await apiCall("/v1/auth/token", { device_code: result.device_code });
|
|
1627
|
-
if (poll.access_token) {
|
|
1628
|
-
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
1629
|
-
fs.writeFileSync(AUTH_FILE, JSON.stringify({
|
|
1630
|
-
access_token: poll.access_token, email: poll.email,
|
|
1631
|
-
plan: poll.plan || "free", authenticated_at: new Date().toISOString(),
|
|
1632
|
-
}, null, 2) + "\n", { mode: 0o600 });
|
|
1633
|
-
log(`Logged in as ${poll.email}`);
|
|
1634
|
-
return;
|
|
1635
|
-
}
|
|
1636
|
-
}
|
|
1637
|
-
log("Timed out");
|
|
1638
|
-
process.exit(1);
|
|
1639
|
-
}
|
|
1640
|
-
}
|
|
1641
|
-
|
|
1642
|
-
function cmdAuthLogout() {
|
|
1643
|
-
try { fs.unlinkSync(AUTH_FILE); } catch {}
|
|
1644
|
-
log("Logged out");
|
|
1645
|
-
}
|
|
1646
|
-
|
|
1647
|
-
async function cmdRedeem(code) {
|
|
1648
|
-
if (!code) {
|
|
1649
|
-
console.log("Usage: 0dai redeem <CODE>");
|
|
1650
|
-
console.log("Example: 0dai redeem ESSE-ABCD-1234");
|
|
1651
|
-
process.exit(1);
|
|
1652
|
-
}
|
|
1653
|
-
try {
|
|
1654
|
-
JSON.parse(fs.readFileSync(AUTH_FILE, "utf8"));
|
|
1655
|
-
} catch {
|
|
1656
|
-
log("Not logged in. Run: 0dai auth login");
|
|
1657
|
-
process.exit(1);
|
|
1658
|
-
}
|
|
1659
|
-
log(`Redeeming code ${T}${code.toUpperCase()}${R}...`);
|
|
1660
|
-
const result = await apiCall("/v1/redeem", { code: code.toUpperCase().trim() });
|
|
1661
|
-
if (result.ok) {
|
|
1662
|
-
log(`${T}✓${R} ${result.message}`);
|
|
1663
|
-
if (result.duration_days) {
|
|
1664
|
-
log(` Plan active for ${result.duration_days} days`);
|
|
1665
|
-
}
|
|
1666
|
-
log(` Run ${D}0dai auth status${R} to see updated limits`);
|
|
1667
|
-
} else {
|
|
1668
|
-
log(`error: ${result.error || "unknown"}`);
|
|
1669
|
-
if (result.hint) log(`hint: ${result.hint}`);
|
|
1670
|
-
process.exit(1);
|
|
1671
|
-
}
|
|
1672
|
-
}
|
|
1673
|
-
|
|
1674
|
-
async function cmdAuthStatus() {
|
|
1675
|
-
try {
|
|
1676
|
-
const auth = JSON.parse(fs.readFileSync(AUTH_FILE, "utf8"));
|
|
1677
|
-
// Backwards compat: old auth.json used `user`, new uses `email`
|
|
1678
|
-
const email = auth.email || auth.user || "unknown";
|
|
1679
|
-
log(`${email} (${auth.plan || "free"} plan)`);
|
|
1680
|
-
// Get usage from API
|
|
1681
|
-
const status = await apiCall("/v1/auth/status");
|
|
1682
|
-
if (status.usage_today) {
|
|
1683
|
-
console.log(" Usage today:");
|
|
1684
|
-
for (const [k, v] of Object.entries(status.usage_today))
|
|
1685
|
-
console.log(` ${k}: ${v} / ${status.limits[k]}`);
|
|
1686
|
-
}
|
|
1687
|
-
} catch {
|
|
1688
|
-
log("Not logged in. Run: 0dai auth login");
|
|
1689
|
-
}
|
|
1690
|
-
}
|
|
1691
|
-
|
|
1692
|
-
async function cmdFeedbackPush(target) {
|
|
1693
|
-
const fbDir = path.join(target, "ai", "feedback");
|
|
1694
|
-
const items = [];
|
|
1695
|
-
|
|
1696
|
-
// Collect from report JSON files
|
|
1697
|
-
try {
|
|
1698
|
-
for (const f of fs.readdirSync(fbDir)) {
|
|
1699
|
-
if (f.endsWith("-report.json") || (f.endsWith(".json") && f.match(/^\d{8}/))) {
|
|
1700
|
-
try {
|
|
1701
|
-
const d = JSON.parse(fs.readFileSync(path.join(fbDir, f), "utf8"));
|
|
1702
|
-
if (d.project || d.verdict) items.push({ type: "report", data: d, file: f });
|
|
1703
|
-
} catch {}
|
|
1704
|
-
}
|
|
1705
|
-
}
|
|
1706
|
-
} catch {}
|
|
1707
|
-
|
|
1708
|
-
// Collect from operational.jsonl (feedback log entries)
|
|
1709
|
-
const jsonlPath = path.join(fbDir, "operational.jsonl");
|
|
1710
|
-
try {
|
|
1711
|
-
if (fs.existsSync(jsonlPath)) {
|
|
1712
|
-
const lines = fs.readFileSync(jsonlPath, "utf8").trim().split("\n").filter(Boolean);
|
|
1713
|
-
for (const line of lines) {
|
|
1714
|
-
try { items.push({ type: "log", data: JSON.parse(line) }); } catch {}
|
|
1715
|
-
}
|
|
1716
|
-
}
|
|
1717
|
-
} catch {}
|
|
1718
|
-
|
|
1719
|
-
if (!items.length) {
|
|
1720
|
-
log("no feedback found");
|
|
1721
|
-
console.log(` ${D}Log feedback first: 0dai feedback log --type suggestion --detail '...'${R}`);
|
|
1722
|
-
return;
|
|
1723
|
-
}
|
|
1724
|
-
|
|
1725
|
-
// Push all items
|
|
1726
|
-
const report = {
|
|
1727
|
-
project: path.basename(target),
|
|
1728
|
-
entries: items.map(i => i.data),
|
|
1729
|
-
count: items.length,
|
|
1730
|
-
submitted_at: new Date().toISOString(),
|
|
1731
|
-
};
|
|
1732
|
-
log(`pushing ${items.length} feedback item(s)...`);
|
|
1733
|
-
const result = await apiCall("/v1/feedback", { report });
|
|
1734
|
-
if (result.received) {
|
|
1735
|
-
log(`received${result.issue ? `: ${result.issue}` : ""}`);
|
|
1736
|
-
if (result.bonus) log(`${T}bonus:${R} ${result.bonus}`);
|
|
1737
|
-
// Archive pushed entries
|
|
1738
|
-
if (fs.existsSync(jsonlPath)) {
|
|
1739
|
-
const archivePath = path.join(fbDir, `pushed-${Date.now()}.jsonl`);
|
|
1740
|
-
fs.renameSync(jsonlPath, archivePath);
|
|
1741
|
-
}
|
|
1742
|
-
} else {
|
|
1743
|
-
log(`error: ${result.error || "unknown"}`);
|
|
1744
|
-
}
|
|
1745
|
-
}
|
|
1746
|
-
|
|
1747
|
-
// --- Models ---
|
|
1748
|
-
function cmdModels(filter) {
|
|
1749
|
-
// Scores from benchmark_models.py (3-task: read/count/review, 2026-04-06)
|
|
1750
|
-
const MODELS = [
|
|
1751
|
-
{ name: "Claude Opus 4.6", tier: "deep", score: 95, cli: "claude", flag: "--model opus" },
|
|
1752
|
-
{ name: "GPT-5.4-mini", tier: "fast", score: 93, cli: "codex", flag: "-m gpt-5.4-mini", tested: true },
|
|
1753
|
-
{ name: "MiniMax M2.7", tier: "balanced", score: 93, cli: "opencode", flag: "-m opencode-go/minimax-m2.7", tested: true },
|
|
1754
|
-
{ name: "Claude Sonnet 4.6", tier: "balanced", score: 90, cli: "claude", flag: "--model sonnet" },
|
|
1755
|
-
{ name: "GPT-5.4", tier: "balanced", score: 90, cli: "codex", flag: "-m gpt-5.4", tested: true },
|
|
1756
|
-
{ name: "Kimi K2.5", tier: "balanced", score: 88, cli: "opencode", flag: "-m opencode-go/kimi-k2.5", tested: true },
|
|
1757
|
-
{ name: "Qwen 3.6+ Free", tier: "free", score: 88, cli: "opencode", flag: "-m opencode/qwen3.6-plus-free", tested: true },
|
|
1758
|
-
{ name: "Gemini 3.1 Pro", tier: "balanced", score: 85, cli: "gemini", flag: "-m gemini-3.1-pro" },
|
|
1759
|
-
{ name: "GPT-5.3 Codex", tier: "deep", score: 83, cli: "codex", flag: "-m gpt-5.3-codex", tested: true },
|
|
1760
|
-
{ name: "GPT-5.3 Spark", tier: "fast", score: 82, cli: "codex", flag: "-m gpt-5.3-codex-spark" },
|
|
1761
|
-
{ name: "Claude Haiku 4.5", tier: "fast", score: 78, cli: "claude", flag: "--model haiku" },
|
|
1762
|
-
{ name: "Gemini 3 Flash", tier: "fast", score: 77, cli: "gemini", flag: "-m gemini-3-flash" },
|
|
1763
|
-
{ name: "Mimo v2 Pro", tier: "fast", score: 74, cli: "opencode", flag: "-m opencode-go/mimo-v2-pro", tested: true },
|
|
1764
|
-
{ name: "GPT-5.4 (opencode)",tier: "fast", score: 74, cli: "opencode", flag: "-m openai/gpt-5.4", tested: true },
|
|
1765
|
-
{ name: "GPT-5.2", tier: "balanced", score: 87, cli: "codex", flag: "-m gpt-5.2", tested: true },
|
|
1766
|
-
{ name: "MiniMax M2.5", tier: "slow", score: 57, cli: "opencode", flag: "-m opencode-go/minimax-m2.5", tested: true },
|
|
1767
|
-
];
|
|
1768
|
-
|
|
1769
|
-
const { execFileSync } = require("child_process");
|
|
1770
|
-
const available = new Set();
|
|
1771
|
-
for (const cli of ["claude", "codex", "opencode", "gemini", "aider"]) {
|
|
1772
|
-
try { execFileSync("/bin/sh", ["-c", `command -v ${cli}`], { stdio: "ignore" }); available.add(cli); } catch {}
|
|
1773
|
-
}
|
|
1774
|
-
|
|
1775
|
-
const isTTY = process.stdout.isTTY;
|
|
1776
|
-
const Y = isTTY ? "\x1b[33m" : "";
|
|
1777
|
-
const G = isTTY ? "\x1b[32m" : "";
|
|
1778
|
-
const DIM = isTTY ? "\x1b[2m" : "";
|
|
1779
|
-
|
|
1780
|
-
let models = [...MODELS].sort((a, b) => b.score - a.score);
|
|
1781
|
-
if (filter === "--fast") models = models.filter(m => m.tier === "fast");
|
|
1782
|
-
if (filter === "--balanced") models = models.filter(m => m.tier === "balanced");
|
|
1783
|
-
if (filter === "--deep") models = models.filter(m => m.tier === "deep");
|
|
1784
|
-
if (filter === "--available") models = models.filter(m => available.has(m.cli));
|
|
1785
|
-
|
|
1786
|
-
const tc = (t) => t === "deep" ? T : t === "balanced" ? G : DIM;
|
|
1787
|
-
console.log(`\n ${T}0dai${R} model ratings — ${models.length} models\n`);
|
|
1788
|
-
console.log(` ${"SCORE".padEnd(6)} ${"MODEL".padEnd(22)} ${"TIER".padEnd(10)} ${"CLI".padEnd(10)} FLAG`);
|
|
1789
|
-
console.log(` ${"-".repeat(64)}`);
|
|
1790
|
-
for (const m of models) {
|
|
1791
|
-
const dim = available.has(m.cli) ? "" : DIM;
|
|
1792
|
-
const mark = m.tested ? ` ${G}✓${R}` : "";
|
|
1793
|
-
console.log(`${dim} ${Y}${String(m.score).padEnd(6)}${R} ${m.name.padEnd(22)} ${tc(m.tier)}${m.tier.padEnd(10)}${R} ${m.cli.padEnd(10)} ${DIM}${m.flag}${R}${mark}${dim ? R : ""}`);
|
|
1794
|
-
}
|
|
1795
|
-
console.log(`\n ${DIM}✓ = swarm-benchmarked | dimmed = CLI not installed${R}`);
|
|
1796
|
-
console.log(` ${DIM}Filter: --fast --balanced --deep --available${R}`);
|
|
1797
|
-
console.log(` ${DIM}Full table: https://0dai.dev/models${R}\n`);
|
|
1798
|
-
}
|
|
1799
|
-
|
|
1800
|
-
// --- Session (local, file-based) ---
|
|
1801
|
-
function cmdSession(target, sub, args) {
|
|
1802
|
-
const sessFile = path.join(target, "ai", "sessions", "active.json");
|
|
1803
|
-
const sessDir = path.dirname(sessFile);
|
|
1804
|
-
|
|
1805
|
-
if (sub === "save") {
|
|
1806
|
-
fs.mkdirSync(sessDir, { recursive: true });
|
|
1807
|
-
const goal = args.find((_, i) => args[i - 1] === "--goal") || "";
|
|
1808
|
-
const summary = args.find((_, i) => args[i - 1] === "--summary") || "";
|
|
1809
|
-
const session = {
|
|
1810
|
-
id: `sess-${Date.now()}`,
|
|
1811
|
-
started: new Date().toISOString(),
|
|
1812
|
-
current_agent: "cli",
|
|
1813
|
-
task: { goal: goal || summary || "active session", status: "in_progress" },
|
|
1814
|
-
handoff_notes: summary,
|
|
1815
|
-
context: { files_touched: [] },
|
|
1816
|
-
};
|
|
1817
|
-
if (fs.existsSync(sessFile)) {
|
|
1818
|
-
const existing = JSON.parse(fs.readFileSync(sessFile, "utf8"));
|
|
1819
|
-
existing.handoff_notes = summary || existing.handoff_notes;
|
|
1820
|
-
if (goal) existing.task.goal = goal;
|
|
1821
|
-
existing.updated = new Date().toISOString();
|
|
1822
|
-
fs.writeFileSync(sessFile, JSON.stringify(existing, null, 2));
|
|
1823
|
-
log("session updated");
|
|
1824
|
-
} else {
|
|
1825
|
-
fs.writeFileSync(sessFile, JSON.stringify(session, null, 2));
|
|
1826
|
-
log(`session started: ${session.id}`);
|
|
1827
|
-
}
|
|
1828
|
-
return;
|
|
1829
|
-
}
|
|
1830
|
-
if (sub === "status") {
|
|
1831
|
-
if (!fs.existsSync(sessFile)) { log("no active session"); return; }
|
|
1832
|
-
const s = JSON.parse(fs.readFileSync(sessFile, "utf8"));
|
|
1833
|
-
log(`session: ${(s.task || {}).goal || "?"}`);
|
|
1834
|
-
console.log(` agent: ${s.current_agent || "?"}`);
|
|
1835
|
-
if (s.handoff_notes) console.log(` handoff: ${s.handoff_notes}`);
|
|
1836
|
-
return;
|
|
1837
|
-
}
|
|
1838
|
-
if (sub === "complete") {
|
|
1839
|
-
if (!fs.existsSync(sessFile)) { log("no active session"); return; }
|
|
1840
|
-
const archiveDir = path.join(target, "ai", "sessions", "archive");
|
|
1841
|
-
fs.mkdirSync(archiveDir, { recursive: true });
|
|
1842
|
-
const s = JSON.parse(fs.readFileSync(sessFile, "utf8"));
|
|
1843
|
-
fs.writeFileSync(path.join(archiveDir, `${s.id || "session"}.json`), JSON.stringify(s, null, 2));
|
|
1844
|
-
fs.unlinkSync(sessFile);
|
|
1845
|
-
log(`session ${s.id} archived`);
|
|
1846
|
-
return;
|
|
1847
|
-
}
|
|
1848
|
-
console.log("Usage: 0dai session [save|status|complete] [--goal '...'] [--summary '...']");
|
|
1849
|
-
}
|
|
1850
|
-
|
|
1851
|
-
// --- Swarm (local, file-based) ---
|
|
1852
|
-
function cmdSwarm(target, sub, args) {
|
|
1853
|
-
const swarmDir = path.join(target, "ai", "swarm");
|
|
1854
|
-
const queueDir = path.join(swarmDir, "queue");
|
|
1855
|
-
|
|
1856
|
-
if (sub === "status") {
|
|
1857
|
-
const count = (d) => { try { return fs.readdirSync(d).filter(f => f.endsWith(".json")).length; } catch { return 0; } };
|
|
1858
|
-
const q = count(path.join(swarmDir, "queue"));
|
|
1859
|
-
const a = count(path.join(swarmDir, "active"));
|
|
1860
|
-
const d = count(path.join(swarmDir, "done"));
|
|
1861
|
-
log(`swarm: ${q} queued, ${a} active, ${d} done`);
|
|
1862
|
-
return;
|
|
1863
|
-
}
|
|
1864
|
-
if (sub === "add" || sub === "delegate") {
|
|
1865
|
-
fs.mkdirSync(queueDir, { recursive: true });
|
|
1866
|
-
const task = args.find((_, i) => args[i - 1] === "--task") || "untitled";
|
|
1867
|
-
const forAgent = args.find((_, i) => ["--for", "--to"].includes(args[i - 1])) || "any";
|
|
1868
|
-
const id = `swarm-${Date.now()}`;
|
|
1869
|
-
const t = { id, title: task, assigned_to: forAgent, status: "pending", created_at: new Date().toISOString(), created_by: "cli" };
|
|
1870
|
-
fs.writeFileSync(path.join(queueDir, `${id}.json`), JSON.stringify(t, null, 2));
|
|
1871
|
-
log(`task created: ${id} → ${forAgent}`);
|
|
1872
|
-
return;
|
|
1873
|
-
}
|
|
1874
|
-
if (sub === "webhook") {
|
|
1875
|
-
const webhooksFile = path.join(swarmDir, "webhooks.json");
|
|
1876
|
-
const loadHooks = () => { try { return JSON.parse(fs.readFileSync(webhooksFile, "utf8")); } catch { return []; } };
|
|
1877
|
-
const saveHooks = (h) => { fs.mkdirSync(swarmDir, { recursive: true }); fs.writeFileSync(webhooksFile, JSON.stringify(h, null, 2)); };
|
|
1878
|
-
const action = args[2] || "";
|
|
1879
|
-
|
|
1880
|
-
if (action === "add") {
|
|
1881
|
-
const url = args[3] || args.find((_, i) => args[i-1] === "--url");
|
|
1882
|
-
const event = args.find((_, i) => args[i-1] === "--event") || "all";
|
|
1883
|
-
const secret = args.find((_, i) => args[i-1] === "--secret") || "";
|
|
1884
|
-
if (!url || !url.startsWith("http")) { log("Usage: 0dai swarm webhook add <url> [--event task_done|task_failed|all] [--secret TOKEN]"); return; }
|
|
1885
|
-
// MED: SSRF protection — block internal/metadata endpoints
|
|
1886
|
-
try {
|
|
1887
|
-
const u = new URL(url);
|
|
1888
|
-
const host = u.hostname;
|
|
1889
|
-
const BLOCKED = /^(127\.|10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.|169\.254\.|::1$|fc00:|fe80:|localhost$|0\.0\.0\.0$)/i;
|
|
1890
|
-
if (BLOCKED.test(host) || host === "metadata.google.internal") {
|
|
1891
|
-
log(`rejected: ${host} is a private/internal address (SSRF protection)`);
|
|
1892
|
-
return;
|
|
1893
|
-
}
|
|
1894
|
-
if (u.protocol !== "https:" && u.protocol !== "http:") {
|
|
1895
|
-
log(`rejected: only http/https allowed, got ${u.protocol}`);
|
|
1896
|
-
return;
|
|
1897
|
-
}
|
|
1898
|
-
} catch {
|
|
1899
|
-
log(`invalid URL: ${url}`);
|
|
1900
|
-
return;
|
|
1901
|
-
}
|
|
1902
|
-
const hooks = loadHooks();
|
|
1903
|
-
if (hooks.find(h => h.url === url)) { log(`already registered: ${url}`); return; }
|
|
1904
|
-
hooks.push({ url, event, secret: secret || undefined, added_at: new Date().toISOString() });
|
|
1905
|
-
saveHooks(hooks);
|
|
1906
|
-
log(`webhook added: ${url} (event: ${event})`);
|
|
1907
|
-
return;
|
|
1908
|
-
}
|
|
1909
|
-
if (action === "list") {
|
|
1910
|
-
const hooks = loadHooks();
|
|
1911
|
-
if (hooks.length === 0) { log("no webhooks registered. Use: 0dai swarm webhook add <url>"); return; }
|
|
1912
|
-
console.log(`\n ${T}Registered webhooks${R}\n`);
|
|
1913
|
-
hooks.forEach((h, i) => {
|
|
1914
|
-
console.log(` ${i+1}. ${h.url}`);
|
|
1915
|
-
console.log(` ${D}event: ${h.event} added: ${h.added_at?.slice(0,10)}${R}`);
|
|
1916
|
-
});
|
|
1917
|
-
console.log();
|
|
1918
|
-
return;
|
|
1919
|
-
}
|
|
1920
|
-
if (action === "remove") {
|
|
1921
|
-
const url = args[3] || "";
|
|
1922
|
-
if (!url) { log("Usage: 0dai swarm webhook remove <url>"); return; }
|
|
1923
|
-
const hooks = loadHooks().filter(h => h.url !== url);
|
|
1924
|
-
saveHooks(hooks);
|
|
1925
|
-
log(`removed: ${url}`);
|
|
1926
|
-
return;
|
|
1927
|
-
}
|
|
1928
|
-
if (action === "test") {
|
|
1929
|
-
const url = args[3] || loadHooks()[0]?.url;
|
|
1930
|
-
if (!url) { log("Usage: 0dai swarm webhook test <url>"); return; }
|
|
1931
|
-
const payload = JSON.stringify({ event: "test", task_id: "test-ping", title: "Webhook test from 0dai", status: "done", timestamp: new Date().toISOString() });
|
|
1932
|
-
const req = https.request(url, { method: "POST", headers: { "Content-Type": "application/json", "User-Agent": "0dai-swarm/1.0", "Content-Length": Buffer.byteLength(payload) } }, (res) => {
|
|
1933
|
-
log(`test sent to ${url} → HTTP ${res.statusCode}`);
|
|
1934
|
-
});
|
|
1935
|
-
req.on("error", (e) => log(`test failed: ${e.message}`));
|
|
1936
|
-
req.setTimeout(5000, () => { req.destroy(); log("test timed out"); });
|
|
1937
|
-
req.write(payload);
|
|
1938
|
-
req.end();
|
|
1939
|
-
return;
|
|
1940
|
-
}
|
|
1941
|
-
console.log("Usage: 0dai swarm webhook [add|list|remove|test] <url> [--event all|task_done|task_failed] [--secret TOKEN]");
|
|
1942
|
-
return;
|
|
1943
|
-
}
|
|
1944
|
-
if (sub === "budget") {
|
|
1945
|
-
const budgetFile = path.join(swarmDir, "budget.json");
|
|
1946
|
-
if (!fs.existsSync(budgetFile)) { log("no budget data yet"); return; }
|
|
1947
|
-
const b = JSON.parse(fs.readFileSync(budgetFile, "utf8"));
|
|
1948
|
-
const B2 = process.stdout.isTTY ? "\x1b[1m" : "";
|
|
1949
|
-
const R2 = process.stdout.isTTY ? "\x1b[0m" : "";
|
|
1950
|
-
const D2 = process.stdout.isTTY ? "\x1b[2m" : "";
|
|
1951
|
-
const G2 = process.stdout.isTTY ? "\x1b[32m" : "";
|
|
1952
|
-
const W2 = process.stdout.isTTY ? "\x1b[33m" : "";
|
|
1953
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
1954
|
-
const sessionKey = process.env.ODAI_SESSION_ID ||
|
|
1955
|
-
new Date().toISOString().slice(0, 13).replace("T", "-");
|
|
1956
|
-
const dailySpent = b.daily?.[today] || 0;
|
|
1957
|
-
const totalSpent = b.total_spent || 0;
|
|
1958
|
-
const sess = b.sessions?.[sessionKey];
|
|
1959
|
-
// Tier distribution across all tasks
|
|
1960
|
-
const tierCount = { fast: 0, balanced: 0, deep: 0 };
|
|
1961
|
-
for (const t of Object.values(b.tasks || {})) {
|
|
1962
|
-
if (t.tier && tierCount[t.tier] !== undefined) tierCount[t.tier]++;
|
|
1963
|
-
}
|
|
1964
|
-
const tieredTotal = tierCount.fast + tierCount.balanced + tierCount.deep;
|
|
1965
|
-
console.log(`\n ${B2}Swarm Budget${R2}`);
|
|
1966
|
-
if (sess && sess.total_cost > 0) {
|
|
1967
|
-
const taskCount = (sess.tasks || []).length;
|
|
1968
|
-
const avgCost = taskCount > 0 ? (sess.total_cost / taskCount).toFixed(4) : "0";
|
|
1969
|
-
console.log(` ${B2}This session${R2} $${sess.total_cost.toFixed(4)} · ${taskCount} tasks · avg $${avgCost}/task`);
|
|
1970
|
-
if (sess.tiers) {
|
|
1971
|
-
const tiers = Object.entries(sess.tiers).filter(([, n]) => n > 0).map(([t, n]) => `${n}×${t}`).join(" ");
|
|
1972
|
-
if (tiers) console.log(` ${D2}Tiers ${tiers}${R2}`);
|
|
1973
|
-
}
|
|
1974
|
-
} else {
|
|
1975
|
-
console.log(` ${D2}This session no tracked spend${R2}`);
|
|
1976
|
-
}
|
|
1977
|
-
if (dailySpent > 0) {
|
|
1978
|
-
const dailyLimit = parseFloat(process.env.ODAI_DAILY_BUDGET || "5");
|
|
1979
|
-
const pct = Math.round((dailySpent / dailyLimit) * 100);
|
|
1980
|
-
const bar = "█".repeat(Math.round(pct / 5)).padEnd(20, "░");
|
|
1981
|
-
const col = pct < 50 ? G2 : pct < 80 ? W2 : "\x1b[31m";
|
|
1982
|
-
console.log(` ${B2}Today${R2} ${col}$${dailySpent.toFixed(4)}${R2} / $${dailyLimit.toFixed(2)} ${D2}${bar} ${pct}%${R2}`);
|
|
1983
|
-
}
|
|
1984
|
-
console.log(` ${B2}All time${R2} ${D2}$${totalSpent.toFixed(4)} (${Object.keys(b.tasks || {}).length} tasks)${R2}`);
|
|
1985
|
-
if (tieredTotal > 0) {
|
|
1986
|
-
const fastPct = Math.round((tierCount.fast / tieredTotal) * 100);
|
|
1987
|
-
console.log(` ${D2}Model routing ${tierCount.fast}×fast ${tierCount.balanced}×balanced ${tierCount.deep}×deep (${fastPct}% cheap)${R2}`);
|
|
1988
|
-
}
|
|
1989
|
-
// Recent sessions (last 5)
|
|
1990
|
-
const sessions = Object.entries(b.sessions || {})
|
|
1991
|
-
.sort(([a], [bb]) => bb.localeCompare(a))
|
|
1992
|
-
.slice(0, 5);
|
|
1993
|
-
if (sessions.length > 1) {
|
|
1994
|
-
console.log(` ${D2}Recent sessions:${R2}`);
|
|
1995
|
-
for (const [key, s] of sessions) {
|
|
1996
|
-
const tasks = (s.tasks || []).length;
|
|
1997
|
-
console.log(` ${D2}${key} $${(s.total_cost || 0).toFixed(4)} · ${tasks} tasks${R2}`);
|
|
1998
|
-
}
|
|
1999
|
-
}
|
|
2000
|
-
console.log();
|
|
2001
|
-
return;
|
|
2002
|
-
}
|
|
2003
|
-
console.log("Usage: 0dai swarm [status|add|delegate|budget] [--task '...'] [--to agent]");
|
|
2004
|
-
}
|
|
2005
|
-
|
|
2006
|
-
// --- Feedback (local + API push) ---
|
|
2007
|
-
async function cmdFeedback(target, sub, args) {
|
|
2008
|
-
const fbDir = path.join(target, "ai", "feedback");
|
|
2009
|
-
|
|
2010
|
-
if (sub === "push") {
|
|
2011
|
-
return cmdFeedbackPush(target);
|
|
2012
|
-
}
|
|
2013
|
-
if (sub === "log") {
|
|
2014
|
-
const type = args.find((_, i) => args[i - 1] === "--type") || "suggestion";
|
|
2015
|
-
const detail = args.find((_, i) => args[i - 1] === "--detail") || "";
|
|
2016
|
-
if (!detail) { console.log("Usage: 0dai feedback log --type bug|suggestion|friction|positive --detail '...'"); return; }
|
|
2017
|
-
fs.mkdirSync(fbDir, { recursive: true });
|
|
2018
|
-
const entry = JSON.stringify({ ts: new Date().toISOString(), type, detail, agent: "cli" });
|
|
2019
|
-
fs.appendFileSync(path.join(fbDir, "operational.jsonl"), entry + "\n");
|
|
2020
|
-
log(`logged: [${type}] ${detail.slice(0, 60)}`);
|
|
2021
|
-
return;
|
|
2022
|
-
}
|
|
2023
|
-
if (sub === "list") {
|
|
2024
|
-
try {
|
|
2025
|
-
const files = fs.readdirSync(fbDir).filter(f => f.endsWith("-report.json"));
|
|
2026
|
-
if (!files.length) { log("no reports"); return; }
|
|
2027
|
-
for (const f of files) {
|
|
2028
|
-
try {
|
|
2029
|
-
const d = JSON.parse(fs.readFileSync(path.join(fbDir, f), "utf8"));
|
|
2030
|
-
console.log(` ${f}: ${d.verdict || "?"} (${d.project || "?"})`);
|
|
2031
|
-
} catch {}
|
|
2032
|
-
}
|
|
2033
|
-
} catch { log("no feedback directory"); }
|
|
2034
|
-
return;
|
|
2035
|
-
}
|
|
2036
|
-
console.log("Usage: 0dai feedback [push|log|list] [--type ...] [--detail '...']");
|
|
2037
|
-
}
|
|
2038
|
-
|
|
2039
|
-
function cmdWatch(target, args) {
|
|
2040
|
-
const isTTY = process.stdout.isTTY;
|
|
2041
|
-
const B2 = isTTY ? "\x1b[1m" : "";
|
|
2042
|
-
const DIM = isTTY ? "\x1b[2m" : "";
|
|
2043
|
-
const G = isTTY ? "\x1b[32m" : "";
|
|
2044
|
-
const Y = isTTY ? "\x1b[33m" : "";
|
|
2045
|
-
const C = isTTY ? "\x1b[36m" : "";
|
|
2046
|
-
const M = isTTY ? "\x1b[35m" : "";
|
|
2047
|
-
const R2 = isTTY ? "\x1b[0m" : "";
|
|
2048
|
-
|
|
2049
|
-
const swarmDir = path.join(target, "ai", "swarm");
|
|
2050
|
-
const interval = parseInt(args.find((_, i) => args[i - 1] === "--interval") || "3", 10) * 1000;
|
|
2051
|
-
|
|
2052
|
-
function readDir(dir) {
|
|
2053
|
-
try {
|
|
2054
|
-
return fs.readdirSync(dir)
|
|
2055
|
-
.filter(f => f.endsWith(".json"))
|
|
2056
|
-
.map(f => { try { return JSON.parse(fs.readFileSync(path.join(dir, f), "utf8")); } catch { return null; } })
|
|
2057
|
-
.filter(Boolean);
|
|
2058
|
-
} catch { return []; }
|
|
2059
|
-
}
|
|
2060
|
-
|
|
2061
|
-
function ago(ts) {
|
|
2062
|
-
if (!ts) return "—";
|
|
2063
|
-
const s = Math.floor((Date.now() - new Date(ts).getTime()) / 1000);
|
|
2064
|
-
if (s < 60) return `${s}s`;
|
|
2065
|
-
if (s < 3600) return `${Math.floor(s / 60)}m`;
|
|
2066
|
-
return `${Math.floor(s / 3600)}h`;
|
|
2067
|
-
}
|
|
2068
|
-
|
|
2069
|
-
function tierColor(tier) {
|
|
2070
|
-
if (tier === "deep") return M;
|
|
2071
|
-
if (tier === "fast") return C;
|
|
2072
|
-
return G;
|
|
2073
|
-
}
|
|
2074
|
-
|
|
2075
|
-
function statusColor(status) {
|
|
2076
|
-
if (status === "done") return G;
|
|
2077
|
-
if (status === "active" || status === "running") return Y;
|
|
2078
|
-
if (status === "failed") return "\x1b[31m";
|
|
2079
|
-
return DIM;
|
|
2080
|
-
}
|
|
2081
|
-
|
|
2082
|
-
function render() {
|
|
2083
|
-
const queue = readDir(path.join(swarmDir, "queue"));
|
|
2084
|
-
const active = readDir(path.join(swarmDir, "active"));
|
|
2085
|
-
const done = readDir(path.join(swarmDir, "done")).sort(
|
|
2086
|
-
(a, b) => new Date(b.completed_at || b.created_at) - new Date(a.completed_at || a.created_at)
|
|
2087
|
-
).slice(0, 8);
|
|
2088
|
-
|
|
2089
|
-
const lines = [];
|
|
2090
|
-
const w = process.stdout.columns || 100;
|
|
2091
|
-
const sep = DIM + "─".repeat(Math.min(w, 96)) + R2;
|
|
2092
|
-
|
|
2093
|
-
lines.push(`\n ${B2}${T}0dai watch${R2} ${DIM}${new Date().toLocaleTimeString()} (q to quit)${R2}`);
|
|
2094
|
-
lines.push(sep);
|
|
2095
|
-
|
|
2096
|
-
const header = ` ${"STATUS".padEnd(9)} ${"AGENT".padEnd(10)} ${"TIER".padEnd(9)} ${"AGE".padEnd(6)} TITLE`;
|
|
2097
|
-
lines.push(DIM + header + R2);
|
|
2098
|
-
lines.push(sep);
|
|
2099
|
-
|
|
2100
|
-
const printTask = (t, statusOverride) => {
|
|
2101
|
-
const status = statusOverride || t.status || "queued";
|
|
2102
|
-
const sc = statusColor(status);
|
|
2103
|
-
const tc = tierColor(t.model_tier);
|
|
2104
|
-
const title = (t.title || "—").slice(0, Math.max(20, w - 42));
|
|
2105
|
-
const age = ago(t.created_at);
|
|
2106
|
-
lines.push(
|
|
2107
|
-
` ${sc}${status.padEnd(9)}${R2} ` +
|
|
2108
|
-
`${DIM}${(t.assigned_to || "—").padEnd(10)}${R2} ` +
|
|
2109
|
-
`${tc}${(t.model_tier || "—").padEnd(9)}${R2} ` +
|
|
2110
|
-
`${DIM}${age.padEnd(6)}${R2} ${title}`
|
|
2111
|
-
);
|
|
2112
|
-
};
|
|
2113
|
-
|
|
2114
|
-
if (active.length) {
|
|
2115
|
-
active.forEach(t => printTask(t, "active"));
|
|
2116
|
-
}
|
|
2117
|
-
if (queue.length) {
|
|
2118
|
-
queue.forEach(t => printTask(t, "queued"));
|
|
2119
|
-
}
|
|
2120
|
-
if (!active.length && !queue.length) {
|
|
2121
|
-
lines.push(` ${DIM}No tasks in queue or active.${R2}`);
|
|
2122
|
-
}
|
|
2123
|
-
|
|
2124
|
-
lines.push(sep);
|
|
2125
|
-
if (done.length) {
|
|
2126
|
-
lines.push(` ${DIM}Recently done:${R2}`);
|
|
2127
|
-
done.forEach(t => printTask(t, "done"));
|
|
2128
|
-
}
|
|
2129
|
-
|
|
2130
|
-
lines.push(`\n ${DIM}queue: ${queue.length} active: ${active.length} done (total): ${readDir(path.join(swarmDir, "done")).length}${R2}\n`);
|
|
2131
|
-
|
|
2132
|
-
if (isTTY) process.stdout.write("\x1b[2J\x1b[H");
|
|
2133
|
-
console.log(lines.join("\n"));
|
|
2134
|
-
}
|
|
2135
|
-
|
|
2136
|
-
render();
|
|
2137
|
-
const timer = setInterval(render, interval);
|
|
2138
|
-
|
|
2139
|
-
// Exit on 'q' or Ctrl+C
|
|
2140
|
-
if (isTTY && process.stdin.setRawMode) {
|
|
2141
|
-
process.stdin.setRawMode(true);
|
|
2142
|
-
process.stdin.resume();
|
|
2143
|
-
process.stdin.once("data", (key) => {
|
|
2144
|
-
clearInterval(timer);
|
|
2145
|
-
process.stdin.setRawMode(false);
|
|
2146
|
-
process.exit(0);
|
|
2147
|
-
});
|
|
2148
|
-
}
|
|
2149
|
-
process.on("SIGINT", () => { clearInterval(timer); process.exit(0); });
|
|
2150
|
-
}
|
|
4
|
+
/**
|
|
5
|
+
* 0dai CLI entry point — routing only.
|
|
6
|
+
*
|
|
7
|
+
* All command implementations live in lib/commands/*.js.
|
|
8
|
+
* Shared config and utilities live in lib/shared.js.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const shared = require("../lib/shared");
|
|
12
|
+
const { T, R, D, log, VERSION, fs, path, spawnSync, findRepoScript, checkVersion } = shared;
|
|
13
|
+
|
|
14
|
+
// --- Command imports ---
|
|
15
|
+
const { cmdAuthLogin, cmdAuthLogout, cmdRedeem, cmdAuthStatus, cmdActivateFree, cmdActivateStatus } = require("../lib/commands/auth");
|
|
16
|
+
const { cmdInit, cmdSync } = require("../lib/commands/init");
|
|
17
|
+
const { cmdDetect } = require("../lib/commands/detect");
|
|
18
|
+
const { cmdAudit } = require("../lib/commands/audit");
|
|
19
|
+
const { cmdDoctor } = require("../lib/commands/doctor");
|
|
20
|
+
const { cmdValidate } = require("../lib/commands/validate");
|
|
21
|
+
const { cmdUpdate } = require("../lib/commands/update");
|
|
22
|
+
const { cmdReflect } = require("../lib/commands/reflect");
|
|
23
|
+
const { cmdMetrics } = require("../lib/commands/metrics");
|
|
24
|
+
const { cmdStatus } = require("../lib/commands/status");
|
|
25
|
+
const { cmdPortfolio } = require("../lib/commands/portfolio");
|
|
26
|
+
const { cmdRun } = require("../lib/commands/run");
|
|
27
|
+
const { cmdWatch } = require("../lib/commands/watch");
|
|
28
|
+
const { cmdModels } = require("../lib/commands/models");
|
|
29
|
+
const { cmdSession } = require("../lib/commands/session");
|
|
30
|
+
const { cmdSwarm } = require("../lib/commands/swarm");
|
|
31
|
+
const { cmdFeedback, cmdFeedbackPush } = require("../lib/commands/feedback");
|
|
32
|
+
const { cmdGraph } = require("../lib/commands/graph");
|
|
33
|
+
const { cmdReport } = require("../lib/commands/report");
|
|
34
|
+
const { cmdExperience } = require("../lib/commands/experience");
|
|
2151
35
|
|
|
2152
36
|
async function main() {
|
|
2153
37
|
const args = process.argv.slice(2);
|
|
@@ -2161,14 +45,59 @@ async function main() {
|
|
|
2161
45
|
// Non-blocking version check (runs in background, once per day)
|
|
2162
46
|
checkVersion();
|
|
2163
47
|
|
|
48
|
+
// Track first run for time-to-init telemetry
|
|
49
|
+
try { require("../lib/onboarding").trackFirstRun(target); } catch {}
|
|
50
|
+
|
|
51
|
+
// First-run wizard prompt for commands that need ai/
|
|
52
|
+
if (["status", "doctor", "sync", "swarm", "detect", "validate", "reflect", "metrics", "experience", "graph", "session", "report"].includes(cmd)) {
|
|
53
|
+
try {
|
|
54
|
+
const { maybeWizard } = require("../lib/wizard");
|
|
55
|
+
const handled = await maybeWizard(target, cmd);
|
|
56
|
+
if (handled) return;
|
|
57
|
+
} catch {}
|
|
58
|
+
}
|
|
59
|
+
|
|
2164
60
|
switch (cmd) {
|
|
61
|
+
case "quickstart": {
|
|
62
|
+
const ob = require("../lib/onboarding");
|
|
63
|
+
await ob.cmdQuickstart(target, { cmdDoctor, cmdStatus, cmdInit: (t, a) => cmdInit(t, a || []), log, ensureAuthenticated: shared.makeEnsureAuthenticated(cmdAuthLogin) });
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
2165
66
|
case "run": await cmdRun(args[1] || "", target, args.slice(2)); break;
|
|
2166
67
|
case "watch": cmdWatch(target, args.slice(1)); break;
|
|
2167
68
|
case "audit": cmdAudit(target); break;
|
|
69
|
+
case "security": {
|
|
70
|
+
const secScript = findRepoScript(target, "scan_secrets.py");
|
|
71
|
+
if (!secScript) { log("secret scanner unavailable"); break; }
|
|
72
|
+
const fwd = [secScript, "--target", target];
|
|
73
|
+
if (args.includes("--json")) fwd.push("--json");
|
|
74
|
+
if (args.includes("--fix")) fwd.push("--fix");
|
|
75
|
+
const sr = spawnSync("python3", fwd, { stdio: "inherit" });
|
|
76
|
+
if (typeof sr.status === "number") process.exit(sr.status);
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
2168
79
|
case "init": await cmdInit(target, args); break;
|
|
2169
80
|
case "sync": await cmdSync(target, args); break;
|
|
2170
81
|
case "detect": await cmdDetect(target); break;
|
|
2171
|
-
case "doctor":
|
|
82
|
+
case "doctor":
|
|
83
|
+
cmdDoctor(target);
|
|
84
|
+
if (args.includes("--drift")) {
|
|
85
|
+
const ds = findRepoScript(target, "drift_detector.py");
|
|
86
|
+
if (ds) spawnSync("python3", [ds, "report", "--target", target], { stdio: "inherit" });
|
|
87
|
+
}
|
|
88
|
+
break;
|
|
89
|
+
case "drift": {
|
|
90
|
+
const ds = findRepoScript(target, "drift_detector.py");
|
|
91
|
+
if (!ds) { log("drift detector unavailable"); break; }
|
|
92
|
+
if (sub === "accept" && args[2]) {
|
|
93
|
+
spawnSync("python3", [ds, "accept", args[2], "--target", target], { stdio: "inherit" });
|
|
94
|
+
} else if (sub === "show" && args[2]) {
|
|
95
|
+
spawnSync("python3", [ds, "show", args[2], "--target", target], { stdio: "inherit" });
|
|
96
|
+
} else {
|
|
97
|
+
spawnSync("python3", [ds, "report", "--target", target], { stdio: "inherit" });
|
|
98
|
+
}
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
2172
101
|
case "validate": cmdValidate(target); break;
|
|
2173
102
|
case "reflect": cmdReflect(target, args); break;
|
|
2174
103
|
case "update": cmdUpdate(args); break;
|
|
@@ -2179,13 +108,19 @@ async function main() {
|
|
|
2179
108
|
if (sub === "login") await cmdAuthLogin();
|
|
2180
109
|
else if (sub === "logout") cmdAuthLogout();
|
|
2181
110
|
else if (sub === "status") await cmdAuthStatus();
|
|
2182
|
-
else
|
|
2183
|
-
|
|
2184
|
-
|
|
111
|
+
else console.log("Usage: 0dai auth [login|logout|status]");
|
|
112
|
+
break;
|
|
113
|
+
case "activate":
|
|
114
|
+
if (sub === "free" || !sub) await cmdActivateFree();
|
|
115
|
+
else if (sub === "status") await cmdActivateStatus();
|
|
116
|
+
else console.log("Usage: 0dai activate [free|status]");
|
|
2185
117
|
break;
|
|
2186
118
|
case "session": cmdSession(target, sub, args); break;
|
|
2187
119
|
case "swarm": cmdSwarm(target, sub, args); break;
|
|
2188
120
|
case "feedback": await cmdFeedback(target, sub, args); break;
|
|
121
|
+
case "report": cmdReport(target, sub, args); break;
|
|
122
|
+
case "experience": cmdExperience(target, sub, args); break;
|
|
123
|
+
case "graph": await cmdGraph(target, sub, args); break;
|
|
2189
124
|
case "models": cmdModels(sub || args[1]); break;
|
|
2190
125
|
case "redeem": await cmdRedeem(sub || args[1]); break;
|
|
2191
126
|
case "terminal": case "term":
|
|
@@ -2207,7 +142,6 @@ async function main() {
|
|
|
2207
142
|
log(`unknown tool: ${tool}. Available: ${Object.keys(TOOL_CMDS).join(", ")}`);
|
|
2208
143
|
break;
|
|
2209
144
|
}
|
|
2210
|
-
// Pass initial prompt if provided after --
|
|
2211
145
|
const dashIdx = args.indexOf("--");
|
|
2212
146
|
const prompt = dashIdx >= 0 ? args.slice(dashIdx + 1).join(" ") : "";
|
|
2213
147
|
const spawnArgs = [...toolConfig.args];
|
|
@@ -2236,7 +170,6 @@ async function main() {
|
|
|
2236
170
|
const prefix = args[1] || "";
|
|
2237
171
|
if (!prefix) { log("usage: 0dai terminal kill <session-id-prefix>"); break; }
|
|
2238
172
|
const all = Array.from(sm.sessions || []);
|
|
2239
|
-
// Kill by prefix match
|
|
2240
173
|
let found = false;
|
|
2241
174
|
for (const [id] of all) {
|
|
2242
175
|
if (id.startsWith(prefix)) { sm.kill(id); log(`killed ${id.slice(0, 8)}`); found = true; }
|
|
@@ -2274,11 +207,21 @@ async function main() {
|
|
|
2274
207
|
console.log(" swarm webhook list Show registered webhooks");
|
|
2275
208
|
console.log(" swarm webhook test Send test ping to a webhook URL");
|
|
2276
209
|
console.log(" feedback push Send feedback to 0dai");
|
|
210
|
+
console.log(" report preview Preview privacy-safe project report");
|
|
211
|
+
console.log(" report push Send report to 0dai (with offline queue)");
|
|
212
|
+
console.log(" report status Show last report, queue, and auto-report status");
|
|
213
|
+
console.log(" experience list Show recent structured experience events");
|
|
214
|
+
console.log(" experience stats Show success and cost stats by agent/model/type");
|
|
215
|
+
console.log(" graph push Upload local graph to server (Pro: edges, Free: nodes)");
|
|
216
|
+
console.log(" graph pull Download server graph and merge locally");
|
|
217
|
+
console.log(" graph status Show local graph stats and sync state");
|
|
2277
218
|
console.log(" models Show model ratings (--fast/--balanced/--deep/--available)");
|
|
2278
219
|
console.log(" terminal Launch interactive agent session");
|
|
2279
220
|
console.log(" auth login Authenticate (device code flow)");
|
|
2280
221
|
console.log(" auth logout Remove credentials");
|
|
2281
222
|
console.log(" auth status Show account and usage");
|
|
223
|
+
console.log(" activate free Claim free activation license");
|
|
224
|
+
console.log(" activate status Show activation and bound-project status");
|
|
2282
225
|
console.log(" redeem <CODE> Redeem a plan upgrade code");
|
|
2283
226
|
console.log(" --version\n");
|
|
2284
227
|
console.log("https://0dai.dev");
|