@happyvertical/smrt-dev-mcp 0.60.9 → 0.60.10

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.
Files changed (50) hide show
  1. package/AGENTS.md +27 -1
  2. package/README.md +17 -1
  3. package/agents/intent-duplicates.md +26 -0
  4. package/dist/dev-plane.js +2 -2
  5. package/dist/{http-CCiO7qQW.js → http-BqKiyqgN.js} +2 -2
  6. package/dist/{http-CCiO7qQW.js.map → http-BqKiyqgN.js.map} +1 -1
  7. package/dist/index.js +12 -409
  8. package/dist/index.js.map +1 -1
  9. package/dist/knowledge/index.d.ts +1 -1
  10. package/dist/knowledge/index.d.ts.map +1 -1
  11. package/dist/knowledge/intents/audit-trail.md +63 -0
  12. package/dist/knowledge/intents/auth-and-users.md +67 -0
  13. package/dist/knowledge/intents/background-work.md +72 -0
  14. package/dist/knowledge/intents/feature-flags-and-entitlements.md +60 -0
  15. package/dist/knowledge/intents/file-and-asset-storage.md +71 -0
  16. package/dist/knowledge/intents/human-approvals.md +56 -0
  17. package/dist/knowledge/intents/index.md +41 -0
  18. package/dist/knowledge/intents/money-and-rates.md +65 -0
  19. package/dist/knowledge/intents/outbound-messages.md +67 -0
  20. package/dist/knowledge/intents/prompts-and-ai-operations.md +72 -0
  21. package/dist/knowledge/intents/relationships-and-trees.md +56 -0
  22. package/dist/knowledge/intents/scheduled-runs.md +61 -0
  23. package/dist/knowledge/intents/search-facts-knowledge.md +68 -0
  24. package/dist/knowledge/intents/secrets-and-config.md +66 -0
  25. package/dist/knowledge/intents/tenant-scoping.md +63 -0
  26. package/dist/knowledge/intents/webhooks-and-events.md +71 -0
  27. package/dist/knowledge/okf-bundle.d.ts +61 -0
  28. package/dist/knowledge/okf-bundle.d.ts.map +1 -0
  29. package/dist/{knowledge-DqoDX-lZ.js → knowledge-DUiMUjEg.js} +218 -3
  30. package/dist/knowledge-DUiMUjEg.js.map +1 -0
  31. package/dist/knowledge.js +2 -2
  32. package/dist/review-cli-DJIWUlFp.js +75 -0
  33. package/dist/review-cli-DJIWUlFp.js.map +1 -0
  34. package/dist/review-smrt-project-ByEwPcor.js +774 -0
  35. package/dist/review-smrt-project-ByEwPcor.js.map +1 -0
  36. package/dist/smrt-knowledge.okf.md +60 -0
  37. package/dist/{tool-catalog-RHq7nyCm.js → tool-catalog-BhnSCF8g.js} +11 -2
  38. package/dist/tool-catalog-BhnSCF8g.js.map +1 -0
  39. package/dist/tool-catalog.d.ts.map +1 -1
  40. package/dist/tools/intent-detector.d.ts +57 -0
  41. package/dist/tools/intent-detector.d.ts.map +1 -0
  42. package/dist/tools/intent-index.d.ts +40 -0
  43. package/dist/tools/intent-index.d.ts.map +1 -0
  44. package/dist/tools/review-cli.d.ts +40 -0
  45. package/dist/tools/review-cli.d.ts.map +1 -0
  46. package/dist/tools/review-smrt-project.d.ts +25 -0
  47. package/dist/tools/review-smrt-project.d.ts.map +1 -1
  48. package/package.json +6 -6
  49. package/dist/knowledge-DqoDX-lZ.js.map +0 -1
  50. package/dist/tool-catalog-RHq7nyCm.js.map +0 -1
package/AGENTS.md CHANGED
@@ -9,7 +9,7 @@ deterministic SMRT ecosystem knowledge, and portable agent workflows.
9
9
  |------|---------|
10
10
  | `generate-smrt-class` | Generates `@smrt()` class with fields, decorator config, imports |
11
11
  | `introspect-project` | Scans project directory for SMRT objects; compact summary by default, `detail: 'full'` for field/schema/method detail |
12
- | `review-smrt-project` | Advisory downstream ecosystem alignment review for dependencies, storage, app shell, auth/tenancy, and manifest generation |
12
+ | `review-smrt-project` | Advisory downstream ecosystem alignment review for dependencies, storage, app shell, auth/tenancy, manifest generation, and intent-driven hand-rolled-equivalent detection (#4690) |
13
13
  | `reflect-knowledge` | Reports deterministic SMRT + HappyVertical SDK knowledge coverage and freshness |
14
14
  | `reflect-domain-knowledge` | Reports downstream/domain artifact coverage and freshness |
15
15
  | `check-knowledge-freshness` | Runs deterministic agent-doc and stale-reference checks |
@@ -33,6 +33,14 @@ deterministic SMRT ecosystem knowledge, and portable agent workflows.
33
33
  README parameter tables to the exported `TOOLS` definitions. Add or change a
34
34
  tool schema and its authored documentation in the same commit.
35
35
 
36
+ ## Intent-driven duplicate detection (#4690)
37
+
38
+ `review-smrt-project` / `smrt-dev-mcp review` flag hand-rolled equivalents of
39
+ smrt packages from the intent index. The rules (`regex:` tokens become `RegExp`)
40
+ come from the full index packaged in this release, never from the reviewed tree;
41
+ `intentIndexPath` / `--intent-index` is caller-trusted. Details, token kinds and
42
+ the CLI/workflow contract: [agents/intent-duplicates.md](agents/intent-duplicates.md).
43
+
36
44
  ## Knowledge Discovery (#2143)
37
45
 
38
46
  Discovery reads the workspace globs instead of assuming `<root>/packages`:
@@ -104,6 +112,18 @@ otherwise emitted docs read `/AGENTS.md` and changed-file matching compares
104
112
  against a leading `/` and selects nothing. The root owns any changed path no
105
113
  member package owns, mirroring the member-excluded scan.
106
114
 
115
+ ### OKF component pages (#4686)
116
+
117
+ `pnpm knowledge:okf` (after `pnpm build`) writes `docs/knowledge/components/<unscoped-name>.md` per
118
+ workspace package from this index; never hand-edit them. `source_revision` is a `sha256` of the page's
119
+ inputs (manifest slice, package AGENTS/module-doc hashes, models, MCP tools, `agentSurface`), not a commit.
120
+ `knowledge:check --strict` fails on stale, edited, missing or extra pages and on dangling `/…md` links in
121
+ hand-authored pages. Details: header of `scripts/knowledge-okf.ts`.
122
+
123
+ Consumer bundle (#4689): `okf-bundle.ts` defines the `.agents/smrt-knowledge/` contract `smrt docs:agents` writes;
124
+ `check-knowledge-freshness` reports its age (`okf-bundle-age` info, `okf-bundle-aged` past 90 days) and flags
125
+ `stale-okf-bundle` when a page's `package_version` no longer matches `node_modules` until `docs:agents` reruns.
126
+
107
127
  ## Agent Skills
108
128
 
109
129
  Bundled skills live under `skills/`, the Agent Plugins fixed discovery
@@ -157,6 +177,12 @@ server is always launched from a repo that installs the package. Prefer an
157
177
  absolute `node /path/to/node_modules/@happyvertical/smrt-dev-mcp/dist/index.js`
158
178
  launcher or a small wrapper script with an absolute Node path.
159
179
 
180
+ This monorepo's `.mcp.json` runs `node scripts/mcp-servers/smrt-dev-mcp.mjs`:
181
+ stdout is protocol-only, the launcher never installs or uses `pnpm exec`/`npx`
182
+ (their stdout banner breaks stdio, #4685), and it falls back to `tsx` on `src/`
183
+ when `dist` is missing or older, so rebuild `dist` after server edits. Guarded by
184
+ `src/launcher-stdio.test.ts`; see [scripts/mcp-servers/README.md](../../scripts/mcp-servers/README.md).
185
+
160
186
  ## MCP Tier Context
161
187
 
162
188
  - **Tier 1** (Runtime): auto-generated from `@smrt()` objects — live data operations
package/README.md CHANGED
@@ -334,7 +334,8 @@ available it falls back to `@happyvertical/smrt-scanner`.
334
334
  Run an advisory downstream ecosystem review. The tool scans package manifests
335
335
  and source imports for missing HappyVertical dependencies, direct storage
336
336
  bypasses, custom HTTP shells, custom object manifest generation, local
337
- auth/tenancy/audit seams, and UI shell drift. It returns deterministic findings
337
+ auth/tenancy/audit seams, UI shell drift, and hand-rolled equivalents of
338
+ smrt packages. It returns deterministic findings
338
339
  and suggested follow-up issue titles; it does not modify files.
339
340
 
340
341
  | Parameter | Type | Required | Description |
@@ -343,6 +344,21 @@ and suggested follow-up issue titles; it does not modify files.
343
344
  | `rootDir` | `string` | No | Compatibility alias for `directory` |
344
345
  | `includeSourceEvidence` | `boolean` | No | Include file/line evidence (default: true) |
345
346
  | `maxFindings` | `number` | No | Limit findings returned |
347
+ | `intentIndexPath` | `string` | No | Caller-trusted intent index driving duplicate detection (`regex:` tokens become `RegExp`). Default: the full index packaged in this release (`dist/knowledge/intents/index.md`), else the monorepo's `docs/knowledge/intents/index.md` in a source checkout; never read from `directory` |
348
+ | `detectDuplicates` | `boolean` | No | Run the intent-driven hand-rolled-equivalent pass (default: true) |
349
+
350
+ Duplicate detection is table-driven: each row of the intent index names home
351
+ packages and `import:`, `call:`, `regex:` and `decimal-field:` signals. When a
352
+ package shows a signal and imports none of the row's home packages, the tool
353
+ emits an advisory `duplicate-detection` finding anchored at `path:line` that
354
+ names the intent page and component pages. Unknown signal kinds are skipped
355
+ with a stderr note. The rules are never taken from the reviewed project, so a
356
+ pull request cannot delete them or inject a pattern; when no index can be
357
+ found the tool reports `skipped` with a note. The same pass runs from a shell with
358
+ `npx @happyvertical/smrt-dev-mcp review --directory . [--intent-index PATH] [--format summary]`
359
+ (exit 2 and a stderr message when no index is found, never a silent pass) and
360
+ from the reusable `consumer-knowledge-review.yml` workflow, which always uses
361
+ the packaged index.
346
362
 
347
363
  ### `reflect-knowledge`
348
364
 
@@ -0,0 +1,26 @@
1
+ # Intent-driven duplicate detection (#4690)
2
+
3
+ `review-smrt-project` parses the intent index at runtime
4
+ (`intent-index.ts`, pure) and `intent-detector.ts` evaluates each row's signals;
5
+ no intent is named in code, so a new row with signals needs no change here.
6
+ Trust boundary: `regex:` tokens become `RegExp`, so rules are never read from the
7
+ reviewed tree. Index lookup: `intentIndexPath` / `--intent-index` (trusted by the
8
+ caller), else the full unfiltered index shipped in this package's
9
+ `dist/knowledge/intents/` (`scripts/ship-knowledge-pages.mjs`, list
10
+ `OKF_INTENT_HOME_PACKAGES`), else the monorepo `docs/knowledge/intents/index.md`
11
+ when run from the smrt checkout. The exported consumer bundle index is filtered
12
+ to installed packages and is not consulted. Tokens:
13
+ `import:<module>`, `call:<ident>`, `regex:<pattern>`, and
14
+ `decimal-field:<glob>` (a property matching the glob declared with a decimal
15
+ literal or `type: 'decimal'`). Unknown kinds are skipped with a stderr note. A
16
+ finding needs a signal and no import of any row home package in the same
17
+ package; it is advisory (`area: duplicate-detection`, `file: path:line`,
18
+ `intentPage`, `componentPages`). Fixtures live in `test-fixtures/` (outside
19
+ `src/`, so tsc, Biome, and vitest globs never see them).
20
+
21
+ `smrt-dev-mcp review --directory . [--intent-index P] [--format summary]
22
+ [--fail-on-findings]` runs the pass from a shell (exit 2 with a stderr message when
23
+ no index is found; the tool reports `skipped` with a note). The reusable
24
+ `.github/workflows/consumer-knowledge-review.yml` calls it through the pinned
25
+ published package (public on npmjs, so private consumers need no token), has no
26
+ index input, and writes the summary to `$GITHUB_STEP_SUMMARY`.
package/dist/dev-plane.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { a as runtimeMigrationStatus, c as runtimeScheduleHealth, g as loadProjectCore, i as runtimeJobHealth, m as ProjectRuntimeResolutionError, o as runtimeRecentChanges, p as safeErrorMessage, r as runtimeDispatchHealth, s as runtimeRegistryDrift, t as RUNTIME_PROVENANCE } from "./tools-B0uaPTz0.js";
2
- import { i as SERVER_VERSION, r as SERVER_NAME, t as bearerMatches } from "./http-CCiO7qQW.js";
3
- import { n as TOOLS } from "./tool-catalog-RHq7nyCm.js";
2
+ import { i as SERVER_VERSION, r as SERVER_NAME, t as bearerMatches } from "./http-BqKiyqgN.js";
3
+ import { n as TOOLS } from "./tool-catalog-BhnSCF8g.js";
4
4
  import { i as runtimeSchemaDiff, n as runtimeObject, r as runtimeRegistry } from "./observation-DbB4z-9t.js";
5
5
  import { Server, createMcpHandler } from "@modelcontextprotocol/server";
6
6
  //#region src/dev-plane.ts
@@ -1,5 +1,5 @@
1
1
  import { a as runtimeMigrationStatus, c as runtimeScheduleHealth, i as runtimeJobHealth, o as runtimeRecentChanges, r as runtimeDispatchHealth, s as runtimeRegistryDrift } from "./tools-B0uaPTz0.js";
2
- import { n as TOOLS } from "./tool-catalog-RHq7nyCm.js";
2
+ import { n as TOOLS } from "./tool-catalog-BhnSCF8g.js";
3
3
  import { a as bootRuntime, i as runtimeSchemaDiff, n as runtimeObject, r as runtimeRegistry } from "./observation-DbB4z-9t.js";
4
4
  import { readFileSync } from "node:fs";
5
5
  import { dirname, join } from "node:path";
@@ -166,4 +166,4 @@ async function startRuntimeHttpHost(options = {}) {
166
166
  //#endregion
167
167
  export { SERVER_VERSION as i, startRuntimeHttpHost as n, SERVER_NAME as r, bearerMatches as t };
168
168
 
169
- //# sourceMappingURL=http-CCiO7qQW.js.map
169
+ //# sourceMappingURL=http-BqKiyqgN.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"http-CCiO7qQW.js","names":[],"sources":["../src/server-info.ts","../src/http.ts"],"sourcesContent":["import { readFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nexport const SERVER_NAME = 'smrt-dev-mcp';\n\nfunction readPackageVersion(): string {\n try {\n const packageRoot = dirname(fileURLToPath(import.meta.url));\n const packageJsonPath = join(packageRoot, '..', 'package.json');\n const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')) as {\n version?: unknown;\n };\n return typeof packageJson.version === 'string'\n ? packageJson.version\n : '0.0.0';\n } catch {\n return '0.0.0';\n }\n}\n\nexport const SERVER_VERSION = readPackageVersion();\n","/**\n * Standalone Level 2 runtime dev-plane host (#1831).\n *\n * Serves a *positive* read-only tool catalog over the stateless 2026-07-28\n * Streamable HTTP transport (#2147): `createMcpHandler` with `legacy: 'reject'`\n * and `maxSubscriptions: 0`, adapted to Node with `toNodeHandler`. No SSE,\n * no session header, no sticky routing.\n *\n * Security boundary (development only):\n * - binds loopback only; SDK localhost Host/Origin validation runs first;\n * - every request needs `Authorization: Bearer <token>` (constant-time\n * compare) — from `SMRT_DEV_MCP_TOKEN` or minted per process;\n * - no authenticated principal exists here, so scope stays fail-closed\n * global-only exactly as in Level 1;\n * - the catalog never includes generated CRUD, custom actions, `do()`, or\n * tool-backed `is()`.\n */\n\nimport { randomBytes, timingSafeEqual } from 'node:crypto';\nimport {\n createServer as createHttpServer,\n type Server as HttpServer,\n type IncomingMessage,\n type ServerResponse,\n} from 'node:http';\nimport type { AddressInfo } from 'node:net';\nimport {\n localhostHostValidation,\n localhostOriginValidation,\n toNodeHandler,\n} from '@modelcontextprotocol/node';\nimport type { CallToolResult } from '@modelcontextprotocol/server';\nimport { createMcpHandler, Server } from '@modelcontextprotocol/server';\nimport { SERVER_NAME, SERVER_VERSION } from './server-info.js';\nimport { TOOLS } from './tool-catalog.js';\nimport { bootRuntime, type RuntimeBoot } from './tools/runtime/boot.js';\nimport {\n runtimeObject,\n runtimeRegistry,\n runtimeSchemaDiff,\n} from './tools/runtime/observation.js';\nimport {\n runtimeDispatchHealth,\n runtimeJobHealth,\n runtimeMigrationStatus,\n runtimeRecentChanges,\n runtimeRegistryDrift,\n runtimeScheduleHealth,\n} from './tools/runtime/tools.js';\n\n/**\n * The complete Level 2 catalog. A tool is exposed over HTTP only if it is\n * named here; the static stdio catalog is deliberately not mounted.\n */\nexport const RUNTIME_HTTP_TOOL_NAMES = [\n 'runtime-registry',\n 'runtime-object',\n 'runtime-schema-diff',\n 'migration-status',\n 'job-health',\n 'schedule-health',\n 'dispatch-health',\n 'recent-changes',\n 'registry-drift',\n] as const;\n\nexport type RuntimeHttpToolName = (typeof RUNTIME_HTTP_TOOL_NAMES)[number];\n\nconst RUNTIME_HTTP_HANDLERS: Record<\n RuntimeHttpToolName,\n (args: Record<string, unknown>) => Promise<unknown>\n> = {\n 'runtime-registry': (args) => runtimeRegistry(args as never),\n 'runtime-object': (args) => runtimeObject(args as never),\n 'runtime-schema-diff': (args) => runtimeSchemaDiff(args as never),\n 'migration-status': (args) => runtimeMigrationStatus(args as never),\n 'job-health': (args) => runtimeJobHealth(args as never),\n 'schedule-health': (args) => runtimeScheduleHealth(args as never),\n 'dispatch-health': (args) => runtimeDispatchHealth(args as never),\n 'recent-changes': (args) => runtimeRecentChanges(args as never),\n 'registry-drift': (args) => runtimeRegistryDrift(args as never),\n};\n\n/** Catalog definitions for the HTTP plane, in catalog order. */\nexport function runtimeHttpTools() {\n const names = new Set<string>(RUNTIME_HTTP_TOOL_NAMES);\n return TOOLS.filter((tool) => names.has(tool.name));\n}\n\n/**\n * Build a fresh protocol server per request (stateless transport contract).\n * `projectRoot` is fixed at host start; per-request `projectPath` arguments\n * are ignored so a client cannot re-point the booted process.\n */\nexport function createRuntimeProtocolServer(projectRoot: string): Server {\n const server = new Server(\n { name: `${SERVER_NAME}-runtime`, version: SERVER_VERSION },\n { capabilities: { tools: {} } },\n );\n server.setRequestHandler('tools/list', async () => ({\n tools: runtimeHttpTools(),\n }));\n server.setRequestHandler('tools/call', async (request) => {\n const name = request.params.name as string;\n const handler = (RUNTIME_HTTP_HANDLERS as Record<string, unknown>)[name];\n if (typeof handler !== 'function') {\n throw new Error(`Unknown runtime tool: ${name}`);\n }\n const { projectPath: _ignored, ...args } = (request.params.arguments ??\n {}) as Record<string, unknown>;\n const result = await (\n handler as (a: Record<string, unknown>) => Promise<unknown>\n )({ ...args, projectPath: projectRoot });\n const text = JSON.stringify(result, null, 2);\n return {\n content: [{ type: 'text', text }],\n structuredContent: result as Record<string, unknown>,\n } satisfies CallToolResult;\n });\n return server;\n}\n\nexport interface RuntimeHttpHostOptions {\n projectRoot?: string;\n /** Loopback interface only; anything else is refused at startup. */\n host?: '127.0.0.1' | 'localhost' | '::1';\n /** `0` (default) picks a free port. */\n port?: number;\n /** Bearer token; defaults to `SMRT_DEV_MCP_TOKEN` or a per-process random token. */\n token?: string;\n /** Path the MCP endpoint is mounted on (default `/mcp`). */\n path?: string;\n}\n\nexport interface RuntimeHttpHost {\n url: string;\n token: string;\n boot: RuntimeBoot;\n close(): Promise<void>;\n}\n\nconst LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '::1']);\n\nexport function bearerMatches(\n header: string | undefined,\n token: string,\n): boolean {\n if (!header?.startsWith('Bearer ')) return false;\n const presented = Buffer.from(header.slice('Bearer '.length));\n const expected = Buffer.from(token);\n return (\n presented.length === expected.length && timingSafeEqual(presented, expected)\n );\n}\n\nfunction deny(res: ServerResponse, status: number, message: string): void {\n res.statusCode = status;\n res.setHeader('content-type', 'application/json');\n if (status === 401) {\n res.setHeader('www-authenticate', 'Bearer realm=\"smrt-dev-mcp runtime\"');\n }\n res.end(JSON.stringify({ error: message }));\n}\n\n/** Start the Level 2 host. Boots the confined runtime before listening. */\nexport async function startRuntimeHttpHost(\n options: RuntimeHttpHostOptions = {},\n): Promise<RuntimeHttpHost> {\n const host = options.host ?? '127.0.0.1';\n if (!LOOPBACK_HOSTS.has(host)) {\n throw new Error(\n 'The runtime dev-plane binds loopback only (127.0.0.1, localhost, ::1).',\n );\n }\n const path = options.path ?? '/mcp';\n const token =\n options.token ??\n process.env.SMRT_DEV_MCP_TOKEN?.trim() ??\n randomBytes(24).toString('base64url');\n if (!token) {\n throw new Error('SMRT_DEV_MCP_TOKEN must not be empty.');\n }\n const projectRoot = options.projectRoot ?? process.cwd();\n const boot = await bootRuntime({ projectRoot });\n\n const mcp = toNodeHandler(\n createMcpHandler(() => createRuntimeProtocolServer(projectRoot), {\n legacy: 'reject',\n maxSubscriptions: 0,\n }),\n );\n const hostGuard = localhostHostValidation();\n const originGuard = localhostOriginValidation();\n\n const httpServer: HttpServer = createHttpServer(\n (req: IncomingMessage, res: ServerResponse) => {\n // Guards answer and end the response themselves when they return false.\n if (!hostGuard(req, res)) return;\n if (!originGuard(req, res)) return;\n const url = new URL(req.url ?? '/', 'http://localhost');\n if (url.pathname !== path) {\n deny(res, 404, 'not found');\n return;\n }\n if (!bearerMatches(req.headers.authorization, token)) {\n deny(res, 401, 'missing or invalid bearer token');\n return;\n }\n void mcp(req, res);\n },\n );\n\n await new Promise<void>((resolve, reject) => {\n httpServer.once('error', reject);\n httpServer.listen(options.port ?? 0, host, () => {\n httpServer.off('error', reject);\n resolve();\n });\n });\n const address = httpServer.address() as AddressInfo;\n const urlHost =\n address.family === 'IPv6' ? `[${address.address}]` : address.address;\n return {\n url: `http://${urlHost}:${address.port}${path}`,\n token,\n boot,\n close: () =>\n new Promise<void>((resolve, reject) => {\n httpServer.closeAllConnections?.();\n httpServer.close((error) => (error ? reject(error) : resolve()));\n }),\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"http-BqKiyqgN.js","names":[],"sources":["../src/server-info.ts","../src/http.ts"],"sourcesContent":["import { readFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nexport const SERVER_NAME = 'smrt-dev-mcp';\n\nfunction readPackageVersion(): string {\n try {\n const packageRoot = dirname(fileURLToPath(import.meta.url));\n const packageJsonPath = join(packageRoot, '..', 'package.json');\n const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')) as {\n version?: unknown;\n };\n return typeof packageJson.version === 'string'\n ? packageJson.version\n : '0.0.0';\n } catch {\n return '0.0.0';\n }\n}\n\nexport const SERVER_VERSION = readPackageVersion();\n","/**\n * Standalone Level 2 runtime dev-plane host (#1831).\n *\n * Serves a *positive* read-only tool catalog over the stateless 2026-07-28\n * Streamable HTTP transport (#2147): `createMcpHandler` with `legacy: 'reject'`\n * and `maxSubscriptions: 0`, adapted to Node with `toNodeHandler`. No SSE,\n * no session header, no sticky routing.\n *\n * Security boundary (development only):\n * - binds loopback only; SDK localhost Host/Origin validation runs first;\n * - every request needs `Authorization: Bearer <token>` (constant-time\n * compare) — from `SMRT_DEV_MCP_TOKEN` or minted per process;\n * - no authenticated principal exists here, so scope stays fail-closed\n * global-only exactly as in Level 1;\n * - the catalog never includes generated CRUD, custom actions, `do()`, or\n * tool-backed `is()`.\n */\n\nimport { randomBytes, timingSafeEqual } from 'node:crypto';\nimport {\n createServer as createHttpServer,\n type Server as HttpServer,\n type IncomingMessage,\n type ServerResponse,\n} from 'node:http';\nimport type { AddressInfo } from 'node:net';\nimport {\n localhostHostValidation,\n localhostOriginValidation,\n toNodeHandler,\n} from '@modelcontextprotocol/node';\nimport type { CallToolResult } from '@modelcontextprotocol/server';\nimport { createMcpHandler, Server } from '@modelcontextprotocol/server';\nimport { SERVER_NAME, SERVER_VERSION } from './server-info.js';\nimport { TOOLS } from './tool-catalog.js';\nimport { bootRuntime, type RuntimeBoot } from './tools/runtime/boot.js';\nimport {\n runtimeObject,\n runtimeRegistry,\n runtimeSchemaDiff,\n} from './tools/runtime/observation.js';\nimport {\n runtimeDispatchHealth,\n runtimeJobHealth,\n runtimeMigrationStatus,\n runtimeRecentChanges,\n runtimeRegistryDrift,\n runtimeScheduleHealth,\n} from './tools/runtime/tools.js';\n\n/**\n * The complete Level 2 catalog. A tool is exposed over HTTP only if it is\n * named here; the static stdio catalog is deliberately not mounted.\n */\nexport const RUNTIME_HTTP_TOOL_NAMES = [\n 'runtime-registry',\n 'runtime-object',\n 'runtime-schema-diff',\n 'migration-status',\n 'job-health',\n 'schedule-health',\n 'dispatch-health',\n 'recent-changes',\n 'registry-drift',\n] as const;\n\nexport type RuntimeHttpToolName = (typeof RUNTIME_HTTP_TOOL_NAMES)[number];\n\nconst RUNTIME_HTTP_HANDLERS: Record<\n RuntimeHttpToolName,\n (args: Record<string, unknown>) => Promise<unknown>\n> = {\n 'runtime-registry': (args) => runtimeRegistry(args as never),\n 'runtime-object': (args) => runtimeObject(args as never),\n 'runtime-schema-diff': (args) => runtimeSchemaDiff(args as never),\n 'migration-status': (args) => runtimeMigrationStatus(args as never),\n 'job-health': (args) => runtimeJobHealth(args as never),\n 'schedule-health': (args) => runtimeScheduleHealth(args as never),\n 'dispatch-health': (args) => runtimeDispatchHealth(args as never),\n 'recent-changes': (args) => runtimeRecentChanges(args as never),\n 'registry-drift': (args) => runtimeRegistryDrift(args as never),\n};\n\n/** Catalog definitions for the HTTP plane, in catalog order. */\nexport function runtimeHttpTools() {\n const names = new Set<string>(RUNTIME_HTTP_TOOL_NAMES);\n return TOOLS.filter((tool) => names.has(tool.name));\n}\n\n/**\n * Build a fresh protocol server per request (stateless transport contract).\n * `projectRoot` is fixed at host start; per-request `projectPath` arguments\n * are ignored so a client cannot re-point the booted process.\n */\nexport function createRuntimeProtocolServer(projectRoot: string): Server {\n const server = new Server(\n { name: `${SERVER_NAME}-runtime`, version: SERVER_VERSION },\n { capabilities: { tools: {} } },\n );\n server.setRequestHandler('tools/list', async () => ({\n tools: runtimeHttpTools(),\n }));\n server.setRequestHandler('tools/call', async (request) => {\n const name = request.params.name as string;\n const handler = (RUNTIME_HTTP_HANDLERS as Record<string, unknown>)[name];\n if (typeof handler !== 'function') {\n throw new Error(`Unknown runtime tool: ${name}`);\n }\n const { projectPath: _ignored, ...args } = (request.params.arguments ??\n {}) as Record<string, unknown>;\n const result = await (\n handler as (a: Record<string, unknown>) => Promise<unknown>\n )({ ...args, projectPath: projectRoot });\n const text = JSON.stringify(result, null, 2);\n return {\n content: [{ type: 'text', text }],\n structuredContent: result as Record<string, unknown>,\n } satisfies CallToolResult;\n });\n return server;\n}\n\nexport interface RuntimeHttpHostOptions {\n projectRoot?: string;\n /** Loopback interface only; anything else is refused at startup. */\n host?: '127.0.0.1' | 'localhost' | '::1';\n /** `0` (default) picks a free port. */\n port?: number;\n /** Bearer token; defaults to `SMRT_DEV_MCP_TOKEN` or a per-process random token. */\n token?: string;\n /** Path the MCP endpoint is mounted on (default `/mcp`). */\n path?: string;\n}\n\nexport interface RuntimeHttpHost {\n url: string;\n token: string;\n boot: RuntimeBoot;\n close(): Promise<void>;\n}\n\nconst LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '::1']);\n\nexport function bearerMatches(\n header: string | undefined,\n token: string,\n): boolean {\n if (!header?.startsWith('Bearer ')) return false;\n const presented = Buffer.from(header.slice('Bearer '.length));\n const expected = Buffer.from(token);\n return (\n presented.length === expected.length && timingSafeEqual(presented, expected)\n );\n}\n\nfunction deny(res: ServerResponse, status: number, message: string): void {\n res.statusCode = status;\n res.setHeader('content-type', 'application/json');\n if (status === 401) {\n res.setHeader('www-authenticate', 'Bearer realm=\"smrt-dev-mcp runtime\"');\n }\n res.end(JSON.stringify({ error: message }));\n}\n\n/** Start the Level 2 host. Boots the confined runtime before listening. */\nexport async function startRuntimeHttpHost(\n options: RuntimeHttpHostOptions = {},\n): Promise<RuntimeHttpHost> {\n const host = options.host ?? '127.0.0.1';\n if (!LOOPBACK_HOSTS.has(host)) {\n throw new Error(\n 'The runtime dev-plane binds loopback only (127.0.0.1, localhost, ::1).',\n );\n }\n const path = options.path ?? '/mcp';\n const token =\n options.token ??\n process.env.SMRT_DEV_MCP_TOKEN?.trim() ??\n randomBytes(24).toString('base64url');\n if (!token) {\n throw new Error('SMRT_DEV_MCP_TOKEN must not be empty.');\n }\n const projectRoot = options.projectRoot ?? process.cwd();\n const boot = await bootRuntime({ projectRoot });\n\n const mcp = toNodeHandler(\n createMcpHandler(() => createRuntimeProtocolServer(projectRoot), {\n legacy: 'reject',\n maxSubscriptions: 0,\n }),\n );\n const hostGuard = localhostHostValidation();\n const originGuard = localhostOriginValidation();\n\n const httpServer: HttpServer = createHttpServer(\n (req: IncomingMessage, res: ServerResponse) => {\n // Guards answer and end the response themselves when they return false.\n if (!hostGuard(req, res)) return;\n if (!originGuard(req, res)) return;\n const url = new URL(req.url ?? '/', 'http://localhost');\n if (url.pathname !== path) {\n deny(res, 404, 'not found');\n return;\n }\n if (!bearerMatches(req.headers.authorization, token)) {\n deny(res, 401, 'missing or invalid bearer token');\n return;\n }\n void mcp(req, res);\n },\n );\n\n await new Promise<void>((resolve, reject) => {\n httpServer.once('error', reject);\n httpServer.listen(options.port ?? 0, host, () => {\n httpServer.off('error', reject);\n resolve();\n });\n });\n const address = httpServer.address() as AddressInfo;\n const urlHost =\n address.family === 'IPv6' ? `[${address.address}]` : address.address;\n return {\n url: `http://${urlHost}:${address.port}${path}`,\n token,\n boot,\n close: () =>\n new Promise<void>((resolve, reject) => {\n httpServer.closeAllConnections?.();\n httpServer.close((error) => (error ? reject(error) : resolve()));\n }),\n };\n}\n"],"mappings":"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"}
package/dist/index.js CHANGED
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import { f as redactConnectionString, m as ProjectRuntimeResolutionError } from "./tools-B0uaPTz0.js";
3
- import { i as SERVER_VERSION, n as startRuntimeHttpHost, r as SERVER_NAME } from "./http-CCiO7qQW.js";
4
- import { n as TOOLS, t as REVIEW_SKILL_NAME } from "./tool-catalog-RHq7nyCm.js";
5
- import { a as buildReviewContext, c as compactContextResult, i as buildPackageSpecialistContext, n as buildContext, o as checkKnowledgeFreshness, r as buildKnowledgeIndex, s as checkKnowledgeFreshnessFromIndex, t as buildArchitectureContext } from "./knowledge-DqoDX-lZ.js";
3
+ import { i as SERVER_VERSION, n as startRuntimeHttpHost, r as SERVER_NAME } from "./http-BqKiyqgN.js";
4
+ import { n as TOOLS, t as REVIEW_SKILL_NAME } from "./tool-catalog-BhnSCF8g.js";
5
+ import { a as buildReviewContext, c as compactContextResult, i as buildPackageSpecialistContext, n as buildContext, o as checkKnowledgeFreshness, r as buildKnowledgeIndex, s as checkKnowledgeFreshnessFromIndex, t as buildArchitectureContext } from "./knowledge-DUiMUjEg.js";
6
+ import { t as reviewSmrtProject } from "./review-smrt-project-ByEwPcor.js";
6
7
  import { existsSync, readFileSync, realpathSync } from "node:fs";
7
- import { dirname, join, relative, resolve, sep } from "node:path";
8
+ import { dirname, join } from "node:path";
8
9
  import { fileURLToPath, pathToFileURL } from "node:url";
9
10
  import { ProtocolError, ProtocolErrorCode, Server } from "@modelcontextprotocol/server";
10
11
  import { serveStdio } from "@modelcontextprotocol/server/stdio";
11
- import { access, readFile, readdir } from "node:fs/promises";
12
12
  //#region src/agent-skills.ts
13
13
  var AGENT_SKILLS = [{
14
14
  name: "smrt-code-review",
@@ -232,409 +232,6 @@ function compactObject(value) {
232
232
  return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== void 0));
233
233
  }
234
234
  //#endregion
235
- //#region src/tools/review-smrt-project.ts
236
- /**
237
- * Ecosystem-aware downstream project review.
238
- * Advisory only: scans manifests and source files, then reports alignment risks.
239
- */
240
- var SOURCE_EXTENSIONS = /\.(tsx?|jsx?|svelte)$/;
241
- var SKIP_DIRS = /* @__PURE__ */ new Set([
242
- "node_modules",
243
- "dist",
244
- "build",
245
- ".git",
246
- ".smrt",
247
- ".svelte-kit",
248
- "coverage"
249
- ]);
250
- var DEPENDENCY_SECTIONS = [
251
- "dependencies",
252
- "devDependencies",
253
- "peerDependencies",
254
- "optionalDependencies"
255
- ];
256
- var SMRT_PACKAGE_PREFIX = "@happyvertical/smrt-";
257
- var HAPPYVERTICAL_PACKAGE_PREFIX = "@happyvertical/";
258
- async function reviewSmrtProject(args) {
259
- const projectPath = resolve(args.directory ?? args.rootDir ?? process.cwd());
260
- if (!await pathExists(projectPath)) return JSON.stringify({
261
- projectPath,
262
- packageCount: 0,
263
- packages: [],
264
- findings: [],
265
- summary: {
266
- high: 0,
267
- medium: 0,
268
- low: 0
269
- },
270
- diagnostics: [{
271
- severity: "warning",
272
- message: `Project directory does not exist: ${projectPath}`
273
- }]
274
- }, null, 2);
275
- const packageContexts = await buildPackageContexts(projectPath);
276
- const sourceFiles = await listSourceFiles(projectPath, packageContexts);
277
- await attachSourceFiles(sourceFiles, packageContexts, projectPath);
278
- const findings = limitFindings([
279
- ...findMissingHappyVerticalDependencies(packageContexts),
280
- ...findCustomManifestGeneration(sourceFiles, projectPath),
281
- ...findDirectStorageBypasses(sourceFiles, packageContexts, projectPath),
282
- ...findCustomHttpShells(sourceFiles, packageContexts, projectPath),
283
- ...findLocalAuthTenancy(sourceFiles, packageContexts, projectPath),
284
- ...findUiShellDrift(packageContexts, projectPath),
285
- ...findMissingManifestArtifacts(sourceFiles, packageContexts, projectPath)
286
- ], args.maxFindings);
287
- const packages = packageContexts.map(packageInventory).sort((left, right) => left.path.localeCompare(right.path));
288
- const summary = summarizeFindings(findings);
289
- return JSON.stringify({
290
- projectPath,
291
- packageCount: packages.length,
292
- packages,
293
- findings: args.includeSourceEvidence === false ? findings.map(({ evidence: _evidence, ...finding }) => finding) : findings,
294
- summary,
295
- referenceChecks: [
296
- "Prefer SMRT scanner/runtime manifests over custom manifest builders.",
297
- "Prefer SvelteKit plus @happyvertical/smrt-svelte for app shells.",
298
- "Prefer @happyvertical/sql and @happyvertical/files/assets/content over direct durable node:fs storage.",
299
- "Prefer smrt-users, smrt-tenancy, and profile/audit packages over local static auth seams."
300
- ],
301
- suggestedFollowUpIssues: findings.map((finding) => ({
302
- title: finding.suggestedIssueTitle,
303
- severity: finding.severity,
304
- area: finding.area
305
- }))
306
- }, null, 2);
307
- }
308
- async function buildPackageContexts(projectPath) {
309
- const packageJsonPaths = await listPackageJsonFiles(projectPath);
310
- const contexts = await Promise.all(packageJsonPaths.map(async (packageJsonPath) => {
311
- const json = JSON.parse(await readFile(packageJsonPath, "utf-8"));
312
- const directory = dirname(packageJsonPath);
313
- return {
314
- directory,
315
- relativePath: relative(projectPath, directory) || ".",
316
- packageJsonPath,
317
- json,
318
- dependencies: collectDependencies(json),
319
- scripts: isRecord$1(json.scripts) ? json.scripts : {},
320
- imports: /* @__PURE__ */ new Map(),
321
- sourceFiles: []
322
- };
323
- }));
324
- if (contexts.length === 0) contexts.push({
325
- directory: projectPath,
326
- relativePath: ".",
327
- packageJsonPath: join(projectPath, "package.json"),
328
- json: {},
329
- dependencies: /* @__PURE__ */ new Set(),
330
- scripts: {},
331
- imports: /* @__PURE__ */ new Map(),
332
- sourceFiles: []
333
- });
334
- return contexts.sort((left, right) => left.directory.length - right.directory.length);
335
- }
336
- async function listPackageJsonFiles(projectPath) {
337
- const files = [];
338
- await visit(projectPath, async (filePath, entryName) => {
339
- if (entryName === "package.json") files.push(filePath);
340
- });
341
- return files;
342
- }
343
- async function listSourceFiles(projectPath, packages) {
344
- const files = [];
345
- await visit(projectPath, async (filePath, entryName) => {
346
- if (!SOURCE_EXTENSIONS.test(entryName)) return;
347
- if (entryName.endsWith(".d.ts") || entryName.endsWith(".test.ts") || entryName.endsWith(".spec.ts")) return;
348
- const packageDir = findOwningPackage(filePath, packages).directory;
349
- const content = await readFile(filePath, "utf-8");
350
- files.push({
351
- path: filePath,
352
- packageDir,
353
- content
354
- });
355
- });
356
- return files;
357
- }
358
- async function attachSourceFiles(sourceFiles, packages, projectPath) {
359
- for (const sourceFile of sourceFiles) {
360
- const owner = packages.find((pkg) => pkg.directory === sourceFile.packageDir);
361
- if (!owner) continue;
362
- owner.sourceFiles.push(sourceFile);
363
- for (const importedPackage of extractImports(sourceFile.content)) {
364
- if (!importedPackage.startsWith(HAPPYVERTICAL_PACKAGE_PREFIX)) continue;
365
- const evidence = {
366
- filePath: relative(projectPath, sourceFile.path),
367
- line: lineNumber(sourceFile.content, importedPackage),
368
- detail: `Imports ${importedPackage}`
369
- };
370
- const existing = owner.imports.get(importedPackage) ?? [];
371
- existing.push(evidence);
372
- owner.imports.set(importedPackage, existing);
373
- }
374
- }
375
- }
376
- function findMissingHappyVerticalDependencies(packages) {
377
- return packages.flatMap((pkg) => {
378
- const ownName = typeof pkg.json.name === "string" ? pkg.json.name : void 0;
379
- const missing = Array.from(pkg.imports.keys()).filter((importedPackage) => importedPackage !== ownName && !pkg.dependencies.has(importedPackage));
380
- if (missing.length === 0) return [];
381
- return [{
382
- severity: missing.some((name) => name.startsWith(SMRT_PACKAGE_PREFIX)) ? "high" : "medium",
383
- area: "dependencies",
384
- code: "missing-happyvertical-dependencies",
385
- title: `${packageLabel(pkg)} imports HappyVertical packages that package.json does not declare`,
386
- evidence: missing.flatMap((name) => pkg.imports.get(name) ?? []),
387
- recommendation: "Declare every imported @happyvertical package in the owning package manifest so downstream installs and generated knowledge stay reproducible.",
388
- suggestedIssueTitle: `Declare missing HappyVertical dependencies in ${packageLabel(pkg)}`
389
- }];
390
- });
391
- }
392
- function findCustomManifestGeneration(sourceFiles, projectPath) {
393
- return sourceFiles.filter((file) => /manifest\.json/.test(file.content) && /objects\s*:/.test(file.content) && /\b(writeFile|writeFileSync)\b/.test(file.content) && !/@happyvertical\/smrt-scanner/.test(file.content)).map((file) => ({
394
- severity: "high",
395
- area: "manifest",
396
- code: "custom-object-manifest-generation",
397
- title: "Custom SMRT object manifest generation detected",
398
- evidence: [{
399
- filePath: relative(projectPath, file.path),
400
- line: lineNumber(file.content, "manifest.json"),
401
- detail: "Writes a manifest.json object inventory outside the SMRT scanner/runtime path."
402
- }],
403
- recommendation: "Use the SMRT scanner/runtime manifest path so defaults, relationships, schemas, tenant fields, and cross-package references match framework behavior.",
404
- suggestedIssueTitle: "Replace custom object manifest generation with SMRT scanner/runtime manifest generation"
405
- }));
406
- }
407
- function findDirectStorageBypasses(sourceFiles, packages, projectPath) {
408
- return sourceFiles.flatMap((file) => {
409
- const owner = findOwningPackage(file.path, packages);
410
- const usesFs = /from\s+['"](?:node:fs|node:fs\/promises|fs|fs\/promises)['"]|require\(['"](?:node:fs|node:fs\/promises|fs|fs\/promises)['"]\)/.test(file.content);
411
- const writesDurableData = /\b(writeFile|appendFile|mkdir|rm|rename)\b/.test(file.content) && /(\.json|data|storage|persist|cache|db)/i.test(file.content);
412
- const usesDirectSql = /from\s+['"](?:better-sqlite3|sqlite3|pg|mysql2?|knex|drizzle-orm)['"]/.test(file.content);
413
- if ((!usesFs || !writesDurableData) && !usesDirectSql) return [];
414
- if (owner.dependencies.has("@happyvertical/sql") || owner.dependencies.has("@happyvertical/files") || owner.dependencies.has("@happyvertical/smrt-assets") || owner.dependencies.has("@happyvertical/smrt-content")) return [];
415
- return [{
416
- severity: "medium",
417
- area: "storage",
418
- code: "direct-storage-bypass",
419
- title: `${packageLabel(owner)} appears to bypass HappyVertical storage packages`,
420
- evidence: [{
421
- filePath: relative(projectPath, file.path),
422
- line: lineNumber(file.content, usesDirectSql ? "sqlite" : "writeFile"),
423
- detail: usesDirectSql ? "Imports a direct SQL/storage library without @happyvertical/sql." : "Uses node:fs-style durable writes without @happyvertical/files/assets/content."
424
- }],
425
- recommendation: "Route durable data through @happyvertical/sql, @happyvertical/files, SMRT assets, or SMRT content unless this is explicitly build-only tooling.",
426
- suggestedIssueTitle: `Review direct storage usage in ${packageLabel(owner)}`
427
- }];
428
- });
429
- }
430
- function findCustomHttpShells(sourceFiles, packages, projectPath) {
431
- const evidenceByPackage = /* @__PURE__ */ new Map();
432
- for (const pkg of packages) {
433
- if (pkg.dependencies.has("@sveltejs/kit")) continue;
434
- const routerDependencies = [
435
- "express",
436
- "fastify",
437
- "hono",
438
- "koa"
439
- ].filter((dep) => pkg.dependencies.has(dep));
440
- if (routerDependencies.length === 0) continue;
441
- appendEvidence(evidenceByPackage, pkg, {
442
- filePath: relative(projectPath, pkg.packageJsonPath),
443
- detail: `Declares custom router package(s): ${routerDependencies.join(", ")}.`
444
- });
445
- }
446
- for (const file of sourceFiles) {
447
- const owner = findOwningPackage(file.path, packages);
448
- if (owner.dependencies.has("@sveltejs/kit")) continue;
449
- if (!/from\s+['"](?:node:http|http|node:https|https)['"]|createServer\s*\(/.test(file.content)) continue;
450
- appendEvidence(evidenceByPackage, owner, {
451
- filePath: relative(projectPath, file.path),
452
- line: lineNumber(file.content, "createServer"),
453
- detail: "Custom HTTP routing found without the SvelteKit/SMRT app-shell dependency pattern."
454
- });
455
- }
456
- return Array.from(evidenceByPackage.entries()).map(([owner, evidence]) => ({
457
- severity: "medium",
458
- area: "api-shell",
459
- code: "custom-http-shell",
460
- title: `${packageLabel(owner)} uses a custom HTTP shell`,
461
- evidence,
462
- recommendation: "Compare the app shell against the Anytown/Ergot SvelteKit + SMRT shell pattern before adding custom HTTP infrastructure.",
463
- suggestedIssueTitle: `Align ${packageLabel(owner)} app shell with SMRT/SvelteKit conventions`
464
- }));
465
- }
466
- function appendEvidence(evidenceByPackage, pkg, evidence) {
467
- const existing = evidenceByPackage.get(pkg);
468
- if (existing) {
469
- existing.push(evidence);
470
- return;
471
- }
472
- evidenceByPackage.set(pkg, [evidence]);
473
- }
474
- function findLocalAuthTenancy(sourceFiles, packages, projectPath) {
475
- return sourceFiles.flatMap((file) => {
476
- const owner = findOwningPackage(file.path, packages);
477
- const pathSignal = /(auth|tenant|audit|session|rbac|user)/i.test(file.path);
478
- const codeSignal = /\b(tenantId|tenant|role|permission|session|auditLog|apiKey)\b/.test(file.content);
479
- if (!pathSignal || !codeSignal) return [];
480
- if (owner.dependencies.has("@happyvertical/smrt-users") || owner.dependencies.has("@happyvertical/smrt-tenancy") || owner.dependencies.has("@happyvertical/smrt-profiles")) return [];
481
- return [{
482
- severity: "medium",
483
- area: "auth-tenancy",
484
- code: "local-auth-tenancy",
485
- title: `${packageLabel(owner)} contains local auth/tenancy/audit logic`,
486
- evidence: [{
487
- filePath: relative(projectPath, file.path),
488
- line: lineNumber(file.content, "tenant"),
489
- detail: "Auth, tenancy, session, role, or audit terminology appears without smrt-users/smrt-tenancy/smrt-profiles dependencies."
490
- }],
491
- recommendation: "Add explicit adapters to smrt-users, smrt-tenancy, and profile/audit models or document why this local implementation is intentionally isolated.",
492
- suggestedIssueTitle: `Review auth and tenancy adapters in ${packageLabel(owner)}`
493
- }];
494
- });
495
- }
496
- function findUiShellDrift(packages, projectPath) {
497
- return packages.flatMap((pkg) => {
498
- if (!(pkg.sourceFiles.some((file) => file.path.endsWith(".svelte")) || /(?:web|ui|app|site|frontend)/i.test(packageLabel(pkg)) || pkg.dependencies.has("svelte") || pkg.dependencies.has("vite"))) return [];
499
- if (pkg.dependencies.has("@happyvertical/smrt-svelte")) return [];
500
- return [{
501
- severity: "low",
502
- area: "ui-shell",
503
- code: "missing-smrt-svelte-shell",
504
- title: `${packageLabel(pkg)} looks like UI work without @happyvertical/smrt-svelte`,
505
- evidence: [{
506
- filePath: relative(projectPath, pkg.packageJsonPath),
507
- detail: "UI-facing package does not declare @happyvertical/smrt-svelte."
508
- }],
509
- recommendation: "Use @happyvertical/smrt-svelte and the SvelteKit shell pattern for downstream app UI unless this package is intentionally framework-agnostic.",
510
- suggestedIssueTitle: `Check SMRT Svelte shell alignment for ${packageLabel(pkg)}`
511
- }];
512
- });
513
- }
514
- function findMissingManifestArtifacts(sourceFiles, packages, projectPath) {
515
- return packages.flatMap((pkg) => {
516
- if (!pkg.sourceFiles.some((file) => /@smrt\s*\(/.test(file.content))) return [];
517
- if (sourceFiles.some((file) => file.packageDir === pkg.directory && /@happyvertical\/smrt-scanner/.test(file.content)) || pathExistsSyncHint(join(pkg.directory, ".smrt", "manifest.json")) || pathExistsSyncHint(join(pkg.directory, "dist", "manifest.json"))) return [];
518
- return [{
519
- severity: "low",
520
- area: "manifest",
521
- code: "missing-generated-manifest-artifact",
522
- title: `${packageLabel(pkg)} has @smrt objects but no generated manifest artifact was found`,
523
- evidence: [{
524
- filePath: relative(projectPath, pkg.packageJsonPath),
525
- detail: "No .smrt/manifest.json or dist/manifest.json was visible during review."
526
- }],
527
- recommendation: "Run the package build/test manifest generation path and verify it uses the SMRT scanner/runtime manifest pipeline.",
528
- suggestedIssueTitle: `Verify SMRT manifest generation for ${packageLabel(pkg)}`
529
- }];
530
- });
531
- }
532
- async function visit(dir, onFile) {
533
- let entries;
534
- try {
535
- entries = await readdir(dir, { withFileTypes: true });
536
- } catch {
537
- return;
538
- }
539
- for (const entry of entries) {
540
- const fullPath = join(dir, entry.name);
541
- if (entry.isDirectory()) {
542
- if (SKIP_DIRS.has(entry.name)) continue;
543
- await visit(fullPath, onFile);
544
- continue;
545
- }
546
- if (entry.isFile()) await onFile(fullPath, entry.name);
547
- }
548
- }
549
- function collectDependencies(packageJson) {
550
- const dependencies = /* @__PURE__ */ new Set();
551
- for (const sectionName of DEPENDENCY_SECTIONS) {
552
- const section = packageJson[sectionName];
553
- if (!isRecord$1(section)) continue;
554
- for (const dependencyName of Object.keys(section)) dependencies.add(dependencyName);
555
- }
556
- return dependencies;
557
- }
558
- function extractImports(content) {
559
- const imports = /* @__PURE__ */ new Set();
560
- for (const pattern of [
561
- /(?:import|export)\s+(?:type\s+)?(?:[^'"]+\s+from\s+)?['"]([^'"]+)['"]/g,
562
- /\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
563
- /\brequire\s*\(\s*['"]([^'"]+)['"]\s*\)/g
564
- ]) for (const match of content.matchAll(pattern)) {
565
- const imported = normalizePackageImport(match[1]);
566
- if (imported) imports.add(imported);
567
- }
568
- return Array.from(imports);
569
- }
570
- function normalizePackageImport(value) {
571
- if (!value || value.startsWith(".") || value.startsWith("/")) return void 0;
572
- if (value.startsWith("@")) {
573
- const [scope, name] = value.split("/");
574
- return scope && name ? `${scope}/${name}` : value;
575
- }
576
- return value.split("/")[0];
577
- }
578
- function findOwningPackage(filePath, packages) {
579
- const normalized = resolve(filePath);
580
- return packages.filter((pkg) => normalized === pkg.directory || normalized.startsWith(`${pkg.directory}${sep}`)).sort((left, right) => right.directory.length - left.directory.length)[0] ?? packages[0];
581
- }
582
- function packageInventory(pkg) {
583
- const declaredHappyVerticalDependencies = Array.from(pkg.dependencies).filter((dependency) => dependency.startsWith(HAPPYVERTICAL_PACKAGE_PREFIX)).sort();
584
- const importedHappyVerticalPackages = Array.from(pkg.imports.keys()).sort();
585
- const ownName = typeof pkg.json.name === "string" ? pkg.json.name : void 0;
586
- return {
587
- name: packageLabel(pkg),
588
- path: pkg.relativePath,
589
- private: typeof pkg.json.private === "boolean" ? pkg.json.private : void 0,
590
- scripts: Object.keys(pkg.scripts).sort(),
591
- declaredHappyVerticalDependencies,
592
- importedHappyVerticalPackages,
593
- missingHappyVerticalDependencies: importedHappyVerticalPackages.filter((name) => name !== ownName && !pkg.dependencies.has(name)),
594
- hasSvelteKit: pkg.dependencies.has("@sveltejs/kit"),
595
- hasSmrtSvelte: pkg.dependencies.has("@happyvertical/smrt-svelte")
596
- };
597
- }
598
- function summarizeFindings(findings) {
599
- return findings.reduce((summary, finding) => {
600
- summary[finding.severity]++;
601
- return summary;
602
- }, {
603
- high: 0,
604
- medium: 0,
605
- low: 0
606
- });
607
- }
608
- function limitFindings(findings, maxFindings) {
609
- const sorted = findings.sort((left, right) => severityRank(left.severity) - severityRank(right.severity) || left.area.localeCompare(right.area) || left.title.localeCompare(right.title));
610
- return maxFindings && maxFindings > 0 ? sorted.slice(0, maxFindings) : sorted;
611
- }
612
- function severityRank(severity) {
613
- return severity === "high" ? 0 : severity === "medium" ? 1 : 2;
614
- }
615
- function packageLabel(pkg) {
616
- return typeof pkg.json.name === "string" ? pkg.json.name : pkg.relativePath;
617
- }
618
- function lineNumber(content, needle) {
619
- const index = content.indexOf(needle);
620
- if (index < 0) return void 0;
621
- return content.slice(0, index).split("\n").length;
622
- }
623
- function isRecord$1(value) {
624
- return !!value && typeof value === "object" && !Array.isArray(value);
625
- }
626
- async function pathExists(path) {
627
- try {
628
- await access(path);
629
- return true;
630
- } catch {
631
- return false;
632
- }
633
- }
634
- function pathExistsSyncHint(path) {
635
- return existsSync(path);
636
- }
637
- //#endregion
638
235
  //#region src/index.ts
639
236
  /**
640
237
  * SMRT Development MCP Server
@@ -1227,7 +824,13 @@ async function mainHttp(cli) {
1227
824
  process.on("SIGINT", shutdown);
1228
825
  process.on("SIGTERM", shutdown);
1229
826
  }
1230
- if (isEntrypoint()) {
827
+ if (isEntrypoint()) if (process.argv[2] === "review") import("./review-cli-DJIWUlFp.js").then(({ runReviewCli }) => runReviewCli(process.argv.slice(3))).then((code) => {
828
+ process.exitCode = code;
829
+ }).catch((error) => {
830
+ console.error(`[${SERVER_NAME}] review failed:`, error);
831
+ process.exit(1);
832
+ });
833
+ else {
1231
834
  const cli = parseHttpCliArgs(process.argv.slice(2));
1232
835
  (cli.http ? mainHttp(cli) : main()).catch((error) => {
1233
836
  console.error(`[${SERVER_NAME}] Fatal error:`, error);