@opengsd/gsd-core 1.9.1 → 1.10.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +2 -3
- package/.opencode/plugins/gsd-core.js +8 -1
- package/agents/gsd-code-fixer.md +27 -3
- package/agents/gsd-debugger.md +12 -246
- package/agents/gsd-executor.md +7 -5
- package/agents/gsd-integration-checker.md +3 -0
- package/agents/gsd-plan-checker.md +9 -0
- package/agents/gsd-planner.md +5 -8
- package/agents/gsd-roadmapper.md +21 -3
- package/agents/gsd-verifier.md +14 -70
- package/bin/install.js +453 -289
- package/commands/gsd/mempalace-capture.md +1 -1
- package/commands/gsd/new-milestone.md +1 -1
- package/commands/gsd/plan-phase.md +1 -1
- package/gsd-core/bin/gsd-tools.cjs +579 -66
- package/gsd-core/bin/lib/active-workstream-store.cjs +25 -0
- package/gsd-core/bin/lib/agent-install-check.cjs +38 -6
- package/gsd-core/bin/lib/api-coverage.cjs +120 -0
- package/gsd-core/bin/lib/audit.cjs +89 -1
- package/gsd-core/bin/lib/broken-windows.cjs +36 -6
- package/gsd-core/bin/lib/capability-registry.cjs +96 -110
- package/gsd-core/bin/lib/capability-validator.cjs +12 -2
- package/gsd-core/bin/lib/check-command-router.cjs +43 -1
- package/gsd-core/bin/lib/command-aliases.cjs +72 -0
- package/gsd-core/bin/lib/commands.cjs +26 -25
- package/gsd-core/bin/lib/commonjs-marker.cjs +136 -0
- package/gsd-core/bin/lib/config-loader.cjs +1 -0
- package/gsd-core/bin/lib/config.cjs +12 -1
- package/gsd-core/bin/lib/context-composer.cjs +278 -0
- package/gsd-core/bin/lib/context-predicates.cjs +506 -0
- package/gsd-core/bin/lib/core-utils.cjs +91 -12
- package/gsd-core/bin/lib/docs.cjs +3 -2
- package/gsd-core/bin/lib/external-job.cjs +19 -4
- package/gsd-core/bin/lib/frontmatter.cjs +84 -12
- package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +57 -6
- package/gsd-core/bin/lib/git-base-branch.cjs +58 -15
- package/gsd-core/bin/lib/graphify.cjs +142 -27
- package/gsd-core/bin/lib/gsd2-import.cjs +27 -4
- package/gsd-core/bin/lib/host-integration.cjs +13 -1
- package/gsd-core/bin/lib/init-command-router.cjs +83 -8
- package/gsd-core/bin/lib/init.cjs +1021 -57
- package/gsd-core/bin/lib/install-engine.cjs +64 -10
- package/gsd-core/bin/lib/install-profiles.cjs +27 -1
- package/gsd-core/bin/lib/installer-migration-authoring.cjs +3 -1
- package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
- package/gsd-core/bin/lib/installer-migrations/007-retire-config-root-commonjs-marker.cjs +149 -0
- package/gsd-core/bin/lib/installer-migrations/008-cursor-retire-commands-surface.cjs +55 -0
- package/gsd-core/bin/lib/installer-migrations/009-pi-retire-reserved-hooks-dir.cjs +199 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +87 -1
- package/gsd-core/bin/lib/io.cjs +28 -3
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +6 -0
- package/gsd-core/bin/lib/mcp-catalog.cjs +518 -0
- package/gsd-core/bin/lib/mcp-server.cjs +135 -3
- package/gsd-core/bin/lib/milestone.cjs +75 -47
- package/gsd-core/bin/lib/phase-id.cjs +63 -0
- package/gsd-core/bin/lib/phase-locator.cjs +138 -45
- package/gsd-core/bin/lib/phase.cjs +260 -28
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +232 -0
- package/gsd-core/bin/lib/planning-workspace.cjs +4 -0
- package/gsd-core/bin/lib/prompt-budget.cjs +128 -165
- package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +80 -0
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +99 -0
- package/gsd-core/bin/lib/review-lane-runner.cjs +30 -6
- package/gsd-core/bin/lib/roadmap-command-router.cjs +42 -9
- package/gsd-core/bin/lib/roadmap-parser.cjs +100 -18
- package/gsd-core/bin/lib/roadmap.cjs +37 -7
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +195 -62
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +15 -3
- package/gsd-core/bin/lib/runtime-homes.cjs +154 -41
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +105 -41
- package/gsd-core/bin/lib/section-manifest.cjs +209 -0
- package/gsd-core/bin/lib/shell-command-projection.cjs +113 -27
- package/gsd-core/bin/lib/smart-entry.cjs +12 -0
- package/gsd-core/bin/lib/state-transition.cjs +73 -8
- package/gsd-core/bin/lib/state.cjs +151 -62
- package/gsd-core/bin/lib/surface.cjs +12 -1
- package/gsd-core/bin/lib/uat-predicate.cjs +11 -1
- package/gsd-core/bin/lib/uat.cjs +320 -21
- package/gsd-core/bin/lib/unusable-input.cjs +9 -0
- package/gsd-core/bin/lib/verification.cjs +29 -12
- package/gsd-core/bin/lib/verify.cjs +10 -2
- package/gsd-core/bin/lib/workflow-fragments.cjs +557 -0
- package/gsd-core/bin/lib/workstream-inventory-builder.cjs +181 -18
- package/gsd-core/bin/lib/workstream-inventory.cjs +519 -27
- package/gsd-core/bin/lib/workstream.cjs +6 -0
- package/gsd-core/bin/lib/worktree-base-ref.cjs +50 -6
- package/gsd-core/bin/lib/worktree-safety.cjs +276 -118
- package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
- package/gsd-core/references/artifact-types.md +10 -3
- package/gsd-core/references/autonomous-ui-design-contract.md +42 -0
- package/gsd-core/references/debugger-techniques.md +255 -0
- package/gsd-core/references/research-documentation-lookup.md +5 -3
- package/gsd-core/references/specless-probe-fallback.md +7 -6
- package/gsd-core/references/verifier-wiring-patterns.md +100 -0
- package/gsd-core/references/worktree-branch-check.md +2 -2
- package/gsd-core/templates/summary-complex.md +2 -0
- package/gsd-core/templates/summary-minimal.md +2 -0
- package/gsd-core/templates/summary-standard.md +2 -0
- package/gsd-core/templates/summary.md +2 -0
- package/gsd-core/workflows/audit-milestone.md +3 -0
- package/gsd-core/workflows/autonomous/steps/converge-banner.md +1 -0
- package/gsd-core/workflows/autonomous/steps/converge-dispatch-bg.md +11 -0
- package/gsd-core/workflows/autonomous/steps/converge-dispatch-inline.md +7 -0
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +21 -0
- package/gsd-core/workflows/autonomous/steps/converge-loop.md +7 -0
- package/gsd-core/workflows/autonomous.md +32 -69
- package/gsd-core/workflows/code-review/steps/dispatch-fix.md +39 -0
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +83 -0
- package/gsd-core/workflows/code-review.md +42 -160
- package/gsd-core/workflows/complete-milestone/steps/git-tag.md +29 -0
- package/gsd-core/workflows/complete-milestone.md +23 -81
- package/gsd-core/workflows/debug.md +9 -12
- package/gsd-core/workflows/diagnose-issues.md +22 -0
- package/gsd-core/workflows/discovery-phase.md +4 -4
- package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +15 -0
- package/gsd-core/workflows/discuss-phase-assumptions.md +5 -16
- package/gsd-core/workflows/docs-update/steps/dispatch-monorepo-packages.md +51 -0
- package/gsd-core/workflows/docs-update.md +8 -51
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +34 -2
- package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +50 -0
- package/gsd-core/workflows/execute-phase/steps/partial-wave.md +31 -0
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +19 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +42 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +43 -37
- package/gsd-core/workflows/execute-phase.md +65 -137
- package/gsd-core/workflows/execute-plan.md +1 -1
- package/gsd-core/workflows/help/modes/full.md +6 -1
- package/gsd-core/workflows/ingest-docs.md +2 -1
- package/gsd-core/workflows/new-milestone/steps/project-md-milestone-write.md +16 -0
- package/gsd-core/workflows/new-milestone/steps/reset-phase-safety.md +19 -0
- package/gsd-core/workflows/new-milestone.md +21 -38
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +176 -0
- package/gsd-core/workflows/new-project/steps/auto-mode-detection.md +32 -0
- package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +18 -0
- package/gsd-core/workflows/new-project.md +13 -226
- package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +15 -0
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +110 -0
- package/gsd-core/workflows/plan-phase/steps/prd-express-gate.md +8 -0
- package/gsd-core/workflows/plan-phase/steps/research-only-early-exit.md +17 -0
- package/gsd-core/workflows/plan-phase/steps/research-only-modifiers.md +16 -0
- package/gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md +17 -0
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +149 -0
- package/gsd-core/workflows/plan-phase.md +49 -193
- package/gsd-core/workflows/progress/steps/forensic-audit.md +125 -0
- package/gsd-core/workflows/progress/steps/mvp-display.md +18 -0
- package/gsd-core/workflows/progress.md +11 -153
- package/gsd-core/workflows/quick/steps/discussion-phase.md +124 -0
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +111 -0
- package/gsd-core/workflows/quick/steps/quick-verification.md +46 -0
- package/gsd-core/workflows/quick/steps/research-phase.md +72 -0
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +37 -0
- package/gsd-core/workflows/quick.md +20 -390
- package/gsd-core/workflows/resume-project.md +3 -0
- package/gsd-core/workflows/review/steps/reviewer-instances-note-1.md +4 -0
- package/gsd-core/workflows/review/steps/reviewer-instances-note-2.md +3 -0
- package/gsd-core/workflows/review.md +15 -8
- package/gsd-core/workflows/section-manifest.json +219 -0
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/spec-phase.md +17 -14
- package/gsd-core/workflows/spike-wrap-up.md +20 -5
- package/gsd-core/workflows/spike.md +50 -16
- package/gsd-core/workflows/sync-skills.md +49 -11
- package/gsd-core/workflows/transition/steps/workstream-collision-check.md +17 -0
- package/gsd-core/workflows/transition.md +8 -21
- package/gsd-core/workflows/ui-phase.md +8 -7
- package/gsd-core/workflows/update/steps/channel-banner.md +7 -0
- package/gsd-core/workflows/update.md +18 -7
- package/gsd-core/workflows/verify-phase.md +4 -7
- package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +36 -0
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +21 -0
- package/gsd-core/workflows/verify-work.md +8 -58
- package/hooks/dist/gsd-agent-isolation-guard.js +428 -0
- package/hooks/dist/gsd-check-update-worker.js +14 -5
- package/hooks/dist/gsd-cursor-subagent-start.js +532 -26
- package/hooks/dist/gsd-read-injection-scanner.js +7 -0
- package/hooks/dist/gsd-statusline.js +72 -6
- package/hooks/dist/gsd-worktree-path-guard.js +2 -1
- package/hooks/dist/gsd-write-guard.js +359 -0
- package/hooks/dist/lib/isolation-sentinel.js +268 -0
- package/hooks/dist/managed-hooks-registry.cjs +2 -0
- package/hooks/gsd-agent-isolation-guard.js +428 -0
- package/hooks/gsd-check-update-worker.js +14 -5
- package/hooks/gsd-cursor-subagent-start.js +532 -26
- package/hooks/gsd-read-injection-scanner.js +7 -0
- package/hooks/gsd-statusline.js +72 -6
- package/hooks/gsd-worktree-path-guard.js +2 -1
- package/hooks/gsd-write-guard.js +359 -0
- package/hooks/hooks.json +12 -0
- package/hooks/lib/isolation-sentinel.js +268 -0
- package/hooks/managed-hooks-registry.cjs +2 -0
- package/package.json +14 -5
- package/pi/gsd.cjs +57 -12
- package/scripts/build-hooks.js +9 -0
- package/scripts/changeset/lint.cjs +9 -2
- package/scripts/changeset/serialize.cjs +5 -1
- package/scripts/gen-capability-matrix.cjs +1 -1
- package/scripts/gen-context-index.cjs +448 -0
- package/scripts/gen-inventory-manifest.cjs +101 -1
- package/scripts/gen-prompt-budget-parity-corpus.cjs +645 -0
- package/scripts/gen-section-manifest.cjs +638 -0
- package/scripts/generate-package-identity.cjs +4 -2
- package/scripts/lint-allow-test-rule-refs.allowlist.json +17 -31
- package/scripts/lint-compiled-artifact-sync.cjs +6 -1
- package/scripts/lint-docs-command-form.cjs +195 -0
- package/scripts/lint-docs-required.cjs +9 -1
- package/scripts/lint-emitted-drift-ack.cjs +215 -20
- package/scripts/lint-example-parser-parity.cjs +395 -0
- package/scripts/lint-test-file-count.allowlist.json +27 -1
- package/scripts/mutation-matrix.cjs +13 -0
- package/scripts/prompt-injection-scan.sh +27 -6
- package/scripts/run-tests.cjs +3 -2
- package/skills/gsd-autonomous/SKILL.md +1 -1
- package/skills/gsd-execute-phase/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +1 -1
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-plan-phase/SKILL.md +2 -2
- package/vscode/package.json +1 -1
- package/scripts/gen-emitted-baseline.cjs +0 -145
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP served catalog — Phase B, issue #3072 (ADR-1671 epic #1671).
|
|
3
|
+
* Design: `.gsd/phase/feat-3072-mcp-served-catalog/40-design.md`.
|
|
4
|
+
*
|
|
5
|
+
* Serves GSD's content tree as MCP **resources** (`gsd-core/workflows/*.md`,
|
|
6
|
+
* `gsd-core/references/*.md`) and the 71 `commands/gsd/*.md` files as MCP
|
|
7
|
+
* **prompts**, through the SAME composition rule the installer applies
|
|
8
|
+
* (`bin/install.js`'s `copyWithPathReplacement`) — additive to the file-copy
|
|
9
|
+
* floor, which stays untouched (ADR-1671 Decision 6).
|
|
10
|
+
*
|
|
11
|
+
* ## The two findings that shape this module's shape (40-design.md)
|
|
12
|
+
*
|
|
13
|
+
* **F1 — composition is scoped to `gsd-core/workflows/`, and that scoping is
|
|
14
|
+
* load-bearing.** `bin/install.js` runs `composeWorkflow` (from
|
|
15
|
+
* `workflow-fragments.cts`) only when the normalized source path matches
|
|
16
|
+
* `(?:^|\/)gsd-core\/workflows\//`. A reference/command doc that merely
|
|
17
|
+
* *documents* `<!-- gsd:section -->` marker syntax with an unfenced example
|
|
18
|
+
* would otherwise be mis-parsed as a real marker and have that line lossily
|
|
19
|
+
* dropped. {@link shouldCompose} is the ONE exported predicate both this
|
|
20
|
+
* module and `bin/install.js` call — never a second, hand-duplicated regex
|
|
21
|
+
* (ADR-1671:309, `DEFECT.GENERATIVE-FIX`).
|
|
22
|
+
*
|
|
23
|
+
* **F2 — parity cannot mean byte-equality with an emitted runtime tree.**
|
|
24
|
+
* Install applies per-runtime path rewrites AFTER composition. The served
|
|
25
|
+
* catalog is host-agnostic and rewrites for no runtime, so served bytes
|
|
26
|
+
* equal the post-composition, pre-rewrite stage — the parity assertion is
|
|
27
|
+
* that the catalog and the installer apply the SAME composition rule to the
|
|
28
|
+
* SAME source, not that final bytes match an emitted tree.
|
|
29
|
+
*
|
|
30
|
+
* ## Shape
|
|
31
|
+
*
|
|
32
|
+
* ```
|
|
33
|
+
* buildCatalog({root?, readFile?, readDir?}) -> Catalog {resources, prompts}
|
|
34
|
+
* readResource(catalog, uri) -> {uri, mimeType, text} | throws CatalogError
|
|
35
|
+
* getPrompt(catalog, name, args?) -> {description, messages} | throws CatalogError
|
|
36
|
+
* shouldCompose(relPath) -> boolean // THE shared F1 predicate
|
|
37
|
+
* listResources(catalog, {cursor?, pageSize?}) -> {resources, nextCursor?} | throws CatalogError
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* `listResources` is a delegation surface beyond the four primitives named in
|
|
41
|
+
* the design's "Shape" block: `src/mcp-server.cts`'s `handleMessage` is
|
|
42
|
+
* documented PURE and must stay thin (design "Shape" section), so cursor
|
|
43
|
+
* pagination over the resource index is catalog-module responsibility, not
|
|
44
|
+
* protocol-handler responsibility, mirroring how `shouldCompose` centralizes
|
|
45
|
+
* the F1 predicate rather than letting it leak into the protocol layer.
|
|
46
|
+
*
|
|
47
|
+
* `buildCatalog`'s `root` is optional: omitted, the real implementation must
|
|
48
|
+
* resolve the package root from THIS MODULE's own location (`__dirname`),
|
|
49
|
+
* never from `ctx.cwd` (design row 16 / test-matrix rows 46-47) — `ctx.cwd`
|
|
50
|
+
* is the user's *project* (state IO), the catalog lives in the *package*.
|
|
51
|
+
*
|
|
52
|
+
* Pure over injected `readFile`/`readDir` seams so tests inject IO faults by
|
|
53
|
+
* monkeypatching the seam (never `chmod 0o000` — root bypasses mode bits and
|
|
54
|
+
* the test would silently pass with zero coverage in CI).
|
|
55
|
+
*
|
|
56
|
+
* Every throw carries a stable {@link REASON} code via a typed `CatalogError`
|
|
57
|
+
* (mirrors `workflow-fragments.cts`'s `REASON`/`fail()` idiom) so tests assert
|
|
58
|
+
* `err.reason === REASON.X` rather than regex-/substring-matching the
|
|
59
|
+
* human-readable message (CONTRIBUTING.md "Prohibited: Raw Text Matching on
|
|
60
|
+
* Test Outputs").
|
|
61
|
+
*
|
|
62
|
+
* `src/mcp-server.cts` gains `resources/*` + `prompts/*` `handleMessage`
|
|
63
|
+
* cases delegating to this module. The catalog is built once per process,
|
|
64
|
+
* lazily, and is immutable for the process's lifetime (Gall's Law — no
|
|
65
|
+
* watching, no invalidation, no subscriptions; see design "Known limits").
|
|
66
|
+
*
|
|
67
|
+
* ADR-457 build-at-publish: compiled by tsc to
|
|
68
|
+
* gsd-core/bin/lib/mcp-catalog.cjs (gitignored).
|
|
69
|
+
*
|
|
70
|
+
* ## Directory-walk fail-open/fail-closed split (buildCatalog)
|
|
71
|
+
*
|
|
72
|
+
* The three catalog roots (`gsd-core/workflows/`, `gsd-core/references/`,
|
|
73
|
+
* `commands/gsd/`) are each reached through exactly ONE "speculative" probe —
|
|
74
|
+
* `readDir(root/gsd-core)` and `readDir(root/commands)` — whose failure is
|
|
75
|
+
* TOLERATED as "this segment has zero entries" (never fatal, never a crash;
|
|
76
|
+
* design row 45 / row 16 "absent root does not fall back to cwd"). Every
|
|
77
|
+
* directory reached AFTER that, because a parent's successful listing already
|
|
78
|
+
* reported it as a real subdirectory entry, is CONFIRMED to exist; a read
|
|
79
|
+
* failure for a confirmed directory is fatal (`REASON.READ_FAILED`, design
|
|
80
|
+
* row 43 "a directory read failure fails closed" — "no partial catalog
|
|
81
|
+
* silently served"). This is what lets an absent `commands/` tree (row 45 /
|
|
82
|
+
* many unit fixtures that only populate `gsd-core/workflows/`) build an empty
|
|
83
|
+
* segment silently, while an INJECTED failure on an already-listed
|
|
84
|
+
* `gsd-core/workflows/` directory (row 43) still fails the whole build.
|
|
85
|
+
*
|
|
86
|
+
* `readFile` is never called at build time — only `readDir`, for discovery.
|
|
87
|
+
* A single resource's content is read (and, for a workflow, composed) lazily
|
|
88
|
+
* inside {@link readResource}/{@link getPrompt}, so a bad `readFile` for ONE
|
|
89
|
+
* entry never prevents that entry from being *listed*, only from being
|
|
90
|
+
* *read* (design row 14 / test-matrix row 42).
|
|
91
|
+
*/
|
|
92
|
+
'use strict';
|
|
93
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
94
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
95
|
+
};
|
|
96
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
97
|
+
exports.DEFAULT_PAGE_SIZE = exports.REASON = void 0;
|
|
98
|
+
exports.shouldCompose = shouldCompose;
|
|
99
|
+
exports.buildCatalog = buildCatalog;
|
|
100
|
+
exports.readResource = readResource;
|
|
101
|
+
exports.listResources = listResources;
|
|
102
|
+
exports.getPrompt = getPrompt;
|
|
103
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
104
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
105
|
+
const security_cjs_1 = require("./security.cjs");
|
|
106
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- workflow-fragments.cjs is a CommonJS module compiled from a sibling .cts source; `import x = require()` reads its module.exports namespace directly.
|
|
107
|
+
const workflowFragments = require("./workflow-fragments.cjs");
|
|
108
|
+
const { composeWorkflow } = workflowFragments;
|
|
109
|
+
/**
|
|
110
|
+
* Frozen, stable reason codes for every typed throw this module's real
|
|
111
|
+
* implementation will produce. Tests assert `err.reason === REASON.X`
|
|
112
|
+
* (CONTRIBUTING.md "Prohibited: Raw Text Matching on Test Outputs") — shape
|
|
113
|
+
* copied from `workflow-fragments.cts`'s own `REASON` enum.
|
|
114
|
+
*
|
|
115
|
+
* - UNKNOWN_RESOURCE — uri (or a real-but-unindexed sibling path) is not a
|
|
116
|
+
* key in the prebuilt resource index; index membership is the sole
|
|
117
|
+
* authority (design "Hostile inputs" gate 1 / negative-space bullets).
|
|
118
|
+
* - UNKNOWN_PROMPT — prompt name is not a key in the prebuilt prompt index.
|
|
119
|
+
* - INVALID_URI — uri is syntactically malformed but not string-typed
|
|
120
|
+
* traversal shape: empty string, wrong scheme (e.g. `file://`).
|
|
121
|
+
* - TRAVERSAL_REFUSED — uri is shaped like a path-traversal or absolute-path
|
|
122
|
+
* escape attempt (`../`, `..\\`, percent/double-encoded, absolute posix/
|
|
123
|
+
* windows paths, null byte, a symlink caught by the second `validatePath`
|
|
124
|
+
* gate) — refused by defense-in-depth, index-first (design "Hostile
|
|
125
|
+
* inputs").
|
|
126
|
+
* - UNKNOWN_CURSOR — an unrecognized/malformed pagination cursor.
|
|
127
|
+
* - READ_FAILED — the injected `readFile`/`readDir` seam threw for one
|
|
128
|
+
* resource (or a directory) at build or read time; the failure is
|
|
129
|
+
* contained to that one entry (design row 14).
|
|
130
|
+
* - UNKNOWN_ROOT — uri's scheme is `gsd://` but its root segment names
|
|
131
|
+
* neither `workflows` nor `references`.
|
|
132
|
+
* - INVALID_PARAMS — a required parameter is missing or wrong-typed (e.g. a
|
|
133
|
+
* non-string uri/name passed to `readResource`/`getPrompt`).
|
|
134
|
+
*
|
|
135
|
+
* Adding a new reason requires updating this map AND the test that locks
|
|
136
|
+
* `Object.keys(REASON).sort()` as a coordinated change.
|
|
137
|
+
*/
|
|
138
|
+
exports.REASON = Object.freeze({
|
|
139
|
+
UNKNOWN_RESOURCE: 'unknown_resource',
|
|
140
|
+
UNKNOWN_PROMPT: 'unknown_prompt',
|
|
141
|
+
INVALID_URI: 'invalid_uri',
|
|
142
|
+
TRAVERSAL_REFUSED: 'traversal_refused',
|
|
143
|
+
UNKNOWN_CURSOR: 'unknown_cursor',
|
|
144
|
+
READ_FAILED: 'read_failed',
|
|
145
|
+
UNKNOWN_ROOT: 'unknown_root',
|
|
146
|
+
INVALID_PARAMS: 'invalid_params',
|
|
147
|
+
});
|
|
148
|
+
/**
|
|
149
|
+
* Default page size for {@link listResources}. ~326 real entries is past the
|
|
150
|
+
* point where a single unpaginated response is polite (design row 3).
|
|
151
|
+
*/
|
|
152
|
+
exports.DEFAULT_PAGE_SIZE = 50;
|
|
153
|
+
/**
|
|
154
|
+
* Throws a `TypeError` carrying `reason` (one of {@link REASON}) as a typed
|
|
155
|
+
* property, mirroring `workflow-fragments.cts`'s own `fail()` idiom so
|
|
156
|
+
* callers/tests never pattern-match message prose. `cause`, when given, is
|
|
157
|
+
* attached via the standard ES2022 `Error` cause chain (never string-baked
|
|
158
|
+
* into the message) so the original injected-seam/compose failure stays
|
|
159
|
+
* inspectable without shape-locking this module's own message text.
|
|
160
|
+
*/
|
|
161
|
+
function fail(reason, message, cause) {
|
|
162
|
+
const options = cause === undefined ? undefined : { cause };
|
|
163
|
+
const err = new TypeError(`mcp-catalog: ${message}`, options);
|
|
164
|
+
err.reason = reason;
|
|
165
|
+
throw err;
|
|
166
|
+
}
|
|
167
|
+
// ─── shouldCompose — the F1 predicate ───────────────────────────────────────
|
|
168
|
+
// Mirrors bin/install.js's pre-refactor inline regex EXACTLY (see that file's
|
|
169
|
+
// comment block, preserved, for the two-reviewer rationale). `bin/install.js`
|
|
170
|
+
// now imports THIS function rather than re-declaring the regex (task D).
|
|
171
|
+
const WORKFLOWS_SCOPE_RE = /(?:^|\/)gsd-core\/workflows\//;
|
|
172
|
+
/**
|
|
173
|
+
* THE shared F1 predicate: does `relPath` (POSIX-normalized, relative to the
|
|
174
|
+
* catalog root) fall under `gsd-core/workflows/`? Only such paths are ever
|
|
175
|
+
* run through `composeWorkflow` — everything else (references, commands) is
|
|
176
|
+
* served verbatim. `bin/install.js` imports and calls this SAME function
|
|
177
|
+
* (replacing its inline regex) so the catalog and the installer can never
|
|
178
|
+
* independently drift on what gets composed (ADR-1671:309,
|
|
179
|
+
* `DEFECT.GENERATIVE-FIX`; the parity gate in
|
|
180
|
+
* `tests/mcp-catalog-parity.test.cjs` asserts exactly this).
|
|
181
|
+
*/
|
|
182
|
+
function shouldCompose(relPath) {
|
|
183
|
+
if (typeof relPath !== 'string')
|
|
184
|
+
return false;
|
|
185
|
+
// Path is normalized UNCONDITIONALLY (backslash paths arrive on Linux too —
|
|
186
|
+
// CONTEXT.md path-separator rule), matching bin/install.js's own note.
|
|
187
|
+
const normalized = relPath.replace(/\\/g, '/');
|
|
188
|
+
return WORKFLOWS_SCOPE_RE.test(normalized);
|
|
189
|
+
}
|
|
190
|
+
// ─── buildCatalog — directory discovery ─────────────────────────────────────
|
|
191
|
+
/** Resolve the package root from THIS MODULE's own compiled location (`gsd-core/bin/lib/mcp-catalog.cjs`), never `ctx.cwd` (design row 16). */
|
|
192
|
+
function defaultPackageRoot() {
|
|
193
|
+
return node_path_1.default.resolve(__dirname, '..', '..', '..');
|
|
194
|
+
}
|
|
195
|
+
function defaultReadFile(absPath) {
|
|
196
|
+
return node_fs_1.default.readFileSync(absPath, 'utf8');
|
|
197
|
+
}
|
|
198
|
+
function defaultReadDir(absPath) {
|
|
199
|
+
return node_fs_1.default.readdirSync(absPath, { withFileTypes: true });
|
|
200
|
+
}
|
|
201
|
+
/** Speculative directory probe: failure (missing, unreadable) is tolerated as "zero entries", never fatal — nothing has confirmed this path exists yet. */
|
|
202
|
+
function tryReadDir(readDir, absPath) {
|
|
203
|
+
try {
|
|
204
|
+
return readDir(absPath);
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Recursively walk `dirAbs` for `.md` files, calling `onFile(relPathFromDirAbs, absPath)`
|
|
212
|
+
* for each in listing order (callers re-sort as needed — see `listResources`).
|
|
213
|
+
* `dirAbs` is assumed CONFIRMED to exist (a parent's successful listing
|
|
214
|
+
* already reported it as a directory entry), so any `readDir` failure here —
|
|
215
|
+
* at this level or deeper — is fail-closed (`REASON.READ_FAILED`, design row
|
|
216
|
+
* 43): "a directory listing failure must surface as a typed error, never a
|
|
217
|
+
* silently partial catalog." Directory symlinks are inert here by
|
|
218
|
+
* construction: {@link DirEntryLike} exposes only `isDirectory()`, and a
|
|
219
|
+
* symlink's dirent type is never reported as a directory, so a symlinked
|
|
220
|
+
* subtree is neither recursed into nor silently skipped-with-a-lie — it is
|
|
221
|
+
* simply not `.md`-matched either, and falls out of the walk (the ONE
|
|
222
|
+
* client-controlled path surface, `resources/read`, still gets the
|
|
223
|
+
* `validatePath` second gate for a symlinked *file*; see `readIndexedResource`).
|
|
224
|
+
*/
|
|
225
|
+
function walkMarkdownFilesFatal(dirAbs, readDir, onFile, relPrefix = '') {
|
|
226
|
+
let entries;
|
|
227
|
+
try {
|
|
228
|
+
entries = readDir(dirAbs);
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
fail(exports.REASON.READ_FAILED, `directory listing failed: ${relPrefix || dirAbs}`, err);
|
|
232
|
+
}
|
|
233
|
+
for (const entry of entries) {
|
|
234
|
+
const childRel = relPrefix ? `${relPrefix}/${entry.name}` : entry.name;
|
|
235
|
+
const childAbs = node_path_1.default.join(dirAbs, entry.name);
|
|
236
|
+
if (entry.isDirectory()) {
|
|
237
|
+
walkMarkdownFilesFatal(childAbs, readDir, onFile, childRel);
|
|
238
|
+
}
|
|
239
|
+
else if (entry.name.endsWith('.md')) {
|
|
240
|
+
onFile(childRel, childAbs);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/** Basename of a POSIX-joined relative path (never touches the OS path separator — every rel path this module builds is joined with literal `/`). */
|
|
245
|
+
function posixBaseName(relPath) {
|
|
246
|
+
const idx = relPath.lastIndexOf('/');
|
|
247
|
+
return idx === -1 ? relPath : relPath.slice(idx + 1);
|
|
248
|
+
}
|
|
249
|
+
/** `plan-phase` / `docs-update` -> `Plan Phase` / `Docs Update`; purely cosmetic, not asserted by any test. */
|
|
250
|
+
function titleFromBaseName(baseName) {
|
|
251
|
+
return baseName.replace(/[-_]+/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Every segment served as an MCP **resource**. `commands` sits alongside
|
|
255
|
+
* `workflows`/`references` here because a host may legitimately want to READ
|
|
256
|
+
* a command's markdown without invoking it as a prompt: `resources/read`
|
|
257
|
+
* returns raw text while `prompts/get` returns wrapped messages — two MCP
|
|
258
|
+
* *shapes* over the SAME one file read through the SAME one read path, not a
|
|
259
|
+
* second source of truth. Drives both the resource listing walk and
|
|
260
|
+
* unindexed-uri classification (`UNKNOWN_ROOT` vs `UNKNOWN_RESOURCE`).
|
|
261
|
+
*/
|
|
262
|
+
const RESOURCE_SEGMENTS = new Set(['workflows', 'references', 'commands']);
|
|
263
|
+
/** The subset of {@link RESOURCE_SEGMENTS} that lives under `gsd-core/` (as opposed to `commands/gsd/`, which is rooted elsewhere). */
|
|
264
|
+
const GSD_CORE_RESOURCE_SEGMENTS = new Set(['workflows', 'references']);
|
|
265
|
+
function indexResourceSegments(root, readDir, out) {
|
|
266
|
+
const gsdCoreAbs = node_path_1.default.join(root, 'gsd-core');
|
|
267
|
+
const gsdCoreEntries = tryReadDir(readDir, gsdCoreAbs);
|
|
268
|
+
if (gsdCoreEntries === null)
|
|
269
|
+
return;
|
|
270
|
+
for (const segmentEntry of gsdCoreEntries) {
|
|
271
|
+
if (!segmentEntry.isDirectory() || !GSD_CORE_RESOURCE_SEGMENTS.has(segmentEntry.name))
|
|
272
|
+
continue;
|
|
273
|
+
const segment = segmentEntry.name;
|
|
274
|
+
const segmentAbs = node_path_1.default.join(gsdCoreAbs, segment);
|
|
275
|
+
walkMarkdownFilesFatal(segmentAbs, readDir, (withinSegmentRelPath, absPath) => {
|
|
276
|
+
const relPath = `gsd-core/${segment}/${withinSegmentRelPath}`;
|
|
277
|
+
const uri = `gsd://${segment}/${withinSegmentRelPath}`;
|
|
278
|
+
const name = withinSegmentRelPath.replace(/\.md$/, '');
|
|
279
|
+
out.set(uri, {
|
|
280
|
+
uri,
|
|
281
|
+
name,
|
|
282
|
+
title: titleFromBaseName(posixBaseName(name)),
|
|
283
|
+
description: `GSD ${segment} resource: ${relPath}`,
|
|
284
|
+
mimeType: 'text/markdown',
|
|
285
|
+
relPath,
|
|
286
|
+
absPath,
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
function indexPromptSegment(root, readDir, out, resourcesOut) {
|
|
292
|
+
const commandsAbs = node_path_1.default.join(root, 'commands');
|
|
293
|
+
const commandsEntries = tryReadDir(readDir, commandsAbs);
|
|
294
|
+
if (commandsEntries === null)
|
|
295
|
+
return;
|
|
296
|
+
for (const entry of commandsEntries) {
|
|
297
|
+
if (!entry.isDirectory() || entry.name !== 'gsd')
|
|
298
|
+
continue;
|
|
299
|
+
const gsdAbs = node_path_1.default.join(commandsAbs, 'gsd');
|
|
300
|
+
walkMarkdownFilesFatal(gsdAbs, readDir, (withinRelPath, absPath) => {
|
|
301
|
+
const relPath = `commands/gsd/${withinRelPath}`;
|
|
302
|
+
// Bare command name — never a path (design row 8 / test-matrix row 31).
|
|
303
|
+
const name = posixBaseName(withinRelPath).replace(/\.md$/, '');
|
|
304
|
+
out.set(name, {
|
|
305
|
+
name,
|
|
306
|
+
title: titleFromBaseName(name),
|
|
307
|
+
description: `GSD command: ${name}`,
|
|
308
|
+
relPath,
|
|
309
|
+
absPath,
|
|
310
|
+
});
|
|
311
|
+
// Same file, second MCP shape: also indexed as a resource (see
|
|
312
|
+
// RESOURCE_SEGMENTS doc comment) so a host can `resources/read` a
|
|
313
|
+
// command's raw markdown without invoking it as a prompt.
|
|
314
|
+
const resourceUri = `gsd://commands/${withinRelPath}`;
|
|
315
|
+
const resourceName = withinRelPath.replace(/\.md$/, '');
|
|
316
|
+
resourcesOut.set(resourceUri, {
|
|
317
|
+
uri: resourceUri,
|
|
318
|
+
name: resourceName,
|
|
319
|
+
title: titleFromBaseName(posixBaseName(resourceName)),
|
|
320
|
+
description: `GSD commands resource: ${relPath}`,
|
|
321
|
+
mimeType: 'text/markdown',
|
|
322
|
+
relPath,
|
|
323
|
+
absPath,
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Build the immutable catalog index over `root` (or, if omitted, this
|
|
330
|
+
* module's own package location — never `ctx.cwd`; design row 16).
|
|
331
|
+
* Pure over the injected `readFile`/`readDir` seams so IO faults are tested
|
|
332
|
+
* by monkeypatching them, never `chmod 0o000`. Only `readDir` is called here
|
|
333
|
+
* (discovery) — `readFile` is never invoked at build time; see the module
|
|
334
|
+
* doc comment's "Directory-walk fail-open/fail-closed split" section.
|
|
335
|
+
*/
|
|
336
|
+
function buildCatalog(opts = {}) {
|
|
337
|
+
const root = opts.root !== undefined ? opts.root : defaultPackageRoot();
|
|
338
|
+
const readFile = opts.readFile || defaultReadFile;
|
|
339
|
+
const readDir = opts.readDir || defaultReadDir;
|
|
340
|
+
const resources = new Map();
|
|
341
|
+
const prompts = new Map();
|
|
342
|
+
indexResourceSegments(root, readDir, resources);
|
|
343
|
+
indexPromptSegment(root, readDir, prompts, resources);
|
|
344
|
+
return { resources, prompts, root, readFile };
|
|
345
|
+
}
|
|
346
|
+
// ─── readResource — the one client-controlled path surface ─────────────────
|
|
347
|
+
/** Percent-decode up to a few passes (double-encoding, design "Hostile inputs"), stopping early once decoding is a no-op or throws on malformed escapes. */
|
|
348
|
+
function decodePassesFor(value) {
|
|
349
|
+
const normalized = value.replace(/\\/g, '/');
|
|
350
|
+
const passes = [normalized];
|
|
351
|
+
let current = normalized;
|
|
352
|
+
for (let i = 0; i < 3; i++) {
|
|
353
|
+
let decoded;
|
|
354
|
+
try {
|
|
355
|
+
decoded = decodeURIComponent(current);
|
|
356
|
+
}
|
|
357
|
+
catch {
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
if (decoded === current)
|
|
361
|
+
break;
|
|
362
|
+
passes.push(decoded);
|
|
363
|
+
current = decoded;
|
|
364
|
+
}
|
|
365
|
+
return passes;
|
|
366
|
+
}
|
|
367
|
+
/** Does any decode pass of `value` contain a literal `..` path segment? */
|
|
368
|
+
function hasTraversalSegment(value) {
|
|
369
|
+
return decodePassesFor(value).some((pass) => pass.split('/').some((seg) => seg === '..'));
|
|
370
|
+
}
|
|
371
|
+
const GSD_URI_RE = /^gsd:\/\/([^/]*)\/(.*)$/;
|
|
372
|
+
const WINDOWS_ABS_RE = /^[A-Za-z]:\//;
|
|
373
|
+
/**
|
|
374
|
+
* Classify a uri NOT present in the index, to pick the right {@link REASON}
|
|
375
|
+
* for the refusal. This is diagnostic only — the actual access decision is
|
|
376
|
+
* the prebuilt index-membership check in {@link readResource}, which already
|
|
377
|
+
* rejects every one of these shapes by construction (design "Hostile inputs"
|
|
378
|
+
* gate 1: "a traversal uri is simply not a key").
|
|
379
|
+
*/
|
|
380
|
+
function classifyUnindexedUri(uri) {
|
|
381
|
+
if (uri.includes('\0'))
|
|
382
|
+
return exports.REASON.TRAVERSAL_REFUSED;
|
|
383
|
+
if (uri.startsWith('file://'))
|
|
384
|
+
return exports.REASON.INVALID_URI;
|
|
385
|
+
const normalized = uri.replace(/\\/g, '/');
|
|
386
|
+
if (!normalized.startsWith('gsd://')) {
|
|
387
|
+
if (normalized.startsWith('/'))
|
|
388
|
+
return exports.REASON.TRAVERSAL_REFUSED;
|
|
389
|
+
if (WINDOWS_ABS_RE.test(normalized))
|
|
390
|
+
return exports.REASON.TRAVERSAL_REFUSED;
|
|
391
|
+
return exports.REASON.INVALID_URI;
|
|
392
|
+
}
|
|
393
|
+
const match = GSD_URI_RE.exec(normalized);
|
|
394
|
+
if (!match)
|
|
395
|
+
return exports.REASON.INVALID_URI;
|
|
396
|
+
const [, rootSegment, rest] = match;
|
|
397
|
+
if (!RESOURCE_SEGMENTS.has(rootSegment))
|
|
398
|
+
return exports.REASON.UNKNOWN_ROOT;
|
|
399
|
+
if (hasTraversalSegment(rest))
|
|
400
|
+
return exports.REASON.TRAVERSAL_REFUSED;
|
|
401
|
+
return exports.REASON.UNKNOWN_RESOURCE;
|
|
402
|
+
}
|
|
403
|
+
/** Second, independent gate (design "Hostile inputs" gate 2): re-validate an INDEXED entry's relPath against the catalog root, catching a symlink planted after the index was built. */
|
|
404
|
+
function readIndexedResource(catalog, entry) {
|
|
405
|
+
const check = (0, security_cjs_1.validatePath)(entry.relPath, catalog.root);
|
|
406
|
+
if (!check.safe) {
|
|
407
|
+
fail(exports.REASON.TRAVERSAL_REFUSED, `indexed resource escapes catalog root: ${entry.relPath}`);
|
|
408
|
+
}
|
|
409
|
+
let raw;
|
|
410
|
+
try {
|
|
411
|
+
raw = catalog.readFile(entry.absPath);
|
|
412
|
+
}
|
|
413
|
+
catch (err) {
|
|
414
|
+
fail(exports.REASON.READ_FAILED, `failed to read resource: ${entry.relPath}`, err);
|
|
415
|
+
}
|
|
416
|
+
let text = raw;
|
|
417
|
+
if (shouldCompose(entry.relPath)) {
|
|
418
|
+
try {
|
|
419
|
+
text = composeWorkflow(raw, { sourcePath: entry.relPath });
|
|
420
|
+
}
|
|
421
|
+
catch (err) {
|
|
422
|
+
fail(exports.REASON.READ_FAILED, `failed to compose resource: ${entry.relPath}`, err);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return { uri: entry.uri, mimeType: entry.mimeType, text };
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Read one resource by uri. Index-membership-then-validate (design "Hostile
|
|
429
|
+
* inputs"): the uri must be an exact key in `catalog.resources`; a workflow
|
|
430
|
+
* entry is served through `shouldCompose`-gated composition (F1), a
|
|
431
|
+
* reference entry is served verbatim. Throws a {@link CatalogError} for any
|
|
432
|
+
* uri not present in the index — never an empty success.
|
|
433
|
+
*/
|
|
434
|
+
function readResource(catalog, uri) {
|
|
435
|
+
if (typeof uri !== 'string') {
|
|
436
|
+
fail(exports.REASON.INVALID_PARAMS, `uri must be a string, got ${typeof uri}`);
|
|
437
|
+
}
|
|
438
|
+
if (uri.length === 0) {
|
|
439
|
+
fail(exports.REASON.INVALID_URI, 'uri must not be empty');
|
|
440
|
+
}
|
|
441
|
+
// Gate 1 — index membership, exact-match, never a path join.
|
|
442
|
+
const entry = catalog.resources.get(uri);
|
|
443
|
+
if (entry) {
|
|
444
|
+
return readIndexedResource(catalog, entry);
|
|
445
|
+
}
|
|
446
|
+
fail(classifyUnindexedUri(uri), `unknown or refused resource uri: ${uri}`);
|
|
447
|
+
}
|
|
448
|
+
function encodeCursor(index) {
|
|
449
|
+
return Buffer.from(JSON.stringify({ i: index }), 'utf8').toString('base64url');
|
|
450
|
+
}
|
|
451
|
+
function decodeCursor(cursor) {
|
|
452
|
+
try {
|
|
453
|
+
const json = Buffer.from(cursor, 'base64url').toString('utf8');
|
|
454
|
+
const parsed = JSON.parse(json);
|
|
455
|
+
if (parsed &&
|
|
456
|
+
typeof parsed === 'object' &&
|
|
457
|
+
Number.isInteger(parsed.i) &&
|
|
458
|
+
parsed.i >= 0) {
|
|
459
|
+
return { index: parsed.i };
|
|
460
|
+
}
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
catch {
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* List resources, sorted deterministically by uri, optionally paginated.
|
|
469
|
+
* Throws a {@link CatalogError} (`REASON.UNKNOWN_CURSOR`) for a malformed or
|
|
470
|
+
* unrecognized cursor rather than silently resetting to page 1.
|
|
471
|
+
*/
|
|
472
|
+
function listResources(catalog, opts = {}) {
|
|
473
|
+
const pageSize = opts.pageSize !== undefined && Number.isFinite(opts.pageSize) && opts.pageSize > 0 ? Math.floor(opts.pageSize) : exports.DEFAULT_PAGE_SIZE;
|
|
474
|
+
const sorted = [...catalog.resources.values()].sort((a, b) => (a.uri < b.uri ? -1 : a.uri > b.uri ? 1 : 0));
|
|
475
|
+
let startIndex = 0;
|
|
476
|
+
if (opts.cursor !== undefined) {
|
|
477
|
+
const decoded = decodeCursor(opts.cursor);
|
|
478
|
+
if (decoded === null) {
|
|
479
|
+
fail(exports.REASON.UNKNOWN_CURSOR, `unrecognized pagination cursor: ${opts.cursor}`);
|
|
480
|
+
}
|
|
481
|
+
startIndex = decoded.index;
|
|
482
|
+
}
|
|
483
|
+
const page = sorted.slice(startIndex, startIndex + pageSize);
|
|
484
|
+
const nextIndex = startIndex + page.length;
|
|
485
|
+
const result = { resources: page };
|
|
486
|
+
if (nextIndex < sorted.length) {
|
|
487
|
+
result.nextCursor = encodeCursor(nextIndex);
|
|
488
|
+
}
|
|
489
|
+
return result;
|
|
490
|
+
}
|
|
491
|
+
// ─── getPrompt ───────────────────────────────────────────────────────────────
|
|
492
|
+
/**
|
|
493
|
+
* Get one prompt by its bare command name (never a path). `args`, if
|
|
494
|
+
* supplied, is accepted and ignored (design row 11 — no command template
|
|
495
|
+
* takes injected arguments today). Throws a {@link CatalogError} for any
|
|
496
|
+
* name not present in the index.
|
|
497
|
+
*/
|
|
498
|
+
function getPrompt(catalog, name, args) {
|
|
499
|
+
void args;
|
|
500
|
+
if (typeof name !== 'string' || name.length === 0) {
|
|
501
|
+
fail(exports.REASON.INVALID_PARAMS, `prompt name must be a non-empty string, got ${typeof name}`);
|
|
502
|
+
}
|
|
503
|
+
const entry = catalog.prompts.get(name);
|
|
504
|
+
if (!entry) {
|
|
505
|
+
fail(exports.REASON.UNKNOWN_PROMPT, `unknown prompt: ${name}`);
|
|
506
|
+
}
|
|
507
|
+
let raw;
|
|
508
|
+
try {
|
|
509
|
+
raw = catalog.readFile(entry.absPath);
|
|
510
|
+
}
|
|
511
|
+
catch (err) {
|
|
512
|
+
fail(exports.REASON.READ_FAILED, `failed to read prompt: ${entry.relPath}`, err);
|
|
513
|
+
}
|
|
514
|
+
return {
|
|
515
|
+
description: entry.description,
|
|
516
|
+
messages: [{ role: 'user', content: { type: 'text', text: raw } }],
|
|
517
|
+
};
|
|
518
|
+
}
|