@dujavi/ai-md 0.5.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/README.md +47 -107
- package/bin/ai-md.js +344 -186
- package/lib/build.js +268 -0
- package/lib/commands.js +292 -99
- package/lib/config-paths.js +73 -0
- package/lib/config.js +85 -57
- package/lib/harnesses/index.js +309 -0
- package/lib/link.js +224 -0
- package/lib/rescue.js +95 -0
- package/lib/skeleton.js +117 -0
- package/lib/status.js +110 -35
- package/package.json +4 -3
- package/skeleton/README.md +3 -0
- package/skeleton/agents/agents/skills/.gitkeep +0 -0
- package/skeleton/agents/claude/rules/.gitkeep +0 -0
- package/skeleton/agents/claude/skills/.gitkeep +0 -0
- package/skeleton/agents/cursor/skills/.gitkeep +0 -0
- package/skeleton/agents/gemini/skills/.gitkeep +0 -0
- package/skeleton/agents/opencode/skills/.gitkeep +0 -0
- package/skeleton/projects/.gitkeep +0 -0
- package/skeleton/scripts/.gitkeep +0 -0
- package/skeleton/shared/rules/edit-source-not-dist.mdc +23 -0
- package/skeleton/shared/rules/personal-config.mdc +49 -0
- package/skeleton/shared/skills/ai-md-config/SKILL.md +51 -0
- package/skeleton/templates/base/rules/.gitkeep +0 -0
- package/skeleton/templates/base/rules/project-context.mdc +8 -0
- package/skeleton/templates/base/skills/.gitkeep +0 -0
package/lib/config.js
CHANGED
|
@@ -1,22 +1,17 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const fs = require("fs");
|
|
4
|
-
const os = require("os");
|
|
5
4
|
const path = require("path");
|
|
5
|
+
const { execFileSync } = require("child_process");
|
|
6
|
+
const { expandHome } = require("./config-paths");
|
|
6
7
|
|
|
7
8
|
const DEFAULT_REMOTE = "https://github.com/dujavi/.ai-md.git";
|
|
8
9
|
|
|
9
|
-
function expandHome(p) {
|
|
10
|
-
if (!p) return p;
|
|
11
|
-
if (p === "~") return os.homedir();
|
|
12
|
-
if (p.startsWith("~/")) return path.join(os.homedir(), p.slice(2));
|
|
13
|
-
return p;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/** Machine-local config (not inside the private content repo). */
|
|
17
10
|
function machineConfigPath(env = process.env) {
|
|
18
11
|
if (env.AI_MD_CONFIG) return expandHome(env.AI_MD_CONFIG);
|
|
19
|
-
const xdg =
|
|
12
|
+
const xdg =
|
|
13
|
+
env.XDG_CONFIG_HOME ||
|
|
14
|
+
path.join(require("os").homedir(), ".config");
|
|
20
15
|
return path.join(xdg, "ai-md", "config.json");
|
|
21
16
|
}
|
|
22
17
|
|
|
@@ -36,20 +31,16 @@ function readMachineConfig(env = process.env) {
|
|
|
36
31
|
}
|
|
37
32
|
}
|
|
38
33
|
|
|
39
|
-
function writeMachineConfig(
|
|
34
|
+
function writeMachineConfig(patch, env = process.env, { dryRun = false } = {}) {
|
|
40
35
|
const configPath = machineConfigPath(env);
|
|
41
36
|
const existing = readMachineConfig(env).raw || {};
|
|
42
37
|
const next = {
|
|
43
38
|
...existing,
|
|
44
|
-
...
|
|
45
|
-
...(remote != null ? { remote: String(remote) } : {}),
|
|
39
|
+
...patch,
|
|
46
40
|
updatedAt: new Date().toISOString(),
|
|
47
41
|
};
|
|
48
|
-
if (
|
|
49
|
-
|
|
50
|
-
err.code = "EINVAL";
|
|
51
|
-
throw err;
|
|
52
|
-
}
|
|
42
|
+
if (patch.dir != null) next.dir = expandHome(patch.dir);
|
|
43
|
+
if (patch.remote != null) next.remote = String(patch.remote);
|
|
53
44
|
if (dryRun) {
|
|
54
45
|
return { action: "would_write", path: configPath, config: next };
|
|
55
46
|
}
|
|
@@ -65,11 +56,9 @@ function writeMachineConfig({ dir, remote }, env = process.env, { dryRun = false
|
|
|
65
56
|
return { action: "wrote", path: configPath, config: next };
|
|
66
57
|
}
|
|
67
58
|
|
|
68
|
-
/**
|
|
69
|
-
* Precedence: explicit opts > env > machine config file > defaults.
|
|
70
|
-
* @param {{ dir?: string, remote?: string }} [opts]
|
|
71
|
-
*/
|
|
72
59
|
function resolveConfig(env = process.env, opts = {}) {
|
|
60
|
+
const os = require("os");
|
|
61
|
+
const { defaultLinkMode } = require("./config-paths");
|
|
73
62
|
const home = os.homedir();
|
|
74
63
|
const stored = readMachineConfig(env);
|
|
75
64
|
const defaultDir = path.join(home, ".ai-md");
|
|
@@ -77,11 +66,8 @@ function resolveConfig(env = process.env, opts = {}) {
|
|
|
77
66
|
const envDir = env.AI_MD_DIR || env.CURSOR_MD_DIR || null;
|
|
78
67
|
const envRemote = env.AI_MD_REMOTE || env.CURSOR_MD_REMOTE || null;
|
|
79
68
|
|
|
80
|
-
const dir = expandHome(
|
|
81
|
-
|
|
82
|
-
);
|
|
83
|
-
const remote =
|
|
84
|
-
opts.remote || envRemote || stored.remote || DEFAULT_REMOTE;
|
|
69
|
+
const dir = expandHome(opts.dir || envDir || stored.dir || defaultDir);
|
|
70
|
+
const remote = opts.remote || envRemote || stored.remote || DEFAULT_REMOTE;
|
|
85
71
|
|
|
86
72
|
const sources = {
|
|
87
73
|
dir: opts.dir
|
|
@@ -100,21 +86,33 @@ function resolveConfig(env = process.env, opts = {}) {
|
|
|
100
86
|
: "default",
|
|
101
87
|
};
|
|
102
88
|
|
|
89
|
+
const raw = stored.raw || {};
|
|
90
|
+
const agents = Array.isArray(raw.agents) && raw.agents.length
|
|
91
|
+
? raw.agents.map((a) => String(a).toLowerCase())
|
|
92
|
+
: ["cursor"];
|
|
93
|
+
const linkMode = opts.linkMode || raw.linkMode || defaultLinkMode();
|
|
94
|
+
|
|
103
95
|
const templatesDir = path.join(dir, "templates");
|
|
104
96
|
return {
|
|
105
97
|
home,
|
|
106
98
|
dir,
|
|
107
99
|
remote,
|
|
108
100
|
sources,
|
|
101
|
+
agents,
|
|
102
|
+
linkMode,
|
|
109
103
|
machineConfigPath: stored.path,
|
|
110
|
-
machineConfig:
|
|
104
|
+
machineConfig: raw,
|
|
111
105
|
cursorSkills: path.join(home, ".cursor", "skills"),
|
|
112
106
|
cursorRules: path.join(home, ".cursor", "rules"),
|
|
113
107
|
projectsDir: path.join(dir, "projects"),
|
|
114
108
|
templatesDir,
|
|
115
109
|
templateDir: path.join(templatesDir, "base"),
|
|
116
|
-
|
|
117
|
-
|
|
110
|
+
// legacy aliases → shared (build uses shared/)
|
|
111
|
+
skillsDir: path.join(dir, "shared", "skills"),
|
|
112
|
+
rulesDir: path.join(dir, "shared", "rules"),
|
|
113
|
+
sharedDir: path.join(dir, "shared"),
|
|
114
|
+
agentsDir: path.join(dir, "agents"),
|
|
115
|
+
distDir: path.join(dir, "dist"),
|
|
118
116
|
};
|
|
119
117
|
}
|
|
120
118
|
|
|
@@ -142,16 +140,14 @@ function countRules(rulesDir) {
|
|
|
142
140
|
if (!fs.existsSync(rulesDir)) return 0;
|
|
143
141
|
return fs
|
|
144
142
|
.readdirSync(rulesDir)
|
|
145
|
-
.filter((f) => f.endsWith(".mdc") || f.endsWith(".md"))
|
|
146
|
-
.length;
|
|
143
|
+
.filter((f) => f.endsWith(".mdc") || f.endsWith(".md")).length;
|
|
147
144
|
}
|
|
148
145
|
|
|
149
146
|
function countSkills(skillsDir) {
|
|
150
147
|
if (!fs.existsSync(skillsDir)) return 0;
|
|
151
148
|
return fs
|
|
152
149
|
.readdirSync(skillsDir, { withFileTypes: true })
|
|
153
|
-
.filter((d) => d.isDirectory() && !d.name.startsWith("."))
|
|
154
|
-
.length;
|
|
150
|
+
.filter((d) => d.isDirectory() && !d.name.startsWith(".")).length;
|
|
155
151
|
}
|
|
156
152
|
|
|
157
153
|
function listSkillNames(skillsDir) {
|
|
@@ -172,33 +168,24 @@ function listRuleNames(rulesDir) {
|
|
|
172
168
|
}
|
|
173
169
|
|
|
174
170
|
function symlinkState(linkPath, expected) {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
stat = fs.lstatSync(linkPath);
|
|
178
|
-
} catch {
|
|
179
|
-
return { path: linkPath, state: "missing", target: null, expected };
|
|
180
|
-
}
|
|
181
|
-
if (!stat.isSymbolicLink()) {
|
|
182
|
-
return { path: linkPath, state: "not_symlink", target: null, expected };
|
|
183
|
-
}
|
|
184
|
-
const target = fs.readlinkSync(linkPath);
|
|
185
|
-
if (target === expected) {
|
|
186
|
-
return { path: linkPath, state: "ok", target, expected };
|
|
187
|
-
}
|
|
188
|
-
return { path: linkPath, state: "wrong_target", target, expected };
|
|
171
|
+
const { linkState } = require("./link");
|
|
172
|
+
return linkState(linkPath, expected);
|
|
189
173
|
}
|
|
190
174
|
|
|
175
|
+
/** @deprecated use harnesses.selectHarnesses */
|
|
191
176
|
function agentSkillTargets(home, agents) {
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
claude: path.join(home, ".claude", "skills"),
|
|
195
|
-
agents: path.join(home, ".agents", "skills"),
|
|
196
|
-
};
|
|
177
|
+
const { resolveHarness } = require("./harnesses");
|
|
178
|
+
const cfg = { home, machineConfig: {} };
|
|
197
179
|
return (agents || [])
|
|
198
|
-
.map((a) =>
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
180
|
+
.map((a) => {
|
|
181
|
+
try {
|
|
182
|
+
const h = resolveHarness(a, { ...cfg, home });
|
|
183
|
+
return h.skills ? { name: h.id, path: h.skills } : null;
|
|
184
|
+
} catch {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
})
|
|
188
|
+
.filter(Boolean);
|
|
202
189
|
}
|
|
203
190
|
|
|
204
191
|
function migrateLegacyTemplate(cfg, { dryRun = false } = {}) {
|
|
@@ -213,6 +200,44 @@ function migrateLegacyTemplate(cfg, { dryRun = false } = {}) {
|
|
|
213
200
|
return { action: "migrated", from: legacy, to: dest };
|
|
214
201
|
}
|
|
215
202
|
|
|
203
|
+
function gitClone(remote, dir, { dryRun = false } = {}) {
|
|
204
|
+
if (dryRun) return { action: "would_clone", remote, dir };
|
|
205
|
+
fs.mkdirSync(path.dirname(dir), { recursive: true });
|
|
206
|
+
execFileSync("git", ["clone", remote, dir], { stdio: "inherit" });
|
|
207
|
+
return { action: "cloned", remote, dir };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function gitPull(dir, { dryRun = false } = {}) {
|
|
211
|
+
if (dryRun) return { action: "would_pull", dir };
|
|
212
|
+
execFileSync("git", ["-C", dir, "pull", "--rebase", "--autostash"], {
|
|
213
|
+
stdio: "inherit",
|
|
214
|
+
});
|
|
215
|
+
return { action: "pulled", dir };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function gitPush(dir, { message, dryRun = false } = {}) {
|
|
219
|
+
if (dryRun) return { action: "would_push", dir, message };
|
|
220
|
+
execFileSync("git", ["-C", dir, "add", "-A"], { stdio: "inherit" });
|
|
221
|
+
let dirty = true;
|
|
222
|
+
try {
|
|
223
|
+
execFileSync("git", ["-C", dir, "diff", "--cached", "--quiet"], {
|
|
224
|
+
stdio: "pipe",
|
|
225
|
+
});
|
|
226
|
+
dirty = false;
|
|
227
|
+
} catch {
|
|
228
|
+
dirty = true;
|
|
229
|
+
}
|
|
230
|
+
if (dirty) {
|
|
231
|
+
execFileSync(
|
|
232
|
+
"git",
|
|
233
|
+
["-C", dir, "commit", "-m", message || "Update personal AI skills/rules"],
|
|
234
|
+
{ stdio: "inherit" }
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
execFileSync("git", ["-C", dir, "push"], { stdio: "inherit" });
|
|
238
|
+
return { action: "pushed", dir, committed: dirty };
|
|
239
|
+
}
|
|
240
|
+
|
|
216
241
|
module.exports = {
|
|
217
242
|
DEFAULT_REMOTE,
|
|
218
243
|
machineConfigPath,
|
|
@@ -230,4 +255,7 @@ module.exports = {
|
|
|
230
255
|
listRuleNames,
|
|
231
256
|
symlinkState,
|
|
232
257
|
agentSkillTargets,
|
|
258
|
+
gitClone,
|
|
259
|
+
gitPull,
|
|
260
|
+
gitPush,
|
|
233
261
|
};
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const os = require("os");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const { expandHome } = require("../config-paths");
|
|
7
|
+
|
|
8
|
+
/** Built-in harness registry. codex aliases agents (shared ~/.agents/skills). */
|
|
9
|
+
const BUILTINS = {
|
|
10
|
+
cursor: {
|
|
11
|
+
id: "cursor",
|
|
12
|
+
tier: "A",
|
|
13
|
+
format: "mdc",
|
|
14
|
+
emitter: "full",
|
|
15
|
+
skills: "~/.cursor/skills",
|
|
16
|
+
rules: "~/.cursor/rules",
|
|
17
|
+
unique: true,
|
|
18
|
+
aliasOf: null,
|
|
19
|
+
detect: { dirs: ["~/.cursor"], bins: ["cursor", "agent"] },
|
|
20
|
+
},
|
|
21
|
+
claude: {
|
|
22
|
+
id: "claude",
|
|
23
|
+
tier: "A",
|
|
24
|
+
format: "md",
|
|
25
|
+
emitter: "full",
|
|
26
|
+
skills: "~/.claude/skills",
|
|
27
|
+
rules: "~/.claude/rules",
|
|
28
|
+
unique: true,
|
|
29
|
+
aliasOf: null,
|
|
30
|
+
detect: { dirs: ["~/.claude"], bins: ["claude"] },
|
|
31
|
+
},
|
|
32
|
+
agents: {
|
|
33
|
+
id: "agents",
|
|
34
|
+
tier: "A",
|
|
35
|
+
format: "skills-only",
|
|
36
|
+
emitter: "full",
|
|
37
|
+
skills: "~/.agents/skills",
|
|
38
|
+
rules: null,
|
|
39
|
+
unique: false,
|
|
40
|
+
aliasOf: null,
|
|
41
|
+
detect: { dirs: ["~/.agents", "~/.codex"], bins: ["codex"] },
|
|
42
|
+
},
|
|
43
|
+
codex: {
|
|
44
|
+
id: "codex",
|
|
45
|
+
tier: "A",
|
|
46
|
+
format: "skills-only",
|
|
47
|
+
emitter: "full",
|
|
48
|
+
skills: "~/.agents/skills",
|
|
49
|
+
rules: null,
|
|
50
|
+
unique: false,
|
|
51
|
+
aliasOf: "agents",
|
|
52
|
+
detect: { dirs: ["~/.codex", "~/.agents"], bins: ["codex"] },
|
|
53
|
+
},
|
|
54
|
+
gemini: {
|
|
55
|
+
id: "gemini",
|
|
56
|
+
tier: "A",
|
|
57
|
+
format: "skills-only",
|
|
58
|
+
emitter: "full",
|
|
59
|
+
skills: "~/.gemini/skills",
|
|
60
|
+
rules: null,
|
|
61
|
+
unique: true,
|
|
62
|
+
aliasOf: null,
|
|
63
|
+
detect: { dirs: ["~/.gemini"], bins: ["gemini"] },
|
|
64
|
+
},
|
|
65
|
+
opencode: {
|
|
66
|
+
id: "opencode",
|
|
67
|
+
tier: "A",
|
|
68
|
+
format: "skills-only",
|
|
69
|
+
emitter: "full",
|
|
70
|
+
skills: "~/.config/opencode/skills",
|
|
71
|
+
rules: null,
|
|
72
|
+
unique: true,
|
|
73
|
+
aliasOf: null,
|
|
74
|
+
detect: { dirs: ["~/.config/opencode"], bins: ["opencode"] },
|
|
75
|
+
},
|
|
76
|
+
copilot: {
|
|
77
|
+
id: "copilot",
|
|
78
|
+
tier: "B",
|
|
79
|
+
format: "skills-only",
|
|
80
|
+
emitter: "stub",
|
|
81
|
+
skills: "~/.copilot/skills",
|
|
82
|
+
rules: null,
|
|
83
|
+
unique: true,
|
|
84
|
+
aliasOf: null,
|
|
85
|
+
detect: { dirs: ["~/.copilot"], bins: ["copilot"] },
|
|
86
|
+
},
|
|
87
|
+
windsurf: {
|
|
88
|
+
id: "windsurf",
|
|
89
|
+
tier: "B",
|
|
90
|
+
format: "skills-only",
|
|
91
|
+
emitter: "stub",
|
|
92
|
+
skills: null,
|
|
93
|
+
rules: null,
|
|
94
|
+
unique: true,
|
|
95
|
+
aliasOf: null,
|
|
96
|
+
detect: { dirs: ["~/.codeium"], bins: ["windsurf"] },
|
|
97
|
+
},
|
|
98
|
+
grok: {
|
|
99
|
+
id: "grok",
|
|
100
|
+
tier: "B",
|
|
101
|
+
format: "skills-only",
|
|
102
|
+
emitter: "stub",
|
|
103
|
+
skills: null,
|
|
104
|
+
rules: null,
|
|
105
|
+
unique: true,
|
|
106
|
+
aliasOf: null,
|
|
107
|
+
detect: { dirs: ["~/.grok"], bins: ["grok"] },
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
function which(bin) {
|
|
112
|
+
const pathEnv = process.env.PATH || "";
|
|
113
|
+
const sep = process.platform === "win32" ? ";" : ":";
|
|
114
|
+
const exts =
|
|
115
|
+
process.platform === "win32"
|
|
116
|
+
? (process.env.PATHEXT || ".EXE;.CMD;.BAT").split(";").filter(Boolean)
|
|
117
|
+
: [""];
|
|
118
|
+
for (const dir of pathEnv.split(sep)) {
|
|
119
|
+
if (!dir) continue;
|
|
120
|
+
for (const ext of exts) {
|
|
121
|
+
const candidate = path.join(dir, bin + ext);
|
|
122
|
+
try {
|
|
123
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
124
|
+
return candidate;
|
|
125
|
+
} catch {
|
|
126
|
+
/* try next */
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function dirExists(p) {
|
|
134
|
+
try {
|
|
135
|
+
return fs.statSync(expandHome(p)).isDirectory();
|
|
136
|
+
} catch {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* True if the harness looks installed on this machine.
|
|
143
|
+
* Custom harnesses: installed if any configured live parent exists, or always if --force-link.
|
|
144
|
+
*/
|
|
145
|
+
function isHarnessInstalled(def, { forceLink = false } = {}) {
|
|
146
|
+
if (forceLink) return true;
|
|
147
|
+
if (!def) return false;
|
|
148
|
+
if (def.detect) {
|
|
149
|
+
for (const d of def.detect.dirs || []) {
|
|
150
|
+
if (dirExists(d)) return true;
|
|
151
|
+
}
|
|
152
|
+
for (const b of def.detect.bins || []) {
|
|
153
|
+
if (which(b)) return true;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// Custom / override: consider installed if skills or rules parent exists
|
|
157
|
+
if (def.skills && dirExists(path.dirname(expandHome(def.skills)))) return true;
|
|
158
|
+
if (def.rules && dirExists(path.dirname(expandHome(def.rules)))) return true;
|
|
159
|
+
// Built-in with no detect hit
|
|
160
|
+
if (BUILTINS[def.id] && def.emitter === "stub" && !def.skills) return false;
|
|
161
|
+
if (!BUILTINS[def.id] && (def.skills || def.rules)) {
|
|
162
|
+
// user-registered: require parent dir or force
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function getBuiltin(id) {
|
|
169
|
+
return BUILTINS[String(id).toLowerCase()] || null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function listBuiltinIds() {
|
|
173
|
+
return Object.keys(BUILTINS).sort();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Merge built-in + machine config harnesses.<id> overrides.
|
|
178
|
+
* @returns {object} resolved harness definition
|
|
179
|
+
*/
|
|
180
|
+
function resolveHarness(id, cfg, oneShot = {}) {
|
|
181
|
+
const key = String(id).toLowerCase();
|
|
182
|
+
const builtin = getBuiltin(key);
|
|
183
|
+
const overrides = (cfg.machineConfig && cfg.machineConfig.harnesses) || {};
|
|
184
|
+
const over = overrides[key] || {};
|
|
185
|
+
|
|
186
|
+
const base = builtin
|
|
187
|
+
? { ...builtin }
|
|
188
|
+
: {
|
|
189
|
+
id: key,
|
|
190
|
+
tier: "C",
|
|
191
|
+
format: "skills-only",
|
|
192
|
+
emitter: "full",
|
|
193
|
+
skills: null,
|
|
194
|
+
rules: null,
|
|
195
|
+
unique: true,
|
|
196
|
+
aliasOf: null,
|
|
197
|
+
detect: null,
|
|
198
|
+
custom: true,
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const skills =
|
|
202
|
+
oneShot.skills ||
|
|
203
|
+
over.skills ||
|
|
204
|
+
base.skills;
|
|
205
|
+
const rules =
|
|
206
|
+
oneShot.rules !== undefined
|
|
207
|
+
? oneShot.rules
|
|
208
|
+
: over.rules !== undefined
|
|
209
|
+
? over.rules
|
|
210
|
+
: base.rules;
|
|
211
|
+
const format =
|
|
212
|
+
oneShot.format ||
|
|
213
|
+
over.format ||
|
|
214
|
+
base.format ||
|
|
215
|
+
(skills && rules ? "md" : "skills-only");
|
|
216
|
+
|
|
217
|
+
const enabled =
|
|
218
|
+
over.enabled !== undefined
|
|
219
|
+
? Boolean(over.enabled)
|
|
220
|
+
: builtin
|
|
221
|
+
? true
|
|
222
|
+
: true;
|
|
223
|
+
|
|
224
|
+
const distId = base.aliasOf || key;
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
...base,
|
|
228
|
+
id: key,
|
|
229
|
+
skills: skills ? expandHome(skills) : null,
|
|
230
|
+
rules: rules ? expandHome(rules) : null,
|
|
231
|
+
skillsRaw: skills || null,
|
|
232
|
+
rulesRaw: rules || null,
|
|
233
|
+
format,
|
|
234
|
+
enabled,
|
|
235
|
+
distId,
|
|
236
|
+
override: Boolean(over.skills || over.rules || over.format || over.enabled !== undefined),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Expand enabled agent list: drop aliases that duplicate canonical, warn.
|
|
242
|
+
* Filters to installed harnesses unless forceLink.
|
|
243
|
+
*/
|
|
244
|
+
function selectHarnesses(cfg, agentIds, { forceLink = false, includeUninstalled = false } = {}) {
|
|
245
|
+
const requested = (agentIds && agentIds.length ? agentIds : cfg.agents || ["cursor"]).map(
|
|
246
|
+
(a) => String(a).trim().toLowerCase()
|
|
247
|
+
);
|
|
248
|
+
const seenLive = new Map(); // live skills path -> id
|
|
249
|
+
const selected = [];
|
|
250
|
+
const skipped = [];
|
|
251
|
+
const warnings = [];
|
|
252
|
+
|
|
253
|
+
for (const id of requested) {
|
|
254
|
+
let def;
|
|
255
|
+
try {
|
|
256
|
+
def = resolveHarness(id, cfg);
|
|
257
|
+
} catch (e) {
|
|
258
|
+
skipped.push({ id, reason: e.message });
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (!def.enabled) {
|
|
262
|
+
skipped.push({ id, reason: "disabled" });
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (def.emitter === "stub" && !def.skills && !def.rules) {
|
|
266
|
+
skipped.push({ id, reason: "stub_no_paths" });
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (!includeUninstalled && !isHarnessInstalled(def, { forceLink })) {
|
|
270
|
+
skipped.push({ id, reason: "not_installed" });
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if (def.aliasOf) {
|
|
274
|
+
warnings.push(`${id} is an alias of ${def.aliasOf}; using dist/${def.distId}`);
|
|
275
|
+
}
|
|
276
|
+
const liveKey = def.skills || def.rules || def.id;
|
|
277
|
+
if (seenLive.has(liveKey)) {
|
|
278
|
+
warnings.push(
|
|
279
|
+
`skip ${id}: shares live path with ${seenLive.get(liveKey)} (${liveKey})`
|
|
280
|
+
);
|
|
281
|
+
skipped.push({ id, reason: "shared_live_path", with: seenLive.get(liveKey) });
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
seenLive.set(liveKey, def.distId);
|
|
285
|
+
selected.push(def);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return { selected, skipped, warnings };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function ensureAgentSourceDirs(cfg, harnessId) {
|
|
292
|
+
const id = harnessId === "codex" ? "agents" : harnessId;
|
|
293
|
+
const root = path.join(cfg.dir, "agents", id);
|
|
294
|
+
fs.mkdirSync(path.join(root, "rules"), { recursive: true });
|
|
295
|
+
fs.mkdirSync(path.join(root, "skills"), { recursive: true });
|
|
296
|
+
return root;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
module.exports = {
|
|
300
|
+
BUILTINS,
|
|
301
|
+
getBuiltin,
|
|
302
|
+
listBuiltinIds,
|
|
303
|
+
resolveHarness,
|
|
304
|
+
selectHarnesses,
|
|
305
|
+
isHarnessInstalled,
|
|
306
|
+
ensureAgentSourceDirs,
|
|
307
|
+
which,
|
|
308
|
+
dirExists,
|
|
309
|
+
};
|