@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.
package/lib/status.js CHANGED
@@ -12,9 +12,16 @@ const {
12
12
  countSkills,
13
13
  listSkillNames,
14
14
  listRuleNames,
15
- symlinkState,
16
- agentSkillTargets,
17
15
  } = require("./config");
16
+ const { linkState } = require("./link");
17
+ const { distRoot, distDirty, sharedRoots } = require("./build");
18
+ const {
19
+ selectHarnesses,
20
+ resolveHarness,
21
+ isHarnessInstalled,
22
+ } = require("./harnesses");
23
+ const { isWsl } = require("./config-paths");
24
+ const { SKELETON_VERSION } = require("./skeleton");
18
25
 
19
26
  function git(repo, args) {
20
27
  try {
@@ -60,9 +67,7 @@ function collectProjects(cfg, { full = false, from = "base" } = {}) {
60
67
  const tmplDir = templatePath(cfg, from);
61
68
  if (!fs.existsSync(cfg.projectsDir)) return projects;
62
69
  for (const name of fs.readdirSync(cfg.projectsDir).sort()) {
63
- if (name.startsWith(".")) continue;
64
- // legacy leftover
65
- if (name === "template") continue;
70
+ if (name.startsWith(".") || name === "template") continue;
66
71
  const projectDir = path.join(cfg.projectsDir, name);
67
72
  if (!fs.statSync(projectDir).isDirectory()) continue;
68
73
  const drift = templateDrift(projectDir, tmplDir);
@@ -83,25 +88,50 @@ function collectProjects(cfg, { full = false, from = "base" } = {}) {
83
88
  function collectStatus(opts = {}) {
84
89
  const cfg = resolveConfig();
85
90
  const full = Boolean(opts.full);
86
- const agents = opts.agents || ["cursor"];
91
+ const agents = opts.agents || cfg.agents || ["cursor"];
87
92
  const from = opts.from || "base";
88
93
 
89
94
  const migration = migrateLegacyTemplate(cfg);
90
-
91
95
  const exists = fs.existsSync(cfg.dir);
92
96
  const isGit = exists && fs.existsSync(path.join(cfg.dir, ".git"));
97
+ const shared = sharedRoots(cfg);
93
98
 
94
- const links = [
95
- symlinkState(cfg.cursorSkills, cfg.skillsDir),
96
- symlinkState(cfg.cursorRules, cfg.rulesDir),
97
- ];
99
+ const { selected, skipped, warnings } = selectHarnesses(cfg, agents, {
100
+ forceLink: false,
101
+ includeUninstalled: true,
102
+ });
98
103
 
99
- const agentLinks = agentSkillTargets(cfg.home, agents)
100
- .filter((t) => t.name !== "cursor")
101
- .map((t) => ({
102
- agent: t.name,
103
- ...symlinkState(t.path, cfg.skillsDir),
104
- }));
104
+ const links = [];
105
+ const harnesses = [];
106
+ for (const h of selected) {
107
+ const installed = isHarnessInstalled(h);
108
+ const dist = distRoot(cfg, h.distId);
109
+ const dirty = distDirty(dist);
110
+ const entry = {
111
+ id: h.id,
112
+ distId: h.distId,
113
+ installed,
114
+ aliasOf: h.aliasOf,
115
+ format: h.format,
116
+ distDirty: dirty.dirty,
117
+ ...(full && dirty.dirty ? { dirtyFiles: dirty.files } : {}),
118
+ };
119
+ if (installed) {
120
+ if (h.skills) {
121
+ const st = linkState(h.skills, path.join(dist, "skills"));
122
+ links.push({ harness: h.id, kind: "skills", ...st });
123
+ entry.skillsLink = st.state;
124
+ }
125
+ if (h.rules && h.format !== "skills-only") {
126
+ const st = linkState(h.rules, path.join(dist, "rules"));
127
+ links.push({ harness: h.id, kind: "rules", ...st });
128
+ entry.rulesLink = st.state;
129
+ }
130
+ } else {
131
+ entry.skillsLink = "skipped_not_installed";
132
+ }
133
+ harnesses.push(entry);
134
+ }
105
135
 
106
136
  const templates = collectTemplates(cfg, { full });
107
137
  const projects = collectProjects(cfg, { full, from });
@@ -124,44 +154,67 @@ function collectStatus(opts = {}) {
124
154
  const problems = [];
125
155
  if (!exists) problems.push("ai_md_missing");
126
156
  else if (!isGit) problems.push("ai_md_not_git");
157
+ if (fs.existsSync(path.join(cfg.dir, "rules")) || fs.existsSync(path.join(cfg.dir, "skills"))) {
158
+ problems.push("legacy_flat_layout");
159
+ }
127
160
  for (const l of links) {
128
- if (l.state !== "ok") problems.push(`cursor_${path.basename(l.path)}_${l.state}`);
161
+ if (l.state !== "ok" && l.state !== "directory") {
162
+ problems.push(`${l.harness}_${l.kind}_${l.state}`);
163
+ }
164
+ }
165
+ for (const h of harnesses) {
166
+ if (h.installed && h.distDirty) problems.push(`dist_dirty_${h.distId}`);
129
167
  }
130
168
  if (!fs.existsSync(templatePath(cfg, "base")) && templates.length === 0) {
131
169
  problems.push("templates_missing");
132
170
  }
133
171
 
172
+ let skeletonVersion = null;
173
+ try {
174
+ skeletonVersion = Number(
175
+ fs.readFileSync(path.join(cfg.dir, ".ai-md-skeleton-version"), "utf8").trim()
176
+ );
177
+ } catch {
178
+ /* none */
179
+ }
180
+ if (skeletonVersion != null && skeletonVersion < SKELETON_VERSION) {
181
+ problems.push("skeleton_outdated");
182
+ }
183
+
134
184
  return {
135
185
  generatedAt: new Date().toISOString(),
136
186
  dir: cfg.dir,
137
187
  remote: remote || cfg.remote,
138
188
  sources: cfg.sources,
189
+ linkMode: cfg.linkMode,
190
+ wsl: isWsl(),
139
191
  machineConfigPath: cfg.machineConfigPath,
140
192
  machineConfig: cfg.machineConfig,
141
193
  branch,
142
194
  dirty,
143
195
  statusLine: aheadBehind,
144
196
  layout: {
145
- system: { rules: cfg.rulesDir, skills: cfg.skillsDir },
197
+ shared: { rules: shared.rules, skills: shared.skills },
198
+ agents: cfg.agentsDir,
199
+ dist: cfg.distDir,
146
200
  templates: cfg.templatesDir,
147
201
  projects: cfg.projectsDir,
148
202
  },
149
203
  migration,
150
204
  counts: {
151
- rules: countRules(cfg.rulesDir),
152
- skills: countSkills(cfg.skillsDir),
205
+ rules: countRules(shared.rules),
206
+ skills: countSkills(shared.skills),
153
207
  templates: templates.length,
154
208
  projects: projects.length,
155
209
  drifting: drifting.length,
156
210
  problems: problems.length,
157
211
  },
212
+ harnesses,
213
+ skipped,
214
+ warnings,
158
215
  links: links.map((l) => ({
159
- path: l.path,
160
- state: l.state,
161
- ...(full ? { target: l.target, expected: l.expected } : {}),
162
- })),
163
- agentLinks: agentLinks.map((l) => ({
164
- agent: l.agent,
216
+ harness: l.harness,
217
+ kind: l.kind,
165
218
  path: l.path,
166
219
  state: l.state,
167
220
  ...(full ? { target: l.target, expected: l.expected } : {}),
@@ -169,6 +222,8 @@ function collectStatus(opts = {}) {
169
222
  templates,
170
223
  projects,
171
224
  driftFrom: from,
225
+ skeletonVersion,
226
+ packageSkeletonVersion: SKELETON_VERSION,
172
227
  problems,
173
228
  };
174
229
  }
@@ -177,7 +232,12 @@ function statusHelp(data) {
177
232
  const help = [];
178
233
  if (data.problems.includes("ai_md_missing") || data.problems.includes("ai_md_not_git")) {
179
234
  help.push(
180
- "Run `ai-md setup --remote <git-url>` (or `ai-md install --remote <git-url>`) to clone and link"
235
+ "Run `ai-md init` (no remote) or `ai-md setup --remote <git-url>`"
236
+ );
237
+ }
238
+ if (data.problems.includes("legacy_flat_layout")) {
239
+ help.push(
240
+ "Migrate flat rules/skills into shared/ (and agents/<id>/); see package README"
181
241
  );
182
242
  }
183
243
  if (!data.machineConfig) {
@@ -185,23 +245,38 @@ function statusHelp(data) {
185
245
  "Persist remote/dir with `ai-md setup --remote <url>` or `ai-md config set --remote <url> --dir ~/.ai-md`"
186
246
  );
187
247
  }
188
- if (data.links.some((l) => l.state !== "ok")) {
189
- help.push("Run `ai-md doctor --fix` to repair ~/.cursor skills/rules symlinks");
248
+ if (data.links.some((l) => l.state !== "ok" && l.state !== "directory")) {
249
+ help.push("Run `ai-md doctor --fix` to rebuild dist and repair links");
250
+ }
251
+ if (data.problems.some((p) => p.startsWith("dist_dirty_"))) {
252
+ help.push("Run `ai-md rescue --agents <id>` or `ai-md build --force`");
253
+ }
254
+ if (data.problems.includes("skeleton_outdated")) {
255
+ help.push("Run `ai-md seed-skeleton` to add new recommended files");
256
+ }
257
+ if (data.wsl) {
258
+ help.push(
259
+ "WSL detected: use Windows ai-md for Windows Cursor (homes differ)"
260
+ );
261
+ }
262
+ if (data.skipped && data.skipped.some((s) => s.reason === "not_installed")) {
263
+ help.push(
264
+ "Some harnesses skipped (AI not installed). Install the tool or use --force-link"
265
+ );
190
266
  }
191
267
  if (data.problems.includes("templates_missing")) {
192
- help.push("Create ~/.ai-md/templates/base with project starters (system skills/rules stay in skills/ and rules/)");
268
+ help.push("Run `ai-md seed-skeleton` or add templates/base");
193
269
  }
194
270
  if (data.counts.drifting > 0) {
195
271
  help.push(
196
- `Run \`ai-md apply-template --project <name> --from ${data.driftFrom || "base"}\` to merge missing template files`
272
+ `Run \`ai-md apply-template --project <name> --from ${data.driftFrom || "base"}\``
197
273
  );
198
274
  }
199
275
  if (data.dirty) {
200
- help.push('Run `ai-md push -m "<why>"` to commit and push private config');
276
+ help.push('Run `ai-md push -m "<why>"` to commit and push');
201
277
  } else {
202
- help.push("Edit system skills/rules under ~/.ai-md/{skills,rules}; project overlays under projects/");
278
+ help.push("Edit shared/ or agents/<id>/ only; then `ai-md build`");
203
279
  }
204
- help.push("Run `ai-md init-project --repo <path> --from base` to seed a project");
205
280
  help.push("Run `ai-md status --json` for machine-readable output");
206
281
  return help;
207
282
  }
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.6.0",
4
+ "description": "AXI-shaped CLI for private ~/.ai-md: shared+agents build to dist, multi-harness link (Cursor, Claude, agents, …)",
5
5
  "bin": {
6
6
  "ai-md": "bin/ai-md.js"
7
7
  },
@@ -9,6 +9,8 @@
9
9
  "bin/",
10
10
  "lib/",
11
11
  "scripts/",
12
+ "skeleton/",
13
+ "docs/",
12
14
  "README.md",
13
15
  "LICENSE"
14
16
  ],
@@ -16,20 +18,22 @@
16
18
  "node": ">=18"
17
19
  },
18
20
  "scripts": {
19
- "test": "node bin/ai-md.js --help && node bin/ai-md.js status --json >/dev/null"
21
+ "test": "node bin/ai-md.js --help && node bin/ai-md.js harness list --json >/dev/null && node bin/ai-md.js status --json >/dev/null"
20
22
  },
21
23
  "dependencies": {
22
24
  "@toon-format/toon": "^2.3.1"
23
25
  },
24
26
  "keywords": [
27
+ "axi",
25
28
  "cursor",
26
- "ai",
29
+ "claude",
30
+ "agents",
27
31
  "skills",
28
32
  "rules",
29
- "quota-axi",
30
- "grok",
33
+ "personal-config",
31
34
  "cli",
32
- "axi"
35
+ "toon",
36
+ "ai-md"
33
37
  ],
34
38
  "author": "dujavi",
35
39
  "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() {
@@ -0,0 +1,3 @@
1
+ # Your private AI config
2
+
3
+ Edit `shared/` and `agents/<id>/` only. Run `ai-md build` after changes.
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -0,0 +1,23 @@
1
+ ---
2
+ description: Edit ~/.ai-md shared/ and agents/ only — never live harness dirs or dist/
3
+ alwaysApply: true
4
+ ---
5
+
6
+ # Edit source, not dist or live harness paths
7
+
8
+ `~/.ai-md` is the source of truth. Live tool dirs (`~/.cursor`, `~/.claude`, `~/.agents`, …) and `~/.ai-md/dist/` are **build outputs**.
9
+
10
+ ## Do
11
+
12
+ - Edit `~/.ai-md/shared/` for rules/skills that apply to all harnesses.
13
+ - Edit `~/.ai-md/agents/<harness>/` for harness-specific rules/skills.
14
+ - After edits: `ai-md build` (or `ai-md pull` / `install`, which rebuild).
15
+
16
+ ## Do not
17
+
18
+ - Edit `~/.cursor/{rules,skills}`, `~/.claude/…`, `~/.agents/…`, or any other live harness path.
19
+ - Edit `~/.ai-md/dist/` directly.
20
+
21
+ ## If you already edited a live path
22
+
23
+ Stop. Run `ai-md rescue --agents <harness>` to promote dirty files into `agents/<harness>/`, then review and `ai-md build`. Do not keep iterating under dist or live dirs.
@@ -0,0 +1,49 @@
1
+ ---
2
+ description: Sync global personal skills and rules via ~/.ai-md and the ai-md CLI (build → dist → link)
3
+ alwaysApply: true
4
+ ---
5
+
6
+ # Personal skills and rules — ai-md
7
+
8
+ **`~/.ai-md`** is the private source of truth. Use **`ai-md`** (`@dujavi/ai-md`) — not Cursor Sync / Gist.
9
+
10
+ ## Layout
11
+
12
+ | Path | Role |
13
+ |------|------|
14
+ | `shared/rules`, `shared/skills` | System base for all harnesses |
15
+ | `agents/<id>/` | Harness-specific overlays |
16
+ | `dist/<id>/` | Build output (gitignored) — do not edit |
17
+ | `templates/<type>/` | Project-type starters |
18
+ | `projects/<name>/` | Per-app overlays |
19
+ | `scripts/` | Private machine scripts |
20
+
21
+ See also `edit-source-not-dist.mdc` and skill `ai-md-config`.
22
+
23
+ ## Sync
24
+
25
+ ```bash
26
+ ai-md pull
27
+ # edit shared/ or agents/<id>/
28
+ ai-md build
29
+ ai-md push -m "why"
30
+ ```
31
+
32
+ `install` / `pull` / `doctor --fix` rebuild and link **only for installed AIs** (detect `~/.cursor`, `claude` on PATH, etc.). Use `--force-link` to override.
33
+
34
+ ## Where to edit
35
+
36
+ | What | Edit here |
37
+ |------|-----------|
38
+ | Cross-harness rules/skills | `~/.ai-md/shared/` |
39
+ | Cursor-only (e.g. Grok CLI policy) | `~/.ai-md/agents/cursor/` |
40
+ | Project starters | `~/.ai-md/templates/<type>/` |
41
+ | App overlays | `~/.ai-md/projects/<repo>/` |
42
+
43
+ Never edit `~/.cursor/skills-cursor/`, live harness dirs, or `dist/`.
44
+
45
+ ## Agent behavior
46
+
47
+ - Global changes → `shared/` or `agents/<id>/`; remind `ai-md build` then `push`.
48
+ - Symlink / dist issues → `ai-md doctor --fix` or `ai-md rescue`.
49
+ - New harness → `ai-md harness set …` then content under `agents/<id>/`.
@@ -0,0 +1,51 @@
1
+ ---
2
+ name: ai-md-config
3
+ description: >-
4
+ Manage personal AI config in ~/.ai-md: shared vs agents layers, build/dist,
5
+ harness registration, rescue dirty dist. Use when adding rules/skills, fixing
6
+ links, or supporting a new AI tool.
7
+ ---
8
+
9
+ # ai-md config playbook
10
+
11
+ ## Layout
12
+
13
+ | Path | Role |
14
+ |------|------|
15
+ | `shared/rules`, `shared/skills` | All harnesses |
16
+ | `agents/<id>/rules`, `agents/<id>/skills` | One harness (overlay wins on name conflict) |
17
+ | `dist/<id>/` | Generated — never edit |
18
+ | `projects/<name>/` | Per-repo overlays (`.cursor` → here) |
19
+
20
+ ## Commands
21
+
22
+ ```bash
23
+ ai-md pull / push -m "why"
24
+ ai-md build [--agents cursor,claude] [--force]
25
+ ai-md rescue --agents cursor
26
+ ai-md harness list | show <id>
27
+ ai-md harness set <id> --skills ~/path [--rules ~/path] [--format mdc|md|skills-only]
28
+ ai-md harness enable|disable <id>
29
+ ai-md init | seed-skeleton
30
+ ai-md doctor --fix
31
+ ```
32
+
33
+ ## Add a rule or skill
34
+
35
+ 1. Choose layer: `shared/` (all tools) vs `agents/<id>/` (one tool).
36
+ 2. Write the file under that path (never under `dist/` or `~/.cursor/…`).
37
+ 3. `ai-md build`
38
+ 4. `ai-md push -m "…"` when ready.
39
+
40
+ ## New harness
41
+
42
+ ```bash
43
+ ai-md harness set my-tool --skills ~/.my-tool/skills --rules ~/.my-tool/rules --format md
44
+ # put tool-specific content in agents/my-tool/
45
+ ai-md build --agents my-tool
46
+ ```
47
+
48
+ ## Unique vs shared `.agents`
49
+
50
+ - Unique live roots: `cursor`, `claude`, `gemini`, `opencode`, `copilot`
51
+ - Shared `~/.agents/skills`: `agents` (canonical writer); `codex` is an alias (no second emit)
File without changes
@@ -0,0 +1,8 @@
1
+ ---
2
+ description: Project-specific context — customize after init-project
3
+ alwaysApply: true
4
+ ---
5
+
6
+ # Project context
7
+
8
+ Replace this stub with stack, phase gates, and doc index for this app.
File without changes
@@ -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"