@jaypie/mcp 0.8.25 → 0.8.28

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.
@@ -1,5 +1,5 @@
1
1
  import { fabricService } from '@jaypie/fabric';
2
- import { createMarkdownStore, normalizeAlias, isValidAlias } from '@jaypie/tildeskill';
2
+ import { createMarkdownStore, createLayeredStore, normalizeAlias, isValidAlias } from '@jaypie/tildeskill';
3
3
  import * as fs from 'node:fs/promises';
4
4
  import * as path from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
@@ -9,7 +9,7 @@ import { gt } from 'semver';
9
9
  /**
10
10
  * Docs Suite - Documentation services (skill, version, release_notes)
11
11
  */
12
- const BUILD_VERSION_STRING = "@jaypie/mcp@0.8.25#e9d2d378"
12
+ const BUILD_VERSION_STRING = "@jaypie/mcp@0.8.28#dbd0faa2"
13
13
  ;
14
14
  const __filename$1 = fileURLToPath(import.meta.url);
15
15
  const __dirname$1 = path.dirname(__filename$1);
@@ -17,10 +17,34 @@ const __dirname$1 = path.dirname(__filename$1);
17
17
  // Environment variables allow overriding paths when bundled (e.g., esbuild Lambda)
18
18
  const RELEASE_NOTES_PATH = process.env.MCP_RELEASE_NOTES_PATH ||
19
19
  path.join(__dirname$1, "..", "..", "..", "release-notes");
20
- const SKILLS_PATH = process.env.MCP_SKILLS_PATH ||
20
+ // Bundled Jaypie skills ship inside the @jaypie/mcp package. MCP_BUILTIN_SKILLS_PATH
21
+ // lets bundlers (esbuild, Lambda) relocate them without disabling the built-in layer.
22
+ const BUILTIN_SKILLS_PATH = process.env.MCP_BUILTIN_SKILLS_PATH ||
21
23
  path.join(__dirname$1, "..", "..", "..", "skills");
22
- // Create skill store using tildeskill
23
- const skillStore = createMarkdownStore({ path: SKILLS_PATH });
24
+ const LOCAL_SKILLS_NAMESPACE = "local";
25
+ const JAYPIE_SKILLS_NAMESPACE = "jaypie";
26
+ const LAYER_SEPARATOR = ":";
27
+ // Skill layers resolved in order: a client's MCP_SKILLS_PATH layers on top of
28
+ // the built-in Jaypie skill pack so `skill("aws")` prefers the client's copy
29
+ // while still exposing bundled Jaypie docs under the `jaypie:` namespace.
30
+ const skillLayers = [];
31
+ const skillLayerPaths = new Map();
32
+ if (process.env.MCP_SKILLS_PATH) {
33
+ skillLayers.push({
34
+ namespace: LOCAL_SKILLS_NAMESPACE,
35
+ store: createMarkdownStore({ path: process.env.MCP_SKILLS_PATH }),
36
+ });
37
+ skillLayerPaths.set(LOCAL_SKILLS_NAMESPACE, process.env.MCP_SKILLS_PATH);
38
+ }
39
+ skillLayers.push({
40
+ namespace: JAYPIE_SKILLS_NAMESPACE,
41
+ store: createMarkdownStore({ path: BUILTIN_SKILLS_PATH }),
42
+ });
43
+ skillLayerPaths.set(JAYPIE_SKILLS_NAMESPACE, BUILTIN_SKILLS_PATH);
44
+ const skillStore = createLayeredStore({
45
+ layers: skillLayers,
46
+ separator: LAYER_SEPARATOR,
47
+ });
24
48
  async function parseReleaseNoteFile(filePath) {
25
49
  try {
26
50
  const content = await fs.readFile(filePath, "utf-8");
@@ -98,27 +122,6 @@ function filterReleaseNotesSince(notes, sinceVersion) {
98
122
  // =============================================================================
99
123
  // SKILL SERVICE
100
124
  // =============================================================================
101
- /**
102
- * Generate alternative spellings for plural/singular matching
103
- */
104
- function getAlternativeSpellings(alias) {
105
- const alternatives = [];
106
- if (alias.endsWith("es")) {
107
- // "indexes" -> try "indexe" and "index"
108
- alternatives.push(alias.slice(0, -1)); // Remove "s" -> "indexe"
109
- alternatives.push(alias.slice(0, -2)); // Remove "es" -> "index"
110
- }
111
- else if (alias.endsWith("s")) {
112
- // "skills" -> try "skill"
113
- alternatives.push(alias.slice(0, -1)); // Remove "s" -> "skill"
114
- }
115
- else {
116
- // "fish" -> try "fishs" and "fishes"
117
- alternatives.push(alias + "s");
118
- alternatives.push(alias + "es");
119
- }
120
- return alternatives;
121
- }
122
125
  /**
123
126
  * Add alias to frontmatter indicating the canonical skill name
124
127
  */
@@ -153,33 +156,32 @@ const skillService = fabricService({
153
156
  }
154
157
  if (alias === "index") {
155
158
  const allSkills = await skillStore.list();
156
- const skills = allSkills.filter((s) => s.alias !== "index");
159
+ // Index entries from every layer hide per-layer "index" records.
160
+ const skills = allSkills.filter((s) => s.alias !== "index" && !s.alias.endsWith(`${LAYER_SEPARATOR}index`));
157
161
  const skillList = skills.map(formatSkillListItem).join("\n");
158
162
  return `# Index of Skills\n\n${skillList}`;
159
163
  }
160
- // Try exact match first
161
- let skill = await skillStore.get(alias);
162
- let matchedAlias = alias;
163
- // If no exact match, try alternative spellings (plural/singular)
164
- if (!skill) {
165
- const alternatives = getAlternativeSpellings(alias);
166
- for (const alt of alternatives) {
167
- skill = await skillStore.get(alt);
168
- if (skill) {
169
- matchedAlias = alt;
170
- break;
171
- }
172
- }
173
- }
164
+ const skill = await skillStore.find(alias);
174
165
  if (!skill) {
175
166
  throw new Error(`Skill "${alias}" not found. Use skill("index") to list available skills.`);
176
167
  }
177
- // Return raw file content for non-index skills (preserve frontmatter)
178
- const skillPath = path.join(SKILLS_PATH, `${matchedAlias}.md`);
168
+ // Split the namespaced alias the layered store returned (e.g., "local:aws")
169
+ // so we can read the raw markdown file from the correct layer directory.
170
+ const separatorIdx = skill.alias.indexOf(LAYER_SEPARATOR);
171
+ const layerNamespace = separatorIdx === -1 ? "" : skill.alias.slice(0, separatorIdx);
172
+ const innerAlias = separatorIdx === -1 ? skill.alias : skill.alias.slice(separatorIdx + 1);
173
+ const layerPath = skillLayerPaths.get(layerNamespace);
174
+ if (!layerPath) {
175
+ // Defensive: layered store returned a layer we don't know the path for.
176
+ return skill.content;
177
+ }
178
+ const skillPath = path.join(layerPath, `${innerAlias}.md`);
179
179
  let content = await fs.readFile(skillPath, "utf-8");
180
- // If we matched via alternative spelling, add alias to indicate canonical name
181
- if (matchedAlias !== alias) {
182
- content = addAliasToFrontmatter(content, matchedAlias);
180
+ // Detect plural/singular fallback so we can annotate the canonical alias.
181
+ const inputSeparatorIdx = alias.indexOf(LAYER_SEPARATOR);
182
+ const inputInnerAlias = inputSeparatorIdx === -1 ? alias : alias.slice(inputSeparatorIdx + 1);
183
+ if (innerAlias !== inputInnerAlias) {
184
+ content = addAliasToFrontmatter(content, skill.alias);
183
185
  }
184
186
  return content;
185
187
  },
@@ -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 { fabricService } from \"@jaypie/fabric\";\nimport {\n createMarkdownStore,\n isValidAlias,\n normalizeAlias,\n type SkillRecord,\n} from \"@jaypie/tildeskill\";\nimport * as fs from \"node:fs/promises\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport matter from \"gray-matter\";\nimport { gt } from \"semver\";\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\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n// From dist/suites/docs/, go up 3 levels to package root where skills/ and release-notes/ live\n// Environment variables allow overriding paths when bundled (e.g., esbuild Lambda)\nconst RELEASE_NOTES_PATH =\n process.env.MCP_RELEASE_NOTES_PATH ||\n path.join(__dirname, \"..\", \"..\", \"..\", \"release-notes\");\nconst SKILLS_PATH =\n process.env.MCP_SKILLS_PATH ||\n path.join(__dirname, \"..\", \"..\", \"..\", \"skills\");\n\n// Create skill store using tildeskill\nconst skillStore = createMarkdownStore({ path: SKILLS_PATH });\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 parsed = matter(content);\n const frontMatter = parsed.data as ReleaseNoteFrontMatter;\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\nfunction formatSkillListItem(skill: SkillRecord): string {\n const { alias, description } = skill;\n if (description) {\n return `* ${alias} - ${description}`;\n }\n return `* ${alias}`;\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\n/**\n * Generate alternative spellings for plural/singular matching\n */\nfunction getAlternativeSpellings(alias: string): string[] {\n const alternatives: string[] = [];\n\n if (alias.endsWith(\"es\")) {\n // \"indexes\" -> try \"indexe\" and \"index\"\n alternatives.push(alias.slice(0, -1)); // Remove \"s\" -> \"indexe\"\n alternatives.push(alias.slice(0, -2)); // Remove \"es\" -> \"index\"\n } else if (alias.endsWith(\"s\")) {\n // \"skills\" -> try \"skill\"\n alternatives.push(alias.slice(0, -1)); // Remove \"s\" -> \"skill\"\n } else {\n // \"fish\" -> try \"fishs\" and \"fishes\"\n alternatives.push(alias + \"s\");\n alternatives.push(alias + \"es\");\n }\n\n return alternatives;\n}\n\n/**\n * Add alias to frontmatter indicating the canonical skill name\n */\nfunction addAliasToFrontmatter(content: string, matchedAlias: string): string {\n if (content.startsWith(\"---\")) {\n // Find the end of frontmatter\n const endIndex = content.indexOf(\"---\", 3);\n if (endIndex !== -1) {\n // Insert alias before the closing ---\n const beforeClose = content.slice(0, endIndex);\n const afterClose = content.slice(endIndex);\n return `${beforeClose}alias: ${matchedAlias}\\n${afterClose}`;\n }\n }\n // No frontmatter exists, create one\n return `---\\nalias: ${matchedAlias}\\n---\\n\\n${content}`;\n}\n\nexport const skillService = fabricService({\n alias: \"skill\",\n description:\n \"Access Jaypie development documentation. Pass a skill alias (e.g., 'aws', 'tests', 'errors') to get that documentation. Pass 'index' or no argument to list all available skills.\",\n input: {\n alias: {\n description:\n \"Skill alias (e.g., 'aws', 'tests'). Omit or use 'index' to list all skills.\",\n required: false,\n type: String,\n },\n },\n service: async ({ alias: inputAlias }: { alias?: string }) => {\n const alias = normalizeAlias(inputAlias || \"index\");\n\n if (!isValidAlias(alias)) {\n throw new Error(\n `Invalid skill alias \"${alias}\". Use alphanumeric characters, hyphens, and underscores only.`,\n );\n }\n\n if (alias === \"index\") {\n const allSkills = await skillStore.list();\n const skills = allSkills.filter(\n (s: { alias: string }) => s.alias !== \"index\",\n );\n const skillList = skills.map(formatSkillListItem).join(\"\\n\");\n\n return `# Index of Skills\\n\\n${skillList}`;\n }\n\n // Try exact match first\n let skill = await skillStore.get(alias);\n let matchedAlias = alias;\n\n // If no exact match, try alternative spellings (plural/singular)\n if (!skill) {\n const alternatives = getAlternativeSpellings(alias);\n for (const alt of alternatives) {\n skill = await skillStore.get(alt);\n if (skill) {\n matchedAlias = alt;\n break;\n }\n }\n }\n\n if (!skill) {\n throw new Error(\n `Skill \"${alias}\" not found. Use skill(\"index\") to list available skills.`,\n );\n }\n\n // Return raw file content for non-index skills (preserve frontmatter)\n const skillPath = path.join(SKILLS_PATH, `${matchedAlias}.md`);\n let content = await fs.readFile(skillPath, \"utf-8\");\n\n // If we matched via alternative spelling, add alias to indicate canonical name\n if (matchedAlias !== alias) {\n content = addAliasToFrontmatter(content, matchedAlias);\n }\n\n return content;\n },\n});\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\nasync function getReleaseNotesHelp(): Promise<string> {\n return fs.readFile(path.join(__dirname, \"release-notes\", \"help.md\"), \"utf-8\");\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 getReleaseNotesHelp();\n }\n\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 Error(\"package is required\");\n if (!p.version) throw new Error(\"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 Error(\n `Unknown command: ${command}. Use release_notes() for help.`,\n );\n }\n },\n});\n"],"names":["__filename","__dirname"],"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 { fabricService } from \"@jaypie/fabric\";\nimport {\n createLayeredStore,\n createMarkdownStore,\n isValidAlias,\n type LayeredStoreLayer,\n normalizeAlias,\n type SkillRecord,\n} from \"@jaypie/tildeskill\";\nimport * as fs from \"node:fs/promises\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport matter from \"gray-matter\";\nimport { gt } from \"semver\";\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\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n// From dist/suites/docs/, go up 3 levels to package root where skills/ and release-notes/ live\n// Environment variables allow overriding paths when bundled (e.g., esbuild Lambda)\nconst RELEASE_NOTES_PATH =\n process.env.MCP_RELEASE_NOTES_PATH ||\n path.join(__dirname, \"..\", \"..\", \"..\", \"release-notes\");\n\n// Bundled Jaypie skills ship inside the @jaypie/mcp package. MCP_BUILTIN_SKILLS_PATH\n// lets bundlers (esbuild, Lambda) relocate them without disabling the built-in layer.\nconst BUILTIN_SKILLS_PATH =\n process.env.MCP_BUILTIN_SKILLS_PATH ||\n path.join(__dirname, \"..\", \"..\", \"..\", \"skills\");\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[] = [];\nconst skillLayerPaths = new Map<string, string>();\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 skillLayerPaths.set(LOCAL_SKILLS_NAMESPACE, process.env.MCP_SKILLS_PATH);\n}\n\nskillLayers.push({\n namespace: JAYPIE_SKILLS_NAMESPACE,\n store: createMarkdownStore({ path: BUILTIN_SKILLS_PATH }),\n});\nskillLayerPaths.set(JAYPIE_SKILLS_NAMESPACE, BUILTIN_SKILLS_PATH);\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 parsed = matter(content);\n const frontMatter = parsed.data as ReleaseNoteFrontMatter;\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\nfunction formatSkillListItem(skill: SkillRecord): string {\n const { alias, description } = skill;\n if (description) {\n return `* ${alias} - ${description}`;\n }\n return `* ${alias}`;\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\n/**\n * Add alias to frontmatter indicating the canonical skill name\n */\nfunction addAliasToFrontmatter(content: string, matchedAlias: string): string {\n if (content.startsWith(\"---\")) {\n // Find the end of frontmatter\n const endIndex = content.indexOf(\"---\", 3);\n if (endIndex !== -1) {\n // Insert alias before the closing ---\n const beforeClose = content.slice(0, endIndex);\n const afterClose = content.slice(endIndex);\n return `${beforeClose}alias: ${matchedAlias}\\n${afterClose}`;\n }\n }\n // No frontmatter exists, create one\n return `---\\nalias: ${matchedAlias}\\n---\\n\\n${content}`;\n}\n\nexport const skillService = fabricService({\n alias: \"skill\",\n description:\n \"Access Jaypie development documentation. Pass a skill alias (e.g., 'aws', 'tests', 'errors') to get that documentation. Pass 'index' or no argument to list all available skills.\",\n input: {\n alias: {\n description:\n \"Skill alias (e.g., 'aws', 'tests'). Omit or use 'index' to list all skills.\",\n required: false,\n type: String,\n },\n },\n service: async ({ alias: inputAlias }: { alias?: string }) => {\n const alias = normalizeAlias(inputAlias || \"index\");\n\n if (!isValidAlias(alias)) {\n throw new Error(\n `Invalid skill alias \"${alias}\". Use alphanumeric characters, hyphens, and underscores only.`,\n );\n }\n\n if (alias === \"index\") {\n const allSkills = await skillStore.list();\n // Index entries from every layer hide per-layer \"index\" records.\n const skills = allSkills.filter(\n (s: { alias: string }) =>\n s.alias !== \"index\" && !s.alias.endsWith(`${LAYER_SEPARATOR}index`),\n );\n const skillList = skills.map(formatSkillListItem).join(\"\\n\");\n\n return `# Index of Skills\\n\\n${skillList}`;\n }\n\n const skill = await skillStore.find(alias);\n\n if (!skill) {\n throw new Error(\n `Skill \"${alias}\" not found. Use skill(\"index\") to list available skills.`,\n );\n }\n\n // Split the namespaced alias the layered store returned (e.g., \"local:aws\")\n // so we can read the raw markdown file from the correct layer directory.\n const separatorIdx = skill.alias.indexOf(LAYER_SEPARATOR);\n const layerNamespace =\n separatorIdx === -1 ? \"\" : skill.alias.slice(0, separatorIdx);\n const innerAlias =\n separatorIdx === -1 ? skill.alias : skill.alias.slice(separatorIdx + 1);\n const layerPath = skillLayerPaths.get(layerNamespace);\n if (!layerPath) {\n // Defensive: layered store returned a layer we don't know the path for.\n return skill.content;\n }\n\n const skillPath = path.join(layerPath, `${innerAlias}.md`);\n let content = await fs.readFile(skillPath, \"utf-8\");\n\n // Detect plural/singular fallback so we can annotate the canonical alias.\n const inputSeparatorIdx = alias.indexOf(LAYER_SEPARATOR);\n const inputInnerAlias =\n inputSeparatorIdx === -1 ? alias : alias.slice(inputSeparatorIdx + 1);\n if (innerAlias !== inputInnerAlias) {\n content = addAliasToFrontmatter(content, skill.alias);\n }\n\n return content;\n },\n});\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\nasync function getReleaseNotesHelp(): Promise<string> {\n return fs.readFile(path.join(__dirname, \"release-notes\", \"help.md\"), \"utf-8\");\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 getReleaseNotesHelp();\n }\n\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 Error(\"package is required\");\n if (!p.version) throw new Error(\"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 Error(\n `Unknown command: ${command}. Use release_notes() for help.`,\n );\n }\n },\n});\n"],"names":["__filename","__dirname"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jaypie/mcp",
3
- "version": "0.8.25",
3
+ "version": "0.8.28",
4
4
  "description": "Jaypie MCP",
5
5
  "repository": {
6
6
  "type": "git",
@@ -40,7 +40,7 @@
40
40
  },
41
41
  "dependencies": {
42
42
  "@jaypie/fabric": "^0.2.4",
43
- "@jaypie/tildeskill": "^0.2.0",
43
+ "@jaypie/tildeskill": "^0.3.0",
44
44
  "@modelcontextprotocol/sdk": "^1.17.0",
45
45
  "commander": "^14.0.0",
46
46
  "gray-matter": "^4.0.3",
@@ -0,0 +1,69 @@
1
+ ---
2
+ version: "0.5.0"
3
+ date: "2026-04-11"
4
+ summary: "Breaking: model-keyed GSIs, pk=id, updatedAt as sort key, auto-bump timestamps"
5
+ ---
6
+
7
+ # @jaypie/dynamodb 0.5.0
8
+
9
+ **Tables must be recreated.** This is a pre-1.0 breaking change that restructures the primary key, GSI schema, and timestamp management.
10
+
11
+ ## Breaking Changes
12
+
13
+ ### Primary Key
14
+
15
+ - **Old**: pk=`model`, sk=`id`
16
+ - **New**: pk=`id` only (no sort key)
17
+
18
+ Entity operations `getEntity`, `deleteEntity`, `archiveEntity`, and `destroyEntity` now take `{ id }` instead of `{ id, model }`.
19
+
20
+ ### GSI Schema
21
+
22
+ GSIs are now model-keyed with composite sort keys instead of scope-keyed with `sequence`:
23
+
24
+ | Old GSI | Old pk pattern | New GSI | New pk pattern |
25
+ |---------|---------------|---------|---------------|
26
+ | indexScope | `{scope}#{model}` | indexModel | `{model}` |
27
+ | indexAlias | `{scope}#{model}#{alias}` | indexModelAlias | `{model}#{alias}` |
28
+ | indexCategory | `{scope}#{model}#{category}` | indexModelCategory | `{model}#{category}` |
29
+ | indexType | `{scope}#{model}#{type}` | indexModelType | `{model}#{type}` |
30
+ | indexXid | `{scope}#{model}#{xid}` | indexModelXid | `{model}#{xid}` |
31
+
32
+ All GSIs now use a composite sort key: `{scope}#{updatedAt}` (stored as `{indexName}Sk`). Queries use `begins_with` on the sk composite to filter by scope.
33
+
34
+ ### Removed
35
+
36
+ - **`sequence` field** -- ordering is now by `updatedAt` in the composite sk
37
+ - **Key builder functions**: `buildIndexScope`, `buildIndexAlias`, `buildIndexCategory`, `buildIndexType`, `buildIndexXid` -- use `buildCompositeKey` instead
38
+ - **Index name constants**: `INDEX_ALIAS`, `INDEX_CATEGORY`, `INDEX_SCOPE`, `INDEX_TYPE`, `INDEX_XID`
39
+ - **Implicit `DEFAULT_INDEXES` fallback** -- models must be registered with `registerModel()` and `fabricIndex()` before querying
40
+
41
+ ### Changed Signatures
42
+
43
+ | Function | Old | New |
44
+ |----------|-----|-----|
45
+ | `getEntity` | `{ id, model }` | `{ id }` |
46
+ | `deleteEntity` | `{ id, model }` | `{ id }` |
47
+ | `archiveEntity` | `{ id, model }` | `{ id }` |
48
+ | `destroyEntity` | `{ id, model }` | `{ id }` |
49
+ | `queryByScope` | `{ model, scope }` (scope required) | `{ model, scope? }` (scope optional) |
50
+ | `queryByAlias` | `{ alias, model, scope }` (scope required) | `{ alias, model, scope? }` (scope optional) |
51
+ | `queryByCategory` | `{ model, scope, category }` | `{ model, category, scope? }` -- throws `ConfigurationError` if model hasn't registered `fabricIndex("category")` |
52
+ | `queryByType` | `{ model, scope, type }` | `{ model, type, scope? }` -- throws `ConfigurationError` if model hasn't registered `fabricIndex("type")` |
53
+
54
+ ### Timestamp Management
55
+
56
+ `indexEntity` now manages `updatedAt` and `createdAt` automatically on every write:
57
+ - `updatedAt` is bumped to `new Date().toISOString()` on every call
58
+ - `createdAt` is backfilled to the same timestamp if not already set
59
+ - Callers should no longer set these fields manually
60
+
61
+ ## Migration
62
+
63
+ 1. Register model indexes using `fabricIndex()` from `@jaypie/fabric`
64
+ 2. Recreate DynamoDB tables (primary key and GSI shapes have changed)
65
+ 3. Remove `sequence` from entity creation code
66
+ 4. Remove `model` parameter from `getEntity`, `deleteEntity`, `archiveEntity`, `destroyEntity` calls
67
+ 5. Remove manual `createdAt`/`updatedAt` assignment
68
+ 6. Replace old key builder functions with `buildCompositeKey`
69
+ 7. Optionally make `scope` omitted in query calls to query across all scopes
@@ -0,0 +1,41 @@
1
+ ---
2
+ version: "0.3.0"
3
+ date: "2026-04-11"
4
+ summary: "Breaking: fabricIndex factory, remove DEFAULT_INDEXES, drop sequence, composite sk support"
5
+ ---
6
+
7
+ # @jaypie/fabric 0.3.0
8
+
9
+ ## Breaking Changes
10
+
11
+ - **Removed `DEFAULT_INDEXES`** export and the implicit fallback in `getModelIndexes()`. Models must now be registered with `registerModel()` before indexing or querying. Calling `getModelIndexes()` on an unregistered model throws `ConfigurationError`.
12
+ - **Removed `sequence` field** from `FabricModel` and `IndexableModel`. Sort ordering is now by `updatedAt` via composite sort keys.
13
+ - **Removed `DEFAULT_SORT_KEY`** constant (was `"sequence"`).
14
+
15
+ ## New Features
16
+
17
+ - **`fabricIndex(field?)`** factory function for building canonical Jaypie GSI definitions:
18
+ - `fabricIndex()` produces `indexModel` with pk=`["model"]`, sk=`["scope", "updatedAt"]`
19
+ - `fabricIndex("alias")` produces `indexModelAlias` with pk=`["model", "alias"]`, sk=`["scope", "updatedAt"]`, sparse
20
+ - Works for any field: `fabricIndex("category")`, `fabricIndex("xid")`, `fabricIndex("customField")`
21
+ - **`getGsiAttributeNames(index)`** returns `{ pk, sk }` attribute names for an index definition. Single source of truth for GSI provisioning and query attribute references.
22
+ - **Composite sk support in `populateIndexKeys`**: when `sk.length > 1`, writes a composite sk attribute named `{indexName}Sk` (e.g., `indexModelSk = "scope#updatedAt"`). When `sk.length === 1`, the single field is used directly as the GSI sort key (no extra attribute written).
23
+
24
+ ## Migration
25
+
26
+ Replace `DEFAULT_INDEXES` usage with explicit `fabricIndex()` calls:
27
+
28
+ ```typescript
29
+ import { fabricIndex, registerModel } from "@jaypie/fabric";
30
+
31
+ registerModel({
32
+ model: "record",
33
+ indexes: [
34
+ fabricIndex(), // indexModel
35
+ fabricIndex("alias"), // indexModelAlias
36
+ fabricIndex("category"), // indexModelCategory
37
+ ],
38
+ });
39
+ ```
40
+
41
+ Remove any references to `sequence` from entity creation or update code. Ordering is now by `updatedAt`, which is managed automatically by `indexEntity`.
@@ -0,0 +1,10 @@
1
+ ---
2
+ version: 0.8.26
3
+ date: 2026-04-11
4
+ summary: Delegate skill plural/singular resolution to @jaypie/tildeskill find()
5
+ ---
6
+
7
+ ## Changes
8
+
9
+ - Skill service now calls `skillStore.find(alias)` from `@jaypie/tildeskill@0.2.1` instead of duplicating the plural/singular fallback loop
10
+ - No behavior change for callers; `skill("indexes")`, `skill("skills")`, etc. still resolve to their canonical files
@@ -0,0 +1,17 @@
1
+ ---
2
+ version: 0.8.27
3
+ date: 2026-04-11
4
+ summary: Layer MCP_SKILLS_PATH over the bundled Jaypie skill pack via createLayeredStore
5
+ ---
6
+
7
+ ## Changes
8
+
9
+ - The skill service now uses `createLayeredStore` from `@jaypie/tildeskill@0.3.0`, composing two layers:
10
+ - `local` — populated from `MCP_SKILLS_PATH` when set.
11
+ - `jaypie` — always present; sources the bundled `packages/mcp/skills/` directory.
12
+ - `skill("aws")` still resolves to the first layer that has it, so a client's local copy wins over the bundled version. Use `skill("jaypie:aws")` or `skill("local:aws")` to target a specific layer. `skill("index")` now emits namespace-prefixed entries (`local:foo`, `jaypie:bar`).
13
+ - New env var `MCP_BUILTIN_SKILLS_PATH` overrides the bundled skills directory for esbuild/Lambda bundling without disabling the layered composition.
14
+
15
+ ## Breaking behavior change
16
+
17
+ Previously `MCP_SKILLS_PATH` **replaced** the bundled skill directory. It now **augments** it as the top `local:` layer. To keep the old "relocate bundled skills" pattern working for Lambda bundlers, set `MCP_BUILTIN_SKILLS_PATH` instead.
@@ -0,0 +1,9 @@
1
+ ---
2
+ version: 1.2.29
3
+ date: 2026-04-11
4
+ summary: Mock new tildeskill exports (find, getAlternativeSpellings)
5
+ ---
6
+
7
+ ## Changes
8
+
9
+ - Add mocks for `SkillStore.find` and the newly exported `getAlternativeSpellings` from `@jaypie/tildeskill@0.2.1`
@@ -0,0 +1,10 @@
1
+ ---
2
+ version: 1.2.30
3
+ date: 2026-04-11
4
+ summary: Mock createLayeredStore and update getByNickname mock to return an array
5
+ ---
6
+
7
+ ## Changes
8
+
9
+ - Added a mock `createLayeredStore` export that returns the standard mock `SkillStore` shape.
10
+ - `SkillStore.getByNickname` mock now resolves to `[]` instead of `null` to match the `Promise<SkillRecord[]>` signature introduced in `@jaypie/tildeskill@0.3.0`.
@@ -0,0 +1,11 @@
1
+ ---
2
+ version: 0.2.1
3
+ date: 2026-04-11
4
+ summary: Add SkillStore.find with plural/singular fallback and export getAlternativeSpellings
5
+ ---
6
+
7
+ ## Changes
8
+
9
+ - Add `SkillStore.find(alias)` that tries an exact lookup then alternative plural/singular spellings, returning the matched record (check `record.alias` to detect a fallback match)
10
+ - Export `getAlternativeSpellings(alias)` core utility for callers that want the candidate list directly
11
+ - Implemented on both `createMarkdownStore` and `createMemoryStore`
@@ -0,0 +1,23 @@
1
+ ---
2
+ version: 0.3.0
3
+ date: 2026-04-11
4
+ summary: Layered skill stores with namespace prefixes; getByNickname now returns all matches
5
+ ---
6
+
7
+ ## Changes
8
+
9
+ - **New `createLayeredStore`** — compose ordered child stores with namespace prefixes (e.g., `local`, `jaypie`). `get`/`find` walk layers top-to-bottom and return the first match with its alias namespaced (`local:aws`). Namespace-qualified inputs (`jaypie:aws`) route directly to that layer. `list`/`search` aggregate every layer. `put` requires a namespace-qualified alias and delegates to the matching layer.
10
+ - **New types** `LayeredStoreLayer` and `LayeredStoreOptions` exported from the package root.
11
+ - **`SkillStore.getByNickname` now returns `Promise<SkillRecord[]>`** (breaking signature change). Returns every record whose `nicknames` list contains the query instead of only the first. A layered store aggregates matches across every layer so a nickname like `sparticus` can resolve to several skills at once.
12
+ - **`isValidAlias` accepts `:`** so callers can validate namespace-qualified inputs destined for a layered store. Path traversal guards (`..`, `/`, `\\`) are unchanged.
13
+
14
+ ## Breaking changes
15
+
16
+ - `getByNickname` return type changed from `SkillRecord | null` to `SkillRecord[]`. Update callers:
17
+
18
+ ```diff
19
+ - const skill = await store.getByNickname("amazon");
20
+ - if (skill) console.log(skill.alias);
21
+ + const matches = await store.getByNickname("amazon");
22
+ + matches.forEach((m) => console.log(m.alias));
23
+ ```
@@ -7,34 +7,24 @@ related: apikey, aws, cdk, models, vocabulary
7
7
 
8
8
  Jaypie provides `@jaypie/dynamodb` for single-table DynamoDB with entity operations, GSI-based queries, hierarchical scoping, and soft delete. Access through the main `jaypie` package or directly.
9
9
 
10
- ## Key Naming Conventions
10
+ ## Key Design
11
11
 
12
- Jaypie uses two key naming patterns. Understanding when to use each avoids confusion.
12
+ ### Primary Key: `id` Only
13
13
 
14
- ### Jaypie Default: `model` / `id`
15
-
16
- `JaypieDynamoDb` creates tables with `model` (partition key) and `id` (sort key) by default. This is the **recommended convention for new Jaypie tables** and the convention used by `@jaypie/dynamodb`:
14
+ `JaypieDynamoDb` creates tables with `id` as the sole partition key (no sort key). `model` and `scope` are regular attributes used in GSI partition keys:
17
15
 
18
16
  ```typescript
19
17
  const table = new JaypieDynamoDb(this, "myApp");
20
- // Creates table with: model (HASH), id (RANGE)
18
+ // Creates table with: id (HASH) no sort key
21
19
  ```
22
20
 
23
21
  Items look like:
24
22
 
25
23
  ```typescript
26
- { model: "user", id: "u_abc123", name: "John", email: "john@example.com" }
27
- { model: "apikey", id: "a1b2c3d4...", ownerId: "u_abc123" }
24
+ { id: "u_abc123", model: "user", name: "John", scope: "@" }
25
+ { id: "a1b2c3d4", model: "apikey", scope: "user#u_abc123" }
28
26
  ```
29
27
 
30
- ### Generic DynamoDB: `pk` / `sk`
31
-
32
- The `pk` / `sk` pattern is the standard DynamoDB convention used in broader DynamoDB literature. Jaypie uses it in local development scripts and educational examples. It's functionally equivalent — just different attribute names.
33
-
34
- ### Entity Prefixing (Value Pattern)
35
-
36
- Entity prefixes like `USER#123` are a **value-level pattern** (how you structure the data stored in keys), not an attribute naming convention. You can use entity prefixes with either `model`/`id` or `pk`/`sk` attribute names.
37
-
38
28
  ## @jaypie/dynamodb Package
39
29
 
40
30
  The runtime package provides entity operations, GSI-based queries, key builders, and client management.
@@ -99,30 +89,26 @@ All entities follow this shape:
99
89
  ```typescript
100
90
  interface StorableEntity {
101
91
  // Primary Key
102
- model: string; // Partition key (e.g., "record", "message")
103
- id: string; // Sort key (UUID)
92
+ id: string; // Partition key (UUID)
104
93
 
105
94
  // Required
106
- name: string;
95
+ model: string; // Entity model name (e.g., "record", "message")
107
96
  scope: string; // APEX ("@") or "{parent.model}#{parent.id}"
108
- sequence: number; // Date.now() for chronological ordering
109
97
 
110
- // Optional (trigger GSI index population when present)
98
+ // Optional identity
99
+ name?: string;
111
100
  alias?: string; // Human-friendly slug
112
101
  category?: string; // Category for filtering
113
102
  type?: string; // Type for filtering
114
103
  xid?: string; // External ID for cross-system lookup
115
104
 
116
- // GSI Keys (auto-populated by putEntity/updateEntity)
117
- indexAlias?: string;
118
- indexCategory?: string;
119
- indexScope?: string;
120
- indexType?: string;
121
- indexXid?: string;
105
+ // GSI Keys (auto-populated by indexEntity on every write)
106
+ // indexModel, indexModelAlias, indexModelCategory, etc.
107
+ // indexModelSk, indexModelAliasSk, etc. (composite sort keys)
122
108
 
123
- // Timestamps (ISO 8601)
124
- createdAt: string;
125
- updatedAt: string;
109
+ // Timestamps (ISO 8601) — managed by indexEntity
110
+ createdAt?: string; // Backfilled on first write
111
+ updatedAt?: string; // Bumped on every write
126
112
  archivedAt?: string;
127
113
  deletedAt?: string;
128
114
 
@@ -137,50 +123,46 @@ interface StorableEntity {
137
123
  ```typescript
138
124
  import { APEX, putEntity, getEntity, updateEntity, deleteEntity, archiveEntity, destroyEntity } from "@jaypie/dynamodb";
139
125
 
140
- const now = new Date().toISOString();
141
-
142
- // Create entity — auto-populates GSI keys
126
+ // Create entity indexEntity auto-populates GSI keys, createdAt, updatedAt
143
127
  const record = await putEntity({
144
128
  entity: {
145
129
  model: "record",
146
130
  id: crypto.randomUUID(),
147
131
  name: "Daily Log",
148
132
  scope: APEX,
149
- sequence: Date.now(),
150
133
  alias: "2026-01-07",
151
134
  category: "memory",
152
- createdAt: now,
153
- updatedAt: now,
154
135
  },
155
136
  });
156
- // indexScope: "@#record" (auto-populated)
157
- // indexAlias: "@#record#2026-01-07" (auto-populated)
158
- // indexCategory: "@#record#memory" (auto-populated)
137
+ // indexModel: "record" (auto-populated)
138
+ // indexModelAlias: "record#2026-01-07" (auto-populated)
139
+ // indexModelCategory: "record#memory" (auto-populated)
140
+ // indexModelSk: "@#2026-01-07T..." (auto-populated)
159
141
 
160
- // Get by primary key
161
- const item = await getEntity({ id: "abc-123", model: "record" });
142
+ // Get by primary key (id only)
143
+ const item = await getEntity({ id: "abc-123" });
162
144
 
163
145
  // Update — sets updatedAt, re-indexes
164
146
  await updateEntity({ entity: { ...item, name: "Updated Name" } });
165
147
 
166
- // Soft delete — sets deletedAt, re-indexes with #deleted suffix
167
- await deleteEntity({ id: "abc-123", model: "record" });
148
+ // Soft delete — sets deletedAt, re-indexes with #deleted suffix on pk
149
+ await deleteEntity({ id: "abc-123" });
168
150
 
169
- // Archive — sets archivedAt, re-indexes with #archived suffix
170
- await archiveEntity({ id: "abc-123", model: "record" });
151
+ // Archive — sets archivedAt, re-indexes with #archived suffix on pk
152
+ await archiveEntity({ id: "abc-123" });
171
153
 
172
154
  // Hard delete — permanently removes
173
- await destroyEntity({ id: "abc-123", model: "record" });
155
+ await destroyEntity({ id: "abc-123" });
174
156
  ```
175
157
 
176
158
  | Function | Description |
177
159
  |----------|-------------|
178
- | `putEntity({ entity })` | Create or replace (auto-indexes GSI keys) |
179
- | `getEntity({ id, model })` | Get by primary key |
160
+ | `putEntity({ entity })` | Create or replace (auto-indexes GSI keys, auto-timestamps) |
161
+ | `getEntity({ id })` | Get by primary key (id only) |
180
162
  | `updateEntity({ entity })` | Update (sets `updatedAt`, re-indexes) |
181
- | `deleteEntity({ id, model })` | Soft delete (`deletedAt`, `#deleted` suffix) |
182
- | `archiveEntity({ id, model })` | Archive (`archivedAt`, `#archived` suffix) |
183
- | `destroyEntity({ id, model })` | Hard delete (permanent) |
163
+ | `deleteEntity({ id })` | Soft delete (`deletedAt`, `#deleted` suffix on GSI pk) |
164
+ | `archiveEntity({ id })` | Archive (`archivedAt`, `#archived` suffix on GSI pk) |
165
+ | `destroyEntity({ id })` | Hard delete (permanent) |
184
166
 
185
167
  ### Scope and Hierarchy
186
168
 
@@ -209,31 +191,48 @@ const { items: chats } = await queryByScope({ model: "chat", scope: APEX });
209
191
 
210
192
  ### GSI Schema
211
193
 
212
- Jaypie defines five GSI patterns, but **do not create all five upfront**. Start with zero GSIs and add only what your access patterns require. The most common first GSI is `indexScope` for hierarchical queries.
194
+ GSIs are defined using `fabricIndex()` from `@jaypie/fabric`. **Do not create all GSIs upfront** start with zero and add only what your access patterns require. The most common first GSI is `indexModel` for listing entities by model.
213
195
 
214
196
  **Important:** DynamoDB allows only **one GSI to be added per deployment**. If you need multiple GSIs, add them sequentially across separate deploys. For production tables, the AWS CLI is often better suited for adding GSIs than CDK (which may try to replace the table).
215
197
 
216
- All GSIs use `sequence` (Number) as the sort key for chronological ordering.
198
+ All GSIs use a composite sort key of `scope#updatedAt` (stored as `{indexName}Sk`). Queries use `begins_with` on the sk to filter by scope; omitting scope lists across all scopes.
199
+
200
+ | GSI Name | Partition Key Pattern | Sort Key | Purpose | Add When |
201
+ |----------|----------------------|----------|---------|----------|
202
+ | `indexModel` | `{model}` | `indexModelSk` = `{scope}#{updatedAt}` | List entities by model | You need to list/query by model |
203
+ | `indexModelAlias` | `{model}#{alias}` (sparse) | `indexModelAliasSk` = `{scope}#{updatedAt}` | Human-friendly slug lookup | You need slug-based lookups |
204
+ | `indexModelCategory` | `{model}#{category}` (sparse) | `indexModelCategorySk` = `{scope}#{updatedAt}` | Category filtering | You need to filter by category |
205
+ | `indexModelType` | `{model}#{type}` (sparse) | `indexModelTypeSk` = `{scope}#{updatedAt}` | Type filtering | You need to filter by type |
206
+ | `indexModelXid` | `{model}#{xid}` (sparse) | `indexModelXidSk` = `{scope}#{updatedAt}` | External ID lookup | You need cross-system ID lookups |
217
207
 
218
- | GSI Name | Partition Key Pattern | Purpose | Add When |
219
- |----------|----------------------|---------|----------|
220
- | `indexScope` | `{scope}#{model}` | List entities by parent | You need hierarchical queries |
221
- | `indexAlias` | `{scope}#{model}#{alias}` | Human-friendly slug lookup | You need slug-based lookups |
222
- | `indexCategory` | `{scope}#{model}#{category}` | Category filtering | You need to filter by category |
223
- | `indexType` | `{scope}#{model}#{type}` | Type filtering | You need to filter by type (note: vocabulary discourages `type` in favor of `category`; `indexType` is retained as a legacy GSI pattern) |
224
- | `indexXid` | `{scope}#{model}#{xid}` | External ID lookup | You need cross-system ID lookups |
208
+ ```typescript
209
+ import { fabricIndex, registerModel } from "@jaypie/fabric";
210
+
211
+ // Register model indexes (must happen before any queries)
212
+ registerModel({
213
+ model: "record",
214
+ indexes: [
215
+ fabricIndex(), // indexModel: pk=["model"], sk=["scope","updatedAt"]
216
+ fabricIndex("alias"), // indexModelAlias: pk=["model","alias"], sparse
217
+ fabricIndex("category"), // indexModelCategory: pk=["model","category"], sparse
218
+ ],
219
+ });
220
+ ```
225
221
 
226
222
  ### Query Functions
227
223
 
228
- All queries return `{ items, lastEvaluatedKey }` and support pagination:
224
+ All queries return `{ items, lastEvaluatedKey }` and support pagination. `scope` is always optional — when omitted, queries span all scopes. `queryByCategory` and `queryByType` throw `ConfigurationError` if the model has not registered the corresponding `fabricIndex()`.
229
225
 
230
226
  ```typescript
231
227
  import { APEX, queryByScope, queryByAlias, queryByCategory, queryByType, queryByXid } from "@jaypie/dynamodb";
232
228
 
233
- // List by parent scope (most common)
229
+ // List by model (scope optional)
234
230
  const { items } = await queryByScope({ model: "record", scope: APEX });
235
231
 
236
- // Filter by category
232
+ // List across all scopes
233
+ const { items: allRecords } = await queryByScope({ model: "record" });
234
+
235
+ // Filter by category (requires fabricIndex("category") registered)
237
236
  const { items: memories } = await queryByCategory({
238
237
  model: "record", scope: APEX, category: "memory",
239
238
  });
@@ -241,7 +240,7 @@ const { items: memories } = await queryByCategory({
241
240
  // Lookup by alias (returns single or null)
242
241
  const item = await queryByAlias({ model: "record", scope: APEX, alias: "2026-01-07" });
243
242
 
244
- // Filter by type
243
+ // Filter by type (requires fabricIndex("type") registered)
245
244
  const { items: drafts } = await queryByType({
246
245
  model: "record", scope: APEX, type: "draft",
247
246
  });
@@ -306,14 +305,13 @@ const json = await exportEntitiesToJson("vocabulary", APEX);
306
305
 
307
306
  ### Key Builders
308
307
 
309
- Build composite GSI keys manually when needed:
308
+ Build composite keys manually when needed:
310
309
 
311
310
  ```typescript
312
- import { buildIndexScope, buildIndexAlias, buildIndexCategory, calculateScope } from "@jaypie/dynamodb";
311
+ import { buildCompositeKey, calculateScope } from "@jaypie/dynamodb";
313
312
 
314
- buildIndexScope(APEX, "record"); // "@#record"
315
- buildIndexAlias(APEX, "record", "daily-log"); // "@#record#daily-log"
316
- buildIndexCategory(APEX, "record", "memory"); // "@#record#memory"
313
+ buildCompositeKey({ model: "record" }, ["model"]); // "record"
314
+ buildCompositeKey({ model: "record", alias: "daily-log" }, ["model", "alias"]); // "record#daily-log"
317
315
  calculateScope({ model: "chat", id: "abc-123" }); // "chat#abc-123"
318
316
  ```
319
317
 
@@ -323,8 +321,8 @@ calculateScope({ model: "chat", id: "abc-123" }); // "chat#abc-123"
323
321
 
324
322
  | Setting | Default | Source |
325
323
  |---------|---------|--------|
326
- | Partition key | `model` (String) | Jaypie construct |
327
- | Sort key | `id` (String) | Jaypie construct |
324
+ | Partition key | `id` (String) | Jaypie construct |
325
+ | Sort key | None | Jaypie construct |
328
326
  | Billing mode | PAY_PER_REQUEST | Jaypie construct |
329
327
  | Removal policy | DESTROY (non-production), RETAIN (production) | Jaypie construct |
330
328
  | Point-in-time recovery | Enabled | Jaypie construct |
@@ -332,14 +330,16 @@ calculateScope({ model: "chat", id: "abc-123" }); // "chat#abc-123"
332
330
 
333
331
  ```typescript
334
332
  import { JaypieDynamoDb } from "@jaypie/constructs";
333
+ import { fabricIndex } from "@jaypie/fabric";
335
334
 
336
- // Recommended: start with no indexes (uses model/id keys)
335
+ // Recommended: start with no indexes
337
336
  const table = new JaypieDynamoDb(this, "myApp");
338
337
 
339
338
  // Add indexes when driven by real access patterns
340
339
  const table = new JaypieDynamoDb(this, "myApp", {
341
340
  indexes: [
342
- { pk: ["scope", "model"], sk: ["sequence"] },
341
+ fabricIndex(), // indexModel
342
+ fabricIndex("alias"), // indexModelAlias (sparse)
343
343
  ],
344
344
  });
345
345
  ```
@@ -356,7 +356,7 @@ Use docker-compose for local DynamoDB. The `@jaypie/dynamodb` MCP tool can gener
356
356
  {
357
357
  "scripts": {
358
358
  "dynamo:init": "docker compose up -d && npm run dynamo:create-table",
359
- "dynamo:create-table": "AWS_ACCESS_KEY_ID=local AWS_SECRET_ACCESS_KEY=local aws dynamodb create-table --table-name jaypie-local --attribute-definitions AttributeName=pk,AttributeType=S AttributeName=sk,AttributeType=S --key-schema AttributeName=pk,KeyType=HASH AttributeName=sk,KeyType=RANGE --billing-mode PAY_PER_REQUEST --endpoint-url http://127.0.0.1:9060 2>/dev/null || true",
359
+ "dynamo:create-table": "AWS_ACCESS_KEY_ID=local AWS_SECRET_ACCESS_KEY=local aws dynamodb create-table --table-name jaypie-local --attribute-definitions AttributeName=id,AttributeType=S --key-schema AttributeName=id,KeyType=HASH --billing-mode PAY_PER_REQUEST --endpoint-url http://127.0.0.1:9060 2>/dev/null || true",
360
360
  "dynamo:remove": "docker compose down -v",
361
361
  "dynamo:start": "docker compose up -d",
362
362
  "dynamo:stop": "docker compose down"
@@ -420,21 +420,24 @@ describe("OrderService", () => {
420
420
  });
421
421
  ```
422
422
 
423
- ## Migration: class to category (v0.4.0)
424
-
425
- Version 0.4.0 renamed `class` `category` and `indexClass` `indexCategory`.
426
-
427
- **If your table was created with an older version:**
428
-
429
- 1. **Local dev**: Delete and recreate the table using MCP `createTable`
430
- 2. **Production**: See `packages/dynamodb/CLAUDE.md` for migration script
431
-
432
- | Old | New |
433
- |-----|-----|
434
- | `class` | `category` |
435
- | `indexClass` | `indexCategory` |
436
- | `INDEX_CLASS` | `INDEX_CATEGORY` |
437
- | `queryByClass()` | `queryByCategory()` |
423
+ ## Migration: v0.4.x to v0.5.0
424
+
425
+ Version 0.5.0 is a breaking change. **Tables must be recreated** (pre-1.0 breaking change).
426
+
427
+ | Old (0.4.x) | New (0.5.0) |
428
+ |-------------|-------------|
429
+ | Primary key: pk=`model`, sk=`id` | Primary key: pk=`id` only |
430
+ | GSI sort key: `sequence` (number) | GSI sort key: composite `scope#updatedAt` |
431
+ | GSI pk: `{scope}#{model}#{field}` | GSI pk: `{model}#{field}` |
432
+ | GSI names: indexScope, indexAlias, indexCategory, indexType, indexXid | GSI names: indexModel, indexModelAlias, indexModelCategory, indexModelType, indexModelXid |
433
+ | `sequence` field on entity | Removed — ordering by `updatedAt` |
434
+ | `getEntity({ id, model })` | `getEntity({ id })` |
435
+ | `deleteEntity({ id, model })` | `deleteEntity({ id })` |
436
+ | `archiveEntity({ id, model })` | `archiveEntity({ id })` |
437
+ | `destroyEntity({ id, model })` | `destroyEntity({ id })` |
438
+ | `buildIndexScope`, `buildIndexAlias`, etc. | Removed; use `buildCompositeKey` |
439
+ | `DEFAULT_INDEXES` implicit fallback | Must `registerModel()` with `fabricIndex()` before querying |
440
+ | Callers set `createdAt`/`updatedAt` | `indexEntity` manages both automatically |
438
441
 
439
442
  ## See Also
440
443
 
@@ -10,12 +10,15 @@ Skill/vocabulary management with pluggable storage backends for AI assistants an
10
10
  ## Overview
11
11
 
12
12
  This package provides a storage abstraction for skill/vocabulary documents with markdown frontmatter support. It enables:
13
+
13
14
  - Loading skills from markdown files with YAML frontmatter
14
15
  - In-memory storage for testing
16
+ - Layered composition of multiple stores with namespace prefixes
15
17
  - Consistent alias normalization and validation
16
18
  - Filtering by namespace and tags
17
19
  - Searching across alias, name, description, content, and tags
18
20
  - Include expansion for composable skills
21
+ - Plural/singular fallback lookup via `find()` and `getAlternativeSpellings()`
19
22
 
20
23
  ## Installation
21
24
 
@@ -27,24 +30,25 @@ npm install @jaypie/tildeskill
27
30
 
28
31
  ```typescript
29
32
  interface SkillRecord {
30
- alias: string; // Lookup key (normalized lowercase)
31
- content: string; // Markdown body
32
- description?: string; // Brief description from frontmatter
33
- includes?: string[]; // Auto-expand these skill aliases on lookup
34
- name?: string; // Display title for the skill
35
- nicknames?: string[]; // Alternate lookup keys for getByNickname
36
- related?: string[]; // Related skill aliases
37
- tags?: string[]; // Categorization tags
33
+ alias: string; // Lookup key (normalized lowercase)
34
+ content: string; // Markdown body
35
+ description?: string; // Brief description from frontmatter
36
+ includes?: string[]; // Auto-expand these skill aliases on lookup
37
+ name?: string; // Display title for the skill
38
+ nicknames?: string[]; // Alternate lookup keys for getByNickname
39
+ related?: string[]; // Related skill aliases
40
+ tags?: string[]; // Categorization tags
38
41
  }
39
42
 
40
43
  interface ListFilter {
41
- namespace?: string; // Namespace prefix matching (e.g., "kit:*")
42
- tag?: string; // Filter by tag
44
+ namespace?: string; // Namespace prefix matching (e.g., "kit:*")
45
+ tag?: string; // Filter by tag
43
46
  }
44
47
 
45
48
  interface SkillStore {
49
+ find(alias: string): Promise<SkillRecord | null>;
46
50
  get(alias: string): Promise<SkillRecord | null>;
47
- getByNickname(nickname: string): Promise<SkillRecord | null>;
51
+ getByNickname(nickname: string): Promise<SkillRecord[]>;
48
52
  list(filter?: ListFilter): Promise<SkillRecord[]>;
49
53
  put(record: SkillRecord): Promise<SkillRecord>;
50
54
  search(term: string): Promise<SkillRecord[]>;
@@ -68,7 +72,7 @@ if (skill) {
68
72
 
69
73
  // List all skills
70
74
  const skills = await store.list();
71
- skills.forEach(s => console.log(`${s.alias}: ${s.description}`));
75
+ skills.forEach((s) => console.log(`${s.alias}: ${s.description}`));
72
76
  ```
73
77
 
74
78
  ### Memory Store (Testing)
@@ -77,7 +81,7 @@ skills.forEach(s => console.log(`${s.alias}: ${s.description}`));
77
81
  import { createMemoryStore } from "@jaypie/tildeskill";
78
82
 
79
83
  const store = createMemoryStore([
80
- { alias: "test", content: "# Test\n\nContent", description: "Test skill" }
84
+ { alias: "test", content: "# Test\n\nContent", description: "Test skill" },
81
85
  ]);
82
86
 
83
87
  const skill = await store.get("test");
@@ -110,25 +114,72 @@ const cloudSkills = await store.list({ tag: "cloud" });
110
114
  // Search across alias, name, description, content, and tags
111
115
  const results = await store.search("lambda");
112
116
 
113
- // Lookup by nickname
114
- const skill = await store.getByNickname("amazon");
117
+ // Lookup by nickname — returns every matching record, so a name like
118
+ // "sparticus" can resolve to multiple skills across layers.
119
+ const matches = await store.getByNickname("amazon");
120
+ ```
121
+
122
+ ## Layered Stores
123
+
124
+ ```typescript
125
+ import { createLayeredStore, createMarkdownStore } from "@jaypie/tildeskill";
126
+
127
+ // Compose multiple stores with namespace prefixes. Earlier layers win
128
+ // for single-result lookups; aggregate methods merge every layer.
129
+ const layered = createLayeredStore({
130
+ layers: [
131
+ { namespace: "local", store: createMarkdownStore({ path: "./my-skills" }) },
132
+ {
133
+ namespace: "jaypie",
134
+ store: createMarkdownStore({ path: "./jaypie-skills" }),
135
+ },
136
+ ],
137
+ });
138
+
139
+ await layered.get("aws"); // → { alias: "local:aws", ... }
140
+ await layered.get("jaypie:aws"); // → { alias: "jaypie:aws", ... }
141
+ await layered.find("skills"); // per-layer plural fallback
142
+ await layered.list(); // prefixed aliases from every layer
143
+ await layered.put({ alias: "local:new", content: "# New" }); // must be qualified
144
+ ```
145
+
146
+ The MCP server itself uses `createLayeredStore` to place `MCP_SKILLS_PATH`
147
+ (the client's local library, namespace `local`) over the bundled Jaypie
148
+ skills (namespace `jaypie`). Set `MCP_BUILTIN_SKILLS_PATH` if a bundler
149
+ needs to relocate the Jaypie base layer.
150
+
151
+ ## Plural/Singular Fallback
152
+
153
+ ```typescript
154
+ // find() tries exact match then plural/singular alternatives
155
+ const skill = await store.find("skills"); // resolves skill.md
156
+ // skill.alias is the canonical filename; compare to the input to detect fallback
157
+
158
+ import { getAlternativeSpellings } from "@jaypie/tildeskill";
159
+ getAlternativeSpellings("skills"); // ["skill"]
160
+ getAlternativeSpellings("indexes"); // ["indexe", "index"]
161
+ getAlternativeSpellings("fish"); // ["fishs", "fishes"]
115
162
  ```
116
163
 
117
164
  ## Validation Utilities
118
165
 
119
166
  ```typescript
120
- import { isValidAlias, validateAlias, normalizeAlias } from "@jaypie/tildeskill";
167
+ import {
168
+ isValidAlias,
169
+ validateAlias,
170
+ normalizeAlias,
171
+ } from "@jaypie/tildeskill";
121
172
 
122
173
  // Check validity
123
- isValidAlias("my-skill"); // true
124
- isValidAlias("../../etc"); // false (path traversal)
174
+ isValidAlias("my-skill"); // true
175
+ isValidAlias("../../etc"); // false (path traversal)
125
176
 
126
177
  // Normalize to lowercase
127
- normalizeAlias("MY-Skill"); // "my-skill"
178
+ normalizeAlias("MY-Skill"); // "my-skill"
128
179
 
129
180
  // Validate and normalize (throws on invalid)
130
- validateAlias("valid"); // returns "valid"
131
- validateAlias("../bad"); // throws BadRequestError
181
+ validateAlias("valid"); // returns "valid"
182
+ validateAlias("../bad"); // throws BadRequestError
132
183
  ```
133
184
 
134
185
  ## Skill File Format
@@ -144,7 +195,6 @@ nicknames: alt-name, another-alias
144
195
  related: alias1, alias2, alias3
145
196
  tags: category1, category2
146
197
  ---
147
-
148
198
  # Skill Title
149
199
 
150
200
  Markdown content...
@@ -49,7 +49,7 @@ Arguably composition, identity, instance, and relation would form a more complet
49
49
  - name: most common way to clearly reference the entity
50
50
  - related: array of id strings, complex "{model}#{id}" strings, or `{ id, model }` objects
51
51
  - scope: organizes entities, usually a reference to a parent entity
52
- - sequence: computed scalar, usually `Date.now()` for chronological ordering
52
+ - sequence: deprecated; ordering now uses `updatedAt` via GSI composite sort key
53
53
  - state: mutable data the entity tracks
54
54
  - status: canceled, complete, error, pending, processing, queued, sending
55
55
  - updatedAt: timestamp
@@ -66,7 +66,7 @@ Arguably composition, identity, instance, and relation would form a more complet
66
66
  - key => alias; make api or secret keys explicit in name
67
67
  - ou => scope
68
68
  - output => state
69
- - type => category, tags; reserved (exception: `indexType` GSI exists in DynamoDB as a legacy pattern; prefer `category` for new work)
69
+ - type => category, tags; reserved (exception: `indexModelType` GSI exists in DynamoDB as a legacy pattern; prefer `category` for new work)
70
70
 
71
71
  Avoid words defined elsewhere (services, terminology)
72
72