@0dai-dev/cli 4.3.5 → 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/bin/0dai.js +87 -10
- package/lib/commands/auth.js +53 -0
- package/lib/commands/detect.js +10 -4
- package/lib/commands/doctor.js +42 -17
- package/lib/commands/export.js +73 -0
- package/lib/commands/init.js +14 -4
- package/lib/commands/mcp.js +33 -3
- package/lib/commands/run.js +4 -1
- package/lib/commands/status.js +6 -1
- package/lib/commands/trust.js +286 -0
- package/lib/commands/vault.js +3 -1
- package/lib/shared.js +2 -2
- package/lib/utils/activation_telemetry.js +233 -11
- package/lib/utils/export-bundler.js +285 -0
- package/lib/utils/identity.js +14 -1
- package/package.json +2 -2
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,6 +154,9 @@ 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");
|
|
@@ -110,7 +168,7 @@ const { cmdStatus } = require("../lib/commands/status");
|
|
|
110
168
|
const { cmdPortfolio } = require("../lib/commands/portfolio");
|
|
111
169
|
const { cmdRun } = require("../lib/commands/run");
|
|
112
170
|
const { cmdWatch } = require("../lib/commands/watch");
|
|
113
|
-
const { cmdModels } = require("../lib/commands/models");
|
|
171
|
+
const { cmdModels, cmdModelsRecommend } = require("../lib/commands/models");
|
|
114
172
|
const { cmdSession } = require("../lib/commands/session");
|
|
115
173
|
const { cmdSwarm, cmdSwarmRun } = require("../lib/commands/swarm");
|
|
116
174
|
const { cmdStandup } = require("../lib/commands/standup");
|
|
@@ -135,10 +193,18 @@ const { cmdLoop } = require("../lib/commands/loop");
|
|
|
135
193
|
const { cmdImportClaudeCodeAgents } = require("../lib/commands/import_claude_code_agents");
|
|
136
194
|
const { cmdRunner } = require("../lib/commands/runner");
|
|
137
195
|
const { cmdCi } = require("../lib/commands/ci");
|
|
196
|
+
const { cmdTrust } = require("../lib/commands/trust");
|
|
138
197
|
|
|
139
198
|
function printHelp() {
|
|
140
199
|
const counts = loadCanonicalCounts();
|
|
141
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("");
|
|
142
208
|
console.log("Start (first 5 minutes):");
|
|
143
209
|
console.log(" init Create ai/ layer + MCP [--local] [--dry-run] [--minimal]");
|
|
144
210
|
console.log(" doctor Check health, credentials, and drift [--drift]");
|
|
@@ -164,22 +230,26 @@ function printHelp() {
|
|
|
164
230
|
console.log(" feedback retry Retry queued feedback after a failed push");
|
|
165
231
|
console.log("");
|
|
166
232
|
console.log("Pro / advanced:");
|
|
167
|
-
console.log(" init-existing
|
|
233
|
+
console.log(" init-existing Legacy alias for init (older docs / scripted bootstraps); use 'init' [--minimal] [--dry-run]");
|
|
168
234
|
console.log(" project bind Bind current repository to your 0dai account [--json]");
|
|
169
235
|
console.log(" project status Show local project binding and health state [--json]");
|
|
170
236
|
console.log(" graph push Upload local graph to server (Pro: edges, Free: nodes)");
|
|
171
237
|
console.log(" graph pull Download server graph and merge locally");
|
|
172
238
|
console.log(" graph status Show local graph stats and sync state");
|
|
173
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]");
|
|
174
242
|
console.log(" heatmap Repo treemap: LOC x agent-edit intensity");
|
|
175
243
|
console.log(" session save Save session for roaming");
|
|
176
244
|
console.log(" provider Local provider profiles, bindings, and direct invoke");
|
|
177
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]");
|
|
178
247
|
console.log(" quota Agent subscription usage table [--refresh] [--json]");
|
|
179
248
|
console.log(" usage Local token, task, and USD usage ledger [status|daily|monthly]");
|
|
180
249
|
console.log(" workspace Manage tmux workspace sessions (init|up|status)");
|
|
181
250
|
console.log(" runner Show runner/project-host architecture, queues, labels, and burst routing [status|plan|queue-status|label-audit|route-dry-run] [--json]");
|
|
182
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]");
|
|
183
253
|
console.log(" compliance SOC2/ISO evidence and ADR audit-trail export");
|
|
184
254
|
console.log(" experience Structured experience events (list|stats|sync|warnings|dismiss)");
|
|
185
255
|
console.log(" persona-simulate Produce a focus-group report and optional issue drafts");
|
|
@@ -288,6 +358,7 @@ async function main() {
|
|
|
288
358
|
case "run": await cmdRun(args[1] || "", target, args.slice(2)); break;
|
|
289
359
|
case "watch": cmdWatch(target, args.slice(1)); break;
|
|
290
360
|
case "audit": cmdAudit(target); break;
|
|
361
|
+
case "export": await cmdExport(target, args); break;
|
|
291
362
|
case "security": {
|
|
292
363
|
const subSec = args[1] || "";
|
|
293
364
|
if (subSec === "install-hook") {
|
|
@@ -331,7 +402,7 @@ async function main() {
|
|
|
331
402
|
if (!driftMode) {
|
|
332
403
|
tryGoHotPath("doctor", target, args.slice(1));
|
|
333
404
|
}
|
|
334
|
-
cmdDoctor(target, { drift: driftMode });
|
|
405
|
+
cmdDoctor(target, { drift: driftMode, json: args.includes("--json") });
|
|
335
406
|
if (args.includes("--drift")) {
|
|
336
407
|
const ds = findRepoScript(target, "drift_detector.py");
|
|
337
408
|
console.log("\n drift report:");
|
|
@@ -385,6 +456,8 @@ async function main() {
|
|
|
385
456
|
else if (sub === "code" || sub === "redeem") await cmdRedeem(args[2]);
|
|
386
457
|
else console.log("Usage: 0dai activate [free|status|code <CODE>]");
|
|
387
458
|
break;
|
|
459
|
+
case "mcp": cmdMcp(target, sub, args); break;
|
|
460
|
+
case "vault": cmdVault(target, args[1], args.slice(2)); break;
|
|
388
461
|
case "session": cmdSession(target, sub, args); break;
|
|
389
462
|
case "swarm": cmdSwarm(target, sub, args); break;
|
|
390
463
|
case "workspace": cmdWorkspace(target, sub, args.slice(2)); break;
|
|
@@ -438,11 +511,15 @@ async function main() {
|
|
|
438
511
|
break;
|
|
439
512
|
}
|
|
440
513
|
case "report": cmdReport(target, sub, args); break;
|
|
514
|
+
case "trust": cmdTrust(target, args.slice(1)); break;
|
|
441
515
|
case "compliance": cmdCompliance(target, args.slice(1)); break;
|
|
442
516
|
case "experience": cmdExperience(target, sub, args); break;
|
|
443
517
|
case "persona-simulate": cmdPersonaSimulate(target, args.slice(1)); break;
|
|
444
518
|
case "graph": await cmdGraph(target, sub, args); break;
|
|
445
|
-
case "models":
|
|
519
|
+
case "models":
|
|
520
|
+
if (sub === "recommend") await cmdModelsRecommend(target, args.slice(2));
|
|
521
|
+
else cmdModels(sub || args[1]);
|
|
522
|
+
break;
|
|
446
523
|
case "delegate": case "delegation": {
|
|
447
524
|
const deScript = findRepoScript(target, "delegation_engine.py");
|
|
448
525
|
if (!deScript) { log("delegation engine unavailable"); break; }
|
package/lib/commands/auth.js
CHANGED
|
@@ -481,6 +481,54 @@ async function cmdAuthStatus() {
|
|
|
481
481
|
}
|
|
482
482
|
}
|
|
483
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
|
+
|
|
484
532
|
async function cmdActivateFree() {
|
|
485
533
|
const ensureAuthenticated = makeEnsureAuthenticated(cmdAuthLogin);
|
|
486
534
|
await ensureAuthenticated("activation");
|
|
@@ -488,6 +536,9 @@ async function cmdActivateFree() {
|
|
|
488
536
|
log(`license ${license.status}`);
|
|
489
537
|
console.log(` activation id: ${license.activation_id}`);
|
|
490
538
|
console.log(` plan: ${license.plan || "free"}`);
|
|
539
|
+
if (license.status === "active") {
|
|
540
|
+
await trackFreeTierActivated();
|
|
541
|
+
}
|
|
491
542
|
}
|
|
492
543
|
|
|
493
544
|
async function cmdActivateStatus() {
|
|
@@ -523,4 +574,6 @@ module.exports = {
|
|
|
523
574
|
parseActivationArgs,
|
|
524
575
|
parseAuthLoginFlags,
|
|
525
576
|
printDeviceLoginInstructions,
|
|
577
|
+
trackFreeTierActivated,
|
|
578
|
+
ACTIVATION_TELEMETRY_NOTICE,
|
|
526
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,15 @@
|
|
|
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
|
+
|
|
5
14
|
function nodePtyProbe() {
|
|
6
15
|
try {
|
|
7
16
|
require.resolve("node-pty");
|
|
@@ -39,9 +48,35 @@ function ghostAuthStatus(home = process.env.HOME || process.env.USERPROFILE || "
|
|
|
39
48
|
};
|
|
40
49
|
}
|
|
41
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
|
+
|
|
42
72
|
function cmdDoctor(target, options = {}) {
|
|
43
73
|
const ai = path.join(target, "ai");
|
|
44
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
|
+
}
|
|
45
80
|
let v = "?", stack = "generic";
|
|
46
81
|
try { v = fs.readFileSync(path.join(ai, "VERSION"), "utf8").trim(); } catch {}
|
|
47
82
|
try { stack = JSON.parse(fs.readFileSync(path.join(ai, "manifest", "discovery.json"), "utf8")).stack || "generic"; } catch {}
|
|
@@ -52,15 +87,6 @@ function cmdDoctor(target, options = {}) {
|
|
|
52
87
|
const R2 = process.stdout.isTTY ? "\x1b[0m" : "";
|
|
53
88
|
|
|
54
89
|
// --- ai/ layer checks ---
|
|
55
|
-
const layerChecks = {
|
|
56
|
-
"ai/VERSION": { path: path.join(ai, "VERSION"), sev: "error" },
|
|
57
|
-
"ai/manifest/project.yaml": { path: path.join(ai, "manifest", "project.yaml"), sev: "error" },
|
|
58
|
-
"ai/manifest/commands.yaml": { path: path.join(ai, "manifest", "commands.yaml"), sev: "warn" },
|
|
59
|
-
"ai/manifest/discovery.json": { path: path.join(ai, "manifest", "discovery.json"),sev: "warn" },
|
|
60
|
-
".claude/settings.json": { path: path.join(target, ".claude", "settings.json"), sev: "warn" },
|
|
61
|
-
"AGENTS.md": { path: path.join(target, "AGENTS.md"), sev: "warn" },
|
|
62
|
-
};
|
|
63
|
-
|
|
64
90
|
// --- credentials checklist ---
|
|
65
91
|
// Detect subscription-based auth (not just env API keys)
|
|
66
92
|
const { execFileSync: _execFile } = require("child_process");
|
|
@@ -118,14 +144,13 @@ function cmdDoctor(target, options = {}) {
|
|
|
118
144
|
|
|
119
145
|
const missingConfigs = [];
|
|
120
146
|
console.log(" ai/ layer:");
|
|
121
|
-
for (const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
sev === "
|
|
125
|
-
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);
|
|
126
151
|
}
|
|
127
|
-
const mark =
|
|
128
|
-
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}`);
|
|
129
154
|
}
|
|
130
155
|
// Explain WHY native configs are missing and what to do
|
|
131
156
|
if (missingConfigs.length > 0) {
|
|
@@ -275,4 +300,4 @@ function cmdDoctor(target, options = {}) {
|
|
|
275
300
|
}
|
|
276
301
|
}
|
|
277
302
|
|
|
278
|
-
module.exports = { cmdDoctor, ghostAuthStatus, nodePtyProbe };
|
|
303
|
+
module.exports = { cmdDoctor, ghostAuthStatus, nodePtyProbe, collectDoctorPayload, collectLayerChecks };
|
|
@@ -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;
|
|
@@ -364,11 +364,15 @@ async function cmdInit(target, args = []) {
|
|
|
364
364
|
else if (!dryRun) log(`sending to API (${projectFiles.length} files, ${clis.length} CLIs)...`);
|
|
365
365
|
const result = await apiCall("/v1/init", {
|
|
366
366
|
project_files: projectFiles,
|
|
367
|
-
|
|
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),
|
|
368
371
|
available_clis: clis,
|
|
369
372
|
dry_run: dryRun,
|
|
370
373
|
minimal: minimal,
|
|
371
374
|
project_name: identity.project_name,
|
|
375
|
+
stack: identity.stack,
|
|
372
376
|
project_id: boundProject.project_id || identity.project_id,
|
|
373
377
|
remote_origin: identity.remote_origin,
|
|
374
378
|
origin: identity.origin,
|
|
@@ -464,7 +468,9 @@ async function cmdInit(target, args = []) {
|
|
|
464
468
|
// First-run proof gate (issue #342). All 4 gates pass once we reach here:
|
|
465
469
|
// license active (line above), project bound, ai/ layer written, heartbeat sent.
|
|
466
470
|
// Idempotent — only fires once per project. See docs/first-run.md.
|
|
467
|
-
recordActivationInit(target, boundProject.project_id || identity.project_id
|
|
471
|
+
recordActivationInit(target, boundProject.project_id || identity.project_id, {
|
|
472
|
+
stack: result.stack || identity.stack || "unknown",
|
|
473
|
+
});
|
|
468
474
|
|
|
469
475
|
const firstRun = logFirstRunSuccess(target, {
|
|
470
476
|
license: true,
|
|
@@ -665,7 +671,11 @@ async function cmdSync(target, args = []) {
|
|
|
665
671
|
|
|
666
672
|
const result = await apiCall("/v1/sync", {
|
|
667
673
|
ai_version: version, stack, agents: agents.length ? agents : clis,
|
|
668
|
-
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),
|
|
669
679
|
dry_run: dryRun, quiet, force,
|
|
670
680
|
project_name: identity.project_name,
|
|
671
681
|
project_id: boundProject.project_id || identity.project_id,
|
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
|
@@ -125,7 +125,10 @@ async function cmdRun(goal, target, args = []) {
|
|
|
125
125
|
|
|
126
126
|
if (created.length > 0) {
|
|
127
127
|
const identity = shared.buildProjectIdentity(target, shared.collectMetadata(target));
|
|
128
|
-
recordActivationFirstTask(target, identity.project_id
|
|
128
|
+
recordActivationFirstTask(target, identity.project_id, {
|
|
129
|
+
outcome: "task_queued",
|
|
130
|
+
task_count: created.length,
|
|
131
|
+
});
|
|
129
132
|
}
|
|
130
133
|
}
|
|
131
134
|
|
package/lib/commands/status.js
CHANGED
|
@@ -4,7 +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 {
|
|
7
|
+
const {
|
|
8
|
+
getActivationDurationStats,
|
|
9
|
+
getActivationMergedPrStats,
|
|
10
|
+
printActivationStats,
|
|
11
|
+
} = require("../utils/activation_telemetry");
|
|
8
12
|
|
|
9
13
|
function countJson(dir) {
|
|
10
14
|
try { return fs.readdirSync(dir).filter(f => f.endsWith(".json")).length; } catch { return 0; }
|
|
@@ -118,6 +122,7 @@ function collectStatusPayload(target) {
|
|
|
118
122
|
},
|
|
119
123
|
warnings: warningCount,
|
|
120
124
|
activation_ttfv: getActivationDurationStats(target),
|
|
125
|
+
activation_first_merged_pr: getActivationMergedPrStats(target),
|
|
121
126
|
};
|
|
122
127
|
}
|
|
123
128
|
|