@happyvertical/smrt-dev-mcp 0.47.1 → 0.48.0

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 (43) hide show
  1. package/AGENTS.md +10 -9
  2. package/README.md +151 -66
  3. package/dist/dev-plane.d.ts +48 -0
  4. package/dist/dev-plane.d.ts.map +1 -0
  5. package/dist/dev-plane.js +211 -0
  6. package/dist/dev-plane.js.map +1 -0
  7. package/dist/http-TeoaK9Xr.js +168 -0
  8. package/dist/http-TeoaK9Xr.js.map +1 -0
  9. package/dist/http.d.ts +96 -0
  10. package/dist/http.d.ts.map +1 -0
  11. package/dist/index.d.ts +11 -1
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +86 -428
  14. package/dist/index.js.map +1 -1
  15. package/dist/knowledge/index.d.ts +27 -1
  16. package/dist/knowledge/index.d.ts.map +1 -1
  17. package/dist/{knowledge-DmZ-7GO9.js → knowledge-BOsSAdPo.js} +82 -655
  18. package/dist/knowledge-BOsSAdPo.js.map +1 -0
  19. package/dist/knowledge.js +2 -2
  20. package/dist/observation-VJgHaPps.js +802 -0
  21. package/dist/observation-VJgHaPps.js.map +1 -0
  22. package/dist/runtime.d.ts +21 -0
  23. package/dist/runtime.d.ts.map +1 -0
  24. package/dist/runtime.js +18 -0
  25. package/dist/runtime.js.map +1 -0
  26. package/dist/server-info.d.ts +3 -0
  27. package/dist/server-info.d.ts.map +1 -0
  28. package/dist/tool-catalog-Zl-bcimC.js +643 -0
  29. package/dist/tool-catalog-Zl-bcimC.js.map +1 -0
  30. package/dist/tool-catalog.d.ts.map +1 -1
  31. package/dist/tools/introspect-project.d.ts +8 -0
  32. package/dist/tools/introspect-project.d.ts.map +1 -1
  33. package/dist/tools/runtime/boot.d.ts +70 -0
  34. package/dist/tools/runtime/boot.d.ts.map +1 -0
  35. package/dist/tools/runtime/connection.d.ts +19 -0
  36. package/dist/tools/runtime/connection.d.ts.map +1 -1
  37. package/dist/tools/runtime/observation.d.ts +43 -0
  38. package/dist/tools/runtime/observation.d.ts.map +1 -0
  39. package/dist/tools/runtime/tools.d.ts +19 -0
  40. package/dist/tools/runtime/tools.d.ts.map +1 -1
  41. package/package.json +14 -6
  42. package/skills/smrt-code-review/SKILL.md +1 -1
  43. package/dist/knowledge-DmZ-7GO9.js.map +0 -1
@@ -0,0 +1,802 @@
1
+ import { existsSync, readFileSync, statSync } from "node:fs";
2
+ import { pathToFileURL } from "node:url";
3
+ import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
4
+ import { createHash } from "node:crypto";
5
+ import { ObjectRegistry, readDispatchHealth, readJobHealth, readMigrationStatus, readRecentChanges, readRegistryDrift, readScheduleHealth, snapshotRegistry } from "@happyvertical/smrt-core";
6
+ import { discoverSmrtPackages, resolveManifestPath } from "@happyvertical/smrt-core/manifest/discover-smrt-packages";
7
+ import { SchemaComparer } from "@happyvertical/smrt-core/migrations";
8
+ import { getPackageConfig, loadConfig } from "@happyvertical/smrt-config";
9
+ import { getDatabase } from "@happyvertical/sql";
10
+ //#region src/tools/runtime/boot.ts
11
+ /**
12
+ * Confined runtime bootstrap for the Level 2 observation plane (#1831).
13
+ *
14
+ * "Booting" here means registering *manifests* into the in-process
15
+ * `ObjectRegistry` — the project's own `.smrt/manifest.json` (or built
16
+ * `dist/manifest.json`) plus every installed SMRT package manifest that the
17
+ * project's dependency tree resolves to. No project source is imported, no
18
+ * module is executed, no database is touched. That confinement is the safety
19
+ * boundary: an observing agent sees what the runtime *would* register, never
20
+ * what arbitrary project code does on import.
21
+ *
22
+ * The registry is a process-global singleton, so a process boots once. There
23
+ * is deliberately no re-boot tool; restart the process to observe a rebuilt
24
+ * manifest.
25
+ */
26
+ /** Provenance label for facts read from authored/installed manifests. */
27
+ var DECLARED_PROVENANCE = "declared (manifest)";
28
+ /** Project manifest candidates, most authoritative first. */
29
+ var PROJECT_MANIFEST_CANDIDATES = [".smrt/manifest.json", "dist/manifest.json"];
30
+ function relativePath(projectRoot, path) {
31
+ const rel = relative(projectRoot, path);
32
+ if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) return basename(path);
33
+ return rel.split(sep).join("/");
34
+ }
35
+ function readManifest(path) {
36
+ try {
37
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
38
+ return parsed && typeof parsed === "object" ? parsed : null;
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
43
+ function registerManifest(manifest, fallbackPackageName) {
44
+ const manifestPackageName = typeof manifest.packageName === "string" && manifest.packageName ? manifest.packageName : fallbackPackageName ?? void 0;
45
+ let count = 0;
46
+ for (const [name, definition] of Object.entries(manifest.objects ?? {})) {
47
+ if (!definition || typeof definition !== "object") continue;
48
+ const ownPackage = definition.packageName;
49
+ ObjectRegistry.registerFromManifest(name, definition, typeof ownPackage === "string" && ownPackage ? ownPackage : manifestPackageName);
50
+ count += 1;
51
+ }
52
+ return count;
53
+ }
54
+ var booted = null;
55
+ var bootedProjectRoot = null;
56
+ var bootedProjectManifestPath = null;
57
+ var bootedProjectManifestMtimeMs = null;
58
+ /**
59
+ * Whether the project manifest on disk is newer than the one this process
60
+ * booted. The registry is process-global and boots once, so this is the only
61
+ * signal an agent has that a rebuild happened underneath it.
62
+ */
63
+ function getBootStaleness() {
64
+ if (!booted || !bootedProjectManifestPath) return {
65
+ stale: false,
66
+ bootedAt: booted?.bootedAt ?? null,
67
+ manifestModifiedAt: null
68
+ };
69
+ try {
70
+ const mtimeMs = statSync(bootedProjectManifestPath).mtimeMs;
71
+ return {
72
+ stale: bootedProjectManifestMtimeMs !== null && mtimeMs > bootedProjectManifestMtimeMs,
73
+ bootedAt: booted.bootedAt,
74
+ manifestModifiedAt: new Date(mtimeMs).toISOString()
75
+ };
76
+ } catch {
77
+ return {
78
+ stale: false,
79
+ bootedAt: booted.bootedAt,
80
+ manifestModifiedAt: null
81
+ };
82
+ }
83
+ }
84
+ /**
85
+ * The resolved root the process booted from. Consumers must relativize
86
+ * paths against *this* root, never a per-request argument, or a caller could
87
+ * widen the root (e.g. `/`) and read the layout back through "relative" paths.
88
+ */
89
+ function getBootedProjectRoot() {
90
+ return bootedProjectRoot;
91
+ }
92
+ /**
93
+ * Boot the confined runtime once per process. A second call returns the
94
+ * existing record without touching the registry.
95
+ */
96
+ async function bootRuntime(options = {}) {
97
+ if (booted) return booted;
98
+ const projectRoot = resolve(options.projectRoot ?? process.cwd());
99
+ const diagnostics = [];
100
+ const manifests = [];
101
+ const projectManifestPath = PROJECT_MANIFEST_CANDIDATES.map((candidate) => join(projectRoot, candidate)).find((path) => existsSync(path));
102
+ const projectPackageName = readProjectPackageName(projectRoot);
103
+ if (!projectManifestPath) diagnostics.push({
104
+ severity: "warning",
105
+ code: "project_manifest_missing",
106
+ message: "No project manifest found (.smrt/manifest.json or dist/manifest.json); run the project build so the runtime manifest exists."
107
+ });
108
+ else {
109
+ const manifest = readManifest(projectManifestPath);
110
+ if (!manifest) diagnostics.push({
111
+ severity: "error",
112
+ code: "project_manifest_invalid",
113
+ message: `Project manifest at ${relativePath(projectRoot, projectManifestPath)} is not valid JSON.`
114
+ });
115
+ else manifests.push({
116
+ kind: "project",
117
+ packageName: typeof manifest.packageName === "string" ? manifest.packageName : projectPackageName,
118
+ path: relativePath(projectRoot, projectManifestPath),
119
+ objectCount: registerManifest(manifest, projectPackageName)
120
+ });
121
+ }
122
+ let dependencyNames = [];
123
+ try {
124
+ dependencyNames = discoverSmrtPackages({
125
+ baseDir: projectRoot,
126
+ noCache: true
127
+ });
128
+ } catch (error) {
129
+ diagnostics.push({
130
+ severity: "warning",
131
+ code: "dependency_discovery_failed",
132
+ message: `Installed SMRT package discovery failed: ${error instanceof Error ? error.message : "unknown error"}`
133
+ });
134
+ }
135
+ for (const dependency of dependencyNames.sort()) {
136
+ const manifestPath = resolveManifestPath(dependency, projectRoot);
137
+ if (!manifestPath) {
138
+ diagnostics.push({
139
+ severity: "info",
140
+ code: "dependency_manifest_missing",
141
+ message: `Installed SMRT package ${dependency} exposes no runtime manifest.`
142
+ });
143
+ continue;
144
+ }
145
+ const manifest = readManifest(manifestPath);
146
+ if (!manifest) {
147
+ diagnostics.push({
148
+ severity: "warning",
149
+ code: "dependency_manifest_invalid",
150
+ message: `Manifest for ${dependency} is not valid JSON.`
151
+ });
152
+ continue;
153
+ }
154
+ manifests.push({
155
+ kind: "dependency",
156
+ packageName: dependency,
157
+ path: relativePath(projectRoot, manifestPath),
158
+ objectCount: registerManifest(manifest, dependency)
159
+ });
160
+ }
161
+ bootedProjectRoot = projectRoot;
162
+ if (projectManifestPath) {
163
+ bootedProjectManifestPath = projectManifestPath;
164
+ try {
165
+ bootedProjectManifestMtimeMs = statSync(projectManifestPath).mtimeMs;
166
+ } catch {
167
+ bootedProjectManifestMtimeMs = null;
168
+ }
169
+ }
170
+ booted = {
171
+ provenance: DECLARED_PROVENANCE,
172
+ bootedAt: (options.now ?? /* @__PURE__ */ new Date()).toISOString(),
173
+ projectName: projectPackageName ?? basename(projectRoot),
174
+ manifests,
175
+ objectCount: manifests.reduce((sum, m) => sum + m.objectCount, 0),
176
+ diagnostics
177
+ };
178
+ return booted;
179
+ }
180
+ function readProjectPackageName(projectRoot) {
181
+ try {
182
+ const parsed = JSON.parse(readFileSync(join(projectRoot, "package.json"), "utf8"));
183
+ return typeof parsed.name === "string" && parsed.name ? parsed.name : null;
184
+ } catch {
185
+ return null;
186
+ }
187
+ }
188
+ //#endregion
189
+ //#region src/tools/runtime/connection.ts
190
+ /**
191
+ * Optional read-only dev-database connection resolution for runtime
192
+ * diagnostics tools (#1824).
193
+ *
194
+ * Resolution order per call:
195
+ * 1. explicit `dbUrl`/`dbType` tool arguments
196
+ * 2. `SMRT_DEV_DB_URL` environment variable
197
+ * 3. the project's cosmiconfig CLI section (`getPackageConfig('cli', ...)`
198
+ * from `@happyvertical/smrt-config`) → `database.{type,url}`
199
+ *
200
+ * No configured connection → `db: null` with `source: 'none'`; callers return
201
+ * a successful static-only envelope. A connection is always opened lazily per
202
+ * call and closed in the caller's `finally` — nothing is cached across calls
203
+ * and the server never holds a database handle.
204
+ *
205
+ * Sensitive handling: connection strings are never logged or echoed. Every
206
+ * surfaced URL passes through {@link redactConnectionString}; driver errors
207
+ * are surfaced only through {@link safeErrorMessage}, which strips anything
208
+ * that looks like a credential-bearing URL.
209
+ */
210
+ var RUNTIME_DATABASE_TYPES = [
211
+ "sqlite",
212
+ "postgres",
213
+ "duckdb"
214
+ ];
215
+ function isRuntimeDatabaseType(value) {
216
+ return RUNTIME_DATABASE_TYPES.includes(value);
217
+ }
218
+ /**
219
+ * Sensitive query-parameter names. Matching normalizes the key (lowercase,
220
+ * `_`/`-` stripped), so camelCase (`authToken`, `accessToken`) and hyphen
221
+ * variants (`api-key`) are masked exactly like their snake_case forms.
222
+ */
223
+ var SENSITIVE_QUERY_PARAMS = [
224
+ "access_token",
225
+ "apikey",
226
+ "api_key",
227
+ "auth",
228
+ "auth_token",
229
+ "connectionstring",
230
+ "connection_string",
231
+ "password",
232
+ "token"
233
+ ];
234
+ function normalizeQueryParamName(key) {
235
+ return key.toLowerCase().replace(/[_-]/g, "");
236
+ }
237
+ var SENSITIVE_QUERY_PARAM_NAMES = new Set(SENSITIVE_QUERY_PARAMS.map(normalizeQueryParamName));
238
+ var DEFAULT_CLI_DATABASE = { database: {
239
+ type: "sqlite",
240
+ url: ":memory:"
241
+ } };
242
+ /**
243
+ * Redact a connection string so it can be shown to an agent without leaking
244
+ * credentials. Mirrors the CLI's `redactConnectionString` (which is CLI
245
+ * private); patterned identically so dev-mcp never depends on the CLI.
246
+ *
247
+ * Query-parameter masking normalizes each key (lowercase, `_`/`-` stripped),
248
+ * so camelCase forms such as Turso/libsql's `?authToken=` mask exactly like
249
+ * their snake_case forms. A final regex pass also masks `key=value` pairs
250
+ * embedded in free text (driver error messages often quote the URL); it treats
251
+ * the start of the string, `?`, `&`, `,`, `(`, and whitespace as the
252
+ * preceding boundary.
253
+ */
254
+ function redactConnectionString(value) {
255
+ let redacted = value;
256
+ try {
257
+ const url = new URL(value);
258
+ if (url.password) url.password = "***";
259
+ for (const key of [...url.searchParams.keys()]) if (SENSITIVE_QUERY_PARAM_NAMES.has(normalizeQueryParamName(key))) url.searchParams.set(key, "***");
260
+ redacted = url.toString();
261
+ } catch {
262
+ redacted = value.replace(/([a-z][a-z0-9+.-]*:\/\/[^:\s/@]+:)(?:[^@\s]|@(?=[^@\s]*@))+(@)/gi, "$1***$2");
263
+ }
264
+ redacted = redacted.replace(/(?:[A-Za-z]:)?(?:[\\/][^\s\\/'"`]+)+[\\/]([^\s\\/'"`]+\.(?:db|sqlite3?|duckdb))/g, "…/$1");
265
+ return redacted.replace(/((?:^|[?&,(\s])([a-z][a-z0-9_-]{0,30})=)([^&,\s)]+)/gi, (match, prefix, key) => SENSITIVE_QUERY_PARAM_NAMES.has(normalizeQueryParamName(key)) ? `${prefix}***` : match);
266
+ }
267
+ /**
268
+ * Build a safe, redacted error message for a database failure. Connection
269
+ * strings and raw driver error objects are never surfaced verbatim.
270
+ */
271
+ function safeErrorMessage(error) {
272
+ return redactConnectionString(error instanceof Error ? error.message : String(error ?? "unknown error"));
273
+ }
274
+ /**
275
+ * Normalize a type hint into an engine `getDatabase` accepts. Unknown values
276
+ * throw a safe error (no URL is included) so the caller can surface a
277
+ * diagnostic instead of silently opening the wrong adapter.
278
+ */
279
+ function toRuntimeDatabaseType(value) {
280
+ const normalized = value.trim().toLowerCase();
281
+ if (isRuntimeDatabaseType(normalized)) return normalized;
282
+ throw new Error(`Unsupported runtime database type "${normalized}"; expected sqlite, postgres, or duckdb`);
283
+ }
284
+ /** Infer an engine hint from a URL scheme when no explicit type is given. */
285
+ function inferDatabaseType(url, hint) {
286
+ if (hint && hint.trim().length > 0) return hint;
287
+ if (/^postgres(ql)?:/i.test(url)) return "postgres";
288
+ if (/^duckdb:/i.test(url)) return "duckdb";
289
+ return "sqlite";
290
+ }
291
+ /**
292
+ * Normalize the accepted dev-database URL forms to what `getDatabase` opens.
293
+ *
294
+ * `@happyvertical/sql` understands `file:` URLs and bare paths for SQLite,
295
+ * but not a `sqlite:` scheme; passing `sqlite:///abs/dev.db` through verbatim
296
+ * made the driver treat the whole string as a relative filename and join it
297
+ * to cwd. Accepted forms:
298
+ *
299
+ * - `sqlite:///abs/path.db`, `sqlite://rel/path.db`, `sqlite:path.db` →
300
+ * `file:` URL with the path resolved against cwd only when it is relative
301
+ * - `file:` URLs, `postgres:`/`postgresql:`, `duckdb:` → unchanged
302
+ * - a bare path → unchanged (the driver resolves it)
303
+ */
304
+ /**
305
+ * Whether a configured value means "in-memory, so not a runtime database":
306
+ * `:memory:` as well as the `sqlite::memory:` / `sqlite://:memory:` spellings.
307
+ */
308
+ function isMemoryDatabaseUrl(url) {
309
+ return /^(?:sqlite:(?:\/\/)?)?:memory:$/i.test(url.trim());
310
+ }
311
+ function normalizeDatabaseUrl(url) {
312
+ const match = /^sqlite:(?:\/\/)?(.*)$/i.exec(url.trim());
313
+ if (!match) return url.trim();
314
+ const path = match[1];
315
+ if (!path) return url.trim();
316
+ return pathToFileURL(path.startsWith("/") ? path : resolve(process.cwd(), path)).href;
317
+ }
318
+ /**
319
+ * Resolve the dev-database connection for one tool call.
320
+ *
321
+ * Returns `db: null` (never throws) when no connection is configured; callers
322
+ * must treat that as "no runtime database" and return a static-only envelope.
323
+ * A thrown connect error is propagated to the caller, which converts it into
324
+ * a diagnostic envelope — it must never reach the MCP transport.
325
+ */
326
+ async function resolveRuntimeConnection(args = {}) {
327
+ const argUrl = args.dbUrl?.trim();
328
+ if (argUrl && !isMemoryDatabaseUrl(argUrl)) {
329
+ const databaseType = toRuntimeDatabaseType(inferDatabaseType(argUrl, args.dbType));
330
+ return {
331
+ db: await getDatabaseInstance({
332
+ type: databaseType,
333
+ url: normalizeDatabaseUrl(argUrl)
334
+ }),
335
+ source: "argument",
336
+ displayUrl: redactConnectionString(argUrl),
337
+ databaseType
338
+ };
339
+ }
340
+ const envUrl = process.env.SMRT_DEV_DB_URL?.trim();
341
+ if (envUrl && !isMemoryDatabaseUrl(envUrl)) {
342
+ const databaseType = toRuntimeDatabaseType(inferDatabaseType(envUrl, args.dbType));
343
+ return {
344
+ db: await getDatabaseInstance({
345
+ type: databaseType,
346
+ url: normalizeDatabaseUrl(envUrl)
347
+ }),
348
+ source: "environment",
349
+ displayUrl: redactConnectionString(envUrl),
350
+ databaseType
351
+ };
352
+ }
353
+ const config = await loadCliDatabaseConfig();
354
+ const configUrl = config?.database?.url?.trim();
355
+ if (configUrl && !isMemoryDatabaseUrl(configUrl)) {
356
+ const databaseType = toRuntimeDatabaseType(config.database?.type || inferDatabaseType(configUrl, args.dbType));
357
+ return {
358
+ db: await getDatabaseInstance({
359
+ type: databaseType,
360
+ url: normalizeDatabaseUrl(configUrl)
361
+ }),
362
+ source: "config",
363
+ displayUrl: redactConnectionString(configUrl),
364
+ databaseType
365
+ };
366
+ }
367
+ return {
368
+ db: null,
369
+ source: "none",
370
+ displayUrl: "",
371
+ databaseType: null
372
+ };
373
+ }
374
+ async function loadCliDatabaseConfig() {
375
+ try {
376
+ await loadConfig();
377
+ const database = getPackageConfig("cli", DEFAULT_CLI_DATABASE).database;
378
+ if (database && typeof database.url === "string") return { database };
379
+ return {};
380
+ } catch {
381
+ return {};
382
+ }
383
+ }
384
+ async function getDatabaseInstance(options) {
385
+ return getDatabase(options);
386
+ }
387
+ /**
388
+ * Best-effort close of a resolved connection. Never throws; diagnostics must
389
+ * not fail because cleanup hiccuped.
390
+ */
391
+ async function closeRuntimeConnection(db) {
392
+ if (!db || typeof db !== "object") return;
393
+ const closeable = db;
394
+ const close = closeable.close ?? closeable.client?.end ?? closeable.client?.close;
395
+ if (typeof close !== "function") return;
396
+ try {
397
+ await close.call(closeable.close ? closeable : closeable.client);
398
+ } catch {}
399
+ }
400
+ //#endregion
401
+ //#region src/tools/runtime/tools.ts
402
+ /**
403
+ * Runtime diagnostics tools (#1824): read-only views over a project's dev
404
+ * database `_smrt_*` system tables, powered by the shared SELECT-only
405
+ * system-diagnostics reader in `@happyvertical/smrt-core`.
406
+ *
407
+ * Contract:
408
+ * - **Optional connection.** No configured connection returns a successful
409
+ * static-only envelope — the server always starts and static tools are
410
+ * unaffected. A live connection is opened lazily per call and closed in
411
+ * `finally`; nothing is cached across calls.
412
+ * - **Read-only.** Every underlying statement is a bounded SELECT; the reader
413
+ * never selects sensitive columns (job payloads/results, schedule
414
+ * `agentConfig`/`methodArgs`, dispatch `payload`/`metadata`).
415
+ * - **Provenance-labeled.** Live results carry `provenance: 'runtime (live DB)'`;
416
+ * static-only results carry `provenance: 'static'` — agents must never
417
+ * conflate runtime facts with declared/manifest facts.
418
+ * - **Fail-safe.** A connect/read error becomes a diagnostic envelope; it must
419
+ * never reach the MCP transport and never includes raw driver text or URLs.
420
+ */
421
+ /** Provenance labels separating runtime facts from static/declared facts. */
422
+ var RUNTIME_PROVENANCE = "runtime (live DB)";
423
+ var STATIC_PROVENANCE = "static";
424
+ /**
425
+ * Serialize resolve → read → close per connection target so overlapping tool
426
+ * calls never close a shared cached handle out from under each other.
427
+ *
428
+ * `@happyvertical/sql`'s `getDatabase` returns a cached handle per URL (no
429
+ * opt-out in its public API). `closeRuntimeConnection` only calls the handle's
430
+ * own `close`/`end`, but the SDK wraps those so a close also evicts the handle
431
+ * from its connection cache. Two concurrent diagnostics calls resolving the
432
+ * same URL would therefore share one handle, with the first finisher closing
433
+ * it mid-read for the second. A per-key promise chain keeps each call's
434
+ * lifecycle private: every call resolves its own view of the connection,
435
+ * performs its read, and only then closes — the next queued call re-resolves
436
+ * a fresh handle.
437
+ *
438
+ * The key is a digest of the resolved target, never the raw URL, so a
439
+ * credential-bearing connection string is not retained in this map.
440
+ */
441
+ var runtimeReadQueues = /* @__PURE__ */ new Map();
442
+ function connectionQueueKey(args) {
443
+ const target = args.dbUrl?.trim() || process.env.SMRT_DEV_DB_URL?.trim() || "cli.config";
444
+ return createHash("sha256").update(target).digest("hex");
445
+ }
446
+ async function enqueueRuntimeRead(key, operation) {
447
+ const run = (runtimeReadQueues.get(key) ?? Promise.resolve()).then(operation, operation);
448
+ const tail = run.catch(() => void 0);
449
+ runtimeReadQueues.set(key, tail);
450
+ tail.then(() => {
451
+ if (runtimeReadQueues.get(key) === tail) runtimeReadQueues.delete(key);
452
+ });
453
+ return run;
454
+ }
455
+ /**
456
+ * Run one read against the optional runtime connection, mapping every outcome
457
+ * to a successful MCP envelope:
458
+ *
459
+ * - no connection configured → static-only envelope (`connected: false`)
460
+ * - connect failure → static envelope with a safe diagnostic
461
+ * - read failure → connected envelope with a safe diagnostic
462
+ * - success → live result under `provenance: 'runtime (live DB)'`; a
463
+ * category-unavailable reader result keeps its `available: false` data and
464
+ * surfaces its message as a diagnostic
465
+ */
466
+ async function withRuntimeConnection(args, read, staticHint) {
467
+ return enqueueRuntimeRead(connectionQueueKey(args), () => runWithRuntimeConnection(args, read, staticHint));
468
+ }
469
+ async function runWithRuntimeConnection(args, read, staticHint) {
470
+ let resolved;
471
+ try {
472
+ resolved = await resolveRuntimeConnection(args);
473
+ } catch (error) {
474
+ return {
475
+ ok: true,
476
+ coverage: null,
477
+ diagnostics: [{
478
+ severity: "warning",
479
+ code: "runtime_connection_error",
480
+ message: safeErrorMessage(error)
481
+ }],
482
+ data: {
483
+ provenance: STATIC_PROVENANCE,
484
+ connected: false
485
+ }
486
+ };
487
+ }
488
+ if (!resolved.db) return {
489
+ ok: true,
490
+ coverage: null,
491
+ diagnostics: [{
492
+ severity: "info",
493
+ code: "runtime_connection_unavailable",
494
+ message: `No runtime dev database configured (set SMRT_DEV_DB_URL or cli.database); returning static-only result: ${staticHint}. Static tools are unaffected.`
495
+ }],
496
+ data: {
497
+ provenance: STATIC_PROVENANCE,
498
+ connected: false
499
+ }
500
+ };
501
+ const { db, source, displayUrl, databaseType } = resolved;
502
+ try {
503
+ const { data, diagnostics } = await read(db);
504
+ return {
505
+ ok: true,
506
+ coverage: null,
507
+ diagnostics,
508
+ data: {
509
+ provenance: RUNTIME_PROVENANCE,
510
+ connected: true,
511
+ connectionSource: source,
512
+ databaseType,
513
+ displayUrl,
514
+ ...data
515
+ }
516
+ };
517
+ } catch (error) {
518
+ return {
519
+ ok: true,
520
+ coverage: null,
521
+ diagnostics: [{
522
+ severity: "warning",
523
+ code: "runtime_read_error",
524
+ message: safeErrorMessage(error)
525
+ }],
526
+ data: {
527
+ provenance: RUNTIME_PROVENANCE,
528
+ connected: true,
529
+ connectionSource: source,
530
+ databaseType,
531
+ displayUrl
532
+ }
533
+ };
534
+ } finally {
535
+ await closeRuntimeConnection(db);
536
+ }
537
+ }
538
+ /**
539
+ * Stored error columns (`error_message`, `last_error`) are free text written
540
+ * at failure time and routinely quote connection URLs or credentials. Every
541
+ * string in a live result passes through {@link redactConnectionString}
542
+ * before it reaches an MCP client; structure and non-string values are kept.
543
+ */
544
+ function redactStrings(value) {
545
+ if (typeof value === "string") return redactConnectionString(value);
546
+ if (Array.isArray(value)) return value.map((item) => redactStrings(item));
547
+ if (value !== null && typeof value === "object") {
548
+ const out = {};
549
+ for (const [key, item] of Object.entries(value)) out[key] = redactStrings(item);
550
+ return out;
551
+ }
552
+ return value;
553
+ }
554
+ /** Convert a reader result into envelope data + diagnostics. */
555
+ function toEnvelopeParts(rawResult) {
556
+ const result = redactStrings(rawResult);
557
+ if (result !== null && typeof result === "object" && "available" in result && result.available === false) {
558
+ const unavailable = result;
559
+ const { message, detail, ...rest } = unavailable;
560
+ return {
561
+ data: rest,
562
+ diagnostics: [{
563
+ severity: unavailable.reason === "retired" ? "info" : "warning",
564
+ code: `category_unavailable_${String(unavailable.reason).replace(/-/g, "_")}`,
565
+ message: detail ? `${String(message)} Cause: ${detail}` : String(message)
566
+ }]
567
+ };
568
+ }
569
+ const data = result;
570
+ const diagnostics = [];
571
+ const behind = Array.isArray(data.schemaBehind) ? data.schemaBehind : [];
572
+ for (const entry of behind) {
573
+ const columns = Array.isArray(entry.missingColumns) ? entry.missingColumns.map(String) : [];
574
+ if (columns.length === 0) continue;
575
+ diagnostics.push({
576
+ severity: "info",
577
+ code: "schema_behind",
578
+ message: `${String(entry.tableName ?? "table")} predates ${columns.join(", ")}; those fields are reported as null. Run db:migrate to bring the system tables current.`
579
+ });
580
+ }
581
+ return {
582
+ data,
583
+ diagnostics
584
+ };
585
+ }
586
+ function readToParts(read) {
587
+ return read.then((result) => toEnvelopeParts(result));
588
+ }
589
+ async function runtimeMigrationStatus(args = {}) {
590
+ const { limit, ...connectionArgs } = args;
591
+ return withRuntimeConnection(connectionArgs, (db) => readToParts(readMigrationStatus(db, { limit })), "no migration status — the manifest still reports the declared schema");
592
+ }
593
+ async function runtimeJobHealth(args = {}) {
594
+ const { limit, ...connectionArgs } = args;
595
+ return withRuntimeConnection(connectionArgs, (db) => readToParts(readJobHealth(db, { limit })), "no job health snapshot — the manifest still reports declared job queues");
596
+ }
597
+ async function runtimeScheduleHealth(args = {}) {
598
+ const { limit, ...connectionArgs } = args;
599
+ return withRuntimeConnection(connectionArgs, (db) => readToParts(readScheduleHealth(db, { limit })), "no schedule health snapshot — the manifest still reports declared schedules");
600
+ }
601
+ async function runtimeDispatchHealth(args = {}) {
602
+ const { limit, ...connectionArgs } = args;
603
+ return withRuntimeConnection(connectionArgs, (db) => readToParts(readDispatchHealth(db, { limit })), "no dispatch health snapshot — the manifest still reports declared dispatch topology");
604
+ }
605
+ async function runtimeRecentChanges(args = {}) {
606
+ const { since, tables, tenantId, limit, ...connectionArgs } = args;
607
+ return withRuntimeConnection(connectionArgs, (db) => readToParts(readRecentChanges(db, {
608
+ since,
609
+ tables,
610
+ tenantId,
611
+ limit
612
+ })), "no recent changes — static knowledge artifacts are unchanged");
613
+ }
614
+ async function runtimeRegistryDrift(args = {}) {
615
+ return withRuntimeConnection(args, (db) => readToParts(readRegistryDrift(db)), "no registry drift report — _smrt_registry is retired; declared objects come from the manifest");
616
+ }
617
+ //#endregion
618
+ //#region src/tools/runtime/observation.ts
619
+ /**
620
+ * Level 2 read-only observation tools over the booted runtime (#1831).
621
+ *
622
+ * Three facts planes, labelled separately in every envelope:
623
+ * - `declared (manifest)`: what the confined boot registered ({@link bootRuntime});
624
+ * - `booted (registry)`: the in-process `ObjectRegistry` projected through the
625
+ * sanitized {@link snapshotRegistry} DTO;
626
+ * - `runtime (live DB)`: the optional read-only connection, reused from Level 1.
627
+ *
628
+ * Nothing here mutates: no writes, no `do()`, no generated CRUD, no project
629
+ * code execution. `runtime-schema-diff` only *introspects* the live schema.
630
+ */
631
+ /** Row budget for `runtime-schema-diff` change lists. */
632
+ var SCHEMA_DIFF_CHANGE_LIMIT = 200;
633
+ function bootDiagnostics(boot) {
634
+ return boot.diagnostics.filter((d) => d.severity !== "info").map((d) => ({
635
+ severity: d.severity === "error" ? "warning" : d.severity,
636
+ code: `boot_${d.code}`,
637
+ message: d.message
638
+ }));
639
+ }
640
+ var bootPreambleSent = false;
641
+ /**
642
+ * The full manifest list is emitted once per process; later envelopes carry
643
+ * only the identifying fields. Every runtime response re-embedding the same
644
+ * multi-manifest block was pure repetition for an agent.
645
+ */
646
+ function bootSummary(boot) {
647
+ const compact = {
648
+ provenance: boot.provenance,
649
+ bootedAt: boot.bootedAt,
650
+ projectName: boot.projectName,
651
+ objectCount: boot.objectCount,
652
+ manifestCount: boot.manifests.length
653
+ };
654
+ if (bootPreambleSent) return compact;
655
+ bootPreambleSent = true;
656
+ return {
657
+ ...compact,
658
+ manifests: boot.manifests
659
+ };
660
+ }
661
+ function stalenessDiagnostics() {
662
+ const staleness = getBootStaleness();
663
+ if (!staleness.stale) return [];
664
+ return [{
665
+ severity: "warning",
666
+ code: "manifest_newer_than_boot",
667
+ message: `The project manifest changed at ${staleness.manifestModifiedAt} after this process booted at ${staleness.bootedAt}; restart the server to observe the rebuilt registry.`
668
+ }];
669
+ }
670
+ function objectKey(object) {
671
+ return object.qualifiedName ?? object.name;
672
+ }
673
+ /** `runtime-registry`: sanitized snapshot of the booted registry. */
674
+ async function runtimeRegistry(args = {}) {
675
+ const boot = await bootRuntime({ projectRoot: args.projectPath });
676
+ const snapshot = snapshotRegistry({
677
+ projectRoot: getBootedProjectRoot() ?? void 0,
678
+ objects: args.objects,
679
+ detail: args.detail ?? Boolean(args.objects?.length)
680
+ });
681
+ const limit = Math.min(Math.max(Math.floor(args.limit ?? 50), 1), 500);
682
+ const cursor = typeof args.cursor === "string" && args.cursor.length > 0 ? args.cursor : null;
683
+ const all = snapshot.objects;
684
+ const afterCursor = cursor ? all.filter((object) => objectKey(object).localeCompare(cursor) > 0) : all;
685
+ const objects = afterCursor.slice(0, limit);
686
+ const nextCursor = afterCursor.length > objects.length && objects.length > 0 ? objectKey(objects[objects.length - 1]) : null;
687
+ return {
688
+ ok: true,
689
+ coverage: null,
690
+ diagnostics: [...bootDiagnostics(boot), ...stalenessDiagnostics()],
691
+ data: {
692
+ provenance: snapshot.provenance,
693
+ boot: bootSummary(boot),
694
+ page: {
695
+ returned: objects.length,
696
+ matched: all.length,
697
+ limit,
698
+ cursor,
699
+ nextCursor
700
+ },
701
+ snapshot: {
702
+ ...snapshot,
703
+ objects
704
+ }
705
+ }
706
+ };
707
+ }
708
+ /** `runtime-object`: one object's sanitized definition plus its generated DDL. */
709
+ async function runtimeObject(args) {
710
+ const boot = await bootRuntime({ projectRoot: args.projectPath });
711
+ const name = typeof args.name === "string" ? args.name.trim() : "";
712
+ const snapshot = snapshotRegistry({
713
+ projectRoot: getBootedProjectRoot() ?? void 0,
714
+ objects: name ? [name] : [],
715
+ detail: true
716
+ });
717
+ const diagnostics = [...bootDiagnostics(boot), ...stalenessDiagnostics()];
718
+ let object = snapshot.objects[0] ?? null;
719
+ if (snapshot.objects.length > 1) {
720
+ object = null;
721
+ diagnostics.push({
722
+ severity: "warning",
723
+ code: "object_ambiguous",
724
+ message: `${name} is registered by several packages; pass a qualified name: ${snapshot.objects.map((candidate) => candidate.qualifiedName ?? candidate.name).join(", ")}`
725
+ });
726
+ } else if (!object) diagnostics.push({
727
+ severity: "warning",
728
+ code: "object_not_found",
729
+ message: name ? `No booted object named ${name}; use runtime-registry to list names.` : "name is required."
730
+ });
731
+ let ddl = null;
732
+ if (object) try {
733
+ ddl = ObjectRegistry.getSchemaDDL(object.qualifiedName ?? object.name, args.engine) ?? null;
734
+ } catch (error) {
735
+ diagnostics.push({
736
+ severity: "warning",
737
+ code: "ddl_unavailable",
738
+ message: `Generated DDL unavailable: ${error instanceof Error ? error.message : "unknown error"}`
739
+ });
740
+ }
741
+ return {
742
+ ok: true,
743
+ coverage: null,
744
+ diagnostics,
745
+ data: {
746
+ provenance: snapshot.provenance,
747
+ boot: bootSummary(boot),
748
+ object,
749
+ ddl
750
+ }
751
+ };
752
+ }
753
+ /**
754
+ * `runtime-schema-diff`: booted registry schemas versus the live database,
755
+ * using the same comparer `db:diff`/`db:migrate` use. Introspection only —
756
+ * drop/relax options are pinned off and nothing is executed.
757
+ */
758
+ async function runtimeSchemaDiff(args = {}) {
759
+ const boot = await bootRuntime({ projectRoot: args.projectPath });
760
+ const envelope = await withRuntimeConnection(args, async (db) => {
761
+ const diff = await new SchemaComparer(db, {
762
+ includeDroppedTables: false,
763
+ includeDroppedColumns: false,
764
+ includeDroppedIndexes: false,
765
+ relaxColumns: false
766
+ }).compare(ObjectRegistry.getAllSchemasAsDefinitions());
767
+ const byType = {};
768
+ for (const change of diff.changes) {
769
+ const type = String(change.type ?? "unknown");
770
+ byType[type] = (byType[type] ?? 0) + 1;
771
+ }
772
+ const addedTables = diff.added_tables.map((t) => t.tableName);
773
+ if (addedTables.length > 0) byType.add_table = addedTables.length;
774
+ if (diff.dropped_tables.length > 0) byType.drop_table = diff.dropped_tables.length;
775
+ const changeCount = diff.changes.length + addedTables.length + diff.dropped_tables.length;
776
+ return {
777
+ data: {
778
+ boot: bootSummary(boot),
779
+ hasChanges: diff.has_changes,
780
+ addedTables,
781
+ droppedTables: diff.dropped_tables,
782
+ orphanTables: diff.orphan_tables ?? [],
783
+ changeCount,
784
+ columnChangeCount: diff.changes.length,
785
+ changesByType: byType,
786
+ changes: diff.changes.slice(0, SCHEMA_DIFF_CHANGE_LIMIT),
787
+ truncated: diff.changes.length > SCHEMA_DIFF_CHANGE_LIMIT
788
+ },
789
+ diagnostics: []
790
+ };
791
+ }, "booted registry schemas only; connect a dev database to diff against live tables");
792
+ envelope.diagnostics = [
793
+ ...bootDiagnostics(boot),
794
+ ...stalenessDiagnostics(),
795
+ ...envelope.diagnostics
796
+ ];
797
+ return envelope;
798
+ }
799
+ //#endregion
800
+ export { STATIC_PROVENANCE as a, runtimeMigrationStatus as c, runtimeScheduleHealth as d, redactConnectionString as f, RUNTIME_PROVENANCE as i, runtimeRecentChanges as l, bootRuntime as m, runtimeRegistry as n, runtimeDispatchHealth as o, safeErrorMessage as p, runtimeSchemaDiff as r, runtimeJobHealth as s, runtimeObject as t, runtimeRegistryDrift as u };
801
+
802
+ //# sourceMappingURL=observation-VJgHaPps.js.map