@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.
@@ -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} Try delegation: ${D}0dai run "write tests for auth"${R}`);
700
- console.log(` ${D}(${agents.join(", ")} detected — delegation will use ${a} by default)${R}`);
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}`);
@@ -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.\n`,
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}\n`);
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": 0, "monthly_budget_usd": 0.0, "max_model_tier": "fast", "free_tier": FREE_TIER_LIMITS},
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,
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@0dai-dev/cli",
3
- "version": "4.4.1",
3
+ "version": "4.4.3",
4
4
  "description": "One config layer for seven AI coding agents \u2014 Claude Code, Codex, OpenCode, Gemini, Aider, Qoder, Cursor",
5
5
  "bin": {
6
6
  "0dai": "./bin/0dai.js"