create-academic-research 0.1.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/LICENSE +21 -0
- package/README.md +160 -0
- package/SECURITY.md +8 -0
- package/dist/bin/academic-research.d.ts +2 -0
- package/dist/bin/academic-research.js +3 -0
- package/dist/bin/create-academic-research.d.ts +2 -0
- package/dist/bin/create-academic-research.js +3 -0
- package/dist/src/capabilities.d.ts +54 -0
- package/dist/src/capabilities.js +487 -0
- package/dist/src/cli.d.ts +3 -0
- package/dist/src/cli.js +451 -0
- package/dist/src/files.d.ts +8 -0
- package/dist/src/files.js +44 -0
- package/dist/src/names.d.ts +3 -0
- package/dist/src/names.js +64 -0
- package/dist/src/project.d.ts +28 -0
- package/dist/src/project.js +229 -0
- package/dist/src/prompts.d.ts +18 -0
- package/dist/src/prompts.js +29 -0
- package/dist/src/runner.d.ts +12 -0
- package/dist/src/runner.js +22 -0
- package/dist/src/stack.d.ts +30 -0
- package/dist/src/stack.js +180 -0
- package/package.json +59 -0
- package/template/AGENTS.md +47 -0
- package/template/README.md +68 -0
- package/template/analysis_outputs/.gitkeep +0 -0
- package/template/artifacts/artifact-checklist.md +7 -0
- package/template/artifacts/cache/.gitkeep +0 -0
- package/template/artifacts/data/.gitkeep +0 -0
- package/template/artifacts/models/.gitkeep +0 -0
- package/template/artifacts/releases/.gitkeep +0 -0
- package/template/configs/agent-stack.yaml +5 -0
- package/template/configs/capabilities.yaml +4 -0
- package/template/configs/default.yaml +15 -0
- package/template/data/external/.gitkeep +0 -0
- package/template/data/interim/.gitkeep +0 -0
- package/template/data/processed/.gitkeep +0 -0
- package/template/data/raw/.gitkeep +0 -0
- package/template/debug_outputs/.gitkeep +0 -0
- package/template/docs/agent/capability-profile.md +6 -0
- package/template/docs/agent/mcp-setup.md +4 -0
- package/template/docs/agent/output-contracts.md +8 -0
- package/template/docs/agent/research-program.md +3 -0
- package/template/docs/data_dictionary/README.md +3 -0
- package/template/docs/ethics/data-governance.md +3 -0
- package/template/docs/methodology/evaluation-plan.md +3 -0
- package/template/docs/methodology/research-design.md +4 -0
- package/template/docs/methodology/threats-to-validity.md +3 -0
- package/template/docs/reproducibility/README.md +3 -0
- package/template/docs/venue/venue-strategy.md +3 -0
- package/template/experiments/registry.csv +1 -0
- package/template/experiments/templates/experiment-record.md +23 -0
- package/template/explore_outputs/.gitkeep +0 -0
- package/template/notebooks/README.md +5 -0
- package/template/outputs/figures/.gitkeep +0 -0
- package/template/outputs/models/.gitkeep +0 -0
- package/template/outputs/tables/.gitkeep +0 -0
- package/template/package.json +17 -0
- package/template/pyproject.toml +41 -0
- package/template/reports/paper/.gitkeep +0 -0
- package/template/reports/proposal/.gitkeep +0 -0
- package/template/reports/rebuttal/README.md +3 -0
- package/template/reports/reviews/README.md +3 -0
- package/template/reports/slides/.gitkeep +0 -0
- package/template/repro_outputs/.gitkeep +0 -0
- package/template/sota/gaps.md +9 -0
- package/template/sota/literature-matrix.csv +1 -0
- package/template/sota/prisma-flow.md +4 -0
- package/template/sota/screening-decisions.csv +1 -0
- package/template/sota/search-strategy.md +14 -0
- package/template/sota/synthesis.md +9 -0
- package/template/sources/assets/.gitkeep +0 -0
- package/template/sources/bib/citation-audit.csv +1 -0
- package/template/sources/bib/references.bib +1 -0
- package/template/sources/conversion-ledger.csv +1 -0
- package/template/sources/markdown/.gitkeep +0 -0
- package/template/sources/metadata/.gitkeep +0 -0
- package/template/sources/pdfs/.gitkeep +0 -0
- package/template/sources/source-ledger.csv +1 -0
- package/template/src/project_package/__init__.py +1 -0
- package/template/tests/test_project_structure.py +25 -0
- package/template/train_outputs/.gitkeep +0 -0
- package/template/wiki/claims/.gitkeep +0 -0
- package/template/wiki/concepts/.gitkeep +0 -0
- package/template/wiki/contradictions.md +3 -0
- package/template/wiki/decisions/.gitkeep +0 -0
- package/template/wiki/experiments/.gitkeep +0 -0
- package/template/wiki/index.md +9 -0
- package/template/wiki/log.md +1 -0
- package/template/wiki/methods/.gitkeep +0 -0
- package/template/wiki/open_questions.md +3 -0
- package/template/wiki/questions/.gitkeep +0 -0
- package/template/wiki/sources/.gitkeep +0 -0
- package/template/wiki/synthesis.md +3 -0
- package/template/wiki/templates/.gitkeep +0 -0
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
import { appendFile, mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join, relative } from "node:path";
|
|
3
|
+
import YAML from "yaml";
|
|
4
|
+
import { defaultRunner } from "./runner.js";
|
|
5
|
+
import { AGENT_STACK, presetMcpServers } from "./stack.js";
|
|
6
|
+
export const DEFAULT_AGENT = "auto";
|
|
7
|
+
export async function readCapabilities(root) {
|
|
8
|
+
try {
|
|
9
|
+
return readCapabilitiesFile(root);
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
if (isMissingFileError(error)) {
|
|
13
|
+
return defaultCapabilities();
|
|
14
|
+
}
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export async function writeCapabilities(root, state) {
|
|
19
|
+
const next = {
|
|
20
|
+
agent: normalizeAgent(state.agent),
|
|
21
|
+
preset: state.preset ?? "default",
|
|
22
|
+
scope: "project-local",
|
|
23
|
+
mcp_servers: [...(state.mcp_servers ?? [])]
|
|
24
|
+
};
|
|
25
|
+
await writeFile(join(root, "configs/capabilities.yaml"), YAML.stringify(next), "utf8");
|
|
26
|
+
await writeCapabilityProfile(root, next);
|
|
27
|
+
await writeMcpSnippet(root, next);
|
|
28
|
+
await appendCapabilityLog(root, next);
|
|
29
|
+
}
|
|
30
|
+
export async function initializeCapabilities(root, options = {}) {
|
|
31
|
+
const preset = options.preset ?? "default";
|
|
32
|
+
const mcpServers = options.mcpServers ?? presetMcpServers(preset);
|
|
33
|
+
await writeCapabilities(root, {
|
|
34
|
+
agent: options.agent,
|
|
35
|
+
preset,
|
|
36
|
+
mcp_servers: mcpServers
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
export async function buildSkillInstallCommands(root, preset = "default", options = {}) {
|
|
40
|
+
const selected = AGENT_STACK.presets[preset];
|
|
41
|
+
if (!selected)
|
|
42
|
+
throw new Error(`unknown skill preset: ${preset}`);
|
|
43
|
+
const state = await readCapabilities(root);
|
|
44
|
+
const agent = normalizeAgent(options.agent ?? state.agent);
|
|
45
|
+
const commands = [];
|
|
46
|
+
for (const bundleName of selected.skill_bundles) {
|
|
47
|
+
const bundle = AGENT_STACK.skill_bundles[bundleName];
|
|
48
|
+
if (!bundle)
|
|
49
|
+
throw new Error(`unknown skill bundle: ${bundleName}`);
|
|
50
|
+
for (const rawCommand of bundle.commands) {
|
|
51
|
+
const command = splitCommand(renderSkillCommand(rawCommand, agent));
|
|
52
|
+
const globalFlag = `--${"global"}`;
|
|
53
|
+
if (command.includes(globalFlag) || command.includes("-g")) {
|
|
54
|
+
throw new Error(`skill command is not project-local: ${rawCommand}`);
|
|
55
|
+
}
|
|
56
|
+
commands.push(command);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return commands;
|
|
60
|
+
}
|
|
61
|
+
export async function installSkills(root, preset = "default", options = {}, runner = defaultRunner) {
|
|
62
|
+
const state = await readCapabilities(root);
|
|
63
|
+
const agent = normalizeAgent(options.agent ?? state.agent);
|
|
64
|
+
const commands = await buildSkillInstallCommands(root, preset, options);
|
|
65
|
+
for (const command of commands) {
|
|
66
|
+
await runner.run(command, { cwd: root });
|
|
67
|
+
}
|
|
68
|
+
if (state.preset !== preset || state.agent !== agent) {
|
|
69
|
+
await writeCapabilities(root, {
|
|
70
|
+
...state,
|
|
71
|
+
agent,
|
|
72
|
+
preset
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return { ok: true, count: commands.length };
|
|
76
|
+
}
|
|
77
|
+
export async function listInstalledSkills(root) {
|
|
78
|
+
const roots = await discoverProjectSkillRoots(root);
|
|
79
|
+
const skills = [];
|
|
80
|
+
for (const skillsRoot of roots) {
|
|
81
|
+
let entries;
|
|
82
|
+
try {
|
|
83
|
+
entries = await readdir(skillsRoot.absolute, { withFileTypes: true });
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
for (const entry of entries) {
|
|
89
|
+
if (!entry.isDirectory())
|
|
90
|
+
continue;
|
|
91
|
+
try {
|
|
92
|
+
await readFile(join(skillsRoot.absolute, entry.name, "SKILL.md"), "utf8");
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
skills.push({
|
|
98
|
+
name: entry.name,
|
|
99
|
+
path: `${skillsRoot.relative}/${entry.name}`,
|
|
100
|
+
root: skillsRoot.relative
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return skills.sort((left, right) => {
|
|
105
|
+
const byName = left.name.localeCompare(right.name);
|
|
106
|
+
return byName === 0 ? left.path.localeCompare(right.path) : byName;
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
export async function removeSkills(root, skills, runner = defaultRunner) {
|
|
110
|
+
if (skills.length === 0)
|
|
111
|
+
throw new Error("no skills selected");
|
|
112
|
+
await runner.run([
|
|
113
|
+
"npm",
|
|
114
|
+
"exec",
|
|
115
|
+
"--yes",
|
|
116
|
+
"--package",
|
|
117
|
+
"skills",
|
|
118
|
+
"--",
|
|
119
|
+
"skills",
|
|
120
|
+
"remove",
|
|
121
|
+
...skills,
|
|
122
|
+
"-y"
|
|
123
|
+
], { cwd: root });
|
|
124
|
+
await removeSkillsFromLock(root, skills);
|
|
125
|
+
return { ok: true, count: skills.length };
|
|
126
|
+
}
|
|
127
|
+
export async function updateSkills(root, runner = defaultRunner) {
|
|
128
|
+
await runner.run(["npm", "exec", "--yes", "--package", "skills", "--", "skills", "update", "--project", "-y"], { cwd: root });
|
|
129
|
+
return { ok: true };
|
|
130
|
+
}
|
|
131
|
+
export async function enableMcpServers(root, servers, options = {}) {
|
|
132
|
+
assertKnownMcpServers(servers);
|
|
133
|
+
const state = await readCapabilities(root);
|
|
134
|
+
const selected = dedupe([...(state.mcp_servers ?? []), ...servers]);
|
|
135
|
+
await writeCapabilities(root, {
|
|
136
|
+
...state,
|
|
137
|
+
agent: options.agent ?? state.agent,
|
|
138
|
+
mcp_servers: selected
|
|
139
|
+
});
|
|
140
|
+
return { ok: true, servers: selected };
|
|
141
|
+
}
|
|
142
|
+
export async function disableMcpServers(root, servers, options = {}) {
|
|
143
|
+
assertKnownMcpServers(servers);
|
|
144
|
+
const state = await readCapabilities(root);
|
|
145
|
+
const blocked = new Set(servers);
|
|
146
|
+
const selected = (state.mcp_servers ?? []).filter((server) => !blocked.has(server));
|
|
147
|
+
await writeCapabilities(root, {
|
|
148
|
+
...state,
|
|
149
|
+
agent: options.agent ?? state.agent,
|
|
150
|
+
mcp_servers: selected
|
|
151
|
+
});
|
|
152
|
+
return { ok: true, servers: selected };
|
|
153
|
+
}
|
|
154
|
+
export function mcpToolCommands(servers, key = "install_command") {
|
|
155
|
+
assertKnownMcpServers(servers);
|
|
156
|
+
const commands = [];
|
|
157
|
+
for (const server of servers) {
|
|
158
|
+
const rawCommand = AGENT_STACK.mcp_servers[server]?.[key];
|
|
159
|
+
if (rawCommand)
|
|
160
|
+
commands.push(splitCommand(rawCommand));
|
|
161
|
+
}
|
|
162
|
+
return commands;
|
|
163
|
+
}
|
|
164
|
+
export function mcpToolCommandTexts(servers, key = "install_command") {
|
|
165
|
+
assertKnownMcpServers(servers);
|
|
166
|
+
const commands = [];
|
|
167
|
+
for (const server of servers) {
|
|
168
|
+
const rawCommand = AGENT_STACK.mcp_servers[server]?.[key];
|
|
169
|
+
if (rawCommand)
|
|
170
|
+
commands.push(rawCommand);
|
|
171
|
+
}
|
|
172
|
+
return commands;
|
|
173
|
+
}
|
|
174
|
+
export async function installMcpTools(root, servers, runner = defaultRunner) {
|
|
175
|
+
const selected = servers.length > 0 ? servers : (await readCapabilities(root)).mcp_servers ?? [];
|
|
176
|
+
assertKnownMcpServers(selected);
|
|
177
|
+
const commands = mcpToolCommands(selected, "install_command");
|
|
178
|
+
for (const command of commands) {
|
|
179
|
+
await runner.run(command, { cwd: root });
|
|
180
|
+
}
|
|
181
|
+
return { ok: true, count: commands.length };
|
|
182
|
+
}
|
|
183
|
+
export async function uninstallMcpTools(root, servers, runner = defaultRunner) {
|
|
184
|
+
const selected = servers.length > 0 ? servers : (await readCapabilities(root)).mcp_servers ?? [];
|
|
185
|
+
assertKnownMcpServers(selected);
|
|
186
|
+
const commands = mcpToolCommands(selected, "uninstall_command");
|
|
187
|
+
for (const command of commands) {
|
|
188
|
+
await runner.run(command, { cwd: root });
|
|
189
|
+
}
|
|
190
|
+
return { ok: true, count: commands.length };
|
|
191
|
+
}
|
|
192
|
+
export async function doctorMcpServers(root) {
|
|
193
|
+
const errors = [];
|
|
194
|
+
const warnings = [];
|
|
195
|
+
let state;
|
|
196
|
+
try {
|
|
197
|
+
state = await readCapabilitiesFile(root);
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
return {
|
|
201
|
+
ok: false,
|
|
202
|
+
errors: [`invalid configs/capabilities.yaml: ${error instanceof Error ? error.message : String(error)}`],
|
|
203
|
+
warnings,
|
|
204
|
+
enabled: []
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
const enabled = state.mcp_servers ?? [];
|
|
208
|
+
const unknown = enabled.filter((server) => !AGENT_STACK.mcp_servers[server]);
|
|
209
|
+
if (unknown.length > 0) {
|
|
210
|
+
errors.push(`unknown MCP server in capabilities: ${unknown.join(", ")}`);
|
|
211
|
+
}
|
|
212
|
+
const generatedServers = new Set();
|
|
213
|
+
const snippetPath = join(root, "docs", "agent", "generated", mcpSnippetFileName(state.agent));
|
|
214
|
+
try {
|
|
215
|
+
const rawSnippet = await readFile(snippetPath, "utf8");
|
|
216
|
+
const snippet = JSON.parse(rawSnippet);
|
|
217
|
+
for (const name of Object.keys(snippet.mcpServers ?? {})) {
|
|
218
|
+
generatedServers.add(name);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
const hasGeneratedServer = enabled.some((server) => AGENT_STACK.mcp_servers[server]?.command);
|
|
223
|
+
if (hasGeneratedServer && isMissingFileError(error)) {
|
|
224
|
+
errors.push(`missing generated MCP snippet: ${snippetPath}`);
|
|
225
|
+
}
|
|
226
|
+
else if (hasGeneratedServer) {
|
|
227
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
228
|
+
errors.push(`invalid generated MCP snippet: ${snippetPath}: ${message}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
for (const name of enabled) {
|
|
232
|
+
const server = AGENT_STACK.mcp_servers[name];
|
|
233
|
+
if (!server)
|
|
234
|
+
continue;
|
|
235
|
+
if (!server.command) {
|
|
236
|
+
warnings.push(`${name}: manual setup only; no generated client command`);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (!generatedServers.has(name)) {
|
|
240
|
+
errors.push(`${name}: enabled but missing from generated MCP snippet`);
|
|
241
|
+
}
|
|
242
|
+
if (!server.install_command) {
|
|
243
|
+
warnings.push(`${name}: no automated install command is defined`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return { ok: errors.length === 0, errors, warnings, enabled };
|
|
247
|
+
}
|
|
248
|
+
async function writeMcpSnippet(root, state) {
|
|
249
|
+
const servers = {};
|
|
250
|
+
for (const name of state.mcp_servers ?? []) {
|
|
251
|
+
const server = AGENT_STACK.mcp_servers[name];
|
|
252
|
+
if (!server?.command)
|
|
253
|
+
continue;
|
|
254
|
+
servers[name] = {
|
|
255
|
+
command: server.command,
|
|
256
|
+
args: server.args
|
|
257
|
+
};
|
|
258
|
+
if (Object.keys(server.env).length > 0)
|
|
259
|
+
servers[name].env = server.env;
|
|
260
|
+
}
|
|
261
|
+
const outputDir = join(root, "docs/agent/generated");
|
|
262
|
+
const outputFile = mcpSnippetFileName(state.agent);
|
|
263
|
+
await mkdir(outputDir, { recursive: true });
|
|
264
|
+
await removeInactiveMcpSnippets(outputDir, outputFile);
|
|
265
|
+
await writeFile(join(outputDir, outputFile), `${JSON.stringify({ mcpServers: servers }, null, 2)}\n`, "utf8");
|
|
266
|
+
}
|
|
267
|
+
async function writeCapabilityProfile(root, state) {
|
|
268
|
+
const lines = [
|
|
269
|
+
"# Agent Capability Profile",
|
|
270
|
+
"",
|
|
271
|
+
`- Agent target: \`${normalizeAgent(state.agent)}\``,
|
|
272
|
+
`- Preset: \`${state.preset ?? "default"}\``,
|
|
273
|
+
"- Scope: `project-local`",
|
|
274
|
+
"",
|
|
275
|
+
"## Skills",
|
|
276
|
+
"",
|
|
277
|
+
`- Install with: \`academic-research skills install --preset ${state.preset ?? "default"}\``,
|
|
278
|
+
"- List installed with: `academic-research skills list`",
|
|
279
|
+
"- List presets with: `academic-research skills presets`",
|
|
280
|
+
"- Remove with: `academic-research skills remove <skill>`",
|
|
281
|
+
"- Update with: `academic-research skills update`",
|
|
282
|
+
"",
|
|
283
|
+
"## MCP Servers",
|
|
284
|
+
""
|
|
285
|
+
];
|
|
286
|
+
if ((state.mcp_servers ?? []).length === 0) {
|
|
287
|
+
lines.push("- No MCP servers enabled.");
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
for (const name of state.mcp_servers) {
|
|
291
|
+
const server = AGENT_STACK.mcp_servers[name];
|
|
292
|
+
const status = server?.command ? "generated config" : "manual setup";
|
|
293
|
+
lines.push(`- \`${name}\` (${status}): ${server?.smoke_test ?? "Smoke-test before use."}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
lines.push("", "## Rules", "", "- Skill installation is project-local by default.", "- Agent target `auto` lets the local skills CLI detect the active agent.", "- MCP enable/disable changes project records; install/uninstall changes external tools.", "- Keep API keys, tokens, cookies, and browser sessions out of git.", "- Cite repository source records, not raw MCP output alone.", "");
|
|
297
|
+
await writeFile(join(root, "docs/agent/capability-profile.md"), lines.join("\n"), "utf8");
|
|
298
|
+
}
|
|
299
|
+
async function appendCapabilityLog(root, state) {
|
|
300
|
+
const logPath = join(root, "wiki/log.md");
|
|
301
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
302
|
+
const servers = (state.mcp_servers ?? []).join(", ") || "none";
|
|
303
|
+
await appendFile(logPath, `\n## [${date}] capability | Updated project-local agent capabilities\n\n- Preset: ${state.preset}\n- Agent: ${state.agent}\n- MCP servers: ${servers}\n`, "utf8");
|
|
304
|
+
}
|
|
305
|
+
function dedupe(values) {
|
|
306
|
+
return [...new Set(values)];
|
|
307
|
+
}
|
|
308
|
+
function renderSkillCommand(command, agent) {
|
|
309
|
+
const normalized = normalizeAgent(agent);
|
|
310
|
+
const agentFlag = normalized === DEFAULT_AGENT ? "" : `--agent '${normalized}'`;
|
|
311
|
+
return command.replaceAll("{agent_flag}", agentFlag).replaceAll("{agent}", normalized);
|
|
312
|
+
}
|
|
313
|
+
async function readCapabilitiesFile(root) {
|
|
314
|
+
const path = join(root, "configs/capabilities.yaml");
|
|
315
|
+
return normalizeCapabilityState(YAML.parse(await readFile(path, "utf8")));
|
|
316
|
+
}
|
|
317
|
+
export function assertKnownMcpServers(servers) {
|
|
318
|
+
const unknown = servers.filter((server) => !AGENT_STACK.mcp_servers[server]);
|
|
319
|
+
if (unknown.length > 0) {
|
|
320
|
+
throw new Error(`unknown MCP server: ${unknown.join(", ")}`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
function splitCommand(command) {
|
|
324
|
+
const result = [];
|
|
325
|
+
let current = "";
|
|
326
|
+
let quote = null;
|
|
327
|
+
for (const char of command) {
|
|
328
|
+
if ((char === "'" || char === '"') && quote === null) {
|
|
329
|
+
quote = char;
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
if (char === quote) {
|
|
333
|
+
quote = null;
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
if (/\s/.test(char) && quote === null) {
|
|
337
|
+
if (current) {
|
|
338
|
+
result.push(current);
|
|
339
|
+
current = "";
|
|
340
|
+
}
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
current += char;
|
|
344
|
+
}
|
|
345
|
+
if (current)
|
|
346
|
+
result.push(current);
|
|
347
|
+
if (quote !== null)
|
|
348
|
+
throw new Error(`unterminated quote in command: ${command}`);
|
|
349
|
+
return result;
|
|
350
|
+
}
|
|
351
|
+
function normalizeCapabilityState(value) {
|
|
352
|
+
const record = typeof value === "object" && value !== null ? value : {};
|
|
353
|
+
return {
|
|
354
|
+
agent: normalizeAgent(typeof record.agent === "string" ? record.agent : undefined),
|
|
355
|
+
preset: typeof record.preset === "string" ? record.preset : "default",
|
|
356
|
+
scope: "project-local",
|
|
357
|
+
mcp_servers: Array.isArray(record.mcp_servers)
|
|
358
|
+
? record.mcp_servers.filter((item) => typeof item === "string")
|
|
359
|
+
: []
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
function defaultCapabilities() {
|
|
363
|
+
return { agent: DEFAULT_AGENT, preset: "default", scope: "project-local", mcp_servers: [] };
|
|
364
|
+
}
|
|
365
|
+
function isMissingFileError(error) {
|
|
366
|
+
return (typeof error === "object" &&
|
|
367
|
+
error !== null &&
|
|
368
|
+
"code" in error &&
|
|
369
|
+
error.code === "ENOENT");
|
|
370
|
+
}
|
|
371
|
+
async function removeSkillsFromLock(root, skills) {
|
|
372
|
+
const path = join(root, "skills-lock.json");
|
|
373
|
+
let lock;
|
|
374
|
+
try {
|
|
375
|
+
lock = JSON.parse(await readFile(path, "utf8"));
|
|
376
|
+
}
|
|
377
|
+
catch (error) {
|
|
378
|
+
if (isMissingFileError(error))
|
|
379
|
+
return;
|
|
380
|
+
throw error;
|
|
381
|
+
}
|
|
382
|
+
const record = typeof lock === "object" && lock !== null ? lock : {};
|
|
383
|
+
const lockedSkills = typeof record.skills === "object" && record.skills !== null
|
|
384
|
+
? record.skills
|
|
385
|
+
: undefined;
|
|
386
|
+
if (!lockedSkills)
|
|
387
|
+
return;
|
|
388
|
+
let changed = false;
|
|
389
|
+
for (const skill of skills) {
|
|
390
|
+
if (Object.hasOwn(lockedSkills, skill)) {
|
|
391
|
+
delete lockedSkills[skill];
|
|
392
|
+
changed = true;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
if (changed) {
|
|
396
|
+
await writeFile(path, `${JSON.stringify(record, null, 2)}\n`, "utf8");
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
function normalizeAgent(agent) {
|
|
400
|
+
const value = agent?.trim();
|
|
401
|
+
return value ? value : DEFAULT_AGENT;
|
|
402
|
+
}
|
|
403
|
+
function mcpSnippetFileName(agent) {
|
|
404
|
+
const normalized = normalizeAgent(agent);
|
|
405
|
+
return normalized === DEFAULT_AGENT ? "mcp.json" : `${normalized}-mcp.json`;
|
|
406
|
+
}
|
|
407
|
+
async function removeInactiveMcpSnippets(outputDir, activeFile) {
|
|
408
|
+
const entries = await readdir(outputDir);
|
|
409
|
+
await Promise.all(entries
|
|
410
|
+
.filter((entry) => entry !== activeFile && (entry === "mcp.json" || entry.endsWith("-mcp.json")))
|
|
411
|
+
.map((entry) => rm(join(outputDir, entry), { force: true })));
|
|
412
|
+
}
|
|
413
|
+
const SKILL_DISCOVERY_IGNORES = new Set([
|
|
414
|
+
".git",
|
|
415
|
+
".hg",
|
|
416
|
+
".svn",
|
|
417
|
+
".venv",
|
|
418
|
+
".mypy_cache",
|
|
419
|
+
".pytest_cache",
|
|
420
|
+
".ruff_cache",
|
|
421
|
+
".tox",
|
|
422
|
+
"build",
|
|
423
|
+
"dist",
|
|
424
|
+
"node_modules",
|
|
425
|
+
"__pycache__"
|
|
426
|
+
]);
|
|
427
|
+
async function discoverProjectSkillRoots(root) {
|
|
428
|
+
const candidates = new Set(["skills"]);
|
|
429
|
+
let topLevel;
|
|
430
|
+
try {
|
|
431
|
+
topLevel = await readdir(root, { withFileTypes: true });
|
|
432
|
+
}
|
|
433
|
+
catch {
|
|
434
|
+
return [];
|
|
435
|
+
}
|
|
436
|
+
for (const entry of topLevel) {
|
|
437
|
+
if (!entry.isDirectory() || SKILL_DISCOVERY_IGNORES.has(entry.name))
|
|
438
|
+
continue;
|
|
439
|
+
if (!entry.name.startsWith("."))
|
|
440
|
+
continue;
|
|
441
|
+
candidates.add(`${entry.name}/skills`);
|
|
442
|
+
let children;
|
|
443
|
+
try {
|
|
444
|
+
children = await readdir(join(root, entry.name), { withFileTypes: true });
|
|
445
|
+
}
|
|
446
|
+
catch {
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
for (const child of children) {
|
|
450
|
+
if (!child.isDirectory() || SKILL_DISCOVERY_IGNORES.has(child.name))
|
|
451
|
+
continue;
|
|
452
|
+
candidates.add(`${entry.name}/${child.name}/skills`);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
const roots = [];
|
|
456
|
+
for (const candidate of candidates) {
|
|
457
|
+
const absolute = join(root, candidate);
|
|
458
|
+
if (await hasInstalledSkill(absolute)) {
|
|
459
|
+
roots.push({ absolute, relative: toPosix(relative(root, absolute)) });
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return roots.sort((left, right) => left.relative.localeCompare(right.relative));
|
|
463
|
+
}
|
|
464
|
+
async function hasInstalledSkill(skillsRoot) {
|
|
465
|
+
let entries;
|
|
466
|
+
try {
|
|
467
|
+
entries = await readdir(skillsRoot, { withFileTypes: true });
|
|
468
|
+
}
|
|
469
|
+
catch {
|
|
470
|
+
return false;
|
|
471
|
+
}
|
|
472
|
+
for (const entry of entries) {
|
|
473
|
+
if (!entry.isDirectory())
|
|
474
|
+
continue;
|
|
475
|
+
try {
|
|
476
|
+
await readFile(join(skillsRoot, entry.name, "SKILL.md"), "utf8");
|
|
477
|
+
return true;
|
|
478
|
+
}
|
|
479
|
+
catch {
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return false;
|
|
484
|
+
}
|
|
485
|
+
function toPosix(value) {
|
|
486
|
+
return value.split(/[\\/]/).join("/");
|
|
487
|
+
}
|