superkit-mcp-server 1.2.6 → 1.2.7

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/ARCHITECTURE.md CHANGED
@@ -17,17 +17,25 @@ Super-Kit is a model-agnostic and agent-agnostic toolkit designed to provide a h
17
17
  ## 🏗️ Directory Structure
18
18
 
19
19
  ```plaintext
20
- super-kit/
20
+ super-kit/ # Global Super-Kit package (npm: superkit-mcp-server)
21
21
  ├── ARCHITECTURE.md # This file
22
22
  ├── SUPERKIT.md # Global rules and activation protocol
23
23
  ├── .core/ # Core engine-independent logic
24
24
  │ ├── rules/ # Universal mandates (e.g., clean-code, security-first)
25
- ├── agents/ # The T-Shaped AI Team Personas
26
- ├── skills/ # The Knowledge Modules
25
+ ├── agents/ # The T-Shaped AI Team Personas [source: "global"]
26
+ ├── skills/ # The Knowledge Modules [source: "global"]
27
27
  │ ├── meta/ # Session-resume, compound-docs, file-todos
28
28
  │ ├── tech/ # Node.js, React, Python, Prisma
29
29
  │ └── workflows/ # TDD, CI/CD, Code Review checklists
30
30
  └── workflows/ # Slash commands and lifecycle loops
31
+
32
+ {your-project}/ # Any project using Super-Kit
33
+ └── .agents/ # Project-local assets [source: "project"]
34
+ ├── agents/ # Custom agent .md files (e.g., my-domain-expert.md)
35
+ ├── skills/
36
+ │ ├── tech/ # Tech skill dirs, each with a SKILL.md
37
+ │ └── meta/ # Meta skill dirs, each with a SKILL.md
38
+ └── workflows/ # Custom workflow .md files
31
39
  ```
32
40
 
33
41
  ---
@@ -100,3 +108,44 @@ The entry point is reading `SUPERKIT.md` to establish global rules.
100
108
  - **Aider**: Run aider with the message `aider --message "Read SUPERKIT.md for your system instructions before doing anything else."`
101
109
 
102
110
  Once the agent has loaded `SUPERKIT.md`, it will follow the instructions to activate the appropriate `@agent` which dynamically reads `SKILL.md` from the relevant directories.
111
+
112
+ ---
113
+
114
+ ## 🗂️ Project-Based Assets
115
+
116
+ Super-Kit supports a two-scope asset system that allows any project to ship its own agents, skills, and workflows alongside the global Super-Kit package assets.
117
+
118
+ ### Scope Definitions
119
+
120
+ | Scope | Source | Location | `source` Label |
121
+ |-------|--------|----------|----------------|
122
+ | **Global** | `superkit-mcp-server` npm package | `super-kit/agents/`, `super-kit/skills/` | `"global"` |
123
+ | **Project** | User's project `.agents/` folder | `{project-root}/.agents/` | `"project"` |
124
+
125
+ ### Resolution Rules
126
+
127
+ - Project assets **complement** global assets — they never override or shadow them.
128
+ - If `.agents/` does not exist in a project, all project-scoped tools return empty results gracefully (no errors thrown).
129
+ - Asset names are validated to prevent path traversal — absolute paths and `..` components are rejected.
130
+
131
+ ### MCP Tool Mapping
132
+
133
+ | Tool | Scope | Description |
134
+ |------|-------|-------------|
135
+ | `list_superkit_assets` | Global (default) | Lists global assets. Accepts `scope: "all"` to merge both scopes with source labels. |
136
+ | `load_superkit_agent` | Global | Loads an agent from the Super-Kit package. |
137
+ | `load_superkit_skill` | Global | Loads a skill from the Super-Kit package. |
138
+ | `load_superkit_workflow` | Global | Loads a workflow from the Super-Kit package. |
139
+ | `list_project_assets` | Project | Lists project-local assets from `.agents/`. Falls back to `process.cwd()` if no `projectPath` given. |
140
+ | `load_project_agent` | Project | Loads an agent from `{projectPath}/.agents/agents/`. |
141
+ | `load_project_skill` | Project | Loads a skill from `{projectPath}/.agents/skills/{category}/{skillName}/SKILL.md`. |
142
+ | `load_project_workflow` | Project | Loads a workflow from `{projectPath}/.agents/workflows/`. |
143
+
144
+ ### Recommended Discovery Order
145
+
146
+ When an agent starts work on a project, it should:
147
+
148
+ 1. Call `list_project_assets` to discover what the project provides.
149
+ 2. Load project-specific agents/skills/workflows first (`load_project_*`).
150
+ 3. Fall back to global Super-Kit assets (`load_superkit_*`) for anything not covered.
151
+ 4. Use `list_superkit_assets({ scope: "all" })` for a unified merged view with source labels.
package/SUPERKIT.md CHANGED
@@ -85,12 +85,18 @@ When user says: "Always use TypeScript strict mode"
85
85
 
86
86
  ## Available Tools
87
87
 
88
- **Super-Kit MCP Tools:**
89
- - `list_superkit_assets` - Lists all available agents, skills, and workflows.
88
+ **Super-Kit MCP Tools (Global Scope):**
89
+ - `list_superkit_assets` - Lists all available agents, skills, and workflows. Accepts optional `scope` (`"global"` | `"project"` | `"all"`) and `projectPath` params.
90
90
  - `load_superkit_agent` - Loads Markdown instructions for an agent (e.g., `data-engineer`).
91
91
  - `load_superkit_skill` - Loads Markdown instructions for a skill (e.g., `tech`, `api-patterns`).
92
92
  - `load_superkit_workflow` - Loads a workflow guide (e.g., `work`, `explore`).
93
93
 
94
+ **Project-Scoped MCP Tools:**
95
+ - `list_project_assets` - Lists project-scoped agents, skills, and workflows from the `.agents/` folder.
96
+ - `load_project_agent` - Loads a project-scoped agent from `{projectPath}/.agents/agents/`.
97
+ - `load_project_skill` - Loads a project-scoped skill's `SKILL.md` from `{projectPath}/.agents/skills/`.
98
+ - `load_project_workflow` - Loads a project-scoped workflow from `{projectPath}/.agents/workflows/`.
99
+
94
100
  **Core Development Tools:**
95
101
  - `kit_create_checkpoint` - Create checkpoint before changes
96
102
  - `kit_restore_checkpoint` - Restore checkpoint if needed
@@ -103,6 +109,30 @@ When user says: "Always use TypeScript strict mode"
103
109
  - `kit_save_learning` - **Save lesson from user feedback**
104
110
  - `kit_get_learnings` - Read saved learnings
105
111
 
112
+ ## 🗂️ Project-Based Assets
113
+
114
+ Any project can define its own agents, skills, and workflows by creating a `.agents/` folder at the project root:
115
+
116
+ ```
117
+ {project-root}/
118
+ └── .agents/
119
+ ├── agents/ # Custom agent .md files (e.g., my-domain-expert.md)
120
+ ├── skills/
121
+ │ ├── tech/ # Tech skill dirs, each containing a SKILL.md
122
+ │ └── meta/ # Meta skill dirs, each containing a SKILL.md
123
+ └── workflows/ # Custom workflow .md files (e.g., deploy-staging.md)
124
+ ```
125
+
126
+ **Resolution rules:**
127
+ - Project assets have `"source": "project"` and **complement** (do not replace) global Super-Kit assets.
128
+ - Global Super-Kit assets always have `"source": "global"`.
129
+ - If `.agents/` does not exist, all project-scoped tools return empty results gracefully — no errors.
130
+
131
+ **When starting work on ANY project, ALWAYS:**
132
+ 1. Call `list_project_assets` (or `list_superkit_assets` with `scope: "all"`) to discover project-specific agents, skills, and workflows.
133
+ 2. Load project assets with `load_project_agent`, `load_project_skill`, or `load_project_workflow` before falling back to global equivalents.
134
+ 3. Use global assets (`load_superkit_agent`, etc.) for anything not covered by the project's `.agents/` folder.
135
+
106
136
  ## Documentation Management
107
137
 
108
138
  - Docs location: `./docs/`
package/build/index.js CHANGED
@@ -16,6 +16,7 @@ import { compoundSearch, updateSolutionRef, validateCompound, auditStateDrift, s
16
16
  import { bootstrapFolderDocs, checkDocsFreshness, discoverUndocumentedFolders, validateFolderDocs, } from "./tools/docsTools.js";
17
17
  import { generateChangelog, validateChangelog, archiveCompleted, prePushHousekeeping, } from "./tools/gitTools.js";
18
18
  import { validateSpecConsistency, completePlan, validateArchitecture, syncSpec, updateSpecPhase, } from "./tools/archTools.js";
19
+ import { list_project_agents, list_project_skills, list_project_workflows, load_project_agent_file, load_project_skill_file, load_project_workflow_file, } from "./tools/ProjectAssets.js";
19
20
  const __filename = fileURLToPath(import.meta.url);
20
21
  const __dirname = path.dirname(__filename);
21
22
  const superKitRoot = path.resolve(__dirname, "../");
@@ -119,7 +120,23 @@ const TOOLS = [
119
120
  },
120
121
  {
121
122
  name: "call_tool_todo_manager",
122
- description: "Manages todos (nextId, create, start, done, complete)",
123
+ description: `Manages todos. Per-action usage:
124
+
125
+ • nextId — Returns the next available todo ID. No extra params needed.
126
+
127
+ • create — Creates a new todo file. Required: title (string), description (1-2 sentence problem statement), priority ("p0"|"p1"|"p2"|"p3"), criteria (string array of acceptance criteria). Optional: projectPath.
128
+ Example: { action: "create", title: "Add auth", description: "Implement JWT login.", priority: "p2", criteria: ["User can log in", "Token is stored"], projectPath: "." }
129
+
130
+ • start — Marks a todo as in-progress. Required: todoId = RELATIVE PATH to the todo file, e.g. "todos/001-pending-p2-my-task.md". The file must exist at that path relative to projectPath.
131
+ Example: { action: "start", todoId: "todos/001-pending-p2-my-task.md", projectPath: "." }
132
+
133
+ • done — Marks a todo as done. Required: todoId = relative path (same format as start). All acceptance criteria checkboxes must be checked, or pass force: true to bypass.
134
+ Example: { action: "done", todoId: "todos/001-in-progress-p2-my-task.md", force: true, projectPath: "." }
135
+
136
+ • complete — Marks a todo as complete (final state). Required: todoId = relative path (same format as start/done).
137
+ Example: { action: "complete", todoId: "todos/001-done-p2-my-task.md", force: true, projectPath: "." }
138
+
139
+ ⚠️ IMPORTANT: todoId must be the FULL RELATIVE FILE PATH (e.g. "todos/001-in-progress-p2-my-task.md"), NOT just the numeric ID ("001"). The filename changes with each status transition, so always use the current filename on disk.`,
123
140
  inputSchema: {
124
141
  type: "object",
125
142
  properties: {
@@ -127,12 +144,33 @@ const TOOLS = [
127
144
  type: "string",
128
145
  enum: ["nextId", "create", "start", "done", "complete"],
129
146
  },
130
- title: { type: "string" },
131
- description: { type: "string" },
132
- priority: { type: "string" },
133
- criteria: { type: "array", items: { type: "string" } },
134
- todoId: { type: "string" },
135
- force: { type: "boolean", default: false },
147
+ title: {
148
+ type: "string",
149
+ description: "Todo title. Used by: create.",
150
+ },
151
+ description: {
152
+ type: "string",
153
+ description: "1-2 sentence problem statement. Used by: create.",
154
+ },
155
+ priority: {
156
+ type: "string",
157
+ enum: ["p0", "p1", "p2", "p3"],
158
+ description: "Priority level. p0=critical, p1=urgent, p2=normal, p3=low. Used by: create.",
159
+ },
160
+ criteria: {
161
+ type: "array",
162
+ items: { type: "string" },
163
+ description: "Acceptance criteria checklist items. Used by: create.",
164
+ },
165
+ todoId: {
166
+ type: "string",
167
+ description: "RELATIVE PATH to the todo file from projectPath, e.g. 'todos/001-pending-p2-my-task.md'. NOT just the numeric ID. Used by: start, done, complete.",
168
+ },
169
+ force: {
170
+ type: "boolean",
171
+ default: false,
172
+ description: "Bypass terminal-state or unchecked-criteria guards. Used by: start, done, complete.",
173
+ },
136
174
  projectPath: { type: "string", default: "." },
137
175
  },
138
176
  required: ["action", "projectPath"],
@@ -233,10 +271,93 @@ const TOOLS = [
233
271
  description: "Lists all available agents, skills, and workflows in the Super-Kit repository.",
234
272
  inputSchema: {
235
273
  type: "object",
236
- properties: {},
274
+ properties: {
275
+ scope: {
276
+ type: "string",
277
+ enum: ["global", "project", "all"],
278
+ default: "global",
279
+ description: "Which scope to list: 'global' (superkit package assets), 'project' (.agents/ folder assets), or 'all' (merged with source labels on every entry).",
280
+ },
281
+ projectPath: {
282
+ type: "string",
283
+ description: "Project root path used when scope includes 'project'. Defaults to process.cwd().",
284
+ },
285
+ },
286
+ required: [],
287
+ },
288
+ },
289
+ {
290
+ name: "list_project_assets",
291
+ description: "Lists project-scoped agents, skills, and workflows from the .agents/ folder in the given project directory. Falls back to process.cwd() if no projectPath is given.",
292
+ inputSchema: {
293
+ type: "object",
294
+ properties: {
295
+ projectPath: {
296
+ type: "string",
297
+ description: "Absolute path to the project root. Defaults to process.cwd().",
298
+ },
299
+ },
237
300
  required: [],
238
301
  },
239
302
  },
303
+ {
304
+ name: "load_project_agent",
305
+ description: "Loads a project-scoped agent markdown file from {projectPath}/.agents/agents/{agentName}.md",
306
+ inputSchema: {
307
+ type: "object",
308
+ properties: {
309
+ agentName: {
310
+ type: "string",
311
+ description: "Agent name without .md extension.",
312
+ },
313
+ projectPath: {
314
+ type: "string",
315
+ description: "Absolute path to the project root. Defaults to process.cwd().",
316
+ },
317
+ },
318
+ required: ["agentName"],
319
+ },
320
+ },
321
+ {
322
+ name: "load_project_skill",
323
+ description: "Loads a project-scoped skill's SKILL.md from {projectPath}/.agents/skills/{category}/{skillName}/SKILL.md",
324
+ inputSchema: {
325
+ type: "object",
326
+ properties: {
327
+ category: {
328
+ type: "string",
329
+ description: "Skill category: 'tech' or 'meta'.",
330
+ },
331
+ skillName: {
332
+ type: "string",
333
+ description: "Skill directory name.",
334
+ },
335
+ projectPath: {
336
+ type: "string",
337
+ description: "Absolute path to the project root. Defaults to process.cwd().",
338
+ },
339
+ },
340
+ required: ["category", "skillName"],
341
+ },
342
+ },
343
+ {
344
+ name: "load_project_workflow",
345
+ description: "Loads a project-scoped workflow markdown file from {projectPath}/.agents/workflows/{workflowName}.md",
346
+ inputSchema: {
347
+ type: "object",
348
+ properties: {
349
+ workflowName: {
350
+ type: "string",
351
+ description: "Workflow name without .md extension.",
352
+ },
353
+ projectPath: {
354
+ type: "string",
355
+ description: "Absolute path to the project root. Defaults to process.cwd().",
356
+ },
357
+ },
358
+ required: ["workflowName"],
359
+ },
360
+ },
240
361
  {
241
362
  name: "load_superkit_agent",
242
363
  description: "Loads the instruction markdown for a specific specialist agent.",
@@ -493,29 +614,106 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
493
614
  return { content: [{ type: "text", text: res }] };
494
615
  }
495
616
  if (request.params.name === "list_superkit_assets") {
617
+ const args = request.params.arguments;
618
+ const scope = args?.scope ?? "global";
619
+ const projectPath = args?.projectPath;
496
620
  const agentsPath = path.join(superKitRoot, "agents");
497
621
  const skillsTechPath = path.join(superKitRoot, "skills", "tech");
498
622
  const skillsMetaPath = path.join(superKitRoot, "skills", "meta");
499
623
  const workflowsPath = path.join(superKitRoot, "skills", "workflows");
500
624
  const commandsPath = path.join(superKitRoot, "commands");
501
- const agents = await listDirectorySafe(agentsPath);
502
- const techSkills = await listDirectorySafe(skillsTechPath);
503
- const metaSkills = await listDirectorySafe(skillsMetaPath);
504
- const workflows = await listDirectorySafe(workflowsPath);
505
- const commands = await listDirectorySafe(commandsPath);
625
+ // Build global asset lists (used for scope "global" or "all")
626
+ let globalData = null;
627
+ if (scope !== "project") {
628
+ const agents = await listDirectorySafe(agentsPath);
629
+ const techSkills = await listDirectorySafe(skillsTechPath);
630
+ const metaSkills = await listDirectorySafe(skillsMetaPath);
631
+ const workflows = await listDirectorySafe(workflowsPath);
632
+ const commands = await listDirectorySafe(commandsPath);
633
+ if (scope === "global") {
634
+ // Original backward-compatible format — no source labels
635
+ globalData = {
636
+ agents: agents.map((a) => a.replace(".md", "")),
637
+ skills: {
638
+ tech: techSkills.map((s) => s.replace("/", "")),
639
+ meta: metaSkills.map((s) => s.replace("/", "")),
640
+ },
641
+ workflows: workflows.map((w) => w.replace(".md", "")),
642
+ commands: commands.map((c) => c.replace(".toml", "")),
643
+ };
644
+ }
645
+ else {
646
+ // "all" scope — include source labels on every entry
647
+ globalData = {
648
+ agents: agents.map((a) => ({
649
+ name: a.replace(".md", ""),
650
+ source: "global",
651
+ })),
652
+ skills: {
653
+ tech: techSkills.map((s) => ({
654
+ name: s.replace("/", ""),
655
+ source: "global",
656
+ })),
657
+ meta: metaSkills.map((s) => ({
658
+ name: s.replace("/", ""),
659
+ source: "global",
660
+ })),
661
+ },
662
+ workflows: workflows.map((w) => ({
663
+ name: w.replace(".md", ""),
664
+ source: "global",
665
+ })),
666
+ commands: commands.map((c) => ({
667
+ name: c.replace(".toml", ""),
668
+ source: "global",
669
+ })),
670
+ };
671
+ }
672
+ }
673
+ // Build project asset lists (used for scope "project" or "all")
674
+ let projectData = null;
675
+ if (scope !== "global") {
676
+ const [projAgents, projSkills, projWorkflows] = await Promise.all([
677
+ list_project_agents(projectPath),
678
+ list_project_skills(projectPath),
679
+ list_project_workflows(projectPath),
680
+ ]);
681
+ projectData = {
682
+ agents: projAgents.map((a) => ({ name: a, source: "project" })),
683
+ skills: {
684
+ tech: projSkills.tech.map((s) => ({ name: s, source: "project" })),
685
+ meta: projSkills.meta.map((s) => ({ name: s, source: "project" })),
686
+ },
687
+ workflows: projWorkflows.map((w) => ({ name: w, source: "project" })),
688
+ };
689
+ }
690
+ // Compose the final response payload
691
+ let payload;
692
+ if (scope === "global") {
693
+ payload = globalData;
694
+ }
695
+ else if (scope === "project") {
696
+ payload = projectData;
697
+ }
698
+ else {
699
+ // "all" — deep-merge both lists
700
+ const g = globalData;
701
+ const p = projectData;
702
+ payload = {
703
+ agents: [...g.agents, ...p.agents],
704
+ skills: {
705
+ tech: [...g.skills.tech, ...p.skills.tech],
706
+ meta: [...g.skills.meta, ...p.skills.meta],
707
+ },
708
+ workflows: [...g.workflows, ...p.workflows],
709
+ commands: g.commands,
710
+ };
711
+ }
506
712
  return {
507
713
  content: [
508
714
  {
509
715
  type: "text",
510
- text: JSON.stringify({
511
- agents: agents.map((a) => a.replace(".md", "")),
512
- skills: {
513
- tech: techSkills.map((s) => s.replace("/", "")),
514
- meta: metaSkills.map((s) => s.replace("/", "")),
515
- },
516
- workflows: workflows.map((w) => w.replace(".md", "")),
517
- commands: commands.map((c) => c.replace(".toml", "")),
518
- }, null, 2),
716
+ text: JSON.stringify(payload, null, 2),
519
717
  },
520
718
  ],
521
719
  };
@@ -573,6 +771,61 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
573
771
  const content = await fs.readFile(safePath, "utf-8");
574
772
  return { content: [{ type: "text", text: content }] };
575
773
  }
774
+ if (request.params.name === "list_project_assets") {
775
+ const args = request.params.arguments;
776
+ const projectPath = args?.projectPath;
777
+ const [agents, skills, workflows] = await Promise.all([
778
+ list_project_agents(projectPath),
779
+ list_project_skills(projectPath),
780
+ list_project_workflows(projectPath),
781
+ ]);
782
+ return {
783
+ content: [
784
+ {
785
+ type: "text",
786
+ text: JSON.stringify({
787
+ source: "project",
788
+ agents: agents.map((a) => ({ name: a, source: "project" })),
789
+ skills: {
790
+ tech: skills.tech.map((s) => ({
791
+ name: s,
792
+ source: "project",
793
+ })),
794
+ meta: skills.meta.map((s) => ({
795
+ name: s,
796
+ source: "project",
797
+ })),
798
+ },
799
+ workflows: workflows.map((w) => ({
800
+ name: w,
801
+ source: "project",
802
+ })),
803
+ }, null, 2),
804
+ },
805
+ ],
806
+ };
807
+ }
808
+ if (request.params.name === "load_project_agent") {
809
+ const args = request.params.arguments;
810
+ if (!args.agentName)
811
+ throw new Error("Missing agentName");
812
+ const content = await load_project_agent_file(args.agentName, args.projectPath);
813
+ return { content: [{ type: "text", text: content }] };
814
+ }
815
+ if (request.params.name === "load_project_skill") {
816
+ const args = request.params.arguments;
817
+ if (!args.category || !args.skillName)
818
+ throw new Error("Missing category or skillName");
819
+ const content = await load_project_skill_file(args.category, args.skillName, args.projectPath);
820
+ return { content: [{ type: "text", text: content }] };
821
+ }
822
+ if (request.params.name === "load_project_workflow") {
823
+ const args = request.params.arguments;
824
+ if (!args.workflowName)
825
+ throw new Error("Missing workflowName");
826
+ const content = await load_project_workflow_file(args.workflowName, args.projectPath);
827
+ return { content: [{ type: "text", text: content }] };
828
+ }
576
829
  throw new Error(`Unknown tool: ${request.params.name}`);
577
830
  }
578
831
  catch (error) {
@@ -0,0 +1,177 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as path from 'path';
3
+ // The conventional folder name for project-local Super-Kit assets
4
+ export const PROJECT_ASSETS_DIR = '.agents';
5
+ /**
6
+ * Resolves the effective project root path.
7
+ * - If an explicit projectPath is provided, it is resolved to an absolute path.
8
+ * - Otherwise, falls back to process.cwd().
9
+ */
10
+ export function resolve_project_path(projectPath) {
11
+ if (projectPath) {
12
+ return path.resolve(projectPath);
13
+ }
14
+ return process.cwd();
15
+ }
16
+ /**
17
+ * Returns the absolute path to the .agents/ root for the given project.
18
+ */
19
+ export function get_project_agents_root(projectPath) {
20
+ return path.join(resolve_project_path(projectPath), PROJECT_ASSETS_DIR);
21
+ }
22
+ /**
23
+ * Internal guard: ensures the resolved path stays strictly within the .agents/ root.
24
+ * Returns the resolved absolute path, or null if a traversal attempt is detected.
25
+ */
26
+ function safe_project_path(agentsRoot, relative) {
27
+ // Normalize the root so the startsWith check is reliable on all platforms
28
+ const normalizedRoot = path.resolve(agentsRoot);
29
+ const resolved = path.resolve(agentsRoot, relative);
30
+ // On Windows path.resolve produces lower-cased drive letters consistently,
31
+ // so this comparison is safe cross-platform.
32
+ if (!resolved.startsWith(normalizedRoot + path.sep) && resolved !== normalizedRoot) {
33
+ return null; // traversal detected
34
+ }
35
+ return resolved;
36
+ }
37
+ /**
38
+ * Validates a user-supplied asset name (agentName, skillName, workflowName).
39
+ * Rejects absolute paths and anything containing path-separator characters.
40
+ */
41
+ function validate_asset_name(name) {
42
+ if (!name || typeof name !== 'string') {
43
+ throw new Error('Asset name must be a non-empty string.');
44
+ }
45
+ if (path.isAbsolute(name)) {
46
+ throw new Error(`Asset name must not be an absolute path: "${name}"`);
47
+ }
48
+ // Reject any component that contains a path separator or navigates upward
49
+ const normalized = path.normalize(name);
50
+ if (normalized.includes('..') || normalized.startsWith('/') || normalized.startsWith('\\')) {
51
+ throw new Error(`Asset name contains invalid path components: "${name}"`);
52
+ }
53
+ }
54
+ // ---------------------------------------------------------------------------
55
+ // Listing helpers
56
+ // ---------------------------------------------------------------------------
57
+ /**
58
+ * Lists project-scoped agent names (without .md extension) from .agents/agents/.
59
+ * Returns an empty array if the directory does not exist.
60
+ */
61
+ export async function list_project_agents(projectPath) {
62
+ const agentsRoot = get_project_agents_root(projectPath);
63
+ const agentsDir = path.join(agentsRoot, 'agents');
64
+ try {
65
+ const entries = await fs.readdir(agentsDir, { withFileTypes: true });
66
+ return entries
67
+ .filter((e) => e.isFile() && e.name.endsWith('.md'))
68
+ .map((e) => e.name.replace(/\.md$/, ''));
69
+ }
70
+ catch {
71
+ // Directory does not exist — graceful degradation
72
+ return [];
73
+ }
74
+ }
75
+ /**
76
+ * Lists project-scoped skill directory names from .agents/skills/tech/ and .agents/skills/meta/.
77
+ * Returns empty arrays for each category if the directories do not exist.
78
+ */
79
+ export async function list_project_skills(projectPath) {
80
+ const agentsRoot = get_project_agents_root(projectPath);
81
+ const list_category_skills = async (category) => {
82
+ const categoryDir = path.join(agentsRoot, 'skills', category);
83
+ try {
84
+ const entries = await fs.readdir(categoryDir, { withFileTypes: true });
85
+ return entries.filter((e) => e.isDirectory()).map((e) => e.name);
86
+ }
87
+ catch {
88
+ return [];
89
+ }
90
+ };
91
+ const [tech, meta] = await Promise.all([
92
+ list_category_skills('tech'),
93
+ list_category_skills('meta'),
94
+ ]);
95
+ return { tech, meta };
96
+ }
97
+ /**
98
+ * Lists project-scoped workflow names (without .md extension) from .agents/workflows/.
99
+ * Returns an empty array if the directory does not exist.
100
+ */
101
+ export async function list_project_workflows(projectPath) {
102
+ const agentsRoot = get_project_agents_root(projectPath);
103
+ const workflowsDir = path.join(agentsRoot, 'workflows');
104
+ try {
105
+ const entries = await fs.readdir(workflowsDir, { withFileTypes: true });
106
+ return entries
107
+ .filter((e) => e.isFile() && e.name.endsWith('.md'))
108
+ .map((e) => e.name.replace(/\.md$/, ''));
109
+ }
110
+ catch {
111
+ return [];
112
+ }
113
+ }
114
+ // ---------------------------------------------------------------------------
115
+ // Loading helpers
116
+ // ---------------------------------------------------------------------------
117
+ /**
118
+ * Loads a project-scoped agent's markdown content from:
119
+ * {projectPath}/.agents/agents/{agentName}.md
120
+ */
121
+ export async function load_project_agent_file(agentName, projectPath) {
122
+ validate_asset_name(agentName);
123
+ const agentsRoot = get_project_agents_root(projectPath);
124
+ const relative = path.join('agents', `${agentName}.md`);
125
+ const safePath = safe_project_path(agentsRoot, relative);
126
+ if (!safePath) {
127
+ throw new Error(`Path traversal detected for agent name: "${agentName}"`);
128
+ }
129
+ try {
130
+ return await fs.readFile(safePath, 'utf-8');
131
+ }
132
+ catch {
133
+ throw new Error(`Project agent not found: "${agentName}". ` +
134
+ `Expected file at: ${safePath}`);
135
+ }
136
+ }
137
+ /**
138
+ * Loads a project-scoped skill's SKILL.md content from:
139
+ * {projectPath}/.agents/skills/{category}/{skillName}/SKILL.md
140
+ */
141
+ export async function load_project_skill_file(category, skillName, projectPath) {
142
+ validate_asset_name(category);
143
+ validate_asset_name(skillName);
144
+ const agentsRoot = get_project_agents_root(projectPath);
145
+ const relative = path.join('skills', category, skillName, 'SKILL.md');
146
+ const safePath = safe_project_path(agentsRoot, relative);
147
+ if (!safePath) {
148
+ throw new Error(`Path traversal detected for skill: category="${category}", skillName="${skillName}"`);
149
+ }
150
+ try {
151
+ return await fs.readFile(safePath, 'utf-8');
152
+ }
153
+ catch {
154
+ throw new Error(`Project skill not found: category="${category}", skillName="${skillName}". ` +
155
+ `Expected SKILL.md at: ${safePath}`);
156
+ }
157
+ }
158
+ /**
159
+ * Loads a project-scoped workflow's markdown content from:
160
+ * {projectPath}/.agents/workflows/{workflowName}.md
161
+ */
162
+ export async function load_project_workflow_file(workflowName, projectPath) {
163
+ validate_asset_name(workflowName);
164
+ const agentsRoot = get_project_agents_root(projectPath);
165
+ const relative = path.join('workflows', `${workflowName}.md`);
166
+ const safePath = safe_project_path(agentsRoot, relative);
167
+ if (!safePath) {
168
+ throw new Error(`Path traversal detected for workflow name: "${workflowName}"`);
169
+ }
170
+ try {
171
+ return await fs.readFile(safePath, 'utf-8');
172
+ }
173
+ catch {
174
+ throw new Error(`Project workflow not found: "${workflowName}". ` +
175
+ `Expected file at: ${safePath}`);
176
+ }
177
+ }