@dujavi/ai-md 0.4.0 → 0.5.0

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 CHANGED
@@ -1,61 +1,133 @@
1
- # ai-md
1
+ # @dujavi/ai-md
2
2
 
3
3
  Public **AXI-shaped** CLI for a **private** personal AI config directory (`~/.ai-md`).
4
4
 
5
+ The npm package ships tooling only — no personal skills, rules, or secrets. Your content lives in a private git repo you point at with `--remote` (or machine config). Default reads are TOON + `help[]`; use `--json` when you need JSON.
6
+
7
+ ![Architecture](docs/architecture.png)
8
+
9
+ ## What it manages
10
+
5
11
  | Layer | Path | Purpose |
6
12
  |-------|------|---------|
7
- | **System** | `skills/`, `rules/` | Global base — linked to `~/.cursor/{skills,rules}` |
13
+ | **System** | `skills/`, `rules/` | Global base — linked into agent harnesses |
8
14
  | **Templates** | `templates/<type>/` | Project-type starters (`base`, later `forms`, …) |
9
15
  | **Projects** | `projects/<name>/` | Per-app overlays (repo `.cursor` → here) |
16
+ | **Scripts** | `scripts/<name>` | Private machine scripts via `ai-md script` / `setup --script` |
10
17
 
11
- No personal content ships in this package. Reads default to TOON + `help[]` (`--json` available).
18
+ ## Supported harnesses
12
19
 
13
- ## New machine
20
+ Skill (and for Cursor, rules) link targets via `--agents` (comma-separated):
21
+
22
+ | Harness | Skills link | Notes |
23
+ |---------|-------------|--------|
24
+ | `cursor` | `~/.cursor/skills` | **Default.** Install also links `~/.cursor/rules` → `~/.ai-md/rules` |
25
+ | `claude` | `~/.claude/skills` | Optional second harness |
26
+ | `agents` | `~/.agents/skills` | Optional agents-skills layout |
14
27
 
15
28
  ```bash
16
- npm i -g @dujavi/ai-md
17
- ai-md setup --remote https://github.com/<you>/.ai-md.git --tools
18
- # persists ~/.config/ai-md/config.json then clones + links (+ optional grok/quota-axi)
29
+ ai-md doctor --fix --agents cursor,claude
30
+ ai-md install --agents cursor,claude,agents
19
31
  ```
20
32
 
21
- Or step by step:
33
+ ## Architecture
22
34
 
23
- ```bash
24
- ai-md config set --remote https://github.com/<you>/.ai-md.git --dir ~/.ai-md
25
- ai-md install
26
- ai-md ensure-tools
35
+ ```mermaid
36
+ flowchart TB
37
+ subgraph publicPkg ["@dujavi/ai-md"]
38
+ cli["ai-md CLI"]
39
+ runner["lib/scripts.js"]
40
+ end
41
+ subgraph privateDir ["~/.ai-md"]
42
+ system["skills + rules"]
43
+ templates["templates/"]
44
+ projects["projects/"]
45
+ scripts["scripts/"]
46
+ end
47
+ subgraph harnesses ["Supported harnesses"]
48
+ cursor["cursor ~/.cursor"]
49
+ claude["claude ~/.claude"]
50
+ agents["agents ~/.agents"]
51
+ end
52
+ cli --> system
53
+ cli --> templates
54
+ cli --> projects
55
+ cli --> runner
56
+ runner --> scripts
57
+ system --> cursor
58
+ system --> claude
59
+ system --> agents
27
60
  ```
28
61
 
29
- Precedence: `--remote`/`--dir` flags > `AI_MD_*` env > `~/.config/ai-md/config.json` > defaults.
62
+ Personal installers (Grok, quota-axi, etc.) are **not** baked into this package. Put them in `~/.ai-md/scripts/` and run with `--script` / `ai-md script`.
30
63
 
31
- ## Layout idea
64
+ ## Quick start
32
65
 
33
- ```text
34
- ~/.ai-md/
35
- skills/ # system — all agents see these
36
- rules/
37
- templates/
38
- base/ # default project starter (customizable stubs only)
39
- # forms/ … # other use cases
40
- projects/
41
- presenter/
42
- sendfolio/
66
+ ```bash
67
+ npm i -g @dujavi/ai-md
68
+
69
+ # First machine: persist config, clone, link, run a private script
70
+ ai-md setup --remote https://github.com/<you>/.ai-md.git --script ensure-tools
71
+
72
+ # Or step by step
73
+ ai-md config set --remote https://github.com/<you>/.ai-md.git --dir ~/.ai-md
74
+ ai-md install
75
+ ai-md script ensure-tools
43
76
  ```
44
77
 
45
- Put shared agentic skills/rules in **system** folders. Put only type-specific or per-project starters under **templates/**.
78
+ Precedence: `--remote` / `--dir` flags > `AI_MD_*` env > `~/.config/ai-md/config.json` > defaults (`~/.ai-md`, package default remote).
46
79
 
47
80
  ## Commands
48
81
 
49
82
  ```bash
50
- ai-md # status
83
+ ai-md # status (AXI)
84
+ ai-md setup --remote <url> --script <name>
85
+ ai-md config | config set --remote <url> --dir ~/.ai-md
86
+ ai-md install [--agents cursor,claude]
87
+ ai-md doctor --fix
88
+ ai-md pull | push -m "why"
89
+ ai-md script <name> [--] [args...] # private ~/.ai-md/scripts/
51
90
  ai-md init-project --repo ~/app --from base
52
91
  ai-md apply-template --project app --from base
53
- ai-md doctor --fix --agents cursor,claude
54
- ai-md pull | push -m "why"
55
- ai-md ensure-tools
92
+ ai-md link-project --repo ~/app --name app
93
+ ```
94
+
95
+ ### Private scripts
96
+
97
+ ```bash
98
+ ai-md script ensure-tools
99
+ ai-md script ensure-tools -- --dry-run
100
+ ai-md setup --remote <url> --script ensure-tools -- --dry-run
101
+ ```
102
+
103
+ - Resolves `$AI_MD_DIR/scripts/<name>` or `<name>.sh` (basename only; no `..` / paths).
104
+ - Args after the script name (or after `--` on `setup` / `install`) are forwarded unchanged.
105
+ - Repeat `--script` on setup/install; the same trailing args apply to each.
106
+
107
+ ## Machine config
108
+
109
+ Persisted at `~/.config/ai-md/config.json` (override path with `AI_MD_CONFIG`):
110
+
111
+ ```json
112
+ { "dir": "/home/you/.ai-md", "remote": "https://github.com/you/.ai-md.git" }
113
+ ```
114
+
115
+ Env one-shots: `AI_MD_DIR`, `AI_MD_REMOTE`.
116
+
117
+ ## Layout example
118
+
119
+ ```text
120
+ ~/.ai-md/
121
+ skills/ # system — harnesses see these
122
+ rules/
123
+ scripts/
124
+ ensure-tools.sh # your private tooling
125
+ templates/
126
+ base/
127
+ projects/
128
+ my-app/
56
129
  ```
57
130
 
58
- ## Environment
131
+ ## License
59
132
 
60
- - `AI_MD_DIR` / `AI_MD_REMOTE` — override for one shot (also set by the CLI after reading config)
61
- - `AI_MD_CONFIG` — path to machine config JSON (default `~/.config/ai-md/config.json`)
133
+ MIT
package/bin/ai-md.js CHANGED
@@ -20,9 +20,78 @@ const {
20
20
  ensureCursorLinks,
21
21
  ensureAgentSkillLinks,
22
22
  } = require("../lib/commands");
23
+ const { runScript, runScripts } = require("../lib/scripts");
23
24
 
24
25
  const scriptsDir = path.join(__dirname, "..", "scripts");
25
26
 
27
+ function applyFlag(out, flag, args) {
28
+ switch (flag) {
29
+ case "--json":
30
+ out.json = true;
31
+ return true;
32
+ case "--full":
33
+ out.full = true;
34
+ return true;
35
+ case "--force":
36
+ out.force = true;
37
+ return true;
38
+ case "--dry-run":
39
+ out.dryRun = true;
40
+ return true;
41
+ case "--fix":
42
+ out.fix = true;
43
+ out.force = true;
44
+ return true;
45
+ case "-m":
46
+ case "--message":
47
+ out.message = args.shift();
48
+ return true;
49
+ case "--repo":
50
+ out.repo = args.shift();
51
+ return true;
52
+ case "--name":
53
+ out.name = args.shift();
54
+ return true;
55
+ case "--project":
56
+ out.project = args.shift();
57
+ return true;
58
+ case "--from":
59
+ out.from = args.shift();
60
+ return true;
61
+ case "--remote":
62
+ out.remote = args.shift();
63
+ return true;
64
+ case "--dir":
65
+ out.dir = expandHome(args.shift());
66
+ return true;
67
+ case "--agents":
68
+ out.agents = String(args.shift() || "cursor")
69
+ .split(",")
70
+ .map((s) => s.trim())
71
+ .filter(Boolean);
72
+ return true;
73
+ case "--script": {
74
+ const name = args.shift();
75
+ if (!name) {
76
+ fail("--script requires a name", {
77
+ exitCode: 2,
78
+ json: out.json,
79
+ help: ["ai-md setup --script ensure-tools"],
80
+ });
81
+ process.exit(2);
82
+ }
83
+ out.scripts.push(name);
84
+ return true;
85
+ }
86
+ case "-h":
87
+ case "--help":
88
+ out.cmdHelp = true;
89
+ return true;
90
+ default:
91
+ return false;
92
+ }
93
+ }
94
+
26
95
  function parseArgs(argv) {
27
96
  const out = {
28
97
  cmd: null,
@@ -31,7 +100,6 @@ function parseArgs(argv) {
31
100
  force: false,
32
101
  dryRun: false,
33
102
  fix: false,
34
- tools: false,
35
103
  message: null,
36
104
  repo: null,
37
105
  name: null,
@@ -40,6 +108,9 @@ function parseArgs(argv) {
40
108
  remote: null,
41
109
  dir: null,
42
110
  agents: ["cursor"],
111
+ scripts: [],
112
+ scriptName: null,
113
+ scriptArgs: [],
43
114
  rest: [],
44
115
  };
45
116
  const args = [...argv];
@@ -57,67 +128,22 @@ function parseArgs(argv) {
57
128
  } else {
58
129
  out.cmd = args.shift();
59
130
  }
60
- while (args.length) {
61
- const a = args.shift();
62
- switch (a) {
63
- case "--json":
64
- out.json = true;
65
- break;
66
- case "--full":
67
- out.full = true;
68
- break;
69
- case "--force":
70
- out.force = true;
71
- break;
72
- case "--dry-run":
73
- out.dryRun = true;
74
- break;
75
- case "--fix":
76
- out.fix = true;
77
- out.force = true;
78
- break;
79
- case "--tools":
80
- out.tools = true;
81
- break;
82
- case "-m":
83
- case "--message":
84
- out.message = args.shift();
85
- break;
86
- case "--repo":
87
- out.repo = args.shift();
88
- break;
89
- case "--name":
90
- out.name = args.shift();
91
- break;
92
- case "--project":
93
- out.project = args.shift();
94
- break;
95
- case "--from":
96
- out.from = args.shift();
97
- break;
98
- case "--remote":
99
- out.remote = args.shift();
100
- break;
101
- case "--dir":
102
- out.dir = expandHome(args.shift());
103
- break;
104
- case "--agents":
105
- out.agents = String(args.shift() || "cursor")
106
- .split(",")
107
- .map((s) => s.trim())
108
- .filter(Boolean);
109
- break;
110
- case "-h":
111
- case "--help":
112
- out.cmdHelp = true;
113
- break;
114
- case "set":
115
- case "show":
116
- // subcommand for `ai-md config set|show`
117
- out.rest.push(a);
118
- break;
119
- default:
120
- if (a.startsWith("-")) {
131
+
132
+ // `script <name> [--] [args...]` — ai-md flags only before the name
133
+ if (out.cmd === "script" || out.cmd === "run-script") {
134
+ while (args.length) {
135
+ const a = args[0];
136
+ if (a === "--") {
137
+ fail("script name required before `--`", {
138
+ exitCode: 2,
139
+ json: out.json,
140
+ help: ["ai-md script ensure-tools -- --dry-run"],
141
+ });
142
+ process.exit(2);
143
+ }
144
+ if (a.startsWith("-")) {
145
+ args.shift();
146
+ if (!applyFlag(out, a, args)) {
121
147
  fail(`unknown flag: ${a}`, {
122
148
  exitCode: 2,
123
149
  json: out.json,
@@ -125,9 +151,46 @@ function parseArgs(argv) {
125
151
  });
126
152
  process.exit(2);
127
153
  }
128
- out.rest.push(a);
129
- break;
154
+ continue;
155
+ }
156
+ out.scriptName = args.shift();
157
+ break;
158
+ }
159
+ if (!out.scriptName) {
160
+ fail("script requires a name", {
161
+ exitCode: 2,
162
+ json: out.json,
163
+ help: ["ai-md script <name> [--] [args...]"],
164
+ });
165
+ process.exit(2);
166
+ }
167
+ if (args[0] === "--") args.shift();
168
+ out.scriptArgs = args;
169
+ return out;
170
+ }
171
+
172
+ while (args.length) {
173
+ const a = args.shift();
174
+ if (a === "--") {
175
+ out.scriptArgs = args;
176
+ break;
177
+ }
178
+ if (a === "set" || a === "show") {
179
+ out.rest.push(a);
180
+ continue;
181
+ }
182
+ if (a.startsWith("-")) {
183
+ if (!applyFlag(out, a, args)) {
184
+ fail(`unknown flag: ${a}`, {
185
+ exitCode: 2,
186
+ json: out.json,
187
+ help: ["Run `ai-md --help`"],
188
+ });
189
+ process.exit(2);
190
+ }
191
+ continue;
130
192
  }
193
+ out.rest.push(a);
131
194
  }
132
195
  return out;
133
196
  }
@@ -143,19 +206,25 @@ Layout:
143
206
  ~/.ai-md/skills, rules System (global) base — linked to ~/.cursor
144
207
  ~/.ai-md/templates/<type> Project-type starters (default: base)
145
208
  ~/.ai-md/projects/<name> Per-app overlays (repo .cursor → here)
209
+ ~/.ai-md/scripts/<name> Private machine scripts (ai-md script / setup --script)
146
210
 
147
211
  Machine config (persisted):
148
212
  ~/.config/ai-md/config.json dir + remote (override with AI_MD_CONFIG)
149
213
  Precedence: --flag > env > config file > defaults
150
214
 
215
+ Supported harnesses (--agents):
216
+ cursor ~/.cursor/skills (+ ~/.cursor/rules via install) [default]
217
+ claude ~/.claude/skills
218
+ agents ~/.agents/skills
219
+
151
220
  Commands:
152
- setup First-time machine setup: save config, install, optional --tools
221
+ setup First-time machine setup: save config, install, optional --script
153
222
  config Show persisted config (or: config set --remote/--dir)
154
223
  status Snapshot (default when no command) [AXI]
155
224
  doctor Diagnose links/projects; --fix repairs
156
225
  install Clone remote if needed; link ~/.cursor + optional agents
157
226
  pull | push Sync private git repo
158
- ensure-tools Install/update grok + quota-axi (alias: tools)
227
+ script Run ~/.ai-md/scripts/<name> (alias: run-script)
159
228
  init-project Seed projects/<name> from templates/<from> + link .cursor/
160
229
  apply-template Merge missing files from a template into a project
161
230
  link-project Link repo .cursor/ without seeding (alias: link)
@@ -166,22 +235,26 @@ Options:
166
235
  --dir <path> Local AI_MD_DIR (default ~/.ai-md; persisted same way)
167
236
  --json JSON instead of TOON
168
237
  --full Include paths and drift details
169
- --agents <list> Skill link targets: cursor,claude,agents (default: cursor)
170
- --tools With setup: also run ensure-tools
238
+ --agents <list> Skill link harnesses: cursor,claude,agents (default: cursor)
239
+ --script <name> With setup/install: run private script (repeatable)
171
240
  --repo <path> App repository root
172
241
  --name <id> Project id under projects/
173
242
  --project <id> Target project for apply-template
174
243
  --from <id> Template under templates/ (default: base)
175
244
  --force Replace non-symlink paths
176
- --dry-run Preview without writing
245
+ --dry-run Preview without writing (before script name / before --)
177
246
  --fix doctor: repair symlinks
178
247
  -m, --message push commit message
248
+ -- End of ai-md options; remaining args go to private scripts
179
249
 
180
250
  Examples:
181
- ai-md setup --remote https://github.com/<you>/.ai-md.git --tools
251
+ ai-md setup --remote https://github.com/<you>/.ai-md.git --script ensure-tools
252
+ ai-md setup --remote <url> --script ensure-tools -- --dry-run
253
+ ai-md script ensure-tools
254
+ ai-md script ensure-tools -- --dry-run
182
255
  ai-md config set --remote https://github.com/<you>/.ai-md.git --dir ~/.ai-md
183
- ai-md config
184
256
  ai-md install --remote https://github.com/<you>/.ai-md.git
257
+ ai-md doctor --fix --agents cursor,claude
185
258
  ai-md init-project --repo ~/presenter --from base
186
259
  `);
187
260
  }
@@ -237,7 +310,6 @@ function main() {
237
310
  return;
238
311
  }
239
312
 
240
- // Resolve after flags so --dir/--remote apply
241
313
  let cfg = resolveConfig(process.env, {
242
314
  dir: opts.dir || undefined,
243
315
  remote: opts.remote || undefined,
@@ -266,11 +338,18 @@ function main() {
266
338
  );
267
339
  cfg = resolveConfig(process.env);
268
340
  emit({
269
- data: { ...saved, resolved: { dir: cfg.dir, remote: cfg.remote, sources: cfg.sources } },
341
+ data: {
342
+ ...saved,
343
+ resolved: {
344
+ dir: cfg.dir,
345
+ remote: cfg.remote,
346
+ sources: cfg.sources,
347
+ },
348
+ },
270
349
  json: opts.json,
271
350
  help: [
272
351
  "Run `ai-md install` if ~/.ai-md is not cloned yet",
273
- "Run `ai-md setup --remote <url> --tools` for first-time machine bootstrap",
352
+ "Run `ai-md setup --remote <url> --script ensure-tools` for first-time bootstrap",
274
353
  ],
275
354
  });
276
355
  break;
@@ -296,9 +375,6 @@ function main() {
296
375
  break;
297
376
  }
298
377
  case "setup": {
299
- if (!opts.remote && !cfg.machineConfig?.remote && cfg.sources.remote === "default") {
300
- // Allow default for dujavi, but recommend explicit remote for others via help
301
- }
302
378
  const saved = writeMachineConfig(
303
379
  {
304
380
  dir: opts.dir || cfg.dir,
@@ -312,18 +388,14 @@ function main() {
312
388
  remote: opts.remote || undefined,
313
389
  });
314
390
  applyEnvFromConfig(cfg);
315
- const links = opts.dryRun
316
- ? []
317
- : runInstall(cfg, opts);
318
- let tools = null;
319
- if (opts.tools && !opts.dryRun) {
320
- const tr = spawnSync(
321
- "bash",
322
- [path.join(scriptsDir, "ensure-agent-tools.sh")],
323
- { stdio: "inherit", env: process.env }
324
- );
325
- tools = { exitCode: tr.status };
326
- }
391
+ const links = opts.dryRun ? [] : runInstall(cfg, opts);
392
+ const scripts =
393
+ opts.scripts.length === 0
394
+ ? []
395
+ : runScripts(cfg, opts.scripts, opts.scriptArgs, {
396
+ dryRun: opts.dryRun,
397
+ });
398
+ const failed = scripts.find((s) => s.exitCode !== 0);
327
399
  const data = collectStatus({
328
400
  full: opts.full,
329
401
  agents: opts.agents,
@@ -334,17 +406,18 @@ function main() {
334
406
  setup: "ok",
335
407
  config: saved,
336
408
  links,
337
- tools,
409
+ scripts,
338
410
  ...data,
339
411
  },
340
412
  json: opts.json,
341
413
  help: [
342
- opts.tools
414
+ opts.scripts.length
343
415
  ? "Run `ai-md status` to verify"
344
- : "Run `ai-md ensure-tools` (or re-run setup with --tools)",
416
+ : "Run `ai-md script <name>` for private machine scripts (e.g. ensure-tools)",
345
417
  "Run `ai-md init-project --repo <path> --from base` for a new app",
346
418
  ],
347
419
  });
420
+ if (failed) process.exit(failed.exitCode);
348
421
  break;
349
422
  }
350
423
  case "status": {
@@ -433,15 +506,23 @@ function main() {
433
506
  applyEnvFromConfig(cfg);
434
507
  }
435
508
  const links = runInstall(cfg, opts);
509
+ const scripts =
510
+ opts.scripts.length === 0
511
+ ? []
512
+ : runScripts(cfg, opts.scripts, opts.scriptArgs, {
513
+ dryRun: opts.dryRun,
514
+ });
515
+ const failed = scripts.find((s) => s.exitCode !== 0);
436
516
  const data = collectStatus({ full: opts.full, agents: opts.agents });
437
517
  emit({
438
- data: { install: "ok", config: saved, links, ...data },
518
+ data: { install: "ok", config: saved, links, scripts, ...data },
439
519
  json: opts.json,
440
520
  help: [
441
- "Run `ai-md ensure-tools` to install grok + quota-axi",
521
+ "Run `ai-md script <name>` for private machine scripts",
442
522
  "Run `ai-md init-project --repo <path>` for a new app",
443
523
  ],
444
524
  });
525
+ if (failed) process.exit(failed.exitCode);
445
526
  break;
446
527
  }
447
528
  case "pull":
@@ -458,14 +539,23 @@ function main() {
458
539
  runBash("sync-config.sh", args, process.env);
459
540
  break;
460
541
  }
461
- case "ensure-tools":
462
- case "tools":
463
- runBash(
464
- "ensure-agent-tools.sh",
465
- [...(opts.dryRun ? ["--dry-run"] : [])],
466
- process.env
467
- );
542
+ case "script":
543
+ case "run-script": {
544
+ const result = runScript(cfg, opts.scriptName, opts.scriptArgs, {
545
+ dryRun: opts.dryRun,
546
+ });
547
+ emit({
548
+ data: { scripts: [result] },
549
+ json: opts.json,
550
+ help: [
551
+ "Scripts live in ~/.ai-md/scripts/ (private content repo)",
552
+ "ai-md script <name> -- [args...]",
553
+ "ai-md setup --script <name> -- [args...]",
554
+ ],
555
+ });
556
+ process.exit(result.exitCode);
468
557
  break;
558
+ }
469
559
  default:
470
560
  fail(`unknown command: ${opts.cmd}`, {
471
561
  exitCode: 2,
@@ -476,7 +566,7 @@ function main() {
476
566
  }
477
567
  } catch (err) {
478
568
  fail(err.message || String(err), {
479
- exitCode: err.code === "EINVAL" ? 2 : 1,
569
+ exitCode: err.code === "EINVAL" || err.code === "ENOENT" ? 2 : 1,
480
570
  json: opts.json,
481
571
  help: ["Run `ai-md --help`"],
482
572
  });
@@ -0,0 +1,316 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>@dujavi/ai-md architecture</title>
7
+ <style>
8
+ * { box-sizing: border-box; margin: 0; padding: 0; }
9
+
10
+ body {
11
+ background: #ffffff;
12
+ font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
13
+ color: #111827;
14
+ -webkit-font-smoothing: antialiased;
15
+ }
16
+
17
+ .canvas {
18
+ width: 1100px;
19
+ padding: 36px 40px 44px;
20
+ background: #ffffff;
21
+ }
22
+
23
+ h1 {
24
+ font-size: 22px;
25
+ font-weight: 700;
26
+ letter-spacing: -0.02em;
27
+ text-align: center;
28
+ margin-bottom: 28px;
29
+ color: #0f172a;
30
+ }
31
+
32
+ .columns {
33
+ display: grid;
34
+ grid-template-columns: 1fr 1fr 1fr;
35
+ gap: 28px;
36
+ align-items: start;
37
+ }
38
+
39
+ .column-label {
40
+ font-size: 12px;
41
+ font-weight: 600;
42
+ text-transform: none;
43
+ letter-spacing: 0.01em;
44
+ color: #475569;
45
+ margin-bottom: 10px;
46
+ text-align: center;
47
+ }
48
+
49
+ .panel {
50
+ border: 2px solid #1e293b;
51
+ border-radius: 10px;
52
+ background: #f8fafc;
53
+ padding: 14px 14px 16px;
54
+ min-height: 280px;
55
+ }
56
+
57
+ .panel-title {
58
+ font-size: 14px;
59
+ font-weight: 700;
60
+ color: #0f172a;
61
+ text-align: center;
62
+ margin-bottom: 12px;
63
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
64
+ }
65
+
66
+ .stack {
67
+ display: flex;
68
+ flex-direction: column;
69
+ gap: 10px;
70
+ }
71
+
72
+ .box {
73
+ border: 1.5px solid #334155;
74
+ border-radius: 8px;
75
+ background: #ffffff;
76
+ padding: 12px 14px;
77
+ text-align: center;
78
+ font-size: 13px;
79
+ font-weight: 600;
80
+ color: #0f172a;
81
+ line-height: 1.35;
82
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
83
+ }
84
+
85
+ .box.primary {
86
+ background: #eff6ff;
87
+ border-color: #1d4ed8;
88
+ color: #1e3a8a;
89
+ }
90
+
91
+ .box.system {
92
+ background: #ecfdf5;
93
+ border-color: #047857;
94
+ color: #064e3b;
95
+ }
96
+
97
+ .box.template {
98
+ background: #fffbeb;
99
+ border-color: #b45309;
100
+ color: #78350f;
101
+ }
102
+
103
+ .box.project {
104
+ background: #fdf2f8;
105
+ border-color: #be185d;
106
+ color: #831843;
107
+ }
108
+
109
+ .box.script {
110
+ background: #f1f5f9;
111
+ border-color: #475569;
112
+ color: #1e293b;
113
+ }
114
+
115
+ .box.harness {
116
+ background: #f5f3ff;
117
+ border-color: #5b21b6;
118
+ color: #4c1d95;
119
+ }
120
+
121
+ .flow {
122
+ position: relative;
123
+ margin-top: 8px;
124
+ }
125
+
126
+ /* SVG overlay for arrows spanning columns */
127
+ .arrows {
128
+ position: absolute;
129
+ inset: 0;
130
+ width: 100%;
131
+ height: 100%;
132
+ pointer-events: none;
133
+ overflow: visible;
134
+ }
135
+
136
+ .diagram {
137
+ position: relative;
138
+ }
139
+
140
+ .legend {
141
+ margin-top: 22px;
142
+ display: flex;
143
+ justify-content: center;
144
+ gap: 28px;
145
+ flex-wrap: wrap;
146
+ font-size: 12px;
147
+ color: #475569;
148
+ }
149
+
150
+ .legend-item {
151
+ display: flex;
152
+ align-items: center;
153
+ gap: 8px;
154
+ }
155
+
156
+ .legend-line {
157
+ width: 28px;
158
+ height: 0;
159
+ border-top: 2px solid #1e293b;
160
+ position: relative;
161
+ }
162
+
163
+ .legend-line::after {
164
+ content: "";
165
+ position: absolute;
166
+ right: -1px;
167
+ top: -4px;
168
+ border: 4px solid transparent;
169
+ border-left: 7px solid #1e293b;
170
+ }
171
+
172
+ .footer-note {
173
+ margin-top: 14px;
174
+ text-align: center;
175
+ font-size: 11px;
176
+ color: #64748b;
177
+ }
178
+ </style>
179
+ </head>
180
+ <body>
181
+ <div class="canvas" id="capture">
182
+ <h1>@dujavi/ai-md</h1>
183
+
184
+ <div class="diagram" id="diagram">
185
+ <div class="columns" id="columns">
186
+ <!-- Left: package -->
187
+ <div class="col" id="col-pkg">
188
+ <div class="column-label">npm package</div>
189
+ <div class="panel">
190
+ <div class="panel-title">@dujavi/ai-md</div>
191
+ <div class="stack">
192
+ <div class="box primary" id="box-cli">ai-md CLI</div>
193
+ <div class="box script" id="box-runner">lib/scripts.js</div>
194
+ </div>
195
+ </div>
196
+ </div>
197
+
198
+ <!-- Center: private dir -->
199
+ <div class="col" id="col-private">
200
+ <div class="column-label">private config</div>
201
+ <div class="panel">
202
+ <div class="panel-title">~/.ai-md</div>
203
+ <div class="stack">
204
+ <div class="box system" id="box-system">skills + rules</div>
205
+ <div class="box template" id="box-templates">templates/</div>
206
+ <div class="box project" id="box-projects">projects/</div>
207
+ <div class="box script" id="box-scripts">scripts/</div>
208
+ </div>
209
+ </div>
210
+ </div>
211
+
212
+ <!-- Right: harnesses -->
213
+ <div class="col" id="col-harness">
214
+ <div class="column-label">Supported harnesses</div>
215
+ <div class="panel">
216
+ <div class="panel-title">Supported harnesses</div>
217
+ <div class="stack">
218
+ <div class="box harness" id="box-cursor">cursor ~/.cursor</div>
219
+ <div class="box harness" id="box-claude">claude ~/.claude</div>
220
+ <div class="box harness" id="box-agents">agents ~/.agents</div>
221
+ </div>
222
+ </div>
223
+ </div>
224
+ </div>
225
+
226
+ <svg class="arrows" id="arrows" xmlns="http://www.w3.org/2000/svg"></svg>
227
+ </div>
228
+
229
+ <div class="legend">
230
+ <div class="legend-item"><span class="legend-line"></span> manages / links</div>
231
+ </div>
232
+ <p class="footer-note">Public package tooling · private content in ~/.ai-md · linked into agent harnesses</p>
233
+ </div>
234
+
235
+ <script>
236
+ function centerOf(el, root) {
237
+ const r = el.getBoundingClientRect();
238
+ const b = root.getBoundingClientRect();
239
+ return {
240
+ x: r.left - b.left + r.width / 2,
241
+ y: r.top - b.top + r.height / 2,
242
+ left: r.left - b.left,
243
+ right: r.left - b.left + r.width,
244
+ top: r.top - b.top,
245
+ bottom: r.top - b.top + r.height,
246
+ width: r.width,
247
+ height: r.height,
248
+ };
249
+ }
250
+
251
+ function draw() {
252
+ const root = document.getElementById("diagram");
253
+ const svg = document.getElementById("arrows");
254
+ const rect = root.getBoundingClientRect();
255
+ svg.setAttribute("width", rect.width);
256
+ svg.setAttribute("height", rect.height);
257
+ svg.style.width = rect.width + "px";
258
+ svg.style.height = rect.height + "px";
259
+ svg.innerHTML = "";
260
+
261
+ const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
262
+ defs.innerHTML = `
263
+ <marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5"
264
+ markerWidth="7" markerHeight="7" orient="auto-start-reverse">
265
+ <path d="M 0 0 L 10 5 L 0 10 z" fill="#1e293b"/>
266
+ </marker>
267
+ `;
268
+ svg.appendChild(defs);
269
+
270
+ const cli = centerOf(document.getElementById("box-cli"), root);
271
+ const runner = centerOf(document.getElementById("box-runner"), root);
272
+ const system = centerOf(document.getElementById("box-system"), root);
273
+ const templates = centerOf(document.getElementById("box-templates"), root);
274
+ const projects = centerOf(document.getElementById("box-projects"), root);
275
+ const scripts = centerOf(document.getElementById("box-scripts"), root);
276
+ const cursor = centerOf(document.getElementById("box-cursor"), root);
277
+ const claude = centerOf(document.getElementById("box-claude"), root);
278
+ const agents = centerOf(document.getElementById("box-agents"), root);
279
+
280
+ function line(x1, y1, x2, y2, opts = {}) {
281
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
282
+ const midX = (x1 + x2) / 2;
283
+ const d = opts.curve
284
+ ? `M ${x1} ${y1} C ${midX} ${y1}, ${midX} ${y2}, ${x2} ${y2}`
285
+ : `M ${x1} ${y1} L ${x2} ${y2}`;
286
+ path.setAttribute("d", d);
287
+ path.setAttribute("fill", "none");
288
+ path.setAttribute("stroke", opts.color || "#1e293b");
289
+ path.setAttribute("stroke-width", opts.width || "2");
290
+ path.setAttribute("marker-end", "url(#arrow)");
291
+ if (opts.dash) path.setAttribute("stroke-dasharray", "5 4");
292
+ svg.appendChild(path);
293
+ }
294
+
295
+ // CLI → skills+rules, templates, projects
296
+ line(cli.right, cli.y, system.left, system.y, { curve: true });
297
+ line(cli.right, cli.y + 2, templates.left, templates.y, { curve: true });
298
+ line(cli.right, cli.y + 4, projects.left, projects.y, { curve: true });
299
+
300
+ // CLI → lib/scripts.js (vertical within left panel)
301
+ line(cli.x, cli.bottom, runner.x, runner.top, { curve: false });
302
+
303
+ // lib/scripts.js → scripts/
304
+ line(runner.right, runner.y, scripts.left, scripts.y, { curve: true });
305
+
306
+ // skills+rules → each harness
307
+ line(system.right, system.y, cursor.left, cursor.y, { curve: true });
308
+ line(system.right, system.y + 2, claude.left, claude.y, { curve: true });
309
+ line(system.right, system.y + 4, agents.left, agents.y, { curve: true });
310
+ }
311
+
312
+ requestAnimationFrame(() => requestAnimationFrame(draw));
313
+ window.addEventListener("resize", draw);
314
+ </script>
315
+ </body>
316
+ </html>
Binary file
package/lib/scripts.js ADDED
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const { spawnSync } = require("child_process");
6
+
7
+ const NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
8
+
9
+ function scriptsDir(cfg) {
10
+ return path.join(cfg.dir, "scripts");
11
+ }
12
+
13
+ /**
14
+ * Resolve a private script by basename only (no path separators).
15
+ * Prefers exact name, then `<name>.sh`.
16
+ */
17
+ function resolveScript(cfg, name) {
18
+ const raw = String(name || "").trim();
19
+ if (!raw || !NAME_RE.test(raw) || raw.includes("..")) {
20
+ const err = new Error(
21
+ `invalid script name: ${JSON.stringify(name)} (use a single basename)`
22
+ );
23
+ err.code = "EINVAL";
24
+ throw err;
25
+ }
26
+
27
+ const dir = scriptsDir(cfg);
28
+ const exact = path.join(dir, raw);
29
+ const withSh = raw.endsWith(".sh") ? null : path.join(dir, `${raw}.sh`);
30
+
31
+ if (fs.existsSync(exact) && fs.statSync(exact).isFile()) {
32
+ return { name: raw, path: exact, dir };
33
+ }
34
+ if (withSh && fs.existsSync(withSh) && fs.statSync(withSh).isFile()) {
35
+ return { name: raw, path: withSh, dir };
36
+ }
37
+
38
+ const err = new Error(
39
+ `script not found: ${raw} (looked in ${dir}/${raw} and ${dir}/${raw}.sh)`
40
+ );
41
+ err.code = "ENOENT";
42
+ throw err;
43
+ }
44
+
45
+ /**
46
+ * Run a private script with forwarded args. Returns { name, args, path, exitCode }.
47
+ * Uses bash for .sh; otherwise executes the file directly.
48
+ */
49
+ function runScript(cfg, name, args = [], { dryRun = false } = {}) {
50
+ const resolved = resolveScript(cfg, name);
51
+ const scriptArgs = Array.isArray(args) ? args.map(String) : [];
52
+ const useBash = resolved.path.endsWith(".sh");
53
+ const argv = useBash
54
+ ? ["bash", resolved.path, ...scriptArgs]
55
+ : [resolved.path, ...scriptArgs];
56
+
57
+ if (dryRun) {
58
+ return {
59
+ name: resolved.name,
60
+ args: scriptArgs,
61
+ path: resolved.path,
62
+ exitCode: 0,
63
+ dryRun: true,
64
+ command: argv,
65
+ };
66
+ }
67
+
68
+ const result = spawnSync(argv[0], argv.slice(1), {
69
+ stdio: "inherit",
70
+ env: process.env,
71
+ });
72
+
73
+ if (result.error) {
74
+ const err = new Error(result.error.message);
75
+ err.code = result.error.code || "EEXEC";
76
+ throw err;
77
+ }
78
+
79
+ return {
80
+ name: resolved.name,
81
+ args: scriptArgs,
82
+ path: resolved.path,
83
+ exitCode: result.status === null ? 1 : result.status,
84
+ };
85
+ }
86
+
87
+ function runScripts(cfg, names, args = [], { dryRun = false } = {}) {
88
+ const results = [];
89
+ for (const name of names) {
90
+ const result = runScript(cfg, name, args, { dryRun });
91
+ results.push(result);
92
+ if (!dryRun && result.exitCode !== 0) {
93
+ break;
94
+ }
95
+ }
96
+ return results;
97
+ }
98
+
99
+ module.exports = {
100
+ scriptsDir,
101
+ resolveScript,
102
+ runScript,
103
+ runScripts,
104
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dujavi/ai-md",
3
- "version": "0.4.0",
4
- "description": "AXI-shaped CLI for ~/.ai-md with persisted dir/remote machine config, templates/, and projects/",
3
+ "version": "0.5.0",
4
+ "description": "AXI-shaped CLI for a private ~/.ai-md: system skills/rules, templates, projects, and private script runner for agent harnesses (Cursor, Claude, agents)",
5
5
  "bin": {
6
6
  "ai-md": "bin/ai-md.js"
7
7
  },
@@ -9,6 +9,7 @@
9
9
  "bin/",
10
10
  "lib/",
11
11
  "scripts/",
12
+ "docs/",
12
13
  "README.md",
13
14
  "LICENSE"
14
15
  ],
@@ -22,14 +23,16 @@
22
23
  "@toon-format/toon": "^2.3.1"
23
24
  },
24
25
  "keywords": [
26
+ "axi",
25
27
  "cursor",
26
- "ai",
28
+ "claude",
29
+ "agents",
27
30
  "skills",
28
31
  "rules",
29
- "quota-axi",
30
- "grok",
32
+ "personal-config",
31
33
  "cli",
32
- "axi"
34
+ "toon",
35
+ "ai-md"
33
36
  ],
34
37
  "author": "dujavi",
35
38
  "license": "MIT",
@@ -22,7 +22,7 @@ Commands:
22
22
  status Show repo state, symlink health, and rule/skill counts
23
23
  doctor Diagnose problems; with --fix, repair symlinks
24
24
 
25
- Also: ai-md ensure-tools (grok + quota-axi)
25
+ Private scripts: ai-md script <name> (from ~/.ai-md/scripts/)
26
26
 
27
27
  Options:
28
28
  -m, --message <msg> Commit message for push (default: generic update)
@@ -146,7 +146,7 @@ cmd_install() {
146
146
  log "Install complete."
147
147
  log " rules: $(count_rules)"
148
148
  log " skills: $(count_skills)"
149
- log "Next: ai-md ensure-tools # grok + quota-axi"
149
+ log "Next: ai-md script <name> # private ~/.ai-md/scripts/"
150
150
  }
151
151
 
152
152
  cmd_pull() {
@@ -1,114 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Ensure Grok Build CLI + quota-axi are installed on this machine.
3
- # Safe to re-run (idempotent). Does not print secrets.
4
- set -euo pipefail
5
-
6
- DRY_RUN=0
7
-
8
- usage() {
9
- cat <<'EOF'
10
- Usage: ensure-agent-tools.sh [--dry-run]
11
-
12
- Installs or updates:
13
- - grok (Grok Build CLI) via https://x.ai/cli/install.sh
14
- - quota-axi via volta (if present) or npm -g
15
-
16
- Then prints version + auth/quota smoke checks (no secrets).
17
- EOF
18
- }
19
-
20
- log() { printf '%s\n' "$*"; }
21
- err() { printf 'error: %s\n' "$*" >&2; }
22
- run() {
23
- if [[ "$DRY_RUN" -eq 1 ]]; then
24
- log "dry-run: $*"
25
- else
26
- "$@"
27
- fi
28
- }
29
-
30
- while [[ $# -gt 0 ]]; do
31
- case "$1" in
32
- --dry-run) DRY_RUN=1; shift ;;
33
- -h|--help|help) usage; exit 0 ;;
34
- *) err "unknown argument: $1"; usage; exit 1 ;;
35
- esac
36
- done
37
-
38
- ensure_path_hint() {
39
- local need=0
40
- case ":$PATH:" in
41
- *":$HOME/.local/bin:"*) ;;
42
- *) need=1; log "note: add \$HOME/.local/bin to PATH" ;;
43
- esac
44
- case ":$PATH:" in
45
- *":$HOME/.grok/bin:"*) ;;
46
- *) need=1; log "note: add \$HOME/.grok/bin to PATH (grok installer usually does)" ;;
47
- esac
48
- return 0
49
- }
50
-
51
- install_grok() {
52
- if command -v grok >/dev/null 2>&1; then
53
- log "grok present: $(grok --version 2>/dev/null || echo unknown)"
54
- log "Updating grok via official installer…"
55
- else
56
- log "Installing grok via official installer…"
57
- fi
58
- if [[ "$DRY_RUN" -eq 1 ]]; then
59
- log "dry-run: curl -fsSL https://x.ai/cli/install.sh | bash"
60
- return 0
61
- fi
62
- curl -fsSL https://x.ai/cli/install.sh | bash
63
- }
64
-
65
- install_quota_axi() {
66
- if command -v quota-axi >/dev/null 2>&1; then
67
- log "quota-axi present: $(quota-axi --version 2>/dev/null || echo unknown)"
68
- else
69
- log "Installing quota-axi…"
70
- fi
71
- if command -v volta >/dev/null 2>&1; then
72
- run volta install quota-axi
73
- elif command -v npm >/dev/null 2>&1; then
74
- run npm install -g quota-axi
75
- else
76
- err "Need volta or npm to install quota-axi"
77
- return 1
78
- fi
79
- }
80
-
81
- smoke() {
82
- log ""
83
- log "=== smoke checks ==="
84
- if command -v grok >/dev/null 2>&1; then
85
- log "grok: $(command -v grok)"
86
- grok --version 2>&1 || true
87
- if [[ -f "$HOME/.grok/auth.json" ]]; then
88
- log "grok auth: ~/.grok/auth.json present"
89
- else
90
- log "grok auth: missing — run \`grok\` (browser) or set XAI_API_KEY"
91
- fi
92
- else
93
- err "grok not on PATH after install"
94
- fi
95
-
96
- if command -v quota-axi >/dev/null 2>&1; then
97
- log "quota-axi: $(command -v quota-axi)"
98
- quota-axi auth 2>&1 || true
99
- quota-axi --provider grok 2>&1 || true
100
- else
101
- err "quota-axi not on PATH after install"
102
- fi
103
-
104
- if ! command -v sqlite3 >/dev/null 2>&1; then
105
- log "note: sqlite3 not found — Cursor quota via quota-axi may fail (install sqlite3)"
106
- fi
107
- }
108
-
109
- ensure_path_hint
110
- install_grok
111
- install_quota_axi
112
- smoke
113
- log ""
114
- log "Done. Re-run anytime; pair with: ai-md install"