@warlock.js/ai-panoptic 4.4.0 → 4.6.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 (93) hide show
  1. package/CHANGELOG.md +39 -31
  2. package/cjs/index.cjs +2393 -31
  3. package/cjs/index.cjs.map +1 -1
  4. package/esm/collector/collector.d.mts +19 -2
  5. package/esm/collector/collector.d.mts.map +1 -1
  6. package/esm/collector/collector.mjs +40 -6
  7. package/esm/collector/collector.mjs.map +1 -1
  8. package/esm/collector/content-capture.type.d.mts +10 -0
  9. package/esm/collector/content-capture.type.d.mts.map +1 -1
  10. package/esm/collector/extract-span-attributes.d.mts.map +1 -1
  11. package/esm/collector/extract-span-attributes.mjs +3 -0
  12. package/esm/collector/extract-span-attributes.mjs.map +1 -1
  13. package/esm/collector/index.d.mts +1 -1
  14. package/esm/collector/index.mjs +7 -0
  15. package/esm/collector/normalize-error.d.mts.map +1 -1
  16. package/esm/collector/normalize-error.mjs +6 -3
  17. package/esm/collector/normalize-error.mjs.map +1 -1
  18. package/esm/collector/report-to-span.mjs +31 -10
  19. package/esm/collector/report-to-span.mjs.map +1 -1
  20. package/esm/collector/report-to-trace.d.mts +8 -5
  21. package/esm/collector/report-to-trace.d.mts.map +1 -1
  22. package/esm/collector/report-to-trace.mjs +8 -5
  23. package/esm/collector/report-to-trace.mjs.map +1 -1
  24. package/esm/config/apply-panoptic-config.d.mts +27 -0
  25. package/esm/config/apply-panoptic-config.d.mts.map +1 -0
  26. package/esm/config/apply-panoptic-config.mjs +89 -0
  27. package/esm/config/apply-panoptic-config.mjs.map +1 -0
  28. package/esm/config/index.d.mts +2 -0
  29. package/esm/config/index.mjs +3 -0
  30. package/esm/config/panoptic-config.type.d.mts +84 -0
  31. package/esm/config/panoptic-config.type.d.mts.map +1 -0
  32. package/esm/contracts/trace.type.d.mts +9 -2
  33. package/esm/contracts/trace.type.d.mts.map +1 -1
  34. package/esm/dashboard/dashboard.d.mts +32 -0
  35. package/esm/dashboard/dashboard.d.mts.map +1 -0
  36. package/esm/dashboard/dashboard.mjs +132 -0
  37. package/esm/dashboard/dashboard.mjs.map +1 -0
  38. package/esm/dashboard/dashboard.type.d.mts +63 -0
  39. package/esm/dashboard/dashboard.type.d.mts.map +1 -0
  40. package/esm/dashboard/index.d.mts +3 -0
  41. package/esm/dashboard/parse-query.d.mts +1 -0
  42. package/esm/dashboard/parse-query.mjs +54 -0
  43. package/esm/dashboard/parse-query.mjs.map +1 -0
  44. package/esm/dashboard/serve.d.mts +1 -0
  45. package/esm/dashboard/serve.mjs +111 -0
  46. package/esm/dashboard/serve.mjs.map +1 -0
  47. package/esm/dashboard/trace-filter.d.mts +187 -0
  48. package/esm/dashboard/trace-filter.d.mts.map +1 -0
  49. package/esm/dashboard/trace-filter.mjs +270 -0
  50. package/esm/dashboard/trace-filter.mjs.map +1 -0
  51. package/esm/dashboard/ui.html.mjs +1330 -0
  52. package/esm/dashboard/ui.html.mjs.map +1 -0
  53. package/esm/dashboard/warlock-logo.mjs +13 -0
  54. package/esm/dashboard/warlock-logo.mjs.map +1 -0
  55. package/esm/exporters/file/file-exporter.mjs +5 -2
  56. package/esm/exporters/file/file-exporter.mjs.map +1 -1
  57. package/esm/exporters/langfuse/langfuse-exporter.d.mts.map +1 -1
  58. package/esm/exporters/langfuse/langfuse-exporter.mjs +5 -2
  59. package/esm/exporters/langfuse/langfuse-exporter.mjs.map +1 -1
  60. package/esm/exporters/langfuse/langfuse-exporter.type.d.mts +3 -1
  61. package/esm/exporters/langfuse/langfuse-exporter.type.d.mts.map +1 -1
  62. package/esm/exporters/otel/otel-api.shim.type.d.mts +60 -0
  63. package/esm/exporters/otel/otel-api.shim.type.d.mts.map +1 -0
  64. package/esm/exporters/otel/otel-exporter.d.mts.map +1 -1
  65. package/esm/exporters/otel/otel-exporter.mjs.map +1 -1
  66. package/esm/exporters/otel/otel-exporter.type.d.mts +7 -3
  67. package/esm/exporters/otel/otel-exporter.type.d.mts.map +1 -1
  68. package/esm/index.d.mts +9 -2
  69. package/esm/index.mjs +9 -1
  70. package/esm/panoptic/index.mjs +4 -0
  71. package/esm/panoptic/panoptic.d.mts.map +1 -1
  72. package/esm/panoptic/panoptic.mjs +6 -1
  73. package/esm/panoptic/panoptic.mjs.map +1 -1
  74. package/esm/panoptic/panoptic.type.d.mts +15 -0
  75. package/esm/panoptic/panoptic.type.d.mts.map +1 -1
  76. package/esm/register.mjs +12 -0
  77. package/esm/register.mjs.map +1 -0
  78. package/esm/store/cache-trace-store.d.mts +81 -0
  79. package/esm/store/cache-trace-store.d.mts.map +1 -0
  80. package/esm/store/cache-trace-store.mjs +290 -0
  81. package/esm/store/cache-trace-store.mjs.map +1 -0
  82. package/esm/store/in-memory-trace-store.mjs +9 -2
  83. package/esm/store/in-memory-trace-store.mjs.map +1 -1
  84. package/esm/store/index.d.mts +1 -0
  85. package/esm/store/index.mjs +1 -0
  86. package/llms-full.txt +351 -4
  87. package/llms.txt +4 -3
  88. package/package.json +6 -2
  89. package/skills/README.md +7 -3
  90. package/skills/export-traces/SKILL.md +33 -1
  91. package/skills/observe-with-panoptic/SKILL.md +49 -2
  92. package/skills/query-traces/SKILL.md +52 -1
  93. package/skills/use-local-dashboard/SKILL.md +213 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apply-panoptic-config.mjs","names":[],"sources":["../../../../../../../@warlock.js/ai-panoptic/src/config/apply-panoptic-config.ts"],"sourcesContent":["import type { ExecutionReport, Observer } from \"@warlock.js/ai\";\nimport { registerObserver, setObserveAll } from \"@warlock.js/ai\";\nimport type { ExporterContract } from \"../contracts/exporter.contract\";\nimport { dashboard } from \"../dashboard/dashboard\";\nimport type { DashboardHandle, DashboardOptions } from \"../dashboard/dashboard.type\";\nimport { panoptic } from \"../panoptic/panoptic\";\nimport type { Panoptic } from \"../panoptic/panoptic.type\";\nimport { createCacheTraceStore } from \"../store/cache-trace-store\";\nimport type { CacheTraceStoreHandle } from \"../store/cache-trace-store\";\nimport { createInMemoryTraceStore } from \"../store/in-memory-trace-store\";\nimport type { TraceStoreContract } from \"../store/trace-store.contract\";\nimport type { PanopticConfig } from \"./panoptic-config.type\";\n\n/**\n * Module-level applied state. Applying panoptic config is idempotent:\n * the collector is registered as a core `Observer` exactly once, the\n * dashboard is started at most once, and repeat calls only refresh the\n * `observeAll` flag (the latest config wins) without double-registering.\n */\ntype AppliedState = {\n /** The single collector registered as a core `Observer`. */\n panopticInstance: Panoptic;\n /** The dashboard handle, once started. Started at most once. */\n dashboardHandle?: DashboardHandle;\n};\n\nlet applied: AppliedState | undefined;\n\n/**\n * Read a {@link PanopticConfig} and wire panoptic onto the core observe\n * seam — the bridge `ai.config({ panoptic })` resolves through. Builds a\n * collector via the EXISTING {@link panoptic} factory (reusing its\n * collection pipeline, not reinventing it), registers it once via core's\n * `registerObserver` (the subscriber's `collect(report)` structurally IS\n * an {@link Observer}, threaded through a thin wrapper), sets\n * `observeAll`, and — when `config.dashboard` is set — starts the\n * dashboard over the config's store (a store-shaped exporter, or a fresh\n * in-memory store panoptic registers when none was supplied).\n *\n * **Idempotent.** Safe to call on every `onConfigApplied` notification:\n * the observer is registered once, the dashboard started once. Repeat\n * calls update `observeAll` to the latest value but never double-register\n * or double-start. `undefined` config is a no-op (nothing wired).\n *\n * @returns the resolved store-shaped exporter when the dashboard needs\n * one and a fresh store was created — primarily for tests; callers can\n * ignore it.\n */\nexport function applyPanopticConfig(config?: PanopticConfig): void {\n if (config === undefined) {\n return;\n }\n\n // observeAll always tracks the latest config, even on a repeat call.\n setObserveAll(Boolean(config.observeAll));\n\n if (applied === undefined) {\n const exporters = [...(config.exporters ?? [])];\n let store = findStore(exporters);\n\n // Track a cache store separately so we can await its hydration before\n // the dashboard reads it — the in-memory store needs no warm-up.\n let cacheStore: CacheTraceStoreHandle | undefined;\n\n // The dashboard needs a queryable store. If none was supplied via\n // exporters but a dashboard is requested, create one and register it\n // as an exporter so the collector fills it. A configured `cache` picks\n // the durable cache-backed store; otherwise fall back to in-memory so\n // the dashboard works with no cache configured.\n if (store === undefined && config.dashboard) {\n if (config.cache !== undefined) {\n cacheStore = createCacheTraceStore(config.cache);\n store = cacheStore;\n } else {\n store = createInMemoryTraceStore();\n }\n\n exporters.push(store as unknown as ExporterContract);\n }\n\n const panopticInstance = panoptic({\n exporters,\n captureContent: config.captureContent,\n fullHistory: config.fullHistory,\n });\n registerObserver(toObserver(panopticInstance));\n\n applied = { panopticInstance };\n\n if (config.dashboard && store !== undefined) {\n const resolvedStore = store;\n\n // Hydrate the cache mirror (if any) before serving, so a restart\n // surfaces previously-persisted traces immediately. A bare in-memory\n // store resolves the readyGate instantly.\n const readyGate =\n cacheStore !== undefined ? cacheStore.ready() : Promise.resolve();\n\n void readyGate\n .catch(() => {\n // Hydration is best-effort — never block the dashboard on it.\n })\n .then(() => startDashboard(resolvedStore, config.dashboard ?? {}))\n .then((handle) => {\n if (applied !== undefined) {\n applied.dashboardHandle = handle;\n }\n });\n }\n }\n}\n\n/**\n * Reset the applied state — internal, test-only. Closes any running\n * dashboard and forgets the registered collector so a fresh\n * `applyPanopticConfig` starts clean. NOT part of the public surface and\n * does NOT unregister from the core observer registry (use core's\n * `clearObservers` for that in tests).\n */\nexport async function resetAppliedPanopticConfig(): Promise<void> {\n const handle = applied?.dashboardHandle;\n applied = undefined;\n\n if (handle !== undefined) {\n await handle.close();\n }\n}\n\n/**\n * The currently-running dashboard handle, or `undefined` when no\n * dashboard is active. Internal, test-only — lets a spec await the\n * asynchronously-started dashboard. NOT part of the public surface.\n */\nexport function getActiveDashboardHandle(): DashboardHandle | undefined {\n return applied?.dashboardHandle;\n}\n\n/**\n * Wrap a {@link Panoptic} subscriber as a core {@link Observer}. The\n * subscriber already exposes `collect(report)` with a matching signature,\n * but the thin wrapper makes the structural adaptation explicit and keeps\n * the registered object a minimal `Observer` rather than the whole\n * subscriber surface.\n */\nfunction toObserver(instance: Panoptic): Observer {\n return {\n collect(report: ExecutionReport): Promise<void> {\n return instance.collect(report);\n },\n };\n}\n\n/**\n * Find the first exporter that satisfies the {@link TraceStoreContract}\n * read surface (`query` / `get` / `aggregate`) — the in-memory store\n * doubles as an exporter, so a store passed via `exporters` is reused for\n * the dashboard rather than creating a second one.\n */\nfunction findStore(exporters: ExporterContract[]): TraceStoreContract | undefined {\n for (const exporter of exporters) {\n const candidate = exporter as unknown as Partial<TraceStoreContract>;\n\n if (\n typeof candidate.query === \"function\" &&\n typeof candidate.get === \"function\" &&\n typeof candidate.aggregate === \"function\"\n ) {\n return candidate as TraceStoreContract;\n }\n }\n\n return undefined;\n}\n\n/** Start the dashboard, normalizing the `true | DashboardOptions` switch. */\nfunction startDashboard(\n store: TraceStoreContract,\n dashboardConfig: boolean | DashboardOptions,\n): Promise<DashboardHandle> {\n const options: DashboardOptions =\n typeof dashboardConfig === \"object\" ? dashboardConfig : {};\n\n return dashboard(store, options);\n}\n"],"mappings":";;;;;;;AA0BA,IAAI;;;;;;;;;;;;;;;;;;;;;AAsBJ,SAAgB,oBAAoB,QAA+B;CACjE,IAAI,WAAW,QACb;CAIF,cAAc,QAAQ,OAAO,UAAU,CAAC;CAExC,IAAI,YAAY,QAAW;EACzB,MAAM,YAAY,CAAC,GAAI,OAAO,aAAa,CAAC,CAAE;EAC9C,IAAI,QAAQ,UAAU,SAAS;EAI/B,IAAI;EAOJ,IAAI,UAAU,UAAa,OAAO,WAAW;GAC3C,IAAI,OAAO,UAAU,QAAW;IAC9B,aAAa,sBAAsB,OAAO,KAAK;IAC/C,QAAQ;GACV,OACE,QAAQ,yBAAyB;GAGnC,UAAU,KAAK,KAAoC;EACrD;EAEA,MAAM,mBAAmB,SAAS;GAChC;GACA,gBAAgB,OAAO;GACvB,aAAa,OAAO;EACtB,CAAC;EACD,iBAAiB,WAAW,gBAAgB,CAAC;EAE7C,UAAU,EAAE,iBAAiB;EAE7B,IAAI,OAAO,aAAa,UAAU,QAAW;GAC3C,MAAM,gBAAgB;GAQtB,CAFE,eAAe,SAAY,WAAW,MAAM,IAAI,QAAQ,QAAQ,EAEpD,CACX,YAAY,CAEb,CAAC,CAAC,CACD,WAAW,eAAe,eAAe,OAAO,aAAa,CAAC,CAAC,CAAC,CAAC,CACjE,MAAM,WAAW;IAChB,IAAI,YAAY,QACd,QAAQ,kBAAkB;GAE9B,CAAC;EACL;CACF;AACF;;;;;;;;AAkCA,SAAS,WAAW,UAA8B;CAChD,OAAO,EACL,QAAQ,QAAwC;EAC9C,OAAO,SAAS,QAAQ,MAAM;CAChC,EACF;AACF;;;;;;;AAQA,SAAS,UAAU,WAA+D;CAChF,KAAK,MAAM,YAAY,WAAW;EAChC,MAAM,YAAY;EAElB,IACE,OAAO,UAAU,UAAU,cAC3B,OAAO,UAAU,QAAQ,cACzB,OAAO,UAAU,cAAc,YAE/B,OAAO;CAEX;AAGF;;AAGA,SAAS,eACP,OACA,iBAC0B;CAI1B,OAAO,UAAU,OAFf,OAAO,oBAAoB,WAAW,kBAAkB,CAAC,CAE5B;AACjC"}
@@ -0,0 +1,2 @@
1
+ import { PanopticConfig } from "./panoptic-config.type.mjs";
2
+ import { applyPanopticConfig } from "./apply-panoptic-config.mjs";
@@ -0,0 +1,3 @@
1
+ import { applyPanopticConfig } from "./apply-panoptic-config.mjs";
2
+
3
+ export { };
@@ -0,0 +1,84 @@
1
+ import { ExporterContract } from "../contracts/exporter.contract.mjs";
2
+ import { CacheDriverInput } from "../store/cache-trace-store.mjs";
3
+ import { DashboardOptions } from "../dashboard/dashboard.type.mjs";
4
+
5
+ //#region ../@warlock.js/ai-panoptic/src/config/panoptic-config.type.d.ts
6
+ /**
7
+ * Declarative panoptic configuration, set once through the global
8
+ * `ai.config({ panoptic: { … } })`. Sugar layered over the existing
9
+ * {@link import("../panoptic/panoptic").panoptic} factory: reading this
10
+ * slot (on the side-effect import of `@warlock.js/ai-panoptic`) builds
11
+ * the same collector, registers it as a core `Observer`, and optionally
12
+ * starts the local dashboard — so app code never hand-wires a store and
13
+ * a server. The explicit `panoptic({ exporters })` / `.attach()` /
14
+ * `.collect()` API stays fully intact as the underlying primitive.
15
+ *
16
+ * @example
17
+ * import { ai } from "@warlock.js/ai";
18
+ * import "@warlock.js/ai-panoptic"; // side-effect wiring
19
+ *
20
+ * ai.config({
21
+ * panoptic: {
22
+ * exporters: [createInMemoryTraceStore({ capacity: 5000 })],
23
+ * dashboard: { port: 4319, open: true },
24
+ * observeAll: true,
25
+ * },
26
+ * });
27
+ */
28
+ type PanopticConfig = {
29
+ /**
30
+ * Exporters the collector feeds (a store, OTel, Langfuse, …). A
31
+ * {@link import("../store/in-memory-trace-store").createInMemoryTraceStore}
32
+ * store doubles as an `ExporterContract`, so passing one here both
33
+ * captures traces and gives the dashboard something to read.
34
+ */
35
+ exporters?: ExporterContract[];
36
+ /**
37
+ * Persist traces through a {@link import("@warlock.js/cache").CacheDriver}
38
+ * (Redis, Postgres, file, …) so they survive a process restart. Accepts a
39
+ * driver instance or a (possibly async) factory resolved lazily on first
40
+ * use — the factory form lets production defer the connect.
41
+ *
42
+ * When set (and no store-shaped exporter was supplied), the dashboard
43
+ * reads a {@link import("../store/cache-trace-store").createCacheTraceStore}
44
+ * store instead of a fresh in-memory one. When omitted, panoptic falls
45
+ * back to {@link import("../store/in-memory-trace-store").createInMemoryTraceStore}
46
+ * — so the dashboard works with no cache configured.
47
+ */
48
+ cache?: CacheDriverInput;
49
+ /**
50
+ * Serve the local dashboard. `true` uses the defaults
51
+ * ({@link DashboardOptions}); an object overrides them. When set, the
52
+ * dashboard reads the store supplied via `exporters` (the first
53
+ * store-shaped exporter), or a fresh in-memory store panoptic creates
54
+ * and registers when none was supplied.
55
+ */
56
+ dashboard?: boolean | DashboardOptions;
57
+ /**
58
+ * Observe every flow (`ai.agent` / `ai.workflow` / `ai.supervisor` /
59
+ * `ai.team`) by default. Individual flows opt out with `observe: false`.
60
+ * Default `false` — opt-in observability.
61
+ */
62
+ observeAll?: boolean;
63
+ /**
64
+ * Capture raw prompt / response / tool I/O onto each span so the dashboard
65
+ * + console exporter can surface them. Off by default (payloads are large
66
+ * and can be sensitive); set `true` for a content-rich local dashboard.
67
+ */
68
+ captureContent?: boolean;
69
+ /**
70
+ * Emit each agent's full message history (every trip, every role) onto
71
+ * `span.input` instead of just `[system, user]`. Requires the agent run to
72
+ * set `captureMessages`. Default `false`.
73
+ */
74
+ fullHistory?: boolean;
75
+ };
76
+ declare module "@warlock.js/ai" {
77
+ interface AIConfig {
78
+ /** Declarative panoptic observability config. See {@link PanopticConfig}. */
79
+ panoptic?: PanopticConfig;
80
+ }
81
+ } //# sourceMappingURL=panoptic-config.type.d.ts.map
82
+ //#endregion
83
+ export { PanopticConfig };
84
+ //# sourceMappingURL=panoptic-config.type.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"panoptic-config.type.d.mts","names":[],"sources":["../../../../../../../@warlock.js/ai-panoptic/src/config/panoptic-config.type.ts"],"mappings":";;;;;;;AA0BA;;;;;;;;;;;;;;;;;;AA8Ca;AACX;KA/CU,cAAA;;;;;;;EAOV,SAAA,GAAY,gBAAA;EAiDe;;;;;;;;;;;;EApC3B,KAAA,GAAQ,gBAAA;;;;;;;;EAQR,SAAA,aAAsB,gBAAA;;;;;;EAMtB,UAAA;;;;;;EAMA,cAAA;;;;;;EAMA,WAAA;AAAA;AAAA;EAAA,UAQU,QAAA;;IAER,QAAA,GAAW,cAAc;EAAA;AAAA"}
@@ -111,9 +111,16 @@ type TraceSpan = {
111
111
  * serialization to a backend collector unchanged.
112
112
  */
113
113
  type TraceSpanError = {
114
- /** Stable error identifier (typed framework error name when available). */type: string; /** Human-readable message. */
115
- message: string; /** Optional captured stack trace, when the source error carried one. */
114
+ /** Stable error identifier (typed framework error name when available). */type: string; /** Human-readable message — secret-scrubbed before storage (S4). */
115
+ message: string; /** Optional captured stack trace, secret-scrubbed when present (S4). */
116
116
  stack?: string;
117
+ /**
118
+ * Optional redacted view of the error's `cause` (S4) — a raw provider
119
+ * SDK error can carry `Authorization` / `x-api-key` / `cookie` on its
120
+ * headers, so the cause is deep-redacted (sensitive keys stripped)
121
+ * before it is attached.
122
+ */
123
+ cause?: unknown;
117
124
  };
118
125
  /**
119
126
  * The root of a Panoptic trace — a top-level {@link TraceSpan} plus the
@@ -1 +1 @@
1
- {"version":3,"file":"trace.type.d.mts","names":[],"sources":["../../../../../../../@warlock.js/ai-panoptic/src/contracts/trace.type.ts"],"mappings":";;;;;AAgCA;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAAY,SAAA;EA2DG;;;;EAtDb,MAAA;EA0EmB;AAAA;AAQrB;;EA7EE,YAAA;EA6EwB;;;;;AAMnB;EA5EL,OAAA;EAqFe;;;;;EA/Ef,SAAA,WAqFM;EAnFN,IAAA;EAuFA;;;;;EAjFA,OAAA,WA2FmB;EAzFnB,IAAA,EAAM,UAAA;EAEN,MAAA,EAAQ,YAAA;EAER,SAAA;EAEA,OAAA;EAEA,QAAA;EAEA,KAAA,EAAO,KAAA;;;;;;EAMP,KAAA,GAAQ,cAAA;;;;;;;;;;EAUR,UAAA,GAAa,MAAA;;;;;;;;EAQb,KAAA;;;;;;EAMA,MAAA;;;;;;EAMA,QAAA,EAAU,SAAA;AAAA;;;;;;KAQA,cAAA;6EAEV,IAAA;EAEA,OAAA;EAEA,KAAA;AAAA;;;;;;;KASU,KAAA;mEAEV,OAAA;EAEA,SAAA;EAEA,IAAA,EAAM,SAAA;EAEN,SAAA;EAEA,OAAA;EAEA,QAAA;EAEA,KAAA,EAAO,KAAK;;;;;;EAMZ,mBAAA;AAAA"}
1
+ {"version":3,"file":"trace.type.d.mts","names":[],"sources":["../../../../../../../@warlock.js/ai-panoptic/src/contracts/trace.type.ts"],"mappings":";;;;;AAgCA;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAAY,SAAA;EA2DG;;;;EAtDb,MAAA;EA0EmB;AAAA;AAQrB;;EA7EE,YAAA;EA6EwB;;;;;;EAtExB,OAAA;EA4FU;;;;;EAtFV,SAAA,WA4FA;EA1FA,IAAA;EA4FA;;;;;EAtFA,OAAA,WAkGmB;EAhGnB,IAAA,EAAM,UAAA;EAEN,MAAA,EAAQ,YAAA;EAER,SAAA;EAEA,OAAA;EAEA,QAAA;EAEA,KAAA,EAAO,KAAA;;;;;;EAMP,KAAA,GAAQ,cAAA;;;;;;;;;;EAUR,UAAA,GAAa,MAAA;;;;;;;;EAQb,KAAA;;;;;;EAMA,MAAA;;;;;;EAMA,QAAA,EAAU,SAAA;AAAA;;;;;;KAQA,cAAA;6EAEV,IAAA;EAEA,OAAA;EAEA,KAAA;;;;;;;EAOA,KAAA;AAAA;;;;;;;KASU,KAAA;mEAEV,OAAA;EAEA,SAAA;EAEA,IAAA,EAAM,SAAA;EAEN,SAAA;EAEA,OAAA;EAEA,QAAA;EAEA,KAAA,EAAO,KAAK;;;;;;EAMZ,mBAAA;AAAA"}
@@ -0,0 +1,32 @@
1
+ import { TraceStoreContract } from "../store/trace-store.contract.mjs";
2
+ import { DashboardHandle, DashboardOptions } from "./dashboard.type.mjs";
3
+
4
+ //#region ../@warlock.js/ai-panoptic/src/dashboard/dashboard.d.ts
5
+ /**
6
+ * Start the local Panoptic dashboard over a {@link TraceStoreContract} —
7
+ * a zero-dependency `node:http` server that serves a read-only JSON API
8
+ * and one self-contained HTML page polling it. The store is the live
9
+ * object the collector fills, so each poll reflects the latest completed
10
+ * traces with no extra wiring.
11
+ *
12
+ * This is a low-level building block. The documented path is
13
+ * `ai.config({ panoptic: { dashboard } })`, which constructs (or reuses)
14
+ * the store and calls this for you. Use it directly only when you manage
15
+ * the store yourself.
16
+ *
17
+ * Binds loopback-only by default (`127.0.0.1`) so prompt content is never
18
+ * exposed to the LAN. Pass `port: 0` for an ephemeral port — the resolved
19
+ * port comes back on the handle. A port already in use rejects with a
20
+ * clear, actionable `Error` rather than the raw `EADDRINUSE`.
21
+ *
22
+ * @example
23
+ * const store = createInMemoryTraceStore();
24
+ * const handle = await dashboard(store, { port: 4319, open: true });
25
+ * console.log(handle.url); // http://127.0.0.1:4319/
26
+ * // ...later:
27
+ * await handle.close();
28
+ */
29
+ declare function dashboard(store: TraceStoreContract, options?: DashboardOptions): Promise<DashboardHandle>;
30
+ //#endregion
31
+ export { dashboard };
32
+ //# sourceMappingURL=dashboard.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dashboard.d.mts","names":[],"sources":["../../../../../../../@warlock.js/ai-panoptic/src/dashboard/dashboard.ts"],"mappings":";;;;;;AAiCA;;;;;;;;;;;;;;;;AAG0B;;;;;;iBAHV,SAAA,CACd,KAAA,EAAO,kBAAA,EACP,OAAA,GAAS,gBAAA,GACR,OAAA,CAAQ,eAAA"}
@@ -0,0 +1,132 @@
1
+ import { createRequestHandler } from "./serve.mjs";
2
+ import { createServer } from "node:http";
3
+
4
+ //#region ../@warlock.js/ai-panoptic/src/dashboard/dashboard.ts
5
+ const DEFAULT_PORT = 4319;
6
+ const DEFAULT_HOST = "127.0.0.1";
7
+ const DEFAULT_TITLE = "Panoptic";
8
+ /**
9
+ * Start the local Panoptic dashboard over a {@link TraceStoreContract} —
10
+ * a zero-dependency `node:http` server that serves a read-only JSON API
11
+ * and one self-contained HTML page polling it. The store is the live
12
+ * object the collector fills, so each poll reflects the latest completed
13
+ * traces with no extra wiring.
14
+ *
15
+ * This is a low-level building block. The documented path is
16
+ * `ai.config({ panoptic: { dashboard } })`, which constructs (or reuses)
17
+ * the store and calls this for you. Use it directly only when you manage
18
+ * the store yourself.
19
+ *
20
+ * Binds loopback-only by default (`127.0.0.1`) so prompt content is never
21
+ * exposed to the LAN. Pass `port: 0` for an ephemeral port — the resolved
22
+ * port comes back on the handle. A port already in use rejects with a
23
+ * clear, actionable `Error` rather than the raw `EADDRINUSE`.
24
+ *
25
+ * @example
26
+ * const store = createInMemoryTraceStore();
27
+ * const handle = await dashboard(store, { port: 4319, open: true });
28
+ * console.log(handle.url); // http://127.0.0.1:4319/
29
+ * // ...later:
30
+ * await handle.close();
31
+ */
32
+ function dashboard(store, options = {}) {
33
+ const port = options.port ?? DEFAULT_PORT;
34
+ const host = options.host ?? DEFAULT_HOST;
35
+ const title = options.title ?? DEFAULT_TITLE;
36
+ const basePath = normalizeBasePath(options.basePath);
37
+ if (!isLoopbackHost(host) && !options.authToken) return Promise.reject(/* @__PURE__ */ new Error(`Panoptic dashboard: binding to a non-loopback host ("${host}") requires an \`authToken\` (the dashboard exposes raw prompt content). Pass \`authToken\`, or bind to 127.0.0.1.`));
38
+ const allowedHosts = (options.allowedHosts ?? defaultAllowedHosts(host)).map((h) => h.toLowerCase());
39
+ const server = createServer(createRequestHandler(store, {
40
+ basePath,
41
+ title,
42
+ authToken: options.authToken,
43
+ allowedHosts
44
+ }));
45
+ return new Promise((resolve, reject) => {
46
+ const onError = (error) => {
47
+ server.off("error", onError);
48
+ if (error.code === "EADDRINUSE") {
49
+ reject(/* @__PURE__ */ new Error(`Panoptic dashboard: port ${port} in use; pass { port: 0 } for an ephemeral port`));
50
+ return;
51
+ }
52
+ reject(error);
53
+ };
54
+ server.on("error", onError);
55
+ server.listen(port, host, () => {
56
+ server.off("error", onError);
57
+ const address = server.address();
58
+ const resolvedPort = typeof address === "object" && address !== null ? address.port : port;
59
+ const url = `http://${host}:${resolvedPort}${basePath}`;
60
+ if (options.open) openBrowser(url);
61
+ resolve({
62
+ url,
63
+ port: resolvedPort,
64
+ close() {
65
+ return new Promise((closeResolve, closeReject) => {
66
+ server.close((closeError) => {
67
+ if (closeError) {
68
+ closeReject(closeError);
69
+ return;
70
+ }
71
+ closeResolve();
72
+ });
73
+ });
74
+ }
75
+ });
76
+ });
77
+ });
78
+ }
79
+ /** Loopback hosts the dashboard may bind without an auth token. */
80
+ function isLoopbackHost(host) {
81
+ const h = host.toLowerCase();
82
+ return h === "127.0.0.1" || h === "::1" || h === "[::1]" || h === "localhost";
83
+ }
84
+ /**
85
+ * Default `Host` allowlist for a given bind host. A loopback bind accepts
86
+ * the loopback names a browser would send; a non-loopback bind defaults to
87
+ * just the bound host (callers can widen via `allowedHosts`).
88
+ */
89
+ function defaultAllowedHosts(host) {
90
+ if (isLoopbackHost(host)) return [
91
+ "localhost",
92
+ "127.0.0.1",
93
+ "[::1]",
94
+ "::1"
95
+ ];
96
+ return [host];
97
+ }
98
+ /**
99
+ * Normalize a caller `basePath` to a leading-and-trailing-slash form the
100
+ * router can prefix routes with. `undefined` / `""` → `"/"`.
101
+ */
102
+ function normalizeBasePath(basePath) {
103
+ if (basePath === void 0 || basePath.length === 0 || basePath === "/") return "/";
104
+ const withLeading = basePath.startsWith("/") ? basePath : `/${basePath}`;
105
+ return withLeading.endsWith("/") ? withLeading : `${withLeading}/`;
106
+ }
107
+ /**
108
+ * Best-effort open of the default browser at `url`. Fire-and-forget and
109
+ * fully swallowed — failing to open a browser must never reject the
110
+ * dashboard start. Uses the platform's native opener via a lazy
111
+ * `node:child_process` import so the dependency is paid only when
112
+ * `open: true`.
113
+ */
114
+ function openBrowser(url) {
115
+ import("node:child_process").then(({ spawn }) => {
116
+ const child = spawn(process.platform === "win32" ? "cmd" : process.platform === "darwin" ? "open" : "xdg-open", process.platform === "win32" ? [
117
+ "/c",
118
+ "start",
119
+ "",
120
+ url
121
+ ] : [url], {
122
+ stdio: "ignore",
123
+ detached: true
124
+ });
125
+ child.on("error", () => {});
126
+ child.unref();
127
+ }).catch(() => {});
128
+ }
129
+
130
+ //#endregion
131
+ export { dashboard };
132
+ //# sourceMappingURL=dashboard.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dashboard.mjs","names":[],"sources":["../../../../../../../@warlock.js/ai-panoptic/src/dashboard/dashboard.ts"],"sourcesContent":["import { createServer } from \"node:http\";\nimport type { TraceStoreContract } from \"../store/trace-store.contract\";\nimport type { DashboardHandle, DashboardOptions } from \"./dashboard.type\";\nimport { createRequestHandler } from \"./serve\";\n\nconst DEFAULT_PORT = 4319;\nconst DEFAULT_HOST = \"127.0.0.1\";\nconst DEFAULT_TITLE = \"Panoptic\";\n\n/**\n * Start the local Panoptic dashboard over a {@link TraceStoreContract} —\n * a zero-dependency `node:http` server that serves a read-only JSON API\n * and one self-contained HTML page polling it. The store is the live\n * object the collector fills, so each poll reflects the latest completed\n * traces with no extra wiring.\n *\n * This is a low-level building block. The documented path is\n * `ai.config({ panoptic: { dashboard } })`, which constructs (or reuses)\n * the store and calls this for you. Use it directly only when you manage\n * the store yourself.\n *\n * Binds loopback-only by default (`127.0.0.1`) so prompt content is never\n * exposed to the LAN. Pass `port: 0` for an ephemeral port — the resolved\n * port comes back on the handle. A port already in use rejects with a\n * clear, actionable `Error` rather than the raw `EADDRINUSE`.\n *\n * @example\n * const store = createInMemoryTraceStore();\n * const handle = await dashboard(store, { port: 4319, open: true });\n * console.log(handle.url); // http://127.0.0.1:4319/\n * // ...later:\n * await handle.close();\n */\nexport function dashboard(\n store: TraceStoreContract,\n options: DashboardOptions = {},\n): Promise<DashboardHandle> {\n const port = options.port ?? DEFAULT_PORT;\n const host = options.host ?? DEFAULT_HOST;\n const title = options.title ?? DEFAULT_TITLE;\n const basePath = normalizeBasePath(options.basePath);\n\n // Secure-by-default off loopback (S4): a non-loopback bind without an\n // auth token would expose raw prompt content to the network, so refuse\n // to start rather than silently exposing it.\n if (!isLoopbackHost(host) && !options.authToken) {\n return Promise.reject(\n new Error(\n `Panoptic dashboard: binding to a non-loopback host (\"${host}\") requires an \\`authToken\\` ` +\n \"(the dashboard exposes raw prompt content). Pass `authToken`, or bind to 127.0.0.1.\",\n ),\n );\n }\n\n const allowedHosts = (options.allowedHosts ?? defaultAllowedHosts(host)).map(h =>\n h.toLowerCase(),\n );\n\n const handler = createRequestHandler(store, {\n basePath,\n title,\n authToken: options.authToken,\n allowedHosts,\n });\n const server = createServer(handler);\n\n return new Promise<DashboardHandle>((resolve, reject) => {\n const onError = (error: NodeJS.ErrnoException): void => {\n server.off(\"error\", onError);\n\n if (error.code === \"EADDRINUSE\") {\n reject(\n new Error(\n `Panoptic dashboard: port ${port} in use; pass { port: 0 } for an ephemeral port`,\n ),\n );\n\n return;\n }\n\n reject(error);\n };\n\n server.on(\"error\", onError);\n\n server.listen(port, host, () => {\n server.off(\"error\", onError);\n\n const address = server.address();\n const resolvedPort = typeof address === \"object\" && address !== null ? address.port : port;\n const url = `http://${host}:${resolvedPort}${basePath}`;\n\n if (options.open) {\n openBrowser(url);\n }\n\n resolve({\n url,\n port: resolvedPort,\n close(): Promise<void> {\n return new Promise<void>((closeResolve, closeReject) => {\n server.close((closeError) => {\n if (closeError) {\n closeReject(closeError);\n\n return;\n }\n\n closeResolve();\n });\n });\n },\n });\n });\n });\n}\n\n/** Loopback hosts the dashboard may bind without an auth token. */\nfunction isLoopbackHost(host: string): boolean {\n const h = host.toLowerCase();\n return h === \"127.0.0.1\" || h === \"::1\" || h === \"[::1]\" || h === \"localhost\";\n}\n\n/**\n * Default `Host` allowlist for a given bind host. A loopback bind accepts\n * the loopback names a browser would send; a non-loopback bind defaults to\n * just the bound host (callers can widen via `allowedHosts`).\n */\nfunction defaultAllowedHosts(host: string): string[] {\n if (isLoopbackHost(host)) {\n return [\"localhost\", \"127.0.0.1\", \"[::1]\", \"::1\"];\n }\n return [host];\n}\n\n/**\n * Normalize a caller `basePath` to a leading-and-trailing-slash form the\n * router can prefix routes with. `undefined` / `\"\"` → `\"/\"`.\n */\nfunction normalizeBasePath(basePath?: string): string {\n if (basePath === undefined || basePath.length === 0 || basePath === \"/\") {\n return \"/\";\n }\n\n const withLeading = basePath.startsWith(\"/\") ? basePath : `/${basePath}`;\n\n return withLeading.endsWith(\"/\") ? withLeading : `${withLeading}/`;\n}\n\n/**\n * Best-effort open of the default browser at `url`. Fire-and-forget and\n * fully swallowed — failing to open a browser must never reject the\n * dashboard start. Uses the platform's native opener via a lazy\n * `node:child_process` import so the dependency is paid only when\n * `open: true`.\n */\nfunction openBrowser(url: string): void {\n void import(\"node:child_process\")\n .then(({ spawn }) => {\n const command =\n process.platform === \"win32\" ? \"cmd\" : process.platform === \"darwin\" ? \"open\" : \"xdg-open\";\n const args = process.platform === \"win32\" ? [\"/c\", \"start\", \"\", url] : [url];\n\n const child = spawn(command, args, { stdio: \"ignore\", detached: true });\n child.on(\"error\", () => {\n // Swallow — opening a browser is best-effort.\n });\n child.unref();\n })\n .catch(() => {\n // Swallow — opening a browser is best-effort.\n });\n}\n"],"mappings":";;;;AAKA,MAAM,eAAe;AACrB,MAAM,eAAe;AACrB,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;AA0BtB,SAAgB,UACd,OACA,UAA4B,CAAC,GACH;CAC1B,MAAM,OAAO,QAAQ,QAAQ;CAC7B,MAAM,OAAO,QAAQ,QAAQ;CAC7B,MAAM,QAAQ,QAAQ,SAAS;CAC/B,MAAM,WAAW,kBAAkB,QAAQ,QAAQ;CAKnD,IAAI,CAAC,eAAe,IAAI,KAAK,CAAC,QAAQ,WACpC,OAAO,QAAQ,uBACb,IAAI,MACF,wDAAwD,KAAK,mHAE/D,CACF;CAGF,MAAM,gBAAgB,QAAQ,gBAAgB,oBAAoB,IAAI,EAAC,CAAE,KAAI,MAC3E,EAAE,YAAY,CAChB;CAQA,MAAM,SAAS,aANC,qBAAqB,OAAO;EAC1C;EACA;EACA,WAAW,QAAQ;EACnB;CACF,CACkC,CAAC;CAEnC,OAAO,IAAI,SAA0B,SAAS,WAAW;EACvD,MAAM,WAAW,UAAuC;GACtD,OAAO,IAAI,SAAS,OAAO;GAE3B,IAAI,MAAM,SAAS,cAAc;IAC/B,uBACE,IAAI,MACF,4BAA4B,KAAK,gDACnC,CACF;IAEA;GACF;GAEA,OAAO,KAAK;EACd;EAEA,OAAO,GAAG,SAAS,OAAO;EAE1B,OAAO,OAAO,MAAM,YAAY;GAC9B,OAAO,IAAI,SAAS,OAAO;GAE3B,MAAM,UAAU,OAAO,QAAQ;GAC/B,MAAM,eAAe,OAAO,YAAY,YAAY,YAAY,OAAO,QAAQ,OAAO;GACtF,MAAM,MAAM,UAAU,KAAK,GAAG,eAAe;GAE7C,IAAI,QAAQ,MACV,YAAY,GAAG;GAGjB,QAAQ;IACN;IACA,MAAM;IACN,QAAuB;KACrB,OAAO,IAAI,SAAe,cAAc,gBAAgB;MACtD,OAAO,OAAO,eAAe;OAC3B,IAAI,YAAY;QACd,YAAY,UAAU;QAEtB;OACF;OAEA,aAAa;MACf,CAAC;KACH,CAAC;IACH;GACF,CAAC;EACH,CAAC;CACH,CAAC;AACH;;AAGA,SAAS,eAAe,MAAuB;CAC7C,MAAM,IAAI,KAAK,YAAY;CAC3B,OAAO,MAAM,eAAe,MAAM,SAAS,MAAM,WAAW,MAAM;AACpE;;;;;;AAOA,SAAS,oBAAoB,MAAwB;CACnD,IAAI,eAAe,IAAI,GACrB,OAAO;EAAC;EAAa;EAAa;EAAS;CAAK;CAElD,OAAO,CAAC,IAAI;AACd;;;;;AAMA,SAAS,kBAAkB,UAA2B;CACpD,IAAI,aAAa,UAAa,SAAS,WAAW,KAAK,aAAa,KAClE,OAAO;CAGT,MAAM,cAAc,SAAS,WAAW,GAAG,IAAI,WAAW,IAAI;CAE9D,OAAO,YAAY,SAAS,GAAG,IAAI,cAAc,GAAG,YAAY;AAClE;;;;;;;;AASA,SAAS,YAAY,KAAmB;CACtC,AAAK,OAAO,qBAAqB,CAC9B,MAAM,EAAE,YAAY;EAKnB,MAAM,QAAQ,MAHZ,QAAQ,aAAa,UAAU,QAAQ,QAAQ,aAAa,WAAW,SAAS,YACrE,QAAQ,aAAa,UAAU;GAAC;GAAM;GAAS;GAAI;EAAG,IAAI,CAAC,GAAG,GAExC;GAAE,OAAO;GAAU,UAAU;EAAK,CAAC;EACtE,MAAM,GAAG,eAAe,CAExB,CAAC;EACD,MAAM,MAAM;CACd,CAAC,CAAC,CACD,YAAY,CAEb,CAAC;AACL"}
@@ -0,0 +1,63 @@
1
+ //#region ../@warlock.js/ai-panoptic/src/dashboard/dashboard.type.d.ts
2
+ /**
3
+ * Options for the local Panoptic {@link dashboard} server.
4
+ *
5
+ * Every field is optional and has a safe default. The dashboard is a dev
6
+ * tool over an in-memory trace store: it binds loopback-only by default
7
+ * (so prompt content is never accidentally exposed to the LAN), serves a
8
+ * single self-contained HTML page, and exposes a tiny read-only JSON API
9
+ * the page polls. There is no auth, persistence, or write surface.
10
+ */
11
+ type DashboardOptions = {
12
+ /**
13
+ * TCP port to listen on. Default `4319`. Pass `0` for an ephemeral
14
+ * port — the OS-assigned port is then returned on
15
+ * {@link DashboardHandle.port}.
16
+ */
17
+ port?: number;
18
+ /**
19
+ * Host / interface to bind. Default `"127.0.0.1"` — loopback only, so
20
+ * the dashboard (and the prompt content it can surface) is never
21
+ * exposed beyond the local machine unless you opt in explicitly.
22
+ */
23
+ host?: string;
24
+ /**
25
+ * Mount-path prefix for the JSON API + UI. Default `"/"`. A non-root
26
+ * value (e.g. `"/panoptic"`) prefixes both the page and every
27
+ * `/api/...` route.
28
+ */
29
+ basePath?: string;
30
+ /**
31
+ * Open the default browser at the resolved URL once the server is
32
+ * listening. Default `false`.
33
+ */
34
+ open?: boolean; /** Title shown in the dashboard header. Default `"Panoptic"`. */
35
+ title?: string;
36
+ /**
37
+ * Bearer token required on every request (S4). When set, a request must
38
+ * present `Authorization: Bearer <token>` (or `?token=<token>`), else
39
+ * `401`. **Required** when binding to a non-loopback `host` — starting
40
+ * the dashboard on a public interface without a token throws.
41
+ */
42
+ authToken?: string;
43
+ /**
44
+ * `Host` header allowlist (S4) — defends against DNS-rebinding. A
45
+ * request whose `Host` is not listed gets `403`. Defaults to the
46
+ * loopback names (`localhost`, `127.0.0.1`, `[::1]`) for a loopback
47
+ * bind; for a non-loopback bind you must list the expected host(s).
48
+ */
49
+ allowedHosts?: string[];
50
+ };
51
+ /**
52
+ * Handle returned by {@link dashboard} once the server is listening.
53
+ * Carries the resolved URL + port (useful when `port: 0` requested an
54
+ * ephemeral one) and a `close()` to release the port.
55
+ */
56
+ type DashboardHandle = {
57
+ /** Resolved URL the UI is served at (`http://host:port{basePath}`). */readonly url: string; /** Resolved port — the real OS-assigned port when `port: 0` was passed. */
58
+ readonly port: number; /** Stop the server and release the port. Resolves once fully closed. */
59
+ close(): Promise<void>;
60
+ };
61
+ //#endregion
62
+ export { DashboardHandle, DashboardOptions };
63
+ //# sourceMappingURL=dashboard.type.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dashboard.type.d.mts","names":[],"sources":["../../../../../../../@warlock.js/ai-panoptic/src/dashboard/dashboard.type.ts"],"mappings":";;AASA;;;;;;;;KAAY,gBAAA;EAgCV;;;AAOY;AAQd;EAzCE,IAAA;;;;;;EAMA,IAAA;EAyCgB;AAAA;;;;EAnChB,QAAA;;;;;EAKA,IAAA;EAEA,KAAA;;;;;;;EAOA,SAAA;;;;;;;EAOA,YAAA;AAAA;;;;;;KAQU,eAAA;kFAED,GAAA;WAEA,IAAA;EAET,KAAA,IAAS,OAAO;AAAA"}
@@ -0,0 +1,3 @@
1
+ import { DashboardHandle, DashboardOptions } from "./dashboard.type.mjs";
2
+ import { dashboard } from "./dashboard.mjs";
3
+ import { NO_PROMPT_KEY, NO_SESSION_KEY, NO_TYPE_KEY, PromptGroup, SessionGroup, TraceFilter, TypeGroup, TypeStat, aggregateByType, filterTraces, groupByPrompt, groupBySession, groupByType, heatIntensity, matchesFilter, maxNodeCost, percentile, rollupCost, traceCost, tracePromptKey } from "./trace-filter.mjs";
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,54 @@
1
+ //#region ../@warlock.js/ai-panoptic/src/dashboard/parse-query.ts
2
+ /**
3
+ * The set of terminal statuses a stored trace can carry. Used to keep
4
+ * `parseQuery` from forwarding arbitrary `?status=` junk into the store.
5
+ */
6
+ const KNOWN_STATUSES = [
7
+ "completed",
8
+ "failed",
9
+ "cancelled",
10
+ "max-iterations",
11
+ "awaiting-input",
12
+ "awaiting-approval"
13
+ ];
14
+ /**
15
+ * Map a request's query string onto a {@link TraceQuery} the trace store
16
+ * understands. Every field is optional — an absent param is "don't care",
17
+ * so an empty query string yields an empty filter that matches every
18
+ * stored trace.
19
+ *
20
+ * - `traceId` / `sessionId` — passed through verbatim (first value wins).
21
+ * - `status` — **repeatable**: `?status=failed&status=cancelled` becomes
22
+ * `["failed", "cancelled"]`; a single value stays a scalar. Unknown
23
+ * status tokens are dropped so a typo never silently matches nothing in
24
+ * a confusing way (it simply isn't filtered on).
25
+ * - `startedAfter` / `startedBefore` — forwarded as ISO strings; the
26
+ * store accepts either a string or `Date`, and `matchTrace` does the
27
+ * inclusive bound comparison.
28
+ *
29
+ * Unknown params are ignored. Pure — takes a `URLSearchParams`, returns a
30
+ * plain object — so it's trivially testable without a live server.
31
+ *
32
+ * @example
33
+ * parseQuery(new URLSearchParams("status=failed&status=cancelled&sessionId=s1"));
34
+ * // → { status: ["failed", "cancelled"], sessionId: "s1" }
35
+ */
36
+ function parseQuery(params) {
37
+ const query = {};
38
+ const traceId = params.get("traceId");
39
+ if (traceId !== null && traceId.length > 0) query.traceId = traceId;
40
+ const sessionId = params.get("sessionId");
41
+ if (sessionId !== null && sessionId.length > 0) query.sessionId = sessionId;
42
+ const statuses = params.getAll("status").filter((value) => KNOWN_STATUSES.includes(value));
43
+ if (statuses.length === 1) query.status = statuses[0];
44
+ else if (statuses.length > 1) query.status = statuses;
45
+ const startedAfter = params.get("startedAfter");
46
+ if (startedAfter !== null && startedAfter.length > 0) query.startedAfter = startedAfter;
47
+ const startedBefore = params.get("startedBefore");
48
+ if (startedBefore !== null && startedBefore.length > 0) query.startedBefore = startedBefore;
49
+ return query;
50
+ }
51
+
52
+ //#endregion
53
+ export { parseQuery };
54
+ //# sourceMappingURL=parse-query.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse-query.mjs","names":[],"sources":["../../../../../../../@warlock.js/ai-panoptic/src/dashboard/parse-query.ts"],"sourcesContent":["import type { ReportStatus } from \"@warlock.js/ai\";\nimport type { TraceQuery } from \"../store/trace-query.type\";\n\n/**\n * The set of terminal statuses a stored trace can carry. Used to keep\n * `parseQuery` from forwarding arbitrary `?status=` junk into the store.\n */\nconst KNOWN_STATUSES: readonly ReportStatus[] = [\n \"completed\",\n \"failed\",\n \"cancelled\",\n \"max-iterations\",\n \"awaiting-input\",\n \"awaiting-approval\",\n] as const;\n\n/**\n * Map a request's query string onto a {@link TraceQuery} the trace store\n * understands. Every field is optional — an absent param is \"don't care\",\n * so an empty query string yields an empty filter that matches every\n * stored trace.\n *\n * - `traceId` / `sessionId` — passed through verbatim (first value wins).\n * - `status` — **repeatable**: `?status=failed&status=cancelled` becomes\n * `[\"failed\", \"cancelled\"]`; a single value stays a scalar. Unknown\n * status tokens are dropped so a typo never silently matches nothing in\n * a confusing way (it simply isn't filtered on).\n * - `startedAfter` / `startedBefore` — forwarded as ISO strings; the\n * store accepts either a string or `Date`, and `matchTrace` does the\n * inclusive bound comparison.\n *\n * Unknown params are ignored. Pure — takes a `URLSearchParams`, returns a\n * plain object — so it's trivially testable without a live server.\n *\n * @example\n * parseQuery(new URLSearchParams(\"status=failed&status=cancelled&sessionId=s1\"));\n * // → { status: [\"failed\", \"cancelled\"], sessionId: \"s1\" }\n */\nexport function parseQuery(params: URLSearchParams): TraceQuery {\n const query: TraceQuery = {};\n\n const traceId = params.get(\"traceId\");\n if (traceId !== null && traceId.length > 0) {\n query.traceId = traceId;\n }\n\n const sessionId = params.get(\"sessionId\");\n if (sessionId !== null && sessionId.length > 0) {\n query.sessionId = sessionId;\n }\n\n const statuses = params\n .getAll(\"status\")\n .filter((value): value is ReportStatus => (KNOWN_STATUSES as readonly string[]).includes(value));\n if (statuses.length === 1) {\n query.status = statuses[0];\n } else if (statuses.length > 1) {\n query.status = statuses;\n }\n\n const startedAfter = params.get(\"startedAfter\");\n if (startedAfter !== null && startedAfter.length > 0) {\n query.startedAfter = startedAfter;\n }\n\n const startedBefore = params.get(\"startedBefore\");\n if (startedBefore !== null && startedBefore.length > 0) {\n query.startedBefore = startedBefore;\n }\n\n return query;\n}\n"],"mappings":";;;;;AAOA,MAAM,iBAA0C;CAC9C;CACA;CACA;CACA;CACA;CACA;AACF;;;;;;;;;;;;;;;;;;;;;;;AAwBA,SAAgB,WAAW,QAAqC;CAC9D,MAAM,QAAoB,CAAC;CAE3B,MAAM,UAAU,OAAO,IAAI,SAAS;CACpC,IAAI,YAAY,QAAQ,QAAQ,SAAS,GACvC,MAAM,UAAU;CAGlB,MAAM,YAAY,OAAO,IAAI,WAAW;CACxC,IAAI,cAAc,QAAQ,UAAU,SAAS,GAC3C,MAAM,YAAY;CAGpB,MAAM,WAAW,OACd,OAAO,QAAQ,CAAC,CAChB,QAAQ,UAAkC,eAAqC,SAAS,KAAK,CAAC;CACjG,IAAI,SAAS,WAAW,GACtB,MAAM,SAAS,SAAS;MACnB,IAAI,SAAS,SAAS,GAC3B,MAAM,SAAS;CAGjB,MAAM,eAAe,OAAO,IAAI,cAAc;CAC9C,IAAI,iBAAiB,QAAQ,aAAa,SAAS,GACjD,MAAM,eAAe;CAGvB,MAAM,gBAAgB,OAAO,IAAI,eAAe;CAChD,IAAI,kBAAkB,QAAQ,cAAc,SAAS,GACnD,MAAM,gBAAgB;CAGxB,OAAO;AACT"}
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,111 @@
1
+ import { parseQuery } from "./parse-query.mjs";
2
+ import { dashboardHtml } from "./ui.html.mjs";
3
+
4
+ //#region ../@warlock.js/ai-panoptic/src/dashboard/serve.ts
5
+ /**
6
+ * Security response headers added to every dashboard response (S4):
7
+ * block MIME-sniffing, framing, referrer leakage, and lock the page's
8
+ * content sources down to itself (the UI is fully self-contained — no CDN).
9
+ */
10
+ const SECURITY_HEADERS = {
11
+ "x-content-type-options": "nosniff",
12
+ "x-frame-options": "DENY",
13
+ "referrer-policy": "no-referrer",
14
+ "content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; connect-src 'self'; img-src 'self' data:; base-uri 'none'; form-action 'none'; frame-ancestors 'none'"
15
+ };
16
+ /** Extract the host (no port) from a `Host` header value. */
17
+ function hostHeaderName(hostHeader) {
18
+ if (!hostHeader) return void 0;
19
+ if (hostHeader.startsWith("[")) return hostHeader.slice(0, hostHeader.indexOf("]") + 1);
20
+ const colon = hostHeader.indexOf(":");
21
+ return colon === -1 ? hostHeader : hostHeader.slice(0, colon);
22
+ }
23
+ /** Constant-ish bearer-token check from `Authorization` header or `?token=`. */
24
+ function isAuthorized(req, url, token) {
25
+ const header = req.headers.authorization;
26
+ if (header && header === `Bearer ${token}`) return true;
27
+ return url.searchParams.get("token") === token;
28
+ }
29
+ /**
30
+ * Build the `node:http` request handler for the dashboard over a given
31
+ * trace store. Kept separate from the server lifecycle so it can be unit
32
+ * tested by feeding it a fake `req`/`res` without binding a port.
33
+ *
34
+ * Routes (all under `config.basePath`, all read-only):
35
+ *
36
+ * - `GET api/traces` → `store.query(parseQuery(searchParams))`
37
+ * - `GET api/traces/:id` → `store.get(id)` or `404`
38
+ * - `GET api/aggregate` → `store.aggregate(parseQuery(searchParams))`
39
+ * - `GET {basePath}` → the self-contained HTML page
40
+ *
41
+ * Anything else → `404`. Non-`GET` methods → `405`. The store shapes
42
+ * ({@link Trace}/{@link TraceSpan}) are already JSON-safe, so responses
43
+ * are a plain `JSON.stringify` with no serializer.
44
+ *
45
+ * @example
46
+ * const handler = createRequestHandler(store, { basePath: "/", title: "Panoptic" });
47
+ * http.createServer(handler).listen(4319, "127.0.0.1");
48
+ */
49
+ function createRequestHandler(store, config) {
50
+ const base = config.basePath;
51
+ const apiPrefix = `${base}api`;
52
+ return function handle(req, res) {
53
+ if (req.method !== "GET") {
54
+ sendJson(res, 405, { error: "method_not_allowed" });
55
+ return;
56
+ }
57
+ const url = new URL(req.url ?? "/", "http://localhost");
58
+ const pathname = url.pathname;
59
+ const host = hostHeaderName(req.headers.host);
60
+ if (!host || !config.allowedHosts.includes(host.toLowerCase())) {
61
+ sendJson(res, 403, { error: "host_not_allowed" });
62
+ return;
63
+ }
64
+ if (config.authToken && !isAuthorized(req, url, config.authToken)) {
65
+ sendJson(res, 401, { error: "unauthorized" });
66
+ return;
67
+ }
68
+ if (pathname === `${apiPrefix}/traces`) {
69
+ sendJson(res, 200, store.query(parseQuery(url.searchParams)));
70
+ return;
71
+ }
72
+ if (pathname.startsWith(`${apiPrefix}/traces/`)) {
73
+ const traceId = decodeURIComponent(pathname.slice(`${apiPrefix}/traces/`.length));
74
+ const trace = traceId.length > 0 ? store.get(traceId) : void 0;
75
+ if (trace === void 0) {
76
+ sendJson(res, 404, {
77
+ error: "trace_not_found",
78
+ traceId
79
+ });
80
+ return;
81
+ }
82
+ sendJson(res, 200, trace);
83
+ return;
84
+ }
85
+ if (pathname === `${apiPrefix}/aggregate`) {
86
+ sendJson(res, 200, store.aggregate(parseQuery(url.searchParams)));
87
+ return;
88
+ }
89
+ if (pathname === base || pathname === base.replace(/\/$/, "")) {
90
+ res.writeHead(200, {
91
+ "content-type": "text/html; charset=utf-8",
92
+ ...SECURITY_HEADERS
93
+ });
94
+ res.end(dashboardHtml(base, config.title));
95
+ return;
96
+ }
97
+ sendJson(res, 404, { error: "not_found" });
98
+ };
99
+ }
100
+ /** Write a JSON response with the given status code and security headers. */
101
+ function sendJson(res, status, body) {
102
+ res.writeHead(status, {
103
+ "content-type": "application/json; charset=utf-8",
104
+ ...SECURITY_HEADERS
105
+ });
106
+ res.end(JSON.stringify(body));
107
+ }
108
+
109
+ //#endregion
110
+ export { createRequestHandler };
111
+ //# sourceMappingURL=serve.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serve.mjs","names":[],"sources":["../../../../../../../@warlock.js/ai-panoptic/src/dashboard/serve.ts"],"sourcesContent":["import type { IncomingMessage, ServerResponse } from \"node:http\";\nimport type { TraceStoreContract } from \"../store/trace-store.contract\";\nimport { parseQuery } from \"./parse-query\";\nimport { dashboardHtml } from \"./ui.html\";\n\n/** Fully-resolved routing config the request handler closes over. */\nexport type ServeConfig = {\n /** Normalized mount path, always ending in `/` (e.g. `\"/\"`). */\n basePath: string;\n /** Header title baked into the served page. */\n title: string;\n /** Bearer token required on every request when set (S4). */\n authToken?: string;\n /** `Host` header allowlist — defends against DNS-rebinding (S4). */\n allowedHosts: string[];\n};\n\n/**\n * Security response headers added to every dashboard response (S4):\n * block MIME-sniffing, framing, referrer leakage, and lock the page's\n * content sources down to itself (the UI is fully self-contained — no CDN).\n */\nconst SECURITY_HEADERS: Record<string, string> = {\n \"x-content-type-options\": \"nosniff\",\n \"x-frame-options\": \"DENY\",\n \"referrer-policy\": \"no-referrer\",\n \"content-security-policy\":\n \"default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; connect-src 'self'; img-src 'self' data:; base-uri 'none'; form-action 'none'; frame-ancestors 'none'\",\n};\n\n/** Extract the host (no port) from a `Host` header value. */\nfunction hostHeaderName(hostHeader: string | undefined): string | undefined {\n if (!hostHeader) return undefined;\n // IPv6 literal `[::1]:4319` → `[::1]`; otherwise strip `:port`.\n if (hostHeader.startsWith(\"[\")) {\n return hostHeader.slice(0, hostHeader.indexOf(\"]\") + 1);\n }\n const colon = hostHeader.indexOf(\":\");\n return colon === -1 ? hostHeader : hostHeader.slice(0, colon);\n}\n\n/** Constant-ish bearer-token check from `Authorization` header or `?token=`. */\nfunction isAuthorized(req: IncomingMessage, url: URL, token: string): boolean {\n const header = req.headers.authorization;\n if (header && header === `Bearer ${token}`) return true;\n return url.searchParams.get(\"token\") === token;\n}\n\n/**\n * Build the `node:http` request handler for the dashboard over a given\n * trace store. Kept separate from the server lifecycle so it can be unit\n * tested by feeding it a fake `req`/`res` without binding a port.\n *\n * Routes (all under `config.basePath`, all read-only):\n *\n * - `GET api/traces` → `store.query(parseQuery(searchParams))`\n * - `GET api/traces/:id` → `store.get(id)` or `404`\n * - `GET api/aggregate` → `store.aggregate(parseQuery(searchParams))`\n * - `GET {basePath}` → the self-contained HTML page\n *\n * Anything else → `404`. Non-`GET` methods → `405`. The store shapes\n * ({@link Trace}/{@link TraceSpan}) are already JSON-safe, so responses\n * are a plain `JSON.stringify` with no serializer.\n *\n * @example\n * const handler = createRequestHandler(store, { basePath: \"/\", title: \"Panoptic\" });\n * http.createServer(handler).listen(4319, \"127.0.0.1\");\n */\nexport function createRequestHandler(\n store: TraceStoreContract,\n config: ServeConfig,\n): (req: IncomingMessage, res: ServerResponse) => void {\n const base = config.basePath;\n const apiPrefix = `${base}api`;\n\n return function handle(req: IncomingMessage, res: ServerResponse): void {\n if (req.method !== \"GET\") {\n sendJson(res, 405, { error: \"method_not_allowed\" });\n\n return;\n }\n\n // `req.url` is path + query only; a dummy origin lets URL parse it.\n const url = new URL(req.url ?? \"/\", \"http://localhost\");\n const pathname = url.pathname;\n\n // Host-header allowlist — blocks DNS-rebinding attacks that point a\n // hostile domain at this loopback port (S4).\n const host = hostHeaderName(req.headers.host);\n if (!host || !config.allowedHosts.includes(host.toLowerCase())) {\n sendJson(res, 403, { error: \"host_not_allowed\" });\n\n return;\n }\n\n // Bearer-token auth when configured (always required off loopback, S4).\n if (config.authToken && !isAuthorized(req, url, config.authToken)) {\n sendJson(res, 401, { error: \"unauthorized\" });\n\n return;\n }\n\n if (pathname === `${apiPrefix}/traces`) {\n sendJson(res, 200, store.query(parseQuery(url.searchParams)));\n\n return;\n }\n\n if (pathname.startsWith(`${apiPrefix}/traces/`)) {\n const traceId = decodeURIComponent(pathname.slice(`${apiPrefix}/traces/`.length));\n const trace = traceId.length > 0 ? store.get(traceId) : undefined;\n\n if (trace === undefined) {\n sendJson(res, 404, { error: \"trace_not_found\", traceId });\n\n return;\n }\n\n sendJson(res, 200, trace);\n\n return;\n }\n\n if (pathname === `${apiPrefix}/aggregate`) {\n sendJson(res, 200, store.aggregate(parseQuery(url.searchParams)));\n\n return;\n }\n\n if (pathname === base || pathname === base.replace(/\\/$/, \"\")) {\n res.writeHead(200, {\n \"content-type\": \"text/html; charset=utf-8\",\n ...SECURITY_HEADERS,\n });\n res.end(dashboardHtml(base, config.title));\n\n return;\n }\n\n sendJson(res, 404, { error: \"not_found\" });\n };\n}\n\n/** Write a JSON response with the given status code and security headers. */\nfunction sendJson(res: ServerResponse, status: number, body: unknown): void {\n res.writeHead(status, {\n \"content-type\": \"application/json; charset=utf-8\",\n ...SECURITY_HEADERS,\n });\n res.end(JSON.stringify(body));\n}\n"],"mappings":";;;;;;;;;AAsBA,MAAM,mBAA2C;CAC/C,0BAA0B;CAC1B,mBAAmB;CACnB,mBAAmB;CACnB,2BACE;AACJ;;AAGA,SAAS,eAAe,YAAoD;CAC1E,IAAI,CAAC,YAAY,OAAO;CAExB,IAAI,WAAW,WAAW,GAAG,GAC3B,OAAO,WAAW,MAAM,GAAG,WAAW,QAAQ,GAAG,IAAI,CAAC;CAExD,MAAM,QAAQ,WAAW,QAAQ,GAAG;CACpC,OAAO,UAAU,KAAK,aAAa,WAAW,MAAM,GAAG,KAAK;AAC9D;;AAGA,SAAS,aAAa,KAAsB,KAAU,OAAwB;CAC5E,MAAM,SAAS,IAAI,QAAQ;CAC3B,IAAI,UAAU,WAAW,UAAU,SAAS,OAAO;CACnD,OAAO,IAAI,aAAa,IAAI,OAAO,MAAM;AAC3C;;;;;;;;;;;;;;;;;;;;;AAsBA,SAAgB,qBACd,OACA,QACqD;CACrD,MAAM,OAAO,OAAO;CACpB,MAAM,YAAY,GAAG,KAAK;CAE1B,OAAO,SAAS,OAAO,KAAsB,KAA2B;EACtE,IAAI,IAAI,WAAW,OAAO;GACxB,SAAS,KAAK,KAAK,EAAE,OAAO,qBAAqB,CAAC;GAElD;EACF;EAGA,MAAM,MAAM,IAAI,IAAI,IAAI,OAAO,KAAK,kBAAkB;EACtD,MAAM,WAAW,IAAI;EAIrB,MAAM,OAAO,eAAe,IAAI,QAAQ,IAAI;EAC5C,IAAI,CAAC,QAAQ,CAAC,OAAO,aAAa,SAAS,KAAK,YAAY,CAAC,GAAG;GAC9D,SAAS,KAAK,KAAK,EAAE,OAAO,mBAAmB,CAAC;GAEhD;EACF;EAGA,IAAI,OAAO,aAAa,CAAC,aAAa,KAAK,KAAK,OAAO,SAAS,GAAG;GACjE,SAAS,KAAK,KAAK,EAAE,OAAO,eAAe,CAAC;GAE5C;EACF;EAEA,IAAI,aAAa,GAAG,UAAU,UAAU;GACtC,SAAS,KAAK,KAAK,MAAM,MAAM,WAAW,IAAI,YAAY,CAAC,CAAC;GAE5D;EACF;EAEA,IAAI,SAAS,WAAW,GAAG,UAAU,SAAS,GAAG;GAC/C,MAAM,UAAU,mBAAmB,SAAS,MAAM,GAAG,UAAU,UAAU,MAAM,CAAC;GAChF,MAAM,QAAQ,QAAQ,SAAS,IAAI,MAAM,IAAI,OAAO,IAAI;GAExD,IAAI,UAAU,QAAW;IACvB,SAAS,KAAK,KAAK;KAAE,OAAO;KAAmB;IAAQ,CAAC;IAExD;GACF;GAEA,SAAS,KAAK,KAAK,KAAK;GAExB;EACF;EAEA,IAAI,aAAa,GAAG,UAAU,aAAa;GACzC,SAAS,KAAK,KAAK,MAAM,UAAU,WAAW,IAAI,YAAY,CAAC,CAAC;GAEhE;EACF;EAEA,IAAI,aAAa,QAAQ,aAAa,KAAK,QAAQ,OAAO,EAAE,GAAG;GAC7D,IAAI,UAAU,KAAK;IACjB,gBAAgB;IAChB,GAAG;GACL,CAAC;GACD,IAAI,IAAI,cAAc,MAAM,OAAO,KAAK,CAAC;GAEzC;EACF;EAEA,SAAS,KAAK,KAAK,EAAE,OAAO,YAAY,CAAC;CAC3C;AACF;;AAGA,SAAS,SAAS,KAAqB,QAAgB,MAAqB;CAC1E,IAAI,UAAU,QAAQ;EACpB,gBAAgB;EAChB,GAAG;CACL,CAAC;CACD,IAAI,IAAI,KAAK,UAAU,IAAI,CAAC;AAC9B"}