@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 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 (status/doctor/version) we attempt to delegate
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 `binary_version` matching the dispatcher VERSION
39
- * (we accept exact match only drift means fall back to Python/Node).
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
- return info.binary_version === VERSION;
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 = { locateGoBinary, goBinaryCompatible, tryGoHotPath };
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 Existing-repo setup alias for init [--minimal] [--dry-run]");
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": cmdModels(sub || args[1]); break;
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; }
@@ -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
  };
@@ -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 { projectFiles, manifestContents, clis: localClis } = collectMetadata(target);
8
- // Send file contents AND local CLI inventory so server can do content-based detection
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
- manifest_contents: manifestContents,
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 || "?"}`);
@@ -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 [name, { path: p, sev }] of Object.entries(layerChecks)) {
122
- const exists = fs.existsSync(p);
123
- if (!exists) {
124
- sev === "error" ? errors++ : warnings++;
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 = exists ? `${G}ok${R2}` : sev === "error" ? `${E}MISSING${R2}` : `${W}missing${R2}`;
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 };
@@ -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
- manifest_contents: manifestContents,
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, manifest_contents: manifestContents,
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,
@@ -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 && (report.tier_manifest.ok || report.catalog.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
 
@@ -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
 
@@ -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 { getActivationDurationStats, printActivationStats } = require("../utils/activation_telemetry");
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