@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.
- package/README.md +30 -5
- package/bin/0dai.js +289 -60
- package/lib/commands/audit.js +13 -0
- package/lib/commands/auth.js +341 -98
- 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 +20 -1
- 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 +440 -28
- package/lib/commands/loop.js +108 -0
- package/lib/commands/mcp.js +410 -0
- package/lib/commands/models.js +27 -3
- package/lib/commands/paste.js +114 -0
- package/lib/commands/play.js +173 -0
- package/lib/commands/provider.js +69 -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/standup.js +40 -0
- package/lib/commands/status.js +26 -1
- package/lib/commands/swarm.js +97 -4
- package/lib/commands/tui.js +81 -13
- package/lib/commands/usage.js +87 -0
- package/lib/commands/vault.js +246 -0
- package/lib/onboarding.js +9 -3
- package/lib/shared.js +29 -14
- package/lib/tui/index.mjs +571 -187
- 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/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 +2 -2
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 0dai import claude-code-agents
|
|
5
|
+
*
|
|
6
|
+
* Reads Claude Code subagent files (markdown with YAML frontmatter) from
|
|
7
|
+
* `.claude/agents/` and converts each into a 0dai persona YAML in
|
|
8
|
+
* `ai/personas/`. The resulting persona records `imported_from: "claude-code"`
|
|
9
|
+
* so `0dai sync` can write the file back into `.claude/agents/` for round-trip
|
|
10
|
+
* fidelity.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* 0dai import claude-code-agents [--source DIR] [--target DIR] [--dry-run]
|
|
14
|
+
*
|
|
15
|
+
* No external YAML dependency — Claude Code subagents use a constrained
|
|
16
|
+
* frontmatter shape (flat key: value pairs) which we parse with a small
|
|
17
|
+
* line-oriented reader.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const fs = require("node:fs");
|
|
21
|
+
const path = require("node:path");
|
|
22
|
+
const os = require("node:os");
|
|
23
|
+
|
|
24
|
+
const T = process.stdout.isTTY ? "\x1b[38;2;45;212;168m" : "";
|
|
25
|
+
const R = process.stdout.isTTY ? "\x1b[0m" : "";
|
|
26
|
+
const D = process.stdout.isTTY ? "\x1b[2m" : "";
|
|
27
|
+
const E = process.stdout.isTTY ? "\x1b[31m" : "";
|
|
28
|
+
const W = process.stdout.isTTY ? "\x1b[33m" : "";
|
|
29
|
+
const log = (msg) => console.log(`${T}[0dai]${R} ${msg}`);
|
|
30
|
+
|
|
31
|
+
function parseFrontmatter(text) {
|
|
32
|
+
if (typeof text !== "string") return null;
|
|
33
|
+
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
|
34
|
+
if (lines[0].trim() !== "---") return null;
|
|
35
|
+
|
|
36
|
+
let endIdx = -1;
|
|
37
|
+
for (let i = 1; i < lines.length; i++) {
|
|
38
|
+
if (lines[i].trim() === "---") { endIdx = i; break; }
|
|
39
|
+
}
|
|
40
|
+
if (endIdx === -1) return null;
|
|
41
|
+
|
|
42
|
+
const frontmatter = {};
|
|
43
|
+
for (let i = 1; i < endIdx; i++) {
|
|
44
|
+
const raw = lines[i];
|
|
45
|
+
if (!raw.trim() || raw.trim().startsWith("#")) continue;
|
|
46
|
+
const m = raw.match(/^([A-Za-z_][A-Za-z0-9_-]*)\s*:\s*(.*)$/);
|
|
47
|
+
if (!m) continue;
|
|
48
|
+
const key = m[1];
|
|
49
|
+
let value = m[2].trim();
|
|
50
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
51
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
52
|
+
value = value.slice(1, -1);
|
|
53
|
+
}
|
|
54
|
+
if (key === "tools") {
|
|
55
|
+
frontmatter[key] = value
|
|
56
|
+
? value.split(",").map((s) => s.trim()).filter(Boolean)
|
|
57
|
+
: [];
|
|
58
|
+
} else {
|
|
59
|
+
frontmatter[key] = value;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const body = lines.slice(endIdx + 1).join("\n").replace(/^\n+/, "").replace(/\s+$/, "");
|
|
64
|
+
return { frontmatter, body };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function serializePersona(persona) {
|
|
68
|
+
const lines = [];
|
|
69
|
+
const ordered = [
|
|
70
|
+
"managed", "name", "display_name", "description",
|
|
71
|
+
"imported_from", "imported_at", "source_file", "model", "tools",
|
|
72
|
+
];
|
|
73
|
+
for (const key of ordered) {
|
|
74
|
+
if (!(key in persona)) continue;
|
|
75
|
+
const v = persona[key];
|
|
76
|
+
if (Array.isArray(v)) {
|
|
77
|
+
if (v.length === 0) {
|
|
78
|
+
lines.push(`${key}: []`);
|
|
79
|
+
} else {
|
|
80
|
+
const inline = v.map((item) => yamlScalar(item)).join(", ");
|
|
81
|
+
lines.push(`${key}: [${inline}]`);
|
|
82
|
+
}
|
|
83
|
+
} else if (typeof v === "boolean" || typeof v === "number") {
|
|
84
|
+
lines.push(`${key}: ${v}`);
|
|
85
|
+
} else {
|
|
86
|
+
lines.push(`${key}: ${yamlScalar(v)}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (persona.system_prompt_addition) {
|
|
91
|
+
lines.push("");
|
|
92
|
+
lines.push("system_prompt_addition: |");
|
|
93
|
+
const body = String(persona.system_prompt_addition).replace(/\s+$/, "");
|
|
94
|
+
for (const ln of body.split("\n")) {
|
|
95
|
+
lines.push(ln ? ` ${ln}` : "");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return lines.join("\n") + "\n";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function yamlScalar(value) {
|
|
103
|
+
const s = String(value);
|
|
104
|
+
// Quote only when YAML would mis-parse: leading sigils that introduce
|
|
105
|
+
// flow/anchor/tag syntax, embedded `: ` (mapping), embedded `#` (comment),
|
|
106
|
+
// wrapping whitespace, or empty string. Hyphens mid-token are fine.
|
|
107
|
+
const needsQuote =
|
|
108
|
+
s === "" ||
|
|
109
|
+
/^\s|\s$/.test(s) ||
|
|
110
|
+
/^[\-?:&*!|>'"%@`{[]/.test(s) ||
|
|
111
|
+
/:\s|\s#/.test(s) ||
|
|
112
|
+
/[\n\r\t]/.test(s);
|
|
113
|
+
if (needsQuote) {
|
|
114
|
+
return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
115
|
+
}
|
|
116
|
+
return s;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function parsePersonaYaml(text) {
|
|
120
|
+
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
|
121
|
+
const out = {};
|
|
122
|
+
let i = 0;
|
|
123
|
+
while (i < lines.length) {
|
|
124
|
+
const raw = lines[i];
|
|
125
|
+
const trimmed = raw.trim();
|
|
126
|
+
if (!trimmed || trimmed.startsWith("#")) { i++; continue; }
|
|
127
|
+
const blockMatch = raw.match(/^([A-Za-z_][A-Za-z0-9_-]*)\s*:\s*\|\s*$/);
|
|
128
|
+
if (blockMatch) {
|
|
129
|
+
const key = blockMatch[1];
|
|
130
|
+
i++;
|
|
131
|
+
const body = [];
|
|
132
|
+
let baseIndent = null;
|
|
133
|
+
while (i < lines.length) {
|
|
134
|
+
const cur = lines[i];
|
|
135
|
+
if (cur.trim() === "") { body.push(""); i++; continue; }
|
|
136
|
+
const indentMatch = cur.match(/^(\s+)/);
|
|
137
|
+
if (!indentMatch) break;
|
|
138
|
+
if (baseIndent === null) baseIndent = indentMatch[1].length;
|
|
139
|
+
if (indentMatch[1].length < baseIndent) break;
|
|
140
|
+
body.push(cur.slice(baseIndent));
|
|
141
|
+
i++;
|
|
142
|
+
}
|
|
143
|
+
out[key] = body.join("\n").replace(/\s+$/, "");
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const kv = raw.match(/^([A-Za-z_][A-Za-z0-9_-]*)\s*:\s*(.*)$/);
|
|
147
|
+
if (kv) {
|
|
148
|
+
const key = kv[1];
|
|
149
|
+
let value = kv[2].trim();
|
|
150
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
151
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
152
|
+
value = value.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
|
153
|
+
}
|
|
154
|
+
if (value.startsWith("[") && value.endsWith("]")) {
|
|
155
|
+
const inner = value.slice(1, -1).trim();
|
|
156
|
+
out[key] = inner ? inner.split(",").map((s) => {
|
|
157
|
+
let v = s.trim();
|
|
158
|
+
if ((v.startsWith('"') && v.endsWith('"')) ||
|
|
159
|
+
(v.startsWith("'") && v.endsWith("'"))) v = v.slice(1, -1);
|
|
160
|
+
return v;
|
|
161
|
+
}) : [];
|
|
162
|
+
} else if (value === "true" || value === "false") {
|
|
163
|
+
out[key] = value === "true";
|
|
164
|
+
} else {
|
|
165
|
+
out[key] = value;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
i++;
|
|
169
|
+
}
|
|
170
|
+
return out;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function agentToPersona(parsed, sourceRelPath, opts = {}) {
|
|
174
|
+
const fm = parsed.frontmatter || {};
|
|
175
|
+
const name = (fm.name || "").trim();
|
|
176
|
+
if (!name) {
|
|
177
|
+
return { error: "missing 'name' in frontmatter" };
|
|
178
|
+
}
|
|
179
|
+
const persona = {
|
|
180
|
+
managed: false,
|
|
181
|
+
name,
|
|
182
|
+
display_name: fm.display_name || titleCase(name),
|
|
183
|
+
description: fm.description || "",
|
|
184
|
+
imported_from: "claude-code",
|
|
185
|
+
imported_at: opts.now || new Date().toISOString(),
|
|
186
|
+
source_file: sourceRelPath,
|
|
187
|
+
};
|
|
188
|
+
if (fm.model) persona.model = fm.model;
|
|
189
|
+
if (Array.isArray(fm.tools) && fm.tools.length) persona.tools = fm.tools;
|
|
190
|
+
if (parsed.body) persona.system_prompt_addition = parsed.body;
|
|
191
|
+
return { persona };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function personaToClaudeAgent(persona) {
|
|
195
|
+
const fm = [];
|
|
196
|
+
fm.push(`name: ${persona.name}`);
|
|
197
|
+
if (persona.description) fm.push(`description: ${yamlScalar(persona.description)}`);
|
|
198
|
+
if (persona.model) fm.push(`model: ${persona.model}`);
|
|
199
|
+
if (Array.isArray(persona.tools) && persona.tools.length) {
|
|
200
|
+
fm.push(`tools: ${persona.tools.join(", ")}`);
|
|
201
|
+
}
|
|
202
|
+
const body = persona.system_prompt_addition || "";
|
|
203
|
+
return `---\n${fm.join("\n")}\n---\n\n${body}\n`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function titleCase(slug) {
|
|
207
|
+
return slug
|
|
208
|
+
.split(/[-_]+/)
|
|
209
|
+
.map((s) => s ? s[0].toUpperCase() + s.slice(1) : "")
|
|
210
|
+
.join(" ");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function findSourceDir(target, explicit) {
|
|
214
|
+
if (explicit) return explicit;
|
|
215
|
+
const repoLocal = path.join(target, ".claude", "agents");
|
|
216
|
+
if (fs.existsSync(repoLocal) && fs.statSync(repoLocal).isDirectory()) {
|
|
217
|
+
return repoLocal;
|
|
218
|
+
}
|
|
219
|
+
const home = path.join(os.homedir(), ".claude", "agents");
|
|
220
|
+
if (fs.existsSync(home) && fs.statSync(home).isDirectory()) {
|
|
221
|
+
return home;
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function cmdImportClaudeCodeAgents(target, args = []) {
|
|
227
|
+
const opts = parseArgs(args);
|
|
228
|
+
const sourceDir = findSourceDir(target, opts.source);
|
|
229
|
+
const targetDir = opts.target || path.join(target, "ai", "personas");
|
|
230
|
+
|
|
231
|
+
if (!sourceDir) {
|
|
232
|
+
log(`${E}error:${R} no claude-code agents directory found`);
|
|
233
|
+
console.log(` ${D}looked at: ${path.join(target, ".claude", "agents")}${R}`);
|
|
234
|
+
console.log(` ${D}and: ${path.join(os.homedir(), ".claude", "agents")}${R}`);
|
|
235
|
+
console.log(` ${D}use --source DIR to point at a non-default location${R}`);
|
|
236
|
+
process.exitCode = 1;
|
|
237
|
+
return { ok: false, error: "no-source-dir", imported: [], skipped: [] };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
let entries;
|
|
241
|
+
try {
|
|
242
|
+
entries = fs.readdirSync(sourceDir);
|
|
243
|
+
} catch (err) {
|
|
244
|
+
log(`${E}error:${R} cannot read ${sourceDir}: ${err.message}`);
|
|
245
|
+
process.exitCode = 1;
|
|
246
|
+
return { ok: false, error: "read-failed", imported: [], skipped: [] };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const candidates = entries.filter((f) => /\.(md|yaml|yml)$/i.test(f));
|
|
250
|
+
if (candidates.length === 0) {
|
|
251
|
+
log(`no claude-code agents found in ${sourceDir}`);
|
|
252
|
+
return { ok: true, imported: [], skipped: [], sourceDir, targetDir };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const imported = [];
|
|
256
|
+
const skipped = [];
|
|
257
|
+
const seenNames = new Set();
|
|
258
|
+
|
|
259
|
+
for (const file of candidates.sort()) {
|
|
260
|
+
const full = path.join(sourceDir, file);
|
|
261
|
+
let raw;
|
|
262
|
+
try {
|
|
263
|
+
raw = fs.readFileSync(full, "utf8");
|
|
264
|
+
} catch (err) {
|
|
265
|
+
skipped.push({ file, reason: `read-failed: ${err.message}` });
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
const parsed = parseFrontmatter(raw);
|
|
269
|
+
if (!parsed) {
|
|
270
|
+
skipped.push({ file, reason: "invalid YAML frontmatter" });
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
const sourceRel = path.relative(target, full).replace(/\\/g, "/");
|
|
274
|
+
const result = agentToPersona(parsed, sourceRel, { now: opts.now });
|
|
275
|
+
if (result.error) {
|
|
276
|
+
skipped.push({ file, reason: result.error });
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
const persona = result.persona;
|
|
280
|
+
if (seenNames.has(persona.name)) {
|
|
281
|
+
skipped.push({ file, reason: `duplicate name '${persona.name}'` });
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
seenNames.add(persona.name);
|
|
285
|
+
|
|
286
|
+
const outPath = path.join(targetDir, `${persona.name}.yaml`);
|
|
287
|
+
const yaml = serializePersona(persona);
|
|
288
|
+
if (!opts.dryRun) {
|
|
289
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
290
|
+
fs.writeFileSync(outPath, yaml, "utf8");
|
|
291
|
+
}
|
|
292
|
+
imported.push({ file, name: persona.name, outPath });
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (!opts.quiet) {
|
|
296
|
+
if (opts.dryRun) log(`${D}dry-run:${R} would import ${imported.length} persona(s) from ${sourceDir}`);
|
|
297
|
+
else log(`imported ${imported.length} persona(s) from ${sourceDir}`);
|
|
298
|
+
for (const it of imported) {
|
|
299
|
+
const rel = path.relative(target, it.outPath).replace(/\\/g, "/");
|
|
300
|
+
console.log(` + ${it.name} ${D}-> ${rel}${R}`);
|
|
301
|
+
}
|
|
302
|
+
if (skipped.length) {
|
|
303
|
+
console.log(` ${W}skipped ${skipped.length}:${R}`);
|
|
304
|
+
for (const s of skipped) console.log(` - ${s.file}: ${s.reason}`);
|
|
305
|
+
}
|
|
306
|
+
console.log(` ${D}total: ${imported.length} imported, ${skipped.length} skipped${R}`);
|
|
307
|
+
if (!opts.dryRun && imported.length) {
|
|
308
|
+
console.log(` ${D}round-trip: 0dai sync will write back to .claude/agents/${R}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return { ok: true, imported, skipped, sourceDir, targetDir };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function parseArgs(args) {
|
|
316
|
+
const out = { dryRun: false, source: null, target: null, quiet: false };
|
|
317
|
+
for (let i = 0; i < args.length; i++) {
|
|
318
|
+
const a = args[i];
|
|
319
|
+
if (a === "--dry-run") out.dryRun = true;
|
|
320
|
+
else if (a === "--quiet" || a === "-q") out.quiet = true;
|
|
321
|
+
else if (a === "--source" || a === "--from") out.source = args[++i];
|
|
322
|
+
else if (a === "--target" || a === "--to") out.target = args[++i];
|
|
323
|
+
else if (a.startsWith("--source=")) out.source = a.slice("--source=".length);
|
|
324
|
+
else if (a.startsWith("--target=")) out.target = a.slice("--target=".length);
|
|
325
|
+
}
|
|
326
|
+
return out;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function syncImportedClaudeCodeAgents(target, opts = {}) {
|
|
330
|
+
const personaDir = path.join(target, "ai", "personas");
|
|
331
|
+
const agentDir = path.join(target, ".claude", "agents");
|
|
332
|
+
if (!fs.existsSync(personaDir)) return { written: [], skipped: [] };
|
|
333
|
+
|
|
334
|
+
const written = [];
|
|
335
|
+
const skipped = [];
|
|
336
|
+
for (const file of fs.readdirSync(personaDir)) {
|
|
337
|
+
if (!/\.ya?ml$/i.test(file)) continue;
|
|
338
|
+
const full = path.join(personaDir, file);
|
|
339
|
+
let raw;
|
|
340
|
+
try { raw = fs.readFileSync(full, "utf8"); } catch { continue; }
|
|
341
|
+
const persona = parsePersonaYaml(raw);
|
|
342
|
+
if (persona.imported_from !== "claude-code") continue;
|
|
343
|
+
if (!persona.name) {
|
|
344
|
+
skipped.push({ file, reason: "missing name" });
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
const outPath = path.join(agentDir, `${persona.name}.md`);
|
|
348
|
+
const md = personaToClaudeAgent(persona);
|
|
349
|
+
if (!opts.dryRun) {
|
|
350
|
+
fs.mkdirSync(agentDir, { recursive: true });
|
|
351
|
+
fs.writeFileSync(outPath, md, "utf8");
|
|
352
|
+
}
|
|
353
|
+
written.push({ name: persona.name, outPath });
|
|
354
|
+
}
|
|
355
|
+
return { written, skipped };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
module.exports = {
|
|
359
|
+
cmdImportClaudeCodeAgents,
|
|
360
|
+
parseFrontmatter,
|
|
361
|
+
serializePersona,
|
|
362
|
+
parsePersonaYaml,
|
|
363
|
+
personaToClaudeAgent,
|
|
364
|
+
agentToPersona,
|
|
365
|
+
syncImportedClaudeCodeAgents,
|
|
366
|
+
findSourceDir,
|
|
367
|
+
};
|