@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
|
@@ -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 };
|