@0dai-dev/cli 4.4.1 → 4.4.3
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/lib/commands/init.js +2 -2
- package/lib/commands/run.js +48 -4
- package/lib/onboarding.js +1 -0
- package/lib/python/usage_ledger.py +1 -1
- package/lib/run/local_executor.js +17 -0
- package/lib/utils/identity.js +12 -1
- package/lib/wizard.js +2 -0
- package/package.json +1 -1
package/lib/commands/init.js
CHANGED
|
@@ -696,8 +696,8 @@ async function cmdInit(target, args = []) {
|
|
|
696
696
|
console.log(` ${D}1.${R} Check health: ${D}0dai doctor${R}`);
|
|
697
697
|
if (agents.length > 0) {
|
|
698
698
|
const a = agents[0];
|
|
699
|
-
console.log(` ${D}2.${R}
|
|
700
|
-
console.log(` ${D}(${agents.join(", ")} detected —
|
|
699
|
+
console.log(` ${D}2.${R} Run a task now: ${D}0dai run "write tests for auth" --now${R}`);
|
|
700
|
+
console.log(` ${D}(${agents.join(", ")} detected — runs ${a} locally and prints a scored receipt; drop --now to just queue)${R}`);
|
|
701
701
|
} else {
|
|
702
702
|
console.log(` ${D}2.${R} Install an agent CLI to enable delegation:`);
|
|
703
703
|
console.log(` ${D}claude:${R} npm i -g @anthropic-ai/claude-code ${D}(or Pro subscription)${R}`);
|
package/lib/commands/run.js
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
const shared = require("../shared");
|
|
3
|
-
const { log, T, R, D, fs, path, apiCall } = shared;
|
|
3
|
+
const { log, T, R, D, fs, path, apiCall, spawnSync } = shared;
|
|
4
|
+
|
|
5
|
+
// Best-effort git read: returns trimmed stdout, or null when not a git repo /
|
|
6
|
+
// git missing. Never throws — safety output degrades silently outside git.
|
|
7
|
+
function _git(target, args) {
|
|
8
|
+
try {
|
|
9
|
+
const r = spawnSync("git", args, { cwd: target, encoding: "utf8", timeout: 5000, stdio: ["ignore", "pipe", "ignore"] });
|
|
10
|
+
return r && r.status === 0 ? (r.stdout || "").trim() : null;
|
|
11
|
+
} catch {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
4
15
|
const {
|
|
5
16
|
estimateRunCost,
|
|
6
17
|
formatTokens,
|
|
@@ -164,13 +175,36 @@ async function cmdRun(goal, target, args = []) {
|
|
|
164
175
|
// Execute the just-queued tasks locally (synchronous), emit a scored receipt,
|
|
165
176
|
// move drained task files queue→done, and record the executed activation outcome.
|
|
166
177
|
function runNow(target, queueDir, entries, { goal, maxCost, costEstimateUsd, identity }) {
|
|
167
|
-
const { drainTasks } = require("../run/local_executor");
|
|
178
|
+
const { drainTasks, previewInvocation } = require("../run/local_executor");
|
|
168
179
|
const { buildReceipt, writeReceipt, renderReceipt } = require("../run/scored_receipt");
|
|
169
180
|
|
|
181
|
+
// Safety snapshot BEFORE anything runs: capture the pre-run commit so the user
|
|
182
|
+
// has a one-line undo, and checkpoint any uncommitted work via `git stash
|
|
183
|
+
// create` (captures a dangling commit WITHOUT touching the working tree or the
|
|
184
|
+
// shared stash stack — never the destructive `git stash` that CLAUDE.md §3
|
|
185
|
+
// forbids). All best-effort: outside a git repo these are null and the run
|
|
186
|
+
// still proceeds.
|
|
187
|
+
const preSha = _git(target, ["rev-parse", "HEAD"]);
|
|
188
|
+
const dirty = preSha != null ? Boolean(_git(target, ["status", "--porcelain"])) : false;
|
|
189
|
+
// `git stash create` captures only TRACKED changes → returns "" for an
|
|
190
|
+
// untracked-only dirty tree. Coerce that to null so we never advertise a
|
|
191
|
+
// checkpoint that wasn't created (untracked files survive a reset anyway).
|
|
192
|
+
const checkpointSha = dirty ? (_git(target, ["stash", "create"]) || null) : null;
|
|
193
|
+
|
|
170
194
|
console.log(
|
|
171
195
|
`\n ${T}Executing locally${R} ${D}(--now)${R} — running the assigned agent on this`
|
|
172
|
-
+ ` repo now; it may modify files
|
|
196
|
+
+ ` repo now; it may modify files. Exact command(s):\n`,
|
|
173
197
|
);
|
|
198
|
+
for (const e of entries) {
|
|
199
|
+
console.log(` ${D}$ ${previewInvocation(e, { budgetUsd: maxCost })}${R}`);
|
|
200
|
+
}
|
|
201
|
+
if (preSha) {
|
|
202
|
+
let treeNote = " (clean)";
|
|
203
|
+
if (dirty && checkpointSha) treeNote = ` (dirty — tracked work checkpointed to ${checkpointSha.slice(0, 9)})`;
|
|
204
|
+
else if (dirty) treeNote = " (dirty — untracked files only; an undo leaves them in place)";
|
|
205
|
+
console.log(` ${D}working tree @ ${preSha.slice(0, 9)}${treeNote}${R}`);
|
|
206
|
+
}
|
|
207
|
+
console.log("");
|
|
174
208
|
|
|
175
209
|
const results = drainTasks(entries, {
|
|
176
210
|
cwd: target,
|
|
@@ -189,11 +223,21 @@ function runNow(target, queueDir, entries, { goal, maxCost, costEstimateUsd, ide
|
|
|
189
223
|
const file = writeReceipt(target, receipt);
|
|
190
224
|
console.log(renderReceipt(receipt, { T, R, D }));
|
|
191
225
|
if (file) {
|
|
192
|
-
console.log(` ${D}receipt: ${path.relative(target, file)}${R}
|
|
226
|
+
console.log(` ${D}receipt: ${path.relative(target, file)}${R}`);
|
|
193
227
|
} else {
|
|
194
228
|
log(`warn: could not persist run receipt to ai/meta/receipts/${runId}.json`);
|
|
195
229
|
}
|
|
196
230
|
|
|
231
|
+
// Undo footer: the literal commands to inspect or reverse what the agent did.
|
|
232
|
+
if (preSha) {
|
|
233
|
+
console.log(` ${D}inspect: git diff ${preSha.slice(0, 12)} | revert tracked changes: git reset --hard ${preSha.slice(0, 12)}${R}`);
|
|
234
|
+
console.log(` ${D}(new files the agent added are kept by reset — 'git status' to see them)${R}`);
|
|
235
|
+
if (checkpointSha) {
|
|
236
|
+
console.log(` ${D}restore your uncommitted work after a revert: git stash apply ${checkpointSha.slice(0, 12)}${R}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
console.log("");
|
|
240
|
+
|
|
197
241
|
// Move drained task files queue→done so `0dai watch` / `status` stay consistent.
|
|
198
242
|
const doneDir = path.join(target, "ai", "swarm", "done");
|
|
199
243
|
try { fs.mkdirSync(doneDir, { recursive: true }); } catch {}
|
package/lib/onboarding.js
CHANGED
|
@@ -44,6 +44,7 @@ function showWhatsNext(mode, isAuthed) {
|
|
|
44
44
|
console.log(" \u2705 0dai initialized! Your AI agents are configured.");
|
|
45
45
|
console.log("");
|
|
46
46
|
console.log(" What's next:");
|
|
47
|
+
console.log(" \u2610 0dai run \"add a test for X\" --now \u2014 run a task locally, get a scored receipt");
|
|
47
48
|
console.log(" \u2610 0dai status \u2014 check your config");
|
|
48
49
|
console.log(" \u2610 0dai doctor \u2014 verify everything works");
|
|
49
50
|
if (mode === "local" && !isAuthed) {
|
|
@@ -53,7 +53,7 @@ FREE_TIER_LIMITS = {
|
|
|
53
53
|
|
|
54
54
|
# Plan entitlements (mirrors tier_gate.py PLAN_LIMITS for task budgets).
|
|
55
55
|
PLAN_ENTITLEMENTS = {
|
|
56
|
-
"free": {"daily_tasks":
|
|
56
|
+
"free": {"daily_tasks": 1, "monthly_budget_usd": 0.0, "max_model_tier": "fast", "free_tier": FREE_TIER_LIMITS},
|
|
57
57
|
"pro": {"daily_tasks": 50, "monthly_budget_usd": 15.0, "max_model_tier": "balanced"},
|
|
58
58
|
"team": {"daily_tasks": 200, "monthly_budget_usd": 49.0, "max_model_tier": "deep"},
|
|
59
59
|
"enterprise": {"daily_tasks": 999999, "monthly_budget_usd": 999999.0, "max_model_tier": "deep"},
|
|
@@ -73,6 +73,22 @@ function buildPrompt(task) {
|
|
|
73
73
|
return lines.join("\n");
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
// Human-readable preview of the exact command `--now` will run, for the
|
|
77
|
+
// pre-run transparency line. The prompt arg (long, multi-line) is collapsed to
|
|
78
|
+
// a short quoted preview so a skeptic sees the literal binary + flags before any
|
|
79
|
+
// agent touches their tree. Returns a note (not a throw) for unsupported agents.
|
|
80
|
+
function previewInvocation(task, opts = {}) {
|
|
81
|
+
const agent = (task && task.assigned_to) || opts.defaultAgent || "claude";
|
|
82
|
+
const inv = buildInvocation(agent, buildPrompt(task || {}), { budgetUsd: opts.budgetUsd });
|
|
83
|
+
if (!inv) return `# no headless invocation for agent '${agent}' — task skipped`;
|
|
84
|
+
const args = inv.args.map((a) => {
|
|
85
|
+
const oneLine = String(a).replace(/\s+/g, " ").trim();
|
|
86
|
+
if (oneLine.length > 48) return `"${oneLine.slice(0, 45)}…"`;
|
|
87
|
+
return /\s/.test(oneLine) ? `"${oneLine}"` : oneLine;
|
|
88
|
+
});
|
|
89
|
+
return `${inv.bin} ${args.join(" ")}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
76
92
|
// Default command runner — a thin wrapper over spawnSync so tests can inject a
|
|
77
93
|
// fake. Signature: runner(bin, args, { cwd, timeoutMs }) -> spawnSync-like result.
|
|
78
94
|
function defaultRunner(bin, args, opts = {}) {
|
|
@@ -216,6 +232,7 @@ function drainTasks(tasks, opts = {}) {
|
|
|
216
232
|
module.exports = {
|
|
217
233
|
buildInvocation,
|
|
218
234
|
buildPrompt,
|
|
235
|
+
previewInvocation,
|
|
219
236
|
executeTask,
|
|
220
237
|
drainTasks,
|
|
221
238
|
agentInstalled,
|
package/lib/utils/identity.js
CHANGED
|
@@ -322,15 +322,17 @@ function detectStackHint(projectFiles, manifestContents) {
|
|
|
322
322
|
// Node deps fallback (#4493): when config files are absent, infer the stack from
|
|
323
323
|
// package.json dependencies so an account-free `init --local` still gets a real stack
|
|
324
324
|
// instead of "unknown". Order matters — react-native ships react, so it must win first.
|
|
325
|
+
let validPackageJson = false;
|
|
325
326
|
if (manifestContents["package.json"]) {
|
|
326
327
|
try {
|
|
327
328
|
const pkg = JSON.parse(manifestContents["package.json"]);
|
|
329
|
+
validPackageJson = true;
|
|
328
330
|
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
329
331
|
if (deps["react-native"] || deps.expo) return "react-native";
|
|
330
332
|
if (deps.next) return "nextjs";
|
|
331
333
|
if (deps.react || deps.vue || deps.svelte || deps["@angular/core"]) return "frontend";
|
|
332
334
|
if (deps.express || deps.fastify || deps.koa || deps["@nestjs/core"] || deps["@hapi/hapi"]) return "backend-api";
|
|
333
|
-
} catch {}
|
|
335
|
+
} catch { validPackageJson = false; }
|
|
334
336
|
}
|
|
335
337
|
|
|
336
338
|
if (manifestContents["go.mod"]) return "go-service";
|
|
@@ -343,6 +345,15 @@ function detectStackHint(projectFiles, manifestContents) {
|
|
|
343
345
|
}
|
|
344
346
|
|
|
345
347
|
if (projectFiles.some((name) => name.startsWith("apps/") || name.startsWith("packages/"))) return "monorepo";
|
|
348
|
+
|
|
349
|
+
// Language floor (#4515 follow-up): a valid package.json with no framework, no
|
|
350
|
+
// go.mod / pyproject, and no monorepo layout is still unmistakably a Node
|
|
351
|
+
// project — label it "node", not "unknown". This matches the unconditional
|
|
352
|
+
// go.mod→go-service and pyproject→python-service floors above (a bare go/python
|
|
353
|
+
// project gets a language label; a bare Node lib/CLI must too). "node" is a real
|
|
354
|
+
// label the offline scaffold writes as-is; it is placed AFTER the go/python/
|
|
355
|
+
// monorepo checks so a polyglot repo still resolves to the more specific stack.
|
|
356
|
+
if (validPackageJson) return "node";
|
|
346
357
|
return "unknown";
|
|
347
358
|
}
|
|
348
359
|
|
package/lib/wizard.js
CHANGED
|
@@ -270,6 +270,7 @@ function stepNext(mode) {
|
|
|
270
270
|
console.log(" ✅ 0dai is ready!");
|
|
271
271
|
console.log("");
|
|
272
272
|
console.log(" Try these commands:");
|
|
273
|
+
console.log(" 0dai run \"...\" --now — run a task locally + get a scored receipt");
|
|
273
274
|
console.log(" 0dai status — see your project config");
|
|
274
275
|
console.log(" 0dai doctor — check config health");
|
|
275
276
|
if (mode === "cloud") {
|
|
@@ -380,6 +381,7 @@ module.exports = {
|
|
|
380
381
|
needsWizard,
|
|
381
382
|
isInteractive,
|
|
382
383
|
stepGenerate,
|
|
384
|
+
stepNext,
|
|
383
385
|
detectLocalStack,
|
|
384
386
|
detectProjectCommands,
|
|
385
387
|
AGENTS,
|
package/package.json
CHANGED