@happyvertical/smrt-dev-mcp 0.60.8 → 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.
- package/AGENTS.md +27 -1
- package/README.md +17 -1
- package/agents/intent-duplicates.md +26 -0
- package/dist/dev-plane.js +2 -2
- package/dist/{http-CCiO7qQW.js → http-BqKiyqgN.js} +2 -2
- package/dist/{http-CCiO7qQW.js.map → http-BqKiyqgN.js.map} +1 -1
- package/dist/index.js +12 -409
- package/dist/index.js.map +1 -1
- package/dist/knowledge/index.d.ts +1 -1
- package/dist/knowledge/index.d.ts.map +1 -1
- package/dist/knowledge/intents/audit-trail.md +63 -0
- package/dist/knowledge/intents/auth-and-users.md +67 -0
- package/dist/knowledge/intents/background-work.md +72 -0
- package/dist/knowledge/intents/feature-flags-and-entitlements.md +60 -0
- package/dist/knowledge/intents/file-and-asset-storage.md +71 -0
- package/dist/knowledge/intents/human-approvals.md +56 -0
- package/dist/knowledge/intents/index.md +41 -0
- package/dist/knowledge/intents/money-and-rates.md +65 -0
- package/dist/knowledge/intents/outbound-messages.md +67 -0
- package/dist/knowledge/intents/prompts-and-ai-operations.md +72 -0
- package/dist/knowledge/intents/relationships-and-trees.md +56 -0
- package/dist/knowledge/intents/scheduled-runs.md +61 -0
- package/dist/knowledge/intents/search-facts-knowledge.md +68 -0
- package/dist/knowledge/intents/secrets-and-config.md +66 -0
- package/dist/knowledge/intents/tenant-scoping.md +63 -0
- package/dist/knowledge/intents/webhooks-and-events.md +71 -0
- package/dist/knowledge/okf-bundle.d.ts +61 -0
- package/dist/knowledge/okf-bundle.d.ts.map +1 -0
- package/dist/{knowledge-DqoDX-lZ.js → knowledge-DUiMUjEg.js} +218 -3
- package/dist/knowledge-DUiMUjEg.js.map +1 -0
- package/dist/knowledge.js +2 -2
- package/dist/review-cli-DJIWUlFp.js +75 -0
- package/dist/review-cli-DJIWUlFp.js.map +1 -0
- package/dist/review-smrt-project-ByEwPcor.js +774 -0
- package/dist/review-smrt-project-ByEwPcor.js.map +1 -0
- package/dist/smrt-knowledge.okf.md +60 -0
- package/dist/{tool-catalog-RHq7nyCm.js → tool-catalog-BhnSCF8g.js} +11 -2
- package/dist/tool-catalog-BhnSCF8g.js.map +1 -0
- package/dist/tool-catalog.d.ts.map +1 -1
- package/dist/tools/intent-detector.d.ts +57 -0
- package/dist/tools/intent-detector.d.ts.map +1 -0
- package/dist/tools/intent-index.d.ts +40 -0
- package/dist/tools/intent-index.d.ts.map +1 -0
- package/dist/tools/review-cli.d.ts +40 -0
- package/dist/tools/review-cli.d.ts.map +1 -0
- package/dist/tools/review-smrt-project.d.ts +25 -0
- package/dist/tools/review-smrt-project.d.ts.map +1 -1
- package/package.json +6 -6
- package/dist/knowledge-DqoDX-lZ.js.map +0 -1
- 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,
|
|
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,
|
|
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-
|
|
3
|
-
import { n as TOOLS } from "./tool-catalog-
|
|
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-
|
|
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-
|
|
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-
|
|
4
|
-
import { n as TOOLS, t as REVIEW_SKILL_NAME } from "./tool-catalog-
|
|
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-
|
|
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
|
|
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);
|