create-academic-research 0.1.12 → 0.1.14
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 -36
- package/dist/src/agents.js +1 -1
- package/dist/src/capabilities.d.ts +7 -0
- package/dist/src/capabilities.js +27 -15
- package/dist/src/cli.js +104 -15
- package/dist/src/project.d.ts +17 -0
- package/dist/src/project.js +358 -18
- package/package.json +1 -1
- package/template/README.md +24 -22
- package/template/docs/agent/mcp-client-setup.md +7 -7
- package/template/docs/agent/mcp-setup.md +4 -5
- package/template/docs/getting-started.md +15 -13
- package/template/package.json +26 -13
package/README.md
CHANGED
|
@@ -55,7 +55,7 @@ The generated repository is agent-neutral. By default the wizard records
|
|
|
55
55
|
`agent: universal`, installs one shared project-local `.agents/skills` copy,
|
|
56
56
|
and writes generic MCP snippets. Use `--agent <id>` only when you want to force
|
|
57
57
|
a specific target recognized by the `skills` CLI. Run
|
|
58
|
-
`
|
|
58
|
+
`npm run agents:list` inside a generated project to see every
|
|
59
59
|
supported target and alias.
|
|
60
60
|
|
|
61
61
|
## When To Use It
|
|
@@ -113,37 +113,48 @@ npx create-academic-research@latest my-project --yes --no-install-skills
|
|
|
113
113
|
Inside a generated project:
|
|
114
114
|
|
|
115
115
|
```bash
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
116
|
+
npm run doctor
|
|
117
|
+
npm run update
|
|
118
|
+
npm run setup
|
|
119
|
+
npm run rename -- --title "New Title" --slug new-title --package new_title
|
|
120
|
+
npm run agents:list
|
|
121
|
+
npm run skills:presets
|
|
122
|
+
npm run skills:install
|
|
123
|
+
npm run skills:install -- --preset enhanced
|
|
124
|
+
npm run skills:install -- source-ingestion sota-literature-review
|
|
125
|
+
npm run skills:list
|
|
126
|
+
npm run skills:status
|
|
127
|
+
npm run skills:remove -- source-ingestion
|
|
128
|
+
npm run skills:uninstall -- source-ingestion
|
|
129
|
+
npm run skills:update
|
|
130
|
+
npm run mcp:list
|
|
131
|
+
npm run mcp:enabled
|
|
132
|
+
npm run mcp:available
|
|
133
|
+
npm run mcp:commands -- arxiv
|
|
134
|
+
npm run mcp:env -- openalex semantic-scholar zotero
|
|
135
|
+
npm run mcp:env -- --dotenv --all > .env.example
|
|
136
|
+
npm run mcp:dotenv
|
|
137
|
+
npm run mcp:enable -- arxiv dblp
|
|
138
|
+
npm run mcp:disable -- arxiv
|
|
139
|
+
npm run mcp:install -- arxiv
|
|
140
|
+
npm run mcp:uninstall -- arxiv
|
|
141
|
+
npm run mcp:smoke -- --env-file .env.local
|
|
142
|
+
npm run mcp:doctor -- --env-file .env.local
|
|
143
|
+
npm run mcp:probe -- arxiv --timeout-ms 5000
|
|
143
144
|
```
|
|
144
145
|
|
|
146
|
+
For direct one-off invocation without the generated package scripts, use
|
|
147
|
+
`npx --yes --package create-academic-research@latest academic-research <command>`.
|
|
148
|
+
|
|
145
149
|
## Command Model
|
|
146
150
|
|
|
151
|
+
`academic-research update` is a dry-run by default. It reports managed project
|
|
152
|
+
files that would change and writes them only with `--apply`.
|
|
153
|
+
|
|
154
|
+
`academic-research init` initializes an existing repository without overwriting
|
|
155
|
+
existing files. It adds the research contract, merges lifecycle package scripts,
|
|
156
|
+
and preserves existing README, `.gitignore`, and custom package scripts.
|
|
157
|
+
|
|
147
158
|
`academic-research setup` is a non-destructive onboarding status command. It
|
|
148
159
|
prints the active preset, agent, skill counts, enabled MCP records, and next
|
|
149
160
|
commands without changing files.
|
|
@@ -189,7 +200,7 @@ Those skills are portable `SKILL.md` instructions, but they require an
|
|
|
189
200
|
agent/runtime that can load skills or include the relevant instructions in
|
|
190
201
|
context. They are not automatic capabilities of every raw model API.
|
|
191
202
|
Use `--agent <id>` for explicit setup with any id from
|
|
192
|
-
`
|
|
203
|
+
`npm run agents:list`. The shorthand `--agent claude` is normalized
|
|
193
204
|
to the supported `claude-code` target.
|
|
194
205
|
Avoid `--agent auto` for unattended setup: the upstream `skills` CLI may expand
|
|
195
206
|
it to every agent it detects on the machine.
|
|
@@ -208,9 +219,9 @@ Preset intent:
|
|
|
208
219
|
MCP defaults are intentionally conservative. Semantic Scholar, OpenAlex,
|
|
209
220
|
Zotero, Overleaf, Crossref, and fallback aggregators are useful, but they need
|
|
210
221
|
API keys, local apps, manual setup, or source-policy review. Enable them with
|
|
211
|
-
`
|
|
212
|
-
`docs/agent/mcp-setup.md`, use `
|
|
213
|
-
|
|
222
|
+
`npm run mcp:enable -- <server>` after reading
|
|
223
|
+
`docs/agent/mcp-setup.md`, use `npm run mcp:env -- <server>` to see runtime
|
|
224
|
+
prerequisites, then run `npm run mcp:doctor`.
|
|
214
225
|
|
|
215
226
|
The MCP catalog distinguishes local runtime adapters from hosted endpoints and
|
|
216
227
|
manual integrations. arXiv and DBLP are low-friction local `uvx` runtimes.
|
|
@@ -225,8 +236,8 @@ until a project explicitly needs them.
|
|
|
225
236
|
|
|
226
237
|
Generated projects include a committed `.env.example` with empty MCP variables
|
|
227
238
|
and ignore filled `.env` or `.env.local` files. Regenerate the example with
|
|
228
|
-
`
|
|
229
|
-
|
|
239
|
+
`npm run mcp:dotenv`. `mcp doctor`, `mcp smoke`, and `mcp probe` check the
|
|
240
|
+
current process environment unless you explicitly pass
|
|
230
241
|
`--env-file .env.local`.
|
|
231
242
|
|
|
232
243
|
Generated MCP snippets are project documentation and client-ready config, not
|
|
@@ -256,8 +267,8 @@ Releases are tag-driven. Update `package.json` and `package-lock.json`, commit
|
|
|
256
267
|
the change, create `vX.Y.Z`, and push the tag:
|
|
257
268
|
|
|
258
269
|
```bash
|
|
259
|
-
git tag -a
|
|
260
|
-
git push origin main
|
|
270
|
+
git tag -a vX.Y.Z -m "vX.Y.Z"
|
|
271
|
+
git push origin main vX.Y.Z
|
|
261
272
|
```
|
|
262
273
|
|
|
263
274
|
Once the GitHub repository is public, the release workflow validates the tag
|
package/dist/src/agents.js
CHANGED
|
@@ -79,7 +79,7 @@ export function assertKnownAgentTarget(agent) {
|
|
|
79
79
|
throw new Error([
|
|
80
80
|
`unknown agent target: ${value}`,
|
|
81
81
|
`Use ${DEFAULT_AGENT}, ${AUTO_AGENT}, or one supported skills.sh agent id.`,
|
|
82
|
-
"List targets with: npx
|
|
82
|
+
"List targets with: npx --yes --package create-academic-research@latest academic-research agents list",
|
|
83
83
|
`Supported ids: ${specificAgentTargets().join(", ")}`,
|
|
84
84
|
`Aliases: ${formatAgentAliasesInline()}`
|
|
85
85
|
].join("\n"));
|
|
@@ -43,6 +43,10 @@ export interface McpProbeOptions {
|
|
|
43
43
|
timeoutMs?: number;
|
|
44
44
|
clientVersion?: string;
|
|
45
45
|
}
|
|
46
|
+
export interface RenderedMcpSnippet {
|
|
47
|
+
fileName: string;
|
|
48
|
+
content: string;
|
|
49
|
+
}
|
|
46
50
|
interface SkillInstallOptions {
|
|
47
51
|
agent?: string;
|
|
48
52
|
}
|
|
@@ -69,4 +73,7 @@ export declare function installMcpTools(root: string, servers: string[], runner?
|
|
|
69
73
|
export declare function uninstallMcpTools(root: string, servers: string[], runner?: Runner): Promise<CapabilityCommandResult>;
|
|
70
74
|
export declare function doctorMcpServers(root: string, options?: McpDoctorOptions): Promise<McpDoctorResult>;
|
|
71
75
|
export declare function probeMcpServers(root: string, servers: string[], options?: McpProbeOptions): Promise<ProbeResult>;
|
|
76
|
+
export declare function renderMcpSnippet(state: CapabilityState): RenderedMcpSnippet;
|
|
77
|
+
export declare function renderCapabilityProfile(state: CapabilityState): string;
|
|
78
|
+
export declare function renderMcpSetup(state: CapabilityState): string;
|
|
72
79
|
export declare function assertKnownMcpServers(servers: string[]): void;
|
package/dist/src/capabilities.js
CHANGED
|
@@ -303,6 +303,13 @@ export async function probeMcpServers(root, servers, options = {}) {
|
|
|
303
303
|
return probeMcpServerList(root, selected, env, timeoutMs, options.clientVersion);
|
|
304
304
|
}
|
|
305
305
|
async function writeMcpSnippet(root, state) {
|
|
306
|
+
const snippet = renderMcpSnippet(state);
|
|
307
|
+
const outputDir = join(root, "docs/agent/generated");
|
|
308
|
+
await mkdir(outputDir, { recursive: true });
|
|
309
|
+
await removeInactiveMcpSnippets(outputDir, snippet.fileName);
|
|
310
|
+
await writeFile(join(outputDir, snippet.fileName), snippet.content, "utf8");
|
|
311
|
+
}
|
|
312
|
+
export function renderMcpSnippet(state) {
|
|
306
313
|
const servers = {};
|
|
307
314
|
for (const name of state.mcp_servers ?? []) {
|
|
308
315
|
const server = AGENT_STACK.mcp_servers[name];
|
|
@@ -315,13 +322,15 @@ async function writeMcpSnippet(root, state) {
|
|
|
315
322
|
if (Object.keys(server.env).length > 0)
|
|
316
323
|
servers[name].env = server.env;
|
|
317
324
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
await writeFile(join(outputDir, outputFile), `${JSON.stringify({ mcpServers: servers }, null, 2)}\n`, "utf8");
|
|
325
|
+
return {
|
|
326
|
+
fileName: mcpSnippetFileName(state.agent),
|
|
327
|
+
content: `${JSON.stringify({ mcpServers: servers }, null, 2)}\n`
|
|
328
|
+
};
|
|
323
329
|
}
|
|
324
330
|
async function writeCapabilityProfile(root, state) {
|
|
331
|
+
await writeFile(join(root, "docs/agent/capability-profile.md"), renderCapabilityProfile(state), "utf8");
|
|
332
|
+
}
|
|
333
|
+
export function renderCapabilityProfile(state) {
|
|
325
334
|
const lines = [
|
|
326
335
|
"# Agent Capability Profile",
|
|
327
336
|
"",
|
|
@@ -331,12 +340,12 @@ async function writeCapabilityProfile(root, state) {
|
|
|
331
340
|
"",
|
|
332
341
|
"## Skills",
|
|
333
342
|
"",
|
|
334
|
-
`- Install with: \`
|
|
335
|
-
"- Install selected skills with: `
|
|
336
|
-
"- List installed with: `
|
|
337
|
-
"- List presets with: `
|
|
338
|
-
"- Remove with: `
|
|
339
|
-
"- Update with: `
|
|
343
|
+
`- Install with: \`npm run skills:install -- --preset ${state.preset ?? "default"}\``,
|
|
344
|
+
"- Install selected skills with: `npm run skills:install -- <skill-id> [...]`",
|
|
345
|
+
"- List installed with: `npm run skills:list`",
|
|
346
|
+
"- List presets with: `npm run skills:presets`",
|
|
347
|
+
"- Remove with: `npm run skills:remove -- <skill>`",
|
|
348
|
+
"- Update with: `npm run skills:update`",
|
|
340
349
|
"",
|
|
341
350
|
"## MCP Servers",
|
|
342
351
|
""
|
|
@@ -352,9 +361,13 @@ async function writeCapabilityProfile(root, state) {
|
|
|
352
361
|
}
|
|
353
362
|
}
|
|
354
363
|
lines.push("", "## Rules", "", "- Skill installation is project-local by default.", "- Agent target `universal` installs one shared project-local `.agents/skills` copy.", "- 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.", "");
|
|
355
|
-
|
|
364
|
+
return lines.join("\n");
|
|
356
365
|
}
|
|
357
366
|
async function writeMcpSetup(root, state) {
|
|
367
|
+
await mkdir(join(root, "docs/agent"), { recursive: true });
|
|
368
|
+
await writeFile(join(root, "docs/agent/mcp-setup.md"), renderMcpSetup(state), "utf8");
|
|
369
|
+
}
|
|
370
|
+
export function renderMcpSetup(state) {
|
|
358
371
|
const enabled = new Set(state.mcp_servers ?? []);
|
|
359
372
|
const lines = [
|
|
360
373
|
"# MCP Setup",
|
|
@@ -383,9 +396,8 @@ async function writeMcpSetup(root, state) {
|
|
|
383
396
|
lines.push(`- \`${name}\` (${status}, ${server.readiness}, ${server.priority}): ${server.source_need}`, ` - Source: \`${server.source}\``, ` - Execution mode: \`${server.execution_mode}\``, ...(server.hosted_url ? [` - Hosted endpoint: <${server.hosted_url}>`] : []), ...server.setup_commands.map((command) => ` - Setup command: \`${command}\``));
|
|
384
397
|
appendMcpPrerequisiteLines(lines, server.required_env, server.recommended_env, server.local_service);
|
|
385
398
|
}
|
|
386
|
-
lines.push("", "## Operating Rules", "", "- Use `.env.example` as a committed reference and put filled secrets in `.env.local`, your shell, or your MCP client secret store.", "- Print a dotenv-style reference with `
|
|
387
|
-
|
|
388
|
-
await writeFile(join(root, "docs/agent/mcp-setup.md"), lines.join("\n"), "utf8");
|
|
399
|
+
lines.push("", "## Operating Rules", "", "- Use `.env.example` as a committed reference and put filled secrets in `.env.local`, your shell, or your MCP client secret store.", "- Print a dotenv-style reference with `npm run mcp:env -- --dotenv --all`.", "- Regenerate a dotenv-style reference with `npm run mcp:dotenv`.", "- Pass `--env-file .env.local` to `mcp doctor`, `mcp smoke`, or `mcp probe` when you want the CLI to read explicit local secrets.", "- Keep secrets in your shell, MCP client secret store, or local untracked files; do not commit tokens or API keys.", "- Prefer the smallest enabled MCP set that covers the current research question.", "- Treat MCP output as retrieval metadata. Promote claims into repository source records only after source ingestion and citation audit.", "- Run `npm run mcp:doctor` after changing MCP records or environment variables.", "- Run `npm run mcp:probe -- <server>` only when you intentionally want to start selected MCP server processes.", "");
|
|
400
|
+
return lines.join("\n");
|
|
389
401
|
}
|
|
390
402
|
async function appendCapabilityLog(root, state) {
|
|
391
403
|
const logPath = join(root, "wiki/log.md");
|
package/dist/src/cli.js
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { basename, delimiter, dirname, join, resolve } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { assertKnownMcpServers, disableMcpServers, doctorMcpServers, enableMcpServers, DEFAULT_AGENT, installMcpTools, installSkillIds, installSkills, formatMcpDotenv, listInstalledSkills, listMcpEnvironmentEntries, mergeMcpEnvironment, mcpToolCommandTexts, probeMcpServers, readCapabilities, readMcpEnvironmentFile, removeSkills, uninstallMcpTools, updateSkills } from "./capabilities.js";
|
|
5
|
-
import { createProject, doctorProject, renameProject } from "./project.js";
|
|
5
|
+
import { createProject, doctorProject, initProject, renameProject, updateProject } from "./project.js";
|
|
6
6
|
import { askCreateOptions } from "./prompts.js";
|
|
7
7
|
import { AGENT_STACK, presetMcpServers } from "./stack.js";
|
|
8
8
|
import { formatAgentAliasLines, formatAgentTargetList, formatSupportedAgentTargetLines } from "./agents.js";
|
|
@@ -19,6 +19,8 @@ const CREATE_FLAGS = flagSchema([
|
|
|
19
19
|
"no-install-mcp-tools"
|
|
20
20
|
], ["title", "slug", "package", "preset", "profile", "agent"]);
|
|
21
21
|
const ROOT_FLAGS = flagSchema(["help"], ["root"]);
|
|
22
|
+
const UPDATE_FLAGS = flagSchema(["help", "dry-run", "apply"], ["root"]);
|
|
23
|
+
const INIT_FLAGS = flagSchema(["help", "install-skills"], ["root", "title", "slug", "package", "preset", "profile", "agent"]);
|
|
22
24
|
const RENAME_FLAGS = flagSchema(["help"], ["root", "title", "slug", "package"]);
|
|
23
25
|
const SKILLS_FLAGS = flagSchema(["help"], ["root", "preset", "agent"]);
|
|
24
26
|
const MCP_FLAGS = flagSchema(["help", "all", "dotenv", "required", "recommended"], ["root", "agent", "env-file", "write", "timeout-ms"]);
|
|
@@ -94,7 +96,7 @@ async function createMain(argv) {
|
|
|
94
96
|
await installMcpTools(result.root, presetMcpServers(answers.preset));
|
|
95
97
|
}
|
|
96
98
|
console.log(`Created ${result.slug} at ${result.root}`);
|
|
97
|
-
console.log("Next: cd into the project and run `
|
|
99
|
+
console.log("Next: cd into the project and run `npm run doctor`.");
|
|
98
100
|
return 0;
|
|
99
101
|
}
|
|
100
102
|
async function askInteractiveCreateOptions(defaults, locks) {
|
|
@@ -113,6 +115,10 @@ async function lifecycleMain(argv) {
|
|
|
113
115
|
}
|
|
114
116
|
if (command === "doctor")
|
|
115
117
|
return doctorCommand(argv.slice(1));
|
|
118
|
+
if (command === "update")
|
|
119
|
+
return updateCommand(argv.slice(1));
|
|
120
|
+
if (command === "init")
|
|
121
|
+
return initCommand(argv.slice(1));
|
|
116
122
|
if (command === "setup")
|
|
117
123
|
return setupCommand(argv.slice(1));
|
|
118
124
|
if (command === "rename")
|
|
@@ -136,10 +142,60 @@ async function doctorCommand(argv) {
|
|
|
136
142
|
const result = await doctorProject(root);
|
|
137
143
|
for (const error of result.errors)
|
|
138
144
|
console.error(`ERROR: ${error}`);
|
|
145
|
+
for (const warning of result.warnings)
|
|
146
|
+
console.warn(`WARN: ${warning}`);
|
|
139
147
|
if (result.ok)
|
|
140
148
|
console.log(`OK: ${root}`);
|
|
141
149
|
return result.ok ? 0 : 1;
|
|
142
150
|
}
|
|
151
|
+
async function updateCommand(argv) {
|
|
152
|
+
const parsed = parseFlags(argv, UPDATE_FLAGS);
|
|
153
|
+
if (flagBool(parsed.flags, "help")) {
|
|
154
|
+
printUpdateHelp();
|
|
155
|
+
return 0;
|
|
156
|
+
}
|
|
157
|
+
assertNoArguments(parsed.positionals, "update");
|
|
158
|
+
if (flagBool(parsed.flags, "dry-run") && flagBool(parsed.flags, "apply")) {
|
|
159
|
+
throw new Error("update cannot use --dry-run and --apply together");
|
|
160
|
+
}
|
|
161
|
+
const root = resolve(flagString(parsed.flags, "root") ?? ".");
|
|
162
|
+
const apply = flagBool(parsed.flags, "apply");
|
|
163
|
+
const result = await updateProject(root, { apply });
|
|
164
|
+
console.log(`${apply ? "UPDATED" : "DRY-RUN"}: ${root}`);
|
|
165
|
+
if (result.changes.length === 0) {
|
|
166
|
+
console.log("No managed file changes.");
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
for (const change of result.changes)
|
|
170
|
+
console.log(`${change.action}\t${change.path}`);
|
|
171
|
+
}
|
|
172
|
+
if (!apply && result.changes.length > 0) {
|
|
173
|
+
console.log("Run `npm run update -- --apply` from a generated project to write these managed changes.");
|
|
174
|
+
}
|
|
175
|
+
return 0;
|
|
176
|
+
}
|
|
177
|
+
async function initCommand(argv) {
|
|
178
|
+
const parsed = parseFlags(argv, INIT_FLAGS);
|
|
179
|
+
if (flagBool(parsed.flags, "help")) {
|
|
180
|
+
printInitHelp();
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
|
183
|
+
assertNoArguments(parsed.positionals, "init");
|
|
184
|
+
const root = resolve(flagString(parsed.flags, "root") ?? ".");
|
|
185
|
+
const result = await initProject({
|
|
186
|
+
target: root,
|
|
187
|
+
title: flagString(parsed.flags, "title"),
|
|
188
|
+
slug: flagString(parsed.flags, "slug"),
|
|
189
|
+
packageName: flagString(parsed.flags, "package"),
|
|
190
|
+
profile: flagString(parsed.flags, "profile") ?? "academic-general",
|
|
191
|
+
preset: flagString(parsed.flags, "preset") ?? "default",
|
|
192
|
+
agent: flagString(parsed.flags, "agent") ?? DEFAULT_AGENT,
|
|
193
|
+
installSkills: flagBool(parsed.flags, "install-skills")
|
|
194
|
+
});
|
|
195
|
+
console.log(`Initialized ${result.slug} at ${result.root}`);
|
|
196
|
+
console.log("Next: run `npm run doctor`.");
|
|
197
|
+
return 0;
|
|
198
|
+
}
|
|
143
199
|
async function setupCommand(argv) {
|
|
144
200
|
const parsed = parseFlags(argv, ROOT_FLAGS);
|
|
145
201
|
if (flagBool(parsed.flags, "help")) {
|
|
@@ -165,16 +221,18 @@ async function setupCommand(argv) {
|
|
|
165
221
|
for (const error of project.errors)
|
|
166
222
|
console.error(`ERROR: ${error}`);
|
|
167
223
|
}
|
|
224
|
+
for (const warning of project.warnings)
|
|
225
|
+
console.warn(`WARN: ${warning}`);
|
|
168
226
|
console.log("");
|
|
169
227
|
console.log("Next Commands");
|
|
170
|
-
console.log(`
|
|
171
|
-
console.log("
|
|
172
|
-
console.log("
|
|
173
|
-
console.log("
|
|
174
|
-
console.log("
|
|
175
|
-
console.log("
|
|
176
|
-
console.log("
|
|
177
|
-
console.log("
|
|
228
|
+
console.log(`npm run skills:install -- --preset ${state.preset}`);
|
|
229
|
+
console.log("npm run skills:status");
|
|
230
|
+
console.log("npm run mcp:list");
|
|
231
|
+
console.log("npm run mcp:env");
|
|
232
|
+
console.log("npm run mcp:dotenv");
|
|
233
|
+
console.log("npm run mcp:smoke");
|
|
234
|
+
console.log("npm run mcp:probe -- arxiv");
|
|
235
|
+
console.log("npm run doctor");
|
|
178
236
|
return project.ok ? 0 : 1;
|
|
179
237
|
}
|
|
180
238
|
async function renameCommand(argv) {
|
|
@@ -579,10 +637,10 @@ export function formatInteractiveCreateGuide() {
|
|
|
579
637
|
" default enables only low-friction arXiv; credentialed/local services are opt-in.",
|
|
580
638
|
" MCP installers are optional and run only finite installer commands.",
|
|
581
639
|
" MCP execution modes are explicit: uvx-runtime, npx-runtime, local-service, manual, or fallback.",
|
|
582
|
-
" Use `
|
|
583
|
-
" Use `
|
|
584
|
-
" Use `
|
|
585
|
-
" Use `
|
|
640
|
+
" Use `npm run mcp:env -- <server>` to inspect env vars and local prerequisites.",
|
|
641
|
+
" Use `npm run mcp:env -- --dotenv --all` to print a committed env example.",
|
|
642
|
+
" Use `npm run mcp:dotenv` to regenerate a committed env example.",
|
|
643
|
+
" Use `npm run mcp:doctor -- --env-file .env.local` to check explicit local secrets.",
|
|
586
644
|
""
|
|
587
645
|
].join("\n");
|
|
588
646
|
}
|
|
@@ -619,7 +677,7 @@ function printMissingTargetHelp() {
|
|
|
619
677
|
}
|
|
620
678
|
function printLifecycleHelp() {
|
|
621
679
|
console.log([
|
|
622
|
-
"Usage: academic-research <doctor|setup|rename|agents|skills|mcp>",
|
|
680
|
+
"Usage: academic-research <doctor|update|init|setup|rename|agents|skills|mcp>",
|
|
623
681
|
"",
|
|
624
682
|
"Manage a generated academic research repository after creation.",
|
|
625
683
|
"",
|
|
@@ -628,6 +686,37 @@ function printLifecycleHelp() {
|
|
|
628
686
|
" -v, --version Show package version."
|
|
629
687
|
].join("\n"));
|
|
630
688
|
}
|
|
689
|
+
function printUpdateHelp() {
|
|
690
|
+
console.log([
|
|
691
|
+
"Usage: academic-research update [options]",
|
|
692
|
+
"",
|
|
693
|
+
"Preview or apply non-destructive updates to managed project files.",
|
|
694
|
+
"",
|
|
695
|
+
"Options:",
|
|
696
|
+
" --root <path> Project root. Default: current directory.",
|
|
697
|
+
" --dry-run Preview managed changes without writing. Default.",
|
|
698
|
+
" --apply Write managed changes.",
|
|
699
|
+
" -h, --help Show this help."
|
|
700
|
+
].join("\n"));
|
|
701
|
+
}
|
|
702
|
+
function printInitHelp() {
|
|
703
|
+
console.log([
|
|
704
|
+
"Usage: academic-research init [options]",
|
|
705
|
+
"",
|
|
706
|
+
"Initialize an existing repository without overwriting existing files.",
|
|
707
|
+
"",
|
|
708
|
+
"Options:",
|
|
709
|
+
" --root <path> Project root. Default: current directory.",
|
|
710
|
+
" --title <name> Project title. Default: title-cased directory name.",
|
|
711
|
+
" --slug <name> Repository/package slug. Default: normalized directory name.",
|
|
712
|
+
" --package <name> Python package name. Default: normalized directory name.",
|
|
713
|
+
" --preset <name> Capability preset: minimal, default, enhanced, literature, writing, full.",
|
|
714
|
+
" --profile <name> Project profile metadata. Default: academic-general.",
|
|
715
|
+
" --agent <id> Agent target: universal, auto, or a supported skills.sh id.",
|
|
716
|
+
" --install-skills Install project-local skills after initialization.",
|
|
717
|
+
" -h, --help Show this help."
|
|
718
|
+
].join("\n"));
|
|
719
|
+
}
|
|
631
720
|
function printSetupHelp() {
|
|
632
721
|
console.log([
|
|
633
722
|
"Usage: academic-research setup [options]",
|
package/dist/src/project.d.ts
CHANGED
|
@@ -13,6 +13,11 @@ export interface RenameProjectOptions {
|
|
|
13
13
|
slug?: string;
|
|
14
14
|
packageName?: string;
|
|
15
15
|
}
|
|
16
|
+
export interface InitProjectOptions extends CreateProjectOptions {
|
|
17
|
+
}
|
|
18
|
+
export interface UpdateProjectOptions {
|
|
19
|
+
apply?: boolean;
|
|
20
|
+
}
|
|
16
21
|
export interface ProjectResult {
|
|
17
22
|
root: string;
|
|
18
23
|
title: string;
|
|
@@ -22,7 +27,19 @@ export interface ProjectResult {
|
|
|
22
27
|
export interface DoctorResult {
|
|
23
28
|
ok: boolean;
|
|
24
29
|
errors: string[];
|
|
30
|
+
warnings: string[];
|
|
31
|
+
}
|
|
32
|
+
export interface ProjectFileChange {
|
|
33
|
+
path: string;
|
|
34
|
+
action: "create" | "update";
|
|
35
|
+
}
|
|
36
|
+
export interface UpdateProjectResult {
|
|
37
|
+
root: string;
|
|
38
|
+
applied: boolean;
|
|
39
|
+
changes: ProjectFileChange[];
|
|
25
40
|
}
|
|
26
41
|
export declare function createProject(options: CreateProjectOptions): Promise<ProjectResult>;
|
|
42
|
+
export declare function initProject(options: InitProjectOptions): Promise<ProjectResult>;
|
|
27
43
|
export declare function renameProject(root: string, options: RenameProjectOptions): Promise<ProjectResult>;
|
|
44
|
+
export declare function updateProject(root: string, options?: UpdateProjectOptions): Promise<UpdateProjectResult>;
|
|
28
45
|
export declare function doctorProject(root: string): Promise<DoctorResult>;
|
package/dist/src/project.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
-
import { basename, dirname, join, resolve } from "node:path";
|
|
1
|
+
import { copyFile, mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import YAML from "yaml";
|
|
5
|
-
import { DEFAULT_AGENT, initializeCapabilities, installSkills, writeMcpEnvironmentExample } from "./capabilities.js";
|
|
5
|
+
import { DEFAULT_AGENT, formatMcpDotenv, initializeCapabilities, installSkills, readCapabilities, renderCapabilityProfile, renderMcpSetup, renderMcpSnippet, writeMcpEnvironmentExample } from "./capabilities.js";
|
|
6
6
|
import { assertKnownAgentTarget } from "./agents.js";
|
|
7
7
|
import { copyDirectory, exists, isNonEmptyDirectory, movePath, readJson, writeJson } from "./files.js";
|
|
8
8
|
import { packageify, slugify, titleFromSlug } from "./names.js";
|
|
@@ -90,11 +90,8 @@ export async function createProject(options) {
|
|
|
90
90
|
const title = options.title ?? titleFromSlug(options.slug ?? basename(target));
|
|
91
91
|
const slug = slugify(options.slug ?? title);
|
|
92
92
|
const packageName = packageify(options.packageName ?? slug);
|
|
93
|
-
const preset = options.preset ?? "default";
|
|
93
|
+
const preset = assertKnownPreset(options.preset ?? "default");
|
|
94
94
|
const agent = assertKnownAgentTarget(options.agent ?? DEFAULT_AGENT);
|
|
95
|
-
if (!AGENT_STACK.presets[preset]) {
|
|
96
|
-
throw new Error(`unknown capability preset: ${preset}. Expected one of: ${Object.keys(AGENT_STACK.presets).join(", ")}`);
|
|
97
|
-
}
|
|
98
95
|
await mkdir(dirname(target), { recursive: true });
|
|
99
96
|
await copyDirectory(templateRoot, target);
|
|
100
97
|
await writeGeneratedGitignore(target);
|
|
@@ -108,6 +105,38 @@ export async function createProject(options) {
|
|
|
108
105
|
}
|
|
109
106
|
return { root: target, title, slug, packageName };
|
|
110
107
|
}
|
|
108
|
+
export async function initProject(options) {
|
|
109
|
+
const target = resolve(options.target);
|
|
110
|
+
const title = options.title ?? titleFromSlug(options.slug ?? basename(target));
|
|
111
|
+
const slug = slugify(options.slug ?? title);
|
|
112
|
+
const packageName = packageify(options.packageName ?? slug);
|
|
113
|
+
const preset = assertKnownPreset(options.preset ?? "default");
|
|
114
|
+
const agent = assertKnownAgentTarget(options.agent ?? DEFAULT_AGENT);
|
|
115
|
+
await mkdir(target, { recursive: true });
|
|
116
|
+
const created = await copyDirectoryMissing(templateRoot, target);
|
|
117
|
+
await writeGeneratedGitignore(target, { overwrite: false });
|
|
118
|
+
const project = await personalizeInitializedProject(target, {
|
|
119
|
+
title,
|
|
120
|
+
slug,
|
|
121
|
+
packageName,
|
|
122
|
+
profile: options.profile ?? "academic-general"
|
|
123
|
+
}, created);
|
|
124
|
+
await writeGeneratedPackageJson(target, { slug: project.slug });
|
|
125
|
+
if (created.has("configs/agent-stack.yaml"))
|
|
126
|
+
await writeAgentStack(target);
|
|
127
|
+
if (created.has(".env.example"))
|
|
128
|
+
await writeMcpEnvironmentExample(target);
|
|
129
|
+
if (created.has("configs/capabilities.yaml")) {
|
|
130
|
+
await initializeCapabilities(target, { preset, agent });
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
await updateManagedCapabilityFiles(target, { apply: true, changes: [] });
|
|
134
|
+
}
|
|
135
|
+
if (options.installSkills) {
|
|
136
|
+
await installSkills(target, preset, { agent });
|
|
137
|
+
}
|
|
138
|
+
return project;
|
|
139
|
+
}
|
|
111
140
|
export async function renameProject(root, options) {
|
|
112
141
|
const target = resolve(root);
|
|
113
142
|
const configPath = join(target, "configs/default.yaml");
|
|
@@ -126,9 +155,23 @@ export async function renameProject(root, options) {
|
|
|
126
155
|
await writeGeneratedPackageJson(target, { slug, preserveExistingSpec: true });
|
|
127
156
|
return { root: target, title, slug, packageName };
|
|
128
157
|
}
|
|
158
|
+
export async function updateProject(root, options = {}) {
|
|
159
|
+
const target = resolve(root);
|
|
160
|
+
const config = await readProjectConfig(target);
|
|
161
|
+
const changes = [];
|
|
162
|
+
await updateGeneratedPackageJson(target, config.project.slug, { apply: options.apply === true, changes });
|
|
163
|
+
await stageTextWrite(target, ".env.example", formatMcpDotenv(Object.keys(AGENT_STACK.mcp_servers)), { apply: options.apply === true, changes });
|
|
164
|
+
await stageTextWrite(target, "configs/agent-stack.yaml", YAML.stringify(AGENT_STACK), {
|
|
165
|
+
apply: options.apply === true,
|
|
166
|
+
changes
|
|
167
|
+
});
|
|
168
|
+
await updateManagedCapabilityFiles(target, { apply: options.apply === true, changes });
|
|
169
|
+
return { root: target, applied: options.apply === true, changes };
|
|
170
|
+
}
|
|
129
171
|
export async function doctorProject(root) {
|
|
130
172
|
const target = resolve(root);
|
|
131
173
|
const errors = [];
|
|
174
|
+
const warnings = [];
|
|
132
175
|
const required = [
|
|
133
176
|
"README.md",
|
|
134
177
|
".gitignore",
|
|
@@ -175,16 +218,82 @@ export async function doctorProject(root) {
|
|
|
175
218
|
}
|
|
176
219
|
if (await exists(join(target, "configs/capabilities.yaml"))) {
|
|
177
220
|
try {
|
|
178
|
-
|
|
221
|
+
const state = await readCapabilities(target);
|
|
222
|
+
const unknown = state.mcp_servers.filter((server) => !AGENT_STACK.mcp_servers[server]);
|
|
223
|
+
if (unknown.length > 0)
|
|
224
|
+
errors.push(`unknown MCP server in configs/capabilities.yaml: ${unknown.join(", ")}`);
|
|
225
|
+
const snippet = renderMcpSnippet(state);
|
|
226
|
+
const commandServers = state.mcp_servers.filter((server) => AGENT_STACK.mcp_servers[server]?.command);
|
|
227
|
+
if (commandServers.length > 0) {
|
|
228
|
+
try {
|
|
229
|
+
const raw = await readFile(join(target, "docs/agent/generated", snippet.fileName), "utf8");
|
|
230
|
+
const generated = JSON.parse(raw);
|
|
231
|
+
for (const server of commandServers) {
|
|
232
|
+
if (!Object.hasOwn(generated.mcpServers ?? {}, server)) {
|
|
233
|
+
errors.push(`${server}: enabled but missing from generated MCP snippet`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
errors.push(`invalid generated MCP snippet: ${error instanceof Error ? error.message : String(error)}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
await validateManagedCapabilityDrift(target, state, warnings);
|
|
179
242
|
}
|
|
180
243
|
catch (error) {
|
|
181
244
|
errors.push(`invalid configs/capabilities.yaml: ${error instanceof Error ? error.message : String(error)}`);
|
|
182
245
|
}
|
|
183
246
|
}
|
|
247
|
+
await validatePackageContract(target, errors, warnings);
|
|
248
|
+
await validateManagedTextDrift(target, warnings);
|
|
249
|
+
await validateStaleCommandReferences(target, warnings);
|
|
184
250
|
for (const [relative, requiredColumns] of Object.entries(REQUIRED_CSV_COLUMNS)) {
|
|
185
251
|
await validateCsvHeader(target, relative, requiredColumns, errors);
|
|
186
252
|
}
|
|
187
|
-
return { ok: errors.length === 0, errors };
|
|
253
|
+
return { ok: errors.length === 0, errors, warnings };
|
|
254
|
+
}
|
|
255
|
+
async function personalizeInitializedProject(root, { title, slug, packageName, profile }, created) {
|
|
256
|
+
const configPath = join(root, "configs/default.yaml");
|
|
257
|
+
let config = await readProjectConfig(root);
|
|
258
|
+
if (created.has("configs/default.yaml")) {
|
|
259
|
+
config.project = {
|
|
260
|
+
...config.project,
|
|
261
|
+
slug,
|
|
262
|
+
title,
|
|
263
|
+
profile,
|
|
264
|
+
package: packageName
|
|
265
|
+
};
|
|
266
|
+
await writeFile(configPath, YAML.stringify(config), "utf8");
|
|
267
|
+
}
|
|
268
|
+
config = await readProjectConfig(root);
|
|
269
|
+
const project = config.project;
|
|
270
|
+
if (created.has("pyproject.toml")) {
|
|
271
|
+
const pyprojectPath = join(root, "pyproject.toml");
|
|
272
|
+
const pyproject = await readFile(pyprojectPath, "utf8");
|
|
273
|
+
await writeFile(pyprojectPath, pyproject.replace(/^name = ".*"$/m, `name = "${project.slug}"`), "utf8");
|
|
274
|
+
}
|
|
275
|
+
if (created.has("README.md")) {
|
|
276
|
+
const readmePath = join(root, "README.md");
|
|
277
|
+
const readme = await readFile(readmePath, "utf8");
|
|
278
|
+
await writeFile(readmePath, readme.replace(/^# .*/m, `# ${project.title}`), "utf8");
|
|
279
|
+
}
|
|
280
|
+
await moveInitializedPythonPackage(root, project.package, created);
|
|
281
|
+
return { root, title: project.title, slug: project.slug, packageName: project.package };
|
|
282
|
+
}
|
|
283
|
+
async function moveInitializedPythonPackage(root, packageName, created) {
|
|
284
|
+
const previous = join(root, "src", "project_package");
|
|
285
|
+
const next = join(root, "src", packageName);
|
|
286
|
+
if (previous !== next && (await exists(previous)) && !(await exists(next))) {
|
|
287
|
+
await movePath(previous, next);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
await mkdir(dirname(join(next, "__init__.py")), { recursive: true });
|
|
291
|
+
if (!(await exists(join(next, "__init__.py")))) {
|
|
292
|
+
await writeFile(join(next, "__init__.py"), "\"\"\"Project package.\"\"\"\n", "utf8");
|
|
293
|
+
}
|
|
294
|
+
if (previous !== next && created.has("src/project_package/__init__.py")) {
|
|
295
|
+
await rm(join(previous, "__init__.py"), { force: true });
|
|
296
|
+
}
|
|
188
297
|
}
|
|
189
298
|
async function personalizeProject(root, { title, slug, packageName, profile, previousPackage = "project_package" }) {
|
|
190
299
|
const configPath = join(root, "configs/default.yaml");
|
|
@@ -218,21 +327,82 @@ async function personalizeProject(root, { title, slug, packageName, profile, pre
|
|
|
218
327
|
async function writeGeneratedPackageJson(root, { slug, preserveExistingSpec = false }) {
|
|
219
328
|
const path = join(root, "package.json");
|
|
220
329
|
const data = await readJson(path);
|
|
330
|
+
const packageSpec = await generatedPackageSpec(data, preserveExistingSpec);
|
|
331
|
+
await writeJson(path, generatedPackageJson(data, slug, packageSpec));
|
|
332
|
+
}
|
|
333
|
+
async function updateGeneratedPackageJson(root, slug, options) {
|
|
334
|
+
const path = join(root, "package.json");
|
|
335
|
+
const data = await readJson(path);
|
|
336
|
+
const packageSpec = await generatedPackageSpec(data, false);
|
|
337
|
+
const next = `${JSON.stringify(generatedPackageJson(data, slug, packageSpec), null, 2)}\n`;
|
|
338
|
+
await stageTextWrite(root, "package.json", next, options);
|
|
339
|
+
}
|
|
340
|
+
async function generatedPackageSpec(data, preserveExistingSpec) {
|
|
221
341
|
const existingSpec = data.devDependencies?.["create-academic-research"];
|
|
222
|
-
|
|
342
|
+
return (process.env.CREATE_ACADEMIC_RESEARCH_PACKAGE_SPEC ??
|
|
223
343
|
(preserveExistingSpec ? existingSpec : undefined) ??
|
|
224
|
-
await currentPackageVersion();
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
344
|
+
(await currentPackageVersion()));
|
|
345
|
+
}
|
|
346
|
+
function generatedPackageJson(data, slug, packageSpec) {
|
|
347
|
+
return {
|
|
348
|
+
...data,
|
|
349
|
+
name: slug,
|
|
350
|
+
scripts: {
|
|
351
|
+
...(data.scripts ?? {}),
|
|
352
|
+
...generatedLifecycleScripts(packageSpec)
|
|
353
|
+
},
|
|
354
|
+
devDependencies: {
|
|
355
|
+
...(data.devDependencies ?? {}),
|
|
356
|
+
"create-academic-research": packageSpec
|
|
357
|
+
}
|
|
229
358
|
};
|
|
230
|
-
await writeJson(path, data);
|
|
231
359
|
}
|
|
232
|
-
|
|
360
|
+
function generatedLifecycleScripts(packageSpec) {
|
|
361
|
+
const command = `npm exec --yes --package=${lifecyclePackageSpec(packageSpec)} -- academic-research`;
|
|
362
|
+
return {
|
|
363
|
+
doctor: `${command} doctor`,
|
|
364
|
+
update: `${command} update`,
|
|
365
|
+
setup: `${command} setup`,
|
|
366
|
+
rename: `${command} rename`,
|
|
367
|
+
"agents:list": `${command} agents list`,
|
|
368
|
+
"skills:install": `${command} skills install`,
|
|
369
|
+
"skills:list": `${command} skills list`,
|
|
370
|
+
"skills:status": `${command} skills status`,
|
|
371
|
+
"skills:presets": `${command} skills presets`,
|
|
372
|
+
"skills:remove": `${command} skills remove`,
|
|
373
|
+
"skills:uninstall": `${command} skills uninstall`,
|
|
374
|
+
"skills:update": `${command} skills update`,
|
|
375
|
+
"mcp:list": `${command} mcp list`,
|
|
376
|
+
"mcp:enabled": `${command} mcp enabled`,
|
|
377
|
+
"mcp:available": `${command} mcp available`,
|
|
378
|
+
"mcp:commands": `${command} mcp commands`,
|
|
379
|
+
"mcp:env": `${command} mcp env`,
|
|
380
|
+
"mcp:dotenv": `${command} mcp env --write .env.example --all`,
|
|
381
|
+
"mcp:enable": `${command} mcp enable`,
|
|
382
|
+
"mcp:disable": `${command} mcp disable`,
|
|
383
|
+
"mcp:install": `${command} mcp install`,
|
|
384
|
+
"mcp:uninstall": `${command} mcp uninstall`,
|
|
385
|
+
"mcp:smoke": `${command} mcp smoke`,
|
|
386
|
+
"mcp:doctor": `${command} mcp doctor`,
|
|
387
|
+
"mcp:probe": `${command} mcp probe`
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
function lifecyclePackageSpec(packageSpec) {
|
|
391
|
+
if (packageSpec === "create-academic-research" || packageSpec.startsWith("create-academic-research@")) {
|
|
392
|
+
return packageSpec;
|
|
393
|
+
}
|
|
394
|
+
if (/^(file:|github:|git[+:]|https?:)/.test(packageSpec) || packageSpec.includes("/")) {
|
|
395
|
+
return packageSpec;
|
|
396
|
+
}
|
|
397
|
+
return `create-academic-research@${packageSpec}`;
|
|
398
|
+
}
|
|
399
|
+
async function writeGeneratedGitignore(root, options = {}) {
|
|
233
400
|
const source = join(root, "_gitignore");
|
|
234
401
|
if (await exists(source)) {
|
|
235
|
-
|
|
402
|
+
const target = join(root, ".gitignore");
|
|
403
|
+
if (options.overwrite !== false || !(await exists(target))) {
|
|
404
|
+
await writeFile(target, await readFile(source, "utf8"), "utf8");
|
|
405
|
+
}
|
|
236
406
|
await rm(source);
|
|
237
407
|
}
|
|
238
408
|
}
|
|
@@ -243,9 +413,179 @@ async function currentPackageVersion() {
|
|
|
243
413
|
}
|
|
244
414
|
return packageJson.version;
|
|
245
415
|
}
|
|
416
|
+
async function readProjectConfig(root) {
|
|
417
|
+
return YAML.parse(await readFile(join(root, "configs/default.yaml"), "utf8"));
|
|
418
|
+
}
|
|
419
|
+
async function updateManagedCapabilityFiles(root, options) {
|
|
420
|
+
const state = await readCapabilities(root);
|
|
421
|
+
await stageTextWrite(root, "docs/agent/capability-profile.md", renderCapabilityProfile(state), options);
|
|
422
|
+
await stageTextWrite(root, "docs/agent/mcp-setup.md", renderMcpSetup(state), options);
|
|
423
|
+
const snippet = renderMcpSnippet(state);
|
|
424
|
+
await stageTextWrite(root, join("docs/agent/generated", snippet.fileName), snippet.content, options);
|
|
425
|
+
}
|
|
426
|
+
async function stageTextWrite(root, relativePath, content, options) {
|
|
427
|
+
const path = join(root, relativePath);
|
|
428
|
+
let current;
|
|
429
|
+
try {
|
|
430
|
+
current = await readFile(path, "utf8");
|
|
431
|
+
}
|
|
432
|
+
catch (error) {
|
|
433
|
+
if (!isMissingFileError(error))
|
|
434
|
+
throw error;
|
|
435
|
+
}
|
|
436
|
+
if (current === content)
|
|
437
|
+
return;
|
|
438
|
+
options.changes.push({ path: toPosix(relativePath), action: current === undefined ? "create" : "update" });
|
|
439
|
+
if (!options.apply)
|
|
440
|
+
return;
|
|
441
|
+
await mkdir(dirname(path), { recursive: true });
|
|
442
|
+
await writeFile(path, content, "utf8");
|
|
443
|
+
}
|
|
444
|
+
async function copyDirectoryMissing(source, target) {
|
|
445
|
+
const created = new Set();
|
|
446
|
+
async function copyChildren(sourceDir, targetDir) {
|
|
447
|
+
await mkdir(targetDir, { recursive: true });
|
|
448
|
+
const entries = await readdir(sourceDir, { withFileTypes: true });
|
|
449
|
+
for (const entry of entries) {
|
|
450
|
+
if (entry.name === "node_modules" || entry.name === "__pycache__")
|
|
451
|
+
continue;
|
|
452
|
+
const sourcePath = join(sourceDir, entry.name);
|
|
453
|
+
const targetPath = join(targetDir, entry.name);
|
|
454
|
+
if (entry.isDirectory()) {
|
|
455
|
+
await copyChildren(sourcePath, targetPath);
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
if (await exists(targetPath))
|
|
459
|
+
continue;
|
|
460
|
+
await mkdir(dirname(targetPath), { recursive: true });
|
|
461
|
+
await copyFile(sourcePath, targetPath);
|
|
462
|
+
created.add(toPosix(relative(target, targetPath)));
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
await copyChildren(source, target);
|
|
466
|
+
return created;
|
|
467
|
+
}
|
|
246
468
|
async function writeAgentStack(root) {
|
|
247
469
|
await writeFile(join(root, "configs/agent-stack.yaml"), YAML.stringify(AGENT_STACK), "utf8");
|
|
248
470
|
}
|
|
471
|
+
async function validatePackageContract(root, errors, warnings) {
|
|
472
|
+
const path = join(root, "package.json");
|
|
473
|
+
if (!(await exists(path)))
|
|
474
|
+
return;
|
|
475
|
+
let data;
|
|
476
|
+
try {
|
|
477
|
+
data = await readJson(path);
|
|
478
|
+
}
|
|
479
|
+
catch (error) {
|
|
480
|
+
errors.push(`invalid package.json: ${error instanceof Error ? error.message : String(error)}`);
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
const packageSpec = data.devDependencies?.["create-academic-research"] ?? (await currentPackageVersion());
|
|
484
|
+
const expectedScripts = generatedLifecycleScripts(packageSpec);
|
|
485
|
+
for (const [name, expected] of Object.entries(expectedScripts)) {
|
|
486
|
+
const actual = data.scripts?.[name];
|
|
487
|
+
if (!actual) {
|
|
488
|
+
warnings.push(`package.json missing lifecycle script: ${name}`);
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
if (isStaleLifecycleCommand(actual)) {
|
|
492
|
+
errors.push(`package.json script ${name} uses stale academic-research invocation`);
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
if (actual !== expected) {
|
|
496
|
+
warnings.push(`package.json script ${name} differs from the current managed command`);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
const current = await currentPackageVersion();
|
|
500
|
+
if (isOlderSimpleVersion(packageSpec, current)) {
|
|
501
|
+
warnings.push(`create-academic-research ${packageSpec} is older than ${current}; run npm run update -- --apply`);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
async function validateManagedTextDrift(root, warnings) {
|
|
505
|
+
const expectedEnv = formatMcpDotenv(Object.keys(AGENT_STACK.mcp_servers));
|
|
506
|
+
await warnIfTextDrift(root, ".env.example", expectedEnv, ".env.example is not current", warnings);
|
|
507
|
+
await warnIfTextDrift(root, "configs/agent-stack.yaml", YAML.stringify(AGENT_STACK), "configs/agent-stack.yaml is not current", warnings);
|
|
508
|
+
}
|
|
509
|
+
async function validateManagedCapabilityDrift(root, state, warnings) {
|
|
510
|
+
await warnIfTextDrift(root, "docs/agent/capability-profile.md", renderCapabilityProfile(state), "docs/agent/capability-profile.md is not current", warnings);
|
|
511
|
+
await warnIfTextDrift(root, "docs/agent/mcp-setup.md", renderMcpSetup(state), "docs/agent/mcp-setup.md is not current", warnings);
|
|
512
|
+
const snippet = renderMcpSnippet(state);
|
|
513
|
+
await warnIfTextDrift(root, join("docs/agent/generated", snippet.fileName), snippet.content, `docs/agent/generated/${snippet.fileName} is not current`, warnings);
|
|
514
|
+
}
|
|
515
|
+
async function warnIfTextDrift(root, relativePath, expected, warning, warnings) {
|
|
516
|
+
try {
|
|
517
|
+
const actual = await readFile(join(root, relativePath), "utf8");
|
|
518
|
+
if (actual !== expected)
|
|
519
|
+
warnings.push(warning);
|
|
520
|
+
}
|
|
521
|
+
catch (error) {
|
|
522
|
+
if (!isMissingFileError(error))
|
|
523
|
+
throw error;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
async function validateStaleCommandReferences(root, warnings) {
|
|
527
|
+
const docs = [
|
|
528
|
+
"README.md",
|
|
529
|
+
"docs/getting-started.md",
|
|
530
|
+
"docs/agent/capability-profile.md",
|
|
531
|
+
"docs/agent/mcp-client-setup.md",
|
|
532
|
+
"docs/agent/mcp-setup.md",
|
|
533
|
+
"scripts/README.md"
|
|
534
|
+
];
|
|
535
|
+
for (const relativePath of docs) {
|
|
536
|
+
try {
|
|
537
|
+
const text = await readFile(join(root, relativePath), "utf8");
|
|
538
|
+
if (containsStaleCommandReference(text)) {
|
|
539
|
+
warnings.push(`stale command reference in ${relativePath}; prefer project npm scripts`);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
catch (error) {
|
|
543
|
+
if (!isMissingFileError(error))
|
|
544
|
+
throw error;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
function containsStaleCommandReference(text) {
|
|
549
|
+
return (/\bnpx\s+academic-research\b/.test(text) ||
|
|
550
|
+
/(^|[`(>\s])academic-research\s+(doctor|setup|rename|agents|skills|mcp)\b/.test(text));
|
|
551
|
+
}
|
|
552
|
+
function isStaleLifecycleCommand(command) {
|
|
553
|
+
return /^academic-research\s+/.test(command) || /\bnpx\s+academic-research\b/.test(command);
|
|
554
|
+
}
|
|
555
|
+
function isOlderSimpleVersion(left, right) {
|
|
556
|
+
const leftParts = parseSimpleVersion(left);
|
|
557
|
+
const rightParts = parseSimpleVersion(right);
|
|
558
|
+
if (!leftParts || !rightParts)
|
|
559
|
+
return false;
|
|
560
|
+
for (let index = 0; index < rightParts.length; index += 1) {
|
|
561
|
+
if (leftParts[index] < rightParts[index])
|
|
562
|
+
return true;
|
|
563
|
+
if (leftParts[index] > rightParts[index])
|
|
564
|
+
return false;
|
|
565
|
+
}
|
|
566
|
+
return false;
|
|
567
|
+
}
|
|
568
|
+
function parseSimpleVersion(value) {
|
|
569
|
+
const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(value);
|
|
570
|
+
if (!match)
|
|
571
|
+
return undefined;
|
|
572
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
573
|
+
}
|
|
574
|
+
function assertKnownPreset(preset) {
|
|
575
|
+
if (!AGENT_STACK.presets[preset]) {
|
|
576
|
+
throw new Error(`unknown capability preset: ${preset}. Expected one of: ${Object.keys(AGENT_STACK.presets).join(", ")}`);
|
|
577
|
+
}
|
|
578
|
+
return preset;
|
|
579
|
+
}
|
|
580
|
+
function isMissingFileError(error) {
|
|
581
|
+
return (typeof error === "object" &&
|
|
582
|
+
error !== null &&
|
|
583
|
+
"code" in error &&
|
|
584
|
+
error.code === "ENOENT");
|
|
585
|
+
}
|
|
586
|
+
function toPosix(value) {
|
|
587
|
+
return value.split(/[\\/]/).join("/");
|
|
588
|
+
}
|
|
249
589
|
async function validateCsvHeader(root, relative, requiredColumns, errors) {
|
|
250
590
|
const path = join(root, relative);
|
|
251
591
|
if (!(await exists(path)))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-academic-research",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"description": "Scaffold agent-ready academic research repositories with SOTA, source ledgers, wiki memory, MCP setup, and project-local skills.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/template/README.md
CHANGED
|
@@ -30,7 +30,8 @@ python3.11 -m venv .venv
|
|
|
30
30
|
source .venv/bin/activate
|
|
31
31
|
python -m pip install --upgrade pip
|
|
32
32
|
python -m pip install -e ".[dev]"
|
|
33
|
-
|
|
33
|
+
npm run doctor
|
|
34
|
+
npm run update
|
|
34
35
|
```
|
|
35
36
|
|
|
36
37
|
## Core Folders
|
|
@@ -53,23 +54,24 @@ npx academic-research doctor
|
|
|
53
54
|
Project-local skills and MCP records are managed with:
|
|
54
55
|
|
|
55
56
|
```bash
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
57
|
+
npm run skills:presets
|
|
58
|
+
npm run agents:list
|
|
59
|
+
npm run skills:install
|
|
60
|
+
npm run skills:install -- --preset enhanced
|
|
61
|
+
npm run skills:install -- source-ingestion sota-literature-review
|
|
62
|
+
npm run skills:list
|
|
63
|
+
npm run skills:status
|
|
64
|
+
npm run update
|
|
65
|
+
npm run setup
|
|
66
|
+
npm run mcp:dotenv
|
|
67
|
+
npm run mcp:list
|
|
68
|
+
npm run mcp:env -- openalex semantic-scholar zotero
|
|
69
|
+
npm run mcp:enable -- arxiv dblp
|
|
70
|
+
npm run mcp:commands -- arxiv
|
|
71
|
+
npm run mcp:install -- arxiv
|
|
72
|
+
npm run mcp:smoke -- --env-file .env.local
|
|
73
|
+
npm run mcp:doctor -- --env-file .env.local
|
|
74
|
+
npm run mcp:probe -- arxiv --timeout-ms 5000
|
|
73
75
|
```
|
|
74
76
|
|
|
75
77
|
`skills list` reports installed project-local skills. `skills presets` reports
|
|
@@ -81,10 +83,10 @@ commands; runtime-only `uvx`/`npx` MCP servers may have no install step and are
|
|
|
81
83
|
started later by the MCP client.
|
|
82
84
|
|
|
83
85
|
`.env.example` is the committed MCP environment reference. Regenerate it with
|
|
84
|
-
`
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
`npm run mcp:dotenv`. Copy it to `.env.local`, your shell profile, or your MCP
|
|
87
|
+
client secret store when secrets are needed. Filled `.env` files are ignored by
|
|
88
|
+
git. `mcp doctor` checks the current process environment unless you explicitly
|
|
89
|
+
pass `--env-file .env.local`.
|
|
88
90
|
|
|
89
91
|
`setup` prints the current project capability state, installed skill counts,
|
|
90
92
|
enabled MCP records, and the next onboarding commands without changing files.
|
|
@@ -19,7 +19,7 @@ store.
|
|
|
19
19
|
Regenerate the committed reference from the current MCP catalog with:
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
|
-
|
|
22
|
+
npm run mcp:dotenv
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
Create a private local file when needed:
|
|
@@ -33,9 +33,9 @@ Do not commit filled `.env`, `.env.local`, tokens, cookies, or browser sessions.
|
|
|
33
33
|
environment unless you explicitly pass `--env-file .env.local`.
|
|
34
34
|
|
|
35
35
|
```bash
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
npm run mcp:doctor -- --env-file .env.local
|
|
37
|
+
npm run mcp:smoke -- --env-file .env.local
|
|
38
|
+
npm run mcp:probe -- arxiv --timeout-ms 5000
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
## Client Notes
|
|
@@ -57,10 +57,10 @@ required variables are already exported.
|
|
|
57
57
|
## Workflow
|
|
58
58
|
|
|
59
59
|
1. Enable only the MCP servers needed for the current research task.
|
|
60
|
-
2. Inspect prerequisites with `
|
|
60
|
+
2. Inspect prerequisites with `npm run mcp:env -- <server>`.
|
|
61
61
|
3. Put required secrets in the MCP client secret store, shell, or `.env.local`.
|
|
62
|
-
4. Run `
|
|
63
|
-
5. Run `
|
|
62
|
+
4. Run `npm run mcp:smoke -- --env-file .env.local`.
|
|
63
|
+
5. Run `npm run mcp:probe -- <server>` only when you want to start
|
|
64
64
|
the server and verify a real stdio handshake.
|
|
65
65
|
6. Load the generated snippet in the MCP client.
|
|
66
66
|
7. Treat MCP output as retrieval metadata until it is ingested into repository
|
|
@@ -5,9 +5,8 @@ and known risks here.
|
|
|
5
5
|
|
|
6
6
|
Use `.env.example` as the committed environment reference. Put filled values in
|
|
7
7
|
`.env.local`, the shell, or the MCP client secret store. Regenerate the example
|
|
8
|
-
with `
|
|
8
|
+
with `npm run mcp:dotenv`.
|
|
9
9
|
|
|
10
|
-
Use `
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
stdio handshake.
|
|
10
|
+
Use `npm run mcp:doctor -- --env-file .env.local` when you want the CLI to read
|
|
11
|
+
an explicit local env file. Use `npm run mcp:probe -- <server>` only when you
|
|
12
|
+
want to start a selected MCP server and verify a real stdio handshake.
|
|
@@ -6,28 +6,30 @@ Use this path for the first working session in a new research repository.
|
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
npm install
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
npm run doctor
|
|
10
|
+
npm run update
|
|
11
|
+
npm run setup
|
|
11
12
|
```
|
|
12
13
|
|
|
13
|
-
`doctor` checks required files and structural contracts. `
|
|
14
|
-
|
|
15
|
-
commands without changing
|
|
14
|
+
`doctor` checks required files and structural contracts. `update` is a dry-run
|
|
15
|
+
unless you pass `-- --apply`. `setup` prints the active skill preset,
|
|
16
|
+
installed skill count, enabled MCP records, and next commands without changing
|
|
17
|
+
files.
|
|
16
18
|
|
|
17
19
|
## 2. Install Project-Local Skills
|
|
18
20
|
|
|
19
21
|
Install the default academic research skill package:
|
|
20
22
|
|
|
21
23
|
```bash
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
npm run skills:install
|
|
25
|
+
npm run skills:status
|
|
24
26
|
```
|
|
25
27
|
|
|
26
28
|
Use `enhanced` only when the project also needs complementary development,
|
|
27
29
|
document, frontend, testing, and conversion skills:
|
|
28
30
|
|
|
29
31
|
```bash
|
|
30
|
-
|
|
32
|
+
npm run skills:install -- --preset enhanced
|
|
31
33
|
```
|
|
32
34
|
|
|
33
35
|
## 3. Prepare MCP Environment
|
|
@@ -36,18 +38,18 @@ Keep `.env.example` committed and empty of real secrets. Put filled values in
|
|
|
36
38
|
`.env.local`, your shell, or your MCP client secret store.
|
|
37
39
|
|
|
38
40
|
```bash
|
|
39
|
-
|
|
41
|
+
npm run mcp:dotenv
|
|
40
42
|
cp .env.example .env.local
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
npm run mcp:env -- openalex semantic-scholar zotero
|
|
44
|
+
npm run mcp:doctor -- --env-file .env.local
|
|
43
45
|
```
|
|
44
46
|
|
|
45
47
|
`mcp smoke` is a non-launching readiness check. `mcp probe` is opt-in and starts
|
|
46
48
|
MCP processes for a real stdio handshake.
|
|
47
49
|
|
|
48
50
|
```bash
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
npm run mcp:smoke -- --env-file .env.local
|
|
52
|
+
npm run mcp:probe -- arxiv --timeout-ms 5000
|
|
51
53
|
```
|
|
52
54
|
|
|
53
55
|
## 4. Start Source Work
|
package/template/package.json
CHANGED
|
@@ -3,20 +3,33 @@
|
|
|
3
3
|
"private": true,
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
|
-
"doctor": "academic-research doctor",
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
6
|
+
"doctor": "npm exec --yes --package=create-academic-research@latest -- academic-research doctor",
|
|
7
|
+
"update": "npm exec --yes --package=create-academic-research@latest -- academic-research update",
|
|
8
|
+
"setup": "npm exec --yes --package=create-academic-research@latest -- academic-research setup",
|
|
9
|
+
"rename": "npm exec --yes --package=create-academic-research@latest -- academic-research rename",
|
|
10
|
+
"agents:list": "npm exec --yes --package=create-academic-research@latest -- academic-research agents list",
|
|
11
|
+
"skills:install": "npm exec --yes --package=create-academic-research@latest -- academic-research skills install",
|
|
12
|
+
"skills:list": "npm exec --yes --package=create-academic-research@latest -- academic-research skills list",
|
|
13
|
+
"skills:status": "npm exec --yes --package=create-academic-research@latest -- academic-research skills status",
|
|
14
|
+
"skills:presets": "npm exec --yes --package=create-academic-research@latest -- academic-research skills presets",
|
|
15
|
+
"skills:remove": "npm exec --yes --package=create-academic-research@latest -- academic-research skills remove",
|
|
16
|
+
"skills:uninstall": "npm exec --yes --package=create-academic-research@latest -- academic-research skills uninstall",
|
|
17
|
+
"skills:update": "npm exec --yes --package=create-academic-research@latest -- academic-research skills update",
|
|
18
|
+
"mcp:list": "npm exec --yes --package=create-academic-research@latest -- academic-research mcp list",
|
|
19
|
+
"mcp:enabled": "npm exec --yes --package=create-academic-research@latest -- academic-research mcp enabled",
|
|
20
|
+
"mcp:available": "npm exec --yes --package=create-academic-research@latest -- academic-research mcp available",
|
|
21
|
+
"mcp:commands": "npm exec --yes --package=create-academic-research@latest -- academic-research mcp commands",
|
|
22
|
+
"mcp:env": "npm exec --yes --package=create-academic-research@latest -- academic-research mcp env",
|
|
23
|
+
"mcp:dotenv": "npm exec --yes --package=create-academic-research@latest -- academic-research mcp env --write .env.example --all",
|
|
24
|
+
"mcp:enable": "npm exec --yes --package=create-academic-research@latest -- academic-research mcp enable",
|
|
25
|
+
"mcp:disable": "npm exec --yes --package=create-academic-research@latest -- academic-research mcp disable",
|
|
26
|
+
"mcp:install": "npm exec --yes --package=create-academic-research@latest -- academic-research mcp install",
|
|
27
|
+
"mcp:uninstall": "npm exec --yes --package=create-academic-research@latest -- academic-research mcp uninstall",
|
|
28
|
+
"mcp:smoke": "npm exec --yes --package=create-academic-research@latest -- academic-research mcp smoke",
|
|
29
|
+
"mcp:doctor": "npm exec --yes --package=create-academic-research@latest -- academic-research mcp doctor",
|
|
30
|
+
"mcp:probe": "npm exec --yes --package=create-academic-research@latest -- academic-research mcp probe"
|
|
18
31
|
},
|
|
19
32
|
"devDependencies": {
|
|
20
|
-
"create-academic-research": "0.1.
|
|
33
|
+
"create-academic-research": "0.1.14"
|
|
21
34
|
}
|
|
22
35
|
}
|