@research-copilot/core 1.0.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ldm2060
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,235 @@
1
+ declare const KINDS: readonly ["literature", "ideation", "experiment", "writing", "polish", "review", "rebuttal"];
2
+ type Kind = (typeof KINDS)[number];
3
+ declare const STATUSES: readonly ["planning", "in_progress", "verify", "completed"];
4
+ type Status = (typeof STATUSES)[number];
5
+ type Priority = "P0" | "P1" | "P2" | "P3";
6
+ interface Gap {
7
+ desc: string;
8
+ suggest_kind: Kind;
9
+ status: "open" | "resolved";
10
+ }
11
+ interface TaskRecord {
12
+ id: string;
13
+ title: string;
14
+ kind: Kind;
15
+ status: Status;
16
+ priority: Priority;
17
+ venue?: string;
18
+ parent?: string;
19
+ children: string[];
20
+ depends_on: string[];
21
+ gaps: Gap[];
22
+ branch?: string;
23
+ created: string;
24
+ updated: string;
25
+ }
26
+ declare function isKind(x: string): x is Kind;
27
+
28
+ interface ResearchPaths {
29
+ root: string;
30
+ tasks: string;
31
+ spec: string;
32
+ workspace: string;
33
+ runtime: string;
34
+ workflow: string;
35
+ config: string;
36
+ activeTask: string;
37
+ graphIndex: string;
38
+ taskDir(id: string): string;
39
+ }
40
+ declare function researchPaths(repoRoot: string): ResearchPaths;
41
+
42
+ declare function slugify(title: string): string;
43
+ interface CreateInput {
44
+ title: string;
45
+ kind: Kind;
46
+ date: string;
47
+ priority?: Priority;
48
+ venue?: string;
49
+ parent?: string;
50
+ now?: string;
51
+ }
52
+ declare function createTask(repo: string, input: CreateInput): TaskRecord;
53
+ declare function taskJsonPath(repo: string, id: string): string;
54
+ declare function writeTask(repo: string, task: TaskRecord, now?: string): void;
55
+ declare function readTask(repo: string, id: string): TaskRecord;
56
+ declare function listTasks(repo: string): TaskRecord[];
57
+ declare function setStatus(repo: string, id: string, to: Status, now: string): void;
58
+
59
+ declare const TRANSITIONS: Record<Status, Status[]>;
60
+ declare function nextStatuses(from: Status): Status[];
61
+ declare function canTransition(from: Status, to: Status): boolean;
62
+ declare function assertTransition(from: Status, to: Status): void;
63
+
64
+ interface GraphNode {
65
+ task: TaskRecord;
66
+ blocked: boolean;
67
+ dependents: string[];
68
+ }
69
+ type Graph = Map<string, GraphNode>;
70
+ declare function buildGraph(tasks: TaskRecord[]): Graph;
71
+
72
+ interface Recommendation {
73
+ action: "resume" | "create";
74
+ taskId?: string;
75
+ suggestKind?: Kind;
76
+ reason: string;
77
+ sourceGap?: string;
78
+ score: number;
79
+ }
80
+ interface ResearchState {
81
+ active: {
82
+ id: string;
83
+ kind: Kind;
84
+ status: Status;
85
+ } | null;
86
+ graph: {
87
+ completed: number;
88
+ in_progress: number;
89
+ blocked: number;
90
+ planning: number;
91
+ };
92
+ openGaps: {
93
+ taskId: string;
94
+ desc: string;
95
+ suggest_kind: Kind;
96
+ }[];
97
+ recommendations: Recommendation[];
98
+ turnTs: string;
99
+ }
100
+ declare function computeResearchState(tasks: TaskRecord[], now: string, activeId?: string): ResearchState;
101
+
102
+ declare function extractWorkflowState(md: string, state: Status | "no_task"): string | null;
103
+
104
+ type Phase = "execute" | "verify";
105
+ interface ContextRef {
106
+ type: "spec" | "context";
107
+ path: string;
108
+ reason: string;
109
+ }
110
+ interface VerifyRow {
111
+ check: string;
112
+ kind: string;
113
+ args?: Record<string, unknown>;
114
+ }
115
+ declare function readPrdGoal(repo: string, id: string): string | null;
116
+ declare function appendContext(repo: string, id: string, phase: Phase, row: ContextRef | VerifyRow): void;
117
+ declare function readContext(repo: string, id: string, phase: Phase): unknown[];
118
+
119
+ interface CheckResult {
120
+ ok: boolean;
121
+ missing: string[];
122
+ }
123
+ declare function numberTraceability(draft: string, artifactsText: string): CheckResult;
124
+ declare function citationCompliance(tex: string, bibtex: string): CheckResult;
125
+
126
+ declare function setActive(repo: string, id: string): void;
127
+ declare function getActive(repo: string): string | null;
128
+
129
+ interface BuildOptions {
130
+ format: "text" | "json";
131
+ now: string;
132
+ eventName?: string;
133
+ }
134
+ declare function renderResearchState(rs: ResearchState): string;
135
+ declare function buildContext(repo: string, opts: BuildOptions): string;
136
+
137
+ /**
138
+ * Skillpack manifest schema
139
+ *
140
+ * A skillpack is a git repository containing:
141
+ * - agents/*.md (agent definitions)
142
+ * - specs/*.md (task specifications)
143
+ * - meta.yaml (pack metadata)
144
+ */
145
+ interface SkillpackManifest {
146
+ /** Pack identifier (kebab-case) */
147
+ name: string;
148
+ /** Human-readable description */
149
+ description: string;
150
+ /** Git repository URL */
151
+ source: string;
152
+ /** Version constraint (git tag/branch, semver-style) */
153
+ version?: string;
154
+ /** Whether this pack is enabled by default */
155
+ enabled?: boolean;
156
+ }
157
+ interface SkillpacksYaml {
158
+ /** List of available skillpacks */
159
+ packs: SkillpackManifest[];
160
+ }
161
+ /**
162
+ * Resolved skillpack with local filesystem path
163
+ */
164
+ interface ResolvedSkillpack extends SkillpackManifest {
165
+ /** Local cache path where pack is stored */
166
+ localPath: string;
167
+ /** Resolved git ref (commit SHA) */
168
+ resolvedRef: string;
169
+ }
170
+
171
+ /**
172
+ * Parse skillpacks.yaml file
173
+ *
174
+ * @param path - Path to skillpacks.yaml
175
+ * @returns Parsed skillpacks manifest
176
+ * @throws If file doesn't exist or YAML is invalid
177
+ */
178
+ declare function parseSkillpacks(path: string): SkillpacksYaml;
179
+
180
+ /**
181
+ * Resolve and fetch skillpacks to local cache
182
+ *
183
+ * @param packs - List of pack manifests from skillpacks.yaml
184
+ * @param cacheDir - Directory to store cached packs (e.g., ~/.cache/research-copilot/skillpacks)
185
+ * @returns List of resolved packs with local paths
186
+ */
187
+ declare function resolveSkillpacks(packs: SkillpackManifest[], cacheDir: string): ResolvedSkillpack[];
188
+ /**
189
+ * Remove cached skillpack
190
+ *
191
+ * @param packName - Name of pack to remove
192
+ * @param cacheDir - Cache directory
193
+ */
194
+ declare function removeSkillpack(packName: string, cacheDir: string): void;
195
+
196
+ /**
197
+ * Lock file entry for a synced skillpack
198
+ */
199
+ interface SkillpackLockEntry {
200
+ name: string;
201
+ source: string;
202
+ resolvedRef: string;
203
+ syncedAt: string;
204
+ agentCount: number;
205
+ specCount: number;
206
+ }
207
+ interface SkillpackLock {
208
+ syncedAt: string;
209
+ packs: SkillpackLockEntry[];
210
+ }
211
+ /**
212
+ * Sync skillpacks from skillpacks.yaml to target directory
213
+ *
214
+ * @param repoRoot - Repository root (where skillpacks.yaml lives)
215
+ * @param cacheDir - Cache directory for cloned packs
216
+ * @param targetDir - Target directory to write agents/specs (e.g., research-kit/)
217
+ * @returns Lock data recording what was synced
218
+ */
219
+ declare function syncSkillpacks(repoRoot: string, cacheDir: string, targetDir: string): SkillpackLock;
220
+ /**
221
+ * Write skillpacks lock file
222
+ *
223
+ * @param lockPath - Path to skillpacks.lock.yaml
224
+ * @param lock - Lock data
225
+ */
226
+ declare function writeLockFile(lockPath: string, lock: SkillpackLock): void;
227
+ /**
228
+ * Read skillpacks lock file
229
+ *
230
+ * @param lockPath - Path to skillpacks.lock.yaml
231
+ * @returns Lock data, or null if file doesn't exist
232
+ */
233
+ declare function readLockFile(lockPath: string): SkillpackLock | null;
234
+
235
+ export { type BuildOptions, type CheckResult, type ContextRef, type CreateInput, type Gap, type Graph, type GraphNode, KINDS, type Kind, type Phase, type Priority, type Recommendation, type ResearchPaths, type ResearchState, type ResolvedSkillpack, STATUSES, type SkillpackLock, type SkillpackLockEntry, type SkillpackManifest, type SkillpacksYaml, type Status, TRANSITIONS, type TaskRecord, type VerifyRow, appendContext, assertTransition, buildContext, buildGraph, canTransition, citationCompliance, computeResearchState, createTask, extractWorkflowState, getActive, isKind, listTasks, nextStatuses, numberTraceability, parseSkillpacks, readContext, readLockFile, readPrdGoal, readTask, removeSkillpack, renderResearchState, researchPaths, resolveSkillpacks, setActive, setStatus, slugify, syncSkillpacks, taskJsonPath, writeLockFile, writeTask };
package/dist/index.js ADDED
@@ -0,0 +1,463 @@
1
+ // src/types.ts
2
+ var KINDS = [
3
+ "literature",
4
+ "ideation",
5
+ "experiment",
6
+ "writing",
7
+ "polish",
8
+ "review",
9
+ "rebuttal"
10
+ ];
11
+ var STATUSES = ["planning", "in_progress", "verify", "completed"];
12
+ function isKind(x) {
13
+ return KINDS.includes(x);
14
+ }
15
+
16
+ // src/paths.ts
17
+ import * as path from "path";
18
+ function researchPaths(repoRoot) {
19
+ const root = path.join(repoRoot, ".research");
20
+ const runtime = path.join(root, ".runtime");
21
+ return {
22
+ root,
23
+ tasks: path.join(root, "tasks"),
24
+ spec: path.join(root, "spec"),
25
+ workspace: path.join(root, "workspace"),
26
+ runtime,
27
+ workflow: path.join(root, "workflow.md"),
28
+ config: path.join(root, "config.yaml"),
29
+ activeTask: path.join(runtime, "active-task"),
30
+ graphIndex: path.join(runtime, "graph-index.json"),
31
+ taskDir: (id) => path.join(root, "tasks", id)
32
+ };
33
+ }
34
+
35
+ // src/task-store.ts
36
+ import * as fs from "fs";
37
+ import * as path2 from "path";
38
+
39
+ // src/lifecycle.ts
40
+ var TRANSITIONS = {
41
+ planning: ["in_progress"],
42
+ in_progress: ["verify"],
43
+ verify: ["in_progress", "completed"],
44
+ completed: []
45
+ };
46
+ function nextStatuses(from) {
47
+ return TRANSITIONS[from];
48
+ }
49
+ function canTransition(from, to) {
50
+ return TRANSITIONS[from].includes(to);
51
+ }
52
+ function assertTransition(from, to) {
53
+ if (!canTransition(from, to)) {
54
+ throw new Error(`illegal transition: ${from} -> ${to} (allowed: ${TRANSITIONS[from].join(", ") || "none"})`);
55
+ }
56
+ }
57
+
58
+ // src/task-store.ts
59
+ function slugify(title) {
60
+ return title.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
61
+ }
62
+ function createTask(repo, input) {
63
+ const id = `${input.date}-${slugify(input.title)}`;
64
+ const now = input.now ?? input.date + "T00:00:00Z";
65
+ const task = {
66
+ id,
67
+ title: input.title,
68
+ kind: input.kind,
69
+ status: "planning",
70
+ priority: input.priority ?? "P2",
71
+ venue: input.venue,
72
+ parent: input.parent,
73
+ children: [],
74
+ depends_on: [],
75
+ gaps: [],
76
+ created: now,
77
+ updated: now
78
+ };
79
+ writeTask(repo, task, now);
80
+ return task;
81
+ }
82
+ function taskJsonPath(repo, id) {
83
+ return path2.join(researchPaths(repo).taskDir(id), "task.json");
84
+ }
85
+ function writeTask(repo, task, now) {
86
+ if (now) task.updated = now;
87
+ const dir2 = researchPaths(repo).taskDir(task.id);
88
+ fs.mkdirSync(path2.join(dir2, "research"), { recursive: true });
89
+ fs.mkdirSync(path2.join(dir2, "artifacts"), { recursive: true });
90
+ fs.writeFileSync(taskJsonPath(repo, task.id), JSON.stringify(task, null, 2) + "\n", "utf8");
91
+ }
92
+ function readTask(repo, id) {
93
+ return JSON.parse(fs.readFileSync(taskJsonPath(repo, id), "utf8"));
94
+ }
95
+ function listTasks(repo) {
96
+ const dir2 = researchPaths(repo).tasks;
97
+ if (!fs.existsSync(dir2)) return [];
98
+ return fs.readdirSync(dir2).filter((id) => fs.existsSync(taskJsonPath(repo, id))).map((id) => readTask(repo, id));
99
+ }
100
+ function setStatus(repo, id, to, now) {
101
+ const t = readTask(repo, id);
102
+ assertTransition(t.status, to);
103
+ t.status = to;
104
+ writeTask(repo, t, now);
105
+ }
106
+
107
+ // src/graph.ts
108
+ function buildGraph(tasks) {
109
+ const byId = new Map(tasks.map((t) => [t.id, t]));
110
+ const g = /* @__PURE__ */ new Map();
111
+ for (const t of tasks) {
112
+ const blocked = t.depends_on.some((d) => byId.get(d)?.status !== "completed");
113
+ g.set(t.id, { task: t, blocked, dependents: [] });
114
+ }
115
+ for (const t of tasks) {
116
+ for (const d of t.depends_on) {
117
+ g.get(d)?.dependents.push(t.id);
118
+ }
119
+ }
120
+ return g;
121
+ }
122
+
123
+ // src/research-state.ts
124
+ var PRIORITY_RANK = { P0: 3, P1: 2, P2: 1, P3: 0 };
125
+ var LIFECYCLE_BONUS = { in_progress: 2, verify: 1, planning: 0.5, completed: 0 };
126
+ var W = { priority: 3, unblocking: 2, lifecycle: 1, age: 1e-3 };
127
+ var MAX_RECS = 3;
128
+ function computeResearchState(tasks, now, activeId) {
129
+ const g = buildGraph(tasks);
130
+ const counts = { completed: 0, in_progress: 0, blocked: 0, planning: 0 };
131
+ for (const n of g.values()) {
132
+ if (n.blocked) counts.blocked++;
133
+ if (n.task.status === "completed") counts.completed++;
134
+ else if (n.task.status === "in_progress") counts.in_progress++;
135
+ else if (n.task.status === "planning") counts.planning++;
136
+ }
137
+ const openGaps = tasks.flatMap((t) => t.gaps.filter((gp) => gp.status === "open").map((gp) => ({ taskId: t.id, desc: gp.desc, suggest_kind: gp.suggest_kind })));
138
+ const ageDays = (iso) => Math.max(0, (Date.parse(now) - Date.parse(iso)) / 864e5);
139
+ const recs = [];
140
+ for (const n of g.values()) {
141
+ if (n.task.status === "completed" || n.blocked) continue;
142
+ const score = W.priority * PRIORITY_RANK[n.task.priority] + W.lifecycle * LIFECYCLE_BONUS[n.task.status] + W.age * ageDays(n.task.updated);
143
+ recs.push({
144
+ action: "resume",
145
+ taskId: n.task.id,
146
+ score,
147
+ reason: `resume ${n.task.kind} task ${n.task.id} (${n.task.status})`
148
+ });
149
+ }
150
+ for (const t of tasks) {
151
+ const dependents = g.get(t.id)?.dependents.length ?? 0;
152
+ for (const gp of t.gaps.filter((x) => x.status === "open")) {
153
+ const score = W.priority * PRIORITY_RANK[t.priority] + W.unblocking * dependents + W.lifecycle * LIFECYCLE_BONUS[t.status];
154
+ recs.push({
155
+ action: "create",
156
+ suggestKind: gp.suggest_kind,
157
+ sourceGap: gp.desc,
158
+ score,
159
+ reason: `create ${gp.suggest_kind} task to resolve "${gp.desc}" (from ${t.id})`
160
+ });
161
+ }
162
+ }
163
+ recs.sort((x, y) => y.score - x.score || (x.taskId ?? x.sourceGap ?? "").localeCompare(y.taskId ?? y.sourceGap ?? ""));
164
+ const active = activeId ? (() => {
165
+ const n = g.get(activeId);
166
+ return n ? { id: n.task.id, kind: n.task.kind, status: n.task.status } : null;
167
+ })() : null;
168
+ return { active, graph: counts, openGaps, recommendations: recs.slice(0, MAX_RECS), turnTs: now };
169
+ }
170
+
171
+ // src/workflow.ts
172
+ function extractWorkflowState(md, state) {
173
+ const re = new RegExp(
174
+ `\\[workflow-state:${state}\\]\\r?\\n([\\s\\S]*?)\\r?\\n\\[/workflow-state\\]`
175
+ );
176
+ const m = md.match(re);
177
+ return m ? m[1].trim() : null;
178
+ }
179
+
180
+ // src/artifacts.ts
181
+ import * as fs2 from "fs";
182
+ import * as path3 from "path";
183
+ function dir(repo, id) {
184
+ return researchPaths(repo).taskDir(id);
185
+ }
186
+ function readPrdGoal(repo, id) {
187
+ const p = path3.join(dir(repo, id), "prd.md");
188
+ if (!fs2.existsSync(p)) return null;
189
+ const md = fs2.readFileSync(p, "utf8");
190
+ const m = md.match(/##\s*Goal\s*\r?\n([\s\S]*?)(\r?\n\s*\r?\n|\r?\n##|$)/);
191
+ return m ? m[1].trim() : null;
192
+ }
193
+ function appendContext(repo, id, phase, row) {
194
+ const p = path3.join(dir(repo, id), `${phase}.jsonl`);
195
+ fs2.appendFileSync(p, JSON.stringify(row) + "\n", "utf8");
196
+ }
197
+ function readContext(repo, id, phase) {
198
+ const p = path3.join(dir(repo, id), `${phase}.jsonl`);
199
+ if (!fs2.existsSync(p)) return [];
200
+ return fs2.readFileSync(p, "utf8").split("\n").filter(Boolean).map((l) => JSON.parse(l));
201
+ }
202
+
203
+ // src/verify.ts
204
+ var NUMBER_RE = /-?\d+(?:\.\d+)?/g;
205
+ var norm = (s) => s.replace(/^(-?)0+(\d)/, "$1$2");
206
+ function numberTraceability(draft, artifactsText) {
207
+ const present = new Set((artifactsText.match(NUMBER_RE) ?? []).map(norm));
208
+ const missing = [];
209
+ for (const tok of draft.match(NUMBER_RE) ?? []) {
210
+ if (!present.has(norm(tok)) && !missing.includes(tok)) missing.push(tok);
211
+ }
212
+ return { ok: missing.length === 0, missing };
213
+ }
214
+ function citationCompliance(tex, bibtex) {
215
+ const keys = /* @__PURE__ */ new Set();
216
+ for (const m of bibtex.matchAll(/@\w+\s*\{\s*([^,\s}]+)/g)) keys.add(m[1]);
217
+ const missing = [];
218
+ for (const m of tex.matchAll(/\\cite[a-zA-Z]*(?:\[[^\]]*\])*\{([^}]*)\}/g)) {
219
+ for (const key of m[1].split(",").map((k) => k.trim()).filter(Boolean)) {
220
+ if (!keys.has(key) && !missing.includes(key)) missing.push(key);
221
+ }
222
+ }
223
+ return { ok: missing.length === 0, missing };
224
+ }
225
+
226
+ // src/active.ts
227
+ import * as fs3 from "fs";
228
+ function setActive(repo, id) {
229
+ const p = researchPaths(repo);
230
+ fs3.mkdirSync(p.runtime, { recursive: true });
231
+ fs3.writeFileSync(p.activeTask, id, "utf8");
232
+ }
233
+ function getActive(repo) {
234
+ const p = researchPaths(repo).activeTask;
235
+ return fs3.existsSync(p) ? fs3.readFileSync(p, "utf8").trim() || null : null;
236
+ }
237
+
238
+ // src/context.ts
239
+ import * as fs4 from "fs";
240
+ function renderResearchState(rs) {
241
+ const lines = ["[research-state]"];
242
+ lines.push(`Active: ${rs.active ? `${rs.active.id} (${rs.active.kind}, ${rs.active.status})` : "none"}`);
243
+ lines.push(`Graph: ${rs.graph.completed} completed \xB7 ${rs.graph.in_progress} in_progress \xB7 ${rs.graph.blocked} blocked`);
244
+ if (rs.openGaps.length) {
245
+ lines.push("Open gaps:");
246
+ for (const g of rs.openGaps) lines.push(` - [from ${g.taskId}] ${g.desc} -> suggests: ${g.suggest_kind}`);
247
+ }
248
+ if (rs.recommendations.length) {
249
+ lines.push("Recommended next (you decide, nothing auto-created):");
250
+ rs.recommendations.forEach((r, i) => lines.push(` ${i + 1}. ${r.reason}`));
251
+ }
252
+ lines.push(`turn-ts: ${rs.turnTs}`);
253
+ return lines.join("\n");
254
+ }
255
+ function buildContext(repo, opts) {
256
+ const tasks = listTasks(repo);
257
+ const active = getActive(repo) ?? void 0;
258
+ const activeStatus = active ? tasks.find((t) => t.id === active)?.status : void 0;
259
+ const rs = computeResearchState(tasks, opts.now, active);
260
+ const wfPath = researchPaths(repo).workflow;
261
+ const wfMd = fs4.existsSync(wfPath) ? fs4.readFileSync(wfPath, "utf8") : "";
262
+ const stateKey = activeStatus ?? "no_task";
263
+ const wfBlock = extractWorkflowState(wfMd, stateKey) ?? "Refer to workflow.md for current step.";
264
+ const text = `[workflow-state:${stateKey}]
265
+ ${wfBlock}
266
+ [/workflow-state]
267
+
268
+ ${renderResearchState(rs)}`;
269
+ if (opts.format === "json") {
270
+ return JSON.stringify({
271
+ hookSpecificOutput: { hookEventName: opts.eventName ?? "UserPromptSubmit", additionalContext: text }
272
+ });
273
+ }
274
+ return text;
275
+ }
276
+
277
+ // src/parse-skillpacks.ts
278
+ import { readFileSync as readFileSync5 } from "fs";
279
+ import { parse as parseYaml } from "yaml";
280
+ function parseSkillpacks(path4) {
281
+ const content = readFileSync5(path4, "utf-8");
282
+ const data = parseYaml(content);
283
+ if (!data || typeof data !== "object") {
284
+ throw new Error("skillpacks.yaml must be an object");
285
+ }
286
+ if (!Array.isArray(data.packs)) {
287
+ throw new Error("skillpacks.yaml must have 'packs' array");
288
+ }
289
+ for (const pack of data.packs) {
290
+ if (!pack.name || typeof pack.name !== "string") {
291
+ throw new Error("Each pack must have a 'name' string");
292
+ }
293
+ if (!pack.source || typeof pack.source !== "string") {
294
+ throw new Error(`Pack '${pack.name}' must have a 'source' URL`);
295
+ }
296
+ }
297
+ return data;
298
+ }
299
+
300
+ // src/resolve-skillpacks.ts
301
+ import { execSync } from "child_process";
302
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, rmSync } from "fs";
303
+ import { join as join4 } from "path";
304
+ function resolveSkillpacks(packs, cacheDir) {
305
+ mkdirSync3(cacheDir, { recursive: true });
306
+ const resolved = [];
307
+ for (const pack of packs) {
308
+ const packDir = join4(cacheDir, pack.name);
309
+ if (!existsSync5(packDir)) {
310
+ console.error(`[skillpacks] Cloning ${pack.name} from ${pack.source}...`);
311
+ execSync(`git clone "${pack.source}" "${packDir}"`, {
312
+ stdio: "inherit",
313
+ encoding: "utf-8"
314
+ });
315
+ } else {
316
+ console.error(`[skillpacks] Updating ${pack.name}...`);
317
+ execSync(`git fetch origin`, {
318
+ cwd: packDir,
319
+ stdio: "inherit",
320
+ encoding: "utf-8"
321
+ });
322
+ }
323
+ const ref = pack.version || "main";
324
+ try {
325
+ execSync(`git checkout "${ref}"`, {
326
+ cwd: packDir,
327
+ stdio: "pipe",
328
+ encoding: "utf-8"
329
+ });
330
+ if (!pack.version) {
331
+ execSync(`git pull origin "${ref}"`, {
332
+ cwd: packDir,
333
+ stdio: "pipe",
334
+ encoding: "utf-8"
335
+ });
336
+ }
337
+ } catch (err) {
338
+ throw new Error(`Failed to checkout ${ref} for pack ${pack.name}: ${err}`);
339
+ }
340
+ const resolvedRef = execSync("git rev-parse HEAD", {
341
+ cwd: packDir,
342
+ encoding: "utf-8"
343
+ }).trim();
344
+ resolved.push({
345
+ ...pack,
346
+ localPath: packDir,
347
+ resolvedRef
348
+ });
349
+ }
350
+ return resolved;
351
+ }
352
+ function removeSkillpack(packName, cacheDir) {
353
+ const packDir = join4(cacheDir, packName);
354
+ if (existsSync5(packDir)) {
355
+ rmSync(packDir, { recursive: true, force: true });
356
+ }
357
+ }
358
+
359
+ // src/sync-skillpacks.ts
360
+ import { existsSync as existsSync6, mkdirSync as mkdirSync4, readdirSync as readdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync3, copyFileSync } from "fs";
361
+ import { join as join5 } from "path";
362
+ import { stringify as stringifyYaml, parse as parseYaml2 } from "yaml";
363
+ function syncSkillpacks(repoRoot, cacheDir, targetDir) {
364
+ const yamlPath = join5(repoRoot, "skillpacks.yaml");
365
+ if (!existsSync6(yamlPath)) {
366
+ throw new Error(`skillpacks.yaml not found at ${yamlPath}`);
367
+ }
368
+ const manifest = parseSkillpacks(yamlPath);
369
+ const enabledPacks = manifest.packs.filter((p) => p.enabled !== false);
370
+ if (enabledPacks.length === 0) {
371
+ console.error("[sync] No enabled packs found in skillpacks.yaml");
372
+ return { syncedAt: (/* @__PURE__ */ new Date()).toISOString(), packs: [] };
373
+ }
374
+ const resolved = resolveSkillpacks(enabledPacks, cacheDir);
375
+ const agentsDir = join5(targetDir, "agents");
376
+ const specsDir = join5(targetDir, "specs");
377
+ mkdirSync4(agentsDir, { recursive: true });
378
+ mkdirSync4(specsDir, { recursive: true });
379
+ const lockEntries = [];
380
+ for (const pack of resolved) {
381
+ let agentCount = 0;
382
+ let specCount = 0;
383
+ const packAgentsDir = join5(pack.localPath, "agents");
384
+ if (existsSync6(packAgentsDir)) {
385
+ const agents = readdirSync2(packAgentsDir).filter((f) => f.endsWith(".md"));
386
+ for (const agent of agents) {
387
+ const src = join5(packAgentsDir, agent);
388
+ const dest = join5(agentsDir, agent);
389
+ copyFileSync(src, dest);
390
+ agentCount++;
391
+ }
392
+ }
393
+ const packSpecsDir = join5(pack.localPath, "specs");
394
+ if (existsSync6(packSpecsDir)) {
395
+ const specs = readdirSync2(packSpecsDir).filter((f) => f.endsWith(".md"));
396
+ for (const spec of specs) {
397
+ const src = join5(packSpecsDir, spec);
398
+ const dest = join5(specsDir, spec);
399
+ copyFileSync(src, dest);
400
+ specCount++;
401
+ }
402
+ }
403
+ lockEntries.push({
404
+ name: pack.name,
405
+ source: pack.source,
406
+ resolvedRef: pack.resolvedRef,
407
+ syncedAt: (/* @__PURE__ */ new Date()).toISOString(),
408
+ agentCount,
409
+ specCount
410
+ });
411
+ console.error(`[sync] ${pack.name}: ${agentCount} agents, ${specCount} specs`);
412
+ }
413
+ return {
414
+ syncedAt: (/* @__PURE__ */ new Date()).toISOString(),
415
+ packs: lockEntries
416
+ };
417
+ }
418
+ function writeLockFile(lockPath, lock) {
419
+ const yaml = stringifyYaml(lock);
420
+ writeFileSync3(lockPath, yaml, "utf-8");
421
+ }
422
+ function readLockFile(lockPath) {
423
+ if (!existsSync6(lockPath)) {
424
+ return null;
425
+ }
426
+ const content = readFileSync6(lockPath, "utf-8");
427
+ return parseYaml2(content);
428
+ }
429
+ export {
430
+ KINDS,
431
+ STATUSES,
432
+ TRANSITIONS,
433
+ appendContext,
434
+ assertTransition,
435
+ buildContext,
436
+ buildGraph,
437
+ canTransition,
438
+ citationCompliance,
439
+ computeResearchState,
440
+ createTask,
441
+ extractWorkflowState,
442
+ getActive,
443
+ isKind,
444
+ listTasks,
445
+ nextStatuses,
446
+ numberTraceability,
447
+ parseSkillpacks,
448
+ readContext,
449
+ readLockFile,
450
+ readPrdGoal,
451
+ readTask,
452
+ removeSkillpack,
453
+ renderResearchState,
454
+ researchPaths,
455
+ resolveSkillpacks,
456
+ setActive,
457
+ setStatus,
458
+ slugify,
459
+ syncSkillpacks,
460
+ taskJsonPath,
461
+ writeLockFile,
462
+ writeTask
463
+ };
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@research-copilot/core",
3
+ "version": "1.0.0",
4
+ "description": "Core library for research-copilot",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "keywords": [
12
+ "research",
13
+ "copilot",
14
+ "ai"
15
+ ],
16
+ "author": "ldm2060",
17
+ "license": "MIT",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://github.com/ldm2060/research_copilot.git",
21
+ "directory": "packages/core"
22
+ },
23
+ "engines": {
24
+ "node": ">=18.0.0"
25
+ },
26
+ "exports": {
27
+ ".": {
28
+ "import": "./dist/index.js",
29
+ "types": "./dist/index.d.ts"
30
+ }
31
+ },
32
+ "dependencies": {
33
+ "yaml": "^2.6.0",
34
+ "zod": "^3.23.0"
35
+ },
36
+ "scripts": {
37
+ "build": "tsup src/index.ts --format esm --dts"
38
+ }
39
+ }