@stackwright-pro/mcp 0.2.0-alpha.17 → 0.2.0-alpha.19

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/dist/integrity.js CHANGED
@@ -32,31 +32,31 @@ var import_path = require("path");
32
32
  var _checksums = /* @__PURE__ */ new Map([
33
33
  [
34
34
  "stackwright-pro-api-otter.json",
35
- "d72f365d7eb16f6195148d09f8d42249cc1b01fab21e769d991533a31b0b588a"
35
+ "0ac26d85a5ad35b072a58965e1d5e090dd5c5f16dc14e68c452c3e99fcbb5510"
36
36
  ],
37
37
  [
38
38
  "stackwright-pro-auth-otter.json",
39
- "6610a5a4e1f981a00771494832a5dbf2e7897f993ed2cfad388528578d15955c"
39
+ "d789b71f196659d5745ebfca87a7bda60a1bb63cfeccd17b4a273ac1e29bb08d"
40
40
  ],
41
41
  [
42
42
  "stackwright-pro-dashboard-otter.json",
43
- "f0316349b38e0923c2fa5385e24c4393511c61cea410a2b080a5ece14ace4801"
43
+ "600e8597429c353e5b886f316731be84a86cd8b93617bf046e3cbf390b31a431"
44
44
  ],
45
45
  [
46
46
  "stackwright-pro-data-otter.json",
47
- "bd85c01c352630088c3f27110bcd23b6f8f7d2eb72d62a324214ac405a4c1c65"
47
+ "b2946e3da3b53282c122d150e6db86b0cb89d2edba2a94a7666b26d27051be96"
48
48
  ],
49
49
  [
50
50
  "stackwright-pro-designer-otter.json",
51
- "aadac659f170c24a3921fe0940464b5dd22db290fb2d7340647c3c4795cfe246"
51
+ "f4dbff5149051c77be1645de5ee12c0bd7d590c687a0b2d86737b915a5a6d5f0"
52
52
  ],
53
53
  [
54
54
  "stackwright-pro-foreman-otter.json",
55
- "5bb8eca6d24efa970acc24974d3789a1ce48ead42624236bb7211c1c881b2584"
55
+ "7464523d7288374dc6efa5c213c825ec0616e00cf4f739d8039eb41df812cbc5"
56
56
  ],
57
57
  [
58
58
  "stackwright-pro-page-otter.json",
59
- "c5ad7b312e1c4f83183c489291233adfb5466050d6db44b8816817ac601faca8"
59
+ "d75a71afa489478a6874abfbaa05baa46dcb2c16e4a5108f50f8187c9f67da60"
60
60
  ],
61
61
  [
62
62
  "stackwright-pro-theme-otter.json",
@@ -64,7 +64,7 @@ var _checksums = /* @__PURE__ */ new Map([
64
64
  ],
65
65
  [
66
66
  "stackwright-pro-workflow-otter.json",
67
- "2b2d132c536148bcd1fa5598d575f2a40b572c050e1cfe17a387d306b62fd855"
67
+ "16da6c109d0b5ee60d0a14e009dbeab02a7bbac3b0947795769da053565b9821"
68
68
  ]
69
69
  ]);
70
70
  Object.freeze(_checksums);
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/integrity.ts"],"sourcesContent":["/**\n * Otter Integrity Verification\n * ============================\n * Protects the Pro Otter Raft from disk-based prompt injection / jailbreak attacks.\n *\n * TypeScript port of python/src/stackwright_pro/raft/integrity.py — this lets\n * the MCP package verify otter files without a Python dependency.\n *\n * Certificate-pinned canonical checksums — hardcoded in the MCP package.\n *\n * These are NOT read from disk. An attacker who modifies otter JSON files\n * in @stackwright-pro/otters cannot also modify these constants without\n * compromising the separately-published @stackwright-pro/mcp package.\n *\n * To update: node scripts/sync-mcp-checksums.cjs\n * (reads from packages/otters/src/checksums.json, writes this file)\n */\nimport { createHash, timingSafeEqual } from 'crypto';\nimport { readFileSync, readdirSync, lstatSync } from 'fs';\nimport { join, basename } from 'path';\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\n\n// ---------------------------------------------------------------------------\n// Certificate-pinned canonical checksums — frozen Map, immutable by design.\n// DO NOT read these from disk — that would defeat the entire purpose.\n// Object.freeze prevents property mutation at runtime; ReadonlyMap prevents\n// .set() / .delete() at compile time (belt-and-suspenders).\n// ---------------------------------------------------------------------------\n\nconst _checksums = new Map<string, string>([\n [\n 'stackwright-pro-api-otter.json',\n 'd72f365d7eb16f6195148d09f8d42249cc1b01fab21e769d991533a31b0b588a',\n ],\n [\n 'stackwright-pro-auth-otter.json',\n '6610a5a4e1f981a00771494832a5dbf2e7897f993ed2cfad388528578d15955c',\n ],\n [\n 'stackwright-pro-dashboard-otter.json',\n 'f0316349b38e0923c2fa5385e24c4393511c61cea410a2b080a5ece14ace4801',\n ],\n [\n 'stackwright-pro-data-otter.json',\n 'bd85c01c352630088c3f27110bcd23b6f8f7d2eb72d62a324214ac405a4c1c65',\n ],\n [\n 'stackwright-pro-designer-otter.json',\n 'aadac659f170c24a3921fe0940464b5dd22db290fb2d7340647c3c4795cfe246',\n ],\n [\n 'stackwright-pro-foreman-otter.json',\n '5bb8eca6d24efa970acc24974d3789a1ce48ead42624236bb7211c1c881b2584',\n ],\n [\n 'stackwright-pro-page-otter.json',\n 'c5ad7b312e1c4f83183c489291233adfb5466050d6db44b8816817ac601faca8',\n ],\n [\n 'stackwright-pro-theme-otter.json',\n 'a303ec6c045420f2c916583e3f6efcda469e9610fedfc84a508ed8a8a75866bc',\n ],\n [\n 'stackwright-pro-workflow-otter.json',\n '2b2d132c536148bcd1fa5598d575f2a40b572c050e1cfe17a387d306b62fd855',\n ],\n]);\nObject.freeze(_checksums);\nconst CANONICAL_CHECKSUMS: ReadonlyMap<string, string> = _checksums;\n\n// ---------------------------------------------------------------------------\n// Import-time format validation — malformed constants are a packaging bug,\n// not a runtime surprise. Fail fast, fail loud.\n// ---------------------------------------------------------------------------\n\nconst SHA256_HEX_RE = /^[0-9a-f]{64}$/;\n\nfor (const [name, digest] of CANONICAL_CHECKSUMS) {\n if (!SHA256_HEX_RE.test(digest)) {\n throw new Error(\n `Malformed SHA-256 in CANONICAL_CHECKSUMS for \"${name}\": ` +\n `expected 64 hex chars, got ${digest.length}: \"${digest}\"`\n );\n }\n}\n\n// 1 MB — generous headroom for agent definitions; anything larger is suspicious.\nconst MAX_OTTER_BYTES = 1 * 1024 * 1024;\n\n// ---------------------------------------------------------------------------\n// Core functions (exported for direct testing — no MCP server needed)\n// ---------------------------------------------------------------------------\n\n/** Compute the hex-encoded SHA-256 digest of raw bytes. Pure, no I/O. */\nexport function computeSha256(data: Buffer): string {\n return createHash('sha256').update(data).digest('hex');\n}\n\n/** Constant-time comparison of two hex digest strings. */\nfunction safeEqual(a: string, b: string): boolean {\n if (a.length !== b.length) return false;\n return timingSafeEqual(Buffer.from(a, 'utf8'), Buffer.from(b, 'utf8'));\n}\n\n// ---------------------------------------------------------------------------\n// Single-file verification\n// ---------------------------------------------------------------------------\n\nexport interface VerifyOtterFileResult {\n verified: boolean;\n filename: string;\n error?: string;\n}\n\n/**\n * Read a single otter JSON file, check its size, compute its SHA-256,\n * and constant-time compare against the canonical checksum.\n *\n * Single read → hash → decode. No TOCTOU window.\n */\nexport function verifyOtterFile(filePath: string): VerifyOtterFileResult {\n const filename = basename(filePath);\n\n // Fast-fail on unknown filenames before any I/O\n const expected = CANONICAL_CHECKSUMS.get(filename);\n if (expected === undefined) {\n return { verified: false, filename, error: `Unknown otter file: not in canonical set` };\n }\n\n // Symlink guard — refuse to follow symlinks (prevents symlink-based swaps)\n let stat: ReturnType<typeof lstatSync>;\n try {\n stat = lstatSync(filePath);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n return { verified: false, filename, error: `Cannot stat file: ${msg}` };\n }\n\n if (stat.isSymbolicLink()) {\n return { verified: false, filename, error: 'Refusing to verify symlink' };\n }\n\n // Stat-based size pre-check — don't materialise oversized payloads\n const size = stat.size;\n\n if (size > MAX_OTTER_BYTES) {\n return {\n verified: false,\n filename,\n error: `File exceeds size limit (${MAX_OTTER_BYTES.toLocaleString()} bytes, got ${size.toLocaleString()})`,\n };\n }\n\n // Single read — used for hashing and UTF-8 validation (zero TOCTOU window)\n let raw: Buffer;\n try {\n raw = readFileSync(filePath);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n return { verified: false, filename, error: `Cannot read file: ${msg}` };\n }\n\n // Belt-and-suspenders: re-check length after read in case of a race\n if (raw.length > MAX_OTTER_BYTES) {\n return {\n verified: false,\n filename,\n error: `File exceeds size limit after read (${MAX_OTTER_BYTES.toLocaleString()} bytes, got ${raw.length.toLocaleString()})`,\n };\n }\n\n // Hash the raw bytes\n const actual = computeSha256(raw);\n\n // Constant-time comparison prevents timing-oracle attacks\n if (!safeEqual(actual, expected)) {\n return {\n verified: false,\n filename,\n error: `SHA-256 mismatch: expected ${expected.substring(0, 8)}…, got ${actual.substring(0, 8)}…`,\n };\n }\n\n // UTF-8 validation — binary injection guard\n try {\n const decoder = new TextDecoder('utf-8', { fatal: true });\n decoder.decode(raw);\n } catch {\n return {\n verified: false,\n filename,\n error: 'File is not valid UTF-8 — may be corrupted or contain binary injection',\n };\n }\n\n return { verified: true, filename };\n}\n\n// ---------------------------------------------------------------------------\n// Directory-level verification\n// ---------------------------------------------------------------------------\n\nexport interface VerifyAllOttersResult {\n verified: string[];\n failed: Array<{ filename: string; error: string }>;\n unknown: string[];\n}\n\n/**\n * Scan a directory for `*-otter.json` files, verify each one against\n * canonical checksums. Returns lists of verified, failed, and unknown files.\n */\nexport function verifyAllOtters(otterDir: string): VerifyAllOttersResult {\n const verified: string[] = [];\n const failed: Array<{ filename: string; error: string }> = [];\n const unknown: string[] = [];\n\n let entries: string[];\n try {\n entries = readdirSync(otterDir);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n return {\n verified: [],\n failed: [{ filename: '<directory>', error: `Cannot read directory: ${msg}` }],\n unknown: [],\n };\n }\n\n const otterFiles = entries.filter((f) => f.endsWith('-otter.json'));\n\n for (const filename of otterFiles) {\n const filePath = join(otterDir, filename);\n\n // Skip symlinks at the directory-scan level too\n try {\n if (lstatSync(filePath).isSymbolicLink()) {\n failed.push({ filename, error: 'Skipped: symlink' });\n continue;\n }\n } catch {\n // verifyOtterFile will handle stat errors\n }\n\n const result = verifyOtterFile(filePath);\n\n if (result.verified) {\n verified.push(result.filename);\n } else if (result.error?.startsWith('Unknown otter file')) {\n unknown.push(result.filename);\n } else {\n failed.push({ filename: result.filename, error: result.error ?? 'Unknown error' });\n }\n }\n\n // Check for missing canonical files — ones we expect but didn't find on disk\n for (const canonicalName of CANONICAL_CHECKSUMS.keys()) {\n if (!otterFiles.includes(canonicalName)) {\n failed.push({ filename: canonicalName, error: 'Missing from directory' });\n }\n }\n\n return { verified, failed, unknown };\n}\n\n// ---------------------------------------------------------------------------\n// Otter directory resolution\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_SEARCH_PATHS = ['node_modules/@stackwright-pro/otters/src/', 'packages/otters/src/'];\n\nfunction resolveOtterDir(): string | null {\n const cwd = process.cwd();\n for (const relative of DEFAULT_SEARCH_PATHS) {\n const candidate = join(cwd, relative);\n try {\n lstatSync(candidate);\n return candidate;\n } catch {\n // Not found, try next\n }\n }\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// MCP tool registration\n// ---------------------------------------------------------------------------\n\nexport function registerIntegrityTools(server: McpServer): void {\n server.tool(\n 'stackwright_pro_verify_otter_integrity',\n 'Verify SHA-256 integrity of all Pro otter agent definitions. Call this at startup before discovering otters. Auto-discovers the otter directory from known paths. Returns verified/failed/unknown lists.',\n {},\n async () => {\n const resolved = resolveOtterDir();\n\n if (!resolved) {\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({\n error: true,\n message:\n 'Could not locate otter directory. Searched: ' + DEFAULT_SEARCH_PATHS.join(', '),\n }),\n },\n ],\n isError: true,\n };\n }\n\n const result = verifyAllOtters(resolved);\n\n const allGood = result.failed.length === 0 && result.unknown.length === 0;\n\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({\n otterDir: resolved,\n totalCanonical: CANONICAL_CHECKSUMS.size,\n verifiedCount: result.verified.length,\n failedCount: result.failed.length,\n unknownCount: result.unknown.length,\n verified: result.verified,\n failed: result.failed,\n unknown: result.unknown,\n warning:\n result.failed.length > 0\n ? 'SHA-256 mismatches detected (non-blocking). PKI-signed manifest support coming soon.'\n : undefined,\n }),\n },\n ],\n isError: false,\n };\n }\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/integrity.ts"],"sourcesContent":["/**\n * Otter Integrity Verification\n * ============================\n * Protects the Pro Otter Raft from disk-based prompt injection / jailbreak attacks.\n *\n * TypeScript port of python/src/stackwright_pro/raft/integrity.py — this lets\n * the MCP package verify otter files without a Python dependency.\n *\n * Certificate-pinned canonical checksums — hardcoded in the MCP package.\n *\n * These are NOT read from disk. An attacker who modifies otter JSON files\n * in @stackwright-pro/otters cannot also modify these constants without\n * compromising the separately-published @stackwright-pro/mcp package.\n *\n * To update: node scripts/sync-mcp-checksums.cjs\n * (reads from packages/otters/src/checksums.json, writes this file)\n */\nimport { createHash, timingSafeEqual } from 'crypto';\nimport { readFileSync, readdirSync, lstatSync } from 'fs';\nimport { join, basename } from 'path';\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\n\n// ---------------------------------------------------------------------------\n// Certificate-pinned canonical checksums — frozen Map, immutable by design.\n// DO NOT read these from disk — that would defeat the entire purpose.\n// Object.freeze prevents property mutation at runtime; ReadonlyMap prevents\n// .set() / .delete() at compile time (belt-and-suspenders).\n// ---------------------------------------------------------------------------\n\nconst _checksums = new Map<string, string>([\n [\n 'stackwright-pro-api-otter.json',\n '0ac26d85a5ad35b072a58965e1d5e090dd5c5f16dc14e68c452c3e99fcbb5510',\n ],\n [\n 'stackwright-pro-auth-otter.json',\n 'd789b71f196659d5745ebfca87a7bda60a1bb63cfeccd17b4a273ac1e29bb08d',\n ],\n [\n 'stackwright-pro-dashboard-otter.json',\n '600e8597429c353e5b886f316731be84a86cd8b93617bf046e3cbf390b31a431',\n ],\n [\n 'stackwright-pro-data-otter.json',\n 'b2946e3da3b53282c122d150e6db86b0cb89d2edba2a94a7666b26d27051be96',\n ],\n [\n 'stackwright-pro-designer-otter.json',\n 'f4dbff5149051c77be1645de5ee12c0bd7d590c687a0b2d86737b915a5a6d5f0',\n ],\n [\n 'stackwright-pro-foreman-otter.json',\n '7464523d7288374dc6efa5c213c825ec0616e00cf4f739d8039eb41df812cbc5',\n ],\n [\n 'stackwright-pro-page-otter.json',\n 'd75a71afa489478a6874abfbaa05baa46dcb2c16e4a5108f50f8187c9f67da60',\n ],\n [\n 'stackwright-pro-theme-otter.json',\n 'a303ec6c045420f2c916583e3f6efcda469e9610fedfc84a508ed8a8a75866bc',\n ],\n [\n 'stackwright-pro-workflow-otter.json',\n '16da6c109d0b5ee60d0a14e009dbeab02a7bbac3b0947795769da053565b9821',\n ],\n]);\nObject.freeze(_checksums);\nconst CANONICAL_CHECKSUMS: ReadonlyMap<string, string> = _checksums;\n\n// ---------------------------------------------------------------------------\n// Import-time format validation — malformed constants are a packaging bug,\n// not a runtime surprise. Fail fast, fail loud.\n// ---------------------------------------------------------------------------\n\nconst SHA256_HEX_RE = /^[0-9a-f]{64}$/;\n\nfor (const [name, digest] of CANONICAL_CHECKSUMS) {\n if (!SHA256_HEX_RE.test(digest)) {\n throw new Error(\n `Malformed SHA-256 in CANONICAL_CHECKSUMS for \"${name}\": ` +\n `expected 64 hex chars, got ${digest.length}: \"${digest}\"`\n );\n }\n}\n\n// 1 MB — generous headroom for agent definitions; anything larger is suspicious.\nconst MAX_OTTER_BYTES = 1 * 1024 * 1024;\n\n// ---------------------------------------------------------------------------\n// Core functions (exported for direct testing — no MCP server needed)\n// ---------------------------------------------------------------------------\n\n/** Compute the hex-encoded SHA-256 digest of raw bytes. Pure, no I/O. */\nexport function computeSha256(data: Buffer): string {\n return createHash('sha256').update(data).digest('hex');\n}\n\n/** Constant-time comparison of two hex digest strings. */\nfunction safeEqual(a: string, b: string): boolean {\n if (a.length !== b.length) return false;\n return timingSafeEqual(Buffer.from(a, 'utf8'), Buffer.from(b, 'utf8'));\n}\n\n// ---------------------------------------------------------------------------\n// Single-file verification\n// ---------------------------------------------------------------------------\n\nexport interface VerifyOtterFileResult {\n verified: boolean;\n filename: string;\n error?: string;\n}\n\n/**\n * Read a single otter JSON file, check its size, compute its SHA-256,\n * and constant-time compare against the canonical checksum.\n *\n * Single read → hash → decode. No TOCTOU window.\n */\nexport function verifyOtterFile(filePath: string): VerifyOtterFileResult {\n const filename = basename(filePath);\n\n // Fast-fail on unknown filenames before any I/O\n const expected = CANONICAL_CHECKSUMS.get(filename);\n if (expected === undefined) {\n return { verified: false, filename, error: `Unknown otter file: not in canonical set` };\n }\n\n // Symlink guard — refuse to follow symlinks (prevents symlink-based swaps)\n let stat: ReturnType<typeof lstatSync>;\n try {\n stat = lstatSync(filePath);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n return { verified: false, filename, error: `Cannot stat file: ${msg}` };\n }\n\n if (stat.isSymbolicLink()) {\n return { verified: false, filename, error: 'Refusing to verify symlink' };\n }\n\n // Stat-based size pre-check — don't materialise oversized payloads\n const size = stat.size;\n\n if (size > MAX_OTTER_BYTES) {\n return {\n verified: false,\n filename,\n error: `File exceeds size limit (${MAX_OTTER_BYTES.toLocaleString()} bytes, got ${size.toLocaleString()})`,\n };\n }\n\n // Single read — used for hashing and UTF-8 validation (zero TOCTOU window)\n let raw: Buffer;\n try {\n raw = readFileSync(filePath);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n return { verified: false, filename, error: `Cannot read file: ${msg}` };\n }\n\n // Belt-and-suspenders: re-check length after read in case of a race\n if (raw.length > MAX_OTTER_BYTES) {\n return {\n verified: false,\n filename,\n error: `File exceeds size limit after read (${MAX_OTTER_BYTES.toLocaleString()} bytes, got ${raw.length.toLocaleString()})`,\n };\n }\n\n // Hash the raw bytes\n const actual = computeSha256(raw);\n\n // Constant-time comparison prevents timing-oracle attacks\n if (!safeEqual(actual, expected)) {\n return {\n verified: false,\n filename,\n error: `SHA-256 mismatch: expected ${expected.substring(0, 8)}…, got ${actual.substring(0, 8)}…`,\n };\n }\n\n // UTF-8 validation — binary injection guard\n try {\n const decoder = new TextDecoder('utf-8', { fatal: true });\n decoder.decode(raw);\n } catch {\n return {\n verified: false,\n filename,\n error: 'File is not valid UTF-8 — may be corrupted or contain binary injection',\n };\n }\n\n return { verified: true, filename };\n}\n\n// ---------------------------------------------------------------------------\n// Directory-level verification\n// ---------------------------------------------------------------------------\n\nexport interface VerifyAllOttersResult {\n verified: string[];\n failed: Array<{ filename: string; error: string }>;\n unknown: string[];\n}\n\n/**\n * Scan a directory for `*-otter.json` files, verify each one against\n * canonical checksums. Returns lists of verified, failed, and unknown files.\n */\nexport function verifyAllOtters(otterDir: string): VerifyAllOttersResult {\n const verified: string[] = [];\n const failed: Array<{ filename: string; error: string }> = [];\n const unknown: string[] = [];\n\n let entries: string[];\n try {\n entries = readdirSync(otterDir);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n return {\n verified: [],\n failed: [{ filename: '<directory>', error: `Cannot read directory: ${msg}` }],\n unknown: [],\n };\n }\n\n const otterFiles = entries.filter((f) => f.endsWith('-otter.json'));\n\n for (const filename of otterFiles) {\n const filePath = join(otterDir, filename);\n\n // Skip symlinks at the directory-scan level too\n try {\n if (lstatSync(filePath).isSymbolicLink()) {\n failed.push({ filename, error: 'Skipped: symlink' });\n continue;\n }\n } catch {\n // verifyOtterFile will handle stat errors\n }\n\n const result = verifyOtterFile(filePath);\n\n if (result.verified) {\n verified.push(result.filename);\n } else if (result.error?.startsWith('Unknown otter file')) {\n unknown.push(result.filename);\n } else {\n failed.push({ filename: result.filename, error: result.error ?? 'Unknown error' });\n }\n }\n\n // Check for missing canonical files — ones we expect but didn't find on disk\n for (const canonicalName of CANONICAL_CHECKSUMS.keys()) {\n if (!otterFiles.includes(canonicalName)) {\n failed.push({ filename: canonicalName, error: 'Missing from directory' });\n }\n }\n\n return { verified, failed, unknown };\n}\n\n// ---------------------------------------------------------------------------\n// Otter directory resolution\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_SEARCH_PATHS = ['node_modules/@stackwright-pro/otters/src/', 'packages/otters/src/'];\n\nfunction resolveOtterDir(): string | null {\n const cwd = process.cwd();\n for (const relative of DEFAULT_SEARCH_PATHS) {\n const candidate = join(cwd, relative);\n try {\n lstatSync(candidate);\n return candidate;\n } catch {\n // Not found, try next\n }\n }\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// MCP tool registration\n// ---------------------------------------------------------------------------\n\nexport function registerIntegrityTools(server: McpServer): void {\n server.tool(\n 'stackwright_pro_verify_otter_integrity',\n 'Verify SHA-256 integrity of all Pro otter agent definitions. Call this at startup before discovering otters. Auto-discovers the otter directory from known paths. Returns verified/failed/unknown lists.',\n {},\n async () => {\n const resolved = resolveOtterDir();\n\n if (!resolved) {\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({\n error: true,\n message:\n 'Could not locate otter directory. Searched: ' + DEFAULT_SEARCH_PATHS.join(', '),\n }),\n },\n ],\n isError: true,\n };\n }\n\n const result = verifyAllOtters(resolved);\n\n const allGood = result.failed.length === 0 && result.unknown.length === 0;\n\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({\n otterDir: resolved,\n totalCanonical: CANONICAL_CHECKSUMS.size,\n verifiedCount: result.verified.length,\n failedCount: result.failed.length,\n unknownCount: result.unknown.length,\n verified: result.verified,\n failed: result.failed,\n unknown: result.unknown,\n warning:\n result.failed.length > 0\n ? 'SHA-256 mismatches detected (non-blocking). PKI-signed manifest support coming soon.'\n : undefined,\n }),\n },\n ],\n isError: false,\n };\n }\n );\n}\n"],"mappings":"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","names":[]}
@@ -5,31 +5,31 @@ import { join, basename } from "path";
5
5
  var _checksums = /* @__PURE__ */ new Map([
6
6
  [
7
7
  "stackwright-pro-api-otter.json",
8
- "d72f365d7eb16f6195148d09f8d42249cc1b01fab21e769d991533a31b0b588a"
8
+ "0ac26d85a5ad35b072a58965e1d5e090dd5c5f16dc14e68c452c3e99fcbb5510"
9
9
  ],
10
10
  [
11
11
  "stackwright-pro-auth-otter.json",
12
- "6610a5a4e1f981a00771494832a5dbf2e7897f993ed2cfad388528578d15955c"
12
+ "d789b71f196659d5745ebfca87a7bda60a1bb63cfeccd17b4a273ac1e29bb08d"
13
13
  ],
14
14
  [
15
15
  "stackwright-pro-dashboard-otter.json",
16
- "f0316349b38e0923c2fa5385e24c4393511c61cea410a2b080a5ece14ace4801"
16
+ "600e8597429c353e5b886f316731be84a86cd8b93617bf046e3cbf390b31a431"
17
17
  ],
18
18
  [
19
19
  "stackwright-pro-data-otter.json",
20
- "bd85c01c352630088c3f27110bcd23b6f8f7d2eb72d62a324214ac405a4c1c65"
20
+ "b2946e3da3b53282c122d150e6db86b0cb89d2edba2a94a7666b26d27051be96"
21
21
  ],
22
22
  [
23
23
  "stackwright-pro-designer-otter.json",
24
- "aadac659f170c24a3921fe0940464b5dd22db290fb2d7340647c3c4795cfe246"
24
+ "f4dbff5149051c77be1645de5ee12c0bd7d590c687a0b2d86737b915a5a6d5f0"
25
25
  ],
26
26
  [
27
27
  "stackwright-pro-foreman-otter.json",
28
- "5bb8eca6d24efa970acc24974d3789a1ce48ead42624236bb7211c1c881b2584"
28
+ "7464523d7288374dc6efa5c213c825ec0616e00cf4f739d8039eb41df812cbc5"
29
29
  ],
30
30
  [
31
31
  "stackwright-pro-page-otter.json",
32
- "c5ad7b312e1c4f83183c489291233adfb5466050d6db44b8816817ac601faca8"
32
+ "d75a71afa489478a6874abfbaa05baa46dcb2c16e4a5108f50f8187c9f67da60"
33
33
  ],
34
34
  [
35
35
  "stackwright-pro-theme-otter.json",
@@ -37,7 +37,7 @@ var _checksums = /* @__PURE__ */ new Map([
37
37
  ],
38
38
  [
39
39
  "stackwright-pro-workflow-otter.json",
40
- "2b2d132c536148bcd1fa5598d575f2a40b572c050e1cfe17a387d306b62fd855"
40
+ "16da6c109d0b5ee60d0a14e009dbeab02a7bbac3b0947795769da053565b9821"
41
41
  ]
42
42
  ]);
43
43
  Object.freeze(_checksums);
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/integrity.ts"],"sourcesContent":["/**\n * Otter Integrity Verification\n * ============================\n * Protects the Pro Otter Raft from disk-based prompt injection / jailbreak attacks.\n *\n * TypeScript port of python/src/stackwright_pro/raft/integrity.py — this lets\n * the MCP package verify otter files without a Python dependency.\n *\n * Certificate-pinned canonical checksums — hardcoded in the MCP package.\n *\n * These are NOT read from disk. An attacker who modifies otter JSON files\n * in @stackwright-pro/otters cannot also modify these constants without\n * compromising the separately-published @stackwright-pro/mcp package.\n *\n * To update: node scripts/sync-mcp-checksums.cjs\n * (reads from packages/otters/src/checksums.json, writes this file)\n */\nimport { createHash, timingSafeEqual } from 'crypto';\nimport { readFileSync, readdirSync, lstatSync } from 'fs';\nimport { join, basename } from 'path';\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\n\n// ---------------------------------------------------------------------------\n// Certificate-pinned canonical checksums — frozen Map, immutable by design.\n// DO NOT read these from disk — that would defeat the entire purpose.\n// Object.freeze prevents property mutation at runtime; ReadonlyMap prevents\n// .set() / .delete() at compile time (belt-and-suspenders).\n// ---------------------------------------------------------------------------\n\nconst _checksums = new Map<string, string>([\n [\n 'stackwright-pro-api-otter.json',\n 'd72f365d7eb16f6195148d09f8d42249cc1b01fab21e769d991533a31b0b588a',\n ],\n [\n 'stackwright-pro-auth-otter.json',\n '6610a5a4e1f981a00771494832a5dbf2e7897f993ed2cfad388528578d15955c',\n ],\n [\n 'stackwright-pro-dashboard-otter.json',\n 'f0316349b38e0923c2fa5385e24c4393511c61cea410a2b080a5ece14ace4801',\n ],\n [\n 'stackwright-pro-data-otter.json',\n 'bd85c01c352630088c3f27110bcd23b6f8f7d2eb72d62a324214ac405a4c1c65',\n ],\n [\n 'stackwright-pro-designer-otter.json',\n 'aadac659f170c24a3921fe0940464b5dd22db290fb2d7340647c3c4795cfe246',\n ],\n [\n 'stackwright-pro-foreman-otter.json',\n '5bb8eca6d24efa970acc24974d3789a1ce48ead42624236bb7211c1c881b2584',\n ],\n [\n 'stackwright-pro-page-otter.json',\n 'c5ad7b312e1c4f83183c489291233adfb5466050d6db44b8816817ac601faca8',\n ],\n [\n 'stackwright-pro-theme-otter.json',\n 'a303ec6c045420f2c916583e3f6efcda469e9610fedfc84a508ed8a8a75866bc',\n ],\n [\n 'stackwright-pro-workflow-otter.json',\n '2b2d132c536148bcd1fa5598d575f2a40b572c050e1cfe17a387d306b62fd855',\n ],\n]);\nObject.freeze(_checksums);\nconst CANONICAL_CHECKSUMS: ReadonlyMap<string, string> = _checksums;\n\n// ---------------------------------------------------------------------------\n// Import-time format validation — malformed constants are a packaging bug,\n// not a runtime surprise. Fail fast, fail loud.\n// ---------------------------------------------------------------------------\n\nconst SHA256_HEX_RE = /^[0-9a-f]{64}$/;\n\nfor (const [name, digest] of CANONICAL_CHECKSUMS) {\n if (!SHA256_HEX_RE.test(digest)) {\n throw new Error(\n `Malformed SHA-256 in CANONICAL_CHECKSUMS for \"${name}\": ` +\n `expected 64 hex chars, got ${digest.length}: \"${digest}\"`\n );\n }\n}\n\n// 1 MB — generous headroom for agent definitions; anything larger is suspicious.\nconst MAX_OTTER_BYTES = 1 * 1024 * 1024;\n\n// ---------------------------------------------------------------------------\n// Core functions (exported for direct testing — no MCP server needed)\n// ---------------------------------------------------------------------------\n\n/** Compute the hex-encoded SHA-256 digest of raw bytes. Pure, no I/O. */\nexport function computeSha256(data: Buffer): string {\n return createHash('sha256').update(data).digest('hex');\n}\n\n/** Constant-time comparison of two hex digest strings. */\nfunction safeEqual(a: string, b: string): boolean {\n if (a.length !== b.length) return false;\n return timingSafeEqual(Buffer.from(a, 'utf8'), Buffer.from(b, 'utf8'));\n}\n\n// ---------------------------------------------------------------------------\n// Single-file verification\n// ---------------------------------------------------------------------------\n\nexport interface VerifyOtterFileResult {\n verified: boolean;\n filename: string;\n error?: string;\n}\n\n/**\n * Read a single otter JSON file, check its size, compute its SHA-256,\n * and constant-time compare against the canonical checksum.\n *\n * Single read → hash → decode. No TOCTOU window.\n */\nexport function verifyOtterFile(filePath: string): VerifyOtterFileResult {\n const filename = basename(filePath);\n\n // Fast-fail on unknown filenames before any I/O\n const expected = CANONICAL_CHECKSUMS.get(filename);\n if (expected === undefined) {\n return { verified: false, filename, error: `Unknown otter file: not in canonical set` };\n }\n\n // Symlink guard — refuse to follow symlinks (prevents symlink-based swaps)\n let stat: ReturnType<typeof lstatSync>;\n try {\n stat = lstatSync(filePath);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n return { verified: false, filename, error: `Cannot stat file: ${msg}` };\n }\n\n if (stat.isSymbolicLink()) {\n return { verified: false, filename, error: 'Refusing to verify symlink' };\n }\n\n // Stat-based size pre-check — don't materialise oversized payloads\n const size = stat.size;\n\n if (size > MAX_OTTER_BYTES) {\n return {\n verified: false,\n filename,\n error: `File exceeds size limit (${MAX_OTTER_BYTES.toLocaleString()} bytes, got ${size.toLocaleString()})`,\n };\n }\n\n // Single read — used for hashing and UTF-8 validation (zero TOCTOU window)\n let raw: Buffer;\n try {\n raw = readFileSync(filePath);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n return { verified: false, filename, error: `Cannot read file: ${msg}` };\n }\n\n // Belt-and-suspenders: re-check length after read in case of a race\n if (raw.length > MAX_OTTER_BYTES) {\n return {\n verified: false,\n filename,\n error: `File exceeds size limit after read (${MAX_OTTER_BYTES.toLocaleString()} bytes, got ${raw.length.toLocaleString()})`,\n };\n }\n\n // Hash the raw bytes\n const actual = computeSha256(raw);\n\n // Constant-time comparison prevents timing-oracle attacks\n if (!safeEqual(actual, expected)) {\n return {\n verified: false,\n filename,\n error: `SHA-256 mismatch: expected ${expected.substring(0, 8)}…, got ${actual.substring(0, 8)}…`,\n };\n }\n\n // UTF-8 validation — binary injection guard\n try {\n const decoder = new TextDecoder('utf-8', { fatal: true });\n decoder.decode(raw);\n } catch {\n return {\n verified: false,\n filename,\n error: 'File is not valid UTF-8 — may be corrupted or contain binary injection',\n };\n }\n\n return { verified: true, filename };\n}\n\n// ---------------------------------------------------------------------------\n// Directory-level verification\n// ---------------------------------------------------------------------------\n\nexport interface VerifyAllOttersResult {\n verified: string[];\n failed: Array<{ filename: string; error: string }>;\n unknown: string[];\n}\n\n/**\n * Scan a directory for `*-otter.json` files, verify each one against\n * canonical checksums. Returns lists of verified, failed, and unknown files.\n */\nexport function verifyAllOtters(otterDir: string): VerifyAllOttersResult {\n const verified: string[] = [];\n const failed: Array<{ filename: string; error: string }> = [];\n const unknown: string[] = [];\n\n let entries: string[];\n try {\n entries = readdirSync(otterDir);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n return {\n verified: [],\n failed: [{ filename: '<directory>', error: `Cannot read directory: ${msg}` }],\n unknown: [],\n };\n }\n\n const otterFiles = entries.filter((f) => f.endsWith('-otter.json'));\n\n for (const filename of otterFiles) {\n const filePath = join(otterDir, filename);\n\n // Skip symlinks at the directory-scan level too\n try {\n if (lstatSync(filePath).isSymbolicLink()) {\n failed.push({ filename, error: 'Skipped: symlink' });\n continue;\n }\n } catch {\n // verifyOtterFile will handle stat errors\n }\n\n const result = verifyOtterFile(filePath);\n\n if (result.verified) {\n verified.push(result.filename);\n } else if (result.error?.startsWith('Unknown otter file')) {\n unknown.push(result.filename);\n } else {\n failed.push({ filename: result.filename, error: result.error ?? 'Unknown error' });\n }\n }\n\n // Check for missing canonical files — ones we expect but didn't find on disk\n for (const canonicalName of CANONICAL_CHECKSUMS.keys()) {\n if (!otterFiles.includes(canonicalName)) {\n failed.push({ filename: canonicalName, error: 'Missing from directory' });\n }\n }\n\n return { verified, failed, unknown };\n}\n\n// ---------------------------------------------------------------------------\n// Otter directory resolution\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_SEARCH_PATHS = ['node_modules/@stackwright-pro/otters/src/', 'packages/otters/src/'];\n\nfunction resolveOtterDir(): string | null {\n const cwd = process.cwd();\n for (const relative of DEFAULT_SEARCH_PATHS) {\n const candidate = join(cwd, relative);\n try {\n lstatSync(candidate);\n return candidate;\n } catch {\n // Not found, try next\n }\n }\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// MCP tool registration\n// ---------------------------------------------------------------------------\n\nexport function registerIntegrityTools(server: McpServer): void {\n server.tool(\n 'stackwright_pro_verify_otter_integrity',\n 'Verify SHA-256 integrity of all Pro otter agent definitions. Call this at startup before discovering otters. Auto-discovers the otter directory from known paths. Returns verified/failed/unknown lists.',\n {},\n async () => {\n const resolved = resolveOtterDir();\n\n if (!resolved) {\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({\n error: true,\n message:\n 'Could not locate otter directory. Searched: ' + DEFAULT_SEARCH_PATHS.join(', '),\n }),\n },\n ],\n isError: true,\n };\n }\n\n const result = verifyAllOtters(resolved);\n\n const allGood = result.failed.length === 0 && result.unknown.length === 0;\n\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({\n otterDir: resolved,\n totalCanonical: CANONICAL_CHECKSUMS.size,\n verifiedCount: result.verified.length,\n failedCount: result.failed.length,\n unknownCount: result.unknown.length,\n verified: result.verified,\n failed: result.failed,\n unknown: result.unknown,\n warning:\n result.failed.length > 0\n ? 'SHA-256 mismatches detected (non-blocking). PKI-signed manifest support coming soon.'\n : undefined,\n }),\n },\n ],\n isError: false,\n };\n }\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/integrity.ts"],"sourcesContent":["/**\n * Otter Integrity Verification\n * ============================\n * Protects the Pro Otter Raft from disk-based prompt injection / jailbreak attacks.\n *\n * TypeScript port of python/src/stackwright_pro/raft/integrity.py — this lets\n * the MCP package verify otter files without a Python dependency.\n *\n * Certificate-pinned canonical checksums — hardcoded in the MCP package.\n *\n * These are NOT read from disk. An attacker who modifies otter JSON files\n * in @stackwright-pro/otters cannot also modify these constants without\n * compromising the separately-published @stackwright-pro/mcp package.\n *\n * To update: node scripts/sync-mcp-checksums.cjs\n * (reads from packages/otters/src/checksums.json, writes this file)\n */\nimport { createHash, timingSafeEqual } from 'crypto';\nimport { readFileSync, readdirSync, lstatSync } from 'fs';\nimport { join, basename } from 'path';\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\n\n// ---------------------------------------------------------------------------\n// Certificate-pinned canonical checksums — frozen Map, immutable by design.\n// DO NOT read these from disk — that would defeat the entire purpose.\n// Object.freeze prevents property mutation at runtime; ReadonlyMap prevents\n// .set() / .delete() at compile time (belt-and-suspenders).\n// ---------------------------------------------------------------------------\n\nconst _checksums = new Map<string, string>([\n [\n 'stackwright-pro-api-otter.json',\n '0ac26d85a5ad35b072a58965e1d5e090dd5c5f16dc14e68c452c3e99fcbb5510',\n ],\n [\n 'stackwright-pro-auth-otter.json',\n 'd789b71f196659d5745ebfca87a7bda60a1bb63cfeccd17b4a273ac1e29bb08d',\n ],\n [\n 'stackwright-pro-dashboard-otter.json',\n '600e8597429c353e5b886f316731be84a86cd8b93617bf046e3cbf390b31a431',\n ],\n [\n 'stackwright-pro-data-otter.json',\n 'b2946e3da3b53282c122d150e6db86b0cb89d2edba2a94a7666b26d27051be96',\n ],\n [\n 'stackwright-pro-designer-otter.json',\n 'f4dbff5149051c77be1645de5ee12c0bd7d590c687a0b2d86737b915a5a6d5f0',\n ],\n [\n 'stackwright-pro-foreman-otter.json',\n '7464523d7288374dc6efa5c213c825ec0616e00cf4f739d8039eb41df812cbc5',\n ],\n [\n 'stackwright-pro-page-otter.json',\n 'd75a71afa489478a6874abfbaa05baa46dcb2c16e4a5108f50f8187c9f67da60',\n ],\n [\n 'stackwright-pro-theme-otter.json',\n 'a303ec6c045420f2c916583e3f6efcda469e9610fedfc84a508ed8a8a75866bc',\n ],\n [\n 'stackwright-pro-workflow-otter.json',\n '16da6c109d0b5ee60d0a14e009dbeab02a7bbac3b0947795769da053565b9821',\n ],\n]);\nObject.freeze(_checksums);\nconst CANONICAL_CHECKSUMS: ReadonlyMap<string, string> = _checksums;\n\n// ---------------------------------------------------------------------------\n// Import-time format validation — malformed constants are a packaging bug,\n// not a runtime surprise. Fail fast, fail loud.\n// ---------------------------------------------------------------------------\n\nconst SHA256_HEX_RE = /^[0-9a-f]{64}$/;\n\nfor (const [name, digest] of CANONICAL_CHECKSUMS) {\n if (!SHA256_HEX_RE.test(digest)) {\n throw new Error(\n `Malformed SHA-256 in CANONICAL_CHECKSUMS for \"${name}\": ` +\n `expected 64 hex chars, got ${digest.length}: \"${digest}\"`\n );\n }\n}\n\n// 1 MB — generous headroom for agent definitions; anything larger is suspicious.\nconst MAX_OTTER_BYTES = 1 * 1024 * 1024;\n\n// ---------------------------------------------------------------------------\n// Core functions (exported for direct testing — no MCP server needed)\n// ---------------------------------------------------------------------------\n\n/** Compute the hex-encoded SHA-256 digest of raw bytes. Pure, no I/O. */\nexport function computeSha256(data: Buffer): string {\n return createHash('sha256').update(data).digest('hex');\n}\n\n/** Constant-time comparison of two hex digest strings. */\nfunction safeEqual(a: string, b: string): boolean {\n if (a.length !== b.length) return false;\n return timingSafeEqual(Buffer.from(a, 'utf8'), Buffer.from(b, 'utf8'));\n}\n\n// ---------------------------------------------------------------------------\n// Single-file verification\n// ---------------------------------------------------------------------------\n\nexport interface VerifyOtterFileResult {\n verified: boolean;\n filename: string;\n error?: string;\n}\n\n/**\n * Read a single otter JSON file, check its size, compute its SHA-256,\n * and constant-time compare against the canonical checksum.\n *\n * Single read → hash → decode. No TOCTOU window.\n */\nexport function verifyOtterFile(filePath: string): VerifyOtterFileResult {\n const filename = basename(filePath);\n\n // Fast-fail on unknown filenames before any I/O\n const expected = CANONICAL_CHECKSUMS.get(filename);\n if (expected === undefined) {\n return { verified: false, filename, error: `Unknown otter file: not in canonical set` };\n }\n\n // Symlink guard — refuse to follow symlinks (prevents symlink-based swaps)\n let stat: ReturnType<typeof lstatSync>;\n try {\n stat = lstatSync(filePath);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n return { verified: false, filename, error: `Cannot stat file: ${msg}` };\n }\n\n if (stat.isSymbolicLink()) {\n return { verified: false, filename, error: 'Refusing to verify symlink' };\n }\n\n // Stat-based size pre-check — don't materialise oversized payloads\n const size = stat.size;\n\n if (size > MAX_OTTER_BYTES) {\n return {\n verified: false,\n filename,\n error: `File exceeds size limit (${MAX_OTTER_BYTES.toLocaleString()} bytes, got ${size.toLocaleString()})`,\n };\n }\n\n // Single read — used for hashing and UTF-8 validation (zero TOCTOU window)\n let raw: Buffer;\n try {\n raw = readFileSync(filePath);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n return { verified: false, filename, error: `Cannot read file: ${msg}` };\n }\n\n // Belt-and-suspenders: re-check length after read in case of a race\n if (raw.length > MAX_OTTER_BYTES) {\n return {\n verified: false,\n filename,\n error: `File exceeds size limit after read (${MAX_OTTER_BYTES.toLocaleString()} bytes, got ${raw.length.toLocaleString()})`,\n };\n }\n\n // Hash the raw bytes\n const actual = computeSha256(raw);\n\n // Constant-time comparison prevents timing-oracle attacks\n if (!safeEqual(actual, expected)) {\n return {\n verified: false,\n filename,\n error: `SHA-256 mismatch: expected ${expected.substring(0, 8)}…, got ${actual.substring(0, 8)}…`,\n };\n }\n\n // UTF-8 validation — binary injection guard\n try {\n const decoder = new TextDecoder('utf-8', { fatal: true });\n decoder.decode(raw);\n } catch {\n return {\n verified: false,\n filename,\n error: 'File is not valid UTF-8 — may be corrupted or contain binary injection',\n };\n }\n\n return { verified: true, filename };\n}\n\n// ---------------------------------------------------------------------------\n// Directory-level verification\n// ---------------------------------------------------------------------------\n\nexport interface VerifyAllOttersResult {\n verified: string[];\n failed: Array<{ filename: string; error: string }>;\n unknown: string[];\n}\n\n/**\n * Scan a directory for `*-otter.json` files, verify each one against\n * canonical checksums. Returns lists of verified, failed, and unknown files.\n */\nexport function verifyAllOtters(otterDir: string): VerifyAllOttersResult {\n const verified: string[] = [];\n const failed: Array<{ filename: string; error: string }> = [];\n const unknown: string[] = [];\n\n let entries: string[];\n try {\n entries = readdirSync(otterDir);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n return {\n verified: [],\n failed: [{ filename: '<directory>', error: `Cannot read directory: ${msg}` }],\n unknown: [],\n };\n }\n\n const otterFiles = entries.filter((f) => f.endsWith('-otter.json'));\n\n for (const filename of otterFiles) {\n const filePath = join(otterDir, filename);\n\n // Skip symlinks at the directory-scan level too\n try {\n if (lstatSync(filePath).isSymbolicLink()) {\n failed.push({ filename, error: 'Skipped: symlink' });\n continue;\n }\n } catch {\n // verifyOtterFile will handle stat errors\n }\n\n const result = verifyOtterFile(filePath);\n\n if (result.verified) {\n verified.push(result.filename);\n } else if (result.error?.startsWith('Unknown otter file')) {\n unknown.push(result.filename);\n } else {\n failed.push({ filename: result.filename, error: result.error ?? 'Unknown error' });\n }\n }\n\n // Check for missing canonical files — ones we expect but didn't find on disk\n for (const canonicalName of CANONICAL_CHECKSUMS.keys()) {\n if (!otterFiles.includes(canonicalName)) {\n failed.push({ filename: canonicalName, error: 'Missing from directory' });\n }\n }\n\n return { verified, failed, unknown };\n}\n\n// ---------------------------------------------------------------------------\n// Otter directory resolution\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_SEARCH_PATHS = ['node_modules/@stackwright-pro/otters/src/', 'packages/otters/src/'];\n\nfunction resolveOtterDir(): string | null {\n const cwd = process.cwd();\n for (const relative of DEFAULT_SEARCH_PATHS) {\n const candidate = join(cwd, relative);\n try {\n lstatSync(candidate);\n return candidate;\n } catch {\n // Not found, try next\n }\n }\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// MCP tool registration\n// ---------------------------------------------------------------------------\n\nexport function registerIntegrityTools(server: McpServer): void {\n server.tool(\n 'stackwright_pro_verify_otter_integrity',\n 'Verify SHA-256 integrity of all Pro otter agent definitions. Call this at startup before discovering otters. Auto-discovers the otter directory from known paths. Returns verified/failed/unknown lists.',\n {},\n async () => {\n const resolved = resolveOtterDir();\n\n if (!resolved) {\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({\n error: true,\n message:\n 'Could not locate otter directory. Searched: ' + DEFAULT_SEARCH_PATHS.join(', '),\n }),\n },\n ],\n isError: true,\n };\n }\n\n const result = verifyAllOtters(resolved);\n\n const allGood = result.failed.length === 0 && result.unknown.length === 0;\n\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({\n otterDir: resolved,\n totalCanonical: CANONICAL_CHECKSUMS.size,\n verifiedCount: result.verified.length,\n failedCount: result.failed.length,\n unknownCount: result.unknown.length,\n verified: result.verified,\n failed: result.failed,\n unknown: result.unknown,\n warning:\n result.failed.length > 0\n ? 'SHA-256 mismatches detected (non-blocking). PKI-signed manifest support coming soon.'\n : undefined,\n }),\n },\n ],\n isError: false,\n };\n }\n );\n}\n"],"mappings":";AAiBA,SAAS,YAAY,uBAAuB;AAC5C,SAAS,cAAc,aAAa,iBAAiB;AACrD,SAAS,MAAM,gBAAgB;AAU/B,IAAM,aAAa,oBAAI,IAAoB;AAAA,EACzC;AAAA,IACE;AAAA,IACA;AAAA,EACF;AAAA,EACA;AAAA,IACE;AAAA,IACA;AAAA,EACF;AAAA,EACA;AAAA,IACE;AAAA,IACA;AAAA,EACF;AAAA,EACA;AAAA,IACE;AAAA,IACA;AAAA,EACF;AAAA,EACA;AAAA,IACE;AAAA,IACA;AAAA,EACF;AAAA,EACA;AAAA,IACE;AAAA,IACA;AAAA,EACF;AAAA,EACA;AAAA,IACE;AAAA,IACA;AAAA,EACF;AAAA,EACA;AAAA,IACE;AAAA,IACA;AAAA,EACF;AAAA,EACA;AAAA,IACE;AAAA,IACA;AAAA,EACF;AACF,CAAC;AACD,OAAO,OAAO,UAAU;AACxB,IAAM,sBAAmD;AAOzD,IAAM,gBAAgB;AAEtB,WAAW,CAAC,MAAM,MAAM,KAAK,qBAAqB;AAChD,MAAI,CAAC,cAAc,KAAK,MAAM,GAAG;AAC/B,UAAM,IAAI;AAAA,MACR,iDAAiD,IAAI,iCACrB,OAAO,MAAM,MAAM,MAAM;AAAA,IAC3D;AAAA,EACF;AACF;AAGA,IAAM,kBAAkB,IAAI,OAAO;AAO5B,SAAS,cAAc,MAAsB;AAClD,SAAO,WAAW,QAAQ,EAAE,OAAO,IAAI,EAAE,OAAO,KAAK;AACvD;AAGA,SAAS,UAAU,GAAW,GAAoB;AAChD,MAAI,EAAE,WAAW,EAAE,OAAQ,QAAO;AAClC,SAAO,gBAAgB,OAAO,KAAK,GAAG,MAAM,GAAG,OAAO,KAAK,GAAG,MAAM,CAAC;AACvE;AAkBO,SAAS,gBAAgB,UAAyC;AACvE,QAAM,WAAW,SAAS,QAAQ;AAGlC,QAAM,WAAW,oBAAoB,IAAI,QAAQ;AACjD,MAAI,aAAa,QAAW;AAC1B,WAAO,EAAE,UAAU,OAAO,UAAU,OAAO,2CAA2C;AAAA,EACxF;AAGA,MAAI;AACJ,MAAI;AACF,WAAO,UAAU,QAAQ;AAAA,EAC3B,SAAS,KAAK;AACZ,UAAM,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC3D,WAAO,EAAE,UAAU,OAAO,UAAU,OAAO,qBAAqB,GAAG,GAAG;AAAA,EACxE;AAEA,MAAI,KAAK,eAAe,GAAG;AACzB,WAAO,EAAE,UAAU,OAAO,UAAU,OAAO,6BAA6B;AAAA,EAC1E;AAGA,QAAM,OAAO,KAAK;AAElB,MAAI,OAAO,iBAAiB;AAC1B,WAAO;AAAA,MACL,UAAU;AAAA,MACV;AAAA,MACA,OAAO,4BAA4B,gBAAgB,eAAe,CAAC,eAAe,KAAK,eAAe,CAAC;AAAA,IACzG;AAAA,EACF;AAGA,MAAI;AACJ,MAAI;AACF,UAAM,aAAa,QAAQ;AAAA,EAC7B,SAAS,KAAK;AACZ,UAAM,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC3D,WAAO,EAAE,UAAU,OAAO,UAAU,OAAO,qBAAqB,GAAG,GAAG;AAAA,EACxE;AAGA,MAAI,IAAI,SAAS,iBAAiB;AAChC,WAAO;AAAA,MACL,UAAU;AAAA,MACV;AAAA,MACA,OAAO,uCAAuC,gBAAgB,eAAe,CAAC,eAAe,IAAI,OAAO,eAAe,CAAC;AAAA,IAC1H;AAAA,EACF;AAGA,QAAM,SAAS,cAAc,GAAG;AAGhC,MAAI,CAAC,UAAU,QAAQ,QAAQ,GAAG;AAChC,WAAO;AAAA,MACL,UAAU;AAAA,MACV;AAAA,MACA,OAAO,8BAA8B,SAAS,UAAU,GAAG,CAAC,CAAC,eAAU,OAAO,UAAU,GAAG,CAAC,CAAC;AAAA,IAC/F;AAAA,EACF;AAGA,MAAI;AACF,UAAM,UAAU,IAAI,YAAY,SAAS,EAAE,OAAO,KAAK,CAAC;AACxD,YAAQ,OAAO,GAAG;AAAA,EACpB,QAAQ;AACN,WAAO;AAAA,MACL,UAAU;AAAA,MACV;AAAA,MACA,OAAO;AAAA,IACT;AAAA,EACF;AAEA,SAAO,EAAE,UAAU,MAAM,SAAS;AACpC;AAgBO,SAAS,gBAAgB,UAAyC;AACvE,QAAM,WAAqB,CAAC;AAC5B,QAAM,SAAqD,CAAC;AAC5D,QAAM,UAAoB,CAAC;AAE3B,MAAI;AACJ,MAAI;AACF,cAAU,YAAY,QAAQ;AAAA,EAChC,SAAS,KAAK;AACZ,UAAM,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC3D,WAAO;AAAA,MACL,UAAU,CAAC;AAAA,MACX,QAAQ,CAAC,EAAE,UAAU,eAAe,OAAO,0BAA0B,GAAG,GAAG,CAAC;AAAA,MAC5E,SAAS,CAAC;AAAA,IACZ;AAAA,EACF;AAEA,QAAM,aAAa,QAAQ,OAAO,CAAC,MAAM,EAAE,SAAS,aAAa,CAAC;AAElE,aAAW,YAAY,YAAY;AACjC,UAAM,WAAW,KAAK,UAAU,QAAQ;AAGxC,QAAI;AACF,UAAI,UAAU,QAAQ,EAAE,eAAe,GAAG;AACxC,eAAO,KAAK,EAAE,UAAU,OAAO,mBAAmB,CAAC;AACnD;AAAA,MACF;AAAA,IACF,QAAQ;AAAA,IAER;AAEA,UAAM,SAAS,gBAAgB,QAAQ;AAEvC,QAAI,OAAO,UAAU;AACnB,eAAS,KAAK,OAAO,QAAQ;AAAA,IAC/B,WAAW,OAAO,OAAO,WAAW,oBAAoB,GAAG;AACzD,cAAQ,KAAK,OAAO,QAAQ;AAAA,IAC9B,OAAO;AACL,aAAO,KAAK,EAAE,UAAU,OAAO,UAAU,OAAO,OAAO,SAAS,gBAAgB,CAAC;AAAA,IACnF;AAAA,EACF;AAGA,aAAW,iBAAiB,oBAAoB,KAAK,GAAG;AACtD,QAAI,CAAC,WAAW,SAAS,aAAa,GAAG;AACvC,aAAO,KAAK,EAAE,UAAU,eAAe,OAAO,yBAAyB,CAAC;AAAA,IAC1E;AAAA,EACF;AAEA,SAAO,EAAE,UAAU,QAAQ,QAAQ;AACrC;AAMA,IAAM,uBAAuB,CAAC,6CAA6C,sBAAsB;AAEjG,SAAS,kBAAiC;AACxC,QAAM,MAAM,QAAQ,IAAI;AACxB,aAAW,YAAY,sBAAsB;AAC3C,UAAM,YAAY,KAAK,KAAK,QAAQ;AACpC,QAAI;AACF,gBAAU,SAAS;AACnB,aAAO;AAAA,IACT,QAAQ;AAAA,IAER;AAAA,EACF;AACA,SAAO;AACT;AAMO,SAAS,uBAAuB,QAAyB;AAC9D,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,CAAC;AAAA,IACD,YAAY;AACV,YAAM,WAAW,gBAAgB;AAEjC,UAAI,CAAC,UAAU;AACb,eAAO;AAAA,UACL,SAAS;AAAA,YACP;AAAA,cACE,MAAM;AAAA,cACN,MAAM,KAAK,UAAU;AAAA,gBACnB,OAAO;AAAA,gBACP,SACE,iDAAiD,qBAAqB,KAAK,IAAI;AAAA,cACnF,CAAC;AAAA,YACH;AAAA,UACF;AAAA,UACA,SAAS;AAAA,QACX;AAAA,MACF;AAEA,YAAM,SAAS,gBAAgB,QAAQ;AAEvC,YAAM,UAAU,OAAO,OAAO,WAAW,KAAK,OAAO,QAAQ,WAAW;AAExE,aAAO;AAAA,QACL,SAAS;AAAA,UACP;AAAA,YACE,MAAM;AAAA,YACN,MAAM,KAAK,UAAU;AAAA,cACnB,UAAU;AAAA,cACV,gBAAgB,oBAAoB;AAAA,cACpC,eAAe,OAAO,SAAS;AAAA,cAC/B,aAAa,OAAO,OAAO;AAAA,cAC3B,cAAc,OAAO,QAAQ;AAAA,cAC7B,UAAU,OAAO;AAAA,cACjB,QAAQ,OAAO;AAAA,cACf,SAAS,OAAO;AAAA,cAChB,SACE,OAAO,OAAO,SAAS,IACnB,yFACA;AAAA,YACR,CAAC;AAAA,UACH;AAAA,QACF;AAAA,QACA,SAAS;AAAA,MACX;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
package/dist/server.js CHANGED
@@ -1135,6 +1135,7 @@ function setupPackages(opts) {
1135
1135
 
1136
1136
  // src/tools/questions.ts
1137
1137
  var import_promises = require("fs/promises");
1138
+ var import_node_fs = require("fs");
1138
1139
  var import_node_path = require("path");
1139
1140
  var import_zod8 = require("zod");
1140
1141
 
@@ -1433,6 +1434,144 @@ function registerQuestionTools(server2) {
1433
1434
  };
1434
1435
  }
1435
1436
  );
1437
+ server2.tool(
1438
+ "stackwright_pro_get_next_question",
1439
+ "Returns the next unanswered question for a phase as a plain JSON object \u2014 one question at a time. Returns { done: true } when all questions are answered or the phase has no questions. Call this in a loop: present the question in plain chat, get user reply, call record_answer, repeat.",
1440
+ { phase: import_zod8.z.string().describe('Phase name e.g. "designer", "api", "auth"') },
1441
+ async ({ phase }) => {
1442
+ const SAFE_PHASE = /^[a-z][a-z0-9-]{0,30}$/;
1443
+ if (!SAFE_PHASE.test(phase)) {
1444
+ return {
1445
+ content: [
1446
+ {
1447
+ type: "text",
1448
+ text: JSON.stringify({ error: `Invalid phase name: "${phase.slice(0, 50)}"` })
1449
+ }
1450
+ ],
1451
+ isError: true
1452
+ };
1453
+ }
1454
+ const cwd = process.cwd();
1455
+ const questionsPath = (0, import_node_path.join)(cwd, ".stackwright", "questions", `${phase}.json`);
1456
+ let questions = [];
1457
+ try {
1458
+ const raw = await (0, import_promises.readFile)(questionsPath, "utf-8");
1459
+ const parsed = JSON.parse(raw);
1460
+ questions = parsed.questions ?? [];
1461
+ } catch {
1462
+ return { content: [{ type: "text", text: JSON.stringify({ done: true }) }] };
1463
+ }
1464
+ if (questions.length === 0) {
1465
+ return { content: [{ type: "text", text: JSON.stringify({ done: true }) }] };
1466
+ }
1467
+ const answersPath = (0, import_node_path.join)(cwd, ".stackwright", "answers", `${phase}.json`);
1468
+ let answeredIds = /* @__PURE__ */ new Set();
1469
+ try {
1470
+ const raw = await (0, import_promises.readFile)(answersPath, "utf-8");
1471
+ const parsed = JSON.parse(raw);
1472
+ answeredIds = new Set(Object.keys(parsed.answers ?? {}));
1473
+ } catch {
1474
+ }
1475
+ const next = questions.find((q) => !answeredIds.has(q.id));
1476
+ if (!next) {
1477
+ return { content: [{ type: "text", text: JSON.stringify({ done: true }) }] };
1478
+ }
1479
+ return {
1480
+ content: [
1481
+ {
1482
+ type: "text",
1483
+ text: JSON.stringify({
1484
+ done: false,
1485
+ questionId: next.id,
1486
+ question: next.question,
1487
+ type: next.type,
1488
+ options: next.options ?? null,
1489
+ help: next.help ?? null,
1490
+ index: questions.indexOf(next) + 1,
1491
+ total: questions.length
1492
+ })
1493
+ }
1494
+ ]
1495
+ };
1496
+ }
1497
+ );
1498
+ server2.tool(
1499
+ "stackwright_pro_record_answer",
1500
+ "Records a single answer to a phase question. Appends to .stackwright/answers/{phase}.json incrementally. Idempotent \u2014 calling twice for the same questionId overwrites. Call after receiving each user reply in the conversational question loop.",
1501
+ {
1502
+ phase: import_zod8.z.string().describe('Phase name e.g. "designer"'),
1503
+ questionId: import_zod8.z.string().describe('The question ID from get_next_question, e.g. "designer-1"'),
1504
+ answer: import_zod8.z.string().describe("The user's free-text answer")
1505
+ },
1506
+ async ({ phase, questionId, answer }) => {
1507
+ const SAFE_PHASE = /^[a-z][a-z0-9-]{0,30}$/;
1508
+ if (!SAFE_PHASE.test(phase)) {
1509
+ return {
1510
+ content: [
1511
+ { type: "text", text: JSON.stringify({ error: "Invalid phase name" }) }
1512
+ ],
1513
+ isError: true
1514
+ };
1515
+ }
1516
+ if (!/^[a-z0-9][a-z0-9-]{0,63}$/i.test(questionId)) {
1517
+ return {
1518
+ content: [
1519
+ { type: "text", text: JSON.stringify({ error: "Invalid questionId" }) }
1520
+ ],
1521
+ isError: true
1522
+ };
1523
+ }
1524
+ const safeAnswer = answer.slice(0, 2e3);
1525
+ const cwd = process.cwd();
1526
+ const answersDir = (0, import_node_path.join)(cwd, ".stackwright", "answers");
1527
+ const answersPath = (0, import_node_path.join)(answersDir, `${phase}.json`);
1528
+ if ((0, import_node_fs.existsSync)(answersDir) && (0, import_node_fs.lstatSync)(answersDir).isSymbolicLink()) {
1529
+ return {
1530
+ content: [
1531
+ {
1532
+ type: "text",
1533
+ text: JSON.stringify({ error: "answers dir is a symlink \u2014 refusing to write" })
1534
+ }
1535
+ ],
1536
+ isError: true
1537
+ };
1538
+ }
1539
+ (0, import_node_fs.mkdirSync)(answersDir, { recursive: true });
1540
+ let existing = {
1541
+ version: "1.0",
1542
+ phase,
1543
+ answers: {}
1544
+ };
1545
+ try {
1546
+ if ((0, import_node_fs.existsSync)(answersPath)) {
1547
+ if ((0, import_node_fs.lstatSync)(answersPath).isSymbolicLink()) {
1548
+ return {
1549
+ content: [
1550
+ {
1551
+ type: "text",
1552
+ text: JSON.stringify({ error: "answers file is a symlink \u2014 refusing to write" })
1553
+ }
1554
+ ],
1555
+ isError: true
1556
+ };
1557
+ }
1558
+ const raw = await (0, import_promises.readFile)(answersPath, "utf-8");
1559
+ existing = JSON.parse(raw);
1560
+ }
1561
+ } catch {
1562
+ }
1563
+ existing.answers[questionId] = safeAnswer;
1564
+ existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1565
+ const tmp = `${answersPath}.tmp`;
1566
+ await (0, import_promises.writeFile)(tmp, JSON.stringify(existing, null, 2), "utf-8");
1567
+ (0, import_node_fs.renameSync)(tmp, answersPath);
1568
+ return {
1569
+ content: [
1570
+ { type: "text", text: JSON.stringify({ recorded: true, phase, questionId }) }
1571
+ ]
1572
+ };
1573
+ }
1574
+ );
1436
1575
  }
1437
1576
 
1438
1577
  // src/tools/orchestration.ts
@@ -2318,12 +2457,49 @@ function registerPipelineTools(server2) {
2318
2457
  );
2319
2458
  server2.tool(
2320
2459
  "stackwright_pro_write_phase_questions",
2321
- `Parse otter question-collection response \u2192 .stackwright/questions/{phase}.json. ${DESC}`,
2460
+ `Parse otter question-collection response \u2192 .stackwright/questions/{phase}.json. Specialists may also call this directly with a parsed questions array. ${DESC}`,
2322
2461
  {
2323
- phase: import_zod10.z.string().describe('Phase name, e.g. "designer"'),
2324
- responseText: import_zod10.z.string().describe("Raw LLM response from QUESTION_COLLECTION_MODE")
2462
+ phase: import_zod10.z.string().optional().describe('Phase name, e.g. "designer" (required for direct write)'),
2463
+ responseText: import_zod10.z.string().optional().describe("Raw LLM response from QUESTION_COLLECTION_MODE"),
2464
+ questions: jsonCoerce(import_zod10.z.array(import_zod10.z.any()).optional()).describe(
2465
+ "Questions array for direct specialist write"
2466
+ )
2325
2467
  },
2326
- async ({ phase, responseText }) => res(handleWritePhaseQuestions({ phase, responseText }))
2468
+ async ({ phase, responseText, questions }) => {
2469
+ if (phase && questions && Array.isArray(questions)) {
2470
+ const SAFE_PHASE = /^[a-z][a-z0-9-]{0,30}$/;
2471
+ if (!SAFE_PHASE.test(phase)) {
2472
+ return {
2473
+ content: [
2474
+ { type: "text", text: JSON.stringify({ error: `Invalid phase name` }) }
2475
+ ],
2476
+ isError: true
2477
+ };
2478
+ }
2479
+ const questionsDir = (0, import_path4.join)(process.cwd(), ".stackwright", "questions");
2480
+ (0, import_fs4.mkdirSync)(questionsDir, { recursive: true });
2481
+ const outPath = (0, import_path4.join)(questionsDir, `${phase}.json`);
2482
+ (0, import_fs4.writeFileSync)(
2483
+ outPath,
2484
+ JSON.stringify({ phase, questions, writtenAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)
2485
+ );
2486
+ return {
2487
+ content: [
2488
+ {
2489
+ type: "text",
2490
+ text: JSON.stringify({
2491
+ written: true,
2492
+ phase,
2493
+ count: questions.length
2494
+ })
2495
+ }
2496
+ ]
2497
+ };
2498
+ }
2499
+ return res(
2500
+ handleWritePhaseQuestions({ phase: phase ?? "", responseText: responseText ?? "" })
2501
+ );
2502
+ }
2327
2503
  );
2328
2504
  server2.tool(
2329
2505
  "stackwright_pro_build_specialist_prompt",
@@ -2992,31 +3168,31 @@ var import_path7 = require("path");
2992
3168
  var _checksums = /* @__PURE__ */ new Map([
2993
3169
  [
2994
3170
  "stackwright-pro-api-otter.json",
2995
- "d72f365d7eb16f6195148d09f8d42249cc1b01fab21e769d991533a31b0b588a"
3171
+ "0ac26d85a5ad35b072a58965e1d5e090dd5c5f16dc14e68c452c3e99fcbb5510"
2996
3172
  ],
2997
3173
  [
2998
3174
  "stackwright-pro-auth-otter.json",
2999
- "6610a5a4e1f981a00771494832a5dbf2e7897f993ed2cfad388528578d15955c"
3175
+ "d789b71f196659d5745ebfca87a7bda60a1bb63cfeccd17b4a273ac1e29bb08d"
3000
3176
  ],
3001
3177
  [
3002
3178
  "stackwright-pro-dashboard-otter.json",
3003
- "f0316349b38e0923c2fa5385e24c4393511c61cea410a2b080a5ece14ace4801"
3179
+ "600e8597429c353e5b886f316731be84a86cd8b93617bf046e3cbf390b31a431"
3004
3180
  ],
3005
3181
  [
3006
3182
  "stackwright-pro-data-otter.json",
3007
- "bd85c01c352630088c3f27110bcd23b6f8f7d2eb72d62a324214ac405a4c1c65"
3183
+ "b2946e3da3b53282c122d150e6db86b0cb89d2edba2a94a7666b26d27051be96"
3008
3184
  ],
3009
3185
  [
3010
3186
  "stackwright-pro-designer-otter.json",
3011
- "aadac659f170c24a3921fe0940464b5dd22db290fb2d7340647c3c4795cfe246"
3187
+ "f4dbff5149051c77be1645de5ee12c0bd7d590c687a0b2d86737b915a5a6d5f0"
3012
3188
  ],
3013
3189
  [
3014
3190
  "stackwright-pro-foreman-otter.json",
3015
- "5bb8eca6d24efa970acc24974d3789a1ce48ead42624236bb7211c1c881b2584"
3191
+ "7464523d7288374dc6efa5c213c825ec0616e00cf4f739d8039eb41df812cbc5"
3016
3192
  ],
3017
3193
  [
3018
3194
  "stackwright-pro-page-otter.json",
3019
- "c5ad7b312e1c4f83183c489291233adfb5466050d6db44b8816817ac601faca8"
3195
+ "d75a71afa489478a6874abfbaa05baa46dcb2c16e4a5108f50f8187c9f67da60"
3020
3196
  ],
3021
3197
  [
3022
3198
  "stackwright-pro-theme-otter.json",
@@ -3024,7 +3200,7 @@ var _checksums = /* @__PURE__ */ new Map([
3024
3200
  ],
3025
3201
  [
3026
3202
  "stackwright-pro-workflow-otter.json",
3027
- "2b2d132c536148bcd1fa5598d575f2a40b572c050e1cfe17a387d306b62fd855"
3203
+ "16da6c109d0b5ee60d0a14e009dbeab02a7bbac3b0947795769da053565b9821"
3028
3204
  ]
3029
3205
  ]);
3030
3206
  Object.freeze(_checksums);
@@ -3630,7 +3806,7 @@ var package_default = {
3630
3806
  "test:coverage": "vitest run --coverage"
3631
3807
  },
3632
3808
  name: "@stackwright-pro/mcp",
3633
- version: "0.2.0-alpha.17",
3809
+ version: "0.2.0-alpha.19",
3634
3810
  description: "MCP tools for Stackwright Pro - Data Explorer, Security, ISR, and Dashboard generation",
3635
3811
  license: "PROPRIETARY",
3636
3812
  main: "./dist/server.js",