@dujavi/ai-md 0.4.0 → 0.6.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.
@@ -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/build.js ADDED
@@ -0,0 +1,268 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const crypto = require("crypto");
6
+ const { ensureDir, fingerprintTree, walkFiles, hashFile } = require("./link");
7
+ const { selectHarnesses, ensureAgentSourceDirs } = require("./harnesses");
8
+
9
+ function sharedRoots(cfg) {
10
+ return {
11
+ rules: path.join(cfg.dir, "shared", "rules"),
12
+ skills: path.join(cfg.dir, "shared", "skills"),
13
+ };
14
+ }
15
+
16
+ function agentRoots(cfg, id) {
17
+ const srcId = id === "codex" ? "agents" : id;
18
+ return {
19
+ rules: path.join(cfg.dir, "agents", srcId, "rules"),
20
+ skills: path.join(cfg.dir, "agents", srcId, "skills"),
21
+ };
22
+ }
23
+
24
+ function distRoot(cfg, distId) {
25
+ return path.join(cfg.dir, "dist", distId);
26
+ }
27
+
28
+ function listRuleFiles(dir) {
29
+ if (!fs.existsSync(dir)) return [];
30
+ return fs
31
+ .readdirSync(dir)
32
+ .filter((f) => f.endsWith(".mdc") || f.endsWith(".md"))
33
+ .sort();
34
+ }
35
+
36
+ function listSkillDirs(dir) {
37
+ if (!fs.existsSync(dir)) return [];
38
+ return fs
39
+ .readdirSync(dir, { withFileTypes: true })
40
+ .filter((d) => d.isDirectory() && !d.name.startsWith("."))
41
+ .map((d) => d.name)
42
+ .sort();
43
+ }
44
+
45
+ function copyFile(src, dest) {
46
+ ensureDir(path.dirname(dest));
47
+ fs.copyFileSync(src, dest);
48
+ }
49
+
50
+ function copyDir(src, dest) {
51
+ fs.cpSync(src, dest, { recursive: true, force: true });
52
+ }
53
+
54
+ /** Merge shared then agent; agent wins on basename. */
55
+ function mergeRules(sharedDir, agentDir, format) {
56
+ const map = new Map(); // basename without forcing ext
57
+ for (const f of listRuleFiles(sharedDir)) {
58
+ map.set(f.replace(/\.(mdc|md)$/i, ""), {
59
+ src: path.join(sharedDir, f),
60
+ name: f,
61
+ layer: "shared",
62
+ });
63
+ }
64
+ for (const f of listRuleFiles(agentDir)) {
65
+ const key = f.replace(/\.(mdc|md)$/i, "");
66
+ map.set(key, {
67
+ src: path.join(agentDir, f),
68
+ name: f,
69
+ layer: "agent",
70
+ });
71
+ }
72
+ const out = [];
73
+ for (const [, ent] of map) {
74
+ let destName = ent.name;
75
+ if (format === "md" && destName.endsWith(".mdc")) {
76
+ destName = destName.slice(0, -4) + ".md";
77
+ } else if (format === "mdc" && destName.endsWith(".md") && !destName.endsWith(".mdc")) {
78
+ // keep .md as-is for mdc harness if already md; cursor prefers .mdc
79
+ if (!destName.endsWith(".mdc")) destName = destName.replace(/\.md$/i, ".mdc");
80
+ }
81
+ out.push({ ...ent, destName });
82
+ }
83
+ return out;
84
+ }
85
+
86
+ function mergeSkills(sharedDir, agentDir) {
87
+ const map = new Map();
88
+ for (const name of listSkillDirs(sharedDir)) {
89
+ map.set(name, { src: path.join(sharedDir, name), layer: "shared" });
90
+ }
91
+ for (const name of listSkillDirs(agentDir)) {
92
+ map.set(name, { src: path.join(agentDir, name), layer: "agent" });
93
+ }
94
+ return [...map.entries()].map(([name, v]) => ({ name, ...v }));
95
+ }
96
+
97
+ function readManifest(distDir) {
98
+ const p = path.join(distDir, ".build-manifest.json");
99
+ try {
100
+ return JSON.parse(fs.readFileSync(p, "utf8"));
101
+ } catch {
102
+ return null;
103
+ }
104
+ }
105
+
106
+ function distDirty(distDir) {
107
+ const manifest = readManifest(distDir);
108
+ if (!manifest || !manifest.files) {
109
+ if (!fs.existsSync(distDir)) return { dirty: false, files: [] };
110
+ // no manifest but files exist
111
+ const files = walkFiles(distDir);
112
+ return {
113
+ dirty: files.length > 0,
114
+ files: files.map((f) => f.rel),
115
+ reason: "missing_manifest",
116
+ };
117
+ }
118
+ const current = fingerprintTree(distDir);
119
+ const dirtyFiles = [];
120
+ for (const [rel, hash] of Object.entries(manifest.files)) {
121
+ if (current[rel] !== hash) dirtyFiles.push(rel);
122
+ }
123
+ for (const rel of Object.keys(current)) {
124
+ if (!manifest.files[rel]) dirtyFiles.push(rel);
125
+ }
126
+ return { dirty: dirtyFiles.length > 0, files: dirtyFiles, reason: "drift" };
127
+ }
128
+
129
+ function emitHarness(cfg, harness, { dryRun = false, verbose = false } = {}) {
130
+ const distId = harness.distId;
131
+ const outDir = distRoot(cfg, distId);
132
+ const shared = sharedRoots(cfg);
133
+ const agent = agentRoots(cfg, harness.id);
134
+ ensureAgentSourceDirs(cfg, harness.id);
135
+
136
+ const overrides = [];
137
+ const rules =
138
+ harness.format === "skills-only"
139
+ ? []
140
+ : mergeRules(shared.rules, agent.rules, harness.format);
141
+ const skills = mergeSkills(shared.skills, agent.skills);
142
+
143
+ for (const r of rules) {
144
+ if (r.layer === "agent") overrides.push(`rules/${r.destName}`);
145
+ }
146
+ for (const s of skills) {
147
+ if (s.layer === "agent") overrides.push(`skills/${s.name}`);
148
+ }
149
+
150
+ if (dryRun) {
151
+ return {
152
+ id: harness.id,
153
+ distId,
154
+ action: "would_build",
155
+ rules: rules.length,
156
+ skills: skills.length,
157
+ overrides: verbose ? overrides : overrides.length,
158
+ outDir,
159
+ };
160
+ }
161
+
162
+ fs.rmSync(outDir, { recursive: true, force: true });
163
+ ensureDir(path.join(outDir, "skills"));
164
+ if (harness.format !== "skills-only") ensureDir(path.join(outDir, "rules"));
165
+
166
+ for (const r of rules) {
167
+ copyFile(r.src, path.join(outDir, "rules", r.destName));
168
+ }
169
+ for (const s of skills) {
170
+ copyDir(s.src, path.join(outDir, "skills", s.name));
171
+ }
172
+
173
+ const files = fingerprintTree(outDir);
174
+ const manifest = {
175
+ version: 1,
176
+ harness: harness.id,
177
+ distId,
178
+ format: harness.format,
179
+ builtAt: new Date().toISOString(),
180
+ inputsHash: crypto
181
+ .createHash("sha256")
182
+ .update(JSON.stringify({ rules, skills: skills.map((s) => s.name) }))
183
+ .digest("hex"),
184
+ files,
185
+ overrides,
186
+ };
187
+ fs.writeFileSync(
188
+ path.join(outDir, ".build-manifest.json"),
189
+ JSON.stringify(manifest, null, 2) + "\n"
190
+ );
191
+
192
+ return {
193
+ id: harness.id,
194
+ distId,
195
+ action: "built",
196
+ rules: rules.length,
197
+ skills: skills.length,
198
+ overrides: verbose ? overrides : overrides.length,
199
+ outDir,
200
+ };
201
+ }
202
+
203
+ /**
204
+ * Build dist for selected harnesses. Refuses if dirty unless force.
205
+ */
206
+ function runBuild(cfg, opts = {}) {
207
+ const {
208
+ agents,
209
+ force = false,
210
+ dryRun = false,
211
+ verbose = false,
212
+ forceLink = false,
213
+ includeUninstalled = false,
214
+ } = opts;
215
+
216
+ const { selected, skipped, warnings } = selectHarnesses(cfg, agents, {
217
+ forceLink,
218
+ includeUninstalled,
219
+ });
220
+
221
+ // Deduplicate by distId for emit
222
+ const byDist = new Map();
223
+ for (const h of selected) {
224
+ if (!byDist.has(h.distId)) byDist.set(h.distId, h);
225
+ }
226
+
227
+ const dirtyBlocks = [];
228
+ for (const [distId] of byDist) {
229
+ const d = distDirty(distRoot(cfg, distId));
230
+ if (d.dirty && !force) {
231
+ dirtyBlocks.push({ distId, ...d });
232
+ }
233
+ }
234
+
235
+ if (dirtyBlocks.length && !dryRun) {
236
+ const err = new Error(
237
+ `dist has edits not in source (${dirtyBlocks.map((d) => d.distId).join(", ")}). ` +
238
+ `Run: ai-md rescue --agents <id> or ai-md build --force`
239
+ );
240
+ err.code = "EDIRTY";
241
+ err.dirty = dirtyBlocks;
242
+ throw err;
243
+ }
244
+
245
+ const results = [];
246
+ for (const h of byDist.values()) {
247
+ results.push(emitHarness(cfg, h, { dryRun, verbose }));
248
+ }
249
+
250
+ return {
251
+ built: results,
252
+ skipped,
253
+ warnings,
254
+ dirtyForced: force ? dirtyBlocks : [],
255
+ };
256
+ }
257
+
258
+ module.exports = {
259
+ sharedRoots,
260
+ agentRoots,
261
+ distRoot,
262
+ mergeRules,
263
+ mergeSkills,
264
+ distDirty,
265
+ readManifest,
266
+ runBuild,
267
+ emitHarness,
268
+ };