@warlock.js/devtools 5.32.0 → 5.34.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 (41) hide show
  1. package/CHANGELOG.md +9 -12
  2. package/cjs/ai-source-lia4YS30.cjs +212 -0
  3. package/cjs/ai-source-lia4YS30.cjs.map +1 -0
  4. package/cjs/index.cjs +2 -727
  5. package/cjs/{server-CUdjfOkO.cjs → server-BNlyhwiR.cjs} +60 -1
  6. package/cjs/server-BNlyhwiR.cjs.map +1 -0
  7. package/cjs/src-CiNStw0i.cjs +699 -0
  8. package/cjs/src-CiNStw0i.cjs.map +1 -0
  9. package/esm/devtools-connector.d.mts.map +1 -1
  10. package/esm/devtools-connector.mjs +6 -0
  11. package/esm/devtools-connector.mjs.map +1 -1
  12. package/esm/index.d.mts +2 -1
  13. package/esm/process-panel.mjs +16 -0
  14. package/esm/process-panel.mjs.map +1 -0
  15. package/esm/server/ai-routes.mjs +32 -0
  16. package/esm/server/ai-routes.mjs.map +1 -0
  17. package/esm/server/index.mjs +4 -0
  18. package/esm/server/index.mjs.map +1 -1
  19. package/esm/server/process-routes.mjs +11 -0
  20. package/esm/server/process-routes.mjs.map +1 -0
  21. package/esm/server/requests-routes.mjs +1 -0
  22. package/esm/server/requests-routes.mjs.map +1 -1
  23. package/esm/server/stream-route.mjs +4 -0
  24. package/esm/server/stream-route.mjs.map +1 -1
  25. package/esm/sources/ai-source.d.mts +29 -0
  26. package/esm/sources/ai-source.d.mts.map +1 -0
  27. package/esm/sources/ai-source.mjs +136 -0
  28. package/esm/sources/ai-source.mjs.map +1 -0
  29. package/esm/sources/log-source.mjs +1 -1
  30. package/esm/sources/query-source.mjs +1 -1
  31. package/esm/types.d.mts +4 -0
  32. package/esm/types.d.mts.map +1 -1
  33. package/llms-full.txt +85 -1
  34. package/llms.txt +1 -1
  35. package/package.json +9 -5
  36. package/skills/devtools-overview/SKILL.md +85 -1
  37. package/ui/app.css +13 -0
  38. package/ui/app.js +213 -16
  39. package/ui/index.html +2 -0
  40. package/cjs/index.cjs.map +0 -1
  41. package/cjs/server-CUdjfOkO.cjs.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"src-CiNStw0i.cjs","names":["requestContext","LogChannel","toSafeValue","fileURLToPath","applyRedact","log","toSafeValue","toSafeValue","attachAiSource"],"sources":["../../../../../../../packages/devtools/src/process-panel.ts","../../../../../../../packages/devtools/src/n-plus-one.ts","../../../../../../../packages/devtools/src/query-shape.ts","../../../../../../../packages/devtools/src/collector.ts","../../../../../../../packages/devtools/src/options.ts","../../../../../../../packages/devtools/src/current-request-id.ts","../../../../../../../packages/devtools/src/sources/cache-source.ts","../../../../../../../packages/devtools/src/sources/log-source.ts","../../../../../../../packages/devtools/src/sources/mail-source.ts","../../../../../../../packages/devtools/src/caller-location.ts","../../../../../../../packages/devtools/src/sources/query-source.ts","../../../../../../../packages/devtools/src/sources/tracing-source.ts","../../../../../../../packages/devtools/src/devtools-connector.ts"],"sourcesContent":["/** Structural contract keeps process supervision an optional application feature. */\nexport type ProcessPanel = { list(): Promise<readonly unknown[]> };\n\n/** Load app-owned generated registration only when devtools boots in development. */\nexport async function loadProcessPanel(): Promise<ProcessPanel | undefined> {\n const core = await import(\"@warlock.js/core\");\n if (!(\"srcPath\" in core)) return undefined;\n const { srcPath } = core;\n const { existsSync } = await import(\"node:fs\");\n const { pathToFileURL } = await import(\"node:url\");\n const file = srcPath(\"app/processes/devtools.ts\");\n if (!existsSync(file)) return undefined;\n const module: { processPanel?: ProcessPanel } = await import(pathToFileURL(file).href);\n return module.processPanel;\n}\n","import type { DevtoolsCallerLocation, DevtoolsQuery, DevtoolsWarning } from \"./types\";\n\n/** Finds repeated query shapes whose occurrence count reaches the supplied threshold. */\nexport function detectNPlusOne(queries: DevtoolsQuery[], threshold: number): DevtoolsWarning[] {\n const groups = new Map<string, DevtoolsQuery[]>();\n for (const query of queries) groups.set(query.shape, [...(groups.get(query.shape) ?? []), query]);\n\n return [...groups.entries()]\n .filter(([, grouped]) => grouped.length >= threshold)\n .sort(([, left], [, right]) => right.length - left.length)\n .map(([shape, grouped]) => ({\n type: \"n+1\" as const,\n shape,\n count: grouped.length,\n message: `N+1: ${grouped.length}× \"${truncate(shape, 120)}\"`,\n ...(mostFrequentCaller(grouped) ? { caller: mostFrequentCaller(grouped) } : {}),\n }));\n}\n\nfunction truncate(value: string, maximum: number): string {\n return value.length > maximum ? `${value.slice(0, maximum - 1)}…` : value;\n}\n\nfunction mostFrequentCaller(queries: DevtoolsQuery[]): DevtoolsCallerLocation | undefined {\n const callers = new Map<string, { caller: DevtoolsCallerLocation; count: number }>();\n for (const query of queries) {\n if (!query.caller) continue;\n const key = `${query.caller.file}:${query.caller.line}`;\n const found = callers.get(key);\n callers.set(\n key,\n found ? { ...found, count: found.count + 1 } : { caller: query.caller, count: 1 },\n );\n }\n return [...callers.values()].sort((left, right) => right.count - left.count)[0]?.caller;\n}\n","import type { DevtoolsQuery } from \"./types\";\n\ntype QueryShapeInput = Pick<\n DevtoolsQuery,\n \"driver\" | \"sql\" | \"collection\" | \"command\" | \"pipeline\"\n>;\n\nconst numericLiteral = /(?<![\\w$])[-+]?(?:\\d+(?:\\.\\d+)?|\\.\\d+)(?![\\w$])/g;\nconst quotedLiteral = /'(?:''|[^'])*'/g;\n\n/** Returns a binding-independent shape for a database query. */\nexport function queryShape(query: QueryShapeInput): string {\n if (query.driver === \"postgres\") {\n return (query.sql ?? \"\")\n .replace(quotedLiteral, \"?\")\n .replace(numericLiteral, \"?\")\n .replace(/\\s+/g, \" \")\n .trim();\n }\n\n return `${query.collection ?? \"\"}.${query.command ?? \"\"} ${JSON.stringify(normalizeMongoValue(query.pipeline))}`;\n}\n\nfunction normalizeMongoValue(value: unknown): unknown {\n if (isMongoLeaf(value)) return \"?\";\n\n if (Array.isArray(value)) {\n if (value.every(isMongoLeaf)) return [\"?\"];\n return value.map(normalizeMongoValue);\n }\n\n const record = value as Record<string, unknown>;\n return Object.fromEntries(\n Object.keys(record).map((key) => [key, normalizeMongoValue(record[key])]),\n );\n}\n\nfunction isMongoLeaf(value: unknown): boolean {\n if (value === null || typeof value !== \"object\") return true;\n if (value instanceof Date) return true;\n const record = value as Record<string, unknown>;\n return typeof record.toHexString === \"function\" || typeof record.$oid === \"string\";\n}\n","import type {\n DevtoolsBackground,\n DevtoolsCollector,\n DevtoolsDisposer,\n DevtoolsLogEntry,\n DevtoolsMail,\n DevtoolsMailSummary,\n DevtoolsQuery,\n DevtoolsRequest,\n DevtoolsRequestSummary,\n DevtoolsStreamEvent,\n ResolvedDevtoolsOptions,\n} from \"./types\";\nimport { detectNPlusOne } from \"./n-plus-one\";\nimport { queryShape } from \"./query-shape\";\n\nconst mailLimit = 100;\nconst logLimit = 1000;\nconst backgroundLimit = 500;\n\n/**\n * Creates the in-memory devtools store.\n *\n * Read methods return shallow structural copies, including copied arrays, so consumers\n * cannot mutate collector-owned arrays. The individual nested entries are retained.\n */\nexport function createCollector(options: ResolvedDevtoolsOptions): DevtoolsCollector {\n const requests = new Map<string, DevtoolsRequest>();\n const mails = new Map<string, DevtoolsMail>();\n const logs: DevtoolsLogEntry[] = [];\n const background: DevtoolsBackground = { queries: [], cache: [], logs: [] };\n const listeners = new Set<(event: DevtoolsStreamEvent) => void>();\n let querySequence = 0;\n\n const emit = (event: DevtoolsStreamEvent) => {\n for (const listener of listeners) {\n try {\n listener(event);\n } catch {\n /* Subscribers must not affect collection. */\n }\n }\n };\n\n const addBackground = <T>(list: T[], value: T) => {\n list.push(value);\n if (list.length > backgroundLimit) list.shift();\n };\n\n return {\n startRequest(request) {\n requests.delete(request.id);\n requests.set(request.id, {\n ...request,\n phases: [],\n queries: [],\n cache: [],\n logs: [],\n mails: [],\n warnings: [],\n });\n while (requests.size > options.maxRequests)\n requests.delete(requests.keys().next().value as string);\n },\n endRequest(id, result) {\n const request = requests.get(id);\n if (!request) return;\n request.status = result.status;\n request.durationMs = result.durationMs;\n request.error = result.error;\n if (result.route !== undefined) request.route = result.route;\n request.warnings = detectNPlusOne(request.queries, options.nPlusOneThreshold);\n emit({ type: \"request\", data: summary(request) });\n },\n addPhase(requestId, phase) {\n requests.get(requestId)?.phases.push(phase);\n },\n addQuery(requestId, query) {\n const stored: DevtoolsQuery = {\n ...query,\n id: `q${++querySequence}`,\n shape: queryShape(query),\n };\n const request = requestId ? requests.get(requestId) : undefined;\n if (request) request.queries.push(stored);\n else addBackground(background.queries, stored);\n return stored;\n },\n addCacheOperation(requestId, operation) {\n const request = requestId ? requests.get(requestId) : undefined;\n if (request) request.cache.push(operation);\n else addBackground(background.cache, operation);\n },\n addLog(requestId, entry) {\n const request = requestId ? requests.get(requestId) : undefined;\n if (request) request.logs.push(entry);\n else addBackground(background.logs, entry);\n logs.unshift(entry);\n if (logs.length > logLimit) logs.pop();\n emit({ type: \"log\", data: entry });\n },\n addMail(mail) {\n mails.delete(mail.id);\n mails.set(mail.id, mail);\n while (mails.size > mailLimit) mails.delete(mails.keys().next().value as string);\n if (mail.requestId) requests.get(mail.requestId)?.mails.push(mail.id);\n emit({ type: \"mail\", data: mailSummary(mail) });\n },\n listRequests() {\n return [...requests.values()].reverse().map(summary);\n },\n getRequest(id) {\n const request = requests.get(id);\n return request && copyRequest(request);\n },\n listMails() {\n return [...mails.values()].reverse().map(mailSummary);\n },\n getMail(id) {\n const mail = mails.get(id);\n return (\n mail && {\n ...mail,\n to: [...mail.to],\n cc: [...mail.cc],\n bcc: [...mail.bcc],\n headers: { ...mail.headers },\n attachments: [...mail.attachments],\n }\n );\n },\n listLogs(filter = {}) {\n const module = filter.module?.toLowerCase();\n return logs\n .filter(\n (entry) =>\n (!filter.level || entry.level === filter.level) &&\n (!module || entry.module.toLowerCase().includes(module)),\n )\n .map((entry) => ({ ...entry }));\n },\n getBackground() {\n return {\n queries: [...background.queries],\n cache: [...background.cache],\n logs: [...background.logs],\n };\n },\n subscribe(listener): DevtoolsDisposer {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n clear() {\n requests.clear();\n mails.clear();\n logs.length = 0;\n background.queries.length = 0;\n background.cache.length = 0;\n background.logs.length = 0;\n },\n };\n}\n\nfunction summary(request: DevtoolsRequest): DevtoolsRequestSummary {\n const { id, method, path, route, status, startedAt, durationMs } = request;\n return {\n id,\n method,\n path,\n route,\n status,\n startedAt,\n durationMs,\n queryCount: request.queries.length,\n warningCount: request.warnings.length,\n };\n}\n\nfunction mailSummary(mail: DevtoolsMail): DevtoolsMailSummary {\n const { id, requestId, capturedAt, to, subject } = mail;\n return { id, requestId, capturedAt, to: [...to], subject };\n}\n\nfunction copyRequest(request: DevtoolsRequest): DevtoolsRequest {\n return {\n ...request,\n phases: [...request.phases],\n queries: [...request.queries],\n cache: [...request.cache],\n logs: [...request.logs],\n mails: [...request.mails],\n warnings: [...request.warnings],\n };\n}\n","import type { DevtoolsOptions, ResolvedDevtoolsOptions } from \"./types\";\n\n/** Resolves dashboard options and rejects invalid capacity or URL-prefix values. */\nexport function resolveDevtoolsOptions(options: DevtoolsOptions = {}): ResolvedDevtoolsOptions {\n const path = (options.path ?? \"/__warlock\").replace(/\\/+$/, \"\");\n const maxRequests = options.maxRequests ?? 200;\n const nPlusOneThreshold = options.nPlusOneThreshold ?? 5;\n\n if (!path || !path.startsWith(\"/\")) {\n throw new Error('Devtools option \"path\" must be a non-root path starting with \"/\".');\n }\n\n if (maxRequests < 1) {\n throw new Error('Devtools option \"maxRequests\" must be at least 1.');\n }\n\n if (nPlusOneThreshold < 2) {\n throw new Error('Devtools option \"nPlusOneThreshold\" must be at least 2.');\n }\n\n return { path, maxRequests, nPlusOneThreshold };\n}\n","import { requestContext } from \"@warlock.js/core\";\n\n/**\n * The current request's id (core's `request.id`, threaded through\n * `AsyncLocalStorage`), or `undefined` outside a request — sources feed\n * devtools' background bucket in that case.\n *\n * Only ever reached through `./sources/*`, which the connector imports\n * dynamically inside `boot()` — by then core is already running, so this\n * static import never drags core's runtime graph into config-load time (see\n * `devtools-connector.ts`).\n */\nexport function currentRequestId(): string | undefined {\n try {\n return requestContext.getRequest()?.id;\n } catch {\n return undefined;\n }\n}\n","import { currentRequestId } from \"../current-request-id\";\nimport type { DevtoolsCacheOperation, DevtoolsCollector, DevtoolsDisposer } from \"../types\";\n\n/** Cache events the dashboard's waterfall and background bucket care about. */\nconst EVENT_TYPES = [\"hit\", \"miss\", \"set\", \"removed\", \"invalidated\", \"flushed\"] as const;\n\n/** The shape `cache.on(event, handler)` hands the handler. */\ntype CacheEventData = {\n key?: string;\n driver: string;\n tags?: string[];\n keys?: string[];\n durationMs?: number;\n};\n\n/**\n * Feeds cache hits, misses and mutations, from `@warlock.js/cache`, into the\n * collector. A no-op when `@warlock.js/cache` isn't installed — it is an\n * optional peer.\n */\nexport async function attachCacheSource(collector: DevtoolsCollector): Promise<DevtoolsDisposer> {\n let cache: {\n on(event: (typeof EVENT_TYPES)[number], handler: (data: CacheEventData) => void): unknown;\n off(event: (typeof EVENT_TYPES)[number], handler: (data: CacheEventData) => void): unknown;\n };\n\n try {\n ({ cache } = await import(\"@warlock.js/cache\"));\n } catch {\n return () => {};\n }\n\n const handlers = EVENT_TYPES.map((type) => {\n const handler = (data: CacheEventData) => {\n const operation: DevtoolsCacheOperation = {\n type,\n driver: data.driver,\n key: data.key,\n tags: data.tags,\n keys: data.keys,\n at: Date.now(),\n durationMs: data.durationMs,\n };\n\n collector.addCacheOperation(currentRequestId(), operation);\n };\n\n cache.on(type, handler);\n\n return { type, handler };\n });\n\n return () => {\n for (const { type, handler } of handlers) cache.off(type, handler);\n };\n}\n","import { LogChannel, log, type LoggingData } from \"@warlock.js/logger\";\nimport { currentRequestId } from \"../current-request-id\";\nimport { toSafeValue } from \"../safe-value\";\nimport type { DevtoolsCollector, DevtoolsDisposer, DevtoolsLogEntry } from \"../types\";\n\n/** `LoggingData.type` includes `fatal`, which devtools folds into `error`. */\nfunction toDevtoolsLevel(type: LoggingData[\"type\"]): DevtoolsLogEntry[\"level\"] {\n return type === \"fatal\" ? \"error\" : type;\n}\n\nclass DevtoolsLogChannel extends LogChannel {\n public override name = \"devtools\";\n public active = true;\n\n public constructor(private readonly collector: DevtoolsCollector) {\n super();\n }\n\n public log(data: LoggingData): void {\n if (!this.active) return;\n\n // Already passed through the logger's redaction floor (`Logger.log`\n // applies it before invoking any channel). It is still app data, and can\n // hold live objects (a request, its socket), so a JSON-safe snapshot is\n // kept instead of the object itself.\n const entry: DevtoolsLogEntry = {\n level: toDevtoolsLevel(data.type),\n module: data.module,\n action: data.action,\n message: data.message,\n at: Date.now(),\n context: toSafeValue(data.context),\n };\n\n this.collector.addLog(currentRequestId(), entry);\n }\n}\n\n/**\n * Feeds every log entry, from `@warlock.js/logger`, into the collector by\n * registering an additional channel. `Logger` has no way to remove a\n * channel, so the disposer flag-ignores further entries instead — the\n * channel stays registered for the process's lifetime.\n */\nexport async function attachLogSource(collector: DevtoolsCollector): Promise<DevtoolsDisposer> {\n const channel = new DevtoolsLogChannel(collector);\n\n log.addChannel(channel);\n\n return () => {\n channel.active = false;\n };\n}\n","import type { CapturedMail, MailAddress, MailAttachment } from \"@warlock.js/core\";\nimport { currentRequestId } from \"../current-request-id\";\nimport type { DevtoolsCollector, DevtoolsDisposer, DevtoolsMailAttachment } from \"../types\";\n\nfunction addressToString(address: MailAddress | undefined): string | undefined {\n if (!address) return undefined;\n\n return typeof address === \"string\" ? address : `${address.name} <${address.address}>`;\n}\n\nfunction attachmentSize(attachment: MailAttachment): number | undefined {\n if (typeof attachment.content === \"string\") return Buffer.byteLength(attachment.content);\n if (Buffer.isBuffer(attachment.content)) return attachment.content.length;\n\n return undefined;\n}\n\nfunction toDevtoolsAttachment(attachment: MailAttachment): DevtoolsMailAttachment {\n return {\n filename: attachment.filename,\n contentType: attachment.contentType,\n size: attachmentSize(attachment),\n };\n}\n\n/**\n * Feeds mails captured in development mode, from core's `mailEvents`, into\n * the collector. The captured attachments carry their content, but only\n * metadata is stored — the bytes are never kept.\n */\nexport async function attachMailSource(collector: DevtoolsCollector): Promise<DevtoolsDisposer> {\n const { mailEvents } = await import(\"@warlock.js/core\");\n\n const subscription = mailEvents.onCaptured((mail: CapturedMail) => {\n const { normalized } = mail;\n\n collector.addMail({\n id: mail.id,\n requestId: currentRequestId(),\n capturedAt: mail.timestamp.getTime(),\n from: addressToString(normalized.from),\n to: normalized.to,\n cc: normalized.cc,\n bcc: normalized.bcc,\n subject: normalized.subject,\n html: normalized.html,\n text: normalized.text,\n headers: normalized.headers,\n attachments: normalized.attachments.map(toDevtoolsAttachment),\n });\n });\n\n return () => subscription.unsubscribe();\n}\n","import { fileURLToPath } from \"node:url\";\n\nimport type { DevtoolsCallerLocation } from \"./types\";\n\nconst packageDirectory = normalizePath(fileURLToPath(new URL(\"../\", import.meta.url)));\n\n/** Returns the first non-runtime, non-devtools frame in a V8 stack trace. */\nexport function captureCallerLocation(\n stack: string = new Error().stack ?? \"\",\n): DevtoolsCallerLocation | undefined {\n for (const line of stack.split(\"\\n\")) {\n const frame = parseFrame(line);\n if (!frame || shouldSkip(frame.file)) continue;\n return frame;\n }\n return undefined;\n}\n\nfunction parseFrame(frame: string): DevtoolsCallerLocation | undefined {\n const match = frame.match(/\\bat\\s+(?:.*?\\s+\\()?(.+?):(\\d+):(\\d+)\\)?\\s*$/);\n const [, rawFile, line, column] = match ?? [];\n if (!rawFile || !line || !column) return undefined;\n const file = normalizePath(rawFile);\n return { file, line: Number(line), column: Number(column) };\n}\n\n/** A drive-letter file URL names a Windows path on whichever host decodes it. */\nconst WINDOWS_DRIVE_FILE_URL = /^file:\\/\\/\\/[A-Za-z]:/;\n\nfunction normalizePath(path: string): string {\n const decoded = !path.startsWith(\"file:\")\n ? path\n : WINDOWS_DRIVE_FILE_URL.test(path)\n ? fileURLToPath(path, { windows: true })\n : fileURLToPath(path);\n return decoded.replace(/\\\\/g, \"/\");\n}\n\nfunction shouldSkip(file: string): boolean {\n return (\n file.startsWith(\"node:\") ||\n file.includes(\"/node_modules/\") ||\n // Node's own frames (`node:internal/…`, or bare `internal/…` on older\n // runtimes); an app folder named `internal` is still an app frame.\n file.startsWith(\"internal/\") ||\n file.startsWith(packageDirectory)\n );\n}\n","import { applyRedact, log } from \"@warlock.js/logger\";\nimport { captureCallerLocation } from \"../caller-location\";\nimport { currentRequestId } from \"../current-request-id\";\nimport { toSafeValue } from \"../safe-value\";\nimport type { DevtoolsCollector, DevtoolsDisposer } from \"../types\";\n\n/** The shape of the event `dataSourceRegistry` emits for `\"query\"`. */\ntype QueryEvent = {\n connection: string;\n driver: \"postgres\" | \"mongodb\";\n sql?: string;\n bindings?: unknown[];\n collection?: string;\n command?: string;\n pipeline?: unknown;\n durationMs: number;\n startedAt: number;\n rowCount?: number;\n error?: unknown;\n};\n\n/** Redacts query bindings through the logger's redaction rules. */\nfunction redactBindings(bindings: unknown[] | undefined): unknown[] | undefined {\n if (!bindings) return bindings;\n\n const redacted = applyRedact(\n {\n type: \"debug\",\n module: \"devtools\",\n action: \"query\",\n message: undefined,\n context: { bindings },\n },\n log.getRedact(),\n );\n\n return (redacted.context as { bindings: unknown[] }).bindings;\n}\n\n/**\n * Feeds every settled database query, from cascade's data source registry,\n * into the collector. A no-op when `@warlock.js/cascade` isn't installed —\n * it is an optional peer.\n */\nexport async function attachQuerySource(collector: DevtoolsCollector): Promise<DevtoolsDisposer> {\n let dataSourceRegistry: {\n on(event: \"query\", listener: (event: QueryEvent) => void): void;\n off(event: \"query\", listener: (event: QueryEvent) => void): void;\n };\n\n try {\n ({ dataSourceRegistry } = await import(\"@warlock.js/cascade\"));\n } catch {\n return () => {};\n }\n\n const listener = (event: QueryEvent) => {\n collector.addQuery(currentRequestId(), {\n connection: event.connection,\n driver: event.driver,\n sql: event.sql,\n bindings: toSafeValue(redactBindings(event.bindings)) as unknown[] | undefined,\n collection: event.collection,\n command: event.command,\n pipeline: event.pipeline,\n startedAt: event.startedAt,\n durationMs: event.durationMs,\n rowCount: event.rowCount,\n error:\n event.error === undefined\n ? undefined\n : event.error instanceof Error\n ? event.error.message\n : String(event.error),\n caller: captureCallerLocation(),\n });\n };\n\n dataSourceRegistry.on(\"query\", listener);\n\n return () => dataSourceRegistry.off(\"query\", listener);\n}\n","import type { TracingContext, TracingPhaseInfo, TracingRequestEndInfo } from \"@warlock.js/core\";\nimport { toSafeValue } from \"../safe-value\";\nimport type { DevtoolsCollector, DevtoolsDisposer, ResolvedDevtoolsOptions } from \"../types\";\n\n/**\n * Feeds request timelines from core's tracing hooks into the collector: one\n * `startRequest`/`endRequest` pair per HTTP request, with every named phase\n * in between. Requests to the dashboard's own path are never recorded, so\n * the dashboard never watches itself.\n */\nexport async function attachTracingSource(\n collector: DevtoolsCollector,\n options: ResolvedDevtoolsOptions,\n): Promise<DevtoolsDisposer> {\n const { registerTracingHooks } = await import(\"@warlock.js/core\");\n\n const isDashboardPath = (path: string): boolean => path.startsWith(options.path);\n\n return registerTracingHooks({\n onRequestStart(ctx: TracingContext) {\n if (isDashboardPath(ctx.path)) return;\n\n collector.startRequest({\n id: ctx.requestId,\n method: ctx.method,\n path: ctx.path,\n route: ctx.route,\n startedAt: performance.timeOrigin + performance.now(),\n });\n },\n onPhase(ctx: TracingContext, phase: TracingPhaseInfo) {\n if (isDashboardPath(ctx.path)) return;\n\n collector.addPhase(ctx.requestId, {\n name: phase.name,\n durationMs: phase.durationMs,\n startedAt: phase.startedAt ?? performance.timeOrigin + performance.now() - phase.durationMs,\n attrs:\n phase.attrs === undefined\n ? undefined\n : (toSafeValue(phase.attrs) as Record<string, unknown>),\n });\n },\n onRequestEnd(ctx: TracingContext, result: TracingRequestEndInfo) {\n if (isDashboardPath(ctx.path)) return;\n\n collector.endRequest(ctx.requestId, {\n status: result.status,\n durationMs: result.durationMs,\n error:\n result.error === undefined\n ? undefined\n : result.error instanceof Error\n ? result.error.message\n : String(result.error),\n route: ctx.route,\n });\n },\n });\n}\n","/**\n * The devtools connector for `warlock.config.ts > connectors`.\n *\n * Deliberately a plain object with TYPE-ONLY imports from core: the config\n * file that constructs it must not drag core's runtime graph in at\n * config-load time. Core is imported lazily inside `boot()`, where the app\n * has already loaded it — see `queue/src/queue-connector.ts`, the pattern\n * this mirrors.\n */\nimport type { Connector, ConnectorLifecyclePhase } from \"@warlock.js/core\";\nimport { loadProcessPanel } from \"./process-panel\";\nimport { createCollector } from \"./collector\";\nimport { resolveDevtoolsOptions } from \"./options\";\nimport type { DevtoolsRouteRow } from \"./server\";\nimport { attachAiSource } from \"./sources/ai-source\";\nimport { attachCacheSource } from \"./sources/cache-source\";\nimport { attachLogSource } from \"./sources/log-source\";\nimport { attachMailSource } from \"./sources/mail-source\";\nimport { attachQuerySource } from \"./sources/query-source\";\nimport { attachTracingSource } from \"./sources/tracing-source\";\nimport type { DevtoolsDisposer, DevtoolsOptions, DevtoolsQuery } from \"./types\";\n\n/**\n * Boots after the queue connector (priority 11), which is the last\n * built-in/framework connector to claim a priority; devtools has nothing\n * that depends on it, so it simply goes last.\n */\nexport const DEVTOOLS_CONNECTOR_PRIORITY = 12;\n\n/** Options accepted by `devtoolsConnector()`. */\nexport type DevtoolsConnectorOptions = DevtoolsOptions;\n\n/**\n * Construct the devtools connector.\n *\n * Refuses to run outside development: `boot()` checks\n * `Application.runtimeStrategy` and, when it isn't `\"development\"`, logs one\n * warning and returns without mounting a route or subscribing to anything.\n * In development it attaches every source (tracing, queries, cache, mail,\n * logs), then mounts the dashboard's routes on the HTTP server — before it\n * starts listening, the only point in the boot sequence where mounting is\n * possible (see `queue/src/dashboard-boot.ts` for the same constraint).\n *\n * Apps don't call this: `warlock dev` registers it automatically when the\n * package is installed (`warlock add devtools`). Keep it OUT of\n * `warlock.config.ts` — devtools is a dev dependency, so a production\n * install doesn't have it and a config import of it would fail there.\n */\nexport function devtoolsConnector(options: DevtoolsConnectorOptions = {}): Connector {\n let active = false;\n const disposers: DevtoolsDisposer[] = [];\n\n const connector: Connector = {\n name: \"devtools\",\n priority: DEVTOOLS_CONNECTOR_PRIORITY,\n // Core's `ConnectorLifecyclePhase.Late`; spelled out so this module stays\n // free of a runtime import of core.\n lifecyclePhase: \"late\" as ConnectorLifecyclePhase,\n isActive: () => active,\n async boot() {\n const { Application, getHttpServer } = await import(\"@warlock.js/core\");\n const { log } = await import(\"@warlock.js/logger\");\n\n if (Application.runtimeStrategy !== \"development\") {\n log.warn(\n \"devtools\",\n \"disabled\",\n \"@warlock.js/devtools only runs in development; it is not mounted\",\n );\n return;\n }\n\n const resolved = resolveDevtoolsOptions(options);\n const collector = createCollector(resolved);\n\n await attachSource(log, \"tracing\", () => attachTracingSource(collector, resolved));\n await attachSource(log, \"query\", () => attachQuerySource(collector));\n await attachSource(log, \"cache\", () => attachCacheSource(collector));\n await attachSource(log, \"mail\", () => attachMailSource(collector));\n await attachSource(log, \"log\", () => attachLogSource(collector));\n\n const ai = await attachAiSource(resolved.maxRequests);\n disposers.push(ai.dispose);\n\n const { mountDevtoolsRoutes } = await import(\"./server\");\n const postmanBuilder = await findPostmanBuilder();\n\n mountDevtoolsRoutes(getHttpServer(), {\n collector,\n ai,\n processPanel: await loadProcessPanel(),\n options: resolved,\n isDevelopment: () => Application.runtimeStrategy === \"development\",\n explain: (query) => explainQuery(query),\n listRoutes: () => listRoutes(),\n getOpenApiDocument: () => getOpenApiDocument(),\n getPostmanCollection: postmanBuilder\n ? async () => unwrapCollection(await postmanBuilder())\n : undefined,\n });\n\n // An N+1 is worth a terminal line, not just a badge the developer has\n // to go looking for: one warning per offending query shape, with the\n // line that issued it.\n disposers.push(\n collector.subscribe((event) => {\n if (event.type !== \"request\" || event.data.warningCount === 0) return;\n\n for (const warning of collector.getRequest(event.data.id)?.warnings ?? []) {\n const where = warning.caller ? ` — ${warning.caller.file}:${warning.caller.line}` : \"\";\n\n log.warn(\n \"devtools\",\n \"n+1\",\n `${warning.message}${where} (${event.data.method} ${event.data.path})`,\n );\n }\n }),\n );\n\n active = true;\n\n log.info(\"devtools\", \"ready\", `devtools ready at ${resolved.path}`);\n },\n async start() {},\n async restart() {\n await connector.shutdown();\n await connector.start();\n },\n async shutdown() {\n for (const dispose of disposers.splice(0)) dispose();\n active = false;\n },\n shouldRestart: () => false,\n };\n\n /** Attaches one source, logging and continuing when it fails to attach. */\n async function attachSource(\n logger: { warn: (module: string, action: string, message: string) => unknown },\n name: string,\n attach: () => Promise<DevtoolsDisposer>,\n ): Promise<void> {\n try {\n disposers.push(await attach());\n } catch (error) {\n logger.warn(\n \"devtools\",\n \"source-failed\",\n `devtools' ${name} source failed to attach: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n\n return connector;\n}\n\n/** Runs `EXPLAIN (FORMAT JSON)` for a stored Postgres query on its own connection. */\nasync function explainQuery(query: DevtoolsQuery): Promise<unknown> {\n const { dataSourceRegistry } = await import(\"@warlock.js/cascade\");\n const dataSource = dataSourceRegistry.get(query.connection);\n const { rows } = await dataSource.raw(`EXPLAIN (FORMAT JSON) ${query.sql}`, query.bindings);\n\n return rows;\n}\n\n/** Lists every registered HTTP route — the same source `warlock routes --json` reads. */\nasync function listRoutes(): Promise<DevtoolsRouteRow[]> {\n const { router } = await import(\"@warlock.js/core\");\n\n return router.list().map((route) => ({\n method: route.method.toUpperCase(),\n path: route.path,\n name: route.name,\n middleware: route.middleware?.map((middleware) => middleware.name || \"anonymous\"),\n }));\n}\n\n/**\n * Builds the OpenAPI 3.1 document from the routes registered in this process —\n * the same generator behind `warlock generate.openapi`. Core is imported\n * lazily so this module stays free of a load-time dependency on it.\n */\nasync function getOpenApiDocument(): Promise<object> {\n const { getDevelopmentOpenApiDocument } = await import(\"@warlock.js/core\");\n const { document } = await getDevelopmentOpenApiDocument();\n\n return document;\n}\n\ntype PostmanBuilder = () => Promise<unknown>;\n\n/**\n * Looks up core's Postman collection builder, `getDevelopmentPostmanCollection`.\n * Older cores do not export it; the route then answers 503. The lookup is\n * guarded because a namespace without the export may throw on access. Core is\n * imported lazily, like the OpenAPI document above.\n */\nasync function findPostmanBuilder(): Promise<PostmanBuilder | undefined> {\n try {\n const core: Record<string, unknown> = await import(\"@warlock.js/core\");\n const build = core.getDevelopmentPostmanCollection;\n\n return typeof build === \"function\" ? (build as PostmanBuilder) : undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Core may return the collection itself or wrap it in `{ collection, warnings }`,\n * like the OpenAPI generator's `{ document, warnings }`.\n */\nfunction unwrapCollection(result: unknown): object {\n if (result && typeof result === \"object\" && \"collection\" in result) {\n return (result as { collection: object }).collection;\n }\n\n return result as object;\n}\n"],"mappings":";;;;;;;;AAIA,eAAsB,mBAAsD;CAC1E,MAAM,OAAO,MAAM,OAAO;CAC1B,IAAI,EAAE,aAAa,OAAO,OAAO;CACjC,MAAM,EAAE,YAAY;CACpB,MAAM,EAAE,eAAe,MAAM,OAAO;CACpC,MAAM,EAAE,kBAAkB,MAAM,OAAO;CACvC,MAAM,OAAO,QAAQ,2BAA2B;CAChD,IAAI,CAAC,WAAW,IAAI,GAAG,OAAO;CAE9B,QAAO,MAD+C,OAAO,cAAc,IAAI,CAAC,CAAC,MACpE,CAAC;AAChB;;;;;ACXA,SAAgB,eAAe,SAA0B,WAAsC;CAC7F,MAAM,yBAAS,IAAI,IAA6B;CAChD,KAAK,MAAM,SAAS,SAAS,OAAO,IAAI,MAAM,OAAO,CAAC,GAAI,OAAO,IAAI,MAAM,KAAK,KAAK,CAAC,GAAI,KAAK,CAAC;CAEhG,OAAO,CAAC,GAAG,OAAO,QAAQ,CAAC,CAAC,CACzB,QAAQ,GAAG,aAAa,QAAQ,UAAU,SAAS,CAAC,CACpD,MAAM,GAAG,OAAO,GAAG,WAAW,MAAM,SAAS,KAAK,MAAM,CAAC,CACzD,KAAK,CAAC,OAAO,cAAc;EAC1B,MAAM;EACN;EACA,OAAO,QAAQ;EACf,SAAS,QAAQ,QAAQ,OAAO,KAAK,SAAS,OAAO,GAAG,EAAE;EAC1D,GAAI,mBAAmB,OAAO,IAAI,EAAE,QAAQ,mBAAmB,OAAO,EAAE,IAAI,CAAC;CAC/E,EAAE;AACN;AAEA,SAAS,SAAS,OAAe,SAAyB;CACxD,OAAO,MAAM,SAAS,UAAU,GAAG,MAAM,MAAM,GAAG,UAAU,CAAC,EAAE,KAAK;AACtE;AAEA,SAAS,mBAAmB,SAA8D;CACxF,MAAM,0BAAU,IAAI,IAA+D;CACnF,KAAK,MAAM,SAAS,SAAS;EAC3B,IAAI,CAAC,MAAM,QAAQ;EACnB,MAAM,MAAM,GAAG,MAAM,OAAO,KAAK,GAAG,MAAM,OAAO;EACjD,MAAM,QAAQ,QAAQ,IAAI,GAAG;EAC7B,QAAQ,IACN,KACA,QAAQ;GAAE,GAAG;GAAO,OAAO,MAAM,QAAQ;EAAE,IAAI;GAAE,QAAQ,MAAM;GAAQ,OAAO;EAAE,CAClF;CACF;CACA,OAAO,CAAC,GAAG,QAAQ,OAAO,CAAC,CAAC,CAAC,MAAM,MAAM,UAAU,MAAM,QAAQ,KAAK,KAAK,CAAC,CAAC,EAAE,EAAE;AACnF;;;;AC5BA,MAAM,iBAAiB;AACvB,MAAM,gBAAgB;;AAGtB,SAAgB,WAAW,OAAgC;CACzD,IAAI,MAAM,WAAW,YACnB,QAAQ,MAAM,OAAO,GAAE,CACpB,QAAQ,eAAe,GAAG,CAAC,CAC3B,QAAQ,gBAAgB,GAAG,CAAC,CAC5B,QAAQ,QAAQ,GAAG,CAAC,CACpB,KAAK;CAGV,OAAO,GAAG,MAAM,cAAc,GAAG,GAAG,MAAM,WAAW,GAAG,GAAG,KAAK,UAAU,oBAAoB,MAAM,QAAQ,CAAC;AAC/G;AAEA,SAAS,oBAAoB,OAAyB;CACpD,IAAI,YAAY,KAAK,GAAG,OAAO;CAE/B,IAAI,MAAM,QAAQ,KAAK,GAAG;EACxB,IAAI,MAAM,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG;EACzC,OAAO,MAAM,IAAI,mBAAmB;CACtC;CAEA,MAAM,SAAS;CACf,OAAO,OAAO,YACZ,OAAO,KAAK,MAAM,CAAC,CAAC,KAAK,QAAQ,CAAC,KAAK,oBAAoB,OAAO,IAAI,CAAC,CAAC,CAC1E;AACF;AAEA,SAAS,YAAY,OAAyB;CAC5C,IAAI,UAAU,QAAQ,OAAO,UAAU,UAAU,OAAO;CACxD,IAAI,iBAAiB,MAAM,OAAO;CAClC,MAAM,SAAS;CACf,OAAO,OAAO,OAAO,gBAAgB,cAAc,OAAO,OAAO,SAAS;AAC5E;;;;AC1BA,MAAM,YAAY;AAClB,MAAM,WAAW;AACjB,MAAM,kBAAkB;;;;;;;AAQxB,SAAgB,gBAAgB,SAAqD;CACnF,MAAM,2BAAW,IAAI,IAA6B;CAClD,MAAM,wBAAQ,IAAI,IAA0B;CAC5C,MAAM,OAA2B,CAAC;CAClC,MAAM,aAAiC;EAAE,SAAS,CAAC;EAAG,OAAO,CAAC;EAAG,MAAM,CAAC;CAAE;CAC1E,MAAM,4BAAY,IAAI,IAA0C;CAChE,IAAI,gBAAgB;CAEpB,MAAM,QAAQ,UAA+B;EAC3C,KAAK,MAAM,YAAY,WACrB,IAAI;GACF,SAAS,KAAK;EAChB,QAAQ,CAER;CAEJ;CAEA,MAAM,iBAAoB,MAAW,UAAa;EAChD,KAAK,KAAK,KAAK;EACf,IAAI,KAAK,SAAS,iBAAiB,KAAK,MAAM;CAChD;CAEA,OAAO;EACL,aAAa,SAAS;GACpB,SAAS,OAAO,QAAQ,EAAE;GAC1B,SAAS,IAAI,QAAQ,IAAI;IACvB,GAAG;IACH,QAAQ,CAAC;IACT,SAAS,CAAC;IACV,OAAO,CAAC;IACR,MAAM,CAAC;IACP,OAAO,CAAC;IACR,UAAU,CAAC;GACb,CAAC;GACD,OAAO,SAAS,OAAO,QAAQ,aAC7B,SAAS,OAAO,SAAS,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,KAAe;EAC1D;EACA,WAAW,IAAI,QAAQ;GACrB,MAAM,UAAU,SAAS,IAAI,EAAE;GAC/B,IAAI,CAAC,SAAS;GACd,QAAQ,SAAS,OAAO;GACxB,QAAQ,aAAa,OAAO;GAC5B,QAAQ,QAAQ,OAAO;GACvB,IAAI,OAAO,UAAU,QAAW,QAAQ,QAAQ,OAAO;GACvD,QAAQ,WAAW,eAAe,QAAQ,SAAS,QAAQ,iBAAiB;GAC5E,KAAK;IAAE,MAAM;IAAW,MAAM,QAAQ,OAAO;GAAE,CAAC;EAClD;EACA,SAAS,WAAW,OAAO;GACzB,SAAS,IAAI,SAAS,CAAC,EAAE,OAAO,KAAK,KAAK;EAC5C;EACA,SAAS,WAAW,OAAO;GACzB,MAAM,SAAwB;IAC5B,GAAG;IACH,IAAI,IAAI,EAAE;IACV,OAAO,WAAW,KAAK;GACzB;GACA,MAAM,UAAU,YAAY,SAAS,IAAI,SAAS,IAAI;GACtD,IAAI,SAAS,QAAQ,QAAQ,KAAK,MAAM;QACnC,cAAc,WAAW,SAAS,MAAM;GAC7C,OAAO;EACT;EACA,kBAAkB,WAAW,WAAW;GACtC,MAAM,UAAU,YAAY,SAAS,IAAI,SAAS,IAAI;GACtD,IAAI,SAAS,QAAQ,MAAM,KAAK,SAAS;QACpC,cAAc,WAAW,OAAO,SAAS;EAChD;EACA,OAAO,WAAW,OAAO;GACvB,MAAM,UAAU,YAAY,SAAS,IAAI,SAAS,IAAI;GACtD,IAAI,SAAS,QAAQ,KAAK,KAAK,KAAK;QAC/B,cAAc,WAAW,MAAM,KAAK;GACzC,KAAK,QAAQ,KAAK;GAClB,IAAI,KAAK,SAAS,UAAU,KAAK,IAAI;GACrC,KAAK;IAAE,MAAM;IAAO,MAAM;GAAM,CAAC;EACnC;EACA,QAAQ,MAAM;GACZ,MAAM,OAAO,KAAK,EAAE;GACpB,MAAM,IAAI,KAAK,IAAI,IAAI;GACvB,OAAO,MAAM,OAAO,WAAW,MAAM,OAAO,MAAM,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,KAAe;GAC/E,IAAI,KAAK,WAAW,SAAS,IAAI,KAAK,SAAS,CAAC,EAAE,MAAM,KAAK,KAAK,EAAE;GACpE,KAAK;IAAE,MAAM;IAAQ,MAAM,YAAY,IAAI;GAAE,CAAC;EAChD;EACA,eAAe;GACb,OAAO,CAAC,GAAG,SAAS,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,IAAI,OAAO;EACrD;EACA,WAAW,IAAI;GACb,MAAM,UAAU,SAAS,IAAI,EAAE;GAC/B,OAAO,WAAW,YAAY,OAAO;EACvC;EACA,YAAY;GACV,OAAO,CAAC,GAAG,MAAM,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,IAAI,WAAW;EACtD;EACA,QAAQ,IAAI;GACV,MAAM,OAAO,MAAM,IAAI,EAAE;GACzB,OACE,QAAQ;IACN,GAAG;IACH,IAAI,CAAC,GAAG,KAAK,EAAE;IACf,IAAI,CAAC,GAAG,KAAK,EAAE;IACf,KAAK,CAAC,GAAG,KAAK,GAAG;IACjB,SAAS,EAAE,GAAG,KAAK,QAAQ;IAC3B,aAAa,CAAC,GAAG,KAAK,WAAW;GACnC;EAEJ;EACA,SAAS,SAAS,CAAC,GAAG;GACpB,MAAM,SAAS,OAAO,QAAQ,YAAY;GAC1C,OAAO,KACJ,QACE,WACE,CAAC,OAAO,SAAS,MAAM,UAAU,OAAO,WACxC,CAAC,UAAU,MAAM,OAAO,YAAY,CAAC,CAAC,SAAS,MAAM,EAC1D,CAAC,CACA,KAAK,WAAW,EAAE,GAAG,MAAM,EAAE;EAClC;EACA,gBAAgB;GACd,OAAO;IACL,SAAS,CAAC,GAAG,WAAW,OAAO;IAC/B,OAAO,CAAC,GAAG,WAAW,KAAK;IAC3B,MAAM,CAAC,GAAG,WAAW,IAAI;GAC3B;EACF;EACA,UAAU,UAA4B;GACpC,UAAU,IAAI,QAAQ;GACtB,aAAa,UAAU,OAAO,QAAQ;EACxC;EACA,QAAQ;GACN,SAAS,MAAM;GACf,MAAM,MAAM;GACZ,KAAK,SAAS;GACd,WAAW,QAAQ,SAAS;GAC5B,WAAW,MAAM,SAAS;GAC1B,WAAW,KAAK,SAAS;EAC3B;CACF;AACF;AAEA,SAAS,QAAQ,SAAkD;CACjE,MAAM,EAAE,IAAI,QAAQ,MAAM,OAAO,QAAQ,WAAW,eAAe;CACnE,OAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA;EACA,YAAY,QAAQ,QAAQ;EAC5B,cAAc,QAAQ,SAAS;CACjC;AACF;AAEA,SAAS,YAAY,MAAyC;CAC5D,MAAM,EAAE,IAAI,WAAW,YAAY,IAAI,YAAY;CACnD,OAAO;EAAE;EAAI;EAAW;EAAY,IAAI,CAAC,GAAG,EAAE;EAAG;CAAQ;AAC3D;AAEA,SAAS,YAAY,SAA2C;CAC9D,OAAO;EACL,GAAG;EACH,QAAQ,CAAC,GAAG,QAAQ,MAAM;EAC1B,SAAS,CAAC,GAAG,QAAQ,OAAO;EAC5B,OAAO,CAAC,GAAG,QAAQ,KAAK;EACxB,MAAM,CAAC,GAAG,QAAQ,IAAI;EACtB,OAAO,CAAC,GAAG,QAAQ,KAAK;EACxB,UAAU,CAAC,GAAG,QAAQ,QAAQ;CAChC;AACF;;;;;AC9LA,SAAgB,uBAAuB,UAA2B,CAAC,GAA4B;CAC7F,MAAM,QAAQ,QAAQ,QAAQ,aAAY,CAAE,QAAQ,QAAQ,EAAE;CAC9D,MAAM,cAAc,QAAQ,eAAe;CAC3C,MAAM,oBAAoB,QAAQ,qBAAqB;CAEvD,IAAI,CAAC,QAAQ,CAAC,KAAK,WAAW,GAAG,GAC/B,MAAM,IAAI,MAAM,uEAAmE;CAGrF,IAAI,cAAc,GAChB,MAAM,IAAI,MAAM,qDAAmD;CAGrE,IAAI,oBAAoB,GACtB,MAAM,IAAI,MAAM,2DAAyD;CAG3E,OAAO;EAAE;EAAM;EAAa;CAAkB;AAChD;;;;;;;;;;;;;;;ACTA,SAAgB,mBAAuC;CACrD,IAAI;EACF,OAAOA,gCAAe,WAAW,CAAC,EAAE;CACtC,QAAQ;EACN;CACF;AACF;;;;;ACdA,MAAM,cAAc;CAAC;CAAO;CAAQ;CAAO;CAAW;CAAe;AAAS;;;;;;AAgB9E,eAAsB,kBAAkB,WAAyD;CAC/F,IAAI;CAKJ,IAAI;EACF,CAAC,CAAE,SAAU,MAAM,OAAO;CAC5B,QAAQ;EACN,aAAa,CAAC;CAChB;CAEA,MAAM,WAAW,YAAY,KAAK,SAAS;EACzC,MAAM,WAAW,SAAyB;GACxC,MAAM,YAAoC;IACxC;IACA,QAAQ,KAAK;IACb,KAAK,KAAK;IACV,MAAM,KAAK;IACX,MAAM,KAAK;IACX,IAAI,KAAK,IAAI;IACb,YAAY,KAAK;GACnB;GAEA,UAAU,kBAAkB,iBAAiB,GAAG,SAAS;EAC3D;EAEA,MAAM,GAAG,MAAM,OAAO;EAEtB,OAAO;GAAE;GAAM;EAAQ;CACzB,CAAC;CAED,aAAa;EACX,KAAK,MAAM,EAAE,MAAM,aAAa,UAAU,MAAM,IAAI,MAAM,OAAO;CACnE;AACF;;;;;ACjDA,SAAS,gBAAgB,MAAsD;CAC7E,OAAO,SAAS,UAAU,UAAU;AACtC;AAEA,IAAM,qBAAN,cAAiCC,8BAAW;CAI1C,AAAO,YAAY,AAAiB,WAA8B;EAChE,MAAM;EAD4B;cAHb;gBACP;CAIhB;CAEA,AAAO,IAAI,MAAyB;EAClC,IAAI,CAAC,KAAK,QAAQ;EAMlB,MAAM,QAA0B;GAC9B,OAAO,gBAAgB,KAAK,IAAI;GAChC,QAAQ,KAAK;GACb,QAAQ,KAAK;GACb,SAAS,KAAK;GACd,IAAI,KAAK,IAAI;GACb,SAASC,8BAAY,KAAK,OAAO;EACnC;EAEA,KAAK,UAAU,OAAO,iBAAiB,GAAG,KAAK;CACjD;AACF;;;;;;;AAQA,eAAsB,gBAAgB,WAAyD;CAC7F,MAAM,UAAU,IAAI,mBAAmB,SAAS;CAEhD,uBAAI,WAAW,OAAO;CAEtB,aAAa;EACX,QAAQ,SAAS;CACnB;AACF;;;;AChDA,SAAS,gBAAgB,SAAsD;CAC7E,IAAI,CAAC,SAAS,OAAO;CAErB,OAAO,OAAO,YAAY,WAAW,UAAU,GAAG,QAAQ,KAAK,IAAI,QAAQ,QAAQ;AACrF;AAEA,SAAS,eAAe,YAAgD;CACtE,IAAI,OAAO,WAAW,YAAY,UAAU,OAAO,OAAO,WAAW,WAAW,OAAO;CACvF,IAAI,OAAO,SAAS,WAAW,OAAO,GAAG,OAAO,WAAW,QAAQ;AAGrE;AAEA,SAAS,qBAAqB,YAAoD;CAChF,OAAO;EACL,UAAU,WAAW;EACrB,aAAa,WAAW;EACxB,MAAM,eAAe,UAAU;CACjC;AACF;;;;;;AAOA,eAAsB,iBAAiB,WAAyD;CAC9F,MAAM,EAAE,eAAe,MAAM,OAAO;CAEpC,MAAM,eAAe,WAAW,YAAY,SAAuB;EACjE,MAAM,EAAE,eAAe;EAEvB,UAAU,QAAQ;GAChB,IAAI,KAAK;GACT,WAAW,iBAAiB;GAC5B,YAAY,KAAK,UAAU,QAAQ;GACnC,MAAM,gBAAgB,WAAW,IAAI;GACrC,IAAI,WAAW;GACf,IAAI,WAAW;GACf,KAAK,WAAW;GAChB,SAAS,WAAW;GACpB,MAAM,WAAW;GACjB,MAAM,WAAW;GACjB,SAAS,WAAW;GACpB,aAAa,WAAW,YAAY,IAAI,oBAAoB;EAC9D,CAAC;CACH,CAAC;CAED,aAAa,aAAa,YAAY;AACxC;;;;ACjDA,MAAM,mBAAmB,kBAAcC,wBAAc,IAAI,IAAI,oDAAsB,CAAC,CAAC;;AAGrF,SAAgB,sBACd,yBAAgB,IAAI,MAAM,EAAC,CAAC,SAAS,IACD;CACpC,KAAK,MAAM,QAAQ,MAAM,MAAM,IAAI,GAAG;EACpC,MAAM,QAAQ,WAAW,IAAI;EAC7B,IAAI,CAAC,SAAS,WAAW,MAAM,IAAI,GAAG;EACtC,OAAO;CACT;AAEF;AAEA,SAAS,WAAW,OAAmD;CAErE,MAAM,GAAG,SAAS,MAAM,UADV,MAAM,MAAM,8CACY,KAAK,CAAC;CAC5C,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,QAAQ,OAAO;CAEzC,OAAO;EAAE,MADI,cAAc,OACf;EAAG,MAAM,OAAO,IAAI;EAAG,QAAQ,OAAO,MAAM;CAAE;AAC5D;;AAGA,MAAM,yBAAyB;AAE/B,SAAS,cAAc,MAAsB;CAM3C,QALgB,CAAC,KAAK,WAAW,OAAO,IACpC,OACA,uBAAuB,KAAK,IAAI,QAC9BA,wBAAc,MAAM,EAAE,SAAS,KAAK,CAAC,QACrCA,wBAAc,IAAI,EACV,CAAC,QAAQ,OAAO,GAAG;AACnC;AAEA,SAAS,WAAW,MAAuB;CACzC,OACE,KAAK,WAAW,OAAO,KACvB,KAAK,SAAS,gBAAgB,KAG9B,KAAK,WAAW,WAAW,KAC3B,KAAK,WAAW,gBAAgB;AAEpC;;;;;ACzBA,SAAS,eAAe,UAAwD;CAC9E,IAAI,CAAC,UAAU,OAAO;CAatB,WAXiBC,gCACf;EACE,MAAM;EACN,QAAQ;EACR,QAAQ;EACR,SAAS;EACT,SAAS,EAAE,SAAS;CACtB,GACAC,uBAAI,UAAU,CAGD,CAAC,CAAC,QAAoC;AACvD;;;;;;AAOA,eAAsB,kBAAkB,WAAyD;CAC/F,IAAI;CAKJ,IAAI;EACF,CAAC,CAAE,sBAAuB,MAAM,OAAO;CACzC,QAAQ;EACN,aAAa,CAAC;CAChB;CAEA,MAAM,YAAY,UAAsB;EACtC,UAAU,SAAS,iBAAiB,GAAG;GACrC,YAAY,MAAM;GAClB,QAAQ,MAAM;GACd,KAAK,MAAM;GACX,UAAUC,8BAAY,eAAe,MAAM,QAAQ,CAAC;GACpD,YAAY,MAAM;GAClB,SAAS,MAAM;GACf,UAAU,MAAM;GAChB,WAAW,MAAM;GACjB,YAAY,MAAM;GAClB,UAAU,MAAM;GAChB,OACE,MAAM,UAAU,SACZ,SACA,MAAM,iBAAiB,QACrB,MAAM,MAAM,UACZ,OAAO,MAAM,KAAK;GAC1B,QAAQ,sBAAsB;EAChC,CAAC;CACH;CAEA,mBAAmB,GAAG,SAAS,QAAQ;CAEvC,aAAa,mBAAmB,IAAI,SAAS,QAAQ;AACvD;;;;;;;;;;ACvEA,eAAsB,oBACpB,WACA,SAC2B;CAC3B,MAAM,EAAE,yBAAyB,MAAM,OAAO;CAE9C,MAAM,mBAAmB,SAA0B,KAAK,WAAW,QAAQ,IAAI;CAE/E,OAAO,qBAAqB;EAC1B,eAAe,KAAqB;GAClC,IAAI,gBAAgB,IAAI,IAAI,GAAG;GAE/B,UAAU,aAAa;IACrB,IAAI,IAAI;IACR,QAAQ,IAAI;IACZ,MAAM,IAAI;IACV,OAAO,IAAI;IACX,WAAW,YAAY,aAAa,YAAY,IAAI;GACtD,CAAC;EACH;EACA,QAAQ,KAAqB,OAAyB;GACpD,IAAI,gBAAgB,IAAI,IAAI,GAAG;GAE/B,UAAU,SAAS,IAAI,WAAW;IAChC,MAAM,MAAM;IACZ,YAAY,MAAM;IAClB,WAAW,MAAM,aAAa,YAAY,aAAa,YAAY,IAAI,IAAI,MAAM;IACjF,OACE,MAAM,UAAU,SACZ,SACCC,8BAAY,MAAM,KAAK;GAChC,CAAC;EACH;EACA,aAAa,KAAqB,QAA+B;GAC/D,IAAI,gBAAgB,IAAI,IAAI,GAAG;GAE/B,UAAU,WAAW,IAAI,WAAW;IAClC,QAAQ,OAAO;IACf,YAAY,OAAO;IACnB,OACE,OAAO,UAAU,SACb,SACA,OAAO,iBAAiB,QACtB,OAAO,MAAM,UACb,OAAO,OAAO,KAAK;IAC3B,OAAO,IAAI;GACb,CAAC;EACH;CACF,CAAC;AACH;;;;;;;;;AChCA,MAAa,8BAA8B;;;;;;;;;;;;;;;;;AAqB3C,SAAgB,kBAAkB,UAAoC,CAAC,GAAc;CACnF,IAAI,SAAS;CACb,MAAM,YAAgC,CAAC;CAEvC,MAAM,YAAuB;EAC3B,MAAM;EACN;EAGA,gBAAgB;EAChB,gBAAgB;EAChB,MAAM,OAAO;GACX,MAAM,EAAE,aAAa,kBAAkB,MAAM,OAAO;GACpD,MAAM,EAAE,QAAQ,MAAM,OAAO;GAE7B,IAAI,YAAY,oBAAoB,eAAe;IACjD,IAAI,KACF,YACA,YACA,kEACF;IACA;GACF;GAEA,MAAM,WAAW,uBAAuB,OAAO;GAC/C,MAAM,YAAY,gBAAgB,QAAQ;GAE1C,MAAM,aAAa,KAAK,iBAAiB,oBAAoB,WAAW,QAAQ,CAAC;GACjF,MAAM,aAAa,KAAK,eAAe,kBAAkB,SAAS,CAAC;GACnE,MAAM,aAAa,KAAK,eAAe,kBAAkB,SAAS,CAAC;GACnE,MAAM,aAAa,KAAK,cAAc,iBAAiB,SAAS,CAAC;GACjE,MAAM,aAAa,KAAK,aAAa,gBAAgB,SAAS,CAAC;GAE/D,MAAM,KAAK,MAAMC,iCAAe,SAAS,WAAW;GACpD,UAAU,KAAK,GAAG,OAAO;GAEzB,MAAM,EAAE,wBAAwB,2CAAM;GACtC,MAAM,iBAAiB,MAAM,mBAAmB;GAEhD,oBAAoB,cAAc,GAAG;IACnC;IACA;IACA,cAAc,MAAM,iBAAiB;IACrC,SAAS;IACT,qBAAqB,YAAY,oBAAoB;IACrD,UAAU,UAAU,aAAa,KAAK;IACtC,kBAAkB,WAAW;IAC7B,0BAA0B,mBAAmB;IAC7C,sBAAsB,iBAClB,YAAY,iBAAiB,MAAM,eAAe,CAAC,IACnD;GACN,CAAC;GAKD,UAAU,KACR,UAAU,WAAW,UAAU;IAC7B,IAAI,MAAM,SAAS,aAAa,MAAM,KAAK,iBAAiB,GAAG;IAE/D,KAAK,MAAM,WAAW,UAAU,WAAW,MAAM,KAAK,EAAE,CAAC,EAAE,YAAY,CAAC,GAAG;KACzE,MAAM,QAAQ,QAAQ,SAAS,MAAM,QAAQ,OAAO,KAAK,GAAG,QAAQ,OAAO,SAAS;KAEpF,IAAI,KACF,YACA,OACA,GAAG,QAAQ,UAAU,MAAM,IAAI,MAAM,KAAK,OAAO,GAAG,MAAM,KAAK,KAAK,EACtE;IACF;GACF,CAAC,CACH;GAEA,SAAS;GAET,IAAI,KAAK,YAAY,SAAS,qBAAqB,SAAS,MAAM;EACpE;EACA,MAAM,QAAQ,CAAC;EACf,MAAM,UAAU;GACd,MAAM,UAAU,SAAS;GACzB,MAAM,UAAU,MAAM;EACxB;EACA,MAAM,WAAW;GACf,KAAK,MAAM,WAAW,UAAU,OAAO,CAAC,GAAG,QAAQ;GACnD,SAAS;EACX;EACA,qBAAqB;CACvB;;CAGA,eAAe,aACb,QACA,MACA,QACe;EACf,IAAI;GACF,UAAU,KAAK,MAAM,OAAO,CAAC;EAC/B,SAAS,OAAO;GACd,OAAO,KACL,YACA,iBACA,aAAa,KAAK,4BAA4B,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,GACrG;EACF;CACF;CAEA,OAAO;AACT;;AAGA,eAAe,aAAa,OAAwC;CAClE,MAAM,EAAE,uBAAuB,MAAM,OAAO;CAE5C,MAAM,EAAE,SAAS,MADE,mBAAmB,IAAI,MAAM,UAChB,CAAC,CAAC,IAAI,yBAAyB,MAAM,OAAO,MAAM,QAAQ;CAE1F,OAAO;AACT;;AAGA,eAAe,aAA0C;CACvD,MAAM,EAAE,WAAW,MAAM,OAAO;CAEhC,OAAO,OAAO,KAAK,CAAC,CAAC,KAAK,WAAW;EACnC,QAAQ,MAAM,OAAO,YAAY;EACjC,MAAM,MAAM;EACZ,MAAM,MAAM;EACZ,YAAY,MAAM,YAAY,KAAK,eAAe,WAAW,QAAQ,WAAW;CAClF,EAAE;AACJ;;;;;;AAOA,eAAe,qBAAsC;CACnD,MAAM,EAAE,kCAAkC,MAAM,OAAO;CACvD,MAAM,EAAE,aAAa,MAAM,8BAA8B;CAEzD,OAAO;AACT;;;;;;;AAUA,eAAe,qBAA0D;CACvE,IAAI;EAEF,MAAM,SAAQ,MAD8B,OAAO,oBACjC,CAAC;EAEnB,OAAO,OAAO,UAAU,aAAc,QAA2B;CACnE,QAAQ;EACN;CACF;AACF;;;;;AAMA,SAAS,iBAAiB,QAAyB;CACjD,IAAI,UAAU,OAAO,WAAW,YAAY,gBAAgB,QAC1D,OAAQ,OAAkC;CAG5C,OAAO;AACT"}
@@ -1 +1 @@
1
- {"version":3,"file":"devtools-connector.d.mts","names":[],"sources":["../../../../../../../packages/devtools/src/devtools-connector.ts"],"mappings":";;;;YA4BY,2BAA2B;;;;;;;;;;;;;;;;;wBAkBvB,kBAAkB,UAAS,2BAAgC"}
1
+ {"version":3,"file":"devtools-connector.d.mts","names":[],"sources":["../../../../../../../packages/devtools/src/devtools-connector.ts"],"mappings":";;;;YA8BY,2BAA2B;;;;;;;;;;;;;;;;;wBAkBvB,kBAAkB,UAAS,2BAAgC"}
@@ -1,5 +1,7 @@
1
+ import { loadProcessPanel } from "./process-panel.mjs";
1
2
  import { createCollector } from "./collector.mjs";
2
3
  import { resolveDevtoolsOptions } from "./options.mjs";
4
+ import { attachAiSource } from "./sources/ai-source.mjs";
3
5
  import { attachCacheSource } from "./sources/cache-source.mjs";
4
6
  import { attachLogSource } from "./sources/log-source.mjs";
5
7
  import { attachMailSource } from "./sources/mail-source.mjs";
@@ -51,10 +53,14 @@ function devtoolsConnector(options = {}) {
51
53
  await attachSource(log, "cache", () => attachCacheSource(collector));
52
54
  await attachSource(log, "mail", () => attachMailSource(collector));
53
55
  await attachSource(log, "log", () => attachLogSource(collector));
56
+ const ai = await attachAiSource(resolved.maxRequests);
57
+ disposers.push(ai.dispose);
54
58
  const { mountDevtoolsRoutes } = await import("./server/index.mjs");
55
59
  const postmanBuilder = await findPostmanBuilder();
56
60
  mountDevtoolsRoutes(getHttpServer(), {
57
61
  collector,
62
+ ai,
63
+ processPanel: await loadProcessPanel(),
58
64
  options: resolved,
59
65
  isDevelopment: () => Application.runtimeStrategy === "development",
60
66
  explain: (query) => explainQuery(query),
@@ -1 +1 @@
1
- {"version":3,"file":"devtools-connector.mjs","names":[],"sources":["../../../../../../../packages/devtools/src/devtools-connector.ts"],"sourcesContent":["/**\n * The devtools connector for `warlock.config.ts > connectors`.\n *\n * Deliberately a plain object with TYPE-ONLY imports from core: the config\n * file that constructs it must not drag core's runtime graph in at\n * config-load time. Core is imported lazily inside `boot()`, where the app\n * has already loaded it — see `queue/src/queue-connector.ts`, the pattern\n * this mirrors.\n */\nimport type { Connector, ConnectorLifecyclePhase } from \"@warlock.js/core\";\nimport { createCollector } from \"./collector\";\nimport { resolveDevtoolsOptions } from \"./options\";\nimport type { DevtoolsRouteRow } from \"./server\";\nimport { attachCacheSource } from \"./sources/cache-source\";\nimport { attachLogSource } from \"./sources/log-source\";\nimport { attachMailSource } from \"./sources/mail-source\";\nimport { attachQuerySource } from \"./sources/query-source\";\nimport { attachTracingSource } from \"./sources/tracing-source\";\nimport type { DevtoolsDisposer, DevtoolsOptions, DevtoolsQuery } from \"./types\";\n\n/**\n * Boots after the queue connector (priority 11), which is the last\n * built-in/framework connector to claim a priority; devtools has nothing\n * that depends on it, so it simply goes last.\n */\nexport const DEVTOOLS_CONNECTOR_PRIORITY = 12;\n\n/** Options accepted by `devtoolsConnector()`. */\nexport type DevtoolsConnectorOptions = DevtoolsOptions;\n\n/**\n * Construct the devtools connector.\n *\n * Refuses to run outside development: `boot()` checks\n * `Application.runtimeStrategy` and, when it isn't `\"development\"`, logs one\n * warning and returns without mounting a route or subscribing to anything.\n * In development it attaches every source (tracing, queries, cache, mail,\n * logs), then mounts the dashboard's routes on the HTTP server — before it\n * starts listening, the only point in the boot sequence where mounting is\n * possible (see `queue/src/dashboard-boot.ts` for the same constraint).\n *\n * Apps don't call this: `warlock dev` registers it automatically when the\n * package is installed (`warlock add devtools`). Keep it OUT of\n * `warlock.config.ts` — devtools is a dev dependency, so a production\n * install doesn't have it and a config import of it would fail there.\n */\nexport function devtoolsConnector(options: DevtoolsConnectorOptions = {}): Connector {\n let active = false;\n const disposers: DevtoolsDisposer[] = [];\n\n const connector: Connector = {\n name: \"devtools\",\n priority: DEVTOOLS_CONNECTOR_PRIORITY,\n // Core's `ConnectorLifecyclePhase.Late`; spelled out so this module stays\n // free of a runtime import of core.\n lifecyclePhase: \"late\" as ConnectorLifecyclePhase,\n isActive: () => active,\n async boot() {\n const { Application, getHttpServer } = await import(\"@warlock.js/core\");\n const { log } = await import(\"@warlock.js/logger\");\n\n if (Application.runtimeStrategy !== \"development\") {\n log.warn(\n \"devtools\",\n \"disabled\",\n \"@warlock.js/devtools only runs in development; it is not mounted\",\n );\n return;\n }\n\n const resolved = resolveDevtoolsOptions(options);\n const collector = createCollector(resolved);\n\n await attachSource(log, \"tracing\", () => attachTracingSource(collector, resolved));\n await attachSource(log, \"query\", () => attachQuerySource(collector));\n await attachSource(log, \"cache\", () => attachCacheSource(collector));\n await attachSource(log, \"mail\", () => attachMailSource(collector));\n await attachSource(log, \"log\", () => attachLogSource(collector));\n\n const { mountDevtoolsRoutes } = await import(\"./server\");\n const postmanBuilder = await findPostmanBuilder();\n\n mountDevtoolsRoutes(getHttpServer(), {\n collector,\n options: resolved,\n isDevelopment: () => Application.runtimeStrategy === \"development\",\n explain: (query) => explainQuery(query),\n listRoutes: () => listRoutes(),\n getOpenApiDocument: () => getOpenApiDocument(),\n getPostmanCollection: postmanBuilder\n ? async () => unwrapCollection(await postmanBuilder())\n : undefined,\n });\n\n // An N+1 is worth a terminal line, not just a badge the developer has\n // to go looking for: one warning per offending query shape, with the\n // line that issued it.\n disposers.push(\n collector.subscribe((event) => {\n if (event.type !== \"request\" || event.data.warningCount === 0) return;\n\n for (const warning of collector.getRequest(event.data.id)?.warnings ?? []) {\n const where = warning.caller ? ` — ${warning.caller.file}:${warning.caller.line}` : \"\";\n\n log.warn(\n \"devtools\",\n \"n+1\",\n `${warning.message}${where} (${event.data.method} ${event.data.path})`,\n );\n }\n }),\n );\n\n active = true;\n\n log.info(\"devtools\", \"ready\", `devtools ready at ${resolved.path}`);\n },\n async start() {},\n async restart() {\n await connector.shutdown();\n await connector.start();\n },\n async shutdown() {\n for (const dispose of disposers.splice(0)) dispose();\n active = false;\n },\n shouldRestart: () => false,\n };\n\n /** Attaches one source, logging and continuing when it fails to attach. */\n async function attachSource(\n logger: { warn: (module: string, action: string, message: string) => unknown },\n name: string,\n attach: () => Promise<DevtoolsDisposer>,\n ): Promise<void> {\n try {\n disposers.push(await attach());\n } catch (error) {\n logger.warn(\n \"devtools\",\n \"source-failed\",\n `devtools' ${name} source failed to attach: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n\n return connector;\n}\n\n/** Runs `EXPLAIN (FORMAT JSON)` for a stored Postgres query on its own connection. */\nasync function explainQuery(query: DevtoolsQuery): Promise<unknown> {\n const { dataSourceRegistry } = await import(\"@warlock.js/cascade\");\n const dataSource = dataSourceRegistry.get(query.connection);\n const { rows } = await dataSource.raw(`EXPLAIN (FORMAT JSON) ${query.sql}`, query.bindings);\n\n return rows;\n}\n\n/** Lists every registered HTTP route — the same source `warlock routes --json` reads. */\nasync function listRoutes(): Promise<DevtoolsRouteRow[]> {\n const { router } = await import(\"@warlock.js/core\");\n\n return router.list().map((route) => ({\n method: route.method.toUpperCase(),\n path: route.path,\n name: route.name,\n middleware: route.middleware?.map((middleware) => middleware.name || \"anonymous\"),\n }));\n}\n\n/**\n * Builds the OpenAPI 3.1 document from the routes registered in this process —\n * the same generator behind `warlock generate.openapi`. Core is imported\n * lazily so this module stays free of a load-time dependency on it.\n */\nasync function getOpenApiDocument(): Promise<object> {\n const { getDevelopmentOpenApiDocument } = await import(\"@warlock.js/core\");\n const { document } = await getDevelopmentOpenApiDocument();\n\n return document;\n}\n\ntype PostmanBuilder = () => Promise<unknown>;\n\n/**\n * Looks up core's Postman collection builder, `getDevelopmentPostmanCollection`.\n * Older cores do not export it; the route then answers 503. The lookup is\n * guarded because a namespace without the export may throw on access. Core is\n * imported lazily, like the OpenAPI document above.\n */\nasync function findPostmanBuilder(): Promise<PostmanBuilder | undefined> {\n try {\n const core: Record<string, unknown> = await import(\"@warlock.js/core\");\n const build = core.getDevelopmentPostmanCollection;\n\n return typeof build === \"function\" ? (build as PostmanBuilder) : undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Core may return the collection itself or wrap it in `{ collection, warnings }`,\n * like the OpenAPI generator's `{ document, warnings }`.\n */\nfunction unwrapCollection(result: unknown): object {\n if (result && typeof result === \"object\" && \"collection\" in result) {\n return (result as { collection: object }).collection;\n }\n\n return result as object;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"devtools-connector.mjs","names":[],"sources":["../../../../../../../packages/devtools/src/devtools-connector.ts"],"sourcesContent":["/**\n * The devtools connector for `warlock.config.ts > connectors`.\n *\n * Deliberately a plain object with TYPE-ONLY imports from core: the config\n * file that constructs it must not drag core's runtime graph in at\n * config-load time. Core is imported lazily inside `boot()`, where the app\n * has already loaded it — see `queue/src/queue-connector.ts`, the pattern\n * this mirrors.\n */\nimport type { Connector, ConnectorLifecyclePhase } from \"@warlock.js/core\";\nimport { loadProcessPanel } from \"./process-panel\";\nimport { createCollector } from \"./collector\";\nimport { resolveDevtoolsOptions } from \"./options\";\nimport type { DevtoolsRouteRow } from \"./server\";\nimport { attachAiSource } from \"./sources/ai-source\";\nimport { attachCacheSource } from \"./sources/cache-source\";\nimport { attachLogSource } from \"./sources/log-source\";\nimport { attachMailSource } from \"./sources/mail-source\";\nimport { attachQuerySource } from \"./sources/query-source\";\nimport { attachTracingSource } from \"./sources/tracing-source\";\nimport type { DevtoolsDisposer, DevtoolsOptions, DevtoolsQuery } from \"./types\";\n\n/**\n * Boots after the queue connector (priority 11), which is the last\n * built-in/framework connector to claim a priority; devtools has nothing\n * that depends on it, so it simply goes last.\n */\nexport const DEVTOOLS_CONNECTOR_PRIORITY = 12;\n\n/** Options accepted by `devtoolsConnector()`. */\nexport type DevtoolsConnectorOptions = DevtoolsOptions;\n\n/**\n * Construct the devtools connector.\n *\n * Refuses to run outside development: `boot()` checks\n * `Application.runtimeStrategy` and, when it isn't `\"development\"`, logs one\n * warning and returns without mounting a route or subscribing to anything.\n * In development it attaches every source (tracing, queries, cache, mail,\n * logs), then mounts the dashboard's routes on the HTTP server — before it\n * starts listening, the only point in the boot sequence where mounting is\n * possible (see `queue/src/dashboard-boot.ts` for the same constraint).\n *\n * Apps don't call this: `warlock dev` registers it automatically when the\n * package is installed (`warlock add devtools`). Keep it OUT of\n * `warlock.config.ts` — devtools is a dev dependency, so a production\n * install doesn't have it and a config import of it would fail there.\n */\nexport function devtoolsConnector(options: DevtoolsConnectorOptions = {}): Connector {\n let active = false;\n const disposers: DevtoolsDisposer[] = [];\n\n const connector: Connector = {\n name: \"devtools\",\n priority: DEVTOOLS_CONNECTOR_PRIORITY,\n // Core's `ConnectorLifecyclePhase.Late`; spelled out so this module stays\n // free of a runtime import of core.\n lifecyclePhase: \"late\" as ConnectorLifecyclePhase,\n isActive: () => active,\n async boot() {\n const { Application, getHttpServer } = await import(\"@warlock.js/core\");\n const { log } = await import(\"@warlock.js/logger\");\n\n if (Application.runtimeStrategy !== \"development\") {\n log.warn(\n \"devtools\",\n \"disabled\",\n \"@warlock.js/devtools only runs in development; it is not mounted\",\n );\n return;\n }\n\n const resolved = resolveDevtoolsOptions(options);\n const collector = createCollector(resolved);\n\n await attachSource(log, \"tracing\", () => attachTracingSource(collector, resolved));\n await attachSource(log, \"query\", () => attachQuerySource(collector));\n await attachSource(log, \"cache\", () => attachCacheSource(collector));\n await attachSource(log, \"mail\", () => attachMailSource(collector));\n await attachSource(log, \"log\", () => attachLogSource(collector));\n\n const ai = await attachAiSource(resolved.maxRequests);\n disposers.push(ai.dispose);\n\n const { mountDevtoolsRoutes } = await import(\"./server\");\n const postmanBuilder = await findPostmanBuilder();\n\n mountDevtoolsRoutes(getHttpServer(), {\n collector,\n ai,\n processPanel: await loadProcessPanel(),\n options: resolved,\n isDevelopment: () => Application.runtimeStrategy === \"development\",\n explain: (query) => explainQuery(query),\n listRoutes: () => listRoutes(),\n getOpenApiDocument: () => getOpenApiDocument(),\n getPostmanCollection: postmanBuilder\n ? async () => unwrapCollection(await postmanBuilder())\n : undefined,\n });\n\n // An N+1 is worth a terminal line, not just a badge the developer has\n // to go looking for: one warning per offending query shape, with the\n // line that issued it.\n disposers.push(\n collector.subscribe((event) => {\n if (event.type !== \"request\" || event.data.warningCount === 0) return;\n\n for (const warning of collector.getRequest(event.data.id)?.warnings ?? []) {\n const where = warning.caller ? ` — ${warning.caller.file}:${warning.caller.line}` : \"\";\n\n log.warn(\n \"devtools\",\n \"n+1\",\n `${warning.message}${where} (${event.data.method} ${event.data.path})`,\n );\n }\n }),\n );\n\n active = true;\n\n log.info(\"devtools\", \"ready\", `devtools ready at ${resolved.path}`);\n },\n async start() {},\n async restart() {\n await connector.shutdown();\n await connector.start();\n },\n async shutdown() {\n for (const dispose of disposers.splice(0)) dispose();\n active = false;\n },\n shouldRestart: () => false,\n };\n\n /** Attaches one source, logging and continuing when it fails to attach. */\n async function attachSource(\n logger: { warn: (module: string, action: string, message: string) => unknown },\n name: string,\n attach: () => Promise<DevtoolsDisposer>,\n ): Promise<void> {\n try {\n disposers.push(await attach());\n } catch (error) {\n logger.warn(\n \"devtools\",\n \"source-failed\",\n `devtools' ${name} source failed to attach: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n\n return connector;\n}\n\n/** Runs `EXPLAIN (FORMAT JSON)` for a stored Postgres query on its own connection. */\nasync function explainQuery(query: DevtoolsQuery): Promise<unknown> {\n const { dataSourceRegistry } = await import(\"@warlock.js/cascade\");\n const dataSource = dataSourceRegistry.get(query.connection);\n const { rows } = await dataSource.raw(`EXPLAIN (FORMAT JSON) ${query.sql}`, query.bindings);\n\n return rows;\n}\n\n/** Lists every registered HTTP route — the same source `warlock routes --json` reads. */\nasync function listRoutes(): Promise<DevtoolsRouteRow[]> {\n const { router } = await import(\"@warlock.js/core\");\n\n return router.list().map((route) => ({\n method: route.method.toUpperCase(),\n path: route.path,\n name: route.name,\n middleware: route.middleware?.map((middleware) => middleware.name || \"anonymous\"),\n }));\n}\n\n/**\n * Builds the OpenAPI 3.1 document from the routes registered in this process —\n * the same generator behind `warlock generate.openapi`. Core is imported\n * lazily so this module stays free of a load-time dependency on it.\n */\nasync function getOpenApiDocument(): Promise<object> {\n const { getDevelopmentOpenApiDocument } = await import(\"@warlock.js/core\");\n const { document } = await getDevelopmentOpenApiDocument();\n\n return document;\n}\n\ntype PostmanBuilder = () => Promise<unknown>;\n\n/**\n * Looks up core's Postman collection builder, `getDevelopmentPostmanCollection`.\n * Older cores do not export it; the route then answers 503. The lookup is\n * guarded because a namespace without the export may throw on access. Core is\n * imported lazily, like the OpenAPI document above.\n */\nasync function findPostmanBuilder(): Promise<PostmanBuilder | undefined> {\n try {\n const core: Record<string, unknown> = await import(\"@warlock.js/core\");\n const build = core.getDevelopmentPostmanCollection;\n\n return typeof build === \"function\" ? (build as PostmanBuilder) : undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Core may return the collection itself or wrap it in `{ collection, warnings }`,\n * like the OpenAPI generator's `{ document, warnings }`.\n */\nfunction unwrapCollection(result: unknown): object {\n if (result && typeof result === \"object\" && \"collection\" in result) {\n return (result as { collection: object }).collection;\n }\n\n return result as object;\n}\n"],"mappings":"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"}
package/esm/index.d.mts CHANGED
@@ -1,3 +1,4 @@
1
+ import { DevtoolsAiEvent, DevtoolsAiRun } from "./sources/ai-source.mjs";
1
2
  import { DevtoolsBackground, DevtoolsCacheOperation, DevtoolsCallerLocation, DevtoolsCollector, DevtoolsDisposer, DevtoolsLogEntry, DevtoolsMail, DevtoolsMailAttachment, DevtoolsMailSummary, DevtoolsOptions, DevtoolsPhase, DevtoolsQuery, DevtoolsRequest, DevtoolsRequestSummary, DevtoolsStreamEvent, DevtoolsWarning, ResolvedDevtoolsOptions } from "./types.mjs";
2
3
  import { devtoolsConnector } from "./devtools-connector.mjs";
3
- export { type DevtoolsBackground, type DevtoolsCacheOperation, type DevtoolsCallerLocation, type DevtoolsCollector, type DevtoolsDisposer, type DevtoolsLogEntry, type DevtoolsMail, type DevtoolsMailAttachment, type DevtoolsMailSummary, type DevtoolsOptions, type DevtoolsPhase, type DevtoolsQuery, type DevtoolsRequest, type DevtoolsRequestSummary, type DevtoolsStreamEvent, type DevtoolsWarning, type ResolvedDevtoolsOptions, devtoolsConnector };
4
+ export { type DevtoolsAiEvent, type DevtoolsAiRun, type DevtoolsBackground, type DevtoolsCacheOperation, type DevtoolsCallerLocation, type DevtoolsCollector, type DevtoolsDisposer, type DevtoolsLogEntry, type DevtoolsMail, type DevtoolsMailAttachment, type DevtoolsMailSummary, type DevtoolsOptions, type DevtoolsPhase, type DevtoolsQuery, type DevtoolsRequest, type DevtoolsRequestSummary, type DevtoolsStreamEvent, type DevtoolsWarning, type ResolvedDevtoolsOptions, devtoolsConnector };
@@ -0,0 +1,16 @@
1
+ //#region ../../packages/devtools/src/process-panel.ts
2
+ /** Load app-owned generated registration only when devtools boots in development. */
3
+ async function loadProcessPanel() {
4
+ const core = await import("@warlock.js/core");
5
+ if (!("srcPath" in core)) return void 0;
6
+ const { srcPath } = core;
7
+ const { existsSync } = await import("node:fs");
8
+ const { pathToFileURL } = await import("node:url");
9
+ const file = srcPath("app/processes/devtools.ts");
10
+ if (!existsSync(file)) return void 0;
11
+ return (await import(pathToFileURL(file).href)).processPanel;
12
+ }
13
+
14
+ //#endregion
15
+ export { loadProcessPanel };
16
+ //# sourceMappingURL=process-panel.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"process-panel.mjs","names":[],"sources":["../../../../../../../packages/devtools/src/process-panel.ts"],"sourcesContent":["/** Structural contract keeps process supervision an optional application feature. */\nexport type ProcessPanel = { list(): Promise<readonly unknown[]> };\n\n/** Load app-owned generated registration only when devtools boots in development. */\nexport async function loadProcessPanel(): Promise<ProcessPanel | undefined> {\n const core = await import(\"@warlock.js/core\");\n if (!(\"srcPath\" in core)) return undefined;\n const { srcPath } = core;\n const { existsSync } = await import(\"node:fs\");\n const { pathToFileURL } = await import(\"node:url\");\n const file = srcPath(\"app/processes/devtools.ts\");\n if (!existsSync(file)) return undefined;\n const module: { processPanel?: ProcessPanel } = await import(pathToFileURL(file).href);\n return module.processPanel;\n}\n"],"mappings":";;AAIA,eAAsB,mBAAsD;CAC1E,MAAM,OAAO,MAAM,OAAO;CAC1B,IAAI,EAAE,aAAa,OAAO,OAAO;CACjC,MAAM,EAAE,YAAY;CACpB,MAAM,EAAE,eAAe,MAAM,OAAO;CACpC,MAAM,EAAE,kBAAkB,MAAM,OAAO;CACvC,MAAM,OAAO,QAAQ,2BAA2B;CAChD,IAAI,CAAC,WAAW,IAAI,GAAG,OAAO;CAE9B,QAAO,MAD+C,OAAO,cAAc,IAAI,CAAC,CAAC,MACpE,CAAC;AAChB"}
@@ -0,0 +1,32 @@
1
+ import { AI_INSTALL_GUIDANCE } from "../sources/ai-source.mjs";
2
+
3
+ //#region ../../packages/devtools/src/server/ai-routes.ts
4
+ /** Read-only AI metadata, inside the same development/loopback guard as every panel. */
5
+ function registerAiRoutes(server, deps) {
6
+ server.get("/api/ai", async (request, reply) => {
7
+ reply.header("Cache-Control", "no-store");
8
+ const source = deps.ai;
9
+ if (!source?.available) return {
10
+ available: false,
11
+ message: source?.message ?? "Install @warlock.js/ai and @warlock.js/ai-panoptic, then restart warlock dev. Import @warlock.js/ai-panoptic and enable ai.config({ panoptic: { observeAll: true } }). If already installed, check that both packages are compatible and can be loaded.",
12
+ runs: []
13
+ };
14
+ const filter = request.query.requestId === void 0 ? void 0 : { requestId: request.query.requestId };
15
+ return {
16
+ available: true,
17
+ runs: source.store.query(filter).map(source.summarize),
18
+ aggregate: source.store.aggregate(filter)
19
+ };
20
+ });
21
+ server.get("/api/ai/runs/:id", async (request, reply) => {
22
+ reply.header("Cache-Control", "no-store");
23
+ const source = deps.ai;
24
+ const trace = source?.available ? source.store.get(request.params.id) : void 0;
25
+ if (!trace) return reply.code(404).send();
26
+ return trace;
27
+ });
28
+ }
29
+
30
+ //#endregion
31
+ export { registerAiRoutes };
32
+ //# sourceMappingURL=ai-routes.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ai-routes.mjs","names":[],"sources":["../../../../../../../../packages/devtools/src/server/ai-routes.ts"],"sourcesContent":["import type { FastifyInstance } from \"fastify\";\nimport { AI_INSTALL_GUIDANCE } from \"../sources/ai-source\";\nimport type { MountDevtoolsRoutesDeps } from \"./types\";\n\n/** Read-only AI metadata, inside the same development/loopback guard as every panel. */\nexport function registerAiRoutes(server: FastifyInstance, deps: MountDevtoolsRoutesDeps): void {\n server.get<{ Querystring: { requestId?: string } }>(\"/api/ai\", async (request, reply) => {\n reply.header(\"Cache-Control\", \"no-store\");\n const source = deps.ai;\n if (!source?.available) {\n return { available: false, message: source?.message ?? AI_INSTALL_GUIDANCE, runs: [] };\n }\n const filter =\n request.query.requestId === undefined ? undefined : { requestId: request.query.requestId };\n return {\n available: true,\n runs: source.store.query(filter).map(source.summarize),\n aggregate: source.store.aggregate(filter),\n };\n });\n\n server.get<{ Params: { id: string } }>(\"/api/ai/runs/:id\", async (request, reply) => {\n reply.header(\"Cache-Control\", \"no-store\");\n const source = deps.ai;\n const trace = source?.available ? source.store.get(request.params.id) : undefined;\n if (!trace) return reply.code(404).send();\n return trace;\n });\n}\n"],"mappings":";;;;AAKA,SAAgB,iBAAiB,QAAyB,MAAqC;CAC7F,OAAO,IAA6C,WAAW,OAAO,SAAS,UAAU;EACvF,MAAM,OAAO,iBAAiB,UAAU;EACxC,MAAM,SAAS,KAAK;EACpB,IAAI,CAAC,QAAQ,WACX,OAAO;GAAE,WAAW;GAAO,SAAS,QAAQ;GAAgC,MAAM,CAAC;EAAE;EAEvF,MAAM,SACJ,QAAQ,MAAM,cAAc,SAAY,SAAY,EAAE,WAAW,QAAQ,MAAM,UAAU;EAC3F,OAAO;GACL,WAAW;GACX,MAAM,OAAO,MAAM,MAAM,MAAM,CAAC,CAAC,IAAI,OAAO,SAAS;GACrD,WAAW,OAAO,MAAM,UAAU,MAAM;EAC1C;CACF,CAAC;CAED,OAAO,IAAgC,oBAAoB,OAAO,SAAS,UAAU;EACnF,MAAM,OAAO,iBAAiB,UAAU;EACxC,MAAM,SAAS,KAAK;EACpB,MAAM,QAAQ,QAAQ,YAAY,OAAO,MAAM,IAAI,QAAQ,OAAO,EAAE,IAAI;EACxE,IAAI,CAAC,OAAO,OAAO,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK;EACxC,OAAO;CACT,CAAC;AACH"}
@@ -1,3 +1,4 @@
1
+ import { registerAiRoutes } from "./ai-routes.mjs";
1
2
  import { registerBackgroundRoutes } from "./background-routes.mjs";
2
3
  import { registerExplainRoute } from "./explain-route.mjs";
3
4
  import { registerDevtoolsGuard } from "./guard.mjs";
@@ -5,6 +6,7 @@ import { registerLogRoutes } from "./log-routes.mjs";
5
6
  import { registerMailRoutes } from "./mail-routes.mjs";
6
7
  import { registerUiAssets } from "./ui-assets.mjs";
7
8
  import { registerOpenApiRoutes } from "./openapi-routes.mjs";
9
+ import { registerProcessRoutes } from "./process-routes.mjs";
8
10
  import { registerRequestsRoutes } from "./requests-routes.mjs";
9
11
  import { registerRouteRoutes } from "./route-routes.mjs";
10
12
  import { registerStreamRoute } from "./stream-route.mjs";
@@ -24,10 +26,12 @@ function mountDevtoolsRoutes(server, deps) {
24
26
  });
25
27
  registerUiAssets(instance);
26
28
  registerRequestsRoutes(instance, deps);
29
+ registerAiRoutes(instance, deps);
27
30
  registerBackgroundRoutes(instance, deps);
28
31
  registerMailRoutes(instance, deps);
29
32
  registerLogRoutes(instance, deps);
30
33
  registerRouteRoutes(instance, deps);
34
+ registerProcessRoutes(instance, deps);
31
35
  registerOpenApiRoutes(instance, deps);
32
36
  registerExplainRoute(instance, deps);
33
37
  registerStreamRoute(instance, deps);
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../../../../../../../packages/devtools/src/server/index.ts"],"sourcesContent":["import type { FastifyError, FastifyInstance } from \"fastify\";\nimport { registerBackgroundRoutes } from \"./background-routes\";\nimport { registerExplainRoute } from \"./explain-route\";\nimport { registerDevtoolsGuard } from \"./guard\";\nimport { registerLogRoutes } from \"./log-routes\";\nimport { registerMailRoutes } from \"./mail-routes\";\nimport { registerOpenApiRoutes } from \"./openapi-routes\";\nimport { registerRequestsRoutes } from \"./requests-routes\";\nimport { registerRouteRoutes } from \"./route-routes\";\nimport { registerStreamRoute } from \"./stream-route\";\nimport type { MountDevtoolsRoutesDeps } from \"./types\";\nimport { registerUiAssets } from \"./ui-assets\";\n\nexport type { DevtoolsRouteRow, MountDevtoolsRoutesDeps } from \"./types\";\n\n/**\n * Mounts the devtools dashboard and its API under `deps.options.path` on\n * `server`. Every route is registered inside a single encapsulated Fastify\n * context so the dev-only, loopback-only guard never reaches the app's\n * other routes.\n */\nexport function mountDevtoolsRoutes(server: FastifyInstance, deps: MountDevtoolsRoutesDeps): void {\n server.register(\n async (instance) => {\n registerDevtoolsGuard(instance, deps);\n\n instance.setErrorHandler((error: FastifyError, _request, reply) => {\n reply.code(error.statusCode ?? 500).send({ error: error.message });\n });\n\n registerUiAssets(instance);\n registerRequestsRoutes(instance, deps);\n registerBackgroundRoutes(instance, deps);\n registerMailRoutes(instance, deps);\n registerLogRoutes(instance, deps);\n registerRouteRoutes(instance, deps);\n registerOpenApiRoutes(instance, deps);\n registerExplainRoute(instance, deps);\n registerStreamRoute(instance, deps);\n },\n { prefix: deps.options.path },\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAqBA,SAAgB,oBAAoB,QAAyB,MAAqC;CAChG,OAAO,SACL,OAAO,aAAa;EAClB,sBAAsB,UAAU,IAAI;EAEpC,SAAS,iBAAiB,OAAqB,UAAU,UAAU;GACjE,MAAM,KAAK,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,EAAE,OAAO,MAAM,QAAQ,CAAC;EACnE,CAAC;EAED,iBAAiB,QAAQ;EACzB,uBAAuB,UAAU,IAAI;EACrC,yBAAyB,UAAU,IAAI;EACvC,mBAAmB,UAAU,IAAI;EACjC,kBAAkB,UAAU,IAAI;EAChC,oBAAoB,UAAU,IAAI;EAClC,sBAAsB,UAAU,IAAI;EACpC,qBAAqB,UAAU,IAAI;EACnC,oBAAoB,UAAU,IAAI;CACpC,GACA,EAAE,QAAQ,KAAK,QAAQ,KAAK,CAC9B;AACF"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../../../../../../../packages/devtools/src/server/index.ts"],"sourcesContent":["import type { FastifyError, FastifyInstance } from \"fastify\";\nimport { registerAiRoutes } from \"./ai-routes\";\nimport { registerBackgroundRoutes } from \"./background-routes\";\nimport { registerExplainRoute } from \"./explain-route\";\nimport { registerDevtoolsGuard } from \"./guard\";\nimport { registerLogRoutes } from \"./log-routes\";\nimport { registerMailRoutes } from \"./mail-routes\";\nimport { registerOpenApiRoutes } from \"./openapi-routes\";\nimport { registerProcessRoutes } from \"./process-routes\";\nimport { registerRequestsRoutes } from \"./requests-routes\";\nimport { registerRouteRoutes } from \"./route-routes\";\nimport { registerStreamRoute } from \"./stream-route\";\nimport type { MountDevtoolsRoutesDeps } from \"./types\";\nimport { registerUiAssets } from \"./ui-assets\";\n\nexport type { DevtoolsRouteRow, MountDevtoolsRoutesDeps } from \"./types\";\n\n/**\n * Mounts the devtools dashboard and its API under `deps.options.path` on\n * `server`. Every route is registered inside a single encapsulated Fastify\n * context so the dev-only, loopback-only guard never reaches the app's\n * other routes.\n */\nexport function mountDevtoolsRoutes(server: FastifyInstance, deps: MountDevtoolsRoutesDeps): void {\n server.register(\n async (instance) => {\n registerDevtoolsGuard(instance, deps);\n\n instance.setErrorHandler((error: FastifyError, _request, reply) => {\n reply.code(error.statusCode ?? 500).send({ error: error.message });\n });\n\n registerUiAssets(instance);\n registerRequestsRoutes(instance, deps);\n registerAiRoutes(instance, deps);\n registerBackgroundRoutes(instance, deps);\n registerMailRoutes(instance, deps);\n registerLogRoutes(instance, deps);\n registerRouteRoutes(instance, deps);\n registerProcessRoutes(instance, deps);\n registerOpenApiRoutes(instance, deps);\n registerExplainRoute(instance, deps);\n registerStreamRoute(instance, deps);\n },\n { prefix: deps.options.path },\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAuBA,SAAgB,oBAAoB,QAAyB,MAAqC;CAChG,OAAO,SACL,OAAO,aAAa;EAClB,sBAAsB,UAAU,IAAI;EAEpC,SAAS,iBAAiB,OAAqB,UAAU,UAAU;GACjE,MAAM,KAAK,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,EAAE,OAAO,MAAM,QAAQ,CAAC;EACnE,CAAC;EAED,iBAAiB,QAAQ;EACzB,uBAAuB,UAAU,IAAI;EACrC,iBAAiB,UAAU,IAAI;EAC/B,yBAAyB,UAAU,IAAI;EACvC,mBAAmB,UAAU,IAAI;EACjC,kBAAkB,UAAU,IAAI;EAChC,oBAAoB,UAAU,IAAI;EAClC,sBAAsB,UAAU,IAAI;EACpC,sBAAsB,UAAU,IAAI;EACpC,qBAAqB,UAAU,IAAI;EACnC,oBAAoB,UAAU,IAAI;CACpC,GACA,EAAE,QAAQ,KAAK,QAAQ,KAAK,CAC9B;AACF"}
@@ -0,0 +1,11 @@
1
+ //#region ../../packages/devtools/src/server/process-routes.ts
2
+ function registerProcessRoutes(server, deps) {
3
+ server.get("/api/processes", async (_request, reply) => {
4
+ reply.header("cache-control", "no-store");
5
+ return deps.processPanel ? await deps.processPanel.list() : [];
6
+ });
7
+ }
8
+
9
+ //#endregion
10
+ export { registerProcessRoutes };
11
+ //# sourceMappingURL=process-routes.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"process-routes.mjs","names":[],"sources":["../../../../../../../../packages/devtools/src/server/process-routes.ts"],"sourcesContent":["import type { FastifyInstance } from \"fastify\";\nimport type { MountDevtoolsRoutesDeps } from \"./types\";\n\nexport function registerProcessRoutes(\n server: FastifyInstance,\n deps: MountDevtoolsRoutesDeps,\n): void {\n server.get(\"/api/processes\", async (_request, reply) => {\n reply.header(\"cache-control\", \"no-store\");\n return deps.processPanel ? await deps.processPanel.list() : [];\n });\n}\n"],"mappings":";AAGA,SAAgB,sBACd,QACA,MACM;CACN,OAAO,IAAI,kBAAkB,OAAO,UAAU,UAAU;EACtD,MAAM,OAAO,iBAAiB,UAAU;EACxC,OAAO,KAAK,eAAe,MAAM,KAAK,aAAa,KAAK,IAAI,CAAC;CAC/D,CAAC;AACH"}
@@ -12,6 +12,7 @@ function registerRequestsRoutes(server, deps) {
12
12
  });
13
13
  server.delete("/api/requests", async (_request, reply) => {
14
14
  deps.collector.clear();
15
+ if (deps.ai?.available) deps.ai.store.clear();
15
16
  reply.code(204).send();
16
17
  });
17
18
  }
@@ -1 +1 @@
1
- {"version":3,"file":"requests-routes.mjs","names":[],"sources":["../../../../../../../../packages/devtools/src/server/requests-routes.ts"],"sourcesContent":["import type { FastifyInstance } from \"fastify\";\nimport type { MountDevtoolsRoutesDeps } from \"./types\";\n\n/** Registers the request list, single-request detail, and clear routes. */\nexport function registerRequestsRoutes(\n server: FastifyInstance,\n deps: MountDevtoolsRoutesDeps,\n): void {\n server.get(\"/api/requests\", async () => deps.collector.listRequests());\n\n server.get<{ Params: { id: string } }>(\"/api/requests/:id\", async (request, reply) => {\n const found = deps.collector.getRequest(request.params.id);\n\n if (!found) {\n reply.code(404).send();\n return;\n }\n\n return found;\n });\n\n server.delete(\"/api/requests\", async (_request, reply) => {\n deps.collector.clear();\n reply.code(204).send();\n });\n}\n"],"mappings":";;AAIA,SAAgB,uBACd,QACA,MACM;CACN,OAAO,IAAI,iBAAiB,YAAY,KAAK,UAAU,aAAa,CAAC;CAErE,OAAO,IAAgC,qBAAqB,OAAO,SAAS,UAAU;EACpF,MAAM,QAAQ,KAAK,UAAU,WAAW,QAAQ,OAAO,EAAE;EAEzD,IAAI,CAAC,OAAO;GACV,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK;GACrB;EACF;EAEA,OAAO;CACT,CAAC;CAED,OAAO,OAAO,iBAAiB,OAAO,UAAU,UAAU;EACxD,KAAK,UAAU,MAAM;EACrB,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK;CACvB,CAAC;AACH"}
1
+ {"version":3,"file":"requests-routes.mjs","names":[],"sources":["../../../../../../../../packages/devtools/src/server/requests-routes.ts"],"sourcesContent":["import type { FastifyInstance } from \"fastify\";\nimport type { MountDevtoolsRoutesDeps } from \"./types\";\n\n/** Registers the request list, single-request detail, and clear routes. */\nexport function registerRequestsRoutes(\n server: FastifyInstance,\n deps: MountDevtoolsRoutesDeps,\n): void {\n server.get(\"/api/requests\", async () => deps.collector.listRequests());\n\n server.get<{ Params: { id: string } }>(\"/api/requests/:id\", async (request, reply) => {\n const found = deps.collector.getRequest(request.params.id);\n\n if (!found) {\n reply.code(404).send();\n return;\n }\n\n return found;\n });\n\n server.delete(\"/api/requests\", async (_request, reply) => {\n deps.collector.clear();\n if (deps.ai?.available) deps.ai.store.clear();\n reply.code(204).send();\n });\n}\n"],"mappings":";;AAIA,SAAgB,uBACd,QACA,MACM;CACN,OAAO,IAAI,iBAAiB,YAAY,KAAK,UAAU,aAAa,CAAC;CAErE,OAAO,IAAgC,qBAAqB,OAAO,SAAS,UAAU;EACpF,MAAM,QAAQ,KAAK,UAAU,WAAW,QAAQ,OAAO,EAAE;EAEzD,IAAI,CAAC,OAAO;GACV,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK;GACrB;EACF;EAEA,OAAO;CACT,CAAC;CAED,OAAO,OAAO,iBAAiB,OAAO,UAAU,UAAU;EACxD,KAAK,UAAU,MAAM;EACrB,IAAI,KAAK,IAAI,WAAW,KAAK,GAAG,MAAM,MAAM;EAC5C,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK;CACvB,CAAC;AACH"}
@@ -18,11 +18,15 @@ function registerStreamRoute(server, deps) {
18
18
  const dispose = deps.collector.subscribe((event) => {
19
19
  res.write(`event: ${event.type}\ndata: ${JSON.stringify(event.data)}\n\n`);
20
20
  });
21
+ const disposeAi = deps.ai?.available ? deps.ai.subscribe((event) => {
22
+ res.write(`event: ai\ndata: ${JSON.stringify(event)}\n\n`);
23
+ }) : void 0;
21
24
  const pingTimer = setInterval(() => res.write(": ping\n\n"), PING_INTERVAL_MS);
22
25
  pingTimer.unref();
23
26
  request.raw.on("close", () => {
24
27
  clearInterval(pingTimer);
25
28
  dispose();
29
+ disposeAi?.();
26
30
  });
27
31
  });
28
32
  }
@@ -1 +1 @@
1
- {"version":3,"file":"stream-route.mjs","names":[],"sources":["../../../../../../../../packages/devtools/src/server/stream-route.ts"],"sourcesContent":["import type { FastifyInstance } from \"fastify\";\nimport type { MountDevtoolsRoutesDeps } from \"./types\";\n\nconst PING_INTERVAL_MS = 15_000;\n\n/**\n * Registers the SSE stream: one `event: <type>\\ndata: <json>\\n\\n` frame per\n * collector event, plus a `: ping` comment every 15s to keep intermediaries\n * from closing an idle connection. The reply is hijacked so Fastify never\n * tries to serialize or close the response itself.\n */\nexport function registerStreamRoute(server: FastifyInstance, deps: MountDevtoolsRoutesDeps): void {\n server.get(\"/api/stream\", async (request, reply) => {\n reply.hijack();\n const res = reply.raw;\n\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-store\",\n Connection: \"keep-alive\",\n });\n\n const dispose = deps.collector.subscribe((event) => {\n res.write(`event: ${event.type}\\ndata: ${JSON.stringify(event.data)}\\n\\n`);\n });\n\n const pingTimer = setInterval(() => res.write(\": ping\\n\\n\"), PING_INTERVAL_MS);\n pingTimer.unref();\n\n request.raw.on(\"close\", () => {\n clearInterval(pingTimer);\n dispose();\n });\n });\n}\n"],"mappings":";AAGA,MAAM,mBAAmB;;;;;;;AAQzB,SAAgB,oBAAoB,QAAyB,MAAqC;CAChG,OAAO,IAAI,eAAe,OAAO,SAAS,UAAU;EAClD,MAAM,OAAO;EACb,MAAM,MAAM,MAAM;EAElB,IAAI,UAAU,KAAK;GACjB,gBAAgB;GAChB,iBAAiB;GACjB,YAAY;EACd,CAAC;EAED,MAAM,UAAU,KAAK,UAAU,WAAW,UAAU;GAClD,IAAI,MAAM,UAAU,MAAM,KAAK,UAAU,KAAK,UAAU,MAAM,IAAI,EAAE,KAAK;EAC3E,CAAC;EAED,MAAM,YAAY,kBAAkB,IAAI,MAAM,YAAY,GAAG,gBAAgB;EAC7E,UAAU,MAAM;EAEhB,QAAQ,IAAI,GAAG,eAAe;GAC5B,cAAc,SAAS;GACvB,QAAQ;EACV,CAAC;CACH,CAAC;AACH"}
1
+ {"version":3,"file":"stream-route.mjs","names":[],"sources":["../../../../../../../../packages/devtools/src/server/stream-route.ts"],"sourcesContent":["import type { FastifyInstance } from \"fastify\";\nimport type { MountDevtoolsRoutesDeps } from \"./types\";\n\nconst PING_INTERVAL_MS = 15_000;\n\n/**\n * Registers the SSE stream: one `event: <type>\\ndata: <json>\\n\\n` frame per\n * collector event, plus a `: ping` comment every 15s to keep intermediaries\n * from closing an idle connection. The reply is hijacked so Fastify never\n * tries to serialize or close the response itself.\n */\nexport function registerStreamRoute(server: FastifyInstance, deps: MountDevtoolsRoutesDeps): void {\n server.get(\"/api/stream\", async (request, reply) => {\n reply.hijack();\n const res = reply.raw;\n\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-store\",\n Connection: \"keep-alive\",\n });\n\n const dispose = deps.collector.subscribe((event) => {\n res.write(`event: ${event.type}\\ndata: ${JSON.stringify(event.data)}\\n\\n`);\n });\n\n const disposeAi = deps.ai?.available\n ? deps.ai.subscribe((event) => {\n res.write(`event: ai\\ndata: ${JSON.stringify(event)}\\n\\n`);\n })\n : undefined;\n\n const pingTimer = setInterval(() => res.write(\": ping\\n\\n\"), PING_INTERVAL_MS);\n pingTimer.unref();\n\n request.raw.on(\"close\", () => {\n clearInterval(pingTimer);\n dispose();\n disposeAi?.();\n });\n });\n}\n"],"mappings":";AAGA,MAAM,mBAAmB;;;;;;;AAQzB,SAAgB,oBAAoB,QAAyB,MAAqC;CAChG,OAAO,IAAI,eAAe,OAAO,SAAS,UAAU;EAClD,MAAM,OAAO;EACb,MAAM,MAAM,MAAM;EAElB,IAAI,UAAU,KAAK;GACjB,gBAAgB;GAChB,iBAAiB;GACjB,YAAY;EACd,CAAC;EAED,MAAM,UAAU,KAAK,UAAU,WAAW,UAAU;GAClD,IAAI,MAAM,UAAU,MAAM,KAAK,UAAU,KAAK,UAAU,MAAM,IAAI,EAAE,KAAK;EAC3E,CAAC;EAED,MAAM,YAAY,KAAK,IAAI,YACvB,KAAK,GAAG,WAAW,UAAU;GAC3B,IAAI,MAAM,oBAAoB,KAAK,UAAU,KAAK,EAAE,KAAK;EAC3D,CAAC,IACD;EAEJ,MAAM,YAAY,kBAAkB,IAAI,MAAM,YAAY,GAAG,gBAAgB;EAC7E,UAAU,MAAM;EAEhB,QAAQ,IAAI,GAAG,eAAe;GAC5B,cAAc,SAAS;GACvB,QAAQ;GACR,YAAY;EACd,CAAC;CACH,CAAC;AACH"}
@@ -0,0 +1,29 @@
1
+ import "../types.mjs";
2
+ import { Trace, TraceSpan } from "@warlock.js/ai-panoptic";
3
+ //#region ../../packages/devtools/src/sources/ai-source.d.ts
4
+ export type DevtoolsAiEvent = {
5
+ traceId: string;
6
+ requestId?: string;
7
+ };
8
+ export type DevtoolsAiRun = {
9
+ traceId: string;
10
+ requestId?: string;
11
+ sessionId?: string;
12
+ name: string;
13
+ status: TraceSpan["status"];
14
+ startedAt: string;
15
+ duration: number;
16
+ usage: Trace["usage"];
17
+ cost?: number;
18
+ models: string[];
19
+ toolCalls: number;
20
+ retries: number;
21
+ cacheHits?: number;
22
+ incomplete: boolean;
23
+ truncatedTrips: number;
24
+ compactions: number;
25
+ skippedCompactions: number;
26
+ providerCompactions: number;
27
+ };
28
+ //#endregion
29
+ //# sourceMappingURL=ai-source.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ai-source.d.mts","names":[],"sources":["../../../../../../../../packages/devtools/src/sources/ai-source.ts"],"mappings":";;;YAOY;EAAoB;EAAiB;;YAarC;EACV;EACA;EACA;EACA;EACA,QAAQ;EACR;EACA;EACA,OAAO;EACP;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA"}
@@ -0,0 +1,136 @@
1
+ import { toSafeValue } from "../safe-value.mjs";
2
+
3
+ //#region ../../packages/devtools/src/sources/ai-source.ts
4
+ const AI_INSTALL_GUIDANCE = "Install @warlock.js/ai and @warlock.js/ai-panoptic, then restart warlock dev. Import @warlock.js/ai-panoptic and enable ai.config({ panoptic: { observeAll: true } }). If already installed, check that both packages are compatible and can be loaded.";
5
+ /** Summaries use root usage once: nested spans already roll up into it. */
6
+ function summarizeAiTrace(trace, cost) {
7
+ const models = /* @__PURE__ */ new Set();
8
+ let toolCalls = 0;
9
+ let retries = 0;
10
+ let cacheHits;
11
+ let incomplete = false;
12
+ let truncatedTrips = 0;
13
+ let compactions = 0;
14
+ let skippedCompactions = 0;
15
+ let providerCompactions = 0;
16
+ const visit = (span) => {
17
+ const attributes = span.attributes ?? {};
18
+ const model = attributes["agent.model.name"];
19
+ const provider = attributes["agent.model.provider"];
20
+ if (typeof model === "string") models.add(typeof provider === "string" ? `${provider}/${model}` : model);
21
+ if (span.type === "tool") toolCalls += 1;
22
+ if (typeof attributes.retries === "number") retries += attributes.retries;
23
+ if (typeof attributes["agent.cacheHits"] === "number") cacheHits = (cacheHits ?? 0) + attributes["agent.cacheHits"];
24
+ if (typeof attributes["agent.truncatedTrips"] === "number") truncatedTrips += attributes["agent.truncatedTrips"];
25
+ if (typeof attributes["agent.providerCompactions"] === "number") providerCompactions += attributes["agent.providerCompactions"];
26
+ if (Array.isArray(attributes["agent.compactions"])) {
27
+ const records = attributes["agent.compactions"];
28
+ for (const record of records) {
29
+ if (typeof record !== "object" || record === null || !("type" in record)) continue;
30
+ if (record.type === "applied") compactions += 1;
31
+ if (record.type === "skipped") skippedCompactions += 1;
32
+ }
33
+ }
34
+ incomplete ||= span.status === "incomplete" || span.error?.type === "AgentIncompleteError" || span.error?.type === "AGENT_INCOMPLETE";
35
+ span.children.forEach(visit);
36
+ };
37
+ visit(trace.root);
38
+ return {
39
+ traceId: trace.traceId,
40
+ requestId: trace.requestId,
41
+ sessionId: trace.sessionId,
42
+ name: trace.root.name,
43
+ status: trace.root.status,
44
+ startedAt: trace.startedAt,
45
+ duration: trace.duration,
46
+ usage: trace.usage,
47
+ cost,
48
+ models: [...models],
49
+ toolCalls,
50
+ retries,
51
+ cacheHits,
52
+ incomplete,
53
+ truncatedTrips,
54
+ compactions,
55
+ skippedCompactions,
56
+ providerCompactions
57
+ };
58
+ }
59
+ /** Content-free snapshot; never retain prompts, completions or tool arguments/results. */
60
+ function snapshotSpan(span) {
61
+ return {
62
+ spanId: span.spanId,
63
+ parentSpanId: span.parentSpanId,
64
+ traceId: span.traceId,
65
+ sessionId: span.sessionId,
66
+ name: span.name,
67
+ version: span.version,
68
+ type: span.type,
69
+ status: span.status,
70
+ startedAt: span.startedAt,
71
+ endedAt: span.endedAt,
72
+ duration: span.duration,
73
+ usage: structuredClone(span.usage),
74
+ error: span.error ? {
75
+ type: span.error.type,
76
+ message: span.error.message
77
+ } : void 0,
78
+ attributes: toSafeValue(span.attributes),
79
+ children: span.children.map(snapshotSpan)
80
+ };
81
+ }
82
+ /**
83
+ * Optional, lazy integration. Retains only traces collected after devtools boots,
84
+ * using Panoptic's bounded query store; existing exporter/cache stores stay independent.
85
+ */
86
+ async function attachAiSource(capacity) {
87
+ let panoptic;
88
+ try {
89
+ panoptic = await import("@warlock.js/ai-panoptic");
90
+ if (typeof panoptic.subscribeTraces !== "function") throw new Error("Trace subscription unavailable");
91
+ } catch {
92
+ return {
93
+ available: false,
94
+ message: AI_INSTALL_GUIDANCE,
95
+ dispose() {}
96
+ };
97
+ }
98
+ const { currentRequestId } = await import("../current-request-id.mjs");
99
+ const store = panoptic.createInMemoryTraceStore({ capacity });
100
+ const listeners = /* @__PURE__ */ new Set();
101
+ const detach = panoptic.subscribeTraces((trace) => {
102
+ const requestId = currentRequestId() ?? trace.requestId;
103
+ store.add({
104
+ ...trace,
105
+ requestId,
106
+ usage: structuredClone(trace.usage),
107
+ root: snapshotSpan(trace.root)
108
+ });
109
+ for (const listener of listeners) try {
110
+ listener({
111
+ traceId: trace.traceId,
112
+ requestId
113
+ });
114
+ } catch {}
115
+ });
116
+ return {
117
+ available: true,
118
+ store,
119
+ summarize: (trace) => summarizeAiTrace(trace, panoptic.totalCostUsd(trace.usage)),
120
+ subscribe(listener) {
121
+ listeners.add(listener);
122
+ return () => {
123
+ listeners.delete(listener);
124
+ };
125
+ },
126
+ dispose() {
127
+ detach();
128
+ listeners.clear();
129
+ store.clear();
130
+ }
131
+ };
132
+ }
133
+
134
+ //#endregion
135
+ export { AI_INSTALL_GUIDANCE, attachAiSource, summarizeAiTrace };
136
+ //# sourceMappingURL=ai-source.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ai-source.mjs","names":[],"sources":["../../../../../../../../packages/devtools/src/sources/ai-source.ts"],"sourcesContent":["import type { Trace, TraceSpan, TraceStoreContract } from \"@warlock.js/ai-panoptic\";\nimport { toSafeValue } from \"../safe-value\";\nimport type { DevtoolsDisposer } from \"../types\";\n\nexport const AI_INSTALL_GUIDANCE =\n \"Install @warlock.js/ai and @warlock.js/ai-panoptic, then restart warlock dev. Import @warlock.js/ai-panoptic and enable ai.config({ panoptic: { observeAll: true } }). If already installed, check that both packages are compatible and can be loaded.\";\n\nexport type DevtoolsAiEvent = { traceId: string; requestId?: string };\n\n/** Only the connector's development branch constructs this source. */\nexport type DevtoolsAiSource =\n | { available: false; message: string; dispose: DevtoolsDisposer }\n | {\n available: true;\n store: TraceStoreContract;\n summarize: (trace: Trace) => DevtoolsAiRun;\n subscribe: (listener: (event: DevtoolsAiEvent) => void) => DevtoolsDisposer;\n dispose: DevtoolsDisposer;\n };\n\nexport type DevtoolsAiRun = {\n traceId: string;\n requestId?: string;\n sessionId?: string;\n name: string;\n status: TraceSpan[\"status\"];\n startedAt: string;\n duration: number;\n usage: Trace[\"usage\"];\n cost?: number;\n models: string[];\n toolCalls: number;\n retries: number;\n cacheHits?: number;\n incomplete: boolean;\n truncatedTrips: number;\n compactions: number;\n skippedCompactions: number;\n providerCompactions: number;\n};\n\n/** Summaries use root usage once: nested spans already roll up into it. */\nexport function summarizeAiTrace(trace: Trace, cost?: number): DevtoolsAiRun {\n const models = new Set<string>();\n let toolCalls = 0;\n let retries = 0;\n let cacheHits: number | undefined;\n let incomplete = false;\n let truncatedTrips = 0;\n let compactions = 0;\n let skippedCompactions = 0;\n let providerCompactions = 0;\n const visit = (span: TraceSpan): void => {\n const attributes = span.attributes ?? {};\n const model = attributes[\"agent.model.name\"];\n const provider = attributes[\"agent.model.provider\"];\n if (typeof model === \"string\")\n models.add(typeof provider === \"string\" ? `${provider}/${model}` : model);\n if (span.type === \"tool\") toolCalls += 1;\n if (typeof attributes.retries === \"number\") retries += attributes.retries;\n if (typeof attributes[\"agent.cacheHits\"] === \"number\")\n cacheHits = (cacheHits ?? 0) + attributes[\"agent.cacheHits\"];\n if (typeof attributes[\"agent.truncatedTrips\"] === \"number\")\n truncatedTrips += attributes[\"agent.truncatedTrips\"];\n if (typeof attributes[\"agent.providerCompactions\"] === \"number\")\n providerCompactions += attributes[\"agent.providerCompactions\"];\n if (Array.isArray(attributes[\"agent.compactions\"])) {\n const records: unknown[] = attributes[\"agent.compactions\"];\n for (const record of records) {\n if (typeof record !== \"object\" || record === null || !(\"type\" in record)) continue;\n if (record.type === \"applied\") compactions += 1;\n if (record.type === \"skipped\") skippedCompactions += 1;\n }\n }\n incomplete ||=\n span.status === \"incomplete\" ||\n span.error?.type === \"AgentIncompleteError\" ||\n span.error?.type === \"AGENT_INCOMPLETE\";\n span.children.forEach(visit);\n };\n visit(trace.root);\n return {\n traceId: trace.traceId,\n requestId: trace.requestId,\n sessionId: trace.sessionId,\n name: trace.root.name,\n status: trace.root.status,\n startedAt: trace.startedAt,\n duration: trace.duration,\n usage: trace.usage,\n cost,\n models: [...models],\n toolCalls,\n retries,\n cacheHits,\n incomplete,\n truncatedTrips,\n compactions,\n skippedCompactions,\n providerCompactions,\n };\n}\n\n/** Content-free snapshot; never retain prompts, completions or tool arguments/results. */\nfunction snapshotSpan(span: TraceSpan): TraceSpan {\n return {\n spanId: span.spanId,\n parentSpanId: span.parentSpanId,\n traceId: span.traceId,\n sessionId: span.sessionId,\n name: span.name,\n version: span.version,\n type: span.type,\n status: span.status,\n startedAt: span.startedAt,\n endedAt: span.endedAt,\n duration: span.duration,\n usage: structuredClone(span.usage),\n error: span.error ? { type: span.error.type, message: span.error.message } : undefined,\n attributes: toSafeValue(span.attributes) as Record<string, unknown> | undefined,\n children: span.children.map(snapshotSpan),\n };\n}\n\n/**\n * Optional, lazy integration. Retains only traces collected after devtools boots,\n * using Panoptic's bounded query store; existing exporter/cache stores stay independent.\n */\nexport async function attachAiSource(capacity: number): Promise<DevtoolsAiSource> {\n let panoptic: typeof import(\"@warlock.js/ai-panoptic\");\n try {\n panoptic = await import(\"@warlock.js/ai-panoptic\");\n if (typeof panoptic.subscribeTraces !== \"function\")\n throw new Error(\"Trace subscription unavailable\");\n } catch {\n return { available: false, message: AI_INSTALL_GUIDANCE, dispose() {} };\n }\n\n const { currentRequestId } = await import(\"../current-request-id\");\n const store = panoptic.createInMemoryTraceStore({ capacity });\n const listeners = new Set<(event: DevtoolsAiEvent) => void>();\n const detach = panoptic.subscribeTraces((trace) => {\n // Read ALS before any asynchronous exporter work, including for runs that\n // finish after the HTTP response. Never use sessionId as a request id.\n const requestId = currentRequestId() ?? trace.requestId;\n store.add({\n ...trace,\n requestId,\n usage: structuredClone(trace.usage),\n root: snapshotSpan(trace.root),\n });\n for (const listener of listeners) {\n try {\n listener({ traceId: trace.traceId, requestId });\n } catch {\n /* isolate clients */\n }\n }\n });\n\n return {\n available: true,\n store,\n summarize: (trace) => summarizeAiTrace(trace, panoptic.totalCostUsd(trace.usage)),\n subscribe(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n dispose() {\n detach();\n listeners.clear();\n store.clear();\n },\n };\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
- import { currentRequestId } from "../current-request-id.mjs";
2
1
  import { toSafeValue } from "../safe-value.mjs";
2
+ import { currentRequestId } from "../current-request-id.mjs";
3
3
  import { LogChannel, log } from "@warlock.js/logger";
4
4
 
5
5
  //#region ../../packages/devtools/src/sources/log-source.ts
@@ -1,5 +1,5 @@
1
- import { currentRequestId } from "../current-request-id.mjs";
2
1
  import { toSafeValue } from "../safe-value.mjs";
2
+ import { currentRequestId } from "../current-request-id.mjs";
3
3
  import { captureCallerLocation } from "../caller-location.mjs";
4
4
  import { applyRedact, log } from "@warlock.js/logger";
5
5
 
package/esm/types.d.mts CHANGED
@@ -1,3 +1,4 @@
1
+ import { DevtoolsAiEvent } from "./sources/ai-source.mjs";
1
2
  //#region ../../packages/devtools/src/types.d.ts
2
3
  /**
3
4
  * The records the devtools collector keeps and the dashboard API returns.
@@ -179,6 +180,9 @@ export type DevtoolsStreamEvent = {
179
180
  } | {
180
181
  type: "log";
181
182
  data: DevtoolsLogEntry;
183
+ } | {
184
+ type: "ai";
185
+ data: DevtoolsAiEvent;
182
186
  };
183
187
  /** Unsubscribes a listener or detaches a source. */
184
188
  export type DevtoolsDisposer = () => void;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../packages/devtools/src/types.ts"],"mappings":";;;;;;;;;YASY;;;;;;EAMV;;;;;;EAMA;;;;;;;EAOA;;;YAIU,0BAA0B,SAAS;;YAGnC;;EAEV;EACA;EACA;;;YAIU;EACV;EACA;EACA;EACA,QAAQ;;;YAIE;;EAEV;EACA;EACA;EACA;;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;;EAEA;;;;;;EAMA;;EAEA,SAAS;;;YAIC;EACV;EACA;EACA;EACA;EACA;EACA;EACA;;;YAIU;EACV;EACA;EACA;EACA;EACA;;EAEA;;;YAIU;EACV;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,SAAS;EACT,aAAa;;;YAIH;EACV;EACA;EACA;;;YAIU;EACV;EACA;EACA;EACA;EACA,SAAS;;;YAIC;;EAEV;EACA;EACA;;EAEA;EACA;EACA;;EAEA;EACA;EACA,QAAQ;EACR,SAAS;EACT,OAAO;EACP,MAAM;;EAEN;EACA,UAAU;;;YAIA;EACV;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;YAIU;EACV;EACA;EACA;EACA;EACA;;;;;;YAOU;EACV,SAAS;EACT,OAAO;EACP,MAAM;;;YAII;EACN;EAAiB,MAAM;;EACvB;EAAc,MAAM;;EACpB;EAAa,MAAM;;;YAGb;;;;;;YAOA;EACV,aAAa;IACX;IACA;IACA;IACA;IACA;;;EAGF,WACE,YACA;IAAU;IAAiB;IAAoB;IAAgB;;EAEjE,SAAS,mBAAmB,OAAO;;EAEnC,SACE,+BACA,OAAO,KAAK,iCACX;EACH,kBAAkB,+BAA+B,WAAW;EAC5D,OAAO,+BAA+B,OAAO;EAC7C,QAAQ,MAAM;EACd,gBAAgB;EAChB,WAAW,aAAa;EACxB,aAAa;EACb,QAAQ,aAAa;EACrB,SAAS;IAAW,QAAQ;IAA2B;MAAoB;EAC3E,iBAAiB;EACjB,UAAU,WAAW,OAAO,+BAA+B;EAC3D"}
1
+ {"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../packages/devtools/src/types.ts"],"mappings":";;;;;;;;;;YAWY;;;;;;EAMV;;;;;;EAMA;;;;;;;EAOA;;;YAIU,0BAA0B,SAAS;;YAGnC;;EAEV;EACA;EACA;;;YAIU;EACV;EACA;EACA;EACA,QAAQ;;;YAIE;;EAEV;EACA;EACA;EACA;;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;;EAEA;;;;;;EAMA;;EAEA,SAAS;;;YAIC;EACV;EACA;EACA;EACA;EACA;EACA;EACA;;;YAIU;EACV;EACA;EACA;EACA;EACA;;EAEA;;;YAIU;EACV;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,SAAS;EACT,aAAa;;;YAIH;EACV;EACA;EACA;;;YAIU;EACV;EACA;EACA;EACA;EACA,SAAS;;;YAIC;;EAEV;EACA;EACA;;EAEA;EACA;EACA;;EAEA;EACA;EACA,QAAQ;EACR,SAAS;EACT,OAAO;EACP,MAAM;;EAEN;EACA,UAAU;;;YAIA;EACV;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;YAIU;EACV;EACA;EACA;EACA;EACA;;;;;;YAOU;EACV,SAAS;EACT,OAAO;EACP,MAAM;;;YAII;EACN;EAAiB,MAAM;;EACvB;EAAc,MAAM;;EACpB;EAAa,MAAM;;EACnB;EAAY,MAAM;;;YAGZ;;;;;;YAOA;EACV,aAAa;IACX;IACA;IACA;IACA;IACA;;;EAGF,WACE,YACA;IAAU;IAAiB;IAAoB;IAAgB;;EAEjE,SAAS,mBAAmB,OAAO;;EAEnC,SACE,+BACA,OAAO,KAAK,iCACX;EACH,kBAAkB,+BAA+B,WAAW;EAC5D,OAAO,+BAA+B,OAAO;EAC7C,QAAQ,MAAM;EACd,gBAAgB;EAChB,WAAW,aAAa;EACxB,aAAa;EACb,QAAQ,aAAa;EACrB,SAAS;IAAW,QAAQ;IAA2B;MAAoB;EAC3E,iBAAiB;EACjB,UAAU,WAAW,OAAO,+BAA+B;EAC3D"}