@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.
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ const shared = require("../shared");
3
+ const { log, T, R, D, fs, path, apiCall } = shared;
4
+
5
+ async function cmdFeedbackPush(target) {
6
+ const fbDir = path.join(target, "ai", "feedback");
7
+ const items = [];
8
+
9
+ // Collect from report JSON files
10
+ try {
11
+ for (const f of fs.readdirSync(fbDir)) {
12
+ if (f.endsWith("-report.json") || (f.endsWith(".json") && f.match(/^\d{8}/))) {
13
+ try {
14
+ const d = JSON.parse(fs.readFileSync(path.join(fbDir, f), "utf8"));
15
+ if (d.project || d.verdict) items.push({ type: "report", data: d, file: f });
16
+ } catch {}
17
+ }
18
+ }
19
+ } catch {}
20
+
21
+ // Collect from operational.jsonl (feedback log entries)
22
+ const jsonlPath = path.join(fbDir, "operational.jsonl");
23
+ try {
24
+ if (fs.existsSync(jsonlPath)) {
25
+ const lines = fs.readFileSync(jsonlPath, "utf8").trim().split("\n").filter(Boolean);
26
+ for (const line of lines) {
27
+ try { items.push({ type: "log", data: JSON.parse(line) }); } catch {}
28
+ }
29
+ }
30
+ } catch {}
31
+
32
+ if (!items.length) {
33
+ log("no feedback found");
34
+ console.log(` ${D}Log feedback first: 0dai feedback log --type suggestion --detail '...'${R}`);
35
+ return;
36
+ }
37
+
38
+ // Push all items
39
+ const report = {
40
+ project: path.basename(target),
41
+ entries: items.map(i => i.data),
42
+ count: items.length,
43
+ submitted_at: new Date().toISOString(),
44
+ };
45
+ log(`pushing ${items.length} feedback item(s)...`);
46
+ const result = await apiCall("/v1/feedback", { report });
47
+ if (result.received) {
48
+ log(`received${result.issue ? `: ${result.issue}` : ""}`);
49
+ if (result.bonus) log(`${T}bonus:${R} ${result.bonus}`);
50
+ // Archive pushed entries
51
+ if (fs.existsSync(jsonlPath)) {
52
+ const archivePath = path.join(fbDir, `pushed-${Date.now()}.jsonl`);
53
+ fs.renameSync(jsonlPath, archivePath);
54
+ }
55
+ } else {
56
+ log(`error: ${result.error || "unknown"}`);
57
+ }
58
+ }
59
+
60
+ async function cmdFeedback(target, sub, args) {
61
+ const fbDir = path.join(target, "ai", "feedback");
62
+
63
+ if (sub === "push") {
64
+ return cmdFeedbackPush(target);
65
+ }
66
+ if (sub === "log") {
67
+ const type = args.find((_, i) => args[i - 1] === "--type") || "suggestion";
68
+ const detail = args.find((_, i) => args[i - 1] === "--detail") || "";
69
+ if (!detail) { console.log("Usage: 0dai feedback log --type bug|suggestion|friction|positive --detail '...'"); return; }
70
+ fs.mkdirSync(fbDir, { recursive: true });
71
+ const entry = JSON.stringify({ ts: new Date().toISOString(), type, detail, agent: "cli" });
72
+ fs.appendFileSync(path.join(fbDir, "operational.jsonl"), entry + "\n");
73
+ log(`logged: [${type}] ${detail.slice(0, 60)}`);
74
+ return;
75
+ }
76
+ if (sub === "list") {
77
+ try {
78
+ const files = fs.readdirSync(fbDir).filter(f => f.endsWith("-report.json"));
79
+ if (!files.length) { log("no reports"); return; }
80
+ for (const f of files) {
81
+ try {
82
+ const d = JSON.parse(fs.readFileSync(path.join(fbDir, f), "utf8"));
83
+ console.log(` ${f}: ${d.verdict || "?"} (${d.project || "?"})`);
84
+ } catch {}
85
+ }
86
+ } catch { log("no feedback directory"); }
87
+ return;
88
+ }
89
+ console.log("Usage: 0dai feedback [push|log|list] [--type ...] [--detail '...']");
90
+ }
91
+
92
+ module.exports = { cmdFeedbackPush, cmdFeedback };
@@ -0,0 +1,171 @@
1
+ "use strict";
2
+ const shared = require("../shared");
3
+ const { log, T, R, D, fs, path, apiCall, AUTH_FILE, buildProjectIdentity, collectMetadata, recordExperienceEvent } = shared;
4
+
5
+ async function cmdGraph(target, sub, args) {
6
+ const graphFile = path.join(target, "ai", "manifest", "project_graph.json");
7
+
8
+ if (sub === "push") {
9
+ // Check auth
10
+ let auth;
11
+ try { auth = JSON.parse(fs.readFileSync(AUTH_FILE, "utf8")); } catch {}
12
+ if (!auth || !auth.access_token) {
13
+ log("Graph push requires an account. Run: 0dai auth login");
14
+ return;
15
+ }
16
+
17
+ if (!fs.existsSync(graphFile)) {
18
+ log("No local graph found. Run: 0dai graph init or create ai/manifest/project_graph.json");
19
+ return;
20
+ }
21
+
22
+ let localGraph;
23
+ try { localGraph = JSON.parse(fs.readFileSync(graphFile, "utf8")); } catch (e) {
24
+ log(`Error reading graph file: ${e.message}`);
25
+ return;
26
+ }
27
+
28
+ const nodes = localGraph.nodes || {};
29
+ const edges = localGraph.edges || [];
30
+ const identity = buildProjectIdentity(target, collectMetadata(target));
31
+
32
+ log(`Pushing graph (${Object.keys(nodes).length} nodes, ${edges.length} edges)...`);
33
+ const result = await apiCall("/v1/graph/sync", {
34
+ project_id: identity.project_id,
35
+ nodes,
36
+ edges,
37
+ });
38
+
39
+ if (result.error) {
40
+ log(`Error: ${result.error}`);
41
+ return;
42
+ }
43
+
44
+ const parts = [];
45
+ if (result.added_nodes) parts.push(`${result.added_nodes} nodes added`);
46
+ if (result.updated_nodes) parts.push(`${result.updated_nodes} nodes updated`);
47
+ if (result.deleted_nodes) parts.push(`${result.deleted_nodes} nodes deleted`);
48
+ if (result.added_edges) parts.push(`${result.added_edges} edges added`);
49
+ if (result.deleted_edges) parts.push(`${result.deleted_edges} edges deleted`);
50
+
51
+ log(`Sync: ${parts.join(", ") || "up to date"}`);
52
+ if (result.edges_rejected) {
53
+ log(`${D}Edges rejected: upgrade to Pro for edge sync (0dai upgrade)${R}`);
54
+ }
55
+ recordExperienceEvent(target, {
56
+ event_type: "graph_synced",
57
+ agent: "cli",
58
+ model: "0dai-cli",
59
+ effort: "medium",
60
+ task: { goal: "push graph to server", task_type: "feat", result: "success", elapsed_seconds: 0, cost_usd: 0 },
61
+ context: { stack: "unknown", files_touched: 1, tests_passed: true, graph_nodes_used: Object.keys(nodes).length, graph_edges_used: edges.length },
62
+ });
63
+ return;
64
+ }
65
+
66
+ if (sub === "pull") {
67
+ let auth;
68
+ try { auth = JSON.parse(fs.readFileSync(AUTH_FILE, "utf8")); } catch {}
69
+ if (!auth || !auth.access_token) {
70
+ log("Graph pull requires an account. Run: 0dai auth login");
71
+ return;
72
+ }
73
+
74
+ const identity = buildProjectIdentity(target, collectMetadata(target));
75
+ log("Pulling graph from server...");
76
+ const result = await apiCall(`/v1/graph/pull?project_id=${identity.project_id}`);
77
+
78
+ if (result.error) {
79
+ log(`Error: ${result.error}`);
80
+ if (result.hint) log(`Hint: ${result.hint}`);
81
+ return;
82
+ }
83
+
84
+ // Merge into local graph
85
+ let localGraph = { nodes: {}, edges: [], meta: {} };
86
+ if (fs.existsSync(graphFile)) {
87
+ try { localGraph = JSON.parse(fs.readFileSync(graphFile, "utf8")); } catch {}
88
+ }
89
+
90
+ // Merge nodes (server wins on conflicts)
91
+ const serverNodes = result.nodes || {};
92
+ const localNodes = localGraph.nodes || {};
93
+ const mergedNodes = { ...localNodes, ...serverNodes };
94
+
95
+ // Merge edges (server wins for Pro users)
96
+ const serverEdges = result.edges || [];
97
+ const localEdges = localGraph.edges || [];
98
+ const mergedEdges = [...localEdges, ...serverEdges];
99
+
100
+ // Deduplicate edges by (from, to, type)
101
+ const seen = new Set();
102
+ const uniqueEdges = [];
103
+ for (const e of mergedEdges) {
104
+ const key = `${e.from}|${e.to}|${e.type}`;
105
+ if (!seen.has(key)) {
106
+ seen.add(key);
107
+ uniqueEdges.push(e);
108
+ }
109
+ }
110
+
111
+ const merged = {
112
+ nodes: mergedNodes,
113
+ edges: uniqueEdges,
114
+ meta: {
115
+ ...localGraph.meta,
116
+ ...result.meta,
117
+ node_count: Object.keys(mergedNodes).length,
118
+ edge_count: uniqueEdges.length,
119
+ },
120
+ };
121
+
122
+ fs.mkdirSync(path.dirname(graphFile), { recursive: true });
123
+ fs.writeFileSync(graphFile, JSON.stringify(merged, null, 2) + "\n", "utf8");
124
+
125
+ log(`Pulled: ${Object.keys(mergedNodes).length} nodes, ${uniqueEdges.length} edges`);
126
+ if (result.meta && result.meta.edges_stripped) {
127
+ log(`${D}Edges not included: upgrade to Pro for full graph sync (0dai upgrade)${R}`);
128
+ }
129
+ recordExperienceEvent(target, {
130
+ event_type: "graph_synced",
131
+ agent: "cli",
132
+ model: "0dai-cli",
133
+ effort: "medium",
134
+ task: { goal: "pull graph from server", task_type: "feat", result: "success", elapsed_seconds: 0, cost_usd: 0 },
135
+ context: { stack: "unknown", files_touched: 1, tests_passed: true, graph_nodes_used: Object.keys(mergedNodes).length, graph_edges_used: uniqueEdges.length },
136
+ });
137
+ return;
138
+ }
139
+
140
+ if (sub === "status") {
141
+ let localNodes = 0, localEdges = 0;
142
+ if (fs.existsSync(graphFile)) {
143
+ try {
144
+ const g = JSON.parse(fs.readFileSync(graphFile, "utf8"));
145
+ localNodes = Object.keys(g.nodes || {}).length;
146
+ localEdges = (g.edges || []).length;
147
+ } catch {}
148
+ }
149
+
150
+ log(`Local graph: ${localNodes} nodes, ${localEdges} edges`);
151
+
152
+ // Check plan for edge sync status
153
+ let auth;
154
+ try { auth = JSON.parse(fs.readFileSync(AUTH_FILE, "utf8")); } catch {}
155
+ if (auth && auth.access_token) {
156
+ const plan = (auth.plan || "free").toLowerCase();
157
+ const edgeStatus = plan === "free" ? `${D}nodes only (Pro for edges)${R}` : `${T}full sync${R}`;
158
+ log(`Server sync: ${edgeStatus}`);
159
+ } else {
160
+ log(`${D}Not authenticated — graph sync unavailable${R}`);
161
+ }
162
+ return;
163
+ }
164
+
165
+ console.log("Usage: 0dai graph [push|pull|status]");
166
+ console.log(" push Upload local graph to server (Pro: edges, Free: nodes only)");
167
+ console.log(" pull Download server graph and merge locally");
168
+ console.log(" status Show local graph stats and sync state");
169
+ }
170
+
171
+ module.exports = { cmdGraph };
@@ -0,0 +1,282 @@
1
+ "use strict";
2
+ const shared = require("../shared");
3
+ const {
4
+ T, R, D, log,
5
+ fs, path,
6
+ VERSION, SUPPORTED_CLIS,
7
+ apiCall, makeEnsureAuthenticated, ensureLicenseActivation,
8
+ collectMetadata, buildProjectIdentity, registerProject,
9
+ writeFiles, sendProjectHeartbeat, recordExperienceEvent,
10
+ } = shared;
11
+ const { cmdAuthLogin } = require("./auth");
12
+
13
+ const ensureAuthenticated = makeEnsureAuthenticated(cmdAuthLogin);
14
+
15
+ // bindProjectForCloud — binds project to cloud via /v1/projects/bind
16
+ async function bindProjectForCloud(target, metadata, identity) {
17
+ const result = await apiCall("/v1/projects/bind", {
18
+ ...identity,
19
+ stack: identity.stack || "unknown",
20
+ });
21
+ if (result.error) {
22
+ log(`error: ${result.error}`);
23
+ if (result.hint) console.log(` ${result.hint}`);
24
+ process.exit(1);
25
+ }
26
+ return result.project || {
27
+ project_id: identity.project_id,
28
+ name: identity.project_name,
29
+ stack: identity.stack || "unknown",
30
+ binding_status: "bound",
31
+ };
32
+ }
33
+
34
+ async function cmdInit(target, args = []) {
35
+ const dryRun = args.includes("--dry-run");
36
+ const minimal = args.includes("--minimal");
37
+ const noWizard = args.includes("--no-wizard");
38
+
39
+ if (fs.existsSync(path.join(target, "ai", "VERSION"))) {
40
+ const v = fs.readFileSync(path.join(target, "ai", "VERSION"), "utf8").trim();
41
+ log(`ai/ layer already exists (v${v}). Run '0dai sync' to update.`);
42
+ return;
43
+ }
44
+
45
+ // First-run wizard (unless --no-wizard or non-interactive)
46
+ if (!noWizard && !dryRun && !minimal) {
47
+ try {
48
+ const { runWizard, isInteractive } = require("../wizard");
49
+ if (isInteractive()) {
50
+ const result = await runWizard(target);
51
+ if (result.completed) {
52
+ try {
53
+ const ob = require("../onboarding");
54
+ ob.trackFirstInit(target);
55
+ ob.showWhatsNext(result.mode || "local", false);
56
+ } catch {}
57
+ return;
58
+ }
59
+ }
60
+ } catch {}
61
+ }
62
+
63
+ const isTTY = process.stdout.isTTY;
64
+ let spinner = null;
65
+ if (isTTY) {
66
+ try { spinner = require("@clack/prompts").spinner(); } catch {}
67
+ }
68
+
69
+ const metadata = collectMetadata(target);
70
+ const { projectFiles, manifestContents, clis } = metadata;
71
+ const authStatus = await ensureAuthenticated("init");
72
+ const license = await ensureLicenseActivation();
73
+ const identity = buildProjectIdentity(target, metadata);
74
+ const boundProject = await bindProjectForCloud(target, metadata, identity);
75
+ if (dryRun) log(`${D}dry-run: would generate ai/ layer (${projectFiles.length} files, ${clis.length} CLIs)${R}`);
76
+ if (spinner) spinner.start(`${dryRun ? "[dry-run] " : ""}Generating ai/ layer (${projectFiles.length} files, ${clis.length} CLIs)...`);
77
+ else if (!dryRun) log(`sending to API (${projectFiles.length} files, ${clis.length} CLIs)...`);
78
+ const result = await apiCall("/v1/init", {
79
+ project_files: projectFiles,
80
+ manifest_contents: manifestContents,
81
+ available_clis: clis,
82
+ dry_run: dryRun,
83
+ minimal: minimal,
84
+ project_name: identity.project_name,
85
+ project_id: boundProject.project_id || identity.project_id,
86
+ remote_origin: identity.remote_origin,
87
+ origin: identity.origin,
88
+ binding_source: "init",
89
+ });
90
+
91
+ if (result.error) {
92
+ if (result.hint) {
93
+ log(`${result.message || result.error}`);
94
+ console.log(` ${result.hint}\n`);
95
+ } else {
96
+ log(`error: ${result.error}`);
97
+ }
98
+ process.exit(1);
99
+ }
100
+
101
+ if (spinner) spinner.stop(`${dryRun ? "[dry-run] " : ""}Detected: ${result.stack || "?"}`);
102
+ else log(`detected: ${result.stack || "?"}`);
103
+ if (dryRun) {
104
+ const files = Object.keys(result.files || {});
105
+ log(`${D}dry-run: would write ${files.length} files:${R}`);
106
+ for (const f of files.slice(0, 20)) console.log(` ${D}+ ${f}${R}`);
107
+ if (files.length > 20) console.log(` ${D}… and ${files.length - 20} more${R}`);
108
+ return;
109
+ }
110
+ writeFiles(target, result.files || {});
111
+
112
+ // Ensure ai/VERSION matches CLI version
113
+ const versionFile = path.join(target, "ai", "VERSION");
114
+ fs.mkdirSync(path.dirname(versionFile), { recursive: true });
115
+ fs.writeFileSync(versionFile, VERSION + "\n", "utf8");
116
+
117
+ // Add to .gitignore
118
+ const gi = path.join(target, ".gitignore");
119
+ try {
120
+ const text = fs.existsSync(gi) ? fs.readFileSync(gi, "utf8") : "";
121
+ if (!text.includes(".0dai")) fs.appendFileSync(gi, "\n.0dai/\n");
122
+ } catch {}
123
+
124
+ // Register in global portfolio
125
+ registerProject(target, path.basename(target), result.stack);
126
+
127
+ log(`initialized (${result.file_count || "?"} files)`);
128
+ console.log(` account: ${authStatus.email} · plan: ${authStatus.plan || license.plan || "free"} · activation: ${license.status}`);
129
+ console.log(` project: ${boundProject.project_id || identity.project_id}`);
130
+ console.log(" skills: /build /review /status /feedback /bugfix /delegate");
131
+
132
+ // Detect agent auth status for smart onboarding hints
133
+ const { execFileSync: _ef } = require("child_process");
134
+ const agents = [];
135
+ try { _ef("claude", ["--version"], { timeout: 8000 }); agents.push("claude"); } catch {}
136
+ try { _ef("codex", ["--version"], { timeout: 8000 }); agents.push("codex"); } catch {}
137
+ try { _ef("gemini", ["--version"], { timeout: 8000 }); agents.push("gemini"); } catch {}
138
+
139
+ // Next steps — guide user to first value
140
+ console.log(`\n ${T}Next steps:${R}`);
141
+ console.log(` ${D}1.${R} Check health: ${D}0dai doctor${R}`);
142
+ if (agents.length > 0) {
143
+ const a = agents[0];
144
+ console.log(` ${D}2.${R} Try delegation: ${D}0dai run "write tests for auth"${R}`);
145
+ console.log(` ${D}(${agents.join(", ")} detected — delegation will use ${a} by default)${R}`);
146
+ } else {
147
+ console.log(` ${D}2.${R} Install an agent CLI to enable delegation:`);
148
+ console.log(` ${D}claude:${R} npm i -g @anthropic-ai/claude-code ${D}(or Pro subscription)${R}`);
149
+ console.log(` ${D}codex:${R} npm i -g @openai/codex ${D}(or ChatGPT Pro)${R}`);
150
+ }
151
+ console.log(` ${D}3.${R} Open dashboard: ${D}https://0dai.dev/dashboard${R}`);
152
+
153
+ await sendProjectHeartbeat(identity, result, {
154
+ project_id: boundProject.project_id || identity.project_id,
155
+ }).catch(() => {});
156
+ recordExperienceEvent(target, {
157
+ event_type: "config_generated",
158
+ agent: "cli",
159
+ model: "0dai-cli",
160
+ effort: "medium",
161
+ task: { goal: "initialize ai layer", task_type: "feat", result: "success", elapsed_seconds: 0, cost_usd: 0 },
162
+ context: { stack: result.stack || identity.stack || "unknown", files_touched: Number(result.file_count || 0), tests_passed: true },
163
+ });
164
+
165
+ // Send anonymous usage ping
166
+ apiCall("/v1/feedback", { report: {
167
+ stack_detected: result.stack || "?", _auto: true, _plan: result.plan || "trial",
168
+ _cli_version: VERSION, _files_generated: result.file_count || 0,
169
+ }}).catch(() => {});
170
+ }
171
+
172
+ async function cmdSync(target, args = []) {
173
+ const dryRun = args.includes("--dry-run");
174
+ const quiet = args.includes("--quiet") || args.includes("-q");
175
+
176
+ // Quick local check: skip API if already at current version (unless dry-run)
177
+ let version = "unknown";
178
+ try { version = fs.readFileSync(path.join(target, "ai", "VERSION"), "utf8").trim(); } catch {}
179
+
180
+ const metadata = collectMetadata(target);
181
+ const { manifestContents, clis } = metadata;
182
+ const authStatus = await ensureAuthenticated("sync");
183
+ const license = await ensureLicenseActivation();
184
+ let stack = "generic", agents = [];
185
+ try {
186
+ const d = JSON.parse(fs.readFileSync(path.join(target, "ai", "manifest", "discovery.json"), "utf8"));
187
+ stack = d.stack || "generic";
188
+ agents = d.selected_agents || [];
189
+ } catch {}
190
+ const identity = buildProjectIdentity(target, metadata, stack);
191
+ const boundProject = await bindProjectForCloud(target, metadata, identity);
192
+
193
+ // Collect current ai/ files
194
+ const currentFiles = {};
195
+ const aiDir = path.join(target, "ai");
196
+ if (fs.existsSync(aiDir)) {
197
+ const walk = (dir) => {
198
+ for (const f of fs.readdirSync(dir, { withFileTypes: true })) {
199
+ const p = path.join(dir, f.name);
200
+ if (f.isDirectory()) walk(p);
201
+ else {
202
+ try {
203
+ const stat = fs.statSync(p);
204
+ if (stat.size < 10000) currentFiles[path.relative(target, p)] = fs.readFileSync(p, "utf8");
205
+ } catch {}
206
+ }
207
+ }
208
+ };
209
+ walk(aiDir);
210
+ }
211
+
212
+ if (dryRun) log(`${D}dry-run: checking what sync would change...${R}`);
213
+
214
+ const result = await apiCall("/v1/sync", {
215
+ ai_version: version, stack, agents: agents.length ? agents : clis,
216
+ current_files: currentFiles, manifest_contents: manifestContents,
217
+ dry_run: dryRun, quiet,
218
+ project_name: identity.project_name,
219
+ project_id: boundProject.project_id || identity.project_id,
220
+ remote_origin: identity.remote_origin,
221
+ origin: identity.origin,
222
+ binding_source: "sync",
223
+ });
224
+
225
+ if (result.error) {
226
+ log(`error: ${result.error}`);
227
+ if (result.hint) console.log(` ${result.hint}`);
228
+ process.exit(1);
229
+ }
230
+
231
+ const updated = result.files_updated || {};
232
+ if (dryRun) {
233
+ const files = Object.keys(updated);
234
+ if (files.length) {
235
+ log(`${D}dry-run: would update ${files.length} file(s):${R}`);
236
+ for (const f of files) console.log(` ${D}~ ${f}${R}`);
237
+ } else {
238
+ log(`${D}dry-run: nothing to update${R}`);
239
+ }
240
+ return;
241
+ }
242
+ const changedCount = Object.keys(updated).length;
243
+ if (changedCount) {
244
+ writeFiles(target, updated);
245
+ if (!quiet) {
246
+ for (const f of Object.keys(updated)) console.log(` ~ ${f}`);
247
+ }
248
+ log(`sync: ${changedCount} file(s) updated`);
249
+ console.log(` ${D}Run: 0dai doctor to verify project health${R}`);
250
+ } else {
251
+ log("already up to date");
252
+ }
253
+ if (!quiet) {
254
+ console.log(` account: ${authStatus.email} · plan: ${authStatus.plan || license.plan || "free"} · activation: ${license.status}`);
255
+ console.log(` project: ${boundProject.project_id || identity.project_id}`);
256
+ }
257
+
258
+ // Ensure ai/VERSION matches CLI version after successful sync
259
+ const versionFile = path.join(target, "ai", "VERSION");
260
+ try {
261
+ const current = fs.existsSync(versionFile) ? fs.readFileSync(versionFile, "utf8").trim() : "";
262
+ if (current !== VERSION) {
263
+ fs.writeFileSync(versionFile, VERSION + "\n", "utf8");
264
+ }
265
+ } catch {}
266
+
267
+ // Update portfolio registry
268
+ registerProject(target, path.basename(target), stack);
269
+ await sendProjectHeartbeat(identity, result, {
270
+ project_id: boundProject.project_id || identity.project_id,
271
+ }).catch(() => {});
272
+ recordExperienceEvent(target, {
273
+ event_type: "config_generated",
274
+ agent: "cli",
275
+ model: "0dai-cli",
276
+ effort: "medium",
277
+ task: { goal: "sync ai layer", task_type: "feat", result: "success", elapsed_seconds: 0, cost_usd: 0 },
278
+ context: { stack: stack || identity.stack || "unknown", files_touched: changedCount, tests_passed: true },
279
+ });
280
+ }
281
+
282
+ module.exports = { cmdInit, cmdSync };
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+ const shared = require("../shared");
3
+ const { fs, path } = shared;
4
+
5
+ function cmdMetrics(target) {
6
+ const ai = path.join(target, "ai");
7
+ const G = "\x1b[32m", W = "\x1b[33m", R2 = "\x1b[0m", D = "\x1b[2m",
8
+ B = "\x1b[34m", T = "\x1b[36m", M = "\x1b[35m";
9
+
10
+ // --- Data sources ---
11
+ let stats = {}, budget = {}, discovery = {};
12
+ try { stats = JSON.parse(fs.readFileSync(path.join(ai, "feedback", ".usage_stats.json"), "utf8")); } catch {}
13
+ try { budget = JSON.parse(fs.readFileSync(path.join(ai, "swarm", "budget.json"), "utf8")); } catch {}
14
+ try { discovery = JSON.parse(fs.readFileSync(path.join(ai, "manifest", "discovery.json"), "utf8")); } catch {}
15
+
16
+ const projectName = discovery.project_name || path.basename(target);
17
+ const stack = discovery.stack || "?";
18
+ const totalSessions = stats.total_sessions || 0;
19
+ const agentBreakdown = stats.agents || {};
20
+ const layerInfo = stats.layer || {};
21
+ const lastSession = stats.last_session ? new Date(stats.last_session) : null;
22
+ const layerVersion = stats.version || "?";
23
+
24
+ // Swarm tasks: count done files
25
+ let tasksDone = 0, tasksQueue = 0;
26
+ const doneDir = path.join(ai, "swarm", "done");
27
+ const queueDir = path.join(ai, "swarm", "queue");
28
+ try { tasksDone = fs.readdirSync(doneDir).filter(f => f.endsWith(".json")).length; } catch {}
29
+ try { tasksQueue = fs.readdirSync(queueDir).filter(f => f.endsWith(".json")).length; } catch {}
30
+
31
+ // Activity: count events from activity.jsonl
32
+ let activityEvents = 0;
33
+ try {
34
+ const lines = fs.readFileSync(path.join(ai, "swarm", "activity.jsonl"), "utf8").trim().split("\n").filter(Boolean);
35
+ activityEvents = lines.length;
36
+ } catch {}
37
+
38
+ // Feedback submissions
39
+ let feedbackCount = layerInfo.feedback_reports || 0;
40
+
41
+ // Budget totals
42
+ const totalSpent = budget.total_spent || 0;
43
+ const sessionsWithBudget = Object.keys(budget.sessions || {}).length;
44
+
45
+ // --- Effectiveness score (0-100) ---
46
+ let score = 0, scoreNotes = [];
47
+
48
+ // Sessions depth: 1 = tried, 3 = habit forming, 7 = regular use
49
+ const sessionScore = Math.min(Math.floor((totalSessions / 7) * 35), 35);
50
+ score += sessionScore;
51
+ if (totalSessions === 0) scoreNotes.push("not started");
52
+ else if (totalSessions === 1) scoreNotes.push("first session");
53
+ else if (totalSessions < 3) scoreNotes.push("early");
54
+ else if (totalSessions < 7) scoreNotes.push("habit forming");
55
+ else scoreNotes.push("regular use");
56
+
57
+ // Delegation: did they delegate to swarm?
58
+ const delegationScore = tasksDone > 0 ? Math.min(Math.floor((tasksDone / 5) * 30), 30) : 0;
59
+ score += delegationScore;
60
+ if (tasksDone > 0) scoreNotes.push(`${tasksDone} tasks delegated`);
61
+
62
+ // Feedback: submitted = trust signal
63
+ const feedbackScore = feedbackCount > 0 ? 20 : 0;
64
+ score += feedbackScore;
65
+ if (feedbackCount > 0) scoreNotes.push("feedback submitted");
66
+
67
+ // Layer completeness: has playbooks and commands?
68
+ const layerScore = (layerInfo.playbooks && layerInfo.commands) ? 15 : (layerInfo.commands ? 8 : 0);
69
+ score += layerScore;
70
+
71
+ const scoreColor = score >= 70 ? G : score >= 40 ? W : "\x1b[31m";
72
+ const bar = "█".repeat(Math.round(score / 5)).padEnd(20, "░");
73
+
74
+ // --- Output ---
75
+ console.log(`\n ${T}Metrics${R2} ${D}${projectName} · ${stack} · ai v${layerVersion}${R2}\n`);
76
+
77
+ // Effectiveness score
78
+ console.log(` ${B}Effectiveness${R2}`);
79
+ console.log(` ${scoreColor}${score}/100${R2} ${D}${bar}${R2}`);
80
+ if (scoreNotes.length) console.log(` ${D}${scoreNotes.join(" · ")}${R2}`);
81
+
82
+ // Adoption funnel
83
+ console.log(`\n ${B}Adoption funnel${R2}`);
84
+ const funnelStep = (label, value, done, hint) => {
85
+ const icon = done ? `${G}✓${R2}` : `${D}○${R2}`;
86
+ const val = value !== null ? ` ${D}${value}${R2}` : "";
87
+ const h = !done && hint ? ` ${D}← ${hint}${R2}` : "";
88
+ console.log(` ${icon} ${label}${val}${h}`);
89
+ };
90
+ funnelStep("Initialized", `ai/ v${layerVersion}`, true);
91
+ funnelStep("Returned (>1 session)", `${totalSessions} total`, totalSessions > 1, "run 0dai reflect after each session");
92
+ funnelStep("Used swarm delegation", tasksDone > 0 ? `${tasksDone} tasks done` : null, tasksDone > 0, "try: 0dai swarm add --task '...' --to codex");
93
+ funnelStep("Submitted feedback", feedbackCount > 0 ? `${feedbackCount} reports` : null, feedbackCount > 0, "0dai feedback log + push");
94
+
95
+ // Session stats
96
+ if (totalSessions > 0) {
97
+ console.log(`\n ${B}Sessions${R2}`);
98
+ console.log(` Total ${totalSessions}`);
99
+ if (lastSession) {
100
+ const daysAgo = Math.floor((Date.now() - lastSession.getTime()) / 86400000);
101
+ const when = daysAgo === 0 ? "today" : daysAgo === 1 ? "yesterday" : `${daysAgo}d ago`;
102
+ console.log(` Last ${when}`);
103
+ }
104
+ const agentEntries = Object.entries(agentBreakdown).sort((a, b) => b[1] - a[1]);
105
+ if (agentEntries.length) {
106
+ console.log(` Agents ${agentEntries.map(([a, n]) => `${a}: ${n}`).join(" ")}`);
107
+ }
108
+ if (sessionsWithBudget > 0 && totalSpent > 0) {
109
+ console.log(` Cost $${totalSpent.toFixed(4)} total ${D}(${sessionsWithBudget} sessions tracked)${R2}`);
110
+ }
111
+ }
112
+
113
+ // Delegation stats
114
+ if (tasksDone > 0 || tasksQueue > 0) {
115
+ console.log(`\n ${B}Delegation${R2}`);
116
+ if (tasksDone > 0) console.log(` Done ${G}${tasksDone}${R2}`);
117
+ if (tasksQueue > 0) console.log(` Queue ${W}${tasksQueue}${R2}`);
118
+ if (activityEvents > 0) console.log(` Events ${activityEvents}`);
119
+ }
120
+
121
+ // Layer health
122
+ console.log(`\n ${B}ai/ layer${R2}`);
123
+ const checks = [
124
+ ["commands.yaml", layerInfo.commands],
125
+ ["playbooks", layerInfo.playbooks],
126
+ ["personas", layerInfo.personas],
127
+ ["session roaming", layerInfo.session_active],
128
+ ["swarm queue", (layerInfo.swarm_queue || 0) > 0],
129
+ ];
130
+ for (const [label, ok] of checks) {
131
+ const icon = ok ? `${G}✓${R2}` : `${D}—${R2}`;
132
+ console.log(` ${icon} ${label}`);
133
+ }
134
+
135
+ // Next suggested action
136
+ console.log(`\n ${B}Next${R2}`);
137
+ if (totalSessions === 0) console.log(` ${D}Start a Claude Code session — session_start hook will print project context${R2}`);
138
+ else if (tasksDone === 0) console.log(` ${D}Try delegating a task: 0dai swarm add --task "write tests for auth module" --to codex${R2}`);
139
+ else if (feedbackCount === 0) console.log(` ${D}Submit feedback: 0dai feedback log --type positive --detail "what worked"${R2}`);
140
+ else console.log(` ${D}Score ${score}/100 — keep delegating and submitting feedback${R2}`);
141
+
142
+ console.log();
143
+ }
144
+
145
+ module.exports = { cmdMetrics };