@jaypie/mcp 0.8.158 → 0.8.159

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -44,6 +44,17 @@ cpSync(assets.skills, join("dist", MCP_ASSET_DIRECTORY.SKILLS), { recursive: tru
44
44
 
45
45
  `MCP_BUILTIN_SKILLS_PATH` and `MCP_RELEASE_NOTES_PATH` point at other locations.
46
46
 
47
+ `getMcpBuildInfo` returns the version and commit of the build that packed the markdown:
48
+
49
+ ```typescript
50
+ import { getMcpBuildInfo } from "@jaypie/mcp/assets";
51
+
52
+ const { commit, version, versionString } = getMcpBuildInfo();
53
+ // { commit: "460f95c3", version: "0.8.159", versionString: "@jaypie/mcp@0.8.159#460f95c3" }
54
+ ```
55
+
56
+ `commit` is empty when `PROJECT_COMMIT` was unset at build time. Unbuilt source returns version `0.0.0`.
57
+
47
58
  ### Filtering Tools
48
59
 
49
60
  `createMcpServer`, `mcpExpressHandler`, and `mcpHttpHandler` accept `services`, an allowlist of tool names:
package/dist/assets.d.ts CHANGED
@@ -8,6 +8,14 @@ interface McpAssetPaths {
8
8
  /** Absolute path of the Jaypie skills directory read by `skill` */
9
9
  skills: string;
10
10
  }
11
+ interface McpBuildInfo {
12
+ /** First 8 characters of `PROJECT_COMMIT` at build time; empty when unset */
13
+ commit: string;
14
+ /** `package.json` version at build time */
15
+ version: string;
16
+ /** `@jaypie/mcp@<version>#<commit>`, the string `version` answers */
17
+ versionString: string;
18
+ }
11
19
  /**
12
20
  * Absolute paths of the markdown directories `skill` and `release_notes` read
13
21
  * at runtime
@@ -29,6 +37,21 @@ interface McpAssetPaths {
29
37
  * ```
30
38
  */
31
39
  declare function getMcpAssetPaths(): McpAssetPaths;
40
+ /**
41
+ * Version and commit of the `@jaypie/mcp` build, compiled in at build time
42
+ *
43
+ * A consumer syncing `skills/` and `release-notes/` reads the build that
44
+ * packed them from the same package. Unbuilt source (tests, `tsx`) returns
45
+ * version `0.0.0` and an empty commit.
46
+ *
47
+ * @example
48
+ * ```typescript
49
+ * import { getMcpBuildInfo } from "@jaypie/mcp/assets";
50
+ *
51
+ * const { commit, version, versionString } = getMcpBuildInfo();
52
+ * ```
53
+ */
54
+ declare function getMcpBuildInfo(): McpBuildInfo;
32
55
 
33
- export { MCP_ASSET_DIRECTORY, getMcpAssetPaths };
34
- export type { McpAssetPaths };
56
+ export { MCP_ASSET_DIRECTORY, getMcpAssetPaths, getMcpBuildInfo };
57
+ export type { McpAssetPaths, McpBuildInfo };
package/dist/assets.js CHANGED
@@ -1,11 +1,8 @@
1
1
  import * as path from 'node:path';
2
2
  import { fileURLToPath } from 'node:url';
3
3
 
4
- // Locate the markdown that ships inside @jaypie/mcp, for build scripts that bundle it
5
- //
6
- //
7
- // Constants
8
- //
4
+ // Locate the markdown that ships inside @jaypie/mcp, and the build that packed it,
5
+ // for build scripts that bundle it
9
6
  const MCP_ASSET_DIRECTORY = {
10
7
  RELEASE_NOTES: "release-notes",
11
8
  SKILLS: "skills",
@@ -42,6 +39,30 @@ function getMcpAssetPaths() {
42
39
  skills: path.join(packageRoot, MCP_ASSET_DIRECTORY.SKILLS),
43
40
  };
44
41
  }
42
+ /**
43
+ * Version and commit of the `@jaypie/mcp` build, compiled in at build time
44
+ *
45
+ * A consumer syncing `skills/` and `release-notes/` reads the build that
46
+ * packed them from the same package. Unbuilt source (tests, `tsx`) returns
47
+ * version `0.0.0` and an empty commit.
48
+ *
49
+ * @example
50
+ * ```typescript
51
+ * import { getMcpBuildInfo } from "@jaypie/mcp/assets";
52
+ *
53
+ * const { commit, version, versionString } = getMcpBuildInfo();
54
+ * ```
55
+ */
56
+ function getMcpBuildInfo() {
57
+ return {
58
+ commit: "4e7a07fe"
59
+ ,
60
+ version: "0.8.159"
61
+ ,
62
+ versionString: "@jaypie/mcp@0.8.159#4e7a07fe"
63
+ ,
64
+ };
65
+ }
45
66
 
46
- export { MCP_ASSET_DIRECTORY, getMcpAssetPaths };
67
+ export { MCP_ASSET_DIRECTORY, getMcpAssetPaths, getMcpBuildInfo };
47
68
  //# sourceMappingURL=assets.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"assets.js","sources":["../src/assets.ts"],"sourcesContent":["// Locate the markdown that ships inside @jaypie/mcp, for build scripts that bundle it\n\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\n//\n//\n// Constants\n//\n\nexport const MCP_ASSET_DIRECTORY = {\n RELEASE_NOTES: \"release-notes\",\n SKILLS: \"skills\",\n} as const;\n\n//\n//\n// Types\n//\n\nexport interface McpAssetPaths {\n /** Absolute path of the release notes directory read by `release_notes` */\n releaseNotes: string;\n /** Absolute path of the Jaypie skills directory read by `skill` */\n skills: string;\n}\n\n//\n//\n// Main\n//\n\n/**\n * Absolute paths of the markdown directories `skill` and `release_notes` read\n * at runtime\n *\n * A bundler (esbuild) inlines the code but not the markdown. Copy each\n * directory beside the bundle under its own name (`skills/`,\n * `release-notes/`) and the docs suite finds it without configuration. Call\n * this from a build script, never from bundled code.\n *\n * @example\n * ```typescript\n * import { cpSync } from \"node:fs\";\n * import { join } from \"node:path\";\n * import { getMcpAssetPaths, MCP_ASSET_DIRECTORY } from \"@jaypie/mcp/assets\";\n *\n * const assets = getMcpAssetPaths();\n * cpSync(assets.skills, join(\"dist\", MCP_ASSET_DIRECTORY.SKILLS), { recursive: true });\n * cpSync(assets.releaseNotes, join(\"dist\", MCP_ASSET_DIRECTORY.RELEASE_NOTES), { recursive: true });\n * ```\n */\nexport function getMcpAssetPaths(): McpAssetPaths {\n // src/assets.ts and dist/assets.js both sit one level below the package root\n const packageRoot = path.join(\n path.dirname(fileURLToPath(import.meta.url)),\n \"..\",\n );\n return {\n releaseNotes: path.join(packageRoot, MCP_ASSET_DIRECTORY.RELEASE_NOTES),\n skills: path.join(packageRoot, MCP_ASSET_DIRECTORY.SKILLS),\n };\n}\n"],"names":[],"mappings":";;;AAAA;AAKA;AACA;AACA;AACA;AAEO,MAAM,mBAAmB,GAAG;AACjC,IAAA,aAAa,EAAE,eAAe;AAC9B,IAAA,MAAM,EAAE,QAAQ;;AAelB;AACA;AACA;AACA;AAEA;;;;;;;;;;;;;;;;;;;AAmBG;SACa,gBAAgB,GAAA;;IAE9B,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAC3B,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAC5C,IAAI,CACL;IACD,OAAO;QACL,YAAY,EAAE,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,mBAAmB,CAAC,aAAa,CAAC;QACvE,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,mBAAmB,CAAC,MAAM,CAAC;KAC3D;AACH;;;;"}
1
+ {"version":3,"file":"assets.js","sources":["../src/assets.ts"],"sourcesContent":["// Locate the markdown that ships inside @jaypie/mcp, and the build that packed it,\n// for build scripts that bundle it\n\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\n//\n//\n// Build-time constants (replaced by @rollup/plugin-replace)\n//\n\ndeclare const __BUILD_COMMIT__: string;\ndeclare const __BUILD_VERSION__: string;\ndeclare const __BUILD_VERSION_STRING__: string;\n\n//\n//\n// Constants\n//\n\nconst UNBUILT_COMMIT = \"\";\nconst UNBUILT_VERSION = \"0.0.0\";\nconst UNBUILT_VERSION_STRING = `@jaypie/mcp@${UNBUILT_VERSION}`;\n\nexport const MCP_ASSET_DIRECTORY = {\n RELEASE_NOTES: \"release-notes\",\n SKILLS: \"skills\",\n} as const;\n\n//\n//\n// Types\n//\n\nexport interface McpAssetPaths {\n /** Absolute path of the release notes directory read by `release_notes` */\n releaseNotes: string;\n /** Absolute path of the Jaypie skills directory read by `skill` */\n skills: string;\n}\n\nexport interface McpBuildInfo {\n /** First 8 characters of `PROJECT_COMMIT` at build time; empty when unset */\n commit: string;\n /** `package.json` version at build time */\n version: string;\n /** `@jaypie/mcp@<version>#<commit>`, the string `version` answers */\n versionString: string;\n}\n\n//\n//\n// Main\n//\n\n/**\n * Absolute paths of the markdown directories `skill` and `release_notes` read\n * at runtime\n *\n * A bundler (esbuild) inlines the code but not the markdown. Copy each\n * directory beside the bundle under its own name (`skills/`,\n * `release-notes/`) and the docs suite finds it without configuration. Call\n * this from a build script, never from bundled code.\n *\n * @example\n * ```typescript\n * import { cpSync } from \"node:fs\";\n * import { join } from \"node:path\";\n * import { getMcpAssetPaths, MCP_ASSET_DIRECTORY } from \"@jaypie/mcp/assets\";\n *\n * const assets = getMcpAssetPaths();\n * cpSync(assets.skills, join(\"dist\", MCP_ASSET_DIRECTORY.SKILLS), { recursive: true });\n * cpSync(assets.releaseNotes, join(\"dist\", MCP_ASSET_DIRECTORY.RELEASE_NOTES), { recursive: true });\n * ```\n */\nexport function getMcpAssetPaths(): McpAssetPaths {\n // src/assets.ts and dist/assets.js both sit one level below the package root\n const packageRoot = path.join(\n path.dirname(fileURLToPath(import.meta.url)),\n \"..\",\n );\n return {\n releaseNotes: path.join(packageRoot, MCP_ASSET_DIRECTORY.RELEASE_NOTES),\n skills: path.join(packageRoot, MCP_ASSET_DIRECTORY.SKILLS),\n };\n}\n\n/**\n * Version and commit of the `@jaypie/mcp` build, compiled in at build time\n *\n * A consumer syncing `skills/` and `release-notes/` reads the build that\n * packed them from the same package. Unbuilt source (tests, `tsx`) returns\n * version `0.0.0` and an empty commit.\n *\n * @example\n * ```typescript\n * import { getMcpBuildInfo } from \"@jaypie/mcp/assets\";\n *\n * const { commit, version, versionString } = getMcpBuildInfo();\n * ```\n */\nexport function getMcpBuildInfo(): McpBuildInfo {\n return {\n commit:\n typeof __BUILD_COMMIT__ !== \"undefined\"\n ? __BUILD_COMMIT__\n : UNBUILT_COMMIT,\n version:\n typeof __BUILD_VERSION__ !== \"undefined\"\n ? __BUILD_VERSION__\n : UNBUILT_VERSION,\n versionString:\n typeof __BUILD_VERSION_STRING__ !== \"undefined\"\n ? __BUILD_VERSION_STRING__\n : UNBUILT_VERSION_STRING,\n };\n}\n"],"names":[],"mappings":";;;AAAA;AACA;AAuBO,MAAM,mBAAmB,GAAG;AACjC,IAAA,aAAa,EAAE,eAAe;AAC9B,IAAA,MAAM,EAAE,QAAQ;;AAwBlB;AACA;AACA;AACA;AAEA;;;;;;;;;;;;;;;;;;;AAmBG;SACa,gBAAgB,GAAA;;IAE9B,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAC3B,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAC5C,IAAI,CACL;IACD,OAAO;QACL,YAAY,EAAE,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,mBAAmB,CAAC,aAAa,CAAC;QACvE,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,mBAAmB,CAAC,MAAM,CAAC;KAC3D;AACH;AAEA;;;;;;;;;;;;;AAaG;SACa,eAAe,GAAA;IAC7B,OAAO;AACL,QAAA,MAAM,EAEA;AACF,YAAgB;AACpB,QAAA,OAAO,EAED;AACF,YAAiB;AACrB,QAAA,aAAa,EAEP;AACF,YAAwB;KAC7B;AACH;;;;"}
@@ -1 +1 @@
1
- export declare const RELEASE_NOTES_HELP = "# Release Notes\n\nBrowse Jaypie package release notes.\n\n## Commands\n\n| Command | Description | Required Parameters |\n|---------|-------------|---------------------|\n| `list` | List available release notes | - |\n| `read` | Read a specific release note | `package`, `version` |\n\n## Options for `list`\n\n| Option | Description |\n|--------|-------------|\n| `package` | Filter by package name (e.g., \"mcp\", \"jaypie\") |\n| `since_version` | Only versions newer than this (e.g., \"1.0.0\") |\n\n## Examples\n\n```\nrelease_notes(\"list\")\nrelease_notes(\"list\", { package: \"mcp\" })\nrelease_notes(\"list\", { package: \"jaypie\", since_version: \"2.0.0\" })\nrelease_notes(\"read\", { package: \"mcp\", version: \"0.4.2\" })\n```\n\n## Available Packages\n\nRelease notes are available for all Jaypie packages including:\n- `jaypie` - Main package\n- `mcp` - MCP server\n- `fabric` - Service handlers and adapters\n- `llm` - LLM provider interface\n- And more...\n\nUse `release_notes(\"list\")` to see all available packages and versions.\n";
1
+ export declare const RELEASE_NOTES_HELP = "# Release Notes\n\nBrowse Jaypie package release notes.\n\n## Commands\n\n| Command | Description | Required Parameters |\n|---------|-------------|---------------------|\n| `list` | List available release notes | - |\n| `read` | Read a specific release note | `package`, `version` |\n\n## Options for `list`\n\n| Option | Description |\n|--------|-------------|\n| `cursor` | Opaque cursor from a previous page's `Next page` footer |\n| `limit` | Notes per page (default 50, maximum 200) |\n| `package` | Filter by package name (e.g., \"mcp\", \"jaypie\") |\n| `since_version` | Only versions newer than this (e.g., \"1.0.0\") |\n\n## Pagination\n\n`list` sorts by package ascending, then version descending, and returns one\npage. When more notes exist, the last line is a footer:\n\n```\nNext page: release_notes(\"list\", { cursor: \"...\" })\n```\n\nThe cursor carries `package`, `since_version`, and `limit`, so the footer call\nalone continues the same listing. Pass `limit` with a cursor to change the page\nsize. Passing a `package` or `since_version` that differs from the cursor is an\nerror.\n\n## Examples\n\n```\nrelease_notes(\"list\")\nrelease_notes(\"list\", { limit: 20 })\nrelease_notes(\"list\", { package: \"mcp\" })\nrelease_notes(\"list\", { package: \"jaypie\", since_version: \"2.0.0\" })\nrelease_notes(\"read\", { package: \"mcp\", version: \"0.4.2\" })\n```\n\n## Available Packages\n\nRelease notes are available for all Jaypie packages including:\n- `jaypie` - Main package\n- `mcp` - MCP server\n- `fabric` - Service handlers and adapters\n- `llm` - LLM provider interface\n- And more...\n\nUse `release_notes(\"list\")` to see all available packages and versions.\n";
@@ -14,13 +14,30 @@ Browse Jaypie package release notes.
14
14
 
15
15
  | Option | Description |
16
16
  |--------|-------------|
17
+ | \`cursor\` | Opaque cursor from a previous page's \`Next page\` footer |
18
+ | \`limit\` | Notes per page (default 50, maximum 200) |
17
19
  | \`package\` | Filter by package name (e.g., "mcp", "jaypie") |
18
20
  | \`since_version\` | Only versions newer than this (e.g., "1.0.0") |
19
21
 
22
+ ## Pagination
23
+
24
+ \`list\` sorts by package ascending, then version descending, and returns one
25
+ page. When more notes exist, the last line is a footer:
26
+
27
+ \`\`\`
28
+ Next page: release_notes("list", { cursor: "..." })
29
+ \`\`\`
30
+
31
+ The cursor carries \`package\`, \`since_version\`, and \`limit\`, so the footer call
32
+ alone continues the same listing. Pass \`limit\` with a cursor to change the page
33
+ size. Passing a \`package\` or \`since_version\` that differs from the cursor is an
34
+ error.
35
+
20
36
  ## Examples
21
37
 
22
38
  \`\`\`
23
39
  release_notes("list")
40
+ release_notes("list", { limit: 20 })
24
41
  release_notes("list", { package: "mcp" })
25
42
  release_notes("list", { package: "jaypie", since_version: "2.0.0" })
26
43
  release_notes("read", { package: "mcp", version: "0.4.2" })
@@ -1 +1 @@
1
- {"version":3,"file":"help.js","sources":["../../../src/suites/docs/help.ts"],"sourcesContent":["// Inlined so bundled builds (esbuild, Lambda) need no shipped file\n\nexport const RELEASE_NOTES_HELP = `# Release Notes\n\nBrowse Jaypie package release notes.\n\n## Commands\n\n| Command | Description | Required Parameters |\n|---------|-------------|---------------------|\n| \\`list\\` | List available release notes | - |\n| \\`read\\` | Read a specific release note | \\`package\\`, \\`version\\` |\n\n## Options for \\`list\\`\n\n| Option | Description |\n|--------|-------------|\n| \\`package\\` | Filter by package name (e.g., \"mcp\", \"jaypie\") |\n| \\`since_version\\` | Only versions newer than this (e.g., \"1.0.0\") |\n\n## Examples\n\n\\`\\`\\`\nrelease_notes(\"list\")\nrelease_notes(\"list\", { package: \"mcp\" })\nrelease_notes(\"list\", { package: \"jaypie\", since_version: \"2.0.0\" })\nrelease_notes(\"read\", { package: \"mcp\", version: \"0.4.2\" })\n\\`\\`\\`\n\n## Available Packages\n\nRelease notes are available for all Jaypie packages including:\n- \\`jaypie\\` - Main package\n- \\`mcp\\` - MCP server\n- \\`fabric\\` - Service handlers and adapters\n- \\`llm\\` - LLM provider interface\n- And more...\n\nUse \\`release_notes(\"list\")\\` to see all available packages and versions.\n`;\n"],"names":[],"mappings":"AAAA;AAEO,MAAM,kBAAkB,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"help.js","sources":["../../../src/suites/docs/help.ts"],"sourcesContent":["// Inlined so bundled builds (esbuild, Lambda) need no shipped file\n\nexport const RELEASE_NOTES_HELP = `# Release Notes\n\nBrowse Jaypie package release notes.\n\n## Commands\n\n| Command | Description | Required Parameters |\n|---------|-------------|---------------------|\n| \\`list\\` | List available release notes | - |\n| \\`read\\` | Read a specific release note | \\`package\\`, \\`version\\` |\n\n## Options for \\`list\\`\n\n| Option | Description |\n|--------|-------------|\n| \\`cursor\\` | Opaque cursor from a previous page's \\`Next page\\` footer |\n| \\`limit\\` | Notes per page (default 50, maximum 200) |\n| \\`package\\` | Filter by package name (e.g., \"mcp\", \"jaypie\") |\n| \\`since_version\\` | Only versions newer than this (e.g., \"1.0.0\") |\n\n## Pagination\n\n\\`list\\` sorts by package ascending, then version descending, and returns one\npage. When more notes exist, the last line is a footer:\n\n\\`\\`\\`\nNext page: release_notes(\"list\", { cursor: \"...\" })\n\\`\\`\\`\n\nThe cursor carries \\`package\\`, \\`since_version\\`, and \\`limit\\`, so the footer call\nalone continues the same listing. Pass \\`limit\\` with a cursor to change the page\nsize. Passing a \\`package\\` or \\`since_version\\` that differs from the cursor is an\nerror.\n\n## Examples\n\n\\`\\`\\`\nrelease_notes(\"list\")\nrelease_notes(\"list\", { limit: 20 })\nrelease_notes(\"list\", { package: \"mcp\" })\nrelease_notes(\"list\", { package: \"jaypie\", since_version: \"2.0.0\" })\nrelease_notes(\"read\", { package: \"mcp\", version: \"0.4.2\" })\n\\`\\`\\`\n\n## Available Packages\n\nRelease notes are available for all Jaypie packages including:\n- \\`jaypie\\` - Main package\n- \\`mcp\\` - MCP server\n- \\`fabric\\` - Service handlers and adapters\n- \\`llm\\` - LLM provider interface\n- And more...\n\nUse \\`release_notes(\"list\")\\` to see all available packages and versions.\n`;\n"],"names":[],"mappings":"AAAA;AAEO,MAAM,kBAAkB,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -3,6 +3,8 @@ export declare const skillService: import("@jaypie/fabric").Service<{
3
3
  }, string, string>;
4
4
  export declare const versionService: import("@jaypie/fabric").Service<Record<string, unknown>, string, string>;
5
5
  interface ReleaseNotesInput {
6
+ cursor?: string;
7
+ limit?: number | string;
6
8
  package?: string;
7
9
  since_version?: string;
8
10
  version?: string;
@@ -6,15 +6,15 @@ import { existsSync, readdirSync } from 'node:fs';
6
6
  import * as fs from 'node:fs/promises';
7
7
  import * as path from 'node:path';
8
8
  import { fileURLToPath } from 'node:url';
9
- import { gt } from 'semver';
10
- import { getMcpAssetPaths, MCP_ASSET_DIRECTORY } from '../../assets.js';
9
+ import { gt, valid, rcompare } from 'semver';
10
+ import { getMcpAssetPaths, MCP_ASSET_DIRECTORY, getMcpBuildInfo } from '../../assets.js';
11
11
  import { RELEASE_NOTES_HELP } from './help.js';
12
12
 
13
13
  /**
14
14
  * Docs Suite - Documentation services (skill, version, release_notes)
15
15
  */
16
- const BUILD_VERSION_STRING = "@jaypie/mcp@0.8.158#55ed6fbc"
17
- ;
16
+ // Build-time constants
17
+ const BUILD_VERSION_STRING = getMcpBuildInfo().versionString;
18
18
  // =============================================================================
19
19
  // ASSET PATHS
20
20
  // =============================================================================
@@ -117,6 +117,102 @@ const skillStore = createLayeredStore({
117
117
  layers: skillLayers,
118
118
  separator: LAYER_SEPARATOR,
119
119
  });
120
+ // =============================================================================
121
+ // HELPER FUNCTIONS
122
+ // =============================================================================
123
+ const CURSOR_ENCODING = "base64url";
124
+ const INVALID_CURSOR_MESSAGE = 'Invalid cursor. Call release_notes("list") to start over.';
125
+ const RELEASE_NOTES_LIST_LIMIT = {
126
+ DEFAULT: 50,
127
+ MAXIMUM: 200,
128
+ };
129
+ function compareText({ a, b }) {
130
+ if (a < b)
131
+ return -1;
132
+ if (a > b)
133
+ return 1;
134
+ return 0;
135
+ }
136
+ /**
137
+ * Package ascending, then semver descending. Valid semver sorts before
138
+ * anything else, and filename breaks every remaining tie so the order is total.
139
+ */
140
+ function compareReleaseNotes({ a, b, }) {
141
+ const byPackage = compareText({ a: a.packageName, b: b.packageName });
142
+ if (byPackage !== 0)
143
+ return byPackage;
144
+ const aVersion = a.version && valid(a.version);
145
+ const bVersion = b.version && valid(b.version);
146
+ if (aVersion && bVersion) {
147
+ const byVersion = rcompare(aVersion, bVersion);
148
+ if (byVersion !== 0)
149
+ return byVersion;
150
+ }
151
+ else if (aVersion) {
152
+ return -1;
153
+ }
154
+ else if (bVersion) {
155
+ return 1;
156
+ }
157
+ return compareText({ a: b.filename, b: a.filename });
158
+ }
159
+ function isPositiveInteger(value) {
160
+ return typeof value === "number" && Number.isInteger(value) && value > 0;
161
+ }
162
+ function isOptionalString(value) {
163
+ return value === undefined || typeof value === "string";
164
+ }
165
+ function encodeListCursor(cursor) {
166
+ const ordered = {
167
+ limit: cursor.limit,
168
+ offset: cursor.offset,
169
+ package: cursor.package,
170
+ since_version: cursor.since_version,
171
+ };
172
+ return Buffer.from(JSON.stringify(ordered)).toString(CURSOR_ENCODING);
173
+ }
174
+ function decodeListCursor(cursor) {
175
+ if (typeof cursor !== "string" || cursor.length === 0) {
176
+ throw new BadRequestError(INVALID_CURSOR_MESSAGE);
177
+ }
178
+ let decoded;
179
+ try {
180
+ decoded = JSON.parse(Buffer.from(cursor, CURSOR_ENCODING).toString("utf-8"));
181
+ }
182
+ catch {
183
+ throw new BadRequestError(INVALID_CURSOR_MESSAGE);
184
+ }
185
+ if (typeof decoded !== "object" || decoded === null) {
186
+ throw new BadRequestError(INVALID_CURSOR_MESSAGE);
187
+ }
188
+ const candidate = decoded;
189
+ const isValid = typeof candidate.offset === "number" &&
190
+ Number.isInteger(candidate.offset) &&
191
+ candidate.offset >= 0 &&
192
+ (candidate.limit === undefined ||
193
+ (isPositiveInteger(candidate.limit) &&
194
+ candidate.limit <= RELEASE_NOTES_LIST_LIMIT.MAXIMUM)) &&
195
+ isOptionalString(candidate.package) &&
196
+ isOptionalString(candidate.since_version);
197
+ if (!isValid) {
198
+ throw new BadRequestError(INVALID_CURSOR_MESSAGE);
199
+ }
200
+ return candidate;
201
+ }
202
+ /**
203
+ * Accept a number or numeric string, reject anything below one, and clamp to
204
+ * the maximum
205
+ */
206
+ function resolveListLimit(value) {
207
+ if (value === undefined || value === null || value === "") {
208
+ return RELEASE_NOTES_LIST_LIMIT.DEFAULT;
209
+ }
210
+ const limit = typeof value === "string" ? Number(value) : value;
211
+ if (!isPositiveInteger(limit)) {
212
+ throw new BadRequestError("limit must be a positive integer");
213
+ }
214
+ return Math.min(limit, RELEASE_NOTES_LIST_LIMIT.MAXIMUM);
215
+ }
120
216
  async function parseReleaseNoteFile(filePath) {
121
217
  try {
122
218
  const content = await fs.readFile(filePath, "utf-8");
@@ -151,21 +247,11 @@ async function getPackageReleaseNotes(packageName) {
151
247
  const packageDir = path.join(RELEASE_NOTES_PATH, packageName);
152
248
  try {
153
249
  const files = await fs.readdir(packageDir);
154
- const mdFiles = files.filter((file) => file.endsWith(".md"));
155
- const notes = await Promise.all(mdFiles.map(async (file) => {
250
+ const mdFiles = files.filter((file) => file.endsWith(MARKDOWN_EXTENSION));
251
+ return await Promise.all(mdFiles.map(async (file) => {
156
252
  const parsed = await parseReleaseNoteFile(path.join(packageDir, file));
157
253
  return { ...parsed, packageName };
158
254
  }));
159
- return notes.sort((a, b) => {
160
- if (!a.version || !b.version)
161
- return 0;
162
- try {
163
- return gt(a.version, b.version) ? -1 : 1;
164
- }
165
- catch {
166
- return b.version.localeCompare(a.version);
167
- }
168
- });
169
255
  }
170
256
  catch {
171
257
  return [];
@@ -198,7 +284,7 @@ const versionService = fabricService({
198
284
  });
199
285
  const releaseNotesService = fabricService({
200
286
  alias: "release_notes",
201
- description: "Browse Jaypie package release notes. Commands: list, read. Call with no args for help.",
287
+ description: "Browse Jaypie package release notes. Commands: list (paginated), read. Call with no args for help.",
202
288
  input: {
203
289
  command: {
204
290
  description: "Command to execute (omit for help)",
@@ -206,7 +292,7 @@ const releaseNotesService = fabricService({
206
292
  type: String,
207
293
  },
208
294
  input: {
209
- description: "Command parameters",
295
+ description: `Command parameters. list: package, since_version, limit (default ${RELEASE_NOTES_LIST_LIMIT.DEFAULT}, maximum ${RELEASE_NOTES_LIST_LIMIT.MAXIMUM}), cursor (from the "Next page" footer). read: package, version (both required).`,
210
296
  required: false,
211
297
  type: Object,
212
298
  },
@@ -219,30 +305,60 @@ const releaseNotesService = fabricService({
219
305
  const p = params || {};
220
306
  switch (command) {
221
307
  case "list": {
308
+ const cursor = p.cursor === undefined ? undefined : decodeListCursor(p.cursor);
309
+ if (cursor) {
310
+ if (p.package !== undefined && p.package !== cursor.package) {
311
+ throw new BadRequestError("package does not match the cursor. Omit package or start over without a cursor.");
312
+ }
313
+ if (p.since_version !== undefined &&
314
+ p.since_version !== cursor.since_version) {
315
+ throw new BadRequestError("since_version does not match the cursor. Omit since_version or start over without a cursor.");
316
+ }
317
+ }
318
+ const limit = resolveListLimit(p.limit ?? cursor?.limit);
319
+ const offset = cursor?.offset ?? 0;
320
+ const packageFilter = cursor ? cursor.package : p.package;
321
+ const sinceVersion = cursor ? cursor.since_version : p.since_version;
222
322
  const entries = await fs.readdir(RELEASE_NOTES_PATH, {
223
323
  withFileTypes: true,
224
324
  });
225
325
  const packageDirs = entries
226
326
  .filter((entry) => entry.isDirectory())
227
327
  .map((entry) => entry.name);
228
- const packagesToList = p.package
229
- ? packageDirs.filter((pkg) => pkg === p.package)
328
+ const packagesToList = packageFilter
329
+ ? packageDirs.filter((pkg) => pkg === packageFilter)
230
330
  : packageDirs;
231
- if (packagesToList.length === 0 && p.package) {
232
- return `No release notes found for package "${p.package}".`;
331
+ if (packagesToList.length === 0 && packageFilter) {
332
+ return `No release notes found for package "${packageFilter}".`;
233
333
  }
234
334
  const allNotes = await Promise.all(packagesToList.map((pkg) => getPackageReleaseNotes(pkg)));
235
335
  let flatNotes = allNotes.flat();
236
- if (p.since_version) {
237
- flatNotes = filterReleaseNotesSince(flatNotes, p.since_version);
336
+ if (sinceVersion) {
337
+ flatNotes = filterReleaseNotesSince(flatNotes, sinceVersion);
238
338
  }
239
339
  if (flatNotes.length === 0) {
240
- const filterDesc = p.since_version
241
- ? ` newer than ${p.since_version}`
242
- : "";
340
+ const filterDesc = sinceVersion ? ` newer than ${sinceVersion}` : "";
243
341
  return `No release notes found${filterDesc}.`;
244
342
  }
245
- return flatNotes.map(formatReleaseNoteListItem).join("\n");
343
+ if (offset >= flatNotes.length) {
344
+ throw new BadRequestError(INVALID_CURSOR_MESSAGE);
345
+ }
346
+ flatNotes.sort((a, b) => compareReleaseNotes({ a, b }));
347
+ const nextOffset = offset + limit;
348
+ const page = flatNotes
349
+ .slice(offset, nextOffset)
350
+ .map(formatReleaseNoteListItem)
351
+ .join("\n");
352
+ if (nextOffset >= flatNotes.length) {
353
+ return page;
354
+ }
355
+ const nextCursor = encodeListCursor({
356
+ limit,
357
+ offset: nextOffset,
358
+ package: packageFilter,
359
+ since_version: sinceVersion,
360
+ });
361
+ return `${page}\n\nNext page: release_notes("list", { cursor: "${nextCursor}" })`;
246
362
  }
247
363
  case "read": {
248
364
  if (!p.package)
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../../../src/suites/docs/index.ts"],"sourcesContent":["/**\n * Docs Suite - Documentation services (skill, version, release_notes)\n */\nimport { BadRequestError } from \"@jaypie/errors\";\nimport { fabricService } from \"@jaypie/fabric\";\nimport { parseFrontmatter } from \"@jaypie/kit\";\nimport {\n createLayeredStore,\n createMarkdownStore,\n createSkillService,\n type LayeredStoreLayer,\n} from \"@jaypie/tildeskill\";\nimport { existsSync, readdirSync, type Dirent } from \"node:fs\";\nimport * as fs from \"node:fs/promises\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { gt } from \"semver\";\n\nimport { getMcpAssetPaths, MCP_ASSET_DIRECTORY } from \"../../assets.js\";\nimport { RELEASE_NOTES_HELP } from \"./help.js\";\n\n// Build-time constants\ndeclare const __BUILD_VERSION_STRING__: string;\nconst BUILD_VERSION_STRING =\n typeof __BUILD_VERSION_STRING__ !== \"undefined\"\n ? __BUILD_VERSION_STRING__\n : \"@jaypie/mcp@0.0.0\";\n\n// =============================================================================\n// ASSET PATHS\n// =============================================================================\n\nconst ENV_BUILTIN_SKILLS_PATH = \"MCP_BUILTIN_SKILLS_PATH\";\nconst ENV_RELEASE_NOTES_PATH = \"MCP_RELEASE_NOTES_PATH\";\nconst MARKDOWN_EXTENSION = \".md\";\nconst WARNING_PREFIX = \"[@jaypie/mcp]\";\n\n// A bundler (esbuild) collapses every module into one file, so this module's\n// directory is the bundle directory rather than dist/suites/docs/\nconst MODULE_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));\nconst PACKAGE_ASSET_PATHS = getMcpAssetPaths();\n\n/**\n * An explicit environment path wins outright. Otherwise the package copy wins,\n * then a copy beside the bundle. Returns the package path when neither exists.\n */\nfunction resolveAssetDirectory({\n bundled,\n envPath,\n packaged,\n}: {\n bundled: string;\n envPath?: string;\n packaged: string;\n}): string {\n if (envPath) return envPath;\n return (\n [packaged, bundled].find((candidate) => existsSync(candidate)) ?? packaged\n );\n}\n\nfunction hasEntries({\n directory,\n isEntry,\n}: {\n directory: string;\n isEntry: (entry: Dirent) => boolean;\n}): boolean {\n try {\n return readdirSync(directory, { withFileTypes: true }).some(isEntry);\n } catch {\n return false;\n }\n}\n\n/**\n * Warn once, on first use, when an asset directory is missing or empty. Waiting\n * for first use keeps servers that never call the service quiet.\n */\nfunction createAssetCheck({\n directory,\n envName,\n isEntry,\n name,\n}: {\n directory: string;\n envName: string;\n isEntry: (entry: Dirent) => boolean;\n name: string;\n}): () => void {\n let checked = false;\n return () => {\n if (checked) return;\n checked = true;\n if (hasEntries({ directory, isEntry })) return;\n // stderr keeps the stdio transport's stdout clean and still reaches CloudWatch\n // eslint-disable-next-line no-console\n console.warn(\n `${WARNING_PREFIX} No ${name} found in \"${directory}\". Copy the ${name} directory from getMcpAssetPaths() in @jaypie/mcp/assets beside the bundle or set ${envName}.`,\n );\n };\n}\n\n/** Run `check` before any property of `target` is read */\nfunction withAssetCheck<T extends object>({\n check,\n target,\n}: {\n check: () => void;\n target: T;\n}): T {\n return new Proxy(target, {\n get(object, property, receiver) {\n check();\n return Reflect.get(object, property, receiver);\n },\n });\n}\n\nconst RELEASE_NOTES_PATH = resolveAssetDirectory({\n bundled: path.join(MODULE_DIRECTORY, MCP_ASSET_DIRECTORY.RELEASE_NOTES),\n envPath: process.env[ENV_RELEASE_NOTES_PATH],\n packaged: PACKAGE_ASSET_PATHS.releaseNotes,\n});\n\n// Bundled Jaypie skills ship inside the @jaypie/mcp package. MCP_BUILTIN_SKILLS_PATH\n// relocates them without disabling the built-in layer.\nconst BUILTIN_SKILLS_PATH = resolveAssetDirectory({\n bundled: path.join(MODULE_DIRECTORY, MCP_ASSET_DIRECTORY.SKILLS),\n envPath: process.env[ENV_BUILTIN_SKILLS_PATH],\n packaged: PACKAGE_ASSET_PATHS.skills,\n});\n\nconst checkReleaseNoteAssets = createAssetCheck({\n directory: RELEASE_NOTES_PATH,\n envName: ENV_RELEASE_NOTES_PATH,\n isEntry: (entry) => entry.isDirectory(),\n name: MCP_ASSET_DIRECTORY.RELEASE_NOTES,\n});\n\nconst checkSkillAssets = createAssetCheck({\n directory: BUILTIN_SKILLS_PATH,\n envName: ENV_BUILTIN_SKILLS_PATH,\n isEntry: (entry) => entry.isFile() && entry.name.endsWith(MARKDOWN_EXTENSION),\n name: MCP_ASSET_DIRECTORY.SKILLS,\n});\n\nconst LOCAL_SKILLS_NAMESPACE = \"local\";\nconst JAYPIE_SKILLS_NAMESPACE = \"jaypie\";\nconst LAYER_SEPARATOR = \":\";\n\n// Skill layers resolved in order: a client's MCP_SKILLS_PATH layers on top of\n// the built-in Jaypie skill pack so `skill(\"aws\")` prefers the client's copy\n// while still exposing bundled Jaypie docs under the `jaypie:` namespace.\nconst skillLayers: LayeredStoreLayer[] = [];\n\nif (process.env.MCP_SKILLS_PATH) {\n skillLayers.push({\n namespace: LOCAL_SKILLS_NAMESPACE,\n store: createMarkdownStore({ path: process.env.MCP_SKILLS_PATH }),\n });\n}\n\nskillLayers.push({\n namespace: JAYPIE_SKILLS_NAMESPACE,\n store: withAssetCheck({\n check: checkSkillAssets,\n target: createMarkdownStore({ path: BUILTIN_SKILLS_PATH }),\n }),\n});\n\nconst skillStore = createLayeredStore({\n layers: skillLayers,\n separator: LAYER_SEPARATOR,\n});\n\n// =============================================================================\n// HELPER FUNCTIONS\n// =============================================================================\n\ninterface ReleaseNoteFrontMatter {\n date?: string;\n summary?: string;\n version?: string;\n}\n\nasync function parseReleaseNoteFile(filePath: string): Promise<{\n date?: string;\n filename: string;\n summary?: string;\n version?: string;\n}> {\n try {\n const content = await fs.readFile(filePath, \"utf-8\");\n const filename = path.basename(filePath, \".md\");\n\n if (content.startsWith(\"---\")) {\n const frontMatter =\n parseFrontmatter<ReleaseNoteFrontMatter>(content).data;\n return {\n date: frontMatter.date,\n filename,\n summary: frontMatter.summary,\n version: frontMatter.version || filename,\n };\n }\n\n return { filename, version: filename };\n } catch {\n return { filename: path.basename(filePath, \".md\") };\n }\n}\n\nfunction formatReleaseNoteListItem(note: {\n date?: string;\n filename: string;\n packageName: string;\n summary?: string;\n version?: string;\n}): string {\n const { date, packageName, summary, version } = note;\n const parts = [`* ${packageName}@${version}`];\n\n if (date) {\n parts.push(`(${date})`);\n }\n\n if (summary) {\n parts.push(`- ${summary}`);\n }\n\n return parts.join(\" \");\n}\n\nasync function getPackageReleaseNotes(packageName: string): Promise<\n Array<{\n date?: string;\n filename: string;\n packageName: string;\n summary?: string;\n version?: string;\n }>\n> {\n const packageDir = path.join(RELEASE_NOTES_PATH, packageName);\n try {\n const files = await fs.readdir(packageDir);\n const mdFiles = files.filter((file) => file.endsWith(\".md\"));\n\n const notes = await Promise.all(\n mdFiles.map(async (file) => {\n const parsed = await parseReleaseNoteFile(path.join(packageDir, file));\n return { ...parsed, packageName };\n }),\n );\n\n return notes.sort((a, b) => {\n if (!a.version || !b.version) return 0;\n try {\n return gt(a.version, b.version) ? -1 : 1;\n } catch {\n return b.version.localeCompare(a.version);\n }\n });\n } catch {\n return [];\n }\n}\n\nfunction filterReleaseNotesSince(\n notes: Array<{\n date?: string;\n filename: string;\n packageName: string;\n summary?: string;\n version?: string;\n }>,\n sinceVersion: string,\n): Array<{\n date?: string;\n filename: string;\n packageName: string;\n summary?: string;\n version?: string;\n}> {\n return notes.filter((note) => {\n if (!note.version) return false;\n try {\n return gt(note.version, sinceVersion);\n } catch {\n return false;\n }\n });\n}\n\n// =============================================================================\n// SKILL SERVICE\n// =============================================================================\n\nexport const skillService = createSkillService(skillStore);\n\n// =============================================================================\n// VERSION SERVICE\n// =============================================================================\n\nexport const versionService = fabricService({\n alias: \"version\",\n description: `Prints the current version and hash, \\`${BUILD_VERSION_STRING}\\``,\n input: {},\n service: async () => BUILD_VERSION_STRING,\n});\n\n// =============================================================================\n// RELEASE NOTES SERVICE\n// =============================================================================\n\ninterface ReleaseNotesInput {\n package?: string;\n since_version?: string;\n version?: string;\n}\n\nexport const releaseNotesService = fabricService({\n alias: \"release_notes\",\n description:\n \"Browse Jaypie package release notes. Commands: list, read. Call with no args for help.\",\n input: {\n command: {\n description: \"Command to execute (omit for help)\",\n required: false,\n type: String,\n },\n input: {\n description: \"Command parameters\",\n required: false,\n type: Object,\n },\n },\n service: async ({\n command,\n input: params,\n }: {\n command?: string;\n input?: ReleaseNotesInput;\n }) => {\n if (!command || command === \"help\") {\n return RELEASE_NOTES_HELP;\n }\n\n checkReleaseNoteAssets();\n const p = params || {};\n\n switch (command) {\n case \"list\": {\n const entries = await fs.readdir(RELEASE_NOTES_PATH, {\n withFileTypes: true,\n });\n const packageDirs = entries\n .filter((entry) => entry.isDirectory())\n .map((entry) => entry.name);\n const packagesToList = p.package\n ? packageDirs.filter((pkg) => pkg === p.package)\n : packageDirs;\n\n if (packagesToList.length === 0 && p.package) {\n return `No release notes found for package \"${p.package}\".`;\n }\n\n const allNotes = await Promise.all(\n packagesToList.map((pkg) => getPackageReleaseNotes(pkg)),\n );\n let flatNotes = allNotes.flat();\n\n if (p.since_version) {\n flatNotes = filterReleaseNotesSince(flatNotes, p.since_version);\n }\n\n if (flatNotes.length === 0) {\n const filterDesc = p.since_version\n ? ` newer than ${p.since_version}`\n : \"\";\n return `No release notes found${filterDesc}.`;\n }\n\n return flatNotes.map(formatReleaseNoteListItem).join(\"\\n\");\n }\n\n case \"read\": {\n if (!p.package) throw new BadRequestError(\"package is required\");\n if (!p.version) throw new BadRequestError(\"version is required\");\n const filePath = path.join(\n RELEASE_NOTES_PATH,\n p.package,\n `${p.version}.md`,\n );\n return fs.readFile(filePath, \"utf-8\");\n }\n\n default:\n throw new BadRequestError(\n `Unknown command: ${command}. Use release_notes() for help.`,\n );\n }\n },\n});\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sources":["../../../src/suites/docs/index.ts"],"sourcesContent":["/**\n * Docs Suite - Documentation services (skill, version, release_notes)\n */\nimport { BadRequestError } from \"@jaypie/errors\";\nimport { fabricService } from \"@jaypie/fabric\";\nimport { parseFrontmatter } from \"@jaypie/kit\";\nimport {\n createLayeredStore,\n createMarkdownStore,\n createSkillService,\n type LayeredStoreLayer,\n} from \"@jaypie/tildeskill\";\nimport { existsSync, readdirSync, type Dirent } from \"node:fs\";\nimport * as fs from \"node:fs/promises\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { gt, rcompare, valid } from \"semver\";\n\nimport {\n getMcpAssetPaths,\n getMcpBuildInfo,\n MCP_ASSET_DIRECTORY,\n} from \"../../assets.js\";\nimport { RELEASE_NOTES_HELP } from \"./help.js\";\n\n// Build-time constants\nconst BUILD_VERSION_STRING = getMcpBuildInfo().versionString;\n\n// =============================================================================\n// ASSET PATHS\n// =============================================================================\n\nconst ENV_BUILTIN_SKILLS_PATH = \"MCP_BUILTIN_SKILLS_PATH\";\nconst ENV_RELEASE_NOTES_PATH = \"MCP_RELEASE_NOTES_PATH\";\nconst MARKDOWN_EXTENSION = \".md\";\nconst WARNING_PREFIX = \"[@jaypie/mcp]\";\n\n// A bundler (esbuild) collapses every module into one file, so this module's\n// directory is the bundle directory rather than dist/suites/docs/\nconst MODULE_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));\nconst PACKAGE_ASSET_PATHS = getMcpAssetPaths();\n\n/**\n * An explicit environment path wins outright. Otherwise the package copy wins,\n * then a copy beside the bundle. Returns the package path when neither exists.\n */\nfunction resolveAssetDirectory({\n bundled,\n envPath,\n packaged,\n}: {\n bundled: string;\n envPath?: string;\n packaged: string;\n}): string {\n if (envPath) return envPath;\n return (\n [packaged, bundled].find((candidate) => existsSync(candidate)) ?? packaged\n );\n}\n\nfunction hasEntries({\n directory,\n isEntry,\n}: {\n directory: string;\n isEntry: (entry: Dirent) => boolean;\n}): boolean {\n try {\n return readdirSync(directory, { withFileTypes: true }).some(isEntry);\n } catch {\n return false;\n }\n}\n\n/**\n * Warn once, on first use, when an asset directory is missing or empty. Waiting\n * for first use keeps servers that never call the service quiet.\n */\nfunction createAssetCheck({\n directory,\n envName,\n isEntry,\n name,\n}: {\n directory: string;\n envName: string;\n isEntry: (entry: Dirent) => boolean;\n name: string;\n}): () => void {\n let checked = false;\n return () => {\n if (checked) return;\n checked = true;\n if (hasEntries({ directory, isEntry })) return;\n // stderr keeps the stdio transport's stdout clean and still reaches CloudWatch\n // eslint-disable-next-line no-console\n console.warn(\n `${WARNING_PREFIX} No ${name} found in \"${directory}\". Copy the ${name} directory from getMcpAssetPaths() in @jaypie/mcp/assets beside the bundle or set ${envName}.`,\n );\n };\n}\n\n/** Run `check` before any property of `target` is read */\nfunction withAssetCheck<T extends object>({\n check,\n target,\n}: {\n check: () => void;\n target: T;\n}): T {\n return new Proxy(target, {\n get(object, property, receiver) {\n check();\n return Reflect.get(object, property, receiver);\n },\n });\n}\n\nconst RELEASE_NOTES_PATH = resolveAssetDirectory({\n bundled: path.join(MODULE_DIRECTORY, MCP_ASSET_DIRECTORY.RELEASE_NOTES),\n envPath: process.env[ENV_RELEASE_NOTES_PATH],\n packaged: PACKAGE_ASSET_PATHS.releaseNotes,\n});\n\n// Bundled Jaypie skills ship inside the @jaypie/mcp package. MCP_BUILTIN_SKILLS_PATH\n// relocates them without disabling the built-in layer.\nconst BUILTIN_SKILLS_PATH = resolveAssetDirectory({\n bundled: path.join(MODULE_DIRECTORY, MCP_ASSET_DIRECTORY.SKILLS),\n envPath: process.env[ENV_BUILTIN_SKILLS_PATH],\n packaged: PACKAGE_ASSET_PATHS.skills,\n});\n\nconst checkReleaseNoteAssets = createAssetCheck({\n directory: RELEASE_NOTES_PATH,\n envName: ENV_RELEASE_NOTES_PATH,\n isEntry: (entry) => entry.isDirectory(),\n name: MCP_ASSET_DIRECTORY.RELEASE_NOTES,\n});\n\nconst checkSkillAssets = createAssetCheck({\n directory: BUILTIN_SKILLS_PATH,\n envName: ENV_BUILTIN_SKILLS_PATH,\n isEntry: (entry) => entry.isFile() && entry.name.endsWith(MARKDOWN_EXTENSION),\n name: MCP_ASSET_DIRECTORY.SKILLS,\n});\n\nconst LOCAL_SKILLS_NAMESPACE = \"local\";\nconst JAYPIE_SKILLS_NAMESPACE = \"jaypie\";\nconst LAYER_SEPARATOR = \":\";\n\n// Skill layers resolved in order: a client's MCP_SKILLS_PATH layers on top of\n// the built-in Jaypie skill pack so `skill(\"aws\")` prefers the client's copy\n// while still exposing bundled Jaypie docs under the `jaypie:` namespace.\nconst skillLayers: LayeredStoreLayer[] = [];\n\nif (process.env.MCP_SKILLS_PATH) {\n skillLayers.push({\n namespace: LOCAL_SKILLS_NAMESPACE,\n store: createMarkdownStore({ path: process.env.MCP_SKILLS_PATH }),\n });\n}\n\nskillLayers.push({\n namespace: JAYPIE_SKILLS_NAMESPACE,\n store: withAssetCheck({\n check: checkSkillAssets,\n target: createMarkdownStore({ path: BUILTIN_SKILLS_PATH }),\n }),\n});\n\nconst skillStore = createLayeredStore({\n layers: skillLayers,\n separator: LAYER_SEPARATOR,\n});\n\n// =============================================================================\n// HELPER FUNCTIONS\n// =============================================================================\n\nconst CURSOR_ENCODING = \"base64url\";\nconst INVALID_CURSOR_MESSAGE =\n 'Invalid cursor. Call release_notes(\"list\") to start over.';\nconst RELEASE_NOTES_LIST_LIMIT = {\n DEFAULT: 50,\n MAXIMUM: 200,\n} as const;\n\ninterface ReleaseNoteFrontMatter {\n date?: string;\n summary?: string;\n version?: string;\n}\n\ninterface ReleaseNoteListItem {\n date?: string;\n filename: string;\n packageName: string;\n summary?: string;\n version?: string;\n}\n\n/** Decoded form of the opaque `list` cursor */\ninterface ReleaseNotesListCursor {\n limit?: number;\n offset: number;\n package?: string;\n since_version?: string;\n}\n\nfunction compareText({ a, b }: { a: string; b: string }): number {\n if (a < b) return -1;\n if (a > b) return 1;\n return 0;\n}\n\n/**\n * Package ascending, then semver descending. Valid semver sorts before\n * anything else, and filename breaks every remaining tie so the order is total.\n */\nfunction compareReleaseNotes({\n a,\n b,\n}: {\n a: ReleaseNoteListItem;\n b: ReleaseNoteListItem;\n}): number {\n const byPackage = compareText({ a: a.packageName, b: b.packageName });\n if (byPackage !== 0) return byPackage;\n\n const aVersion = a.version && valid(a.version);\n const bVersion = b.version && valid(b.version);\n if (aVersion && bVersion) {\n const byVersion = rcompare(aVersion, bVersion);\n if (byVersion !== 0) return byVersion;\n } else if (aVersion) {\n return -1;\n } else if (bVersion) {\n return 1;\n }\n\n return compareText({ a: b.filename, b: a.filename });\n}\n\nfunction isPositiveInteger(value: unknown): value is number {\n return typeof value === \"number\" && Number.isInteger(value) && value > 0;\n}\n\nfunction isOptionalString(value: unknown): boolean {\n return value === undefined || typeof value === \"string\";\n}\n\nfunction encodeListCursor(cursor: ReleaseNotesListCursor): string {\n const ordered: ReleaseNotesListCursor = {\n limit: cursor.limit,\n offset: cursor.offset,\n package: cursor.package,\n since_version: cursor.since_version,\n };\n return Buffer.from(JSON.stringify(ordered)).toString(CURSOR_ENCODING);\n}\n\nfunction decodeListCursor(cursor: unknown): ReleaseNotesListCursor {\n if (typeof cursor !== \"string\" || cursor.length === 0) {\n throw new BadRequestError(INVALID_CURSOR_MESSAGE);\n }\n\n let decoded: unknown;\n try {\n decoded = JSON.parse(\n Buffer.from(cursor, CURSOR_ENCODING).toString(\"utf-8\"),\n );\n } catch {\n throw new BadRequestError(INVALID_CURSOR_MESSAGE);\n }\n\n if (typeof decoded !== \"object\" || decoded === null) {\n throw new BadRequestError(INVALID_CURSOR_MESSAGE);\n }\n\n const candidate = decoded as Record<string, unknown>;\n const isValid =\n typeof candidate.offset === \"number\" &&\n Number.isInteger(candidate.offset) &&\n candidate.offset >= 0 &&\n (candidate.limit === undefined ||\n (isPositiveInteger(candidate.limit) &&\n candidate.limit <= RELEASE_NOTES_LIST_LIMIT.MAXIMUM)) &&\n isOptionalString(candidate.package) &&\n isOptionalString(candidate.since_version);\n\n if (!isValid) {\n throw new BadRequestError(INVALID_CURSOR_MESSAGE);\n }\n\n return candidate as unknown as ReleaseNotesListCursor;\n}\n\n/**\n * Accept a number or numeric string, reject anything below one, and clamp to\n * the maximum\n */\nfunction resolveListLimit(value: unknown): number {\n if (value === undefined || value === null || value === \"\") {\n return RELEASE_NOTES_LIST_LIMIT.DEFAULT;\n }\n const limit = typeof value === \"string\" ? Number(value) : value;\n if (!isPositiveInteger(limit)) {\n throw new BadRequestError(\"limit must be a positive integer\");\n }\n return Math.min(limit, RELEASE_NOTES_LIST_LIMIT.MAXIMUM);\n}\n\nasync function parseReleaseNoteFile(filePath: string): Promise<{\n date?: string;\n filename: string;\n summary?: string;\n version?: string;\n}> {\n try {\n const content = await fs.readFile(filePath, \"utf-8\");\n const filename = path.basename(filePath, \".md\");\n\n if (content.startsWith(\"---\")) {\n const frontMatter =\n parseFrontmatter<ReleaseNoteFrontMatter>(content).data;\n return {\n date: frontMatter.date,\n filename,\n summary: frontMatter.summary,\n version: frontMatter.version || filename,\n };\n }\n\n return { filename, version: filename };\n } catch {\n return { filename: path.basename(filePath, \".md\") };\n }\n}\n\nfunction formatReleaseNoteListItem(note: {\n date?: string;\n filename: string;\n packageName: string;\n summary?: string;\n version?: string;\n}): string {\n const { date, packageName, summary, version } = note;\n const parts = [`* ${packageName}@${version}`];\n\n if (date) {\n parts.push(`(${date})`);\n }\n\n if (summary) {\n parts.push(`- ${summary}`);\n }\n\n return parts.join(\" \");\n}\n\nasync function getPackageReleaseNotes(\n packageName: string,\n): Promise<ReleaseNoteListItem[]> {\n const packageDir = path.join(RELEASE_NOTES_PATH, packageName);\n try {\n const files = await fs.readdir(packageDir);\n const mdFiles = files.filter((file) => file.endsWith(MARKDOWN_EXTENSION));\n\n return await Promise.all(\n mdFiles.map(async (file) => {\n const parsed = await parseReleaseNoteFile(path.join(packageDir, file));\n return { ...parsed, packageName };\n }),\n );\n } catch {\n return [];\n }\n}\n\nfunction filterReleaseNotesSince(\n notes: ReleaseNoteListItem[],\n sinceVersion: string,\n): ReleaseNoteListItem[] {\n return notes.filter((note) => {\n if (!note.version) return false;\n try {\n return gt(note.version, sinceVersion);\n } catch {\n return false;\n }\n });\n}\n\n// =============================================================================\n// SKILL SERVICE\n// =============================================================================\n\nexport const skillService = createSkillService(skillStore);\n\n// =============================================================================\n// VERSION SERVICE\n// =============================================================================\n\nexport const versionService = fabricService({\n alias: \"version\",\n description: `Prints the current version and hash, \\`${BUILD_VERSION_STRING}\\``,\n input: {},\n service: async () => BUILD_VERSION_STRING,\n});\n\n// =============================================================================\n// RELEASE NOTES SERVICE\n// =============================================================================\n\ninterface ReleaseNotesInput {\n cursor?: string;\n limit?: number | string;\n package?: string;\n since_version?: string;\n version?: string;\n}\n\nexport const releaseNotesService = fabricService({\n alias: \"release_notes\",\n description:\n \"Browse Jaypie package release notes. Commands: list (paginated), read. Call with no args for help.\",\n input: {\n command: {\n description: \"Command to execute (omit for help)\",\n required: false,\n type: String,\n },\n input: {\n description: `Command parameters. list: package, since_version, limit (default ${RELEASE_NOTES_LIST_LIMIT.DEFAULT}, maximum ${RELEASE_NOTES_LIST_LIMIT.MAXIMUM}), cursor (from the \"Next page\" footer). read: package, version (both required).`,\n required: false,\n type: Object,\n },\n },\n service: async ({\n command,\n input: params,\n }: {\n command?: string;\n input?: ReleaseNotesInput;\n }) => {\n if (!command || command === \"help\") {\n return RELEASE_NOTES_HELP;\n }\n\n checkReleaseNoteAssets();\n const p = params || {};\n\n switch (command) {\n case \"list\": {\n const cursor =\n p.cursor === undefined ? undefined : decodeListCursor(p.cursor);\n if (cursor) {\n if (p.package !== undefined && p.package !== cursor.package) {\n throw new BadRequestError(\n \"package does not match the cursor. Omit package or start over without a cursor.\",\n );\n }\n if (\n p.since_version !== undefined &&\n p.since_version !== cursor.since_version\n ) {\n throw new BadRequestError(\n \"since_version does not match the cursor. Omit since_version or start over without a cursor.\",\n );\n }\n }\n\n const limit = resolveListLimit(p.limit ?? cursor?.limit);\n const offset = cursor?.offset ?? 0;\n const packageFilter = cursor ? cursor.package : p.package;\n const sinceVersion = cursor ? cursor.since_version : p.since_version;\n\n const entries = await fs.readdir(RELEASE_NOTES_PATH, {\n withFileTypes: true,\n });\n const packageDirs = entries\n .filter((entry) => entry.isDirectory())\n .map((entry) => entry.name);\n const packagesToList = packageFilter\n ? packageDirs.filter((pkg) => pkg === packageFilter)\n : packageDirs;\n\n if (packagesToList.length === 0 && packageFilter) {\n return `No release notes found for package \"${packageFilter}\".`;\n }\n\n const allNotes = await Promise.all(\n packagesToList.map((pkg) => getPackageReleaseNotes(pkg)),\n );\n let flatNotes = allNotes.flat();\n\n if (sinceVersion) {\n flatNotes = filterReleaseNotesSince(flatNotes, sinceVersion);\n }\n\n if (flatNotes.length === 0) {\n const filterDesc = sinceVersion ? ` newer than ${sinceVersion}` : \"\";\n return `No release notes found${filterDesc}.`;\n }\n\n if (offset >= flatNotes.length) {\n throw new BadRequestError(INVALID_CURSOR_MESSAGE);\n }\n\n flatNotes.sort((a, b) => compareReleaseNotes({ a, b }));\n const nextOffset = offset + limit;\n const page = flatNotes\n .slice(offset, nextOffset)\n .map(formatReleaseNoteListItem)\n .join(\"\\n\");\n\n if (nextOffset >= flatNotes.length) {\n return page;\n }\n\n const nextCursor = encodeListCursor({\n limit,\n offset: nextOffset,\n package: packageFilter,\n since_version: sinceVersion,\n });\n return `${page}\\n\\nNext page: release_notes(\"list\", { cursor: \"${nextCursor}\" })`;\n }\n\n case \"read\": {\n if (!p.package) throw new BadRequestError(\"package is required\");\n if (!p.version) throw new BadRequestError(\"version is required\");\n const filePath = path.join(\n RELEASE_NOTES_PATH,\n p.package,\n `${p.version}.md`,\n );\n return fs.readFile(filePath, \"utf-8\");\n }\n\n default:\n throw new BadRequestError(\n `Unknown command: ${command}. Use release_notes() for help.`,\n );\n }\n },\n});\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jaypie/mcp",
3
- "version": "0.8.158",
3
+ "version": "0.8.159",
4
4
  "description": "Jaypie MCP",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,19 @@
1
+ ---
2
+ version: 1.2.87
3
+ date: 2026-09-14
4
+ summary: JaypieStack warns at synth on an unset or non-hex PROJECT_NONCE
5
+ ---
6
+
7
+ ## Changes
8
+
9
+ - **`JaypieStack`** warns at synth (`@jaypie/constructs:projectNonceFormat`)
10
+ when `CDK_DEFAULT_ACCOUNT` is set and `PROJECT_NONCE` is unset or does not
11
+ match `^[0-9a-f]{6,}$`. A word or branch-name nonce lands in immutable stack
12
+ names and global S3 bucket names, where it repeats across projects and
13
+ accounts (#554).
14
+
15
+ ## Upgrade
16
+
17
+ - A stack deployed with a non-hex nonce keeps warning. Stack and bucket names
18
+ cannot be renamed: acknowledge `@jaypie/constructs:projectNonceFormat`, or
19
+ deploy a new install with a hex nonce and tear down the old one.
@@ -0,0 +1,50 @@
1
+ ---
2
+ version: 0.8.159
3
+ date: 2026-09-14
4
+ summary: PROJECT_NONCE is a required 8-hex environment variable in every skill
5
+ ---
6
+
7
+ ## Changes
8
+
9
+ - **`cicd-actions`** makes `project-nonce` `required: true` in
10
+ `setup-environment` and fails with `::error::` when it is empty. The branch
11
+ name and `prod` fallbacks are removed (#554).
12
+ - **`cicd-environments`** moves `PROJECT_NONCE` to Required Variables (8
13
+ lowercase hex, unique per environment, `openssl rand -hex 4`) and adds it to
14
+ the sandbox and production examples.
15
+ - **`cicd-deploy`** passes `project-nonce: ${{ vars.PROJECT_NONCE }}` in both
16
+ deploy jobs, replaces hand-rolled `api-${env}-${nonce}` naming with the
17
+ `JaypieStack` generated name, and adds One Install per Environment.
18
+ - **`variables`**, **`cdk`**, **`jaypie`**, **`recipe-api-server`**, and
19
+ **`debugging`** use hex nonces in place of `dev`, `staging`, and `prod`.
20
+ - **`cdk`** documents the `@jaypie/constructs:projectNonceFormat` synth
21
+ warning.
22
+ - **`github`** notes that `setup-environment` fails on an empty
23
+ `project-nonce` instead of falling back.
24
+ - **`@jaypie/mcp/assets`** exports `getMcpBuildInfo()`, which returns
25
+ `{ commit, version, versionString }` from the same build-time replacements the
26
+ `version` tool reports. A consumer syncing `skills/` and `release-notes/`
27
+ reads the build from the same package. The **`mcp`** skill documents it
28
+ (#555).
29
+ - **`tildeskill`** documents that layered `find` checks every layer for an
30
+ exact alias before plural/singular fallback (#558).
31
+ - **`tildeskill`** documents `createDynamoDbStore` from
32
+ `@jaypie/tildeskill/dynamodb`, `syncSkills`, and `delete` semantics for every
33
+ store. **`dynamodb`** notes that skills use `indexModelCategory` and that
34
+ `registerSkillModel()` declares it (#557).
35
+ - **`tildeskill`** documents that `createSkillService` throws `BadRequestError`
36
+ for an invalid alias and `NotFoundError` for a missing skill.
37
+ - **`release_notes("list")`** paginates. `limit` defaults to 50 and caps at
38
+ 200. Notes sort by package ascending, then version descending. A
39
+ `Next page: release_notes("list", { cursor: "..." })` footer appears only
40
+ when more notes exist, and its opaque cursor carries `package`,
41
+ `since_version`, and `limit`. Invalid cursors and filters that conflict with
42
+ the cursor return a bad request. The unfiltered first page drops from about
43
+ 61K to about 5.7K characters. The **`mcp`** and **`releasenotes`** skills
44
+ document it (#556).
45
+
46
+ ## Upgrade
47
+
48
+ - Projects built from the previous `cicd-actions` template may have deployed
49
+ branch-name nonces. Set `PROJECT_NONCE` on each GitHub environment. The new
50
+ value creates a new install, so tear down the old stacks and buckets.
@@ -0,0 +1,12 @@
1
+ ---
2
+ version: 1.2.74
3
+ date: 2026-09-14
4
+ summary: Tildeskill mocks cover the DynamoDB store, syncSkills, and SkillStore delete
5
+ ---
6
+
7
+ ## Changes
8
+
9
+ - Mock stores implement `delete`, resolving `false` (#557).
10
+ - New tildeskill mocks: `createDynamoDbStore`, `hashSkill`,
11
+ `registerSkillModel`, `syncSkills` (resolves empty lists), and the
12
+ `SKILL_MODEL`, `SKILL_MODEL_NAME`, and `SKILL_NAMESPACE` constants (#557).
@@ -0,0 +1,34 @@
1
+ ---
2
+ version: 0.3.5
3
+ date: 2026-09-14
4
+ summary: DynamoDB skill store, syncSkills, and SkillStore delete; layered find prefers an exact alias in any layer
5
+ ---
6
+
7
+ ## Changes
8
+
9
+ - `createLayeredStore().find` checks every layer for an exact alias before it
10
+ tries plural/singular fallback in any layer. With `studio:test` and
11
+ `jaypie:tests`, `find("tests")` returns `jaypie:tests` in place of
12
+ `studio:test`, so a local skill cannot shadow an existing Jaypie skill under
13
+ a different spelling (#558).
14
+ - `createSkillService` resolves through `find`, so `skill("tests")` follows the
15
+ same order. Namespace-qualified aliases still search only their own layer.
16
+ - `createDynamoDbStore({ category })` from the new `@jaypie/tildeskill/dynamodb`
17
+ subpath serves skills from `@jaypie/dynamodb` as `skill` entities with
18
+ deterministic ids (`uuidv5("<category>:<alias>", SKILL_NAMESPACE)`) and
19
+ `metadata.hash`. `@jaypie/dynamodb` is an optional peer dependency that the
20
+ main entry never imports. The store loads it with a dynamic import on first
21
+ use, so a CJS consumer shares the host's initialized client (#557).
22
+ - `syncSkills({ from, to })` puts added and changed records, leaves unchanged
23
+ records alone, deletes records missing from `from`, and returns
24
+ `{ added, removed, unchanged, updated }`. An empty `from` with a non-empty
25
+ `to` throws `ConfigurationError` unless `allowEmptySource: true` (#557).
26
+ - `SkillStore` gains `delete(alias)`. Memory removes the record, markdown
27
+ removes the file, layered requires a namespace-qualified alias, and DynamoDB
28
+ soft deletes (#557).
29
+ - New main exports: `hashSkill`, `registerSkillModel`, `SKILL_MODEL`,
30
+ `SKILL_MODEL_NAME`, `syncSkills`, and the `DynamoDbStoreOptions`,
31
+ `SyncSkillsOptions`, and `SyncSkillsResult` types.
32
+ - `createSkillService` throws `BadRequestError` for an invalid alias and
33
+ `NotFoundError` for a missing skill in place of a vanilla `Error`. Messages
34
+ are unchanged.
package/skills/cdk.md CHANGED
@@ -153,6 +153,8 @@ Props extend `StackProps` with one addition:
153
153
 
154
154
  If you see `undefined` or `unknown` in a generated stack name or tag, an env var is missing — fix the environment rather than overriding `stackName`.
155
155
 
156
+ `JaypieStack` warns at synth (`@jaypie/constructs:projectNonceFormat`) when `CDK_DEFAULT_ACCOUNT` is set and `PROJECT_NONCE` is unset or is not lowercase hex of six or more characters. The warning fires before the first deploy makes a word or branch-name nonce permanent.
157
+
156
158
  #### `JaypieAppStack` / `JaypieInfrastructureStack`
157
159
 
158
160
  Thin convenience subclasses that pre-fill `key`:
@@ -206,12 +208,14 @@ See `skill("variables")` for the role of `PROJECT_ENV` and `PROJECT_NONCE`.
206
208
 
207
209
  ```bash
208
210
  # Deploy to sandbox
209
- PROJECT_ENV=sandbox PROJECT_NONCE=dev cdk deploy
211
+ PROJECT_ENV=sandbox PROJECT_NONCE=3f9c21ab cdk deploy
210
212
 
211
213
  # Deploy to production
212
- PROJECT_ENV=production PROJECT_NONCE=prod cdk deploy
214
+ PROJECT_ENV=production PROJECT_NONCE=ba342b91 cdk deploy
213
215
  ```
214
216
 
217
+ Deploys run in CI (see `skill("cicd-deploy")`). Each environment carries its own `PROJECT_NONCE`, 8 lowercase hex characters generated once with `openssl rand -hex 4`. See `skill("github")` for provisioning.
218
+
215
219
  ## Lambda with DynamoDB Tables
216
220
 
217
221
  Pass tables to `JaypieLambda` for automatic permissions and environment wiring:
@@ -49,9 +49,8 @@ inputs:
49
49
  description: 'Sponsor segment of the generated stack name'
50
50
  required: true
51
51
  project-nonce:
52
- description: 'Unique resource identifier'
53
- required: false
54
- default: ''
52
+ description: '8 lowercase hex characters, unique per environment'
53
+ required: true
55
54
  log-level:
56
55
  description: 'Log level (trace, debug, info, warn, error)'
57
56
  required: false
@@ -92,14 +91,11 @@ runs:
92
91
  echo "PROJECT_KEY=${{ inputs.project-key }}" >> $GITHUB_ENV
93
92
  echo "PROJECT_SPONSOR=${{ inputs.project-sponsor }}" >> $GITHUB_ENV
94
93
 
95
- # Resolve PROJECT_NONCE (default: branch name or 'prod')
94
+ # Resolve PROJECT_NONCE (required, no fallback)
96
95
  NONCE="${{ inputs.project-nonce }}"
97
96
  if [ -z "$NONCE" ]; then
98
- if [ "$PROJECT_ENV" = "production" ]; then
99
- NONCE="prod"
100
- else
101
- NONCE="${GITHUB_REF_NAME//\//-}"
102
- fi
97
+ echo "::error::project-nonce is empty. Set PROJECT_NONCE on the GitHub environment (openssl rand -hex 4)"
98
+ exit 1
103
99
  fi
104
100
  echo "project-nonce=${NONCE}" >> $GITHUB_OUTPUT
105
101
  echo "PROJECT_NONCE=${NONCE}" >> $GITHUB_ENV
@@ -131,6 +127,8 @@ runs:
131
127
 
132
128
  `project-sponsor` is `required: true` on purpose. `PROJECT_SPONSOR` is the first segment of the generated stack name (`cdk-{PROJECT_SPONSOR}-{PROJECT_KEY}-{PROJECT_ENV}-{PROJECT_NONCE}`), and an optional input with an empty default deploys `cdk-undefined-...` silently. A stack name is immutable, so correcting it later means a stack replacement. See `skill("cdk")` for stack naming and `skill("variables")` for the full variable reference.
133
129
 
130
+ `project-nonce` is `required: true` with no fallback, and the step exits with `::error::` when it is empty because GitHub does not enforce `required` on composite action inputs. `PROJECT_NONCE` is 8 lowercase hex characters, generated once per environment (`openssl rand -hex 4`) and stored as a GitHub environment variable (see `skill("cicd-environments")`). It lands in globally unique names (S3 buckets), SSM parameter paths, and immutable stack names. A branch name or word such as `prod` repeats across projects and accounts, and changing the nonce later means a new install beside the old one.
131
+
134
132
  ## configure-aws/action.yml
135
133
 
136
134
  Configures AWS credentials via OIDC.
@@ -75,6 +75,7 @@ jobs:
75
75
  project-key: my-project
76
76
  project-sponsor: ${{ vars.PROJECT_SPONSOR }}
77
77
  project-env: sandbox
78
+ project-nonce: ${{ vars.PROJECT_NONCE }}
78
79
 
79
80
  - uses: ./.github/actions/configure-aws
80
81
  with:
@@ -163,6 +164,7 @@ jobs:
163
164
  project-key: my-project
164
165
  project-sponsor: ${{ vars.PROJECT_SPONSOR }}
165
166
  project-env: production
167
+ project-nonce: ${{ vars.PROJECT_NONCE }}
166
168
 
167
169
  - uses: ./.github/actions/configure-aws
168
170
  with:
@@ -271,27 +273,28 @@ Requirements:
271
273
 
272
274
  See `skill("cicd-actions")` for the `web-deploy` action body and `skill("web")` for the construct.
273
275
 
274
- ## Environment-Specific Stack Naming
276
+ ## Stack Naming
275
277
 
276
- Use consistent stack naming with environment and nonce:
278
+ `JaypieStack` names every stack `cdk-{PROJECT_SPONSOR}-{PROJECT_KEY}-{PROJECT_ENV}-{PROJECT_NONCE}[-{key}]` and resolves the account and region (see `skill("cdk")`). Do not compose stack names in `app.ts`:
277
279
 
278
280
  ```typescript
279
281
  // workspaces/cdk/src/app.ts
280
- const env = process.env.PROJECT_ENV || "sandbox";
281
- const nonce = process.env.PROJECT_NONCE || "dev";
282
-
283
- new ApiStack(app, `api-${env}-${nonce}`, {
284
- env: {
285
- account: process.env.CDK_DEFAULT_ACCOUNT,
286
- region: process.env.CDK_DEFAULT_REGION,
287
- },
288
- });
282
+ import { App } from "aws-cdk-lib";
283
+ import { JaypieAppStack } from "@jaypie/constructs";
284
+
285
+ const app = new App();
286
+ new JaypieAppStack(app, "AppStack");
289
287
  ```
290
288
 
291
- This produces stack names like:
292
- - `api-sandbox-feat-new-feature`
293
- - `api-sandbox-main`
294
- - `api-production-prod`
289
+ With `PROJECT_NONCE` set per environment (see `skill("cicd-environments")`), this produces:
290
+ - `cdk-finlaysonstudio-my-project-sandbox-3f9c21ab-app`
291
+ - `cdk-finlaysonstudio-my-project-production-ba342b91-app`
292
+
293
+ The `cdk-deploy` globs `*-sandbox-*` and `*-production-*` match these names.
294
+
295
+ ### One Install per Environment
296
+
297
+ The nonce belongs to the environment, so every branch that deploys to sandbox updates the same stacks. A project serving fixed hostnames (`api.sandbox.example.com`) holds one install per environment, because CloudFront aliases and Route53 records collide. Override `project-nonce` with a new hex value only for an intentional clean install beside an existing one. Stack and bucket names cannot be renamed, so recovering from a wrong nonce means that new install and a teardown of the old one.
295
298
 
296
299
  ## Deployment Flow
297
300
 
@@ -12,6 +12,9 @@ GitHub Environments provide deployment-specific variables for Jaypie CDK workflo
12
12
  | Variable | Description |
13
13
  |----------|-------------|
14
14
  | `AWS_ROLE_ARN` | OIDC role ARN for assuming AWS credentials |
15
+ | `PROJECT_NONCE` | 8 lowercase hex characters, unique per environment |
16
+
17
+ Generate `PROJECT_NONCE` once per environment with `openssl rand -hex 4`. It scopes stack names, S3 bucket names, and SSM paths, so a new value deploys a new install and strands the old one. `setup-environment` fails when it is missing (see `skill("cicd-actions")`).
15
18
 
16
19
  ## Optional Variables
17
20
 
@@ -21,7 +24,6 @@ GitHub Environments provide deployment-specific variables for Jaypie CDK workflo
21
24
  | `LOG_LEVEL` | `trace` (sandbox) / `info` (production) | Application log level |
22
25
  | `PROJECT_CHAOS` | `full` (sandbox) / `none` (production) | Chaos engineering mode |
23
26
  | `PROJECT_ENV` | Environment name | Environment identifier |
24
- | `PROJECT_NONCE` | Branch name or `prod` | Unique identifier for resources |
25
27
 
26
28
  ## Environment Setup
27
29
 
@@ -43,6 +45,7 @@ GitHub Environments provide deployment-specific variables for Jaypie CDK workflo
43
45
  AWS_ROLE_ARN = arn:aws:iam::123456789012:role/GitHubActions-Sandbox
44
46
  AWS_REGION = us-east-1
45
47
  PROJECT_ENV = sandbox
48
+ PROJECT_NONCE = 3f9c21ab
46
49
  LOG_LEVEL = trace
47
50
  PROJECT_CHAOS = full
48
51
  ```
@@ -53,6 +56,7 @@ PROJECT_CHAOS = full
53
56
  AWS_ROLE_ARN = arn:aws:iam::123456789012:role/GitHubActions-Production
54
57
  AWS_REGION = us-east-1
55
58
  PROJECT_ENV = production
59
+ PROJECT_NONCE = ba342b91
56
60
  LOG_LEVEL = info
57
61
  PROJECT_CHAOS = none
58
62
  ```
@@ -39,11 +39,15 @@ If you see `undefined` in a CDK stack name, a required variable is missing:
39
39
  // BAD: undefined if PROJECT_NONCE not set
40
40
  const stackName = `api-${process.env.PROJECT_NONCE}`;
41
41
 
42
- // GOOD: Provide default
42
+ // BAD: a word default repeats across projects and accounts
43
43
  const nonce = process.env.PROJECT_NONCE || "dev";
44
- const stackName = `api-${nonce}`;
44
+
45
+ // GOOD: JaypieStack names the stack from PROJECT_* variables
46
+ new JaypieAppStack(app, "AppStack");
45
47
  ```
46
48
 
49
+ Set the missing variable on the GitHub environment rather than adding a default (see `skill("cicd-environments")`).
50
+
47
51
  ### "unknown" in Logs
48
52
 
49
53
  `unknown` indicates a value was expected but fell back to a default:
@@ -366,6 +366,10 @@ const nextPage = await queryByScope({
366
366
 
367
367
  `storeExchange(envelope, { exchange?, scope? })` persists an `@jaypie/llm` exchange envelope (see `skill("llm")`) as an `exchange` entity. It registers the canonical exchange model (`registerExchangeModel()` from `@jaypie/fabric`), maps envelope fields onto reserved attributes (`input`, `content`, `data`, `llm`, `status`, `xid`), and owns 400KB item-limit safety (drops the history delta, then truncates long fields, marking `metadata.truncated`). It warns and returns `null` — never throws — when `initClient()` has not run or the write fails. `@jaypie/llm` calls it automatically when `LLM_EXCHANGE_ENABLED` is set; pass `exchange` (parent id) for turn chains and `scope` to nest under a parent entity.
368
368
 
369
+ ### Skill Storage
370
+
371
+ `@jaypie/tildeskill/dynamodb` stores skills as `skill` entities queried through `indexModelCategory` (see `skill("tildeskill")`). Call `registerSkillModel()` from `@jaypie/tildeskill` before `getAllRegisteredIndexes()` so CDK declares the index.
372
+
369
373
  ### TTL (Time to Live)
370
374
 
371
375
  DynamoDB expires items whose TTL attribute holds a past Unix epoch **in seconds**. Jaypie writes that attribute (`ttl` by default) and the `JaypieDynamoDb` construct enables TTL on it by default.
package/skills/github.md CHANGED
@@ -260,7 +260,7 @@ jobs:
260
260
 
261
261
  - Deploy jobs declare `permissions` with `contents: read` and `id-token: write`
262
262
  - Composite actions under `.github/actions` receive every value through `inputs` and never reference `vars.*` or `secrets.*`
263
- - `setup-environment` supplies fallback defaults when an input is empty
263
+ - `setup-environment` supplies fallback defaults when an input is empty, except `project-nonce`, which fails the job
264
264
  - The OIDC subject for an environment job is `repo:<org>/<repo>:environment:<env>` (see ~cicd-environments for the trust policy)
265
265
 
266
266
  ## Audit
package/skills/jaypie.md CHANGED
@@ -66,7 +66,7 @@ project/
66
66
  |----------|---------|
67
67
  | `PROJECT_ENV` | Environment: local, sandbox, production |
68
68
  | `PROJECT_KEY` | Project identifier for logging |
69
- | `PROJECT_NONCE` | Unique resource identifier |
69
+ | `PROJECT_NONCE` | 8 lowercase hex characters, unique per environment |
70
70
  | `LOG_LEVEL` | Log level: trace, debug, info, warn, error |
71
71
 
72
72
  ### Error Handling
package/skills/mcp.md CHANGED
@@ -27,11 +27,15 @@ skill("tests") # Testing patterns
27
27
 
28
28
  ```
29
29
  release_notes() # Show help
30
- release_notes("list") # List all release notes
30
+ release_notes("list") # First page of all release notes
31
+ release_notes("list", { limit: 20 }) # Smaller page (default 50, maximum 200)
31
32
  release_notes("list", { package: "mcp" }) # Filter by package
33
+ release_notes("list", { cursor: "..." }) # Next page, from the footer
32
34
  release_notes("read", { package: "mcp", version: "0.5.0" }) # Read specific note
33
35
  ```
34
36
 
37
+ `list` sorts by package ascending, then version descending. When more notes exist, the last line reads `Next page: release_notes("list", { cursor: "..." })`. The cursor carries `package`, `since_version`, and `limit`, so that call alone continues the listing.
38
+
35
39
  ## Datadog Tool
36
40
 
37
41
  Unified tool for logs, monitors, metrics, synthetics, and RUM.
@@ -134,6 +138,17 @@ Each directory resolves to the first match: its environment variable, the instal
134
138
  | `MCP_RELEASE_NOTES_PATH` | Release notes directory |
135
139
  | `MCP_SKILLS_PATH` | Client skills directory layered over the Jaypie skills (namespace `local`) |
136
140
 
141
+ `getMcpBuildInfo` from `@jaypie/mcp/assets` returns `{ commit, version, versionString }` of the build that packed the markdown, the same values the `version` tool reports. A consumer that syncs the copied directories reads it from the same package without a registry call.
142
+
143
+ ```typescript
144
+ import { getMcpBuildInfo } from "@jaypie/mcp/assets";
145
+
146
+ const { commit, version, versionString } = getMcpBuildInfo();
147
+ // versionString: "@jaypie/mcp@0.8.159#460f95c3"
148
+ ```
149
+
150
+ `commit` is the first 8 characters of `PROJECT_COMMIT` at build time and is empty when it was unset. Unbuilt source returns version `0.0.0`.
151
+
137
152
  ### SDK Transport (`mcpExpressHandler`)
138
153
 
139
154
  `mcpExpressHandler` from `@jaypie/mcp` wraps the SDK's `StreamableHTTPServerTransport` with optional sessions. It suits a long-running Express server, not Lambda.
@@ -178,7 +178,8 @@ See `skill("apikey")` for full key generation/validation/hashing documentation.
178
178
 
179
179
  ```bash
180
180
  # First deploy creates secrets and table
181
- PROJECT_ENV=sandbox PROJECT_NONCE=dev npx cdk deploy
181
+ # PROJECT_NONCE is the environment's 8-hex nonce (see skill("cicd-environments"))
182
+ PROJECT_ENV=sandbox PROJECT_NONCE=3f9c21ab npx cdk deploy
182
183
  ```
183
184
 
184
185
  No workflow secrets needed for generated values — CloudFormation creates them on first deploy and preserves them on subsequent deploys.
@@ -9,12 +9,15 @@ How to access and write release notes for Jaypie packages.
9
9
  ## MCP Tools
10
10
 
11
11
  ```
12
- mcp__jaypie__list_release_notes()
13
- mcp__jaypie__list_release_notes(package: "mcp")
14
- mcp__jaypie__list_release_notes(package: "jaypie", since_version: "1.0.0")
15
- mcp__jaypie__read_release_note(package: "mcp", version: "0.3.4")
12
+ release_notes("list")
13
+ release_notes("list", { package: "mcp" })
14
+ release_notes("list", { package: "jaypie", since_version: "1.0.0" })
15
+ release_notes("list", { cursor: "..." })
16
+ release_notes("read", { package: "mcp", version: "0.3.4" })
16
17
  ```
17
18
 
19
+ `list` returns 50 notes per page by default (`limit`, maximum 200). When more notes exist, the last line is a `Next page` footer with a `cursor` that continues the same listing.
20
+
18
21
  ## Directory Structure
19
22
 
20
23
  ```
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: Skill/vocabulary storage with pluggable backends (pre-1.0)
3
- related: fabric, mcp
3
+ related: dynamodb, fabric, mcp
4
4
  ---
5
5
 
6
6
  # @jaypie/tildeskill
@@ -14,6 +14,8 @@ This package provides a storage abstraction for skill/vocabulary documents with
14
14
  - Loading skills from markdown files with YAML frontmatter
15
15
  - In-memory storage for testing
16
16
  - Layered composition of multiple stores with namespace prefixes
17
+ - DynamoDB storage through the `@jaypie/tildeskill/dynamodb` subpath
18
+ - Store-to-store sync with `syncSkills`
17
19
  - Consistent alias normalization and validation
18
20
  - Filtering by namespace and tags
19
21
  - Searching across alias, name, description, content, and tags
@@ -46,6 +48,7 @@ interface ListFilter {
46
48
  }
47
49
 
48
50
  interface SkillStore {
51
+ delete(alias: string): Promise<boolean>;
49
52
  find(alias: string): Promise<SkillRecord | null>;
50
53
  get(alias: string): Promise<SkillRecord | null>;
51
54
  getByNickname(nickname: string): Promise<SkillRecord[]>;
@@ -87,6 +90,71 @@ const store = createMemoryStore([
87
90
  const skill = await store.get("test");
88
91
  ```
89
92
 
93
+ ## DynamoDB Store
94
+
95
+ ```typescript
96
+ import { initClient } from "@jaypie/dynamodb";
97
+ import { createSkillService } from "@jaypie/tildeskill";
98
+ import { createDynamoDbStore } from "@jaypie/tildeskill/dynamodb";
99
+
100
+ initClient();
101
+ const store = createDynamoDbStore({ category: "jaypie" });
102
+ const skillService = createSkillService(store);
103
+ ```
104
+
105
+ `@jaypie/dynamodb` is an optional peer dependency. Only the
106
+ `@jaypie/tildeskill/dynamodb` subpath imports it, so markdown-only consumers
107
+ never load it. The store loads it with a dynamic import on first use, so a
108
+ CJS consumer shares the host's initialized client. Call `initClient()` before
109
+ using the store.
110
+
111
+ - Records are `skill` entities at APEX scope. `category` is the store
112
+ namespace; `alias` is unqualified.
113
+ - Ids are deterministic: `uuidv5("<category>:<alias>", SKILL_NAMESPACE)`.
114
+ `SKILL_NAMESPACE` is exported from the subpath and never changes.
115
+ - `metadata.hash` holds `hashSkill(record)` (sha256) for change detection.
116
+ - `includes`, `nicknames`, and `related` are stored in `metadata`, since
117
+ `related` is reserved for entity references.
118
+ - `list`, `getByNickname`, and `search` query `indexModelCategory`.
119
+ `createDynamoDbStore` calls `registerSkillModel()`; declare the index in CDK
120
+ by calling `registerSkillModel()` before `getAllRegisteredIndexes()`.
121
+ - `find` keeps the plural/singular fallback.
122
+ - `delete` soft deletes with `deleteEntity`. Reads skip archived and deleted
123
+ entities, and a later `put` restores the same id.
124
+
125
+ ## Syncing Stores
126
+
127
+ ```typescript
128
+ import { createMarkdownStore, syncSkills } from "@jaypie/tildeskill";
129
+
130
+ const result = await syncSkills({
131
+ from: createMarkdownStore({ path: "./skills" }),
132
+ to: store,
133
+ });
134
+ // { added: ["aws"], removed: ["retired"], unchanged: ["tests"], updated: [] }
135
+ ```
136
+
137
+ `syncSkills` makes `to` match `from`. It puts records missing from `to`
138
+ (`added`) or whose `hashSkill` differs (`updated`), skips records whose hash
139
+ matches (`unchanged`), and deletes records missing from `from` (`removed`).
140
+ Each list holds aliases sorted alphabetically. An empty `from` with a
141
+ non-empty `to` throws `ConfigurationError` before writing, since a markdown
142
+ store pointed at a missing directory lists nothing. Pass
143
+ `allowEmptySource: true` to empty `to` on purpose.
144
+
145
+ ## Deleting Records
146
+
147
+ Every store implements `delete(alias)`. It matches the exact alias (no
148
+ plural/singular fallback) and returns `true` when a record was removed and
149
+ `false` when none existed.
150
+
151
+ | Store | `delete` behavior |
152
+ |-------|-------------------|
153
+ | memory | Removes the record from the map |
154
+ | markdown | Removes `<alias>.md`; throws `BadRequestError` for an invalid alias |
155
+ | layered | Requires a namespace-qualified alias and delegates to that layer; throws `ConfigurationError` when unqualified |
156
+ | dynamodb | Soft deletes the entity; reads skip it and `put` restores it |
157
+
90
158
  ## Skill Service Factory
91
159
 
92
160
  ```typescript
@@ -102,6 +170,8 @@ const skillService = createSkillService(store);
102
170
  await skillService({ alias: "aws" }); // → skill content (with expandIncludes)
103
171
  await skillService({ alias: "index" }); // → formatted listing
104
172
  await skillService(); // → same as "index"
173
+ await skillService({ alias: "missing" }); // throws NotFoundError
174
+ await skillService({ alias: "../bad" }); // throws BadRequestError
105
175
 
106
176
  // Use with fabricTool for Llm.operate toolkits
107
177
  import { fabricTool } from "@jaypie/fabric/llm";
@@ -159,11 +229,16 @@ const layered = createLayeredStore({
159
229
 
160
230
  await layered.get("aws"); // → { alias: "local:aws", ... }
161
231
  await layered.get("jaypie:aws"); // → { alias: "jaypie:aws", ... }
162
- await layered.find("skills"); // per-layer plural fallback
232
+ await layered.find("skills"); // exact alias in any layer, then fallback
163
233
  await layered.list(); // prefixed aliases from every layer
164
234
  await layered.put({ alias: "local:new", content: "# New" }); // must be qualified
165
235
  ```
166
236
 
237
+ `find` checks every layer for an exact alias before it tries plural/singular
238
+ fallback in any layer. With `local:test` and `jaypie:tests`, `find("tests")`
239
+ returns `jaypie:tests`, so a local skill cannot shadow a Jaypie skill under a
240
+ different spelling. Namespace-qualified aliases search only their own layer.
241
+
167
242
  The MCP server itself uses `createLayeredStore` to place `MCP_SKILLS_PATH`
168
243
  (the client's local library, namespace `local`) over the bundled Jaypie
169
244
  skills (namespace `jaypie`). A bundled build finds the Jaypie base layer in
@@ -13,7 +13,7 @@ Configuration variables used in Jaypie applications.
13
13
  |----------|-------------|--------|
14
14
  | `PROJECT_ENV` | Environment identifier; scopes physical resource names | local, sandbox, kitchen, lab, studio, production |
15
15
  | `PROJECT_KEY` | Project name for logging | e.g., my-api |
16
- | `PROJECT_NONCE` | Unique resource suffix; scopes physical resource names | e.g., dev, staging, prod |
16
+ | `PROJECT_NONCE` | 8 lowercase hex characters, unique per environment; scopes physical resource names | e.g., ba342b91 |
17
17
  | `PROJECT_SPONSOR` | Sponsor segment of the generated stack name and the `sponsor` tag | e.g., finlaysonstudio |
18
18
  | `PROJECT_CHAOS` | Chaos engineering mode | none, partial, full |
19
19
  | `PROJECT_SALT` | Secret salt for HMAC hashing (API keys) | Generated by JaypieEnvSecret |
@@ -23,6 +23,8 @@ Configuration variables used in Jaypie applications.
23
23
 
24
24
  `PROJECT_ENV` and `PROJECT_NONCE` together scope physical AWS resource names to avoid collisions when multiple stacks deploy to the same account. Any explicitly named resource (ECR repos, log groups, ECS task families, EventBridge rules, etc.) must include both values. See `skill("cdk")` for the full naming convention.
25
25
 
26
+ Generate `PROJECT_NONCE` once per environment with `openssl rand -hex 4` and store it as a GitHub environment variable (see `skill("github")`). A word or branch name repeats across projects and accounts, and S3 bucket names are global.
27
+
26
28
  ### Stack Naming
27
29
 
28
30
  `JaypieStack` generates `cdk-{PROJECT_SPONSOR}-{PROJECT_KEY}-{PROJECT_ENV}-{PROJECT_NONCE}[-{key}]`. Every segment must be set in CI or the name contains `undefined`, and a stack name is immutable. `skill("cicd-actions")` sets all four in `setup-environment`.