@0dai-dev/cli 1.2.1

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 (3) hide show
  1. package/README.md +39 -0
  2. package/bin/0dai.js +274 -0
  3. package/package.json +24 -0
package/README.md ADDED
@@ -0,0 +1,39 @@
1
+ # 0dai
2
+
3
+ One config layer for 5 AI agent CLIs — Claude Code, Codex, OpenCode, Gemini, Aider.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g @0dai-dev/cli
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```bash
14
+ cd your-project
15
+ 0dai init # detect stack, generate ai/ layer + native configs
16
+ 0dai sync # update after changes
17
+ 0dai detect # show detected stack
18
+ 0dai doctor # check health
19
+ 0dai status # maturity, swarm tasks, session
20
+ ```
21
+
22
+ ## What it does
23
+
24
+ `0dai init` sends your project metadata (file names + package.json) to the API and generates:
25
+
26
+ - `ai/` — manifests, personas, skills, playbooks, delegation policy
27
+ - `.claude/` — settings, agents, hooks, rules
28
+ - `.codex/` — config, agents
29
+ - `.gemini/` — settings, agents
30
+ - `.aider/` — config, agents
31
+ - `AGENTS.md`, `.mcp.json`
32
+
33
+ Your source code is never sent. Only file names and package manifests.
34
+
35
+ ## Links
36
+
37
+ - Website: https://0dai.dev
38
+ - Docs: https://docs.0dai.dev
39
+ - API: https://api.0dai.dev
package/bin/0dai.js ADDED
@@ -0,0 +1,274 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const https = require("https");
5
+ const http = require("http");
6
+ const fs = require("fs");
7
+ const path = require("path");
8
+ const os = require("os");
9
+
10
+ const VERSION = "1.2.1";
11
+ const API_URL = process.env.ODAI_API_URL || "https://api.0dai.dev";
12
+ const CONFIG_DIR = path.join(os.homedir(), ".0dai");
13
+ const AUTH_FILE = path.join(CONFIG_DIR, "auth.json");
14
+
15
+ const MANIFEST_FILES = [
16
+ "package.json", "go.mod", "pyproject.toml", "requirements.txt",
17
+ "pubspec.yaml", "Cargo.toml", "next.config.js", "next.config.mjs",
18
+ "next.config.ts", "tsconfig.json", "Makefile", "docker-compose.yml",
19
+ "Dockerfile", "pom.xml", "build.gradle",
20
+ ];
21
+
22
+ const PROBE_DIRS = [
23
+ "src", "lib", "app", "apps", "packages", "services",
24
+ "cmd", "internal", "web", "frontend", "backend",
25
+ "tests", "test", "spec", "__tests__",
26
+ "infra", "deploy", "docker", ".github",
27
+ "android", "ios", "linux", "macos", "windows",
28
+ ];
29
+
30
+ function apiCall(endpoint, data) {
31
+ return new Promise((resolve, reject) => {
32
+ const url = new URL(endpoint, API_URL);
33
+ const mod = url.protocol === "https:" ? https : http;
34
+ const body = data ? JSON.stringify(data) : null;
35
+ const headers = { "Content-Type": "application/json" };
36
+
37
+ try {
38
+ const auth = JSON.parse(fs.readFileSync(AUTH_FILE, "utf8"));
39
+ const token = auth.api_key || auth.token;
40
+ if (token) headers["Authorization"] = `Bearer ${token}`;
41
+ } catch {}
42
+
43
+ const opts = {
44
+ hostname: url.hostname,
45
+ port: url.port || (url.protocol === "https:" ? 443 : 80),
46
+ path: url.pathname,
47
+ method: body ? "POST" : "GET",
48
+ headers,
49
+ timeout: 30000,
50
+ };
51
+ if (body) opts.headers["Content-Length"] = Buffer.byteLength(body);
52
+
53
+ const req = mod.request(opts, (res) => {
54
+ let chunks = [];
55
+ res.on("data", (c) => chunks.push(c));
56
+ res.on("end", () => {
57
+ try {
58
+ resolve(JSON.parse(Buffer.concat(chunks).toString()));
59
+ } catch {
60
+ resolve({ error: `HTTP ${res.statusCode}` });
61
+ }
62
+ });
63
+ });
64
+ req.on("error", (e) => resolve({ error: `${e.message}. Is ${API_URL} reachable?` }));
65
+ req.on("timeout", () => { req.destroy(); resolve({ error: "timeout" }); });
66
+ if (body) req.write(body);
67
+ req.end();
68
+ });
69
+ }
70
+
71
+ function collectMetadata(target) {
72
+ const projectFiles = [];
73
+ const fileContents = {};
74
+
75
+ for (const d of PROBE_DIRS) {
76
+ try { if (fs.statSync(path.join(target, d)).isDirectory()) projectFiles.push(d + "/"); } catch {}
77
+ }
78
+
79
+ for (const f of MANIFEST_FILES) {
80
+ const p = path.join(target, f);
81
+ try {
82
+ const stat = fs.statSync(p);
83
+ if (stat.isFile()) {
84
+ projectFiles.push(f);
85
+ const content = fs.readFileSync(p, "utf8");
86
+ if (content.length < 10000) fileContents[f] = content;
87
+ }
88
+ } catch {}
89
+ }
90
+
91
+ const clis = [];
92
+ for (const cli of ["claude", "codex", "opencode", "gemini", "aider"]) {
93
+ try {
94
+ const { execSync } = require("child_process");
95
+ execSync(`which ${cli}`, { stdio: "ignore" });
96
+ clis.push(cli);
97
+ } catch {}
98
+ }
99
+
100
+ return { projectFiles, fileContents, clis };
101
+ }
102
+
103
+ function writeFiles(target, files) {
104
+ let created = 0, updated = 0, unchanged = 0;
105
+ for (const [rel, content] of Object.entries(files)) {
106
+ const p = path.join(target, rel);
107
+ fs.mkdirSync(path.dirname(p), { recursive: true });
108
+ try {
109
+ if (fs.existsSync(p) && fs.readFileSync(p, "utf8") === content) {
110
+ unchanged++;
111
+ continue;
112
+ }
113
+ if (fs.existsSync(p)) updated++;
114
+ else created++;
115
+ } catch { created++; }
116
+ fs.writeFileSync(p, content, "utf8");
117
+ }
118
+ console.log(`[0dai] ${created} created, ${updated} updated, ${unchanged} unchanged`);
119
+ return created + updated;
120
+ }
121
+
122
+ async function cmdInit(target) {
123
+ if (fs.existsSync(path.join(target, "ai", "VERSION"))) {
124
+ const v = fs.readFileSync(path.join(target, "ai", "VERSION"), "utf8").trim();
125
+ console.log(`[0dai] ai/ layer already exists (v${v}). Run '0dai sync' to update.`);
126
+ return;
127
+ }
128
+
129
+ console.log("[0dai] collecting project metadata...");
130
+ const { projectFiles, fileContents, clis } = collectMetadata(target);
131
+
132
+ console.log(`[0dai] sending to API (${projectFiles.length} files, ${clis.length} CLIs)...`);
133
+ const result = await apiCall("/v1/init", {
134
+ project_files: projectFiles,
135
+ file_contents: fileContents,
136
+ available_clis: clis,
137
+ });
138
+
139
+ if (result.error) { console.error(`[0dai] error: ${result.error}`); process.exit(1); }
140
+
141
+ console.log(`[0dai] detected: ${result.stack || "?"}`);
142
+ writeFiles(target, result.files || {});
143
+
144
+ // Add to .gitignore
145
+ const gi = path.join(target, ".gitignore");
146
+ try {
147
+ const text = fs.existsSync(gi) ? fs.readFileSync(gi, "utf8") : "";
148
+ if (!text.includes(".0dai")) fs.appendFileSync(gi, "\n.0dai/\n");
149
+ } catch {}
150
+
151
+ console.log(`[0dai] initialized (${result.file_count || "?"} files)`);
152
+ console.log(" skills: /build /review /status /feedback /bugfix /delegate");
153
+ }
154
+
155
+ async function cmdSync(target) {
156
+ const { projectFiles, fileContents, clis } = collectMetadata(target);
157
+
158
+ let version = "unknown", stack = "generic", agents = [];
159
+ try { version = fs.readFileSync(path.join(target, "ai", "VERSION"), "utf8").trim(); } catch {}
160
+ try {
161
+ const d = JSON.parse(fs.readFileSync(path.join(target, "ai", "manifest", "discovery.json"), "utf8"));
162
+ stack = d.stack || "generic";
163
+ agents = d.selected_agents || [];
164
+ } catch {}
165
+
166
+ // Collect current ai/ files
167
+ const currentFiles = {};
168
+ const aiDir = path.join(target, "ai");
169
+ if (fs.existsSync(aiDir)) {
170
+ const walk = (dir) => {
171
+ for (const f of fs.readdirSync(dir, { withFileTypes: true })) {
172
+ const p = path.join(dir, f.name);
173
+ if (f.isDirectory()) walk(p);
174
+ else {
175
+ try {
176
+ const stat = fs.statSync(p);
177
+ if (stat.size < 10000) currentFiles[path.relative(target, p)] = fs.readFileSync(p, "utf8");
178
+ } catch {}
179
+ }
180
+ }
181
+ };
182
+ walk(aiDir);
183
+ }
184
+
185
+ const result = await apiCall("/v1/sync", {
186
+ ai_version: version, stack, agents: agents.length ? agents : clis,
187
+ current_files: currentFiles, file_contents: fileContents,
188
+ });
189
+
190
+ if (result.error) { console.error(`[0dai] error: ${result.error}`); process.exit(1); }
191
+
192
+ const updated = result.files_updated || {};
193
+ if (Object.keys(updated).length) writeFiles(target, updated);
194
+ else console.log("[0dai] already up to date");
195
+ }
196
+
197
+ async function cmdDetect(target) {
198
+ const { projectFiles } = collectMetadata(target);
199
+ const result = await apiCall("/v1/detect", { files: projectFiles });
200
+ if (result.error) { console.error(`[0dai] error: ${result.error}`); return; }
201
+ console.log(`stack: ${result.stack || "?"}`);
202
+ console.log(`clis: ${(result.available_clis || []).join(",")}`);
203
+ }
204
+
205
+ function cmdDoctor(target) {
206
+ const ai = path.join(target, "ai");
207
+ if (!fs.existsSync(ai)) { console.log("[0dai] no ai/ layer. Run '0dai init' first."); return; }
208
+ let v = "?";
209
+ try { v = fs.readFileSync(path.join(ai, "VERSION"), "utf8").trim(); } catch {}
210
+ const checks = {
211
+ "ai/VERSION": fs.existsSync(path.join(ai, "VERSION")),
212
+ "ai/manifest/project.yaml": fs.existsSync(path.join(ai, "manifest", "project.yaml")),
213
+ "ai/manifest/commands.yaml": fs.existsSync(path.join(ai, "manifest", "commands.yaml")),
214
+ "ai/manifest/discovery.json": fs.existsSync(path.join(ai, "manifest", "discovery.json")),
215
+ ".claude/settings.json": fs.existsSync(path.join(target, ".claude", "settings.json")),
216
+ "AGENTS.md": fs.existsSync(path.join(target, "AGENTS.md")),
217
+ };
218
+ const ok = Object.values(checks).every(Boolean);
219
+ console.log(`[0dai] v${v} — ${ok ? "healthy" : "issues found"}`);
220
+ for (const [name, exists] of Object.entries(checks)) console.log(` ${exists ? "ok" : "MISSING"}: ${name}`);
221
+ }
222
+
223
+ function cmdStatus(target) {
224
+ const ai = path.join(target, "ai");
225
+ let v = "?", stack = "?";
226
+ try { v = fs.readFileSync(path.join(ai, "VERSION"), "utf8").trim(); } catch {}
227
+ try { stack = JSON.parse(fs.readFileSync(path.join(ai, "manifest", "discovery.json"), "utf8")).stack || "?"; } catch {}
228
+ console.log(`[0dai] v${v} | stack: ${stack}`);
229
+
230
+ const count = (dir) => { try { return fs.readdirSync(dir).filter(f => f.endsWith(".json")).length; } catch { return 0; } };
231
+ const q = count(path.join(ai, "swarm", "queue"));
232
+ const a = count(path.join(ai, "swarm", "active"));
233
+ const d = count(path.join(ai, "swarm", "done"));
234
+ if (q || a || d) console.log(` swarm: ${q} queued, ${a} active, ${d} done`);
235
+
236
+ try {
237
+ const s = JSON.parse(fs.readFileSync(path.join(ai, "sessions", "active.json"), "utf8"));
238
+ console.log(` session: ${(s.task || {}).goal || "?"} (agent: ${s.current_agent || "?"})`);
239
+ } catch {}
240
+ }
241
+
242
+ async function main() {
243
+ const args = process.argv.slice(2);
244
+ let target = process.cwd();
245
+ const ti = args.indexOf("--target");
246
+ if (ti >= 0 && args[ti + 1]) { target = path.resolve(args[ti + 1]); args.splice(ti, 2); }
247
+
248
+ const cmd = args[0] || "help";
249
+
250
+ switch (cmd) {
251
+ case "init": await cmdInit(target); break;
252
+ case "sync": await cmdSync(target); break;
253
+ case "detect": await cmdDetect(target); break;
254
+ case "doctor": cmdDoctor(target); break;
255
+ case "status": cmdStatus(target); break;
256
+ case "--version": console.log(`0dai ${VERSION}`); break;
257
+ case "help": case "--help": case "-h":
258
+ console.log(`0dai v${VERSION} — One config for 5 AI agent CLIs\n`);
259
+ console.log("Commands:");
260
+ console.log(" init Initialize ai/ layer (via API)");
261
+ console.log(" sync Update ai/ layer (via API)");
262
+ console.log(" detect Show detected stack");
263
+ console.log(" doctor Check health");
264
+ console.log(" status Show maturity, swarm, session");
265
+ console.log(" --version\n");
266
+ console.log("https://0dai.dev");
267
+ break;
268
+ default:
269
+ console.error(`[0dai] unknown command: ${cmd}. Run '0dai --help'`);
270
+ process.exit(1);
271
+ }
272
+ }
273
+
274
+ main().catch((e) => { console.error(`[0dai] ${e.message}`); process.exit(1); });
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@0dai-dev/cli",
3
+ "version": "1.2.1",
4
+ "description": "One config layer for 5 AI agent CLIs — Claude Code, Codex, OpenCode, Gemini, Aider",
5
+ "bin": {
6
+ "0dai": "./bin/0dai.js"
7
+ },
8
+ "keywords": ["ai", "agents", "claude", "codex", "gemini", "aider", "developer-tools", "mcp"],
9
+ "author": "0dai-dev <dev@0dai.dev>",
10
+ "license": "MIT",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/0dai-dev/0dai"
14
+ },
15
+ "homepage": "https://0dai.dev",
16
+ "engines": {
17
+ "node": ">=16"
18
+ },
19
+ "files": [
20
+ "bin/",
21
+ "lib/",
22
+ "README.md"
23
+ ]
24
+ }