@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.
- package/README.md +30 -5
- package/bin/0dai.js +308 -60
- package/lib/commands/audit.js +13 -0
- package/lib/commands/auth.js +404 -122
- package/lib/commands/boneyard.js +44 -0
- package/lib/commands/ci.js +329 -0
- package/lib/commands/compliance.js +20 -0
- package/lib/commands/doctor.js +79 -14
- package/lib/commands/experience.js +5 -1
- package/lib/commands/feedback.js +92 -5
- package/lib/commands/gh.js +506 -0
- package/lib/commands/graph.js +78 -10
- package/lib/commands/heatmap.js +17 -0
- package/lib/commands/import_claude_code_agents.js +367 -0
- package/lib/commands/init.js +553 -53
- package/lib/commands/loop.js +108 -0
- package/lib/commands/mcp.js +410 -0
- package/lib/commands/models.js +42 -12
- package/lib/commands/paste.js +114 -0
- package/lib/commands/persona-simulate.js +19 -0
- package/lib/commands/play.js +173 -0
- package/lib/commands/provider.js +87 -0
- package/lib/commands/quota.js +76 -0
- package/lib/commands/receipt.js +53 -0
- package/lib/commands/report.js +29 -2
- package/lib/commands/run.js +44 -4
- package/lib/commands/runner.js +527 -0
- package/lib/commands/session.js +1 -7
- package/lib/commands/ssh.js +416 -0
- package/lib/commands/standup.js +40 -0
- package/lib/commands/status.js +131 -36
- package/lib/commands/swarm.js +97 -4
- package/lib/commands/tui.js +117 -0
- package/lib/commands/usage.js +87 -0
- package/lib/commands/vault.js +246 -0
- package/lib/commands/workspace.js +1 -0
- package/lib/onboarding.js +30 -10
- package/lib/shared.js +153 -96
- package/lib/tui/index.mjs +34994 -0
- package/lib/utils/auth.js +1 -0
- package/lib/utils/canonical-counts.js +54 -0
- package/lib/utils/diff-preview.js +192 -0
- package/lib/utils/identity.js +76 -18
- package/lib/utils/mcp-auth.js +607 -0
- package/lib/utils/model_ratings.js +77 -0
- package/lib/utils/plan.js +37 -2
- package/lib/vault/cipher.js +125 -0
- package/lib/vault/identity.js +122 -0
- package/lib/vault/index.js +184 -0
- package/lib/vault/storage.js +84 -0
- package/lib/wizard.js +19 -12
- package/package.json +13 -5
- 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
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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: "
|
|
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
|
|
115
|
-
|
|
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
|
-
|
|
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",
|
|
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
|
|
228
|
-
if (existing.includes("managed: false")) { unchanged++; continue; }
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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,
|
|
412
|
+
mergeSettingsJson, writeFiles, findRepoScript,
|
|
356
413
|
// Version
|
|
357
414
|
checkVersion,
|
|
358
415
|
// Re-exports for convenience
|