@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
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
|
@@ -31,18 +31,58 @@ const { T, R, D, log, VERSION, fs, path, spawnSync, findRepoScript, checkVersion
|
|
|
31
31
|
/**
|
|
32
32
|
* Hot-path Go binary fallback (issue #2424).
|
|
33
33
|
*
|
|
34
|
-
* For read-only hot-path commands
|
|
35
|
-
* to a Go binary when:
|
|
34
|
+
* For read-only hot-path commands we attempt to delegate to a Go binary when:
|
|
36
35
|
* 1. ODAI_GO_BIN is set and points at an executable file, OR a binary called
|
|
37
36
|
* `0dai-go` is found on PATH.
|
|
38
|
-
* 2. The binary reports `
|
|
39
|
-
* (
|
|
37
|
+
* 2. The binary reports `dispatcher_compat_version` matching the dispatcher
|
|
38
|
+
* VERSION (legacy binaries may still use `binary_version` for this).
|
|
40
39
|
* 3. ODAI_GO_DISABLE is NOT set to a truthy value.
|
|
40
|
+
* 4. The command's batch flag is not disabled. SPEC-035 rollback Level 1
|
|
41
|
+
* is `ODAI_GO_BATCH_<N>=0`, which transparently routes the whole batch
|
|
42
|
+
* back to the Node/Python implementation without reinstalling.
|
|
43
|
+
*
|
|
44
|
+
* Only commands listed in GO_HOT_PATH_COMMANDS are eligible for automatic
|
|
45
|
+
* delegation. `status` moved into batch 2 only after the #4098 Go↔Node
|
|
46
|
+
* payload parity proof landed. Base `doctor` moved into the same batch only
|
|
47
|
+
* after #4111 made the local `doctor_checks` shadow contract explicit;
|
|
48
|
+
* `doctor --drift` stays on the full Node implementation.
|
|
41
49
|
*
|
|
42
50
|
* If any of these checks fail we silently fall through to the existing
|
|
43
51
|
* Python/Node implementations. The goal is zero behaviour change when the Go
|
|
44
52
|
* binary is missing, broken, or version-skewed.
|
|
45
53
|
*/
|
|
54
|
+
const GO_HOT_PATH_COMMANDS = new Set(["version", "status", "doctor"]);
|
|
55
|
+
const GO_HOT_PATH_BATCHES = Object.freeze({
|
|
56
|
+
version: 1,
|
|
57
|
+
status: 2,
|
|
58
|
+
doctor: 2,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
function goFlagDisabled(raw) {
|
|
62
|
+
if (raw === undefined || raw === null || raw === "") return false;
|
|
63
|
+
const value = String(raw).trim().toLowerCase();
|
|
64
|
+
return ["0", "false", "no", "off"].includes(value);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function goCommandEnvName(cmdName) {
|
|
68
|
+
return `ODAI_GO_COMMAND_${String(cmdName).toUpperCase().replace(/[^A-Z0-9]+/g, "_")}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function goBatchEnvName(batch) {
|
|
72
|
+
return `ODAI_GO_BATCH_${batch}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function goBatchEnabled(cmdName) {
|
|
76
|
+
const batch = GO_HOT_PATH_BATCHES[cmdName];
|
|
77
|
+
if (!batch) return false;
|
|
78
|
+
if (goFlagDisabled(process.env[goBatchEnvName(batch)])) return false;
|
|
79
|
+
|
|
80
|
+
// Drill-only per-command rollback override. Batch flags remain the release
|
|
81
|
+
// contract; this override is intentionally narrower for staging drills.
|
|
82
|
+
if (goFlagDisabled(process.env[goCommandEnvName(cmdName)])) return false;
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
|
|
46
86
|
function locateGoBinary() {
|
|
47
87
|
if (process.env.ODAI_GO_DISABLE && process.env.ODAI_GO_DISABLE !== "0") return null;
|
|
48
88
|
const explicit = process.env.ODAI_GO_BIN;
|
|
@@ -68,7 +108,11 @@ function goBinaryCompatible(binPath) {
|
|
|
68
108
|
if (res.status !== 0 || !res.stdout) return false;
|
|
69
109
|
const info = JSON.parse(res.stdout.toString());
|
|
70
110
|
if (!info || typeof info.binary_version !== "string") return false;
|
|
71
|
-
|
|
111
|
+
const compatVersion =
|
|
112
|
+
typeof info.dispatcher_compat_version === "string"
|
|
113
|
+
? info.dispatcher_compat_version
|
|
114
|
+
: info.binary_version;
|
|
115
|
+
return compatVersion === VERSION;
|
|
72
116
|
} catch {
|
|
73
117
|
return false;
|
|
74
118
|
}
|
|
@@ -80,6 +124,8 @@ function goBinaryCompatible(binPath) {
|
|
|
80
124
|
* caller must fall back to the existing Python/Node path.
|
|
81
125
|
*/
|
|
82
126
|
function tryGoHotPath(cmdName, target, argv) {
|
|
127
|
+
if (!GO_HOT_PATH_COMMANDS.has(cmdName)) return false;
|
|
128
|
+
if (!goBatchEnabled(cmdName)) return false;
|
|
83
129
|
const bin = locateGoBinary();
|
|
84
130
|
if (!bin) return false;
|
|
85
131
|
if (!goBinaryCompatible(bin)) return false;
|
|
@@ -91,7 +137,16 @@ function tryGoHotPath(cmdName, target, argv) {
|
|
|
91
137
|
}
|
|
92
138
|
|
|
93
139
|
// Export for tests; harmless at runtime.
|
|
94
|
-
module.exports = {
|
|
140
|
+
module.exports = {
|
|
141
|
+
locateGoBinary,
|
|
142
|
+
goBinaryCompatible,
|
|
143
|
+
goBatchEnabled,
|
|
144
|
+
goBatchEnvName,
|
|
145
|
+
goCommandEnvName,
|
|
146
|
+
tryGoHotPath,
|
|
147
|
+
GO_HOT_PATH_BATCHES,
|
|
148
|
+
GO_HOT_PATH_COMMANDS,
|
|
149
|
+
};
|
|
95
150
|
const { loadCanonicalCounts, mcpToolsLabel } = require("../lib/utils/canonical-counts");
|
|
96
151
|
|
|
97
152
|
// --- Command imports ---
|
|
@@ -99,9 +154,13 @@ const { cmdAuthLogin, cmdAuthLogout, cmdRedeem, cmdAuthStatus, cmdAuthMcp, cmdAc
|
|
|
99
154
|
const { cmdInit, cmdSync, cmdProjectBind } = require("../lib/commands/init");
|
|
100
155
|
const { cmdDetect } = require("../lib/commands/detect");
|
|
101
156
|
const { cmdAudit } = require("../lib/commands/audit");
|
|
157
|
+
const { cmdExport } = require("../lib/commands/export");
|
|
158
|
+
const { cmdMcp } = require("../lib/commands/mcp");
|
|
159
|
+
const { cmdVault } = require("../lib/commands/vault");
|
|
102
160
|
const { cmdDoctor } = require("../lib/commands/doctor");
|
|
103
161
|
const { cmdValidate } = require("../lib/commands/validate");
|
|
104
162
|
const { cmdUpdate } = require("../lib/commands/update");
|
|
163
|
+
const { cmdUpgrade } = require("../lib/commands/upgrade");
|
|
105
164
|
const { cmdReflect } = require("../lib/commands/reflect");
|
|
106
165
|
const { cmdMetrics } = require("../lib/commands/metrics");
|
|
107
166
|
const { cmdHeatmap } = require("../lib/commands/heatmap");
|
|
@@ -109,7 +168,7 @@ const { cmdStatus } = require("../lib/commands/status");
|
|
|
109
168
|
const { cmdPortfolio } = require("../lib/commands/portfolio");
|
|
110
169
|
const { cmdRun } = require("../lib/commands/run");
|
|
111
170
|
const { cmdWatch } = require("../lib/commands/watch");
|
|
112
|
-
const { cmdModels } = require("../lib/commands/models");
|
|
171
|
+
const { cmdModels, cmdModelsRecommend } = require("../lib/commands/models");
|
|
113
172
|
const { cmdSession } = require("../lib/commands/session");
|
|
114
173
|
const { cmdSwarm, cmdSwarmRun } = require("../lib/commands/swarm");
|
|
115
174
|
const { cmdStandup } = require("../lib/commands/standup");
|
|
@@ -128,15 +187,24 @@ const { cmdPaste } = require("../lib/commands/paste");
|
|
|
128
187
|
const { cmdReceipt } = require("../lib/commands/receipt");
|
|
129
188
|
const { cmdBoneyard } = require("../lib/commands/boneyard");
|
|
130
189
|
const { cmdQuota } = require("../lib/commands/quota");
|
|
190
|
+
const { cmdUsage } = require("../lib/commands/usage");
|
|
131
191
|
const { cmdGh } = require("../lib/commands/gh");
|
|
132
192
|
const { cmdLoop } = require("../lib/commands/loop");
|
|
133
193
|
const { cmdImportClaudeCodeAgents } = require("../lib/commands/import_claude_code_agents");
|
|
134
194
|
const { cmdRunner } = require("../lib/commands/runner");
|
|
135
195
|
const { cmdCi } = require("../lib/commands/ci");
|
|
196
|
+
const { cmdTrust } = require("../lib/commands/trust");
|
|
136
197
|
|
|
137
198
|
function printHelp() {
|
|
138
199
|
const counts = loadCanonicalCounts();
|
|
139
200
|
console.log(`\n ${T}0dai${R} v${VERSION} — One config for ${counts.agent_clis_total} AI agent CLIs · ${mcpToolsLabel(counts)}\n`);
|
|
201
|
+
console.log("First-run sequence (canonical):");
|
|
202
|
+
console.log(" npm install -g @0dai-dev/cli # install once, globally");
|
|
203
|
+
console.log(" 0dai auth login # sign in (OAuth / device code)");
|
|
204
|
+
console.log(" 0dai activate free # claim free-tier license");
|
|
205
|
+
console.log(" 0dai init # generate ai/ layer in cwd");
|
|
206
|
+
console.log(" 0dai doctor # verify health and drift");
|
|
207
|
+
console.log("");
|
|
140
208
|
console.log("Start (first 5 minutes):");
|
|
141
209
|
console.log(" init Create ai/ layer + MCP [--local] [--dry-run] [--minimal]");
|
|
142
210
|
console.log(" doctor Check health, credentials, and drift [--drift]");
|
|
@@ -150,7 +218,7 @@ function printHelp() {
|
|
|
150
218
|
console.log("");
|
|
151
219
|
console.log("Daily (regular work):");
|
|
152
220
|
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]");
|
|
221
|
+
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
222
|
console.log(" swarm status Show queued, active, and done tasks");
|
|
155
223
|
console.log(" swarm add Queue one task for an agent [--task '...' --to agent]");
|
|
156
224
|
console.log(" swarm-run Add, dispatch, and wait for one swarm task as JSON");
|
|
@@ -162,21 +230,26 @@ function printHelp() {
|
|
|
162
230
|
console.log(" feedback retry Retry queued feedback after a failed push");
|
|
163
231
|
console.log("");
|
|
164
232
|
console.log("Pro / advanced:");
|
|
165
|
-
console.log(" init-existing
|
|
233
|
+
console.log(" init-existing Legacy alias for init (older docs / scripted bootstraps); use 'init' [--minimal] [--dry-run]");
|
|
166
234
|
console.log(" project bind Bind current repository to your 0dai account [--json]");
|
|
167
235
|
console.log(" project status Show local project binding and health state [--json]");
|
|
168
236
|
console.log(" graph push Upload local graph to server (Pro: edges, Free: nodes)");
|
|
169
237
|
console.log(" graph pull Download server graph and merge locally");
|
|
170
238
|
console.log(" graph status Show local graph stats and sync state");
|
|
171
239
|
console.log(" ci Plan portable 0dai CI pipelines and inspect AI-MQ [list|plan|mq-status] [--json]");
|
|
240
|
+
console.log(" mcp MCP server, tools, and health [list|catalog|doctor|call] [--json]");
|
|
241
|
+
console.log(" vault Local age-encrypted secrets vault [init|add|get] [--json]");
|
|
172
242
|
console.log(" heatmap Repo treemap: LOC x agent-edit intensity");
|
|
173
243
|
console.log(" session save Save session for roaming");
|
|
174
244
|
console.log(" provider Local provider profiles, bindings, and direct invoke");
|
|
175
245
|
console.log(" models Show model ratings (--fast/--balanced/--deep/--available)");
|
|
246
|
+
console.log(" models recommend Ledger-ranked model pick for a task type [--task TYPE] [--goal '...'] [--json]");
|
|
176
247
|
console.log(" quota Agent subscription usage table [--refresh] [--json]");
|
|
248
|
+
console.log(" usage Local token, task, and USD usage ledger [status|daily|monthly]");
|
|
177
249
|
console.log(" workspace Manage tmux workspace sessions (init|up|status)");
|
|
178
250
|
console.log(" runner Show runner/project-host architecture, queues, labels, and burst routing [status|plan|queue-status|label-audit|route-dry-run] [--json]");
|
|
179
251
|
console.log(" report Privacy-safe project reports (preview|push|status)");
|
|
252
|
+
console.log(" trust Pre-run blast-radius: protected paths, authority matrix, egress [--json]");
|
|
180
253
|
console.log(" compliance SOC2/ISO evidence and ADR audit-trail export");
|
|
181
254
|
console.log(" experience Structured experience events (list|stats|sync|warnings|dismiss)");
|
|
182
255
|
console.log(" persona-simulate Produce a focus-group report and optional issue drafts");
|
|
@@ -206,6 +279,8 @@ const SYNC_ALLOWED_FLAGS = new Set([
|
|
|
206
279
|
"--force",
|
|
207
280
|
"--no-diff",
|
|
208
281
|
"--no-mcp-auth",
|
|
282
|
+
"--skip-link-check",
|
|
283
|
+
"--strict-links",
|
|
209
284
|
]);
|
|
210
285
|
|
|
211
286
|
function printSyncHelp() {
|
|
@@ -220,6 +295,8 @@ function printSyncHelp() {
|
|
|
220
295
|
console.log(" --force Also overwrite native config files from managed ai/ sources");
|
|
221
296
|
console.log(" --no-diff Hide unified diff output in previews/prompts");
|
|
222
297
|
console.log(" --no-mcp-auth Skip MCP cloud-token bootstrap during sync");
|
|
298
|
+
console.log(" --skip-link-check Skip the SPEC-028 doc cross-link scan after sync");
|
|
299
|
+
console.log(" --strict-links Fail sync if doc_link_check reports broken or closed refs");
|
|
223
300
|
console.log(" --target PATH Run sync against another project path");
|
|
224
301
|
console.log("");
|
|
225
302
|
}
|
|
@@ -281,6 +358,7 @@ async function main() {
|
|
|
281
358
|
case "run": await cmdRun(args[1] || "", target, args.slice(2)); break;
|
|
282
359
|
case "watch": cmdWatch(target, args.slice(1)); break;
|
|
283
360
|
case "audit": cmdAudit(target); break;
|
|
361
|
+
case "export": await cmdExport(target, args); break;
|
|
284
362
|
case "security": {
|
|
285
363
|
const subSec = args[1] || "";
|
|
286
364
|
if (subSec === "install-hook") {
|
|
@@ -324,7 +402,7 @@ async function main() {
|
|
|
324
402
|
if (!driftMode) {
|
|
325
403
|
tryGoHotPath("doctor", target, args.slice(1));
|
|
326
404
|
}
|
|
327
|
-
cmdDoctor(target, { drift: driftMode });
|
|
405
|
+
cmdDoctor(target, { drift: driftMode, json: args.includes("--json") });
|
|
328
406
|
if (args.includes("--drift")) {
|
|
329
407
|
const ds = findRepoScript(target, "drift_detector.py");
|
|
330
408
|
console.log("\n drift report:");
|
|
@@ -352,6 +430,7 @@ async function main() {
|
|
|
352
430
|
case "validate": cmdValidate(target); break;
|
|
353
431
|
case "reflect": cmdReflect(target, args); break;
|
|
354
432
|
case "update": cmdUpdate(args); break;
|
|
433
|
+
case "upgrade": cmdUpgrade(); break;
|
|
355
434
|
case "metrics": cmdMetrics(target); break;
|
|
356
435
|
case "heatmap": cmdHeatmap(target, args.slice(1)); break;
|
|
357
436
|
case "portfolio": cmdPortfolio(); break;
|
|
@@ -377,6 +456,8 @@ async function main() {
|
|
|
377
456
|
else if (sub === "code" || sub === "redeem") await cmdRedeem(args[2]);
|
|
378
457
|
else console.log("Usage: 0dai activate [free|status|code <CODE>]");
|
|
379
458
|
break;
|
|
459
|
+
case "mcp": cmdMcp(target, sub, args); break;
|
|
460
|
+
case "vault": cmdVault(target, args[1], args.slice(2)); break;
|
|
380
461
|
case "session": cmdSession(target, sub, args); break;
|
|
381
462
|
case "swarm": cmdSwarm(target, sub, args); break;
|
|
382
463
|
case "workspace": cmdWorkspace(target, sub, args.slice(2)); break;
|
|
@@ -408,6 +489,7 @@ async function main() {
|
|
|
408
489
|
case "receipt": cmdReceipt(target, args.slice(1)); break;
|
|
409
490
|
case "boneyard": cmdBoneyard(target, args.slice(1)); break;
|
|
410
491
|
case "quota": case "quotas": cmdQuota(target, args.slice(1)); break;
|
|
492
|
+
case "usage": cmdUsage(target, args.slice(1)); break;
|
|
411
493
|
case "gh": await cmdGh(target, sub, args); break;
|
|
412
494
|
case "loop": cmdLoop(target, sub, args); break;
|
|
413
495
|
case "import": {
|
|
@@ -429,11 +511,15 @@ async function main() {
|
|
|
429
511
|
break;
|
|
430
512
|
}
|
|
431
513
|
case "report": cmdReport(target, sub, args); break;
|
|
514
|
+
case "trust": cmdTrust(target, args.slice(1)); break;
|
|
432
515
|
case "compliance": cmdCompliance(target, args.slice(1)); break;
|
|
433
516
|
case "experience": cmdExperience(target, sub, args); break;
|
|
434
517
|
case "persona-simulate": cmdPersonaSimulate(target, args.slice(1)); break;
|
|
435
518
|
case "graph": await cmdGraph(target, sub, args); break;
|
|
436
|
-
case "models":
|
|
519
|
+
case "models":
|
|
520
|
+
if (sub === "recommend") await cmdModelsRecommend(target, args.slice(2));
|
|
521
|
+
else cmdModels(sub || args[1]);
|
|
522
|
+
break;
|
|
437
523
|
case "delegate": case "delegation": {
|
|
438
524
|
const deScript = findRepoScript(target, "delegation_engine.py");
|
|
439
525
|
if (!deScript) { log("delegation engine unavailable"); break; }
|
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
|
|
|
@@ -478,6 +481,54 @@ async function cmdAuthStatus() {
|
|
|
478
481
|
}
|
|
479
482
|
}
|
|
480
483
|
|
|
484
|
+
// Disclosure text for the remote activation event.
|
|
485
|
+
// Exported so tests can assert the notice references the real payload fields.
|
|
486
|
+
const ACTIVATION_TELEMETRY_NOTICE =
|
|
487
|
+
"Telemetry: on activation, 0dai sends an event (free_tier_activated, timestamp, "
|
|
488
|
+
+ "path=cli://activate-free, source=cli) to api.0dai.dev/v1/events via your "
|
|
489
|
+
+ "authenticated session with a device identifier (X-Device-ID header). "
|
|
490
|
+
+ "To opt out: set DO_NOT_TRACK=1 or ODAI_NO_TELEMETRY=1.";
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Post a best-effort activation event to the remote analytics endpoint.
|
|
494
|
+
*
|
|
495
|
+
* Opt-out: set DO_NOT_TRACK=1 or ODAI_NO_TELEMETRY=1 in the environment.
|
|
496
|
+
* When opted out, the local activation log (ai/meta/telemetry/activation.jsonl)
|
|
497
|
+
* is NOT affected — only the remote HTTP POST is skipped.
|
|
498
|
+
*
|
|
499
|
+
* @param {object} [deps]
|
|
500
|
+
* @param {Function} [deps.apiCallFn] - injectable stand-in for apiCall (tests)
|
|
501
|
+
* @param {object} [deps.env] - injectable env (defaults to process.env)
|
|
502
|
+
* @param {Function} [deps.print] - injectable print fn (defaults to console.log)
|
|
503
|
+
*/
|
|
504
|
+
async function trackFreeTierActivated(deps = {}) {
|
|
505
|
+
const env = deps.env || process.env;
|
|
506
|
+
const print = deps.print || console.log;
|
|
507
|
+
const callFn = deps.apiCallFn || apiCall;
|
|
508
|
+
|
|
509
|
+
// Honour standard cross-ecosystem opt-out AND 0dai-specific form.
|
|
510
|
+
if (env.DO_NOT_TRACK === "1" || env.ODAI_NO_TELEMETRY === "1") {
|
|
511
|
+
return; // remote POST skipped; local log is unaffected
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// One-time honest disclosure: what is sent, where, and how to opt out.
|
|
515
|
+
print(ACTIVATION_TELEMETRY_NOTICE);
|
|
516
|
+
|
|
517
|
+
try {
|
|
518
|
+
await callFn("/v1/events", {
|
|
519
|
+
events: [{
|
|
520
|
+
event: "free_tier_activated",
|
|
521
|
+
timestamp: new Date().toISOString(),
|
|
522
|
+
path: "cli://activate-free",
|
|
523
|
+
page: "0dai activate free",
|
|
524
|
+
props: { source: "cli" },
|
|
525
|
+
}],
|
|
526
|
+
});
|
|
527
|
+
} catch {
|
|
528
|
+
// best-effort telemetry — network failure is non-fatal
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
481
532
|
async function cmdActivateFree() {
|
|
482
533
|
const ensureAuthenticated = makeEnsureAuthenticated(cmdAuthLogin);
|
|
483
534
|
await ensureAuthenticated("activation");
|
|
@@ -485,6 +536,9 @@ async function cmdActivateFree() {
|
|
|
485
536
|
log(`license ${license.status}`);
|
|
486
537
|
console.log(` activation id: ${license.activation_id}`);
|
|
487
538
|
console.log(` plan: ${license.plan || "free"}`);
|
|
539
|
+
if (license.status === "active") {
|
|
540
|
+
await trackFreeTierActivated();
|
|
541
|
+
}
|
|
488
542
|
}
|
|
489
543
|
|
|
490
544
|
async function cmdActivateStatus() {
|
|
@@ -520,4 +574,6 @@ module.exports = {
|
|
|
520
574
|
parseActivationArgs,
|
|
521
575
|
parseAuthLoginFlags,
|
|
522
576
|
printDeviceLoginInstructions,
|
|
577
|
+
trackFreeTierActivated,
|
|
578
|
+
ACTIVATION_TELEMETRY_NOTICE,
|
|
523
579
|
};
|
package/lib/commands/detect.js
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
const shared = require("../shared");
|
|
3
|
-
const { D, R, log, apiCall, collectMetadata } = shared;
|
|
3
|
+
const { D, R, log, apiCall, collectMetadata, buildProjectIdentity, hashManifestFiles } = shared;
|
|
4
4
|
|
|
5
5
|
async function cmdDetect(target) {
|
|
6
6
|
const OPTIONAL_CLIS = ["gemini", "aider", "opencode"];
|
|
7
|
-
const
|
|
8
|
-
|
|
7
|
+
const metadata = collectMetadata(target);
|
|
8
|
+
const { projectFiles, manifestContents, clis: localClis } = metadata;
|
|
9
|
+
const identity = buildProjectIdentity(target, metadata);
|
|
10
|
+
// Privacy: send only filenames + SHA-256 hashes, never raw content (closes #4016).
|
|
11
|
+
// Local stack detection is pre-computed and passed as `stack`; server works from
|
|
12
|
+
// project_files + client-derived stack, not file content.
|
|
9
13
|
const result = await apiCall("/v1/detect", {
|
|
10
14
|
project_files: projectFiles,
|
|
11
|
-
|
|
15
|
+
manifest_files: hashManifestFiles(manifestContents),
|
|
12
16
|
available_clis: localClis,
|
|
17
|
+
project_name: identity.project_name,
|
|
18
|
+
stack: identity.stack,
|
|
13
19
|
});
|
|
14
20
|
if (result.error) { log(`error: ${result.error}`); return; }
|
|
15
21
|
console.log(`stack: ${result.stack || "?"}`);
|
package/lib/commands/doctor.js
CHANGED
|
@@ -2,6 +2,34 @@
|
|
|
2
2
|
const shared = require("../shared");
|
|
3
3
|
const { log, T, R, D, fs, path, spawnSync, findRepoScript, SUPPORTED_CLIS, recordExperienceEvent } = shared;
|
|
4
4
|
|
|
5
|
+
const LOCAL_LAYER_CHECKS = Object.freeze([
|
|
6
|
+
["ai/VERSION", "ai/VERSION", "error"],
|
|
7
|
+
["ai/manifest/project.yaml", "ai/manifest/project.yaml", "error"],
|
|
8
|
+
["ai/manifest/discovery.json", "ai/manifest/discovery.json", "warn"],
|
|
9
|
+
["ai/manifest/commands.yaml", "ai/manifest/commands.yaml", "warn"],
|
|
10
|
+
[".claude/settings.json", ".claude/settings.json", "warn"],
|
|
11
|
+
["AGENTS.md", "AGENTS.md", "warn"],
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
function nodePtyProbe() {
|
|
15
|
+
try {
|
|
16
|
+
require.resolve("node-pty");
|
|
17
|
+
return {
|
|
18
|
+
name: "node-pty",
|
|
19
|
+
present: true,
|
|
20
|
+
sev: "ok",
|
|
21
|
+
hint: "node-pty available for terminal session spawn",
|
|
22
|
+
};
|
|
23
|
+
} catch {
|
|
24
|
+
return {
|
|
25
|
+
name: "node-pty",
|
|
26
|
+
present: false,
|
|
27
|
+
sev: "warn",
|
|
28
|
+
hint: "install node-pty for terminal sessions: npm i -g node-pty",
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
5
33
|
function ghostAuthStatus(home = process.env.HOME || process.env.USERPROFILE || "") {
|
|
6
34
|
const customApiEnv = home ? path.join(home, ".custom-api", "custom-api.env") : "";
|
|
7
35
|
const legacyAnthropicEnv = home ? path.join(home, ".claude-api-isolated", "anthropic.env") : "";
|
|
@@ -20,9 +48,35 @@ function ghostAuthStatus(home = process.env.HOME || process.env.USERPROFILE || "
|
|
|
20
48
|
};
|
|
21
49
|
}
|
|
22
50
|
|
|
51
|
+
function collectLayerChecks(target) {
|
|
52
|
+
return LOCAL_LAYER_CHECKS.map(([name, relPath, missingSev]) => {
|
|
53
|
+
const fullPath = path.join(target, relPath);
|
|
54
|
+
const present = fs.existsSync(fullPath);
|
|
55
|
+
return {
|
|
56
|
+
name,
|
|
57
|
+
ok: present,
|
|
58
|
+
sev: present ? "ok" : missingSev,
|
|
59
|
+
hint: present ? "present" : `missing: ${fullPath}`,
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function collectDoctorPayload(target) {
|
|
65
|
+
return {
|
|
66
|
+
binary: "node",
|
|
67
|
+
binary_version: shared.VERSION,
|
|
68
|
+
checks: collectLayerChecks(target),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
23
72
|
function cmdDoctor(target, options = {}) {
|
|
24
73
|
const ai = path.join(target, "ai");
|
|
25
74
|
if (!fs.existsSync(ai)) { log("No 0dai config found. Run: 0dai init"); return; }
|
|
75
|
+
if (options.json) {
|
|
76
|
+
const payload = collectDoctorPayload(target);
|
|
77
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
78
|
+
return payload;
|
|
79
|
+
}
|
|
26
80
|
let v = "?", stack = "generic";
|
|
27
81
|
try { v = fs.readFileSync(path.join(ai, "VERSION"), "utf8").trim(); } catch {}
|
|
28
82
|
try { stack = JSON.parse(fs.readFileSync(path.join(ai, "manifest", "discovery.json"), "utf8")).stack || "generic"; } catch {}
|
|
@@ -33,15 +87,6 @@ function cmdDoctor(target, options = {}) {
|
|
|
33
87
|
const R2 = process.stdout.isTTY ? "\x1b[0m" : "";
|
|
34
88
|
|
|
35
89
|
// --- ai/ layer checks ---
|
|
36
|
-
const layerChecks = {
|
|
37
|
-
"ai/VERSION": { path: path.join(ai, "VERSION"), sev: "error" },
|
|
38
|
-
"ai/manifest/project.yaml": { path: path.join(ai, "manifest", "project.yaml"), sev: "error" },
|
|
39
|
-
"ai/manifest/commands.yaml": { path: path.join(ai, "manifest", "commands.yaml"), sev: "warn" },
|
|
40
|
-
"ai/manifest/discovery.json": { path: path.join(ai, "manifest", "discovery.json"),sev: "warn" },
|
|
41
|
-
".claude/settings.json": { path: path.join(target, ".claude", "settings.json"), sev: "warn" },
|
|
42
|
-
"AGENTS.md": { path: path.join(target, "AGENTS.md"), sev: "warn" },
|
|
43
|
-
};
|
|
44
|
-
|
|
45
90
|
// --- credentials checklist ---
|
|
46
91
|
// Detect subscription-based auth (not just env API keys)
|
|
47
92
|
const { execFileSync: _execFile } = require("child_process");
|
|
@@ -99,14 +144,13 @@ function cmdDoctor(target, options = {}) {
|
|
|
99
144
|
|
|
100
145
|
const missingConfigs = [];
|
|
101
146
|
console.log(" ai/ layer:");
|
|
102
|
-
for (const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
sev === "
|
|
106
|
-
if (sev === "warn") missingConfigs.push(name);
|
|
147
|
+
for (const check of collectLayerChecks(target)) {
|
|
148
|
+
if (!check.ok) {
|
|
149
|
+
check.sev === "error" ? errors++ : warnings++;
|
|
150
|
+
if (check.sev === "warn") missingConfigs.push(check.name);
|
|
107
151
|
}
|
|
108
|
-
const mark =
|
|
109
|
-
console.log(` ${mark.padEnd(22)} ${name}`);
|
|
152
|
+
const mark = check.ok ? `${G}ok${R2}` : check.sev === "error" ? `${E}MISSING${R2}` : `${W}missing${R2}`;
|
|
153
|
+
console.log(` ${mark.padEnd(22)} ${check.name}`);
|
|
110
154
|
}
|
|
111
155
|
// Explain WHY native configs are missing and what to do
|
|
112
156
|
if (missingConfigs.length > 0) {
|
|
@@ -256,4 +300,4 @@ function cmdDoctor(target, options = {}) {
|
|
|
256
300
|
}
|
|
257
301
|
}
|
|
258
302
|
|
|
259
|
-
module.exports = { cmdDoctor, ghostAuthStatus };
|
|
303
|
+
module.exports = { cmdDoctor, ghostAuthStatus, nodePtyProbe, collectDoctorPayload, collectLayerChecks };
|