@0dai-dev/cli 4.3.4 → 4.3.6
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 +75 -12
- package/bin/0dai.js +97 -11
- package/lib/commands/auth.js +62 -6
- package/lib/commands/detect.js +10 -4
- package/lib/commands/doctor.js +61 -17
- package/lib/commands/export.js +73 -0
- package/lib/commands/init.js +77 -3
- package/lib/commands/mcp.js +33 -3
- package/lib/commands/run.js +64 -4
- package/lib/commands/status.js +9 -0
- package/lib/commands/trust.js +286 -0
- package/lib/commands/upgrade.js +58 -0
- package/lib/commands/vault.js +3 -1
- package/lib/shared.js +2 -2
- package/lib/utils/activation_telemetry.js +378 -0
- package/lib/utils/constants.js +7 -0
- package/lib/utils/export-bundler.js +285 -0
- package/lib/utils/identity.js +14 -1
- package/lib/utils/plan.js +10 -0
- package/lib/utils/run_cost.js +91 -0
- package/package.json +8 -4
- package/lib/tui/index.mjs +0 -34994
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) 2026 0dai.dev
|
|
3
|
+
//
|
|
4
|
+
// F14 G4 Phase 2 — `0dai export --all` CLI command.
|
|
5
|
+
//
|
|
6
|
+
// Bundles user-owned tenant data into a tarball per the contract
|
|
7
|
+
// at docs/governance/data-export-contract.md (F14 G4 Phase 1 /
|
|
8
|
+
// PR #3569). Phase 2 ships personas + path-protect.yaml as real
|
|
9
|
+
// data, plus usage-ledger.jsonl when present; the other 6 surfaces emit placeholder JSON
|
|
10
|
+
// ({"_status": "not-implemented", "since": "F14 G4 Phase 2"}).
|
|
11
|
+
//
|
|
12
|
+
// Cosign signing is deferred to Phase 3 (stubs .sig + .crt as
|
|
13
|
+
// empty files alongside the tarball so the layout contract holds).
|
|
14
|
+
//
|
|
15
|
+
// Usage:
|
|
16
|
+
// 0dai export --all [--output PATH] [--target DIR]
|
|
17
|
+
|
|
18
|
+
"use strict";
|
|
19
|
+
|
|
20
|
+
const shared = require("../shared");
|
|
21
|
+
const { T, R, D, log } = shared;
|
|
22
|
+
const { buildExportTarball } = require("../utils/export-bundler");
|
|
23
|
+
|
|
24
|
+
function parseExportArgs(args) {
|
|
25
|
+
const out = { all: false, output: null, target: process.cwd() };
|
|
26
|
+
for (let i = 0; i < args.length; i++) {
|
|
27
|
+
const a = args[i];
|
|
28
|
+
if (a === "--all") out.all = true;
|
|
29
|
+
else if (a === "--output" && args[i + 1]) { out.output = args[i + 1]; i++; }
|
|
30
|
+
else if (a === "--target" && args[i + 1]) { out.target = args[i + 1]; i++; }
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function defaultOutputName() {
|
|
36
|
+
const now = new Date().toISOString().replace(/[:.]/g, "-").replace(/Z$/, "Z");
|
|
37
|
+
return `0dai-export-${now}.tar.gz`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function cmdExport(target, args) {
|
|
41
|
+
const opts = parseExportArgs(args);
|
|
42
|
+
if (!opts.all) {
|
|
43
|
+
console.error(`${T}[0dai-export]${R} --all is required (partial export is a Phase 2 follow-up)`);
|
|
44
|
+
process.exit(2);
|
|
45
|
+
}
|
|
46
|
+
const sourceRoot = opts.target || target || process.cwd();
|
|
47
|
+
const outputPath = opts.output || defaultOutputName();
|
|
48
|
+
|
|
49
|
+
log(`tenant data source: ${sourceRoot}`);
|
|
50
|
+
log(`output: ${outputPath}`);
|
|
51
|
+
|
|
52
|
+
let result;
|
|
53
|
+
try {
|
|
54
|
+
result = await buildExportTarball({ sourceRoot, outputPath });
|
|
55
|
+
} catch (err) {
|
|
56
|
+
console.error(`${T}[0dai-export]${R} bundle failed: ${err.message}`);
|
|
57
|
+
process.exit(3);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
log(`personas: ${result.counts.personas} bundled`);
|
|
61
|
+
log(`path-protect: ${result.counts.pathProtect} file(s)`);
|
|
62
|
+
log(`usage-ledger: ${result.counts.usageLedger} file(s)`);
|
|
63
|
+
log(`placeholders: ${result.counts.placeholders} surfaces (Phase 2 not-implemented)`);
|
|
64
|
+
log(`tarball: ${result.tarballPath}`);
|
|
65
|
+
log(`sha256: ${result.sha256}`);
|
|
66
|
+
if (result.signed) {
|
|
67
|
+
log(`signature: cosign keyless signed (${result.sigPath})`);
|
|
68
|
+
} else {
|
|
69
|
+
log(`${D}signature: ${result.signSkipReason} — stub .sig + .crt written${R}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = { cmdExport };
|
package/lib/commands/init.js
CHANGED
|
@@ -7,7 +7,7 @@ const {
|
|
|
7
7
|
VERSION, SUPPORTED_CLIS,
|
|
8
8
|
CONFIG_DIR, PROJECTS_FILE,
|
|
9
9
|
apiCall, makeEnsureAuthenticated, ensureLicenseActivation, loadAuthState,
|
|
10
|
-
collectMetadata, buildProjectIdentity, registerProject,
|
|
10
|
+
collectMetadata, buildProjectIdentity, registerProject, hashManifestFiles,
|
|
11
11
|
writeFiles, sendProjectHeartbeat, recordExperienceEvent,
|
|
12
12
|
logFirstRunSuccess,
|
|
13
13
|
} = shared;
|
|
@@ -15,6 +15,7 @@ const { cmdAuthLogin, ensureAccountForActivation, parseActivationArgs } = requir
|
|
|
15
15
|
const { ensureGithubFlowPolicy, warnHooksPathDrift } = require("./gh");
|
|
16
16
|
const { renderFileMapDiff, confirmOrExit, shouldAutoYes } = require("../utils/diff-preview");
|
|
17
17
|
const { bootstrapMcp } = require("../utils/mcp-auth");
|
|
18
|
+
const { recordActivationInit } = require("../utils/activation_telemetry");
|
|
18
19
|
|
|
19
20
|
const ensureAuthenticated = makeEnsureAuthenticated(cmdAuthLogin);
|
|
20
21
|
const SYNC_FULL_CONTENT_LIMIT = 10000;
|
|
@@ -363,11 +364,15 @@ async function cmdInit(target, args = []) {
|
|
|
363
364
|
else if (!dryRun) log(`sending to API (${projectFiles.length} files, ${clis.length} CLIs)...`);
|
|
364
365
|
const result = await apiCall("/v1/init", {
|
|
365
366
|
project_files: projectFiles,
|
|
366
|
-
|
|
367
|
+
// Privacy: send only filenames + SHA-256 hashes, never raw content (closes #4016).
|
|
368
|
+
// Local detection (inferProjectName, detectStackHint) still reads content locally —
|
|
369
|
+
// the results are passed as project_name and stack so the server needs no raw content.
|
|
370
|
+
manifest_files: hashManifestFiles(manifestContents),
|
|
367
371
|
available_clis: clis,
|
|
368
372
|
dry_run: dryRun,
|
|
369
373
|
minimal: minimal,
|
|
370
374
|
project_name: identity.project_name,
|
|
375
|
+
stack: identity.stack,
|
|
371
376
|
project_id: boundProject.project_id || identity.project_id,
|
|
372
377
|
remote_origin: identity.remote_origin,
|
|
373
378
|
origin: identity.origin,
|
|
@@ -463,6 +468,10 @@ async function cmdInit(target, args = []) {
|
|
|
463
468
|
// First-run proof gate (issue #342). All 4 gates pass once we reach here:
|
|
464
469
|
// license active (line above), project bound, ai/ layer written, heartbeat sent.
|
|
465
470
|
// Idempotent — only fires once per project. See docs/first-run.md.
|
|
471
|
+
recordActivationInit(target, boundProject.project_id || identity.project_id, {
|
|
472
|
+
stack: result.stack || identity.stack || "unknown",
|
|
473
|
+
});
|
|
474
|
+
|
|
466
475
|
const firstRun = logFirstRunSuccess(target, {
|
|
467
476
|
license: true,
|
|
468
477
|
project_bound: true,
|
|
@@ -557,6 +566,60 @@ function normalizeEnvironmentManifest(target) {
|
|
|
557
566
|
return { path: filePath, changed: true };
|
|
558
567
|
}
|
|
559
568
|
|
|
569
|
+
// runDocLinkCheck — SPEC-028 Phase 2 (#2689). After the managed layer has been
|
|
570
|
+
// written, run scripts/doc_link_check.py to surface broken/closed cross-refs
|
|
571
|
+
// in ai/ and docs/. Warn-only by default so a flaky reference never blocks an
|
|
572
|
+
// otherwise successful sync; --strict-links promotes failures to a non-zero
|
|
573
|
+
// exit, and --skip-link-check bypasses the hook entirely.
|
|
574
|
+
//
|
|
575
|
+
// The hook intentionally stays silent when the script is missing (Phase 1 may
|
|
576
|
+
// not have shipped to a downstream project) and when python3 is unavailable.
|
|
577
|
+
// Output is streamed inline via stdio: "inherit" so authors see exact paths.
|
|
578
|
+
function runDocLinkCheck(target, args = [], options = {}) {
|
|
579
|
+
if (args.includes("--skip-link-check")) {
|
|
580
|
+
return { skipped: true, reason: "flag" };
|
|
581
|
+
}
|
|
582
|
+
const script = path.join(target, "scripts", "doc_link_check.py");
|
|
583
|
+
if (!fs.existsSync(script)) {
|
|
584
|
+
return { skipped: true, reason: "missing-script" };
|
|
585
|
+
}
|
|
586
|
+
const quiet = !!options.quiet;
|
|
587
|
+
const strict = args.includes("--strict-links");
|
|
588
|
+
const { spawnSync } = require("child_process");
|
|
589
|
+
if (!quiet) log("doc-link-check: scanning ai/**/*.md docs/**/*.md");
|
|
590
|
+
const result = spawnSync(
|
|
591
|
+
"python3",
|
|
592
|
+
[
|
|
593
|
+
script,
|
|
594
|
+
"--repo-root",
|
|
595
|
+
target,
|
|
596
|
+
"--paths",
|
|
597
|
+
"ai/**/*.md",
|
|
598
|
+
"docs/**/*.md",
|
|
599
|
+
"--format",
|
|
600
|
+
"md",
|
|
601
|
+
"--fail-on",
|
|
602
|
+
"broken,closed",
|
|
603
|
+
],
|
|
604
|
+
{ stdio: "inherit", cwd: target },
|
|
605
|
+
);
|
|
606
|
+
if (result.error && result.error.code === "ENOENT") {
|
|
607
|
+
if (!quiet) console.log(` ${D}doc-link-check skipped: python3 not found${R}`);
|
|
608
|
+
return { skipped: true, reason: "no-python" };
|
|
609
|
+
}
|
|
610
|
+
const status = typeof result.status === "number" ? result.status : 0;
|
|
611
|
+
if (status !== 0) {
|
|
612
|
+
if (strict) {
|
|
613
|
+
log(`${W}doc-link-check failed (exit ${status}); --strict-links is set${R}`);
|
|
614
|
+
process.exit(status);
|
|
615
|
+
}
|
|
616
|
+
if (!quiet) log(`${W}doc-link-check found issues (exit ${status}); warn-only (pass --strict-links to fail sync)${R}`);
|
|
617
|
+
return { ran: true, status, strict, broken: true };
|
|
618
|
+
}
|
|
619
|
+
if (!quiet) log("doc-link-check: clean");
|
|
620
|
+
return { ran: true, status: 0, strict, broken: false };
|
|
621
|
+
}
|
|
622
|
+
|
|
560
623
|
async function cmdSync(target, args = []) {
|
|
561
624
|
const dryRun = args.includes("--dry-run");
|
|
562
625
|
const quiet = args.includes("--quiet") || args.includes("-q");
|
|
@@ -608,7 +671,11 @@ async function cmdSync(target, args = []) {
|
|
|
608
671
|
|
|
609
672
|
const result = await apiCall("/v1/sync", {
|
|
610
673
|
ai_version: version, stack, agents: agents.length ? agents : clis,
|
|
611
|
-
current_files: currentFiles,
|
|
674
|
+
current_files: currentFiles,
|
|
675
|
+
// Privacy: send only filenames + SHA-256 hashes, never raw content (closes #4031).
|
|
676
|
+
// Local detection (stack, agents) is derived locally and sent separately so the
|
|
677
|
+
// server needs no raw manifest content — mirrors the /v1/init fix from #4030.
|
|
678
|
+
manifest_files: hashManifestFiles(manifestContents),
|
|
612
679
|
dry_run: dryRun, quiet, force,
|
|
613
680
|
project_name: identity.project_name,
|
|
614
681
|
project_id: boundProject.project_id || identity.project_id,
|
|
@@ -669,6 +736,12 @@ async function cmdSync(target, args = []) {
|
|
|
669
736
|
} else {
|
|
670
737
|
log("already up to date");
|
|
671
738
|
}
|
|
739
|
+
|
|
740
|
+
// SPEC-028 Phase 2 (#2689) — scan managed docs for broken cross-refs after
|
|
741
|
+
// the layer is on disk. Warn-only unless --strict-links; --skip-link-check
|
|
742
|
+
// bypasses. Runs even when no files changed so stale local refs surface on
|
|
743
|
+
// every sync.
|
|
744
|
+
runDocLinkCheck(target, args, { quiet });
|
|
672
745
|
const envManifest = normalizeEnvironmentManifest(target);
|
|
673
746
|
if (!quiet && envManifest.changed) {
|
|
674
747
|
log("environment manifest target normalized: ai/manifest/environment.yaml");
|
|
@@ -823,5 +896,6 @@ module.exports = {
|
|
|
823
896
|
hashFile,
|
|
824
897
|
detectRegistryDrift,
|
|
825
898
|
guardRegistryDrift,
|
|
899
|
+
runDocLinkCheck,
|
|
826
900
|
SYNC_FULL_CONTENT_LIMIT,
|
|
827
901
|
};
|
package/lib/commands/mcp.js
CHANGED
|
@@ -72,7 +72,7 @@ function _toolsFromTierManifest(target, plan) {
|
|
|
72
72
|
const section = exposure[plan] || exposure.free;
|
|
73
73
|
if (!section) continue;
|
|
74
74
|
const tools = Array.isArray(section.tools) ? section.tools.slice() : [];
|
|
75
|
-
return { source: candidate, plan, tools, count: tools.length };
|
|
75
|
+
return { source: candidate, plan, tools, count: tools.length, exposure };
|
|
76
76
|
}
|
|
77
77
|
return null;
|
|
78
78
|
}
|
|
@@ -203,12 +203,33 @@ function _cmdCatalog(target, args) {
|
|
|
203
203
|
console.log(`\n ${T}available stacks${R}: ${available.join(", ")}\n`);
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
+
// Flag MCP server entries left behind by a pre-4.3 `0dai sync` that no longer
|
|
207
|
+
// connect. Two dead patterns: the deprecated `/sse` operator endpoint (HTTP 404,
|
|
208
|
+
// 0dai-dev/docs#56) and the `transport: "sse"` form. `mergeMcpConfig` only manages
|
|
209
|
+
// its own servers and never prunes unknown user entries -- even with --reset -- so
|
|
210
|
+
// these persist after upgrade until removed by hand. The current generator connects
|
|
211
|
+
// operator-MCP via the headerless "claude.ai 0dai" (OAuth /mcp) entry, so the stale
|
|
212
|
+
// block is dead weight. Detection stays on the unambiguous /sse signal to avoid
|
|
213
|
+
// false positives on a legitimately-wired bearer host-pool config (#57).
|
|
214
|
+
function _detectStaleMcpServers(mcpServers) {
|
|
215
|
+
const stale = [];
|
|
216
|
+
for (const [name, cfg] of Object.entries(mcpServers || {})) {
|
|
217
|
+
if (!cfg || typeof cfg !== "object") continue;
|
|
218
|
+
const url = typeof cfg.url === "string" ? cfg.url : "";
|
|
219
|
+
const transport = typeof cfg.transport === "string" ? cfg.transport : "";
|
|
220
|
+
if (/\/sse\/?(\?|$)/.test(url) || transport === "sse") {
|
|
221
|
+
stale.push({ name, reason: "deprecated SSE endpoint/transport (HTTP 404)" });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return stale;
|
|
225
|
+
}
|
|
226
|
+
|
|
206
227
|
function _cmdDoctor(target, args) {
|
|
207
228
|
const wantJson = args.includes("--json");
|
|
208
229
|
const report = {
|
|
209
230
|
target,
|
|
210
231
|
server_script: { path: "", ok: false },
|
|
211
|
-
mcp_config: { path: "", ok: false, servers: [] },
|
|
232
|
+
mcp_config: { path: "", ok: false, servers: [], stale: [] },
|
|
212
233
|
tier_manifest: { path: "", ok: false, free_count: 0, pro_count: 0 },
|
|
213
234
|
catalog: { path: "", ok: false, stacks: 0 },
|
|
214
235
|
};
|
|
@@ -226,6 +247,7 @@ function _cmdDoctor(target, args) {
|
|
|
226
247
|
if (data && data.mcpServers && typeof data.mcpServers === "object") {
|
|
227
248
|
report.mcp_config.ok = true;
|
|
228
249
|
report.mcp_config.servers = Object.keys(data.mcpServers);
|
|
250
|
+
report.mcp_config.stale = _detectStaleMcpServers(data.mcpServers);
|
|
229
251
|
}
|
|
230
252
|
}
|
|
231
253
|
|
|
@@ -268,6 +290,12 @@ function _cmdDoctor(target, args) {
|
|
|
268
290
|
if (report.mcp_config.ok) {
|
|
269
291
|
console.log(` servers: ${report.mcp_config.servers.join(", ")}`);
|
|
270
292
|
}
|
|
293
|
+
if (report.mcp_config.stale.length) {
|
|
294
|
+
console.log(` ${W}stale${R} (remove from .mcp.json — superseded by the "claude.ai 0dai" OAuth /mcp entry):`);
|
|
295
|
+
for (const s of report.mcp_config.stale) {
|
|
296
|
+
console.log(` ${W}!${R} ${s.name} ${D}— ${s.reason}${R}`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
271
299
|
console.log(` tier manifest ${ok(report.tier_manifest.ok)} ${D}${report.tier_manifest.path || "(not found)"}${R}`);
|
|
272
300
|
if (report.tier_manifest.ok) {
|
|
273
301
|
console.log(` free=${report.tier_manifest.free_count} pro=${report.tier_manifest.pro_count}`);
|
|
@@ -278,7 +306,9 @@ function _cmdDoctor(target, args) {
|
|
|
278
306
|
}
|
|
279
307
|
console.log("");
|
|
280
308
|
|
|
281
|
-
const allOk = report.server_script.ok && report.mcp_config.ok
|
|
309
|
+
const allOk = report.server_script.ok && report.mcp_config.ok
|
|
310
|
+
&& report.mcp_config.stale.length === 0
|
|
311
|
+
&& (report.tier_manifest.ok || report.catalog.ok);
|
|
282
312
|
if (!allOk) process.exitCode = 1;
|
|
283
313
|
}
|
|
284
314
|
|
package/lib/commands/run.js
CHANGED
|
@@ -1,26 +1,43 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
const shared = require("../shared");
|
|
3
3
|
const { log, T, R, D, fs, path, apiCall } = shared;
|
|
4
|
+
const {
|
|
5
|
+
estimateRunCost,
|
|
6
|
+
formatTokens,
|
|
7
|
+
formatUsd,
|
|
8
|
+
formatUsdPer1k,
|
|
9
|
+
} = require("../utils/run_cost");
|
|
10
|
+
const { recordActivationFirstTask } = require("../utils/activation_telemetry");
|
|
4
11
|
|
|
5
12
|
async function cmdRun(goal, target, args = []) {
|
|
6
13
|
if (!goal) {
|
|
7
|
-
console.log(`Usage: 0dai run <goal> [--dry-run] [--agent claude|codex|gemini] [--provider deepseek|gemini-direct|codex|claude-opus]`);
|
|
14
|
+
console.log(`Usage: 0dai run <goal> [--dry-run] [--dry-cost] [--max-cost N] [--agent claude|codex|gemini] [--provider deepseek|gemini-direct|codex|claude-opus]`);
|
|
8
15
|
console.log(` Example: 0dai run "add dark mode to settings page"`);
|
|
16
|
+
console.log(` Example: 0dai run "fix login bug" --dry-cost`);
|
|
17
|
+
console.log(` Example: 0dai run "refactor auth" --max-cost 0.50`);
|
|
9
18
|
return;
|
|
10
19
|
}
|
|
11
20
|
|
|
12
21
|
const dryRun = args.includes("--dry-run");
|
|
22
|
+
const dryCost = args.includes("--dry-cost");
|
|
13
23
|
const agentIdx = args.indexOf("--agent");
|
|
14
24
|
const agentOverride = agentIdx >= 0 ? args[agentIdx + 1] : null;
|
|
15
25
|
const providerIdx = args.indexOf("--provider");
|
|
16
26
|
const providerOverride = providerIdx >= 0 ? normalizeProvider(args[providerIdx + 1] || "") : null;
|
|
17
27
|
const providerModelIdx = args.indexOf("--provider-model");
|
|
18
28
|
const providerModel = providerModelIdx >= 0 ? (args[providerModelIdx + 1] || "") : "";
|
|
29
|
+
const maxCostIdx = args.indexOf("--max-cost");
|
|
30
|
+
const maxCostRaw = maxCostIdx >= 0 ? args[maxCostIdx + 1] : null;
|
|
31
|
+
const maxCost = maxCostRaw != null ? parseFloat(maxCostRaw) : null;
|
|
19
32
|
|
|
20
33
|
if (providerIdx >= 0 && !providerOverride) {
|
|
21
34
|
log("error: unsupported provider. Supported: deepseek, gemini-direct, codex, claude-opus, claude-sonnet");
|
|
22
35
|
return;
|
|
23
36
|
}
|
|
37
|
+
if (maxCostIdx >= 0 && (maxCostRaw == null || Number.isNaN(maxCost) || maxCost < 0)) {
|
|
38
|
+
log("error: --max-cost requires a non-negative number");
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
24
41
|
|
|
25
42
|
// Read project context
|
|
26
43
|
let stack = "generic", agents = ["claude"], commands = {};
|
|
@@ -30,9 +47,9 @@ async function cmdRun(goal, target, args = []) {
|
|
|
30
47
|
agents = disc.selected_agents || ["claude"];
|
|
31
48
|
} catch {}
|
|
32
49
|
|
|
33
|
-
if (!dryRun) process.stdout.write(`${T}[0dai]${R} decomposing goal...`);
|
|
50
|
+
if (!dryRun && !dryCost) process.stdout.write(`${T}[0dai]${R} decomposing goal...`);
|
|
34
51
|
const result = await apiCall("/v1/run", { goal, context: { stack, agents, commands } });
|
|
35
|
-
if (!dryRun) process.stdout.write("\r" + " ".repeat(40) + "\r");
|
|
52
|
+
if (!dryRun && !dryCost) process.stdout.write("\r" + " ".repeat(40) + "\r");
|
|
36
53
|
|
|
37
54
|
if (result.error) { log(`error: ${result.error}`); return; }
|
|
38
55
|
|
|
@@ -50,6 +67,23 @@ async function cmdRun(goal, target, args = []) {
|
|
|
50
67
|
console.log(` ${D}→ ${agent}${providerText} [${t.model_tier}]${t.description ? " " + t.description : ""}${R}`);
|
|
51
68
|
}
|
|
52
69
|
|
|
70
|
+
const costEstimate = estimateRunCost(tasks, {
|
|
71
|
+
providerOverride,
|
|
72
|
+
resolveAgent: (task) => agentOverride || providerAgent(providerOverride) || task.assigned_to,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
if (dryCost) {
|
|
76
|
+
printCostPreview(costEstimate);
|
|
77
|
+
console.log(`\n ${D}[dry-cost] estimate only — no tasks queued${R}\n`);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (maxCost != null && costEstimate.totalUsd > maxCost) {
|
|
82
|
+
log(`error: estimated cost ${formatUsd(costEstimate.totalUsd)} exceeds --max-cost ${formatUsd(maxCost)}`);
|
|
83
|
+
console.log(` ${D}Re-run with --dry-cost to inspect the breakdown, or raise --max-cost${R}`);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
53
87
|
if (dryRun) {
|
|
54
88
|
console.log(`\n ${D}[dry-run] would create ${tasks.length} task(s) in swarm queue${R}\n`);
|
|
55
89
|
return;
|
|
@@ -88,6 +122,32 @@ async function cmdRun(goal, target, args = []) {
|
|
|
88
122
|
|
|
89
123
|
console.log(`\n ${T}✓${R} ${created.length} task${created.length === 1 ? "" : "s"} added to swarm queue`);
|
|
90
124
|
console.log(` ${D}Monitor: 0dai watch | Queue: 0dai swarm status${R}\n`);
|
|
125
|
+
|
|
126
|
+
if (created.length > 0) {
|
|
127
|
+
const identity = shared.buildProjectIdentity(target, shared.collectMetadata(target));
|
|
128
|
+
recordActivationFirstTask(target, identity.project_id, {
|
|
129
|
+
outcome: "task_queued",
|
|
130
|
+
task_count: created.length,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function printCostPreview(costEstimate) {
|
|
136
|
+
console.log(`\n ${T}Cost preview${R} (estimate):\n`);
|
|
137
|
+
costEstimate.tasks.forEach((row, index) => {
|
|
138
|
+
console.log(
|
|
139
|
+
` ${T}${index + 1}.${R} ${row.title || "task"}`
|
|
140
|
+
+ `\n ${D}→ ${row.agent} [${row.tier}]`
|
|
141
|
+
+ ` ~${formatTokens(row.tokens)} tokens`
|
|
142
|
+
+ ` ~${formatUsd(row.estimatedUsd)}`
|
|
143
|
+
+ ` (${formatUsdPer1k(row.usdPer1k)})${R}`,
|
|
144
|
+
);
|
|
145
|
+
});
|
|
146
|
+
console.log(
|
|
147
|
+
`\n ${T}Total:${R} ~${formatTokens(costEstimate.totalTokens)} tokens`
|
|
148
|
+
+ ` ~${formatUsd(costEstimate.totalUsd)}`
|
|
149
|
+
+ ` (blended ${formatUsdPer1k(costEstimate.blendedUsdPer1k)})`,
|
|
150
|
+
);
|
|
91
151
|
}
|
|
92
152
|
|
|
93
153
|
function normalizeProvider(provider) {
|
|
@@ -114,4 +174,4 @@ function providerAgent(provider) {
|
|
|
114
174
|
return null;
|
|
115
175
|
}
|
|
116
176
|
|
|
117
|
-
module.exports = { cmdRun };
|
|
177
|
+
module.exports = { cmdRun, printCostPreview };
|
package/lib/commands/status.js
CHANGED
|
@@ -4,6 +4,11 @@ const {
|
|
|
4
4
|
log, T, R, D, fs, path, spawnSync, findRepoScript,
|
|
5
5
|
getSwarmQuotaLocal, _detectPlanLocal, PLAN_LEVELS, loadAuthState,
|
|
6
6
|
} = shared;
|
|
7
|
+
const {
|
|
8
|
+
getActivationDurationStats,
|
|
9
|
+
getActivationMergedPrStats,
|
|
10
|
+
printActivationStats,
|
|
11
|
+
} = require("../utils/activation_telemetry");
|
|
7
12
|
|
|
8
13
|
function countJson(dir) {
|
|
9
14
|
try { return fs.readdirSync(dir).filter(f => f.endsWith(".json")).length; } catch { return 0; }
|
|
@@ -116,6 +121,8 @@ function collectStatusPayload(target) {
|
|
|
116
121
|
detected: driftDetected,
|
|
117
122
|
},
|
|
118
123
|
warnings: warningCount,
|
|
124
|
+
activation_ttfv: getActivationDurationStats(target),
|
|
125
|
+
activation_first_merged_pr: getActivationMergedPrStats(target),
|
|
119
126
|
};
|
|
120
127
|
}
|
|
121
128
|
|
|
@@ -158,6 +165,8 @@ function cmdStatus(target, options = {}) {
|
|
|
158
165
|
console.log(` drift: config changes detected — run: 0dai doctor --drift`);
|
|
159
166
|
}
|
|
160
167
|
|
|
168
|
+
printActivationStats(target);
|
|
169
|
+
|
|
161
170
|
return payload;
|
|
162
171
|
}
|
|
163
172
|
|