@0dai-dev/cli 2.8.0 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/0dai.js +593 -25
- package/package.json +1 -1
package/bin/0dai.js
CHANGED
|
@@ -7,7 +7,7 @@ const fs = require("fs");
|
|
|
7
7
|
const path = require("path");
|
|
8
8
|
const os = require("os");
|
|
9
9
|
|
|
10
|
-
const VERSION = "
|
|
10
|
+
const VERSION = "3.0.0";
|
|
11
11
|
const API_URL = process.env.ODAI_API_URL || "https://api.0dai.dev";
|
|
12
12
|
const T = process.stdout.isTTY ? "\x1b[38;2;45;212;168m" : ""; // teal
|
|
13
13
|
const R = process.stdout.isTTY ? "\x1b[0m" : ""; // reset
|
|
@@ -16,6 +16,7 @@ const log = (msg) => console.log(`${T}[0dai]${R} ${msg}`);
|
|
|
16
16
|
const CONFIG_DIR = path.join(os.homedir(), ".0dai");
|
|
17
17
|
const AUTH_FILE = path.join(CONFIG_DIR, "auth.json");
|
|
18
18
|
const VERSION_CHECK_FILE = path.join(CONFIG_DIR, ".version_check");
|
|
19
|
+
const PROJECTS_FILE = path.join(CONFIG_DIR, "projects.json");
|
|
19
20
|
|
|
20
21
|
const MANIFEST_FILES = [
|
|
21
22
|
"package.json", "go.mod", "pyproject.toml", "requirements.txt",
|
|
@@ -53,6 +54,183 @@ function deviceFingerprint() {
|
|
|
53
54
|
return crypto.createHash("sha256").update(parts.join(":")).digest("hex").slice(0, 32);
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
function registerProject(projectPath, name, stack) {
|
|
58
|
+
try {
|
|
59
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
60
|
+
let projects = [];
|
|
61
|
+
try { projects = JSON.parse(fs.readFileSync(PROJECTS_FILE, "utf8")).projects || []; } catch {}
|
|
62
|
+
const abs = path.resolve(projectPath);
|
|
63
|
+
const idx = projects.findIndex(p => p.path === abs);
|
|
64
|
+
const entry = { path: abs, name: name || path.basename(abs), stack: stack || "?", last_seen: new Date().toISOString() };
|
|
65
|
+
if (idx >= 0) projects[idx] = entry;
|
|
66
|
+
else projects.unshift(entry);
|
|
67
|
+
fs.writeFileSync(PROJECTS_FILE, JSON.stringify({ projects: projects.slice(0, 50) }, null, 2));
|
|
68
|
+
} catch {}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function cmdPortfolio() {
|
|
72
|
+
let projects = [];
|
|
73
|
+
try { projects = JSON.parse(fs.readFileSync(PROJECTS_FILE, "utf8")).projects || []; } catch {}
|
|
74
|
+
|
|
75
|
+
if (!projects.length) {
|
|
76
|
+
log(`no projects registered yet`);
|
|
77
|
+
console.log(` Run ${D}0dai init${R} in a project to start tracking it.`);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const rows = [];
|
|
82
|
+
let totalSessions = 0, totalScore = 0, scored = 0;
|
|
83
|
+
|
|
84
|
+
for (const p of projects) {
|
|
85
|
+
if (!fs.existsSync(p.path)) continue;
|
|
86
|
+
|
|
87
|
+
let sessions = 0, lastSession = null, agentMap = {};
|
|
88
|
+
try {
|
|
89
|
+
const stats = JSON.parse(fs.readFileSync(path.join(p.path, "ai", "feedback", ".usage_stats.json"), "utf8"));
|
|
90
|
+
sessions = stats.total_sessions || 0;
|
|
91
|
+
lastSession = stats.last_session || null;
|
|
92
|
+
agentMap = stats.agents || {};
|
|
93
|
+
} catch {}
|
|
94
|
+
|
|
95
|
+
let name = p.name, stack = p.stack;
|
|
96
|
+
try {
|
|
97
|
+
const disc = JSON.parse(fs.readFileSync(path.join(p.path, "ai", "manifest", "discovery.json"), "utf8"));
|
|
98
|
+
name = disc.project_name || name;
|
|
99
|
+
stack = disc.stack || stack;
|
|
100
|
+
} catch {}
|
|
101
|
+
|
|
102
|
+
// Effectiveness score (mirrors metrics command)
|
|
103
|
+
let score = 0;
|
|
104
|
+
if (sessions > 0) {
|
|
105
|
+
score += Math.min(sessions * 5, 35);
|
|
106
|
+
let done = 0;
|
|
107
|
+
try { done = fs.readdirSync(path.join(p.path, "ai", "swarm", "done")).filter(f => f.endsWith(".json")).length; } catch {}
|
|
108
|
+
if (done > 0) score += Math.min(done * 6, 30);
|
|
109
|
+
try {
|
|
110
|
+
const hasFb = fs.readdirSync(path.join(p.path, "ai", "feedback")).some(f => f.endsWith("-report.json"));
|
|
111
|
+
if (hasFb) score += 20;
|
|
112
|
+
} catch {}
|
|
113
|
+
const layerFiles = ["ai/manifest/discovery.json", "ai/manifest/commands.yaml", "ai/playbooks/quick-start.md"];
|
|
114
|
+
score += Math.round(layerFiles.filter(f => fs.existsSync(path.join(p.path, f))).length / layerFiles.length * 15);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
totalSessions += sessions;
|
|
118
|
+
if (sessions > 0) { totalScore += score; scored++; }
|
|
119
|
+
|
|
120
|
+
const agentList = Object.keys(agentMap).join("·") || "—";
|
|
121
|
+
|
|
122
|
+
let ago = "never";
|
|
123
|
+
if (lastSession) {
|
|
124
|
+
const h = Math.floor((Date.now() - new Date(lastSession).getTime()) / 3600000);
|
|
125
|
+
const d = Math.floor(h / 24);
|
|
126
|
+
if (h < 1) ago = "<1h ago";
|
|
127
|
+
else if (h < 24) ago = `${h}h ago`;
|
|
128
|
+
else if (d < 7) ago = `${d}d ago`;
|
|
129
|
+
else ago = `${Math.floor(d / 7)}w ago`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
rows.push({ name, stack, score: sessions > 0 ? score : null, sessions, agents: agentList, ago });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!rows.length) {
|
|
136
|
+
log("no projects found (paths may have moved)");
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const nameW = Math.min(Math.max(...rows.map(r => r.name.length), 4), 28);
|
|
141
|
+
const stackW = Math.min(Math.max(...rows.map(r => r.stack.length), 5), 16);
|
|
142
|
+
|
|
143
|
+
console.log(`\n ${T}Portfolio${R} — ${rows.length} project${rows.length === 1 ? "" : "s"}\n`);
|
|
144
|
+
for (const r of rows) {
|
|
145
|
+
const nm = r.name.slice(0, nameW).padEnd(nameW);
|
|
146
|
+
const st = r.stack.slice(0, stackW).padEnd(stackW);
|
|
147
|
+
const sc = r.score !== null ? `score ${String(r.score).padStart(3)}` : " ";
|
|
148
|
+
const se = `${String(r.sessions).padStart(2)} session${r.sessions === 1 ? " " : "s"}`;
|
|
149
|
+
console.log(` ${T}${nm}${R} ${D}${st}${R} ${sc} ${se} ${r.agents.padEnd(14)} ${D}${r.ago}${R}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (totalSessions > 0) {
|
|
153
|
+
const avg = scored > 0 ? Math.round(totalScore / scored) : 0;
|
|
154
|
+
const stacks = [...new Set(rows.map(r => r.stack))].length;
|
|
155
|
+
console.log(`\n ${D}${"─".repeat(70)}`);
|
|
156
|
+
console.log(` Total: ${totalSessions} sessions ${stacks} stack${stacks === 1 ? "" : "s"} avg effectiveness: ${avg}${R}\n`);
|
|
157
|
+
} else {
|
|
158
|
+
console.log(`\n ${D}Tip: run '0dai init' in your projects to start tracking sessions.${R}\n`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function cmdRun(goal, target, args = []) {
|
|
163
|
+
if (!goal) {
|
|
164
|
+
console.log(`Usage: 0dai run <goal> [--dry-run] [--agent claude|codex|gemini]`);
|
|
165
|
+
console.log(` Example: 0dai run "add dark mode to settings page"`);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const dryRun = args.includes("--dry-run");
|
|
170
|
+
const agentIdx = args.indexOf("--agent");
|
|
171
|
+
const agentOverride = agentIdx >= 0 ? args[agentIdx + 1] : null;
|
|
172
|
+
|
|
173
|
+
// Read project context
|
|
174
|
+
let stack = "generic", agents = ["claude"], commands = {};
|
|
175
|
+
try {
|
|
176
|
+
const disc = JSON.parse(fs.readFileSync(path.join(target, "ai", "manifest", "discovery.json"), "utf8"));
|
|
177
|
+
stack = disc.stack || "generic";
|
|
178
|
+
agents = disc.selected_agents || ["claude"];
|
|
179
|
+
} catch {}
|
|
180
|
+
|
|
181
|
+
if (!dryRun) process.stdout.write(`${T}[0dai]${R} decomposing goal...`);
|
|
182
|
+
const result = await apiCall("/v1/run", { goal, context: { stack, agents, commands } });
|
|
183
|
+
if (!dryRun) process.stdout.write("\r" + " ".repeat(40) + "\r");
|
|
184
|
+
|
|
185
|
+
if (result.error) { log(`error: ${result.error}`); return; }
|
|
186
|
+
|
|
187
|
+
const tasks = result.tasks || [];
|
|
188
|
+
if (!tasks.length) { log("no tasks returned"); return; }
|
|
189
|
+
|
|
190
|
+
console.log(`\n ${T}Goal:${R} ${goal}`);
|
|
191
|
+
console.log(` Decomposed into ${tasks.length} task${tasks.length === 1 ? "" : "s"}:\n`);
|
|
192
|
+
|
|
193
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
194
|
+
const t = tasks[i];
|
|
195
|
+
const agent = agentOverride || t.assigned_to;
|
|
196
|
+
console.log(` ${T}${i + 1}.${R} ${t.title}`);
|
|
197
|
+
console.log(` ${D}→ ${agent} [${t.model_tier}]${t.description ? " " + t.description : ""}${R}`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (dryRun) {
|
|
201
|
+
console.log(`\n ${D}[dry-run] would create ${tasks.length} task(s) in swarm queue${R}\n`);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Create queue entries
|
|
206
|
+
const queueDir = path.join(target, "ai", "swarm", "queue");
|
|
207
|
+
try { fs.mkdirSync(queueDir, { recursive: true }); } catch {}
|
|
208
|
+
|
|
209
|
+
const created = [];
|
|
210
|
+
for (const t of tasks) {
|
|
211
|
+
const ts = Date.now();
|
|
212
|
+
const rand = Math.random().toString(36).slice(2, 6);
|
|
213
|
+
const id = `run-${ts}-${rand}`;
|
|
214
|
+
const entry = {
|
|
215
|
+
id,
|
|
216
|
+
title: t.title,
|
|
217
|
+
description: t.description || "",
|
|
218
|
+
assigned_to: agentOverride || t.assigned_to,
|
|
219
|
+
model_tier: t.model_tier,
|
|
220
|
+
created_by: "run",
|
|
221
|
+
created_at: new Date().toISOString(),
|
|
222
|
+
context: { goal },
|
|
223
|
+
};
|
|
224
|
+
try {
|
|
225
|
+
fs.writeFileSync(path.join(queueDir, `${id}.json`), JSON.stringify(entry, null, 2));
|
|
226
|
+
created.push(id);
|
|
227
|
+
} catch (e) { log(`warn: could not write task ${id}: ${e.message}`); }
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
console.log(`\n ${T}✓${R} ${created.length} task${created.length === 1 ? "" : "s"} added to swarm queue`);
|
|
231
|
+
console.log(` ${D}Monitor: 0dai watch | Queue: 0dai swarm status${R}\n`);
|
|
232
|
+
}
|
|
233
|
+
|
|
56
234
|
function apiCall(endpoint, data) {
|
|
57
235
|
return new Promise((resolve, reject) => {
|
|
58
236
|
const url = new URL(endpoint, API_URL);
|
|
@@ -126,27 +304,67 @@ function collectMetadata(target) {
|
|
|
126
304
|
return { projectFiles, fileContents, clis };
|
|
127
305
|
}
|
|
128
306
|
|
|
307
|
+
// Fields in settings.json that belong to the user, not to 0dai
|
|
308
|
+
const SETTINGS_PRESERVE_FIELDS = ["model", "permissionMode", "effortLevel"];
|
|
309
|
+
|
|
310
|
+
function mergeSettingsJson(existing, incoming) {
|
|
311
|
+
try {
|
|
312
|
+
const base = JSON.parse(incoming);
|
|
313
|
+
const user = JSON.parse(existing);
|
|
314
|
+
// Preserve user-owned fields
|
|
315
|
+
for (const field of SETTINGS_PRESERVE_FIELDS) {
|
|
316
|
+
if (field in user && user[field] !== base[field]) {
|
|
317
|
+
base[field] = user[field];
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return JSON.stringify(base, null, 2) + "\n";
|
|
321
|
+
} catch { return incoming; }
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function mergeAgentsMd(existing, incoming) {
|
|
325
|
+
// If user marked as unmanaged, don't touch
|
|
326
|
+
if (existing.includes("managed: false")) return existing;
|
|
327
|
+
// If managed, update with new content but preserve user additions after managed block
|
|
328
|
+
return incoming;
|
|
329
|
+
}
|
|
330
|
+
|
|
129
331
|
function writeFiles(target, files) {
|
|
130
|
-
let created = 0, updated = 0, unchanged = 0;
|
|
332
|
+
let created = 0, updated = 0, unchanged = 0, merged = 0;
|
|
131
333
|
for (const [rel, content] of Object.entries(files)) {
|
|
132
334
|
const p = path.join(target, rel);
|
|
133
335
|
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
336
|
+
|
|
337
|
+
let finalContent = content;
|
|
338
|
+
|
|
339
|
+
// Smart merge for specific files
|
|
340
|
+
if (fs.existsSync(p)) {
|
|
341
|
+
const existing = fs.readFileSync(p, "utf8");
|
|
342
|
+
if (existing === content) { unchanged++; continue; }
|
|
343
|
+
|
|
344
|
+
if (rel.endsWith("settings.json")) {
|
|
345
|
+
finalContent = mergeSettingsJson(existing, content);
|
|
346
|
+
merged++;
|
|
347
|
+
} else if (rel === "AGENTS.md" && existing.includes("managed: false")) {
|
|
348
|
+
unchanged++; // User owns this file, skip
|
|
137
349
|
continue;
|
|
350
|
+
} else {
|
|
351
|
+
updated++;
|
|
138
352
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
|
|
353
|
+
} else {
|
|
354
|
+
created++;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
fs.writeFileSync(p, finalContent, "utf8");
|
|
143
358
|
}
|
|
144
|
-
|
|
359
|
+
const parts = [`${created} created`, `${updated} updated`, `${unchanged} unchanged`];
|
|
360
|
+
if (merged) parts.push(`${merged} merged`);
|
|
361
|
+
log(parts.join(", "));
|
|
145
362
|
return created + updated;
|
|
146
363
|
}
|
|
147
364
|
|
|
148
365
|
async function cmdInit(target, args = []) {
|
|
149
366
|
const dryRun = args.includes("--dry-run");
|
|
367
|
+
const minimal = args.includes("--minimal");
|
|
150
368
|
|
|
151
369
|
if (fs.existsSync(path.join(target, "ai", "VERSION"))) {
|
|
152
370
|
const v = fs.readFileSync(path.join(target, "ai", "VERSION"), "utf8").trim();
|
|
@@ -169,6 +387,7 @@ async function cmdInit(target, args = []) {
|
|
|
169
387
|
file_contents: fileContents,
|
|
170
388
|
available_clis: clis,
|
|
171
389
|
dry_run: dryRun,
|
|
390
|
+
minimal: minimal,
|
|
172
391
|
});
|
|
173
392
|
|
|
174
393
|
if (result.error) {
|
|
@@ -199,23 +418,28 @@ async function cmdInit(target, args = []) {
|
|
|
199
418
|
if (!text.includes(".0dai")) fs.appendFileSync(gi, "\n.0dai/\n");
|
|
200
419
|
} catch {}
|
|
201
420
|
|
|
421
|
+
// Register in global portfolio
|
|
422
|
+
registerProject(target, path.basename(target), result.stack);
|
|
423
|
+
|
|
202
424
|
log(`initialized (${result.file_count || "?"} files)`);
|
|
203
425
|
console.log(" skills: /build /review /status /feedback /bugfix /delegate");
|
|
204
426
|
|
|
205
|
-
//
|
|
427
|
+
// Next steps — guide user to first value
|
|
428
|
+
console.log(`\n ${T}Next steps:${R}`);
|
|
429
|
+
console.log(` ${D}1.${R} Check health: ${D}0dai doctor${R}`);
|
|
430
|
+
console.log(` ${D}2.${R} Try delegation: ${D}0dai run "write tests for auth"${R}`);
|
|
431
|
+
console.log(` ${D}3.${R} Open dashboard: ${D}https://0dai.dev/dashboard${R}`);
|
|
432
|
+
|
|
433
|
+
// Send anonymous usage ping
|
|
206
434
|
apiCall("/v1/feedback", { report: {
|
|
207
435
|
stack_detected: result.stack || "?", _auto: true, _plan: result.plan || "trial",
|
|
208
436
|
_cli_version: VERSION, _files_generated: result.file_count || 0,
|
|
209
437
|
}}).catch(() => {});
|
|
210
|
-
|
|
211
|
-
// Encourage feedback
|
|
212
|
-
console.log(`\n ${T}Tip:${R} Send feedback to earn +5 init/day for 7 days:`);
|
|
213
|
-
console.log(` ${D}0dai feedback log --type positive --detail "what worked"${R}`);
|
|
214
|
-
console.log(` ${D}0dai feedback push${R}`);
|
|
215
438
|
}
|
|
216
439
|
|
|
217
440
|
async function cmdSync(target, args = []) {
|
|
218
441
|
const dryRun = args.includes("--dry-run");
|
|
442
|
+
const quiet = args.includes("--quiet") || args.includes("-q");
|
|
219
443
|
|
|
220
444
|
// Quick local check: skip API if already at current version (unless dry-run)
|
|
221
445
|
let version = "unknown";
|
|
@@ -257,7 +481,7 @@ async function cmdSync(target, args = []) {
|
|
|
257
481
|
const result = await apiCall("/v1/sync", {
|
|
258
482
|
ai_version: version, stack, agents: agents.length ? agents : clis,
|
|
259
483
|
current_files: currentFiles, file_contents: fileContents,
|
|
260
|
-
dry_run: dryRun,
|
|
484
|
+
dry_run: dryRun, quiet,
|
|
261
485
|
});
|
|
262
486
|
|
|
263
487
|
if (result.error) { log(`error: ${result.error}`); process.exit(1); }
|
|
@@ -273,8 +497,20 @@ async function cmdSync(target, args = []) {
|
|
|
273
497
|
}
|
|
274
498
|
return;
|
|
275
499
|
}
|
|
276
|
-
|
|
277
|
-
|
|
500
|
+
const changedCount = Object.keys(updated).length;
|
|
501
|
+
if (changedCount) {
|
|
502
|
+
writeFiles(target, updated);
|
|
503
|
+
if (!quiet) {
|
|
504
|
+
for (const f of Object.keys(updated)) console.log(` ~ ${f}`);
|
|
505
|
+
}
|
|
506
|
+
log(`sync: ${changedCount} file(s) updated`);
|
|
507
|
+
console.log(` ${D}Run: 0dai doctor to verify project health${R}`);
|
|
508
|
+
} else {
|
|
509
|
+
log("already up to date");
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Update portfolio registry
|
|
513
|
+
registerProject(target, path.basename(target), stack);
|
|
278
514
|
}
|
|
279
515
|
|
|
280
516
|
async function cmdDetect(target) {
|
|
@@ -543,14 +779,36 @@ function cmdValidate(target) {
|
|
|
543
779
|
for (const f of agentFiles[agent] || []) required.push(f);
|
|
544
780
|
}
|
|
545
781
|
|
|
782
|
+
const FIX_HINTS = {
|
|
783
|
+
"ai/VERSION": "run: 0dai init",
|
|
784
|
+
"ai/VERSION_SCHEMA": "run: 0dai sync",
|
|
785
|
+
"ai/manifest/project.yaml": "run: 0dai init",
|
|
786
|
+
"ai/manifest/discovery.json": "run: 0dai init",
|
|
787
|
+
"ai/manifest/applied-lock.json": "run: 0dai sync",
|
|
788
|
+
"ai/manifest/environment.yaml": "run: 0dai sync",
|
|
789
|
+
"ai/manifest/commands.yaml": "run: 0dai sync",
|
|
790
|
+
"AGENTS.md": "run: 0dai sync",
|
|
791
|
+
".claude/settings.json": "run: 0dai sync",
|
|
792
|
+
".claude/CLAUDE.md": "run: 0dai sync",
|
|
793
|
+
".mcp.json": "run: 0dai sync",
|
|
794
|
+
".codex/config.toml": "install codex, then: 0dai sync",
|
|
795
|
+
"opencode.json": "install opencode, then: 0dai sync",
|
|
796
|
+
};
|
|
797
|
+
|
|
798
|
+
const present = required.filter(f => fs.existsSync(path.join(target, f)));
|
|
546
799
|
const missing = required.filter(f => !fs.existsSync(path.join(target, f)));
|
|
800
|
+
|
|
801
|
+
for (const f of present) console.log(` ${G}✓${R2} ${f}`);
|
|
802
|
+
for (const f of missing) {
|
|
803
|
+
const hint = FIX_HINTS[f] || "run: 0dai sync";
|
|
804
|
+
console.log(` ${E}✗${R2} ${f} ${D2}— ${hint}${R2}`);
|
|
805
|
+
}
|
|
806
|
+
|
|
547
807
|
if (missing.length) {
|
|
548
|
-
log(
|
|
549
|
-
for (const f of missing) console.log(` ${E}✗${R2} ${f}`);
|
|
550
|
-
console.log(`\n ${D2}Run: 0dai sync --target . to fix missing files${R2}`);
|
|
808
|
+
console.log(`\n${E}${missing.length} missing${R2} / ${present.length + missing.length} total`);
|
|
551
809
|
process.exitCode = 1;
|
|
552
810
|
} else {
|
|
553
|
-
log(`${G}validate ok${R2} — all required files present`);
|
|
811
|
+
log(`${G}validate ok${R2} — all ${present.length} required files present`);
|
|
554
812
|
}
|
|
555
813
|
}
|
|
556
814
|
|
|
@@ -693,6 +951,55 @@ function cmdReflect(target, args) {
|
|
|
693
951
|
}
|
|
694
952
|
} catch {}
|
|
695
953
|
|
|
954
|
+
// Insights — learn from patterns
|
|
955
|
+
if (totalDone >= 3) {
|
|
956
|
+
const insights = [];
|
|
957
|
+
// Best agent by completion rate
|
|
958
|
+
const agentEntries = Object.entries(byAgent).map(([a, d]) => {
|
|
959
|
+
const pending = allPendingByAgent[a] || 0;
|
|
960
|
+
const total = d.done + pending;
|
|
961
|
+
return { agent: a, done: d.done, total, rate: total > 0 ? d.done / total : 1 };
|
|
962
|
+
});
|
|
963
|
+
const best = agentEntries.sort((a, b) => b.rate - a.rate)[0];
|
|
964
|
+
if (best && agentEntries.length > 1) {
|
|
965
|
+
insights.push(`${best.agent} has the highest success rate (${Math.round(best.rate * 100)}%)`);
|
|
966
|
+
}
|
|
967
|
+
// Fastest agent by avg elapsed
|
|
968
|
+
try {
|
|
969
|
+
const budget = JSON.parse(fs.readFileSync(path.join(ai, "swarm", "budget.json"), "utf8"));
|
|
970
|
+
const agentElapsed = {};
|
|
971
|
+
const agentCounts = {};
|
|
972
|
+
for (const t of Object.values(budget.tasks || {})) {
|
|
973
|
+
if (t.elapsed > 0) {
|
|
974
|
+
agentElapsed[t.agent] = (agentElapsed[t.agent] || 0) + t.elapsed;
|
|
975
|
+
agentCounts[t.agent] = (agentCounts[t.agent] || 0) + 1;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
let fastest = null, fastestAvg = Infinity;
|
|
979
|
+
for (const [a, total] of Object.entries(agentElapsed)) {
|
|
980
|
+
const avg = total / agentCounts[a];
|
|
981
|
+
if (avg < fastestAvg) { fastest = a; fastestAvg = avg; }
|
|
982
|
+
}
|
|
983
|
+
if (fastest && Object.keys(agentElapsed).length > 1) {
|
|
984
|
+
insights.push(`${fastest} is fastest (avg ${Math.round(fastestAvg)}s per task)`);
|
|
985
|
+
}
|
|
986
|
+
} catch {}
|
|
987
|
+
// Cost efficiency
|
|
988
|
+
if (allDone.length > 0) {
|
|
989
|
+
try {
|
|
990
|
+
const budget = JSON.parse(fs.readFileSync(path.join(ai, "swarm", "budget.json"), "utf8"));
|
|
991
|
+
if (budget.total_spent > 0) {
|
|
992
|
+
const costPerTask = budget.total_spent / allDone.length;
|
|
993
|
+
insights.push(`avg cost: $${costPerTask.toFixed(4)}/task`);
|
|
994
|
+
}
|
|
995
|
+
} catch {}
|
|
996
|
+
}
|
|
997
|
+
if (insights.length) {
|
|
998
|
+
console.log(`\n ${B}Insights:${R2}`);
|
|
999
|
+
for (const i of insights) console.log(` ${G}→${R2} ${i}`);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
696
1003
|
// Remaining blockers
|
|
697
1004
|
if (allQueue.length) {
|
|
698
1005
|
console.log(`\n ${B}Blockers / queue:${R2}`);
|
|
@@ -712,6 +1019,146 @@ function cmdReflect(target, args) {
|
|
|
712
1019
|
console.log();
|
|
713
1020
|
}
|
|
714
1021
|
|
|
1022
|
+
function cmdMetrics(target) {
|
|
1023
|
+
const ai = path.join(target, "ai");
|
|
1024
|
+
const G = "\x1b[32m", W = "\x1b[33m", R2 = "\x1b[0m", D = "\x1b[2m",
|
|
1025
|
+
B = "\x1b[34m", T = "\x1b[36m", M = "\x1b[35m";
|
|
1026
|
+
|
|
1027
|
+
// --- Data sources ---
|
|
1028
|
+
let stats = {}, budget = {}, discovery = {};
|
|
1029
|
+
try { stats = JSON.parse(fs.readFileSync(path.join(ai, "feedback", ".usage_stats.json"), "utf8")); } catch {}
|
|
1030
|
+
try { budget = JSON.parse(fs.readFileSync(path.join(ai, "swarm", "budget.json"), "utf8")); } catch {}
|
|
1031
|
+
try { discovery = JSON.parse(fs.readFileSync(path.join(ai, "manifest", "discovery.json"), "utf8")); } catch {}
|
|
1032
|
+
|
|
1033
|
+
const projectName = discovery.project_name || path.basename(target);
|
|
1034
|
+
const stack = discovery.stack || "?";
|
|
1035
|
+
const totalSessions = stats.total_sessions || 0;
|
|
1036
|
+
const agentBreakdown = stats.agents || {};
|
|
1037
|
+
const layerInfo = stats.layer || {};
|
|
1038
|
+
const lastSession = stats.last_session ? new Date(stats.last_session) : null;
|
|
1039
|
+
const layerVersion = stats.version || "?";
|
|
1040
|
+
|
|
1041
|
+
// Swarm tasks: count done files
|
|
1042
|
+
let tasksDone = 0, tasksQueue = 0;
|
|
1043
|
+
const doneDir = path.join(ai, "swarm", "done");
|
|
1044
|
+
const queueDir = path.join(ai, "swarm", "queue");
|
|
1045
|
+
try { tasksDone = fs.readdirSync(doneDir).filter(f => f.endsWith(".json")).length; } catch {}
|
|
1046
|
+
try { tasksQueue = fs.readdirSync(queueDir).filter(f => f.endsWith(".json")).length; } catch {}
|
|
1047
|
+
|
|
1048
|
+
// Activity: count events from activity.jsonl
|
|
1049
|
+
let activityEvents = 0;
|
|
1050
|
+
try {
|
|
1051
|
+
const lines = fs.readFileSync(path.join(ai, "swarm", "activity.jsonl"), "utf8").trim().split("\n").filter(Boolean);
|
|
1052
|
+
activityEvents = lines.length;
|
|
1053
|
+
} catch {}
|
|
1054
|
+
|
|
1055
|
+
// Feedback submissions
|
|
1056
|
+
let feedbackCount = layerInfo.feedback_reports || 0;
|
|
1057
|
+
|
|
1058
|
+
// Budget totals
|
|
1059
|
+
const totalSpent = budget.total_spent || 0;
|
|
1060
|
+
const sessionsWithBudget = Object.keys(budget.sessions || {}).length;
|
|
1061
|
+
|
|
1062
|
+
// --- Effectiveness score (0-100) ---
|
|
1063
|
+
let score = 0, scoreNotes = [];
|
|
1064
|
+
|
|
1065
|
+
// Sessions depth: 1 = tried, 3 = habit forming, 7 = regular use
|
|
1066
|
+
const sessionScore = Math.min(Math.floor((totalSessions / 7) * 35), 35);
|
|
1067
|
+
score += sessionScore;
|
|
1068
|
+
if (totalSessions === 0) scoreNotes.push("not started");
|
|
1069
|
+
else if (totalSessions === 1) scoreNotes.push("first session");
|
|
1070
|
+
else if (totalSessions < 3) scoreNotes.push("early");
|
|
1071
|
+
else if (totalSessions < 7) scoreNotes.push("habit forming");
|
|
1072
|
+
else scoreNotes.push("regular use");
|
|
1073
|
+
|
|
1074
|
+
// Delegation: did they delegate to swarm?
|
|
1075
|
+
const delegationScore = tasksDone > 0 ? Math.min(Math.floor((tasksDone / 5) * 30), 30) : 0;
|
|
1076
|
+
score += delegationScore;
|
|
1077
|
+
if (tasksDone > 0) scoreNotes.push(`${tasksDone} tasks delegated`);
|
|
1078
|
+
|
|
1079
|
+
// Feedback: submitted = trust signal
|
|
1080
|
+
const feedbackScore = feedbackCount > 0 ? 20 : 0;
|
|
1081
|
+
score += feedbackScore;
|
|
1082
|
+
if (feedbackCount > 0) scoreNotes.push("feedback submitted");
|
|
1083
|
+
|
|
1084
|
+
// Layer completeness: has playbooks and commands?
|
|
1085
|
+
const layerScore = (layerInfo.playbooks && layerInfo.commands) ? 15 : (layerInfo.commands ? 8 : 0);
|
|
1086
|
+
score += layerScore;
|
|
1087
|
+
|
|
1088
|
+
const scoreColor = score >= 70 ? G : score >= 40 ? W : "\x1b[31m";
|
|
1089
|
+
const bar = "█".repeat(Math.round(score / 5)).padEnd(20, "░");
|
|
1090
|
+
|
|
1091
|
+
// --- Output ---
|
|
1092
|
+
console.log(`\n ${T}Metrics${R2} ${D}${projectName} · ${stack} · ai v${layerVersion}${R2}\n`);
|
|
1093
|
+
|
|
1094
|
+
// Effectiveness score
|
|
1095
|
+
console.log(` ${B}Effectiveness${R2}`);
|
|
1096
|
+
console.log(` ${scoreColor}${score}/100${R2} ${D}${bar}${R2}`);
|
|
1097
|
+
if (scoreNotes.length) console.log(` ${D}${scoreNotes.join(" · ")}${R2}`);
|
|
1098
|
+
|
|
1099
|
+
// Adoption funnel
|
|
1100
|
+
console.log(`\n ${B}Adoption funnel${R2}`);
|
|
1101
|
+
const funnelStep = (label, value, done, hint) => {
|
|
1102
|
+
const icon = done ? `${G}✓${R2}` : `${D}○${R2}`;
|
|
1103
|
+
const val = value !== null ? ` ${D}${value}${R2}` : "";
|
|
1104
|
+
const h = !done && hint ? ` ${D}← ${hint}${R2}` : "";
|
|
1105
|
+
console.log(` ${icon} ${label}${val}${h}`);
|
|
1106
|
+
};
|
|
1107
|
+
funnelStep("Initialized", `ai/ v${layerVersion}`, true);
|
|
1108
|
+
funnelStep("Returned (>1 session)", `${totalSessions} total`, totalSessions > 1, "run 0dai reflect after each session");
|
|
1109
|
+
funnelStep("Used swarm delegation", tasksDone > 0 ? `${tasksDone} tasks done` : null, tasksDone > 0, "try: 0dai swarm add --task '...' --to codex");
|
|
1110
|
+
funnelStep("Submitted feedback", feedbackCount > 0 ? `${feedbackCount} reports` : null, feedbackCount > 0, "0dai feedback log + push");
|
|
1111
|
+
|
|
1112
|
+
// Session stats
|
|
1113
|
+
if (totalSessions > 0) {
|
|
1114
|
+
console.log(`\n ${B}Sessions${R2}`);
|
|
1115
|
+
console.log(` Total ${totalSessions}`);
|
|
1116
|
+
if (lastSession) {
|
|
1117
|
+
const daysAgo = Math.floor((Date.now() - lastSession.getTime()) / 86400000);
|
|
1118
|
+
const when = daysAgo === 0 ? "today" : daysAgo === 1 ? "yesterday" : `${daysAgo}d ago`;
|
|
1119
|
+
console.log(` Last ${when}`);
|
|
1120
|
+
}
|
|
1121
|
+
const agentEntries = Object.entries(agentBreakdown).sort((a, b) => b[1] - a[1]);
|
|
1122
|
+
if (agentEntries.length) {
|
|
1123
|
+
console.log(` Agents ${agentEntries.map(([a, n]) => `${a}: ${n}`).join(" ")}`);
|
|
1124
|
+
}
|
|
1125
|
+
if (sessionsWithBudget > 0 && totalSpent > 0) {
|
|
1126
|
+
console.log(` Cost $${totalSpent.toFixed(4)} total ${D}(${sessionsWithBudget} sessions tracked)${R2}`);
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// Delegation stats
|
|
1131
|
+
if (tasksDone > 0 || tasksQueue > 0) {
|
|
1132
|
+
console.log(`\n ${B}Delegation${R2}`);
|
|
1133
|
+
if (tasksDone > 0) console.log(` Done ${G}${tasksDone}${R2}`);
|
|
1134
|
+
if (tasksQueue > 0) console.log(` Queue ${W}${tasksQueue}${R2}`);
|
|
1135
|
+
if (activityEvents > 0) console.log(` Events ${activityEvents}`);
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// Layer health
|
|
1139
|
+
console.log(`\n ${B}ai/ layer${R2}`);
|
|
1140
|
+
const checks = [
|
|
1141
|
+
["commands.yaml", layerInfo.commands],
|
|
1142
|
+
["playbooks", layerInfo.playbooks],
|
|
1143
|
+
["personas", layerInfo.personas],
|
|
1144
|
+
["session roaming", layerInfo.session_active],
|
|
1145
|
+
["swarm queue", (layerInfo.swarm_queue || 0) > 0],
|
|
1146
|
+
];
|
|
1147
|
+
for (const [label, ok] of checks) {
|
|
1148
|
+
const icon = ok ? `${G}✓${R2}` : `${D}—${R2}`;
|
|
1149
|
+
console.log(` ${icon} ${label}`);
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
// Next suggested action
|
|
1153
|
+
console.log(`\n ${B}Next${R2}`);
|
|
1154
|
+
if (totalSessions === 0) console.log(` ${D}Start a Claude Code session — session_start hook will print project context${R2}`);
|
|
1155
|
+
else if (tasksDone === 0) console.log(` ${D}Try delegating a task: 0dai swarm add --task "write tests for auth module" --to codex${R2}`);
|
|
1156
|
+
else if (feedbackCount === 0) console.log(` ${D}Submit feedback: 0dai feedback log --type positive --detail "what worked"${R2}`);
|
|
1157
|
+
else console.log(` ${D}Score ${score}/100 — keep delegating and submitting feedback${R2}`);
|
|
1158
|
+
|
|
1159
|
+
console.log();
|
|
1160
|
+
}
|
|
1161
|
+
|
|
715
1162
|
function cmdStatus(target) {
|
|
716
1163
|
const ai = path.join(target, "ai");
|
|
717
1164
|
let v = "?", stack = "?";
|
|
@@ -1225,6 +1672,119 @@ async function cmdFeedback(target, sub, args) {
|
|
|
1225
1672
|
console.log("Usage: 0dai feedback [push|log|list] [--type ...] [--detail '...']");
|
|
1226
1673
|
}
|
|
1227
1674
|
|
|
1675
|
+
function cmdWatch(target, args) {
|
|
1676
|
+
const isTTY = process.stdout.isTTY;
|
|
1677
|
+
const B2 = isTTY ? "\x1b[1m" : "";
|
|
1678
|
+
const DIM = isTTY ? "\x1b[2m" : "";
|
|
1679
|
+
const G = isTTY ? "\x1b[32m" : "";
|
|
1680
|
+
const Y = isTTY ? "\x1b[33m" : "";
|
|
1681
|
+
const C = isTTY ? "\x1b[36m" : "";
|
|
1682
|
+
const M = isTTY ? "\x1b[35m" : "";
|
|
1683
|
+
const R2 = isTTY ? "\x1b[0m" : "";
|
|
1684
|
+
|
|
1685
|
+
const swarmDir = path.join(target, "ai", "swarm");
|
|
1686
|
+
const interval = parseInt(args.find((_, i) => args[i - 1] === "--interval") || "3", 10) * 1000;
|
|
1687
|
+
|
|
1688
|
+
function readDir(dir) {
|
|
1689
|
+
try {
|
|
1690
|
+
return fs.readdirSync(dir)
|
|
1691
|
+
.filter(f => f.endsWith(".json"))
|
|
1692
|
+
.map(f => { try { return JSON.parse(fs.readFileSync(path.join(dir, f), "utf8")); } catch { return null; } })
|
|
1693
|
+
.filter(Boolean);
|
|
1694
|
+
} catch { return []; }
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
function ago(ts) {
|
|
1698
|
+
if (!ts) return "—";
|
|
1699
|
+
const s = Math.floor((Date.now() - new Date(ts).getTime()) / 1000);
|
|
1700
|
+
if (s < 60) return `${s}s`;
|
|
1701
|
+
if (s < 3600) return `${Math.floor(s / 60)}m`;
|
|
1702
|
+
return `${Math.floor(s / 3600)}h`;
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
function tierColor(tier) {
|
|
1706
|
+
if (tier === "deep") return M;
|
|
1707
|
+
if (tier === "fast") return C;
|
|
1708
|
+
return G;
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
function statusColor(status) {
|
|
1712
|
+
if (status === "done") return G;
|
|
1713
|
+
if (status === "active" || status === "running") return Y;
|
|
1714
|
+
if (status === "failed") return "\x1b[31m";
|
|
1715
|
+
return DIM;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
function render() {
|
|
1719
|
+
const queue = readDir(path.join(swarmDir, "queue"));
|
|
1720
|
+
const active = readDir(path.join(swarmDir, "active"));
|
|
1721
|
+
const done = readDir(path.join(swarmDir, "done")).sort(
|
|
1722
|
+
(a, b) => new Date(b.completed_at || b.created_at) - new Date(a.completed_at || a.created_at)
|
|
1723
|
+
).slice(0, 8);
|
|
1724
|
+
|
|
1725
|
+
const lines = [];
|
|
1726
|
+
const w = process.stdout.columns || 100;
|
|
1727
|
+
const sep = DIM + "─".repeat(Math.min(w, 96)) + R2;
|
|
1728
|
+
|
|
1729
|
+
lines.push(`\n ${B2}${T}0dai watch${R2} ${DIM}${new Date().toLocaleTimeString()} (q to quit)${R2}`);
|
|
1730
|
+
lines.push(sep);
|
|
1731
|
+
|
|
1732
|
+
const header = ` ${"STATUS".padEnd(9)} ${"AGENT".padEnd(10)} ${"TIER".padEnd(9)} ${"AGE".padEnd(6)} TITLE`;
|
|
1733
|
+
lines.push(DIM + header + R2);
|
|
1734
|
+
lines.push(sep);
|
|
1735
|
+
|
|
1736
|
+
const printTask = (t, statusOverride) => {
|
|
1737
|
+
const status = statusOverride || t.status || "queued";
|
|
1738
|
+
const sc = statusColor(status);
|
|
1739
|
+
const tc = tierColor(t.model_tier);
|
|
1740
|
+
const title = (t.title || "—").slice(0, Math.max(20, w - 42));
|
|
1741
|
+
const age = ago(t.created_at);
|
|
1742
|
+
lines.push(
|
|
1743
|
+
` ${sc}${status.padEnd(9)}${R2} ` +
|
|
1744
|
+
`${DIM}${(t.assigned_to || "—").padEnd(10)}${R2} ` +
|
|
1745
|
+
`${tc}${(t.model_tier || "—").padEnd(9)}${R2} ` +
|
|
1746
|
+
`${DIM}${age.padEnd(6)}${R2} ${title}`
|
|
1747
|
+
);
|
|
1748
|
+
};
|
|
1749
|
+
|
|
1750
|
+
if (active.length) {
|
|
1751
|
+
active.forEach(t => printTask(t, "active"));
|
|
1752
|
+
}
|
|
1753
|
+
if (queue.length) {
|
|
1754
|
+
queue.forEach(t => printTask(t, "queued"));
|
|
1755
|
+
}
|
|
1756
|
+
if (!active.length && !queue.length) {
|
|
1757
|
+
lines.push(` ${DIM}No tasks in queue or active.${R2}`);
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
lines.push(sep);
|
|
1761
|
+
if (done.length) {
|
|
1762
|
+
lines.push(` ${DIM}Recently done:${R2}`);
|
|
1763
|
+
done.forEach(t => printTask(t, "done"));
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
lines.push(`\n ${DIM}queue: ${queue.length} active: ${active.length} done (total): ${readDir(path.join(swarmDir, "done")).length}${R2}\n`);
|
|
1767
|
+
|
|
1768
|
+
if (isTTY) process.stdout.write("\x1b[2J\x1b[H");
|
|
1769
|
+
console.log(lines.join("\n"));
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
render();
|
|
1773
|
+
const timer = setInterval(render, interval);
|
|
1774
|
+
|
|
1775
|
+
// Exit on 'q' or Ctrl+C
|
|
1776
|
+
if (isTTY && process.stdin.setRawMode) {
|
|
1777
|
+
process.stdin.setRawMode(true);
|
|
1778
|
+
process.stdin.resume();
|
|
1779
|
+
process.stdin.once("data", (key) => {
|
|
1780
|
+
clearInterval(timer);
|
|
1781
|
+
process.stdin.setRawMode(false);
|
|
1782
|
+
process.exit(0);
|
|
1783
|
+
});
|
|
1784
|
+
}
|
|
1785
|
+
process.on("SIGINT", () => { clearInterval(timer); process.exit(0); });
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1228
1788
|
async function main() {
|
|
1229
1789
|
const args = process.argv.slice(2);
|
|
1230
1790
|
let target = process.cwd();
|
|
@@ -1238,6 +1798,8 @@ async function main() {
|
|
|
1238
1798
|
checkVersion();
|
|
1239
1799
|
|
|
1240
1800
|
switch (cmd) {
|
|
1801
|
+
case "run": await cmdRun(args[1] || "", target, args.slice(2)); break;
|
|
1802
|
+
case "watch": cmdWatch(target, args.slice(1)); break;
|
|
1241
1803
|
case "audit": cmdAudit(target); break;
|
|
1242
1804
|
case "init": await cmdInit(target, args); break;
|
|
1243
1805
|
case "sync": await cmdSync(target, args); break;
|
|
@@ -1245,6 +1807,8 @@ async function main() {
|
|
|
1245
1807
|
case "doctor": cmdDoctor(target); break;
|
|
1246
1808
|
case "validate": cmdValidate(target); break;
|
|
1247
1809
|
case "reflect": cmdReflect(target, args); break;
|
|
1810
|
+
case "metrics": cmdMetrics(target); break;
|
|
1811
|
+
case "portfolio": cmdPortfolio(); break;
|
|
1248
1812
|
case "status": cmdStatus(target); break;
|
|
1249
1813
|
case "auth":
|
|
1250
1814
|
if (sub === "login") await cmdAuthLogin();
|
|
@@ -1284,13 +1848,17 @@ async function main() {
|
|
|
1284
1848
|
case "help": case "--help": case "-h":
|
|
1285
1849
|
console.log(`\n ${T}0dai${R} v${VERSION} — One config for 5 AI agent CLIs\n`);
|
|
1286
1850
|
console.log("Commands:");
|
|
1851
|
+
console.log(" run <goal> AI-decompose goal → swarm tasks (auto-routed) [--dry-run]");
|
|
1852
|
+
console.log(" watch Live task monitor: queue, active, recently done [--interval N]");
|
|
1287
1853
|
console.log(" audit Scan ai/ and agent configs for leaked secrets");
|
|
1288
|
-
console.log(" init Initialize ai/ layer (via API) [--dry-run]");
|
|
1289
|
-
console.log(" sync Update ai/ layer (via API) [--dry-run]");
|
|
1854
|
+
console.log(" init Initialize ai/ layer (via API) [--dry-run] [--minimal]");
|
|
1855
|
+
console.log(" sync Update ai/ layer (via API) [--dry-run] [--quiet]");
|
|
1290
1856
|
console.log(" detect Show detected stack");
|
|
1291
1857
|
console.log(" doctor Check health + credentials checklist");
|
|
1292
1858
|
console.log(" validate Validate ai/ layer completeness");
|
|
1293
1859
|
console.log(" reflect Session reflection: delivered, delegation rate, blockers");
|
|
1860
|
+
console.log(" metrics Effectiveness score: adoption funnel, sessions, delegation");
|
|
1861
|
+
console.log(" portfolio All tracked projects: score, sessions, agents, last activity");
|
|
1294
1862
|
console.log(" status Show maturity, swarm, session");
|
|
1295
1863
|
console.log(" session save Save session for roaming");
|
|
1296
1864
|
console.log(" swarm status Task queue & delegation");
|