@0dai-dev/cli 4.1.0 → 4.3.4

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.
Files changed (53) hide show
  1. package/README.md +30 -5
  2. package/bin/0dai.js +308 -60
  3. package/lib/commands/audit.js +13 -0
  4. package/lib/commands/auth.js +404 -122
  5. package/lib/commands/boneyard.js +44 -0
  6. package/lib/commands/ci.js +329 -0
  7. package/lib/commands/compliance.js +20 -0
  8. package/lib/commands/doctor.js +79 -14
  9. package/lib/commands/experience.js +5 -1
  10. package/lib/commands/feedback.js +92 -5
  11. package/lib/commands/gh.js +506 -0
  12. package/lib/commands/graph.js +78 -10
  13. package/lib/commands/heatmap.js +17 -0
  14. package/lib/commands/import_claude_code_agents.js +367 -0
  15. package/lib/commands/init.js +553 -53
  16. package/lib/commands/loop.js +108 -0
  17. package/lib/commands/mcp.js +410 -0
  18. package/lib/commands/models.js +42 -12
  19. package/lib/commands/paste.js +114 -0
  20. package/lib/commands/persona-simulate.js +19 -0
  21. package/lib/commands/play.js +173 -0
  22. package/lib/commands/provider.js +87 -0
  23. package/lib/commands/quota.js +76 -0
  24. package/lib/commands/receipt.js +53 -0
  25. package/lib/commands/report.js +29 -2
  26. package/lib/commands/run.js +44 -4
  27. package/lib/commands/runner.js +527 -0
  28. package/lib/commands/session.js +1 -7
  29. package/lib/commands/ssh.js +416 -0
  30. package/lib/commands/standup.js +40 -0
  31. package/lib/commands/status.js +131 -36
  32. package/lib/commands/swarm.js +97 -4
  33. package/lib/commands/tui.js +117 -0
  34. package/lib/commands/usage.js +87 -0
  35. package/lib/commands/vault.js +246 -0
  36. package/lib/commands/workspace.js +1 -0
  37. package/lib/onboarding.js +30 -10
  38. package/lib/shared.js +153 -96
  39. package/lib/tui/index.mjs +34994 -0
  40. package/lib/utils/auth.js +1 -0
  41. package/lib/utils/canonical-counts.js +54 -0
  42. package/lib/utils/diff-preview.js +192 -0
  43. package/lib/utils/identity.js +76 -18
  44. package/lib/utils/mcp-auth.js +607 -0
  45. package/lib/utils/model_ratings.js +77 -0
  46. package/lib/utils/plan.js +37 -2
  47. package/lib/vault/cipher.js +125 -0
  48. package/lib/vault/identity.js +122 -0
  49. package/lib/vault/index.js +184 -0
  50. package/lib/vault/storage.js +84 -0
  51. package/lib/wizard.js +19 -12
  52. package/package.json +13 -5
  53. package/scripts/build-tui.js +77 -0
package/lib/shared.js CHANGED
@@ -11,6 +11,7 @@ const http = require("http");
11
11
  const fs = require("fs");
12
12
  const path = require("path");
13
13
  const os = require("os");
14
+ const crypto = require("crypto");
14
15
  const { spawnSync } = require("child_process");
15
16
 
16
17
  const VERSION = require("../package.json").version;
@@ -48,13 +49,18 @@ const CONFIG_DIR = path.join(os.homedir(), ".0dai");
48
49
  const AUTH_FILE = path.join(CONFIG_DIR, "auth.json");
49
50
  const VERSION_CHECK_FILE = path.join(CONFIG_DIR, ".version_check");
50
51
  const PROJECTS_FILE = path.join(CONFIG_DIR, "projects.json");
51
- const MANAGED_BEGIN = "<!-- 0dai:managed:begin -->";
52
- const MANAGED_END = "<!-- 0dai:managed:end -->";
53
- const LEGACY_MANAGED_BEGIN = "<!-- zerodayai:managed:begin -->";
54
- const LEGACY_MANAGED_END = "<!-- zerodayai:managed:end -->";
52
+ const DRIFT_TRACKED_CONFIGS = [
53
+ "CLAUDE.md",
54
+ "AGENTS.md",
55
+ "GEMINI.md",
56
+ "opencode.json",
57
+ ".cursorrules",
58
+ ".windsurfrules",
59
+ ".aider.conf.yml",
60
+ ];
55
61
 
56
62
  // --- API ---
57
- function apiCall(endpoint, data) {
63
+ function apiCall(endpoint, data, options = {}) {
58
64
  return new Promise((resolve) => {
59
65
  const url = new URL(endpoint, API_URL);
60
66
  const mod = url.protocol === "https:" ? https : http;
@@ -65,11 +71,16 @@ function apiCall(endpoint, data) {
65
71
  "X-CLI-Version": VERSION,
66
72
  "X-Client-Channel": "npm",
67
73
  };
68
- try {
69
- const auth = JSON.parse(fs.readFileSync(AUTH_FILE, "utf8"));
70
- const token = auth.api_key || auth.access_token || auth.token;
71
- if (token) headers["Authorization"] = `Bearer ${token}`;
72
- } catch {}
74
+ const accessToken = options && options.accessToken ? String(options.accessToken).trim() : "";
75
+ if (accessToken) {
76
+ headers["Authorization"] = `Bearer ${accessToken}`;
77
+ } else {
78
+ try {
79
+ const auth = JSON.parse(fs.readFileSync(AUTH_FILE, "utf8"));
80
+ const token = auth.api_key || auth.access_token || auth.token;
81
+ if (token) headers["Authorization"] = `Bearer ${token}`;
82
+ } catch {}
83
+ }
73
84
  const opts = {
74
85
  hostname: url.hostname,
75
86
  port: url.port || (url.protocol === "https:" ? 443 : 80),
@@ -88,7 +99,7 @@ function apiCall(endpoint, data) {
88
99
  });
89
100
  });
90
101
  req.on("error", (e) => resolve({ error: `${e.message}. Is ${API_URL} reachable?` }));
91
- req.on("timeout", () => { req.destroy(); resolve({ error: "timeout" }); });
102
+ req.on("timeout", () => { req.destroy(); resolve({ error: "request timed out after 60s. Check your internet connection or try again." }); });
92
103
  if (body) req.write(body);
93
104
  req.end();
94
105
  });
@@ -110,12 +121,14 @@ function updateAuthState(patch) {
110
121
  saveAuthState({ ...current, ...patch });
111
122
  }
112
123
 
113
- async function fetchAuthStatus() {
114
- const status = await apiCall("/v1/auth/status");
115
- if (status && !status.error && status.email) {
124
+ async function fetchAuthStatus(accessToken = "") {
125
+ const token = String(accessToken || "").trim();
126
+ const status = await apiCall("/v1/auth/status", null, token ? { accessToken: token } : undefined);
127
+ if (status && !status.error && status.email && !token) {
116
128
  updateAuthState({
117
129
  email: status.email, plan: status.plan || "free",
118
130
  name: status.name || "", license: status.license || {},
131
+ plan_expires_at: status.plan_expires_at || "",
119
132
  });
120
133
  }
121
134
  return status;
@@ -164,14 +177,123 @@ async function ensureLicenseActivation() {
164
177
  }
165
178
 
166
179
  // --- Project Heartbeat ---
167
- async function sendProjectHeartbeat(identity, result, extra = {}) {
180
+ function _hashFileSha256(filePath) {
181
+ const buf = fs.readFileSync(filePath);
182
+ return crypto.createHash("sha256").update(buf).digest("hex");
183
+ }
184
+
185
+ function computeProjectDriftSummary(target) {
186
+ const hashesPath = path.join(target, "ai", "manifest", "config_hashes.json");
187
+ if (!fs.existsSync(hashesPath)) return null;
188
+ let hashes;
189
+ try {
190
+ hashes = JSON.parse(fs.readFileSync(hashesPath, "utf8"));
191
+ } catch {
192
+ return null;
193
+ }
194
+ const findings = [];
195
+ let totalConfigs = 0;
196
+ for (const name of DRIFT_TRACKED_CONFIGS) {
197
+ const filePath = path.join(target, name);
198
+ const exists = fs.existsSync(filePath) && fs.statSync(filePath).isFile();
199
+ const recorded = hashes[name];
200
+ if (exists) totalConfigs += 1;
201
+ if (recorded && !exists) {
202
+ findings.push({ config: name, type: "missing", severity: "warning" });
203
+ continue;
204
+ }
205
+ if (!recorded && exists) {
206
+ findings.push({ config: name, type: "extra", severity: "info" });
207
+ continue;
208
+ }
209
+ if (recorded && exists) {
210
+ try {
211
+ const currentHash = _hashFileSha256(filePath);
212
+ if (currentHash !== String(recorded.hash || "")) {
213
+ findings.push({ config: name, type: "modified", severity: "warning" });
214
+ }
215
+ } catch {
216
+ findings.push({ config: name, type: "unreadable", severity: "warning" });
217
+ }
218
+ }
219
+ }
220
+ const driftedCount = findings.filter((f) => f.type === "modified" || f.type === "missing").length;
221
+ return {
222
+ available: true,
223
+ clean: findings.length === 0,
224
+ drifted_count: driftedCount,
225
+ total_configs: totalConfigs,
226
+ findings,
227
+ updated_at: new Date().toISOString(),
228
+ };
229
+ }
230
+
231
+ async function sendProjectHeartbeat(target, identity, result, extra = {}) {
232
+ const drift = computeProjectDriftSummary(target);
168
233
  return apiCall("/v1/projects/heartbeat", {
169
234
  project_id: identity.project_id, stack: result.stack || identity.stack || "unknown",
170
235
  cli_version: VERSION, activation_status: "active", binding_status: "bound",
171
- runtime_sessions: 0, swarm_active: 0, swarm_done: 0, channel: "npm", ...extra,
236
+ runtime_sessions: 0, swarm_active: 0, swarm_done: 0, channel: "npm",
237
+ ...(drift ? { drift } : {}),
238
+ ...extra,
172
239
  });
173
240
  }
174
241
 
242
+ // --- First-run proof gate (issue #342) ---
243
+ //
244
+ // Idempotently appends a `first_run.success` event to ai/manifest/audit.jsonl
245
+ // when all 4 activation gates have passed. Matches scripts/audit.py shape.
246
+ // Returns { fired: boolean, elapsedS: number|null }.
247
+ function logFirstRunSuccess(target, gates) {
248
+ try {
249
+ const auditPath = path.join(target, "ai", "manifest", "audit.jsonl");
250
+ if (!fs.existsSync(path.dirname(auditPath))) {
251
+ fs.mkdirSync(path.dirname(auditPath), { recursive: true });
252
+ }
253
+
254
+ // Idempotency: skip if a first_run.success event already exists for this project.
255
+ if (fs.existsSync(auditPath)) {
256
+ const existing = fs.readFileSync(auditPath, "utf8");
257
+ if (existing.includes('"action":"first_run.success"') ||
258
+ existing.includes('"action": "first_run.success"')) {
259
+ return { fired: false, elapsedS: null };
260
+ }
261
+ }
262
+
263
+ // Compute elapsed_s from the local first-run marker.
264
+ let elapsedS = null;
265
+ try {
266
+ const ob = require("./onboarding");
267
+ ob.trackFirstInit(target);
268
+ elapsedS = ob.getTimeToInit(target);
269
+ } catch {}
270
+
271
+ // Read current ai/VERSION for the entry's ai_version field.
272
+ let aiVersion = null;
273
+ try {
274
+ const vf = path.join(target, "ai", "VERSION");
275
+ if (fs.existsSync(vf)) aiVersion = fs.readFileSync(vf, "utf8").trim();
276
+ } catch {}
277
+
278
+ const entry = {
279
+ timestamp: new Date().toISOString(),
280
+ action: "first_run.success",
281
+ actor: "cli",
282
+ user: process.env.USER || process.env.USERNAME || "unknown",
283
+ ai_version: aiVersion,
284
+ details: JSON.stringify({
285
+ elapsed_s: elapsedS,
286
+ gates: gates || {},
287
+ }),
288
+ };
289
+
290
+ fs.appendFileSync(auditPath, JSON.stringify(entry) + "\n", "utf8");
291
+ return { fired: true, elapsedS };
292
+ } catch {
293
+ return { fired: false, elapsedS: null };
294
+ }
295
+ }
296
+
175
297
  // --- File Writing ---
176
298
  function mergeSettingsJson(existing, incoming) {
177
299
  try {
@@ -184,34 +306,11 @@ function mergeSettingsJson(existing, incoming) {
184
306
  } catch { return incoming; }
185
307
  }
186
308
 
187
- function mergeManagedMarkdown(existing, incoming) {
188
- let src = incoming;
189
- if (src.startsWith("# managed: true")) {
190
- src = src.split("\n").slice(1).join("\n").trimStart();
191
- }
192
- const managedBody = `${MANAGED_BEGIN}\n${src.trim()}\n${MANAGED_END}\n`;
193
- if (existing.includes(MANAGED_BEGIN) && existing.includes(MANAGED_END)) {
194
- const start = existing.indexOf(MANAGED_BEGIN);
195
- const finish = existing.indexOf(MANAGED_END) + MANAGED_END.length;
196
- return existing.slice(0, start) + managedBody + existing.slice(finish);
197
- }
198
- if (existing.includes(LEGACY_MANAGED_BEGIN) && existing.includes(LEGACY_MANAGED_END)) {
199
- const finish = existing.indexOf(LEGACY_MANAGED_END) + LEGACY_MANAGED_END.length;
200
- const rest = existing.slice(finish).trimStart();
201
- return rest ? `${managedBody}\n${rest}` : managedBody;
202
- }
203
- return `${managedBody}\n${existing}`;
204
- }
205
-
206
- function contentLooksManaged(existing) {
207
- return existing.includes("managed: true") || existing.includes('"managed": true') ||
208
- (existing.includes(MANAGED_BEGIN) && existing.includes(MANAGED_END)) ||
209
- (existing.includes(LEGACY_MANAGED_BEGIN) && existing.includes(LEGACY_MANAGED_END));
210
- }
211
-
212
309
  function writeFiles(target, files) {
213
310
  let created = 0, updated = 0, unchanged = 0, merged = 0, skipped = 0;
214
311
  const targetResolved = path.resolve(target);
312
+ let backupDir = null;
313
+ const backupConfigs = new Set(["CLAUDE.md", "AGENTS.md"]);
215
314
  for (const [rel, content] of Object.entries(files)) {
216
315
  if (typeof rel !== "string" || !rel || path.isAbsolute(rel) || rel.split(/[/\\]/).includes("..")) {
217
316
  skipped++; continue;
@@ -224,11 +323,16 @@ function writeFiles(target, files) {
224
323
  const existing = fs.readFileSync(p, "utf8");
225
324
  if (existing === content) { unchanged++; continue; }
226
325
  if (rel.endsWith("settings.json")) { finalContent = mergeSettingsJson(existing, content); merged++; }
227
- else if (rel === "AGENTS.md") {
228
- if (existing.includes("managed: false")) { unchanged++; continue; }
229
- const backupDir = path.join(target, "ai", ".backups");
230
- fs.mkdirSync(backupDir, { recursive: true });
231
- fs.writeFileSync(path.join(backupDir, "AGENTS.md.bak"), existing, "utf8");
326
+ else if (backupConfigs.has(rel)) {
327
+ if (rel === "AGENTS.md" && existing.includes("managed: false")) { unchanged++; continue; }
328
+ if (!backupDir) {
329
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
330
+ backupDir = path.join(targetResolved, "ai", ".backups", timestamp);
331
+ fs.mkdirSync(backupDir, { recursive: true });
332
+ }
333
+ const backupPath = path.join(backupDir, rel);
334
+ fs.writeFileSync(backupPath, existing, "utf8");
335
+ log(`backed up existing ${rel} to ${backupPath}`);
232
336
  updated++;
233
337
  } else { updated++; }
234
338
  } else { created++; }
@@ -241,60 +345,13 @@ function writeFiles(target, files) {
241
345
  return created + updated;
242
346
  }
243
347
 
244
- function writeManagedFiles(target, files) {
245
- let created = 0, updated = 0, unchanged = 0, merged = 0, staged = 0, skipped = 0;
246
- const targetResolved = path.resolve(target);
247
- for (const [rel, content] of Object.entries(files)) {
248
- if (typeof rel !== "string" || !rel || path.isAbsolute(rel) || rel.split(/[/\\]/).includes("..")) {
249
- skipped++; continue;
250
- }
251
- const p = path.resolve(targetResolved, rel);
252
- if (!p.startsWith(targetResolved + path.sep) && p !== targetResolved) { skipped++; continue; }
253
- fs.mkdirSync(path.dirname(p), { recursive: true });
254
- if (!fs.existsSync(p)) {
255
- fs.writeFileSync(p, content, "utf8");
256
- created++;
257
- continue;
258
- }
259
- const existing = fs.readFileSync(p, "utf8");
260
- if (existing === content) {
261
- unchanged++;
262
- continue;
263
- }
264
-
265
- if (rel.endsWith("settings.json")) {
266
- fs.writeFileSync(p, mergeSettingsJson(existing, content), "utf8");
267
- merged++;
268
- continue;
269
- }
270
- if (rel === "AGENTS.md" || rel.endsWith("/CLAUDE.md")) {
271
- fs.writeFileSync(p, mergeManagedMarkdown(existing, content), "utf8");
272
- merged++;
273
- continue;
274
- }
275
- if (!contentLooksManaged(existing)) {
276
- fs.writeFileSync(`${p}.generated`, content, "utf8");
277
- staged++;
278
- continue;
279
- }
280
-
281
- fs.writeFileSync(p, content, "utf8");
282
- updated++;
283
- }
284
- const parts = [`${created} created`, `${updated} updated`, `${unchanged} unchanged`];
285
- if (merged) parts.push(`${merged} merged`);
286
- if (staged) parts.push(`${staged} staged`);
287
- if (skipped) parts.push(`${skipped} skipped (unsafe path)`);
288
- log(parts.join(", "));
289
- return created + updated + merged;
290
- }
291
-
292
348
  // --- Repo Script Lookup ---
293
349
  function findRepoScript(target, scriptName) {
294
350
  const candidates = [
295
351
  path.join(target, "scripts", scriptName),
296
352
  path.join(process.cwd(), "scripts", scriptName),
297
353
  path.join(__dirname, "..", "..", "..", "scripts", scriptName),
354
+ path.join(__dirname, "..", "..", "..", "..", "scripts", scriptName),
298
355
  ];
299
356
  for (const c of candidates) { if (fs.existsSync(c)) return c; }
300
357
  return null;
@@ -350,9 +407,9 @@ module.exports = {
350
407
  // Plan / Tier
351
408
  _detectPlanLocal, requirePlan, getSwarmQuotaLocal,
352
409
  // Project
353
- sendProjectHeartbeat, recordExperienceEvent,
410
+ sendProjectHeartbeat, recordExperienceEvent, logFirstRunSuccess,
354
411
  // Files
355
- mergeSettingsJson, mergeManagedMarkdown, writeFiles, writeManagedFiles, findRepoScript,
412
+ mergeSettingsJson, writeFiles, findRepoScript,
356
413
  // Version
357
414
  checkVersion,
358
415
  // Re-exports for convenience