@0dai-dev/cli 4.3.4 → 4.3.5
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 +10 -1
- package/lib/commands/auth.js +9 -6
- package/lib/commands/doctor.js +20 -1
- package/lib/commands/init.js +64 -0
- package/lib/commands/run.js +61 -4
- package/lib/commands/status.js +4 -0
- package/lib/commands/upgrade.js +58 -0
- package/lib/utils/activation_telemetry.js +156 -0
- package/lib/utils/constants.js +7 -0
- package/lib/utils/plan.js +10 -0
- package/lib/utils/run_cost.js +91 -0
- package/package.json +7 -3
- package/lib/tui/index.mjs +0 -34994
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 0dai
|
|
2
2
|
|
|
3
|
-
One config layer for <!-- canonical-count:agent_clis_total -->
|
|
3
|
+
One config layer for <!-- canonical-count:agent_clis_total -->7<!-- /canonical-count --> AI agent CLIs — Claude Code, Codex, OpenCode, Gemini, Aider, Qoder, Cursor.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -8,22 +8,85 @@ One config layer for <!-- canonical-count:agent_clis_total -->6<!-- /canonical-c
|
|
|
8
8
|
npm install -g @0dai-dev/cli
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Five commands that matter
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
14
|
cd your-project
|
|
15
|
-
0dai init
|
|
16
|
-
0dai
|
|
17
|
-
0dai
|
|
18
|
-
0dai
|
|
19
|
-
0dai
|
|
20
|
-
0dai detect # show detected stack
|
|
21
|
-
0dai doctor # check health
|
|
22
|
-
0dai status # maturity, swarm tasks, session
|
|
23
|
-
0dai feedback retry # retry queued feedback after an API failure
|
|
24
|
-
0dai persona-simulate "topic" # run synthetic focus-group simulation
|
|
15
|
+
0dai init # create ai/ layer, auth, and MCP bootstrap
|
|
16
|
+
0dai status # maturity, swarm tasks, and session state
|
|
17
|
+
0dai sync # refresh ai/ after repo or server changes
|
|
18
|
+
0dai run "…" # split a backlog item into agent tasks
|
|
19
|
+
0dai doctor # health check for credentials, drift, and env
|
|
25
20
|
```
|
|
26
21
|
|
|
22
|
+
## All commands
|
|
23
|
+
|
|
24
|
+
Start (first 5 minutes):
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
0dai init # create ai/ layer + MCP [--local] [--dry-run] [--minimal]
|
|
28
|
+
0dai init --auth-code <code> --code <TEAM-CODE> # non-interactive auth + activation
|
|
29
|
+
0dai doctor # check health, credentials, and drift [--drift]
|
|
30
|
+
0dai status # maturity, swarm, and session state [--json]
|
|
31
|
+
0dai quickstart # auth, init, doctor, and status in one pass
|
|
32
|
+
0dai detect # show detected stack
|
|
33
|
+
0dai auth login # OAuth/device flow [--device] [--no-browser] [--code CODE] [--mcp]
|
|
34
|
+
0dai auth mcp # store MCP token from current auth or device code
|
|
35
|
+
0dai auth status # account and usage
|
|
36
|
+
0dai activate free # claim free activation license
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Daily (regular work):
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
0dai sync # update ai/ after repo or server changes [--dry-run] [--yes] [--quiet] [--force]
|
|
43
|
+
0dai run <goal> # split a backlog item into agent tasks [--dry-run] [--dry-cost] [--agent claude|codex|gemini]
|
|
44
|
+
0dai swarm status # show queued, active, and done tasks
|
|
45
|
+
0dai swarm add # queue one task [--task '...' --to agent]
|
|
46
|
+
0dai swarm-run # add, dispatch, and wait for one swarm task as JSON
|
|
47
|
+
0dai harvest # convert experience events into candidate lessons
|
|
48
|
+
0dai watch # live task monitor [--interval N]
|
|
49
|
+
0dai reflect # session reflection: delivered, delegation, blockers
|
|
50
|
+
0dai standup # morning voice briefing about overnight agent work
|
|
51
|
+
0dai feedback push # send feedback to 0dai
|
|
52
|
+
0dai feedback retry # retry queued feedback after a failed push
|
|
53
|
+
0dai persona-simulate "topic" # focus-group report and optional issue drafts
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Pro / advanced:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
0dai init-existing # existing-repo setup alias for init
|
|
60
|
+
0dai project bind # bind repository to your 0dai account [--json]
|
|
61
|
+
0dai project status # local project binding and health [--json]
|
|
62
|
+
0dai graph push # upload local graph (Pro: edges, Free: nodes)
|
|
63
|
+
0dai graph pull # download server graph and merge locally
|
|
64
|
+
0dai graph status # local graph stats and sync state
|
|
65
|
+
0dai ci # portable CI pipelines and AI-MQ [list|plan|mq-status]
|
|
66
|
+
0dai heatmap # repo treemap: LOC × agent-edit intensity
|
|
67
|
+
0dai session save # save session for roaming
|
|
68
|
+
0dai provider # local provider profiles and direct invoke
|
|
69
|
+
0dai models # model ratings (--fast/--balanced/--deep/--available)
|
|
70
|
+
0dai quota # agent subscription usage [--refresh] [--json]
|
|
71
|
+
0dai usage # local token, task, and USD ledger [status|daily|monthly]
|
|
72
|
+
0dai workspace # tmux workspace sessions (init|up|status)
|
|
73
|
+
0dai runner # runner/host architecture and queues [--json]
|
|
74
|
+
0dai report # privacy-safe project reports (preview|push|status)
|
|
75
|
+
0dai compliance # SOC2/ISO evidence and ADR audit-trail export
|
|
76
|
+
0dai experience # structured experience events (list|stats|sync|warnings|dismiss)
|
|
77
|
+
0dai receipt # session receipt PNG [--last|--active|--session ID]
|
|
78
|
+
0dai boneyard # weekly digest of worst agent moves [--week YYYY-WW|current]
|
|
79
|
+
0dai gh branch-protection # GitHub branch protection [print|apply|install]
|
|
80
|
+
0dai import claude-code-agents # import .claude/agents/*.md as personas [--dry-run]
|
|
81
|
+
0dai auth logout # remove credentials
|
|
82
|
+
0dai activate code <TEAM-CODE> # redeem Pro/Team activation code
|
|
83
|
+
0dai activate status # activation and bound-project status
|
|
84
|
+
0dai redeem <CODE> # redeem a plan upgrade code
|
|
85
|
+
0dai upgrade # open pricing page (browser or printed URL)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Global flags: `--target PATH`, `--version`, `--help`, `--json`, `--quiet`. See `0dai --help` for the full surface.
|
|
89
|
+
|
|
27
90
|
## What it does
|
|
28
91
|
|
|
29
92
|
`0dai init` and `0dai sync` are activation-first. `init` can complete auth and plan activation in one run: browser/OAuth exchange code via `--auth-code`, Team/Pro activation code via `--code` or `--activation-code`, or the interactive OAuth/device-code prompt. After that it binds the project and sends only allowlisted project metadata (file names + package/build manifests) to the API and generates:
|
package/bin/0dai.js
CHANGED
|
@@ -102,6 +102,7 @@ const { cmdAudit } = require("../lib/commands/audit");
|
|
|
102
102
|
const { cmdDoctor } = require("../lib/commands/doctor");
|
|
103
103
|
const { cmdValidate } = require("../lib/commands/validate");
|
|
104
104
|
const { cmdUpdate } = require("../lib/commands/update");
|
|
105
|
+
const { cmdUpgrade } = require("../lib/commands/upgrade");
|
|
105
106
|
const { cmdReflect } = require("../lib/commands/reflect");
|
|
106
107
|
const { cmdMetrics } = require("../lib/commands/metrics");
|
|
107
108
|
const { cmdHeatmap } = require("../lib/commands/heatmap");
|
|
@@ -128,6 +129,7 @@ const { cmdPaste } = require("../lib/commands/paste");
|
|
|
128
129
|
const { cmdReceipt } = require("../lib/commands/receipt");
|
|
129
130
|
const { cmdBoneyard } = require("../lib/commands/boneyard");
|
|
130
131
|
const { cmdQuota } = require("../lib/commands/quota");
|
|
132
|
+
const { cmdUsage } = require("../lib/commands/usage");
|
|
131
133
|
const { cmdGh } = require("../lib/commands/gh");
|
|
132
134
|
const { cmdLoop } = require("../lib/commands/loop");
|
|
133
135
|
const { cmdImportClaudeCodeAgents } = require("../lib/commands/import_claude_code_agents");
|
|
@@ -150,7 +152,7 @@ function printHelp() {
|
|
|
150
152
|
console.log("");
|
|
151
153
|
console.log("Daily (regular work):");
|
|
152
154
|
console.log(" sync Update ai/ layer after repo or server changes [--dry-run] [--yes] [--quiet] [--force]");
|
|
153
|
-
console.log(" run <goal> Split a backlog item into agent tasks [--dry-run] [--agent claude|codex|gemini] [--provider X]");
|
|
155
|
+
console.log(" run <goal> Split a backlog item into agent tasks [--dry-run] [--dry-cost] [--max-cost N] [--agent claude|codex|gemini] [--provider X]");
|
|
154
156
|
console.log(" swarm status Show queued, active, and done tasks");
|
|
155
157
|
console.log(" swarm add Queue one task for an agent [--task '...' --to agent]");
|
|
156
158
|
console.log(" swarm-run Add, dispatch, and wait for one swarm task as JSON");
|
|
@@ -174,6 +176,7 @@ function printHelp() {
|
|
|
174
176
|
console.log(" provider Local provider profiles, bindings, and direct invoke");
|
|
175
177
|
console.log(" models Show model ratings (--fast/--balanced/--deep/--available)");
|
|
176
178
|
console.log(" quota Agent subscription usage table [--refresh] [--json]");
|
|
179
|
+
console.log(" usage Local token, task, and USD usage ledger [status|daily|monthly]");
|
|
177
180
|
console.log(" workspace Manage tmux workspace sessions (init|up|status)");
|
|
178
181
|
console.log(" runner Show runner/project-host architecture, queues, labels, and burst routing [status|plan|queue-status|label-audit|route-dry-run] [--json]");
|
|
179
182
|
console.log(" report Privacy-safe project reports (preview|push|status)");
|
|
@@ -206,6 +209,8 @@ const SYNC_ALLOWED_FLAGS = new Set([
|
|
|
206
209
|
"--force",
|
|
207
210
|
"--no-diff",
|
|
208
211
|
"--no-mcp-auth",
|
|
212
|
+
"--skip-link-check",
|
|
213
|
+
"--strict-links",
|
|
209
214
|
]);
|
|
210
215
|
|
|
211
216
|
function printSyncHelp() {
|
|
@@ -220,6 +225,8 @@ function printSyncHelp() {
|
|
|
220
225
|
console.log(" --force Also overwrite native config files from managed ai/ sources");
|
|
221
226
|
console.log(" --no-diff Hide unified diff output in previews/prompts");
|
|
222
227
|
console.log(" --no-mcp-auth Skip MCP cloud-token bootstrap during sync");
|
|
228
|
+
console.log(" --skip-link-check Skip the SPEC-028 doc cross-link scan after sync");
|
|
229
|
+
console.log(" --strict-links Fail sync if doc_link_check reports broken or closed refs");
|
|
223
230
|
console.log(" --target PATH Run sync against another project path");
|
|
224
231
|
console.log("");
|
|
225
232
|
}
|
|
@@ -352,6 +359,7 @@ async function main() {
|
|
|
352
359
|
case "validate": cmdValidate(target); break;
|
|
353
360
|
case "reflect": cmdReflect(target, args); break;
|
|
354
361
|
case "update": cmdUpdate(args); break;
|
|
362
|
+
case "upgrade": cmdUpgrade(); break;
|
|
355
363
|
case "metrics": cmdMetrics(target); break;
|
|
356
364
|
case "heatmap": cmdHeatmap(target, args.slice(1)); break;
|
|
357
365
|
case "portfolio": cmdPortfolio(); break;
|
|
@@ -408,6 +416,7 @@ async function main() {
|
|
|
408
416
|
case "receipt": cmdReceipt(target, args.slice(1)); break;
|
|
409
417
|
case "boneyard": cmdBoneyard(target, args.slice(1)); break;
|
|
410
418
|
case "quota": case "quotas": cmdQuota(target, args.slice(1)); break;
|
|
419
|
+
case "usage": cmdUsage(target, args.slice(1)); break;
|
|
411
420
|
case "gh": await cmdGh(target, sub, args); break;
|
|
412
421
|
case "loop": cmdLoop(target, sub, args); break;
|
|
413
422
|
case "import": {
|
package/lib/commands/auth.js
CHANGED
|
@@ -133,16 +133,19 @@ async function loginWithExchangeCode(code) {
|
|
|
133
133
|
if (!exchanged || exchanged.error || !exchanged.access_token) {
|
|
134
134
|
throw new Error(exchanged && exchanged.error ? exchanged.error : "invalid or expired auth code");
|
|
135
135
|
}
|
|
136
|
+
const status = await fetchAuthStatus(exchanged.access_token);
|
|
137
|
+
if (!status || status.error || !status.email) {
|
|
138
|
+
throw new Error(status && status.error ? status.error : "cloud validation failed after auth exchange");
|
|
139
|
+
}
|
|
136
140
|
saveAuthState({
|
|
137
141
|
access_token: exchanged.access_token,
|
|
138
|
-
email:
|
|
139
|
-
|
|
142
|
+
email: status.email,
|
|
143
|
+
plan: status.plan || "free",
|
|
144
|
+
name: status.name || exchanged.name || "",
|
|
145
|
+
license: status.license || {},
|
|
146
|
+
plan_expires_at: status.plan_expires_at || "",
|
|
140
147
|
authenticated_at: new Date().toISOString(),
|
|
141
148
|
});
|
|
142
|
-
const status = await fetchAuthStatus();
|
|
143
|
-
if (!status || status.error || !status.email) {
|
|
144
|
-
throw new Error(status && status.error ? status.error : "cloud validation failed after auth exchange");
|
|
145
|
-
}
|
|
146
149
|
return status;
|
|
147
150
|
}
|
|
148
151
|
|
package/lib/commands/doctor.js
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
const shared = require("../shared");
|
|
3
3
|
const { log, T, R, D, fs, path, spawnSync, findRepoScript, SUPPORTED_CLIS, recordExperienceEvent } = shared;
|
|
4
4
|
|
|
5
|
+
function nodePtyProbe() {
|
|
6
|
+
try {
|
|
7
|
+
require.resolve("node-pty");
|
|
8
|
+
return {
|
|
9
|
+
name: "node-pty",
|
|
10
|
+
present: true,
|
|
11
|
+
sev: "ok",
|
|
12
|
+
hint: "node-pty available for terminal session spawn",
|
|
13
|
+
};
|
|
14
|
+
} catch {
|
|
15
|
+
return {
|
|
16
|
+
name: "node-pty",
|
|
17
|
+
present: false,
|
|
18
|
+
sev: "warn",
|
|
19
|
+
hint: "install node-pty for terminal sessions: npm i -g node-pty",
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
5
24
|
function ghostAuthStatus(home = process.env.HOME || process.env.USERPROFILE || "") {
|
|
6
25
|
const customApiEnv = home ? path.join(home, ".custom-api", "custom-api.env") : "";
|
|
7
26
|
const legacyAnthropicEnv = home ? path.join(home, ".claude-api-isolated", "anthropic.env") : "";
|
|
@@ -256,4 +275,4 @@ function cmdDoctor(target, options = {}) {
|
|
|
256
275
|
}
|
|
257
276
|
}
|
|
258
277
|
|
|
259
|
-
module.exports = { cmdDoctor, ghostAuthStatus };
|
|
278
|
+
module.exports = { cmdDoctor, ghostAuthStatus, nodePtyProbe };
|
package/lib/commands/init.js
CHANGED
|
@@ -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;
|
|
@@ -463,6 +464,8 @@ async function cmdInit(target, args = []) {
|
|
|
463
464
|
// First-run proof gate (issue #342). All 4 gates pass once we reach here:
|
|
464
465
|
// license active (line above), project bound, ai/ layer written, heartbeat sent.
|
|
465
466
|
// Idempotent — only fires once per project. See docs/first-run.md.
|
|
467
|
+
recordActivationInit(target, boundProject.project_id || identity.project_id);
|
|
468
|
+
|
|
466
469
|
const firstRun = logFirstRunSuccess(target, {
|
|
467
470
|
license: true,
|
|
468
471
|
project_bound: true,
|
|
@@ -557,6 +560,60 @@ function normalizeEnvironmentManifest(target) {
|
|
|
557
560
|
return { path: filePath, changed: true };
|
|
558
561
|
}
|
|
559
562
|
|
|
563
|
+
// runDocLinkCheck — SPEC-028 Phase 2 (#2689). After the managed layer has been
|
|
564
|
+
// written, run scripts/doc_link_check.py to surface broken/closed cross-refs
|
|
565
|
+
// in ai/ and docs/. Warn-only by default so a flaky reference never blocks an
|
|
566
|
+
// otherwise successful sync; --strict-links promotes failures to a non-zero
|
|
567
|
+
// exit, and --skip-link-check bypasses the hook entirely.
|
|
568
|
+
//
|
|
569
|
+
// The hook intentionally stays silent when the script is missing (Phase 1 may
|
|
570
|
+
// not have shipped to a downstream project) and when python3 is unavailable.
|
|
571
|
+
// Output is streamed inline via stdio: "inherit" so authors see exact paths.
|
|
572
|
+
function runDocLinkCheck(target, args = [], options = {}) {
|
|
573
|
+
if (args.includes("--skip-link-check")) {
|
|
574
|
+
return { skipped: true, reason: "flag" };
|
|
575
|
+
}
|
|
576
|
+
const script = path.join(target, "scripts", "doc_link_check.py");
|
|
577
|
+
if (!fs.existsSync(script)) {
|
|
578
|
+
return { skipped: true, reason: "missing-script" };
|
|
579
|
+
}
|
|
580
|
+
const quiet = !!options.quiet;
|
|
581
|
+
const strict = args.includes("--strict-links");
|
|
582
|
+
const { spawnSync } = require("child_process");
|
|
583
|
+
if (!quiet) log("doc-link-check: scanning ai/**/*.md docs/**/*.md");
|
|
584
|
+
const result = spawnSync(
|
|
585
|
+
"python3",
|
|
586
|
+
[
|
|
587
|
+
script,
|
|
588
|
+
"--repo-root",
|
|
589
|
+
target,
|
|
590
|
+
"--paths",
|
|
591
|
+
"ai/**/*.md",
|
|
592
|
+
"docs/**/*.md",
|
|
593
|
+
"--format",
|
|
594
|
+
"md",
|
|
595
|
+
"--fail-on",
|
|
596
|
+
"broken,closed",
|
|
597
|
+
],
|
|
598
|
+
{ stdio: "inherit", cwd: target },
|
|
599
|
+
);
|
|
600
|
+
if (result.error && result.error.code === "ENOENT") {
|
|
601
|
+
if (!quiet) console.log(` ${D}doc-link-check skipped: python3 not found${R}`);
|
|
602
|
+
return { skipped: true, reason: "no-python" };
|
|
603
|
+
}
|
|
604
|
+
const status = typeof result.status === "number" ? result.status : 0;
|
|
605
|
+
if (status !== 0) {
|
|
606
|
+
if (strict) {
|
|
607
|
+
log(`${W}doc-link-check failed (exit ${status}); --strict-links is set${R}`);
|
|
608
|
+
process.exit(status);
|
|
609
|
+
}
|
|
610
|
+
if (!quiet) log(`${W}doc-link-check found issues (exit ${status}); warn-only (pass --strict-links to fail sync)${R}`);
|
|
611
|
+
return { ran: true, status, strict, broken: true };
|
|
612
|
+
}
|
|
613
|
+
if (!quiet) log("doc-link-check: clean");
|
|
614
|
+
return { ran: true, status: 0, strict, broken: false };
|
|
615
|
+
}
|
|
616
|
+
|
|
560
617
|
async function cmdSync(target, args = []) {
|
|
561
618
|
const dryRun = args.includes("--dry-run");
|
|
562
619
|
const quiet = args.includes("--quiet") || args.includes("-q");
|
|
@@ -669,6 +726,12 @@ async function cmdSync(target, args = []) {
|
|
|
669
726
|
} else {
|
|
670
727
|
log("already up to date");
|
|
671
728
|
}
|
|
729
|
+
|
|
730
|
+
// SPEC-028 Phase 2 (#2689) — scan managed docs for broken cross-refs after
|
|
731
|
+
// the layer is on disk. Warn-only unless --strict-links; --skip-link-check
|
|
732
|
+
// bypasses. Runs even when no files changed so stale local refs surface on
|
|
733
|
+
// every sync.
|
|
734
|
+
runDocLinkCheck(target, args, { quiet });
|
|
672
735
|
const envManifest = normalizeEnvironmentManifest(target);
|
|
673
736
|
if (!quiet && envManifest.changed) {
|
|
674
737
|
log("environment manifest target normalized: ai/manifest/environment.yaml");
|
|
@@ -823,5 +886,6 @@ module.exports = {
|
|
|
823
886
|
hashFile,
|
|
824
887
|
detectRegistryDrift,
|
|
825
888
|
guardRegistryDrift,
|
|
889
|
+
runDocLinkCheck,
|
|
826
890
|
SYNC_FULL_CONTENT_LIMIT,
|
|
827
891
|
};
|
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,29 @@ 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
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function printCostPreview(costEstimate) {
|
|
133
|
+
console.log(`\n ${T}Cost preview${R} (estimate):\n`);
|
|
134
|
+
costEstimate.tasks.forEach((row, index) => {
|
|
135
|
+
console.log(
|
|
136
|
+
` ${T}${index + 1}.${R} ${row.title || "task"}`
|
|
137
|
+
+ `\n ${D}→ ${row.agent} [${row.tier}]`
|
|
138
|
+
+ ` ~${formatTokens(row.tokens)} tokens`
|
|
139
|
+
+ ` ~${formatUsd(row.estimatedUsd)}`
|
|
140
|
+
+ ` (${formatUsdPer1k(row.usdPer1k)})${R}`,
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
console.log(
|
|
144
|
+
`\n ${T}Total:${R} ~${formatTokens(costEstimate.totalTokens)} tokens`
|
|
145
|
+
+ ` ~${formatUsd(costEstimate.totalUsd)}`
|
|
146
|
+
+ ` (blended ${formatUsdPer1k(costEstimate.blendedUsdPer1k)})`,
|
|
147
|
+
);
|
|
91
148
|
}
|
|
92
149
|
|
|
93
150
|
function normalizeProvider(provider) {
|
|
@@ -114,4 +171,4 @@ function providerAgent(provider) {
|
|
|
114
171
|
return null;
|
|
115
172
|
}
|
|
116
173
|
|
|
117
|
-
module.exports = { cmdRun };
|
|
174
|
+
module.exports = { cmdRun, printCostPreview };
|
package/lib/commands/status.js
CHANGED
|
@@ -4,6 +4,7 @@ const {
|
|
|
4
4
|
log, T, R, D, fs, path, spawnSync, findRepoScript,
|
|
5
5
|
getSwarmQuotaLocal, _detectPlanLocal, PLAN_LEVELS, loadAuthState,
|
|
6
6
|
} = shared;
|
|
7
|
+
const { getActivationDurationStats, printActivationStats } = require("../utils/activation_telemetry");
|
|
7
8
|
|
|
8
9
|
function countJson(dir) {
|
|
9
10
|
try { return fs.readdirSync(dir).filter(f => f.endsWith(".json")).length; } catch { return 0; }
|
|
@@ -116,6 +117,7 @@ function collectStatusPayload(target) {
|
|
|
116
117
|
detected: driftDetected,
|
|
117
118
|
},
|
|
118
119
|
warnings: warningCount,
|
|
120
|
+
activation_ttfv: getActivationDurationStats(target),
|
|
119
121
|
};
|
|
120
122
|
}
|
|
121
123
|
|
|
@@ -158,6 +160,8 @@ function cmdStatus(target, options = {}) {
|
|
|
158
160
|
console.log(` drift: config changes detected — run: 0dai doctor --drift`);
|
|
159
161
|
}
|
|
160
162
|
|
|
163
|
+
printActivationStats(target);
|
|
164
|
+
|
|
161
165
|
return payload;
|
|
162
166
|
}
|
|
163
167
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { spawnSync } = require("child_process");
|
|
4
|
+
const { buildUpgradeUrl } = require("../utils/plan");
|
|
5
|
+
|
|
6
|
+
function isInteractive() {
|
|
7
|
+
return !!(process.stdin && process.stdin.isTTY && process.stdout && process.stdout.isTTY);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function isBrowserDisabled(env = process.env) {
|
|
11
|
+
const browser = env.BROWSER;
|
|
12
|
+
if (browser == null) return false;
|
|
13
|
+
const normalized = String(browser).trim().toLowerCase();
|
|
14
|
+
return normalized === "" || normalized === "0" || normalized === "false" || normalized === "none";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function openBrowser(url, deps = {}) {
|
|
18
|
+
const spawn = deps.spawnSync || spawnSync;
|
|
19
|
+
const platform = deps.platform || process.platform;
|
|
20
|
+
let command;
|
|
21
|
+
let args;
|
|
22
|
+
if (platform === "darwin") {
|
|
23
|
+
command = "open";
|
|
24
|
+
args = [url];
|
|
25
|
+
} else if (platform === "win32") {
|
|
26
|
+
command = "cmd";
|
|
27
|
+
args = ["/c", "start", "", url];
|
|
28
|
+
} else {
|
|
29
|
+
command = "xdg-open";
|
|
30
|
+
args = [url];
|
|
31
|
+
}
|
|
32
|
+
const result = spawn(command, args, { stdio: "ignore", timeout: 5000 });
|
|
33
|
+
return result.status === 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function cmdUpgrade(options = {}) {
|
|
37
|
+
const writeLine = typeof options.writeLine === "function" ? options.writeLine : (msg) => console.log(msg);
|
|
38
|
+
const url = buildUpgradeUrl(options.params);
|
|
39
|
+
const interactive = typeof options.isInteractive === "function"
|
|
40
|
+
? options.isInteractive()
|
|
41
|
+
: isInteractive();
|
|
42
|
+
const browserDisabled = isBrowserDisabled(options.env || process.env);
|
|
43
|
+
|
|
44
|
+
if (interactive && !browserDisabled) {
|
|
45
|
+
const opened = openBrowser(url, options);
|
|
46
|
+
if (!opened) writeLine(url);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
writeLine(url);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = {
|
|
54
|
+
cmdUpgrade,
|
|
55
|
+
openBrowser,
|
|
56
|
+
isInteractive,
|
|
57
|
+
isBrowserDisabled,
|
|
58
|
+
};
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
|
|
6
|
+
function activationPath(target) {
|
|
7
|
+
return path.join(target, "ai", "meta", "telemetry", "activation.jsonl");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function nowIso() {
|
|
11
|
+
return new Date().toISOString();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function ensureDir(filePath) {
|
|
15
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function readEvents(target) {
|
|
19
|
+
const file = activationPath(target);
|
|
20
|
+
if (!fs.existsSync(file)) return [];
|
|
21
|
+
const lines = fs.readFileSync(file, "utf8").trim().split("\n").filter(Boolean);
|
|
22
|
+
const events = [];
|
|
23
|
+
for (const ln of lines) {
|
|
24
|
+
try {
|
|
25
|
+
const row = JSON.parse(ln);
|
|
26
|
+
if (row && row.event) events.push(row);
|
|
27
|
+
} catch { /* ignore malformed lines */ }
|
|
28
|
+
}
|
|
29
|
+
return events;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function appendEvent(target, entry) {
|
|
33
|
+
const file = activationPath(target);
|
|
34
|
+
try {
|
|
35
|
+
ensureDir(file);
|
|
36
|
+
fs.appendFileSync(file, JSON.stringify(entry) + "\n", "utf8");
|
|
37
|
+
return true;
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function hasEvent(events, eventName, projectId) {
|
|
44
|
+
return events.some((e) => e.event === eventName && e.project_id === projectId);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function findInitTs(events, projectId) {
|
|
48
|
+
const init = events.find((e) => e.event === "init" && e.project_id === projectId);
|
|
49
|
+
return init && init.ts ? init.ts : null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function parseTs(ts) {
|
|
53
|
+
const ms = Date.parse(ts);
|
|
54
|
+
return Number.isFinite(ms) ? ms : null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Idempotently append `{event:"init", ts, project_id}` on successful init.
|
|
58
|
+
function recordActivationInit(target, projectId) {
|
|
59
|
+
try {
|
|
60
|
+
if (!projectId) return { fired: false };
|
|
61
|
+
const events = readEvents(target);
|
|
62
|
+
if (hasEvent(events, "init", projectId)) return { fired: false };
|
|
63
|
+
const entry = { event: "init", ts: nowIso(), project_id: projectId };
|
|
64
|
+
return { fired: appendEvent(target, entry) };
|
|
65
|
+
} catch {
|
|
66
|
+
return { fired: false };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Idempotently append first_task with duration_ms = first_task.ts - init.ts.
|
|
71
|
+
function recordActivationFirstTask(target, projectId) {
|
|
72
|
+
try {
|
|
73
|
+
if (!projectId) return { fired: false, durationMs: null };
|
|
74
|
+
const events = readEvents(target);
|
|
75
|
+
if (hasEvent(events, "first_task", projectId)) {
|
|
76
|
+
return { fired: false, durationMs: null };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const initTs = findInitTs(events, projectId);
|
|
80
|
+
const ts = nowIso();
|
|
81
|
+
let durationMs = null;
|
|
82
|
+
if (initTs) {
|
|
83
|
+
const initMs = parseTs(initTs);
|
|
84
|
+
const taskMs = parseTs(ts);
|
|
85
|
+
if (initMs != null && taskMs != null) {
|
|
86
|
+
durationMs = Math.max(0, taskMs - initMs);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const entry = {
|
|
91
|
+
event: "first_task",
|
|
92
|
+
ts,
|
|
93
|
+
project_id: projectId,
|
|
94
|
+
duration_ms: durationMs,
|
|
95
|
+
};
|
|
96
|
+
const fired = appendEvent(target, entry);
|
|
97
|
+
return { fired, durationMs };
|
|
98
|
+
} catch {
|
|
99
|
+
return { fired: false, durationMs: null };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function percentile(sorted, p) {
|
|
104
|
+
if (!sorted.length) return null;
|
|
105
|
+
if (sorted.length === 1) return sorted[0];
|
|
106
|
+
const idx = (sorted.length - 1) * p;
|
|
107
|
+
const lo = Math.floor(idx);
|
|
108
|
+
const hi = Math.ceil(idx);
|
|
109
|
+
if (lo === hi) return sorted[lo];
|
|
110
|
+
const weight = idx - lo;
|
|
111
|
+
return Math.round(sorted[lo] * (1 - weight) + sorted[hi] * weight);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function getActivationDurationStats(target) {
|
|
115
|
+
const events = readEvents(target);
|
|
116
|
+
const durations = events
|
|
117
|
+
.filter((e) => e.event === "first_task"
|
|
118
|
+
&& typeof e.duration_ms === "number"
|
|
119
|
+
&& Number.isFinite(e.duration_ms))
|
|
120
|
+
.map((e) => e.duration_ms)
|
|
121
|
+
.sort((a, b) => a - b);
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
count: durations.length,
|
|
125
|
+
p50_ms: percentile(durations, 0.5),
|
|
126
|
+
p90_ms: percentile(durations, 0.9),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function formatDurationMs(ms) {
|
|
131
|
+
if (ms == null) return "?";
|
|
132
|
+
if (ms < 1000) return `${ms}ms`;
|
|
133
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
134
|
+
return `${(ms / 60000).toFixed(1)}m`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function printActivationStats(target) {
|
|
138
|
+
const stats = getActivationDurationStats(target);
|
|
139
|
+
if (stats.count === 0) return stats;
|
|
140
|
+
console.log(
|
|
141
|
+
` activation TTFV: p50 ${formatDurationMs(stats.p50_ms)}`
|
|
142
|
+
+ ` / p90 ${formatDurationMs(stats.p90_ms)}`
|
|
143
|
+
+ ` (${stats.count} sample${stats.count === 1 ? "" : "s"})`,
|
|
144
|
+
);
|
|
145
|
+
return stats;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
module.exports = {
|
|
149
|
+
activationPath,
|
|
150
|
+
readEvents,
|
|
151
|
+
recordActivationInit,
|
|
152
|
+
recordActivationFirstTask,
|
|
153
|
+
getActivationDurationStats,
|
|
154
|
+
printActivationStats,
|
|
155
|
+
formatDurationMs,
|
|
156
|
+
};
|