@dujavi/ai-md 0.2.0 → 0.3.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,69 +1,49 @@
1
1
  # ai-md
2
2
 
3
- Public **AXI-shaped** CLI for a **private** personal AI config directory (`~/.ai-md`): global skills/rules, per-project templates, and agent tool bootstrap.
3
+ Public **AXI-shaped** CLI for a **private** personal AI config directory (`~/.ai-md`).
4
4
 
5
- | Layer | What | Where |
6
- |-------|------|--------|
7
- | **Public** | This package (`@dujavi/ai-md`) | npm + [github.com/dujavi/ai-md](https://github.com/dujavi/ai-md) |
8
- | **Private** | Your skills, rules, `projects/` | `~/.ai-md` git repo |
5
+ | Layer | Path | Purpose |
6
+ |-------|------|---------|
7
+ | **System** | `skills/`, `rules/` | Global base — linked to `~/.cursor/{skills,rules}` |
8
+ | **Templates** | `templates/<type>/` | Project-type starters (`base`, later `forms`, …) |
9
+ | **Projects** | `projects/<name>/` | Per-app overlays (repo `.cursor` → here) |
9
10
 
10
- No personal rules or skills ship in this package. Output defaults to [TOON](https://toonformat.dev/) with `help[]` next steps (`--json` available).
11
+ No personal content ships in this package. Reads default to TOON + `help[]` (`--json` available).
11
12
 
12
13
  ## Install
13
14
 
14
15
  ```bash
15
16
  npm i -g @dujavi/ai-md
16
- # binary: ai-md
17
17
  ```
18
18
 
19
- ## New machine
20
-
21
- ```bash
22
- export AI_MD_REMOTE=https://github.com/<you>/.ai-md.git
23
- ai-md install
24
- ai-md ensure-tools
25
- ```
26
-
27
- Optional multi-agent skill links:
28
-
29
- ```bash
30
- ai-md install --agents cursor,claude,agents
19
+ ## Layout idea
20
+
21
+ ```text
22
+ ~/.ai-md/
23
+ skills/ # system — all agents see these
24
+ rules/
25
+ templates/
26
+ base/ # default project starter (customizable stubs only)
27
+ # forms/ … # other use cases
28
+ projects/
29
+ presenter/
30
+ sendfolio/
31
31
  ```
32
32
 
33
- ## Day-to-day
34
-
35
- ```bash
36
- ai-md # status (content-first)
37
- ai-md status --json
38
- ai-md pull
39
- ai-md push -m "why"
40
- ai-md doctor --fix
41
- ```
33
+ Put shared agentic skills/rules in **system** folders. Put only type-specific or per-project starters under **templates/**.
42
34
 
43
- ## Projects (templating)
35
+ ## Commands
44
36
 
45
37
  ```bash
46
- # Seed from projects/template + link repo/.cursor + gitignore
47
- ai-md init-project --repo ~/my-app
48
-
49
- # Merge missing baseline files into an existing project
50
- ai-md apply-template --project my-app
51
-
52
- # Link only (empty rules/skills dirs if missing)
53
- ai-md link-project --repo ~/my-app
38
+ ai-md # status
39
+ ai-md init-project --repo ~/app --from base
40
+ ai-md apply-template --project app --from base
41
+ ai-md doctor --fix --agents cursor,claude
42
+ ai-md pull | push -m "why"
43
+ ai-md ensure-tools
54
44
  ```
55
45
 
56
- ## AXI-shaped reads
57
-
58
- | Flag | Effect |
59
- |------|--------|
60
- | (default) | TOON on stdout + `help[]` |
61
- | `--json` | Normalized JSON |
62
- | `--full` | Paths + template drift details |
63
-
64
- Mutations (`push`, `ensure-tools`) stay human/git-oriented.
65
-
66
46
  ## Environment
67
47
 
68
48
  - `AI_MD_DIR` → `~/.ai-md`
69
- - `AI_MD_REMOTE` → `https://github.com/dujavi/.ai-md.git`
49
+ - `AI_MD_REMOTE` → private content git URL
package/bin/ai-md.js CHANGED
@@ -35,7 +35,7 @@ function parseArgs(argv) {
35
35
  repo: null,
36
36
  name: null,
37
37
  project: null,
38
- from: "template",
38
+ from: "base",
39
39
  agents: ["cursor"],
40
40
  rest: [],
41
41
  };
@@ -117,20 +117,25 @@ function parseArgs(argv) {
117
117
  }
118
118
 
119
119
  function printHelp() {
120
- process.stdout.write(`ai-md — private ~/.ai-md skills/rules + project templates (AXI-shaped)
120
+ process.stdout.write(`ai-md — private ~/.ai-md: system skills/rules + templates/ + projects/ (AXI-shaped)
121
121
 
122
122
  Usage:
123
123
  ai-md [command] [options]
124
124
  npx -y @dujavi/ai-md [command] [options]
125
125
 
126
+ Layout:
127
+ ~/.ai-md/skills, rules System (global) base — linked to ~/.cursor
128
+ ~/.ai-md/templates/<type> Project-type starters (default: base)
129
+ ~/.ai-md/projects/<name> Per-app overlays (repo .cursor → here)
130
+
126
131
  Commands:
127
132
  status Snapshot (default when no command) [AXI]
128
133
  doctor Diagnose links/projects; --fix repairs
129
134
  install Clone remote if needed; link ~/.cursor + optional agents
130
135
  pull | push Sync private git repo
131
136
  ensure-tools Install/update grok + quota-axi (alias: tools)
132
- init-project Seed projects/<name> from template + link repo .cursor/
133
- apply-template Merge missing template files into an existing project
137
+ init-project Seed projects/<name> from templates/<from> + link .cursor/
138
+ apply-template Merge missing files from a template into a project
134
139
  link-project Link repo .cursor/ without seeding (alias: link)
135
140
  help Show this help
136
141
 
@@ -141,23 +146,18 @@ Options:
141
146
  --repo <path> App repository root
142
147
  --name <id> Project id under projects/ (default: basename)
143
148
  --project <id> Target project for apply-template
144
- --from <id> Template folder (default: template)
149
+ --from <id> Template under templates/ (default: base)
145
150
  --force Replace non-symlink .cursor / re-merge
146
151
  --dry-run Preview without writing
147
152
  --fix doctor: repair symlinks
148
153
  -m, --message push commit message
149
154
 
150
- Environment:
151
- AI_MD_DIR Private config dir (default: ~/.ai-md)
152
- AI_MD_REMOTE Clone URL (default: https://github.com/dujavi/.ai-md.git)
153
-
154
155
  Examples:
155
156
  ai-md
156
157
  ai-md status --json
158
+ ai-md init-project --repo ~/presenter --from base
159
+ ai-md apply-template --project presenter --from base
157
160
  ai-md doctor --fix --agents cursor,claude
158
- ai-md init-project --repo ~/presenter
159
- ai-md apply-template --project presenter
160
- ai-md push -m "Add rule"
161
161
  `);
162
162
  }
163
163
 
@@ -184,7 +184,11 @@ function main() {
184
184
  try {
185
185
  switch (opts.cmd) {
186
186
  case "status": {
187
- const data = collectStatus({ full: opts.full, agents: opts.agents });
187
+ const data = collectStatus({
188
+ full: opts.full,
189
+ agents: opts.agents,
190
+ from: opts.from,
191
+ });
188
192
  emit({ data, json: opts.json, help: statusHelp(data) });
189
193
  process.exitCode = data.counts.problems > 0 ? 1 : 0;
190
194
  break;
@@ -225,6 +229,7 @@ function main() {
225
229
  case "apply-template": {
226
230
  const data = applyTemplate({
227
231
  project: opts.project || opts.name,
232
+ from: opts.from,
228
233
  dryRun: opts.dryRun,
229
234
  });
230
235
  emit({
package/lib/commands.js CHANGED
@@ -4,6 +4,7 @@ const fs = require("fs");
4
4
  const path = require("path");
5
5
  const {
6
6
  resolveConfig,
7
+ templatePath,
7
8
  symlinkState,
8
9
  agentSkillTargets,
9
10
  } = require("./config");
@@ -86,7 +87,7 @@ function copyDir(src, dest, { dryRun = false } = {}) {
86
87
  function initProject({
87
88
  repo,
88
89
  name,
89
- from = "template",
90
+ from = "base",
90
91
  force = false,
91
92
  dryRun = false,
92
93
  } = {}) {
@@ -103,11 +104,13 @@ function initProject({
103
104
  }
104
105
  const absRepo = fs.realpathSync(repo);
105
106
  const projectName = name || path.basename(absRepo);
106
- const templateDir = path.join(cfg.projectsDir, from);
107
+ const tmplDir = templatePath(cfg, from);
107
108
  const projectDir = path.join(cfg.projectsDir, projectName);
108
109
 
109
- if (!fs.existsSync(templateDir)) {
110
- const err = new Error(`template not found: ${templateDir}`);
110
+ if (!fs.existsSync(tmplDir)) {
111
+ const err = new Error(
112
+ `template not found: ${tmplDir} (available under ${cfg.templatesDir}/)`
113
+ );
111
114
  err.code = "ENOENT";
112
115
  throw err;
113
116
  }
@@ -121,11 +124,10 @@ function initProject({
121
124
  if (hasContent && !force) {
122
125
  actions.push({ action: "exists", path: projectDir });
123
126
  } else {
124
- // Empty project dir, or --force: merge missing baseline files only
125
- actions.push(...mergeTemplate(templateDir, projectDir, { dryRun }));
127
+ actions.push(...mergeTemplate(tmplDir, projectDir, { dryRun }));
126
128
  }
127
129
  } else {
128
- actions.push(copyDir(templateDir, projectDir, { dryRun }));
130
+ actions.push(copyDir(tmplDir, projectDir, { dryRun }));
129
131
  }
130
132
 
131
133
  const link = path.join(absRepo, ".cursor");
@@ -137,6 +139,7 @@ function initProject({
137
139
  projectDir,
138
140
  repo: absRepo,
139
141
  from,
142
+ templateDir: tmplDir,
140
143
  actions,
141
144
  };
142
145
  }
@@ -172,7 +175,7 @@ function mergeTemplate(templateDir, projectDir, { dryRun = false } = {}) {
172
175
  return actions;
173
176
  }
174
177
 
175
- function applyTemplate({ project, dryRun = false } = {}) {
178
+ function applyTemplate({ project, from = "base", dryRun = false } = {}) {
176
179
  const cfg = resolveConfig();
177
180
  if (!project) {
178
181
  const err = new Error("missing --project <name>");
@@ -185,13 +188,14 @@ function applyTemplate({ project, dryRun = false } = {}) {
185
188
  err.code = "ENOENT";
186
189
  throw err;
187
190
  }
188
- if (project === "template") {
189
- const err = new Error("cannot apply-template onto template itself");
190
- err.code = "EINVAL";
191
+ const tmplDir = templatePath(cfg, from);
192
+ if (!fs.existsSync(tmplDir)) {
193
+ const err = new Error(`template not found: ${tmplDir}`);
194
+ err.code = "ENOENT";
191
195
  throw err;
192
196
  }
193
- const actions = mergeTemplate(cfg.templateDir, projectDir, { dryRun });
194
- return { project, projectDir, actions };
197
+ const actions = mergeTemplate(tmplDir, projectDir, { dryRun });
198
+ return { project, projectDir, from, templateDir: tmplDir, actions };
195
199
  }
196
200
 
197
201
  function linkProject({ repo, name, force = false, dryRun = false } = {}) {
package/lib/config.js CHANGED
@@ -12,6 +12,7 @@ function resolveConfig(env = process.env) {
12
12
  env.AI_MD_REMOTE ||
13
13
  env.CURSOR_MD_REMOTE ||
14
14
  "https://github.com/dujavi/.ai-md.git";
15
+ const templatesDir = path.join(dir, "templates");
15
16
  return {
16
17
  home,
17
18
  dir,
@@ -19,12 +20,27 @@ function resolveConfig(env = process.env) {
19
20
  cursorSkills: path.join(home, ".cursor", "skills"),
20
21
  cursorRules: path.join(home, ".cursor", "rules"),
21
22
  projectsDir: path.join(dir, "projects"),
22
- templateDir: path.join(dir, "projects", "template"),
23
+ templatesDir,
24
+ /** @deprecated use templatePath(cfg, name) */
25
+ templateDir: path.join(templatesDir, "base"),
23
26
  skillsDir: path.join(dir, "skills"),
24
27
  rulesDir: path.join(dir, "rules"),
25
28
  };
26
29
  }
27
30
 
31
+ function templatePath(cfg, name = "base") {
32
+ return path.join(cfg.templatesDir, name);
33
+ }
34
+
35
+ function listTemplates(cfg) {
36
+ if (!fs.existsSync(cfg.templatesDir)) return [];
37
+ return fs
38
+ .readdirSync(cfg.templatesDir, { withFileTypes: true })
39
+ .filter((d) => d.isDirectory() && !d.name.startsWith("."))
40
+ .map((d) => d.name)
41
+ .sort();
42
+ }
43
+
28
44
  function countRules(rulesDir) {
29
45
  if (!fs.existsSync(rulesDir)) return 0;
30
46
  return fs
@@ -88,8 +104,24 @@ function agentSkillTargets(home, agents) {
88
104
  .map((name) => ({ name, path: map[name] }));
89
105
  }
90
106
 
107
+ /** One-time: projects/template → templates/base */
108
+ function migrateLegacyTemplate(cfg, { dryRun = false } = {}) {
109
+ const legacy = path.join(cfg.projectsDir, "template");
110
+ const dest = templatePath(cfg, "base");
111
+ if (!fs.existsSync(legacy) || fs.existsSync(dest)) {
112
+ return { action: "skip" };
113
+ }
114
+ if (dryRun) return { action: "would_migrate", from: legacy, to: dest };
115
+ fs.mkdirSync(cfg.templatesDir, { recursive: true });
116
+ fs.renameSync(legacy, dest);
117
+ return { action: "migrated", from: legacy, to: dest };
118
+ }
119
+
91
120
  module.exports = {
92
121
  resolveConfig,
122
+ templatePath,
123
+ listTemplates,
124
+ migrateLegacyTemplate,
93
125
  countRules,
94
126
  countSkills,
95
127
  listSkillNames,
package/lib/status.js CHANGED
@@ -5,6 +5,9 @@ const path = require("path");
5
5
  const { execFileSync } = require("child_process");
6
6
  const {
7
7
  resolveConfig,
8
+ templatePath,
9
+ listTemplates,
10
+ migrateLegacyTemplate,
8
11
  countRules,
9
12
  countSkills,
10
13
  listSkillNames,
@@ -19,17 +22,17 @@ function git(repo, args) {
19
22
  encoding: "utf8",
20
23
  stdio: ["ignore", "pipe", "pipe"],
21
24
  }).trim();
22
- } catch (err) {
25
+ } catch {
23
26
  return null;
24
27
  }
25
28
  }
26
29
 
27
- function templateDrift(projectDir, templateDir) {
28
- if (!fs.existsSync(templateDir)) {
30
+ function templateDrift(projectDir, tmplDir) {
31
+ if (!fs.existsSync(tmplDir)) {
29
32
  return { missingRules: [], missingSkills: [], extraRules: [], extraSkills: [] };
30
33
  }
31
- const tRules = new Set(listRuleNames(path.join(templateDir, "rules")));
32
- const tSkills = new Set(listSkillNames(path.join(templateDir, "skills")));
34
+ const tRules = new Set(listRuleNames(path.join(tmplDir, "rules")));
35
+ const tSkills = new Set(listSkillNames(path.join(tmplDir, "skills")));
33
36
  const pRules = new Set(listRuleNames(path.join(projectDir, "rules")));
34
37
  const pSkills = new Set(listSkillNames(path.join(projectDir, "skills")));
35
38
  return {
@@ -40,44 +43,38 @@ function templateDrift(projectDir, templateDir) {
40
43
  };
41
44
  }
42
45
 
43
- function collectProjects(cfg, { full = false } = {}) {
46
+ function collectTemplates(cfg, { full = false } = {}) {
47
+ return listTemplates(cfg).map((name) => {
48
+ const dir = templatePath(cfg, name);
49
+ return {
50
+ name,
51
+ rules: countRules(path.join(dir, "rules")),
52
+ skills: countSkills(path.join(dir, "skills")),
53
+ ...(full ? { path: dir } : {}),
54
+ };
55
+ });
56
+ }
57
+
58
+ function collectProjects(cfg, { full = false, from = "base" } = {}) {
44
59
  const projects = [];
60
+ const tmplDir = templatePath(cfg, from);
45
61
  if (!fs.existsSync(cfg.projectsDir)) return projects;
46
62
  for (const name of fs.readdirSync(cfg.projectsDir).sort()) {
47
63
  if (name.startsWith(".")) continue;
64
+ // legacy leftover
65
+ if (name === "template") continue;
48
66
  const projectDir = path.join(cfg.projectsDir, name);
49
67
  if (!fs.statSync(projectDir).isDirectory()) continue;
50
- if (name === "template") {
51
- projects.push({
52
- name,
53
- kind: "template",
54
- rules: countRules(path.join(projectDir, "rules")),
55
- skills: countSkills(path.join(projectDir, "skills")),
56
- missingRules: 0,
57
- missingSkills: 0,
58
- ...(full
59
- ? {
60
- path: projectDir,
61
- drift: templateDrift(projectDir, cfg.templateDir),
62
- }
63
- : {}),
64
- });
65
- continue;
66
- }
67
- const drift = templateDrift(projectDir, cfg.templateDir);
68
+ const drift = templateDrift(projectDir, tmplDir);
68
69
  projects.push({
69
70
  name,
70
71
  kind: "project",
72
+ from,
71
73
  rules: countRules(path.join(projectDir, "rules")),
72
74
  skills: countSkills(path.join(projectDir, "skills")),
73
75
  missingRules: drift.missingRules.length,
74
76
  missingSkills: drift.missingSkills.length,
75
- ...(full
76
- ? {
77
- path: projectDir,
78
- drift,
79
- }
80
- : {}),
77
+ ...(full ? { path: projectDir, drift } : {}),
81
78
  });
82
79
  }
83
80
  return projects;
@@ -87,6 +84,9 @@ function collectStatus(opts = {}) {
87
84
  const cfg = resolveConfig();
88
85
  const full = Boolean(opts.full);
89
86
  const agents = opts.agents || ["cursor"];
87
+ const from = opts.from || "base";
88
+
89
+ const migration = migrateLegacyTemplate(cfg);
90
90
 
91
91
  const exists = fs.existsSync(cfg.dir);
92
92
  const isGit = exists && fs.existsSync(path.join(cfg.dir, ".git"));
@@ -103,9 +103,9 @@ function collectStatus(opts = {}) {
103
103
  ...symlinkState(t.path, cfg.skillsDir),
104
104
  }));
105
105
 
106
- const projects = collectProjects(cfg, { full });
107
- const linkedProjects = projects.filter((p) => p.kind === "project");
108
- const drifting = linkedProjects.filter(
106
+ const templates = collectTemplates(cfg, { full });
107
+ const projects = collectProjects(cfg, { full, from });
108
+ const drifting = projects.filter(
109
109
  (p) => p.missingRules > 0 || p.missingSkills > 0
110
110
  );
111
111
 
@@ -127,6 +127,9 @@ function collectStatus(opts = {}) {
127
127
  for (const l of links) {
128
128
  if (l.state !== "ok") problems.push(`cursor_${path.basename(l.path)}_${l.state}`);
129
129
  }
130
+ if (!fs.existsSync(templatePath(cfg, "base")) && templates.length === 0) {
131
+ problems.push("templates_missing");
132
+ }
130
133
 
131
134
  return {
132
135
  generatedAt: new Date().toISOString(),
@@ -135,10 +138,17 @@ function collectStatus(opts = {}) {
135
138
  branch,
136
139
  dirty,
137
140
  statusLine: aheadBehind,
141
+ layout: {
142
+ system: { rules: cfg.rulesDir, skills: cfg.skillsDir },
143
+ templates: cfg.templatesDir,
144
+ projects: cfg.projectsDir,
145
+ },
146
+ migration,
138
147
  counts: {
139
148
  rules: countRules(cfg.rulesDir),
140
149
  skills: countSkills(cfg.skillsDir),
141
- projects: linkedProjects.length,
150
+ templates: templates.length,
151
+ projects: projects.length,
142
152
  drifting: drifting.length,
143
153
  problems: problems.length,
144
154
  },
@@ -153,18 +163,9 @@ function collectStatus(opts = {}) {
153
163
  state: l.state,
154
164
  ...(full ? { target: l.target, expected: l.expected } : {}),
155
165
  })),
156
- projects: projects.map((p) =>
157
- full
158
- ? p
159
- : {
160
- name: p.name,
161
- kind: p.kind,
162
- rules: p.rules,
163
- skills: p.skills,
164
- missingRules: p.missingRules,
165
- missingSkills: p.missingSkills,
166
- }
167
- ),
166
+ templates,
167
+ projects,
168
+ driftFrom: from,
168
169
  problems,
169
170
  };
170
171
  }
@@ -177,17 +178,20 @@ function statusHelp(data) {
177
178
  if (data.links.some((l) => l.state !== "ok")) {
178
179
  help.push("Run `ai-md doctor --fix` to repair ~/.cursor skills/rules symlinks");
179
180
  }
181
+ if (data.problems.includes("templates_missing")) {
182
+ help.push("Create ~/.ai-md/templates/base with project starters (system skills/rules stay in skills/ and rules/)");
183
+ }
180
184
  if (data.counts.drifting > 0) {
181
185
  help.push(
182
- "Run `ai-md apply-template --project <name>` to merge missing baseline files from projects/template"
186
+ `Run \`ai-md apply-template --project <name> --from ${data.driftFrom || "base"}\` to merge missing template files`
183
187
  );
184
188
  }
185
189
  if (data.dirty) {
186
190
  help.push('Run `ai-md push -m "<why>"` to commit and push private config');
187
191
  } else {
188
- help.push("Run `ai-md pull` before editing global skills/rules");
192
+ help.push("Edit system skills/rules under ~/.ai-md/{skills,rules}; project overlays under projects/");
189
193
  }
190
- help.push("Run `ai-md init-project --repo <path>` to seed a project from template and link .cursor/");
194
+ help.push("Run `ai-md init-project --repo <path> --from base` to seed a project");
191
195
  help.push("Run `ai-md status --json` for machine-readable output");
192
196
  return help;
193
197
  }
@@ -195,6 +199,7 @@ function statusHelp(data) {
195
199
  module.exports = {
196
200
  collectStatus,
197
201
  collectProjects,
202
+ collectTemplates,
198
203
  templateDrift,
199
204
  statusHelp,
200
205
  git,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dujavi/ai-md",
3
- "version": "0.2.0",
4
- "description": "AXI-shaped CLI for private ~/.ai-md skills/rules, project templates, and agent tool bootstrap",
3
+ "version": "0.3.0",
4
+ "description": "AXI-shaped CLI for ~/.ai-md system skills/rules, templates/, and projects/",
5
5
  "bin": {
6
6
  "ai-md": "bin/ai-md.js"
7
7
  },