@0dai-dev/cli 4.0.0 → 4.1.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/bin/0dai.js +57 -1
- package/lib/commands/graph.js +103 -4
- package/lib/commands/init.js +48 -3
- package/lib/commands/models.js +34 -1
- package/lib/commands/swarm.js +39 -1
- package/lib/commands/workspace.js +296 -0
- package/lib/shared.js +78 -1
- package/lib/wizard.js +1 -1
- package/package.json +9 -4
package/bin/0dai.js
CHANGED
|
@@ -32,6 +32,7 @@ const { cmdFeedback, cmdFeedbackPush } = require("../lib/commands/feedback");
|
|
|
32
32
|
const { cmdGraph } = require("../lib/commands/graph");
|
|
33
33
|
const { cmdReport } = require("../lib/commands/report");
|
|
34
34
|
const { cmdExperience } = require("../lib/commands/experience");
|
|
35
|
+
const { cmdWorkspace } = require("../lib/commands/workspace");
|
|
35
36
|
|
|
36
37
|
async function main() {
|
|
37
38
|
const args = process.argv.slice(2);
|
|
@@ -67,6 +68,29 @@ async function main() {
|
|
|
67
68
|
case "watch": cmdWatch(target, args.slice(1)); break;
|
|
68
69
|
case "audit": cmdAudit(target); break;
|
|
69
70
|
case "security": {
|
|
71
|
+
const subSec = args[1] || "";
|
|
72
|
+
if (subSec === "install-hook") {
|
|
73
|
+
const hooksDir = path.join(target, ".git", "hooks");
|
|
74
|
+
if (!fs.existsSync(hooksDir)) { log("not a git repo"); break; }
|
|
75
|
+
const hookPath = path.join(hooksDir, "pre-commit");
|
|
76
|
+
const hookSource = path.join(__dirname, "..", "..", "..", "scripts", "hooks", "pre-commit.sh");
|
|
77
|
+
if (fs.existsSync(hookSource)) {
|
|
78
|
+
fs.copyFileSync(hookSource, hookPath);
|
|
79
|
+
fs.chmodSync(hookPath, 0o755);
|
|
80
|
+
log("pre-commit hook installed: " + hookPath);
|
|
81
|
+
} else {
|
|
82
|
+
// Inline install: copy from repo scripts if available
|
|
83
|
+
const repoHook = findRepoScript(target, "hooks/pre-commit.sh");
|
|
84
|
+
if (repoHook) {
|
|
85
|
+
fs.copyFileSync(repoHook, hookPath);
|
|
86
|
+
fs.chmodSync(hookPath, 0o755);
|
|
87
|
+
log("pre-commit hook installed: " + hookPath);
|
|
88
|
+
} else {
|
|
89
|
+
log("hook source not found — run: 0dai security install-hook from repo root");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
70
94
|
const secScript = findRepoScript(target, "scan_secrets.py");
|
|
71
95
|
if (!secScript) { log("secret scanner unavailable"); break; }
|
|
72
96
|
const fwd = [secScript, "--target", target];
|
|
@@ -117,11 +141,38 @@ async function main() {
|
|
|
117
141
|
break;
|
|
118
142
|
case "session": cmdSession(target, sub, args); break;
|
|
119
143
|
case "swarm": cmdSwarm(target, sub, args); break;
|
|
144
|
+
case "workspace": cmdWorkspace(target, sub, args.slice(2)); break;
|
|
120
145
|
case "feedback": await cmdFeedback(target, sub, args); break;
|
|
121
146
|
case "report": cmdReport(target, sub, args); break;
|
|
122
147
|
case "experience": cmdExperience(target, sub, args); break;
|
|
123
148
|
case "graph": await cmdGraph(target, sub, args); break;
|
|
124
149
|
case "models": cmdModels(sub || args[1]); break;
|
|
150
|
+
case "delegate": case "delegation": {
|
|
151
|
+
const deScript = findRepoScript(target, "delegation_engine.py");
|
|
152
|
+
if (!deScript) { log("delegation engine unavailable"); break; }
|
|
153
|
+
const deCmd = cmd === "delegate" ? "delegate" : (sub || "show");
|
|
154
|
+
const fwd = [deScript, deCmd, "--target", target];
|
|
155
|
+
if (deCmd === "delegate") {
|
|
156
|
+
// Find goal: first non-flag arg after delegate, or --goal value
|
|
157
|
+
let goal = "";
|
|
158
|
+
for (let i = 0; i < args.length; i++) {
|
|
159
|
+
if (args[i] === "--goal" && args[i + 1]) { goal = args[i + 1]; i++; }
|
|
160
|
+
else if (!args[i].startsWith("-") && !goal) goal = args[i];
|
|
161
|
+
}
|
|
162
|
+
if (sub && !sub.startsWith("-")) goal = goal || sub;
|
|
163
|
+
if (goal) fwd.push(goal);
|
|
164
|
+
for (let i = 0; i < args.length; i++) {
|
|
165
|
+
if (args[i] === "--agent" && args[i + 1]) { fwd.push("--agent", args[i + 1]); i++; }
|
|
166
|
+
else if (args[i] === "--model" && args[i + 1]) { fwd.push("--model", args[i + 1]); i++; }
|
|
167
|
+
else if (args[i] === "--task-type" && args[i + 1]) { fwd.push("--task-type", args[i + 1]); i++; }
|
|
168
|
+
}
|
|
169
|
+
if (args.includes("--dry-run")) fwd.push("--dry-run");
|
|
170
|
+
}
|
|
171
|
+
if (args.includes("--json")) fwd.push("--json");
|
|
172
|
+
const result = spawnSync("python3", fwd, { stdio: "inherit", timeout: 15000 });
|
|
173
|
+
if (typeof result.status === "number" && result.status !== 0) process.exit(result.status);
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
125
176
|
case "redeem": await cmdRedeem(sub || args[1]); break;
|
|
126
177
|
case "terminal": case "term":
|
|
127
178
|
try {
|
|
@@ -192,7 +243,7 @@ async function main() {
|
|
|
192
243
|
console.log(" watch Live task monitor: queue, active, recently done [--interval N]");
|
|
193
244
|
console.log(" audit Scan ai/ and agent configs for leaked secrets");
|
|
194
245
|
console.log(" init Initialize ai/ layer (via API) [--dry-run] [--minimal]");
|
|
195
|
-
console.log(" sync Update ai/ layer (via API) [--dry-run] [--quiet]");
|
|
246
|
+
console.log(" sync Update ai/ layer (via API) [--dry-run] [--quiet] [--force]");
|
|
196
247
|
console.log(" detect Show detected stack");
|
|
197
248
|
console.log(" doctor Check health + credentials checklist");
|
|
198
249
|
console.log(" update Update all installed agent CLIs to latest [--dry-run]");
|
|
@@ -206,6 +257,9 @@ async function main() {
|
|
|
206
257
|
console.log(" swarm webhook add Register webhook (fires on task done/failed)");
|
|
207
258
|
console.log(" swarm webhook list Show registered webhooks");
|
|
208
259
|
console.log(" swarm webhook test Send test ping to a webhook URL");
|
|
260
|
+
console.log(" workspace init Create tmux workspace config (auto-detect services)");
|
|
261
|
+
console.log(" workspace up Start all workspace sessions");
|
|
262
|
+
console.log(" workspace status Show session status table");
|
|
209
263
|
console.log(" feedback push Send feedback to 0dai");
|
|
210
264
|
console.log(" report preview Preview privacy-safe project report");
|
|
211
265
|
console.log(" report push Send report to 0dai (with offline queue)");
|
|
@@ -216,6 +270,8 @@ async function main() {
|
|
|
216
270
|
console.log(" graph pull Download server graph and merge locally");
|
|
217
271
|
console.log(" graph status Show local graph stats and sync state");
|
|
218
272
|
console.log(" models Show model ratings (--fast/--balanced/--deep/--available)");
|
|
273
|
+
console.log(" delegate Auto-route task to best agent/model (0dai delegate 'goal')");
|
|
274
|
+
console.log(" delegation show Show current delegation policy");
|
|
219
275
|
console.log(" terminal Launch interactive agent session");
|
|
220
276
|
console.log(" auth login Authenticate (device code flow)");
|
|
221
277
|
console.log(" auth logout Remove credentials");
|
package/lib/commands/graph.js
CHANGED
|
@@ -162,10 +162,109 @@ async function cmdGraph(target, sub, args) {
|
|
|
162
162
|
return;
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
165
|
+
if (sub === "history") {
|
|
166
|
+
const authFile = path.join(require("os").homedir(), ".0dai", "auth.json");
|
|
167
|
+
let auth = {};
|
|
168
|
+
try { auth = JSON.parse(fs.readFileSync(authFile, "utf8")); } catch {}
|
|
169
|
+
const plan = (auth.plan || "free").toLowerCase();
|
|
170
|
+
const limitIdx = args.indexOf("--limit");
|
|
171
|
+
const limit = limitIdx >= 0 && args[limitIdx + 1] ? parseInt(args[limitIdx + 1], 10) || 50 : 50;
|
|
172
|
+
const sinceIdx = args.indexOf("--since");
|
|
173
|
+
const since = sinceIdx >= 0 ? args[sinceIdx + 1] : null;
|
|
174
|
+
const nodeIdx = args.indexOf("--node");
|
|
175
|
+
const nodeId = nodeIdx >= 0 ? args[nodeIdx + 1] : null;
|
|
176
|
+
|
|
177
|
+
if (["pro", "team", "enterprise"].includes(plan) && auth.access_token) {
|
|
178
|
+
const identity = buildProjectIdentity(target, collectMetadata(target));
|
|
179
|
+
const params = new URLSearchParams({
|
|
180
|
+
project_id: identity.project_id,
|
|
181
|
+
limit: String(Math.max(1, limit)),
|
|
182
|
+
});
|
|
183
|
+
if (since) params.set("since", since);
|
|
184
|
+
if (nodeId) params.set("target_id", nodeId);
|
|
185
|
+
const result = await apiCall(`/v1/graph/history?${params.toString()}`);
|
|
186
|
+
if (result.error) {
|
|
187
|
+
log(`Error: ${result.error}`);
|
|
188
|
+
if (result.hint) log(result.hint);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const entries = result.mutations || [];
|
|
192
|
+
if (!entries.length) { console.log(" No graph history yet."); return; }
|
|
193
|
+
for (const entry of entries) {
|
|
194
|
+
const actor = (entry.agent || "unknown").padEnd(7);
|
|
195
|
+
const context = entry.context ? ` | "${entry.context}"` : "";
|
|
196
|
+
console.log(` ${entry.timestamp || "?"} | ${actor} | ${entry.action.padEnd(13)} | ${entry.target_id}${context}`);
|
|
197
|
+
}
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
log(`${D}Graph history requires Pro plan. Upgrade: 0dai upgrade${R}`);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (sub === "context") {
|
|
205
|
+
if (!fs.existsSync(graphFile)) {
|
|
206
|
+
log("No local graph found. Run: 0dai graph init or create ai/manifest/project_graph.json");
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const slicerScript = shared.findRepoScript(target, "graph_slicer_cli.py");
|
|
210
|
+
if (!slicerScript) {
|
|
211
|
+
log("graph slicer unavailable in this environment");
|
|
212
|
+
console.log(` ${D}Expected scripts/graph_slicer_cli.py in repo checkout${R}`);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const forwarded = [slicerScript, "--target", target];
|
|
216
|
+
let i = 0;
|
|
217
|
+
while (i < args.length) {
|
|
218
|
+
if (args[i] === "--scope" && args[i + 1]) { forwarded.push("--scope", args[i + 1]); i += 2; }
|
|
219
|
+
else if (args[i] === "--task" && args[i + 1]) { forwarded.push("--task", args[i + 1]); i += 2; }
|
|
220
|
+
else if (args[i] === "--depth" && args[i + 1]) { forwarded.push("--depth", args[i + 1]); i += 2; }
|
|
221
|
+
else if (args[i] === "--budget" && args[i + 1]) { forwarded.push("--budget", args[i + 1]); i += 2; }
|
|
222
|
+
else if (args[i] === "--json") { forwarded.push("--json"); i += 1; }
|
|
223
|
+
else { i += 1; }
|
|
224
|
+
}
|
|
225
|
+
const result = shared.spawnSync("python3", forwarded, { stdio: "inherit" });
|
|
226
|
+
if (typeof result.status === "number" && result.status !== 0) process.exit(result.status);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (sub === "outcomes") {
|
|
231
|
+
const authFile = path.join(require("os").homedir(), ".0dai", "auth.json");
|
|
232
|
+
let auth = {};
|
|
233
|
+
try { auth = JSON.parse(fs.readFileSync(authFile, "utf8")); } catch {}
|
|
234
|
+
const plan = (auth.plan || "free").toLowerCase();
|
|
235
|
+
if (!["pro", "team", "enterprise"].includes(plan)) {
|
|
236
|
+
log(`${D}Outcomes require Pro plan. Upgrade: 0dai upgrade${R}`);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (!auth.access_token) {
|
|
240
|
+
log("Graph outcomes requires an account. Run: 0dai auth login");
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const identity = buildProjectIdentity(target, collectMetadata(target));
|
|
244
|
+
const params = new URLSearchParams({ project_id: identity.project_id });
|
|
245
|
+
const limitIdx = args.indexOf("--limit");
|
|
246
|
+
if (limitIdx >= 0 && args[limitIdx + 1]) params.set("limit", args[limitIdx + 1]);
|
|
247
|
+
const sinceIdx = args.indexOf("--since");
|
|
248
|
+
if (sinceIdx >= 0 && args[sinceIdx + 1]) params.set("since", args[sinceIdx + 1]);
|
|
249
|
+
const result = await apiCall(`/v1/outcomes?${params.toString()}`);
|
|
250
|
+
if (result.error) { log(`Error: ${result.error}`); return; }
|
|
251
|
+
const stats = result.stats || {};
|
|
252
|
+
log(`Outcomes: ${stats.total || 0} total, ${stats.success_rate || 0}% success rate`);
|
|
253
|
+
if (result.outcomes && result.outcomes.length) {
|
|
254
|
+
for (const o of result.outcomes.slice(0, 10)) {
|
|
255
|
+
console.log(` ${o.result || "?"} | ${o.agent || "?"} | ${o.title || o.task_id || "?"}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
console.log("Usage: 0dai graph [push|pull|status|history|context|outcomes]");
|
|
262
|
+
console.log(" push Upload local graph to server (Pro: edges, Free: nodes only)");
|
|
263
|
+
console.log(" pull Download server graph and merge locally");
|
|
264
|
+
console.log(" status Show local graph stats and sync state");
|
|
265
|
+
console.log(" history Show graph mutation timeline [--since 7d] [--node ID] [--limit 50]");
|
|
266
|
+
console.log(" context Get relevant graph context for a task [--scope FILE] [--task TYPE]");
|
|
267
|
+
console.log(" outcomes Show outcome analytics (Pro only)");
|
|
169
268
|
}
|
|
170
269
|
|
|
171
270
|
module.exports = { cmdGraph };
|
package/lib/commands/init.js
CHANGED
|
@@ -6,7 +6,7 @@ const {
|
|
|
6
6
|
VERSION, SUPPORTED_CLIS,
|
|
7
7
|
apiCall, makeEnsureAuthenticated, ensureLicenseActivation,
|
|
8
8
|
collectMetadata, buildProjectIdentity, registerProject,
|
|
9
|
-
writeFiles, sendProjectHeartbeat, recordExperienceEvent,
|
|
9
|
+
writeFiles, writeManagedFiles, sendProjectHeartbeat, recordExperienceEvent,
|
|
10
10
|
} = shared;
|
|
11
11
|
const { cmdAuthLogin } = require("./auth");
|
|
12
12
|
|
|
@@ -172,8 +172,10 @@ async function cmdInit(target, args = []) {
|
|
|
172
172
|
async function cmdSync(target, args = []) {
|
|
173
173
|
const dryRun = args.includes("--dry-run");
|
|
174
174
|
const quiet = args.includes("--quiet") || args.includes("-q");
|
|
175
|
+
const force = args.includes("--force");
|
|
176
|
+
const updateTemplates = args.includes("--update-templates");
|
|
175
177
|
|
|
176
|
-
// Quick local check: skip API if already at current version (unless dry-run)
|
|
178
|
+
// Quick local check: skip API if already at current version (unless dry-run or force)
|
|
177
179
|
let version = "unknown";
|
|
178
180
|
try { version = fs.readFileSync(path.join(target, "ai", "VERSION"), "utf8").trim(); } catch {}
|
|
179
181
|
|
|
@@ -210,11 +212,13 @@ async function cmdSync(target, args = []) {
|
|
|
210
212
|
}
|
|
211
213
|
|
|
212
214
|
if (dryRun) log(`${D}dry-run: checking what sync would change...${R}`);
|
|
215
|
+
if (force && !dryRun) log(`${T}force mode: will overwrite native configs from ai/ source${R}`);
|
|
216
|
+
if (updateTemplates && !dryRun) log(`${T}template update mode: will refresh managed native configs from latest templates${R}`);
|
|
213
217
|
|
|
214
218
|
const result = await apiCall("/v1/sync", {
|
|
215
219
|
ai_version: version, stack, agents: agents.length ? agents : clis,
|
|
216
220
|
current_files: currentFiles, manifest_contents: manifestContents,
|
|
217
|
-
dry_run: dryRun, quiet,
|
|
221
|
+
dry_run: dryRun, quiet, force, update_templates: updateTemplates,
|
|
218
222
|
project_name: identity.project_name,
|
|
219
223
|
project_id: boundProject.project_id || identity.project_id,
|
|
220
224
|
remote_origin: identity.remote_origin,
|
|
@@ -237,6 +241,9 @@ async function cmdSync(target, args = []) {
|
|
|
237
241
|
} else {
|
|
238
242
|
log(`${D}dry-run: nothing to update${R}`);
|
|
239
243
|
}
|
|
244
|
+
if (result.template_update_available) {
|
|
245
|
+
console.log(` ${D}template update available: run 0dai sync --update-templates${R}`);
|
|
246
|
+
}
|
|
240
247
|
return;
|
|
241
248
|
}
|
|
242
249
|
const changedCount = Object.keys(updated).length;
|
|
@@ -250,6 +257,44 @@ async function cmdSync(target, args = []) {
|
|
|
250
257
|
} else {
|
|
251
258
|
log("already up to date");
|
|
252
259
|
}
|
|
260
|
+
|
|
261
|
+
if (result.template_update_available && !updateTemplates && !quiet) {
|
|
262
|
+
console.log(` ${D}Template update available: run 0dai sync --update-templates to refresh managed native configs${R}`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// --force: also overwrite native configs (CLAUDE.md, AGENTS.md, etc.) from ai/ source
|
|
266
|
+
if (force && result.native_configs) {
|
|
267
|
+
let overwritten = 0;
|
|
268
|
+
for (const [name, content] of Object.entries(result.native_configs)) {
|
|
269
|
+
const targetPath = path.join(target, name);
|
|
270
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
271
|
+
if (content) {
|
|
272
|
+
fs.writeFileSync(targetPath, content, "utf8");
|
|
273
|
+
overwritten++;
|
|
274
|
+
if (!quiet) console.log(` [force] ${name} overwritten from ai/ source`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (overwritten && !quiet) {
|
|
278
|
+
log(`force: ${overwritten} native config file(s) overwritten`);
|
|
279
|
+
}
|
|
280
|
+
} else if (updateTemplates && result.native_configs) {
|
|
281
|
+
writeManagedFiles(target, result.native_configs);
|
|
282
|
+
if (!quiet) {
|
|
283
|
+
log("template update: managed native configs refreshed");
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// --force: update drift baseline hashes so drift clears after regeneration
|
|
288
|
+
if (force) {
|
|
289
|
+
try {
|
|
290
|
+
const { spawnSync } = require("child_process");
|
|
291
|
+
const driftScript = path.join(target, "scripts", "drift_detector.py");
|
|
292
|
+
if (fs.existsSync(driftScript)) {
|
|
293
|
+
spawnSync("python3", [driftScript, "record", "--target", target], { stdio: "inherit" });
|
|
294
|
+
}
|
|
295
|
+
} catch {}
|
|
296
|
+
}
|
|
297
|
+
|
|
253
298
|
if (!quiet) {
|
|
254
299
|
console.log(` account: ${authStatus.email} · plan: ${authStatus.plan || license.plan || "free"} · activation: ${license.status}`);
|
|
255
300
|
console.log(` project: ${boundProject.project_id || identity.project_id}`);
|
package/lib/commands/models.js
CHANGED
|
@@ -54,4 +54,37 @@ function cmdModels(filter) {
|
|
|
54
54
|
console.log(` ${DIM}Full table: https://0dai.dev/models${R}\n`);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
async function cmdModelsRecommend(target, args) {
|
|
58
|
+
const shared = require("../shared");
|
|
59
|
+
const { log, T, R, D, findRepoScript, spawnSync, requirePlan } = shared;
|
|
60
|
+
|
|
61
|
+
const gate = requirePlan("pro", "Model Recommend", target);
|
|
62
|
+
if (gate) { log(gate.error); log(gate.hint); return; }
|
|
63
|
+
|
|
64
|
+
const taskType = args.find((_, i) => args[i - 1] === "--task") || "";
|
|
65
|
+
const goal = args.find((_, i) => args[i - 1] === "--goal") || "";
|
|
66
|
+
const maxCost = parseFloat(args.find((_, i) => args[i - 1] === "--max-cost") || "0");
|
|
67
|
+
const minQuality = parseFloat(args.find((_, i) => args[i - 1] === "--min-quality") || "0");
|
|
68
|
+
const asJson = args.includes("--json");
|
|
69
|
+
|
|
70
|
+
if (!taskType && !goal) {
|
|
71
|
+
console.log("Usage: 0dai models recommend --task TYPE [--goal '...'] [--max-cost N] [--min-quality N] [--json]");
|
|
72
|
+
console.log(" TYPE: feat, fix, refactor, test, docs");
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const recScript = findRepoScript(target, "model_router.py");
|
|
77
|
+
if (!recScript) { log("model router unavailable"); return; }
|
|
78
|
+
|
|
79
|
+
const fwd = [recScript, "recommend", "--target", target];
|
|
80
|
+
if (taskType) fwd.push("--task", taskType);
|
|
81
|
+
if (goal) fwd.push("--goal", goal);
|
|
82
|
+
if (maxCost > 0) fwd.push("--max-cost", String(maxCost));
|
|
83
|
+
if (minQuality > 0) fwd.push("--min-quality", String(minQuality));
|
|
84
|
+
if (asJson) fwd.push("--json");
|
|
85
|
+
|
|
86
|
+
const result = spawnSync("python3", fwd, { stdio: "inherit" });
|
|
87
|
+
if (typeof result.status === "number" && result.status !== 0) process.exit(result.status);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = { cmdModels, cmdModelsRecommend };
|
package/lib/commands/swarm.js
CHANGED
|
@@ -155,7 +155,45 @@ function cmdSwarm(target, sub, args) {
|
|
|
155
155
|
console.log();
|
|
156
156
|
return;
|
|
157
157
|
}
|
|
158
|
-
|
|
158
|
+
if (sub === "estimate") {
|
|
159
|
+
const gate = requirePlan("pro", "Swarm Estimate", target);
|
|
160
|
+
if (gate) { log(gate.error); log(gate.hint); return; }
|
|
161
|
+
const goal = args.find((_, i) => args[i - 1] === "--goal") || "";
|
|
162
|
+
if (!goal) { console.log("Usage: 0dai swarm estimate --goal '...' [--agent claude|codex] [--model tier] [--json]"); return; }
|
|
163
|
+
const agent = args.find((_, i) => args[i - 1] === "--agent") || "";
|
|
164
|
+
const model = args.find((_, i) => args[i - 1] === "--model") || "";
|
|
165
|
+
const asJson = args.includes("--json");
|
|
166
|
+
// Call API for cost estimate
|
|
167
|
+
const identity = shared.buildProjectIdentity(target, shared.collectMetadata(target));
|
|
168
|
+
shared.apiCall("/v1/swarm/estimate", { goal, agent, model_tier: model, project_id: identity.project_id }).then((result) => {
|
|
169
|
+
if (result.error) { log(`error: ${result.error}`); return; }
|
|
170
|
+
if (asJson) { console.log(JSON.stringify(result, null, 2)); return; }
|
|
171
|
+
log(`Cost estimate for: ${goal}`);
|
|
172
|
+
console.log(` ${D}Estimated: $${(result.estimated_cost_usd || 0).toFixed(4)} · ${result.estimated_tokens || "?"} tokens · ${result.estimated_time_s || "?"}s${R}`);
|
|
173
|
+
if (result.model_recommendation) console.log(` ${T}Recommended: ${result.model_recommendation}${R}`);
|
|
174
|
+
if (result.tier_breakdown) {
|
|
175
|
+
for (const [tier, cost] of Object.entries(result.tier_breakdown)) {
|
|
176
|
+
console.log(` ${tier}: $${cost.toFixed(4)}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (sub === "quality") {
|
|
183
|
+
const scorerScript = findRepoScript(target, "quality_scorer.py");
|
|
184
|
+
if (!scorerScript) { log("quality scorer unavailable"); return; }
|
|
185
|
+
const fwd = [scorerScript, "--target", target];
|
|
186
|
+
if (args.includes("--json")) fwd.push("--json");
|
|
187
|
+
for (let i = 2; i < args.length; i++) {
|
|
188
|
+
if (args[i] === "--last" && args[i + 1]) { fwd.push("--last", args[i + 1]); i++; }
|
|
189
|
+
else if (args[i] === "--details" && args[i + 1]) { fwd.push("--details", args[i + 1]); i++; }
|
|
190
|
+
}
|
|
191
|
+
const result = spawnSync("python3", fwd, { stdio: "inherit", timeout: 15000 });
|
|
192
|
+
if (typeof result.status === "number" && result.status !== 0) process.exit(result.status);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
console.log("Usage: 0dai swarm [status|add|delegate|budget|estimate|quality] [--task '...'] [--to agent]");
|
|
196
|
+
console.log(" quality Show quality scores for recent tasks (--last N / --details TASK_ID)");
|
|
159
197
|
}
|
|
160
198
|
|
|
161
199
|
module.exports = { cmdSwarm };
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const shared = require("../shared");
|
|
3
|
+
const { log, T, R, D, fs, path, spawnSync } = shared;
|
|
4
|
+
const os = require("os");
|
|
5
|
+
|
|
6
|
+
const CONFIG_NAME = "workspace.json";
|
|
7
|
+
|
|
8
|
+
function findConfig(target) {
|
|
9
|
+
const local = path.join(target, ".0dai", CONFIG_NAME);
|
|
10
|
+
if (fs.existsSync(local)) return { file: local, scope: "local" };
|
|
11
|
+
const globalFile = path.join(os.homedir(), ".0dai", CONFIG_NAME);
|
|
12
|
+
if (fs.existsSync(globalFile)) return { file: globalFile, scope: "global" };
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function loadConfig(target) {
|
|
17
|
+
const found = findConfig(target);
|
|
18
|
+
if (!found) return null;
|
|
19
|
+
try { return JSON.parse(fs.readFileSync(found.file, "utf8")); } catch { return null; }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function saveConfig(target, ws, globalFlag) {
|
|
23
|
+
const configFile = globalFlag
|
|
24
|
+
? path.join(os.homedir(), ".0dai", CONFIG_NAME)
|
|
25
|
+
: path.join(target, ".0dai", CONFIG_NAME);
|
|
26
|
+
fs.mkdirSync(path.dirname(configFile), { recursive: true, mode: 0o700 });
|
|
27
|
+
fs.writeFileSync(configFile, JSON.stringify(ws, null, 2) + "\n", { mode: 0o600 });
|
|
28
|
+
return configFile;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function tmux(args) {
|
|
32
|
+
try {
|
|
33
|
+
return spawnSync("tmux", args, { encoding: "utf8", timeout: 5000 });
|
|
34
|
+
} catch { return { status: 1, stderr: "tmux not found" }; }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function sessionName(name) {
|
|
38
|
+
return "0dai-" + name.replace(/[^a-z0-9-]/gi, "-").toLowerCase();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function listTmuxSessions() {
|
|
42
|
+
const r = tmux(["list-sessions", "-F", "#{session_name}\t#{session_created}\t#{pane_pid}"]);
|
|
43
|
+
if (r.status !== 0) return [];
|
|
44
|
+
return r.stdout.trim().split("\n").filter(Boolean).map(line => {
|
|
45
|
+
const [name, created, pid] = line.split("\t");
|
|
46
|
+
return { name, created, pid };
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// --- Auto-detection ---
|
|
51
|
+
|
|
52
|
+
function detectSessions(target) {
|
|
53
|
+
const sessions = [];
|
|
54
|
+
|
|
55
|
+
// API server
|
|
56
|
+
if (fs.existsSync(path.join(target, "scripts", "api_server.py"))) {
|
|
57
|
+
sessions.push({
|
|
58
|
+
name: "api",
|
|
59
|
+
cmd: "python3 api_server.py --public --port 8440",
|
|
60
|
+
dir: "scripts",
|
|
61
|
+
auto_start: true,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Web dev (Next.js, etc.)
|
|
66
|
+
const pkgPath = path.join(target, "web", "package.json");
|
|
67
|
+
if (fs.existsSync(pkgPath)) {
|
|
68
|
+
try {
|
|
69
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
70
|
+
if (pkg.scripts && pkg.scripts.dev) {
|
|
71
|
+
sessions.push({ name: "web", cmd: "npm run dev", dir: "web", auto_start: true });
|
|
72
|
+
}
|
|
73
|
+
} catch {}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Uptime monitor
|
|
77
|
+
if (fs.existsSync(path.join(target, "scripts", "uptime_monitor.py"))) {
|
|
78
|
+
sessions.push({
|
|
79
|
+
name: "monitor",
|
|
80
|
+
cmd: "python3 uptime_monitor.py --url http://localhost:8440",
|
|
81
|
+
dir: "scripts",
|
|
82
|
+
auto_start: false,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Procfile support
|
|
87
|
+
const procfile = path.join(target, "Procfile");
|
|
88
|
+
if (fs.existsSync(procfile)) {
|
|
89
|
+
try {
|
|
90
|
+
const lines = fs.readFileSync(procfile, "utf8").split("\n");
|
|
91
|
+
for (const line of lines) {
|
|
92
|
+
const m = line.match(/^(\w+):\s*(.+)$/);
|
|
93
|
+
if (m) {
|
|
94
|
+
const existing = sessions.find(s => s.name === m[1]);
|
|
95
|
+
if (!existing) {
|
|
96
|
+
sessions.push({ name: m[1], cmd: m[2], dir: ".", auto_start: true });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
} catch {}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return sessions;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// --- Commands ---
|
|
107
|
+
|
|
108
|
+
function cmdWorkspaceInit(target, args) {
|
|
109
|
+
const globalFlag = args.includes("--global");
|
|
110
|
+
|
|
111
|
+
if (sessions.length === 0) {
|
|
112
|
+
log("no services detected. Create workspace config manually with: 0dai workspace add");
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const ws = {
|
|
117
|
+
name: path.basename(target),
|
|
118
|
+
root: target,
|
|
119
|
+
sessions,
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const configFile = saveConfig(target, ws, globalFlag);
|
|
123
|
+
log(`workspace config created: ${T}${configFile}${R}`);
|
|
124
|
+
log(`${sessions.length} session(s) detected: ${sessions.map(s => s.name).join(", ")}`);
|
|
125
|
+
log(`run: ${T}0dai workspace up${R}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function cmdWorkspaceAdd(target, args) {
|
|
129
|
+
const nameIdx = args.indexOf("add");
|
|
130
|
+
const name = nameIdx >= 0 ? args[nameIdx + 1] : args[0];
|
|
131
|
+
if (!name) { log("usage: 0dai workspace add <name> --cmd '...' [--dir scripts] [--auto]"); return; }
|
|
132
|
+
|
|
133
|
+
const ws = loadConfig(target) || { name: path.basename(target), sessions: [] };
|
|
134
|
+
const cmdIdx = args.indexOf("--cmd");
|
|
135
|
+
const dirIdx = args.indexOf("--dir");
|
|
136
|
+
const cmd = cmdIdx >= 0 && args[cmdIdx + 1] ? args[cmdIdx + 1] : name;
|
|
137
|
+
const dir = dirIdx >= 0 && args[dirIdx + 1] ? args[dirIdx + 1] : ".";
|
|
138
|
+
const auto = args.includes("--auto");
|
|
139
|
+
|
|
140
|
+
const existing = ws.sessions.findIndex(s => s.name === name);
|
|
141
|
+
const entry = { name, cmd, dir, auto_start: auto };
|
|
142
|
+
if (existing >= 0) ws.sessions[existing] = entry;
|
|
143
|
+
else ws.sessions.push(entry);
|
|
144
|
+
|
|
145
|
+
const found = findConfig(target);
|
|
146
|
+
const configFile = found ? found.file : saveConfig(target, ws, false);
|
|
147
|
+
fs.writeFileSync(configFile, JSON.stringify(ws, null, 2) + "\n", { mode: 0o600 });
|
|
148
|
+
log(`session "${T}${name}${R}" ${existing >= 0 ? "updated" : "added"}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function cmdWorkspaceRm(target, name) {
|
|
152
|
+
if (!name) { log("usage: 0dai workspace rm <name>"); return; }
|
|
153
|
+
const ws = loadConfig(target);
|
|
154
|
+
if (!ws) { log("no workspace config"); return; }
|
|
155
|
+
ws.sessions = ws.sessions.filter(s => s.name !== name);
|
|
156
|
+
const found = findConfig(target);
|
|
157
|
+
fs.writeFileSync(found.file, JSON.stringify(ws, null, 2) + "\n", { mode: 0o600 });
|
|
158
|
+
log(`session "${T}${name}${R}" removed`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function cmdWorkspaceUp(target, args) {
|
|
162
|
+
const ws = loadConfig(target);
|
|
163
|
+
if (!ws) { log("no workspace config. Run: 0dai workspace init"); return; }
|
|
164
|
+
|
|
165
|
+
// Filter by name if specified
|
|
166
|
+
const filterNames = args.filter(a => !a.startsWith("-"));
|
|
167
|
+
const running = listTmuxSessions();
|
|
168
|
+
|
|
169
|
+
log(`starting workspace "${T}${ws.name}${R}"`);
|
|
170
|
+
|
|
171
|
+
for (const s of ws.sessions) {
|
|
172
|
+
if (!s.auto_start) continue;
|
|
173
|
+
if (filterNames.length && !filterNames.includes(s.name)) continue;
|
|
174
|
+
|
|
175
|
+
const sname = sessionName(s.name);
|
|
176
|
+
if (running.find(x => x.name === sname)) {
|
|
177
|
+
log(`${D}${s.name} already running (pid ${running.find(x => x.name === sname).pid})${R}`);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const dir = s.dir ? path.join(ws.root || target, s.dir) : (ws.root || target);
|
|
182
|
+
const cmd = `cd ${dir} && ${s.cmd}`;
|
|
183
|
+
const r = tmux(["new-session", "-d", "-s", sname, "bash", "-lc", cmd]);
|
|
184
|
+
if (r.status === 0) log(`${T}✓${R} ${s.name} started`);
|
|
185
|
+
else log(`${R}✗${R} ${s.name} failed: ${r.stderr?.trim() || r.error || "unknown"}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (filterNames.length === 0) {
|
|
189
|
+
log(`workspace ready. ${D}Status: 0dai workspace status${R}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function cmdWorkspaceDown(target, args) {
|
|
194
|
+
const ws = loadConfig(target);
|
|
195
|
+
if (!ws) { log("no workspace config"); return; }
|
|
196
|
+
|
|
197
|
+
const filterNames = args.filter(a => !a.startsWith("-"));
|
|
198
|
+
log(`stopping workspace "${T}${ws.name}${R}"`);
|
|
199
|
+
|
|
200
|
+
for (const s of ws.sessions) {
|
|
201
|
+
if (filterNames.length && !filterNames.includes(s.name)) continue;
|
|
202
|
+
const sname = sessionName(s.name);
|
|
203
|
+
tmux(["kill-session", "-t", sname]);
|
|
204
|
+
log(`${D}${s.name} stopped${R}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function cmdWorkspaceStatus(target) {
|
|
209
|
+
const ws = loadConfig(target);
|
|
210
|
+
if (!ws) { log("no workspace config. Run: 0dai workspace init"); return; }
|
|
211
|
+
|
|
212
|
+
const running = listTmuxSessions();
|
|
213
|
+
const isTTY = process.stdout.isTTY;
|
|
214
|
+
const G = isTTY ? "\x1b[32m" : "";
|
|
215
|
+
const RD = isTTY ? "\x1b[31m" : "";
|
|
216
|
+
const DIM = isTTY ? "\x1b[2m" : "";
|
|
217
|
+
|
|
218
|
+
console.log(`\n ${T}Workspace: ${ws.name}${R}\n`);
|
|
219
|
+
console.log(` ${"SESSION".padEnd(14)} ${"STATUS".padEnd(10)} ${"PID".padEnd(8)} DIR`);
|
|
220
|
+
console.log(` ${"-".repeat(60)}`);
|
|
221
|
+
|
|
222
|
+
for (const s of ws.sessions) {
|
|
223
|
+
const sname = sessionName(s.name);
|
|
224
|
+
const match = running.find(x => x.name === sname);
|
|
225
|
+
const status = match ? `${G}running${R}` : `${RD}stopped${R}`;
|
|
226
|
+
const pid = match ? match.pid : "—";
|
|
227
|
+
const dir = s.dir || ".";
|
|
228
|
+
console.log(` ${s.name.padEnd(14)} ${status.padEnd(10)} ${String(pid).padEnd(8)} ${DIM}${dir}${R}`);
|
|
229
|
+
}
|
|
230
|
+
console.log();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function cmdWorkspaceAttach(target, name) {
|
|
234
|
+
if (!name) { log("usage: 0dai workspace attach <name>"); return; }
|
|
235
|
+
const sname = sessionName(name);
|
|
236
|
+
const running = listTmuxSessions();
|
|
237
|
+
if (!running.find(x => x.name === sname)) {
|
|
238
|
+
log(`session "${name}" not running. Run: 0dai workspace up`);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
log(`attaching to ${T}${name}${R}`);
|
|
242
|
+
try {
|
|
243
|
+
const { execSync } = require("child_process");
|
|
244
|
+
execSync(`tmux attach -t ${sname}`, { stdio: "inherit" });
|
|
245
|
+
} catch (e) { log(`error: ${e.message}`); }
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function cmdWorkspaceLogs(target, name, args) {
|
|
249
|
+
if (!name) { log("usage: 0dai workspace logs <name> [-f]"); return; }
|
|
250
|
+
const sname = sessionName(name);
|
|
251
|
+
const running = listTmuxSessions();
|
|
252
|
+
if (!running.find(x => x.name === sname)) { log(`session "${name}" not running`); return; }
|
|
253
|
+
|
|
254
|
+
const follow = args.includes("-f") || args.includes("--follow");
|
|
255
|
+
if (follow) {
|
|
256
|
+
// Live follow mode
|
|
257
|
+
log(`following ${T}${name}${R} logs (Ctrl+C to exit)`);
|
|
258
|
+
try {
|
|
259
|
+
const { execSync } = require("child_process");
|
|
260
|
+
execSync(`tmux capture-pane -t ${sname} -p -e`, { stdio: "inherit" });
|
|
261
|
+
} catch (e) { log(`error: ${e.message}`); }
|
|
262
|
+
} else {
|
|
263
|
+
const r = tmux(["capture-pane", "-t", sname, "-p"]);
|
|
264
|
+
if (r.stdout) {
|
|
265
|
+
const lines = r.stdout.trim().split("\n").slice(-50);
|
|
266
|
+
console.log(lines.join("\n"));
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function cmdWorkspace(target, sub, args) {
|
|
272
|
+
switch (sub) {
|
|
273
|
+
case "init": cmdWorkspaceInit(target, args); break;
|
|
274
|
+
case "up": cmdWorkspaceUp(target, args); break;
|
|
275
|
+
case "down": cmdWorkspaceDown(target, args); break;
|
|
276
|
+
case "status": cmdWorkspaceStatus(target); break;
|
|
277
|
+
case "attach": cmdWorkspaceAttach(target, args[0]); break;
|
|
278
|
+
case "logs": cmdWorkspaceLogs(target, args[0], args); break;
|
|
279
|
+
case "add": cmdWorkspaceAdd(target, args); break;
|
|
280
|
+
case "rm": cmdWorkspaceRm(target, args[0]); break;
|
|
281
|
+
default:
|
|
282
|
+
console.log(`\n ${T}0dai workspace${R} — tmux session manager\n`);
|
|
283
|
+
console.log("Commands:");
|
|
284
|
+
console.log(" workspace init [--global] Create config (auto-detect services)");
|
|
285
|
+
console.log(" workspace up [name...] Start auto_start sessions (filter by name)");
|
|
286
|
+
console.log(" workspace down [name...] Stop sessions (filter by name)");
|
|
287
|
+
console.log(" workspace status Show session status table");
|
|
288
|
+
console.log(" workspace attach <name> Attach to a running session");
|
|
289
|
+
console.log(" workspace logs <name> [-f] Show session output (-f = follow)");
|
|
290
|
+
console.log(" workspace add <name> --cmd '...' [--dir X] [--auto]");
|
|
291
|
+
console.log(" workspace rm <name> Remove session from config");
|
|
292
|
+
console.log();
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
module.exports = { cmdWorkspace };
|
package/lib/shared.js
CHANGED
|
@@ -48,6 +48,10 @@ const CONFIG_DIR = path.join(os.homedir(), ".0dai");
|
|
|
48
48
|
const AUTH_FILE = path.join(CONFIG_DIR, "auth.json");
|
|
49
49
|
const VERSION_CHECK_FILE = path.join(CONFIG_DIR, ".version_check");
|
|
50
50
|
const PROJECTS_FILE = path.join(CONFIG_DIR, "projects.json");
|
|
51
|
+
const MANAGED_BEGIN = "<!-- 0dai:managed:begin -->";
|
|
52
|
+
const MANAGED_END = "<!-- 0dai:managed:end -->";
|
|
53
|
+
const LEGACY_MANAGED_BEGIN = "<!-- zerodayai:managed:begin -->";
|
|
54
|
+
const LEGACY_MANAGED_END = "<!-- zerodayai:managed:end -->";
|
|
51
55
|
|
|
52
56
|
// --- API ---
|
|
53
57
|
function apiCall(endpoint, data) {
|
|
@@ -180,6 +184,31 @@ function mergeSettingsJson(existing, incoming) {
|
|
|
180
184
|
} catch { return incoming; }
|
|
181
185
|
}
|
|
182
186
|
|
|
187
|
+
function mergeManagedMarkdown(existing, incoming) {
|
|
188
|
+
let src = incoming;
|
|
189
|
+
if (src.startsWith("# managed: true")) {
|
|
190
|
+
src = src.split("\n").slice(1).join("\n").trimStart();
|
|
191
|
+
}
|
|
192
|
+
const managedBody = `${MANAGED_BEGIN}\n${src.trim()}\n${MANAGED_END}\n`;
|
|
193
|
+
if (existing.includes(MANAGED_BEGIN) && existing.includes(MANAGED_END)) {
|
|
194
|
+
const start = existing.indexOf(MANAGED_BEGIN);
|
|
195
|
+
const finish = existing.indexOf(MANAGED_END) + MANAGED_END.length;
|
|
196
|
+
return existing.slice(0, start) + managedBody + existing.slice(finish);
|
|
197
|
+
}
|
|
198
|
+
if (existing.includes(LEGACY_MANAGED_BEGIN) && existing.includes(LEGACY_MANAGED_END)) {
|
|
199
|
+
const finish = existing.indexOf(LEGACY_MANAGED_END) + LEGACY_MANAGED_END.length;
|
|
200
|
+
const rest = existing.slice(finish).trimStart();
|
|
201
|
+
return rest ? `${managedBody}\n${rest}` : managedBody;
|
|
202
|
+
}
|
|
203
|
+
return `${managedBody}\n${existing}`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function contentLooksManaged(existing) {
|
|
207
|
+
return existing.includes("managed: true") || existing.includes('"managed": true') ||
|
|
208
|
+
(existing.includes(MANAGED_BEGIN) && existing.includes(MANAGED_END)) ||
|
|
209
|
+
(existing.includes(LEGACY_MANAGED_BEGIN) && existing.includes(LEGACY_MANAGED_END));
|
|
210
|
+
}
|
|
211
|
+
|
|
183
212
|
function writeFiles(target, files) {
|
|
184
213
|
let created = 0, updated = 0, unchanged = 0, merged = 0, skipped = 0;
|
|
185
214
|
const targetResolved = path.resolve(target);
|
|
@@ -212,6 +241,54 @@ function writeFiles(target, files) {
|
|
|
212
241
|
return created + updated;
|
|
213
242
|
}
|
|
214
243
|
|
|
244
|
+
function writeManagedFiles(target, files) {
|
|
245
|
+
let created = 0, updated = 0, unchanged = 0, merged = 0, staged = 0, skipped = 0;
|
|
246
|
+
const targetResolved = path.resolve(target);
|
|
247
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
248
|
+
if (typeof rel !== "string" || !rel || path.isAbsolute(rel) || rel.split(/[/\\]/).includes("..")) {
|
|
249
|
+
skipped++; continue;
|
|
250
|
+
}
|
|
251
|
+
const p = path.resolve(targetResolved, rel);
|
|
252
|
+
if (!p.startsWith(targetResolved + path.sep) && p !== targetResolved) { skipped++; continue; }
|
|
253
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
254
|
+
if (!fs.existsSync(p)) {
|
|
255
|
+
fs.writeFileSync(p, content, "utf8");
|
|
256
|
+
created++;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
const existing = fs.readFileSync(p, "utf8");
|
|
260
|
+
if (existing === content) {
|
|
261
|
+
unchanged++;
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (rel.endsWith("settings.json")) {
|
|
266
|
+
fs.writeFileSync(p, mergeSettingsJson(existing, content), "utf8");
|
|
267
|
+
merged++;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
if (rel === "AGENTS.md" || rel.endsWith("/CLAUDE.md")) {
|
|
271
|
+
fs.writeFileSync(p, mergeManagedMarkdown(existing, content), "utf8");
|
|
272
|
+
merged++;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (!contentLooksManaged(existing)) {
|
|
276
|
+
fs.writeFileSync(`${p}.generated`, content, "utf8");
|
|
277
|
+
staged++;
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
fs.writeFileSync(p, content, "utf8");
|
|
282
|
+
updated++;
|
|
283
|
+
}
|
|
284
|
+
const parts = [`${created} created`, `${updated} updated`, `${unchanged} unchanged`];
|
|
285
|
+
if (merged) parts.push(`${merged} merged`);
|
|
286
|
+
if (staged) parts.push(`${staged} staged`);
|
|
287
|
+
if (skipped) parts.push(`${skipped} skipped (unsafe path)`);
|
|
288
|
+
log(parts.join(", "));
|
|
289
|
+
return created + updated + merged;
|
|
290
|
+
}
|
|
291
|
+
|
|
215
292
|
// --- Repo Script Lookup ---
|
|
216
293
|
function findRepoScript(target, scriptName) {
|
|
217
294
|
const candidates = [
|
|
@@ -275,7 +352,7 @@ module.exports = {
|
|
|
275
352
|
// Project
|
|
276
353
|
sendProjectHeartbeat, recordExperienceEvent,
|
|
277
354
|
// Files
|
|
278
|
-
mergeSettingsJson, writeFiles, findRepoScript,
|
|
355
|
+
mergeSettingsJson, mergeManagedMarkdown, writeFiles, writeManagedFiles, findRepoScript,
|
|
279
356
|
// Version
|
|
280
357
|
checkVersion,
|
|
281
358
|
// Re-exports for convenience
|
package/lib/wizard.js
CHANGED
|
@@ -69,7 +69,7 @@ async function stepAuth(rl) {
|
|
|
69
69
|
console.log(" 0dai works in two modes:");
|
|
70
70
|
console.log("");
|
|
71
71
|
console.log(" Local mode — generate configs offline, no account needed");
|
|
72
|
-
console.log(" Cloud mode — full graph, swarm, roaming,
|
|
72
|
+
console.log(" Cloud mode — full graph, swarm, roaming, 56 MCP tools");
|
|
73
73
|
console.log("");
|
|
74
74
|
|
|
75
75
|
const answer = await ask(rl, " Sign in now? [Y/n]: ", "y");
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@0dai-dev/cli",
|
|
3
|
-
"version": "4.
|
|
4
|
-
"description": "One config layer for
|
|
3
|
+
"version": "4.1.0",
|
|
4
|
+
"description": "One config layer for seven AI coding agents — Claude Code, Codex, OpenCode, Gemini, Aider, Qoder",
|
|
5
5
|
"bin": {
|
|
6
6
|
"0dai": "./bin/0dai.js"
|
|
7
7
|
},
|
|
@@ -12,8 +12,11 @@
|
|
|
12
12
|
"codex",
|
|
13
13
|
"gemini",
|
|
14
14
|
"aider",
|
|
15
|
+
"opencode",
|
|
16
|
+
"qoder",
|
|
15
17
|
"developer-tools",
|
|
16
|
-
"mcp"
|
|
18
|
+
"mcp",
|
|
19
|
+
"cli"
|
|
17
20
|
],
|
|
18
21
|
"author": "0dai-dev <dev@0dai.dev>",
|
|
19
22
|
"license": "MIT",
|
|
@@ -35,7 +38,9 @@
|
|
|
35
38
|
"README.md"
|
|
36
39
|
],
|
|
37
40
|
"dependencies": {
|
|
38
|
-
"@clack/prompts": "^1.2.0"
|
|
41
|
+
"@clack/prompts": "^1.2.0"
|
|
42
|
+
},
|
|
43
|
+
"optionalDependencies": {
|
|
39
44
|
"node-pty": "^1.0.0"
|
|
40
45
|
}
|
|
41
46
|
}
|