@0dai-dev/cli 4.2.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 (48) hide show
  1. package/README.md +30 -5
  2. package/bin/0dai.js +289 -60
  3. package/lib/commands/audit.js +13 -0
  4. package/lib/commands/auth.js +341 -98
  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 +20 -1
  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 +440 -28
  16. package/lib/commands/loop.js +108 -0
  17. package/lib/commands/mcp.js +410 -0
  18. package/lib/commands/models.js +27 -3
  19. package/lib/commands/paste.js +114 -0
  20. package/lib/commands/play.js +173 -0
  21. package/lib/commands/provider.js +69 -0
  22. package/lib/commands/quota.js +76 -0
  23. package/lib/commands/receipt.js +53 -0
  24. package/lib/commands/report.js +29 -2
  25. package/lib/commands/run.js +44 -4
  26. package/lib/commands/runner.js +527 -0
  27. package/lib/commands/session.js +1 -7
  28. package/lib/commands/standup.js +40 -0
  29. package/lib/commands/status.js +26 -1
  30. package/lib/commands/swarm.js +97 -4
  31. package/lib/commands/tui.js +81 -13
  32. package/lib/commands/usage.js +87 -0
  33. package/lib/commands/vault.js +246 -0
  34. package/lib/onboarding.js +9 -3
  35. package/lib/shared.js +29 -14
  36. package/lib/tui/index.mjs +571 -187
  37. package/lib/utils/auth.js +1 -0
  38. package/lib/utils/canonical-counts.js +54 -0
  39. package/lib/utils/diff-preview.js +192 -0
  40. package/lib/utils/identity.js +76 -18
  41. package/lib/utils/mcp-auth.js +607 -0
  42. package/lib/utils/plan.js +37 -2
  43. package/lib/vault/cipher.js +125 -0
  44. package/lib/vault/identity.js +122 -0
  45. package/lib/vault/index.js +184 -0
  46. package/lib/vault/storage.js +84 -0
  47. package/lib/wizard.js +19 -12
  48. package/package.json +2 -2
package/lib/utils/auth.js CHANGED
@@ -85,6 +85,7 @@ async function fetchAuthStatus(apiCallFn, API_URL) {
85
85
  plan: status.plan || "free",
86
86
  name: status.name || "",
87
87
  license: status.license || {},
88
+ plan_expires_at: status.plan_expires_at || "",
88
89
  });
89
90
  }
90
91
  return status;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Runtime reader for generated canonical product counts.
3
+ */
4
+ "use strict";
5
+
6
+ const fs = require("fs");
7
+ const path = require("path");
8
+ const { SUPPORTED_CLIS } = require("./constants");
9
+
10
+ const FALLBACK = Object.freeze({
11
+ schema_version: 0,
12
+ supported_agents: SUPPORTED_CLIS.map((cli) => cli.name),
13
+ supported_agents_total: SUPPORTED_CLIS.length,
14
+ supported_runtime_agents: SUPPORTED_CLIS.map((cli) => cli.name),
15
+ supported_runtime_agents_total: SUPPORTED_CLIS.length,
16
+ supported_agent_clis: SUPPORTED_CLIS.map((cli) => cli.name),
17
+ supported_agent_cli_display_names: SUPPORTED_CLIS.map((cli) => cli.name),
18
+ agent_clis_total: SUPPORTED_CLIS.length,
19
+ mcp_tools_total: null,
20
+ free_mcp_tools_total: null,
21
+ pro_mcp_tools_total: null,
22
+ cli_commands_total: null,
23
+ integrations_total: null,
24
+ });
25
+
26
+ function candidatePaths() {
27
+ const envPath = process.env.ODAI_CANONICAL_COUNTS_PATH;
28
+ return [
29
+ envPath,
30
+ path.resolve(__dirname, "../../../../ai/manifest/canonical-counts.json"),
31
+ path.resolve(process.cwd(), "ai/manifest/canonical-counts.json"),
32
+ ].filter(Boolean);
33
+ }
34
+
35
+ function loadCanonicalCounts() {
36
+ for (const candidate of candidatePaths()) {
37
+ try {
38
+ if (!fs.existsSync(candidate)) continue;
39
+ const parsed = JSON.parse(fs.readFileSync(candidate, "utf8"));
40
+ if (parsed && typeof parsed === "object") {
41
+ return { ...FALLBACK, ...parsed };
42
+ }
43
+ } catch {}
44
+ }
45
+ return { ...FALLBACK };
46
+ }
47
+
48
+ function mcpToolsLabel(counts = loadCanonicalCounts()) {
49
+ return Number.isInteger(counts.mcp_tools_total)
50
+ ? `${counts.mcp_tools_total} MCP tools`
51
+ : "MCP tools";
52
+ }
53
+
54
+ module.exports = { loadCanonicalCounts, mcpToolsLabel };
@@ -0,0 +1,192 @@
1
+ "use strict";
2
+
3
+ const crypto = require("crypto");
4
+ const readline = require("readline");
5
+
6
+ function _lines(text) {
7
+ if (text == null) return [];
8
+ const s = String(text);
9
+ return s.length === 0 ? [] : s.split(/\r?\n/);
10
+ }
11
+
12
+ function unifiedDiff(relPath, before, after, context = 3) {
13
+ const a = _lines(before);
14
+ const b = _lines(after);
15
+ if (a.join("\n") === b.join("\n")) return "";
16
+
17
+ const n = a.length;
18
+ const m = b.length;
19
+ const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
20
+ for (let i = n - 1; i >= 0; i--) {
21
+ for (let j = m - 1; j >= 0; j--) {
22
+ if (a[i] === b[j]) dp[i][j] = dp[i + 1][j + 1] + 1;
23
+ else dp[i][j] = Math.max(dp[i + 1][j], dp[i][j + 1]);
24
+ }
25
+ }
26
+
27
+ const ops = [];
28
+ let i = 0, j = 0;
29
+ while (i < n && j < m) {
30
+ if (a[i] === b[j]) { ops.push(["=", a[i]]); i++; j++; }
31
+ else if (dp[i + 1][j] >= dp[i][j + 1]) { ops.push(["-", a[i]]); i++; }
32
+ else { ops.push(["+", b[j]]); j++; }
33
+ }
34
+ while (i < n) { ops.push(["-", a[i++]]); }
35
+ while (j < m) { ops.push(["+", b[j++]]); }
36
+
37
+ const hunks = [];
38
+ let k = 0;
39
+ while (k < ops.length) {
40
+ if (ops[k][0] === "=") { k++; continue; }
41
+ let start = Math.max(0, k - context);
42
+ let end = k;
43
+ while (end < ops.length) {
44
+ if (ops[end][0] !== "=") { end++; continue; }
45
+ let run = 0;
46
+ let peek = end;
47
+ while (peek < ops.length && ops[peek][0] === "=") { peek++; run++; }
48
+ if (run > context * 2 || peek === ops.length) {
49
+ end += Math.min(run, context);
50
+ break;
51
+ }
52
+ end = peek;
53
+ }
54
+ hunks.push([start, end]);
55
+ k = end;
56
+ }
57
+
58
+ const out = [`--- a/${relPath}`, `+++ b/${relPath}`];
59
+ for (const [hs, he] of hunks) {
60
+ let oldStart = 0, oldLen = 0, newStart = 0, newLen = 0;
61
+ for (let p = 0; p < hs; p++) {
62
+ if (ops[p][0] !== "+") oldStart++;
63
+ if (ops[p][0] !== "-") newStart++;
64
+ }
65
+ for (let p = hs; p < he; p++) {
66
+ if (ops[p][0] !== "+") oldLen++;
67
+ if (ops[p][0] !== "-") newLen++;
68
+ }
69
+ out.push(`@@ -${oldStart + 1},${oldLen} +${newStart + 1},${newLen} @@`);
70
+ for (let p = hs; p < he; p++) {
71
+ const [tag, line] = ops[p];
72
+ const prefix = tag === "=" ? " " : tag;
73
+ out.push(prefix + line);
74
+ }
75
+ }
76
+ return out.join("\n");
77
+ }
78
+
79
+ function isHashOnlyFile(value) {
80
+ return !!(
81
+ value &&
82
+ typeof value === "object" &&
83
+ !Array.isArray(value) &&
84
+ Number.isFinite(Number(value.size)) &&
85
+ typeof value.sha256 === "string" &&
86
+ /^[a-f0-9]{64}$/i.test(value.sha256)
87
+ );
88
+ }
89
+
90
+ function formatFileSize(size) {
91
+ const bytes = Number(size) || 0;
92
+ if (bytes < 1000) return `${bytes}B`;
93
+ if (bytes < 1000 * 1000) return `${Math.round(bytes / 1000)}KB`;
94
+ return `${Math.round(bytes / 100000) / 10}MB`;
95
+ }
96
+
97
+ function shortSha256(sha256) {
98
+ const value = String(sha256 || "");
99
+ return value.length > 12 ? `${value.slice(0, 12)}...` : value;
100
+ }
101
+
102
+ function hashText(text) {
103
+ return crypto.createHash("sha256").update(String(text), "utf8").digest("hex");
104
+ }
105
+
106
+ function descriptorForText(text) {
107
+ const value = String(text);
108
+ return {
109
+ size: Buffer.byteLength(value, "utf8"),
110
+ sha256: hashText(value),
111
+ compare: "hash-only",
112
+ };
113
+ }
114
+
115
+ function formatHashOnlyFile(value) {
116
+ return `size: ${formatFileSize(value.size)} sha256: ${shortSha256(value.sha256)} [hash-only compare]`;
117
+ }
118
+
119
+ function fileValueEqual(a, b) {
120
+ if (a === b) return true;
121
+ if (a == null || b == null) return a == null && b == null;
122
+ const aHashOnly = isHashOnlyFile(a);
123
+ const bHashOnly = isHashOnlyFile(b);
124
+ if (!aHashOnly && !bHashOnly) return false;
125
+ const aDescriptor = aHashOnly ? a : descriptorForText(a);
126
+ const bDescriptor = bHashOnly ? b : descriptorForText(b);
127
+ return Number(aDescriptor.size) === Number(bDescriptor.size) &&
128
+ String(aDescriptor.sha256).toLowerCase() === String(bDescriptor.sha256).toLowerCase();
129
+ }
130
+
131
+ function valueForDiff(value, peerValue) {
132
+ if (isHashOnlyFile(value)) return formatHashOnlyFile(value);
133
+ if (value == null) return "";
134
+ if (isHashOnlyFile(peerValue)) return formatHashOnlyFile(descriptorForText(value));
135
+ return String(value);
136
+ }
137
+
138
+ function renderFileMapDiff(changes, currentFiles = {}) {
139
+ const parts = [];
140
+ const summary = { added: [], modified: [], removed: [] };
141
+ for (const rel of Object.keys(changes).sort()) {
142
+ const hasCurrent = Object.prototype.hasOwnProperty.call(currentFiles, rel);
143
+ const before = hasCurrent ? currentFiles[rel] : "";
144
+ const after = changes[rel];
145
+ if (fileValueEqual(before, after)) continue;
146
+ if (!hasCurrent) summary.added.push(rel);
147
+ else if (after == null) summary.removed.push(rel);
148
+ else summary.modified.push(rel);
149
+ const d = unifiedDiff(rel, valueForDiff(before, after), valueForDiff(after, before));
150
+ if (d) parts.push(d);
151
+ }
152
+ return { diff: parts.join("\n"), summary };
153
+ }
154
+
155
+ function shouldAutoYes(args = []) {
156
+ if (args.includes("--yes") || args.includes("-y")) return true;
157
+ if (process.env.ODAI_ASSUME_YES === "1") return true;
158
+ if (process.env.CI === "true") return true;
159
+ if (!process.stdin.isTTY) return true;
160
+ return false;
161
+ }
162
+
163
+ function promptConfirm(question, defaultYes = false) {
164
+ return new Promise((resolve) => {
165
+ if (!process.stdin.isTTY) { resolve(defaultYes); return; }
166
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
167
+ const suffix = defaultYes ? " [Y/n] " : " [y/N] ";
168
+ rl.question(question + suffix, (ans) => {
169
+ rl.close();
170
+ const a = String(ans || "").trim().toLowerCase();
171
+ if (!a) return resolve(defaultYes);
172
+ resolve(a === "y" || a === "yes");
173
+ });
174
+ });
175
+ }
176
+
177
+ async function confirmOrExit({ args = [], quiet = false, message = "Apply these changes?", defaultYes = false } = {}) {
178
+ if (quiet) return true;
179
+ if (shouldAutoYes(args)) return true;
180
+ const ok = await promptConfirm(message, defaultYes);
181
+ return ok;
182
+ }
183
+
184
+ module.exports = {
185
+ unifiedDiff,
186
+ renderFileMapDiff,
187
+ isHashOnlyFile,
188
+ formatHashOnlyFile,
189
+ shouldAutoYes,
190
+ promptConfirm,
191
+ confirmOrExit,
192
+ };
@@ -30,31 +30,85 @@ function deviceFingerprint() {
30
30
  }
31
31
 
32
32
  function registerProject(projectPath, name, stack, CONFIG_DIR, PROJECTS_FILE) {
33
+ if (!CONFIG_DIR || !PROJECTS_FILE) {
34
+ throw new Error("registerProject requires CONFIG_DIR and PROJECTS_FILE");
35
+ }
36
+
37
+ fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
38
+ let projects = [];
39
+ if (fs.existsSync(PROJECTS_FILE)) {
40
+ const parsed = JSON.parse(fs.readFileSync(PROJECTS_FILE, "utf8"));
41
+ projects = Array.isArray(parsed && parsed.projects) ? parsed.projects : [];
42
+ }
43
+
44
+ const abs = path.resolve(projectPath);
45
+ const idx = projects.findIndex(p => p.path === abs);
46
+ const entry = { path: abs, name: name || path.basename(abs), stack: stack || "?", last_seen: new Date().toISOString() };
47
+ if (idx >= 0) projects[idx] = entry;
48
+ else projects.unshift(entry);
49
+ fs.writeFileSync(PROJECTS_FILE, JSON.stringify({ projects: projects.slice(0, 50) }, null, 2));
50
+ return entry;
51
+ }
52
+
53
+ function scrubRemoteUrl(url) {
54
+ return String(url || "").replace(/^(https?:\/\/)[^@/\s]+@/, "$1");
55
+ }
56
+
57
+ function parseScalar(value) {
58
+ return String(value || "").trim().replace(/^["']|["']$/g, "");
59
+ }
60
+
61
+ function readProjectManifest(target) {
62
+ const file = path.join(target, "ai", "manifest", "project.yaml");
63
+ const result = {};
64
+ let inProject = false;
33
65
  try {
34
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
35
- let projects = [];
36
- try { projects = JSON.parse(fs.readFileSync(PROJECTS_FILE, "utf8")).projects || []; } catch {}
37
- const abs = path.resolve(projectPath);
38
- const idx = projects.findIndex(p => p.path === abs);
39
- const entry = { path: abs, name: name || path.basename(abs), stack: stack || "?", last_seen: new Date().toISOString() };
40
- if (idx >= 0) projects[idx] = entry;
41
- else projects.unshift(entry);
42
- fs.writeFileSync(PROJECTS_FILE, JSON.stringify({ projects: projects.slice(0, 50) }, null, 2));
66
+ const lines = fs.readFileSync(file, "utf8").split(/\r?\n/);
67
+ for (const raw of lines) {
68
+ const line = raw.trim();
69
+ if (!line || line.startsWith("#")) continue;
70
+ const topLevel = raw.length === raw.trimStart().length;
71
+ if (topLevel) inProject = false;
72
+ if (topLevel && line === "project:") {
73
+ inProject = true;
74
+ continue;
75
+ }
76
+ if (topLevel && line.startsWith("name:")) result.name = parseScalar(line.split(":", 2)[1]);
77
+ else if (topLevel && line.startsWith("stack:")) result.stack = parseScalar(line.split(":", 2)[1]);
78
+ else if (topLevel && line.startsWith("repo:")) result.repo = scrubRemoteUrl(parseScalar(line.split(":", 2)[1]));
79
+ else if (inProject && line.startsWith("name:") && !result.name) result.name = parseScalar(line.split(":", 2)[1]);
80
+ else if (inProject && line.startsWith("stack:") && !result.stack) result.stack = parseScalar(line.split(":", 2)[1]);
81
+ else if (inProject && line.startsWith("type:") && !result.stack) result.stack = parseScalar(line.split(":", 2)[1]);
82
+ else if (inProject && line.startsWith("repo:") && !result.repo) result.repo = scrubRemoteUrl(parseScalar(line.split(":", 2)[1]));
83
+ }
43
84
  } catch {}
85
+ return result;
86
+ }
87
+
88
+ function readDiscovery(target) {
89
+ try {
90
+ return JSON.parse(fs.readFileSync(path.join(target, "ai", "manifest", "discovery.json"), "utf8"));
91
+ } catch {
92
+ return {};
93
+ }
44
94
  }
45
95
 
46
96
  function projectIdFor(target, projectName, remoteOrigin) {
47
97
  const crypto = require("crypto");
48
- const seed = JSON.stringify({ name: projectName, origin: remoteOrigin || path.resolve(target) });
98
+ const seed = projectIdSeed(projectName, remoteOrigin || path.resolve(target));
49
99
  return "prj_" + crypto.createHash("sha256").update(seed).digest("hex").slice(0, 16);
50
100
  }
51
101
 
102
+ function projectIdSeed(projectName, origin) {
103
+ return `{"name": ${JSON.stringify(projectName)}, "origin": ${JSON.stringify(origin)}}`;
104
+ }
105
+
52
106
  function getGitRemoteOrigin(target) {
53
107
  try {
54
108
  const { execFileSync } = require("child_process");
55
- return execFileSync("git", ["config", "--get", "remote.origin.url"], {
109
+ return scrubRemoteUrl(execFileSync("git", ["config", "--get", "remote.origin.url"], {
56
110
  cwd: target, stdio: ["ignore", "pipe", "ignore"], encoding: "utf8", timeout: 5000,
57
- }).trim();
111
+ }).trim());
58
112
  } catch { return ""; }
59
113
  }
60
114
 
@@ -127,21 +181,25 @@ function collectMetadata(target) {
127
181
  }
128
182
 
129
183
  function buildProjectIdentity(target, metadata, detectedStack = "") {
130
- const projectName = inferProjectName(target, metadata.manifestContents);
184
+ const manifest = readProjectManifest(target);
185
+ const discovery = readDiscovery(target);
186
+ const projectName = manifest.name || inferProjectName(target, metadata.manifestContents);
131
187
  const remoteOrigin = getGitRemoteOrigin(target);
188
+ const originValue = remoteOrigin || manifest.repo || path.resolve(target);
132
189
  return {
133
190
  project_name: projectName,
134
- stack: detectedStack || detectStackHint(metadata.projectFiles, metadata.manifestContents),
135
- project_id: projectIdFor(target, projectName, remoteOrigin),
136
- origin: remoteOrigin ? "git" : "local",
137
- remote_origin: remoteOrigin,
191
+ stack: detectedStack || manifest.stack || discovery.stack || detectStackHint(metadata.projectFiles, metadata.manifestContents),
192
+ project_id: projectIdFor(target, projectName, originValue),
193
+ origin: remoteOrigin ? "git" : (manifest.repo ? "manifest" : "local"),
194
+ remote_origin: remoteOrigin || manifest.repo || "",
138
195
  binding_source: "npm-cli",
139
196
  };
140
197
  }
141
198
 
142
199
  module.exports = {
143
200
  MANIFEST_FILES, PROBE_DIRS,
144
- deviceFingerprint, registerProject, projectIdFor,
201
+ deviceFingerprint, registerProject, projectIdFor, projectIdSeed,
202
+ scrubRemoteUrl, readProjectManifest, readDiscovery,
145
203
  getGitRemoteOrigin, inferProjectName, detectStackHint,
146
204
  collectMetadata, buildProjectIdentity,
147
205
  };