@sapiom/harness 0.0.0 → 0.1.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 (246) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +44 -43
  3. package/dist/cli/auth.d.ts +26 -0
  4. package/dist/cli/auth.d.ts.map +1 -0
  5. package/dist/cli/auth.js +39 -0
  6. package/dist/cli/auth.js.map +1 -0
  7. package/dist/cli/bin.d.ts +3 -0
  8. package/dist/cli/bin.d.ts.map +1 -0
  9. package/dist/cli/bin.js +163 -0
  10. package/dist/cli/bin.js.map +1 -0
  11. package/dist/cli/consent.d.ts +15 -0
  12. package/dist/cli/consent.d.ts.map +1 -0
  13. package/dist/cli/consent.js +81 -0
  14. package/dist/cli/consent.js.map +1 -0
  15. package/dist/cli/doctor.d.ts +27 -0
  16. package/dist/cli/doctor.d.ts.map +1 -0
  17. package/dist/cli/doctor.js +74 -0
  18. package/dist/cli/doctor.js.map +1 -0
  19. package/dist/cli/machine-id.d.ts +19 -0
  20. package/dist/cli/machine-id.d.ts.map +1 -0
  21. package/dist/cli/machine-id.js +75 -0
  22. package/dist/cli/machine-id.js.map +1 -0
  23. package/dist/cli/paths.d.ts +3 -0
  24. package/dist/cli/paths.d.ts.map +1 -0
  25. package/dist/cli/paths.js +11 -0
  26. package/dist/cli/paths.js.map +1 -0
  27. package/dist/cli/settings.d.ts +26 -0
  28. package/dist/cli/settings.d.ts.map +1 -0
  29. package/dist/cli/settings.js +123 -0
  30. package/dist/cli/settings.js.map +1 -0
  31. package/dist/core/adapters/claude-code.d.ts +41 -0
  32. package/dist/core/adapters/claude-code.d.ts.map +1 -0
  33. package/dist/core/adapters/claude-code.js +238 -0
  34. package/dist/core/adapters/claude-code.js.map +1 -0
  35. package/dist/core/adapters/codex.d.ts +59 -0
  36. package/dist/core/adapters/codex.d.ts.map +1 -0
  37. package/dist/core/adapters/codex.js +319 -0
  38. package/dist/core/adapters/codex.js.map +1 -0
  39. package/dist/core/canvas-body.d.ts +45 -0
  40. package/dist/core/canvas-body.d.ts.map +1 -0
  41. package/dist/core/canvas-body.js +94 -0
  42. package/dist/core/canvas-body.js.map +1 -0
  43. package/dist/core/canvas-cache.d.ts +23 -0
  44. package/dist/core/canvas-cache.d.ts.map +1 -0
  45. package/dist/core/canvas-cache.js +64 -0
  46. package/dist/core/canvas-cache.js.map +1 -0
  47. package/dist/core/canvas-enrich.d.ts +106 -0
  48. package/dist/core/canvas-enrich.d.ts.map +1 -0
  49. package/dist/core/canvas-enrich.js +178 -0
  50. package/dist/core/canvas-enrich.js.map +1 -0
  51. package/dist/core/canvas-enrichment.d.ts +144 -0
  52. package/dist/core/canvas-enrichment.d.ts.map +1 -0
  53. package/dist/core/canvas-enrichment.js +192 -0
  54. package/dist/core/canvas-enrichment.js.map +1 -0
  55. package/dist/core/canvas-graph.d.ts +71 -0
  56. package/dist/core/canvas-graph.d.ts.map +1 -0
  57. package/dist/core/canvas-graph.js +111 -0
  58. package/dist/core/canvas-graph.js.map +1 -0
  59. package/dist/core/canvas-index-classify.d.ts +41 -0
  60. package/dist/core/canvas-index-classify.d.ts.map +1 -0
  61. package/dist/core/canvas-index-classify.js +63 -0
  62. package/dist/core/canvas-index-classify.js.map +1 -0
  63. package/dist/core/canvas-interconnections.d.ts +24 -0
  64. package/dist/core/canvas-interconnections.d.ts.map +1 -0
  65. package/dist/core/canvas-interconnections.js +96 -0
  66. package/dist/core/canvas-interconnections.js.map +1 -0
  67. package/dist/core/canvas-manifest-check.d.ts +15 -0
  68. package/dist/core/canvas-manifest-check.d.ts.map +1 -0
  69. package/dist/core/canvas-manifest-check.js +90 -0
  70. package/dist/core/canvas-manifest-check.js.map +1 -0
  71. package/dist/core/canvas-render.d.ts +73 -0
  72. package/dist/core/canvas-render.d.ts.map +1 -0
  73. package/dist/core/canvas-render.js +145 -0
  74. package/dist/core/canvas-render.js.map +1 -0
  75. package/dist/core/canvas-svg.d.ts +50 -0
  76. package/dist/core/canvas-svg.d.ts.map +1 -0
  77. package/dist/core/canvas-svg.js +240 -0
  78. package/dist/core/canvas-svg.js.map +1 -0
  79. package/dist/core/canvas-template.d.ts +43 -0
  80. package/dist/core/canvas-template.d.ts.map +1 -0
  81. package/dist/core/canvas-template.js +365 -0
  82. package/dist/core/canvas-template.js.map +1 -0
  83. package/dist/core/canvas-watcher.d.ts +25 -0
  84. package/dist/core/canvas-watcher.d.ts.map +1 -0
  85. package/dist/core/canvas-watcher.js +144 -0
  86. package/dist/core/canvas-watcher.js.map +1 -0
  87. package/dist/core/collector/batcher.d.ts +57 -0
  88. package/dist/core/collector/batcher.d.ts.map +1 -0
  89. package/dist/core/collector/batcher.js +143 -0
  90. package/dist/core/collector/batcher.js.map +1 -0
  91. package/dist/core/collector/codex-tailer.d.ts +85 -0
  92. package/dist/core/collector/codex-tailer.d.ts.map +1 -0
  93. package/dist/core/collector/codex-tailer.js +270 -0
  94. package/dist/core/collector/codex-tailer.js.map +1 -0
  95. package/dist/core/collector/normalizer.d.ts +33 -0
  96. package/dist/core/collector/normalizer.d.ts.map +1 -0
  97. package/dist/core/collector/normalizer.js +94 -0
  98. package/dist/core/collector/normalizer.js.map +1 -0
  99. package/dist/core/collector/seq.d.ts +14 -0
  100. package/dist/core/collector/seq.d.ts.map +1 -0
  101. package/dist/core/collector/seq.js +20 -0
  102. package/dist/core/collector/seq.js.map +1 -0
  103. package/dist/core/collector/store.d.ts +16 -0
  104. package/dist/core/collector/store.d.ts.map +1 -0
  105. package/dist/core/collector/store.js +30 -0
  106. package/dist/core/collector/store.js.map +1 -0
  107. package/dist/core/collector/transcript.d.ts +38 -0
  108. package/dist/core/collector/transcript.d.ts.map +1 -0
  109. package/dist/core/collector/transcript.js +110 -0
  110. package/dist/core/collector/transcript.js.map +1 -0
  111. package/dist/core/event-bus.d.ts +9 -0
  112. package/dist/core/event-bus.d.ts.map +1 -0
  113. package/dist/core/event-bus.js +25 -0
  114. package/dist/core/event-bus.js.map +1 -0
  115. package/dist/core/example-seed.d.ts +34 -0
  116. package/dist/core/example-seed.d.ts.map +1 -0
  117. package/dist/core/example-seed.js +277 -0
  118. package/dist/core/example-seed.js.map +1 -0
  119. package/dist/core/inject/claude-settings.d.ts +34 -0
  120. package/dist/core/inject/claude-settings.d.ts.map +1 -0
  121. package/dist/core/inject/claude-settings.js +152 -0
  122. package/dist/core/inject/claude-settings.js.map +1 -0
  123. package/dist/core/inject/mcp-config.d.ts +26 -0
  124. package/dist/core/inject/mcp-config.d.ts.map +1 -0
  125. package/dist/core/inject/mcp-config.js +39 -0
  126. package/dist/core/inject/mcp-config.js.map +1 -0
  127. package/dist/core/inject/retention.d.ts +66 -0
  128. package/dist/core/inject/retention.d.ts.map +1 -0
  129. package/dist/core/inject/retention.js +119 -0
  130. package/dist/core/inject/retention.js.map +1 -0
  131. package/dist/core/inject/system-prompt.d.ts +11 -0
  132. package/dist/core/inject/system-prompt.d.ts.map +1 -0
  133. package/dist/core/inject/system-prompt.js +22 -0
  134. package/dist/core/inject/system-prompt.js.map +1 -0
  135. package/dist/core/macro-runner.d.ts +36 -0
  136. package/dist/core/macro-runner.d.ts.map +1 -0
  137. package/dist/core/macro-runner.js +66 -0
  138. package/dist/core/macro-runner.js.map +1 -0
  139. package/dist/core/macros.d.ts +10 -0
  140. package/dist/core/macros.d.ts.map +1 -0
  141. package/dist/core/macros.js +59 -0
  142. package/dist/core/macros.js.map +1 -0
  143. package/dist/core/path-safety.d.ts +33 -0
  144. package/dist/core/path-safety.d.ts.map +1 -0
  145. package/dist/core/path-safety.js +63 -0
  146. package/dist/core/path-safety.js.map +1 -0
  147. package/dist/core/paths.d.ts +27 -0
  148. package/dist/core/paths.d.ts.map +1 -0
  149. package/dist/core/paths.js +43 -0
  150. package/dist/core/paths.js.map +1 -0
  151. package/dist/core/port-detector.d.ts +69 -0
  152. package/dist/core/port-detector.d.ts.map +1 -0
  153. package/dist/core/port-detector.js +121 -0
  154. package/dist/core/port-detector.js.map +1 -0
  155. package/dist/core/session-manager.d.ts +275 -0
  156. package/dist/core/session-manager.d.ts.map +1 -0
  157. package/dist/core/session-manager.js +729 -0
  158. package/dist/core/session-manager.js.map +1 -0
  159. package/dist/core/task-manager.d.ts +138 -0
  160. package/dist/core/task-manager.d.ts.map +1 -0
  161. package/dist/core/task-manager.js +286 -0
  162. package/dist/core/task-manager.js.map +1 -0
  163. package/dist/core/task-stream.d.ts +32 -0
  164. package/dist/core/task-stream.d.ts.map +1 -0
  165. package/dist/core/task-stream.js +87 -0
  166. package/dist/core/task-stream.js.map +1 -0
  167. package/dist/core/workflow-registry.d.ts +36 -0
  168. package/dist/core/workflow-registry.d.ts.map +1 -0
  169. package/dist/core/workflow-registry.js +220 -0
  170. package/dist/core/workflow-registry.js.map +1 -0
  171. package/dist/core/workspace-context.d.ts +43 -0
  172. package/dist/core/workspace-context.d.ts.map +1 -0
  173. package/dist/core/workspace-context.js +108 -0
  174. package/dist/core/workspace-context.js.map +1 -0
  175. package/dist/core/workspace-watcher.d.ts +26 -0
  176. package/dist/core/workspace-watcher.d.ts.map +1 -0
  177. package/dist/core/workspace-watcher.js +174 -0
  178. package/dist/core/workspace-watcher.js.map +1 -0
  179. package/dist/index.d.ts +6 -0
  180. package/dist/index.d.ts.map +1 -0
  181. package/dist/index.js +6 -0
  182. package/dist/index.js.map +1 -0
  183. package/dist/profiles/canvas-guidelines.d.ts +20 -0
  184. package/dist/profiles/canvas-guidelines.d.ts.map +1 -0
  185. package/dist/profiles/canvas-guidelines.js +48 -0
  186. package/dist/profiles/canvas-guidelines.js.map +1 -0
  187. package/dist/profiles/default.d.ts +10 -0
  188. package/dist/profiles/default.d.ts.map +1 -0
  189. package/dist/profiles/default.js +70 -0
  190. package/dist/profiles/default.js.map +1 -0
  191. package/dist/server/auth.d.ts +9 -0
  192. package/dist/server/auth.d.ts.map +1 -0
  193. package/dist/server/auth.js +28 -0
  194. package/dist/server/auth.js.map +1 -0
  195. package/dist/server/canvas-render.d.ts +18 -0
  196. package/dist/server/canvas-render.d.ts.map +1 -0
  197. package/dist/server/canvas-render.js +24 -0
  198. package/dist/server/canvas-render.js.map +1 -0
  199. package/dist/server/canvas.d.ts +12 -0
  200. package/dist/server/canvas.d.ts.map +1 -0
  201. package/dist/server/canvas.js +174 -0
  202. package/dist/server/canvas.js.map +1 -0
  203. package/dist/server/events-ws.d.ts +11 -0
  204. package/dist/server/events-ws.d.ts.map +1 -0
  205. package/dist/server/events-ws.js +24 -0
  206. package/dist/server/events-ws.js.map +1 -0
  207. package/dist/server/fs.d.ts +4 -0
  208. package/dist/server/fs.d.ts.map +1 -0
  209. package/dist/server/fs.js +88 -0
  210. package/dist/server/fs.js.map +1 -0
  211. package/dist/server/index.d.ts +83 -0
  212. package/dist/server/index.d.ts.map +1 -0
  213. package/dist/server/index.js +0 -0
  214. package/dist/server/index.js.map +1 -0
  215. package/dist/server/ingest.d.ts +74 -0
  216. package/dist/server/ingest.d.ts.map +1 -0
  217. package/dist/server/ingest.js +74 -0
  218. package/dist/server/ingest.js.map +1 -0
  219. package/dist/server/macros.d.ts +32 -0
  220. package/dist/server/macros.d.ts.map +1 -0
  221. package/dist/server/macros.js +76 -0
  222. package/dist/server/macros.js.map +1 -0
  223. package/dist/server/rest.d.ts +75 -0
  224. package/dist/server/rest.d.ts.map +1 -0
  225. package/dist/server/rest.js +235 -0
  226. package/dist/server/rest.js.map +1 -0
  227. package/dist/server/static.d.ts +8 -0
  228. package/dist/server/static.d.ts.map +1 -0
  229. package/dist/server/static.js +36 -0
  230. package/dist/server/static.js.map +1 -0
  231. package/dist/server/terminal-ws.d.ts +9 -0
  232. package/dist/server/terminal-ws.d.ts.map +1 -0
  233. package/dist/server/terminal-ws.js +59 -0
  234. package/dist/server/terminal-ws.js.map +1 -0
  235. package/dist/server/ws-router.d.ts +16 -0
  236. package/dist/server/ws-router.d.ts.map +1 -0
  237. package/dist/server/ws-router.js +26 -0
  238. package/dist/server/ws-router.js.map +1 -0
  239. package/dist/shared/types.d.ts +485 -0
  240. package/dist/shared/types.d.ts.map +1 -0
  241. package/dist/shared/types.js +80 -0
  242. package/dist/shared/types.js.map +1 -0
  243. package/dist/web/assets/index-B9R4TzH7.css +32 -0
  244. package/dist/web/assets/index-Dh97QAgq.js +192 -0
  245. package/dist/web/index.html +27 -0
  246. package/package.json +84 -7
@@ -0,0 +1,365 @@
1
+ /**
2
+ * Canvas kit: a prewritten template — CSS (light + dark, following the
3
+ * app's own theme) and a documented set of markup building blocks — that an
4
+ * agent clones and fills in with real content, instead of hand-writing a
5
+ * canvas from scratch every time. This is deliberately NOT a JSON-data +
6
+ * JS-renderer scheme: the agent authors real HTML/SVG using the template's
7
+ * classes, so it keeps full expressive freedom over the graph itself while
8
+ * the CSS (and therefore the visual language: colors, glow, edge styling,
9
+ * legend markers) is locked and can't drift.
10
+ *
11
+ * `renderCanvasDocument(bodyHtml)` is the single shared shell (CSS + theme
12
+ * switch) — `TEMPLATE_HTML` (the pristine, empty-state document) and
13
+ * `scripts/seed-example.mjs` (prefilled with real content) both wrap their
14
+ * body through it, so the kit and the seed can't drift from each other.
15
+ */
16
+ import * as fs from "node:fs/promises";
17
+ import * as path from "node:path";
18
+ import { CANVAS_DIR, CANVAS_INDEX } from "../shared/types.js";
19
+ import { isLegacyDeterministicCanvas } from "./canvas-index-classify.js";
20
+ /** Lives alongside index.html, in the same CANVAS_DIR — a pristine copy of
21
+ * the template, written once and never touched again, so "clone the
22
+ * template" always has a clean, un-filled-in source to clone from. */
23
+ export const CANVAS_TEMPLATE_FILE = `${CANVAS_DIR}/_template.html`;
24
+ function themeStyleBlock() {
25
+ // Values ported 1:1 from web/src/styles.css's :root (light, default) and
26
+ // [data-theme="dark"] tokens — kept in sync by eye; see this module's own
27
+ // doc comment. Dark is also this canvas kit's own fallback default
28
+ // (prefers-color-scheme: no-preference) since it's the app's own default
29
+ // canvas look before per-theme passthrough landed for embedded iframes.
30
+ return `
31
+ :root {
32
+ --canvas-bg: #f3f4f6;
33
+ --canvas-panel: #f5f5f5;
34
+ --canvas-border: #e5e5e5;
35
+ --canvas-border-strong: #d4d4d8;
36
+ --canvas-text: #1a1a1a;
37
+ --canvas-text-dim: #737373;
38
+ --canvas-accent: #05a9bc;
39
+ --canvas-success: #05a9bc;
40
+ --canvas-escalation: #b45309;
41
+ --canvas-failure: #ef4444;
42
+ }
43
+ :root[data-canvas-theme="dark"] {
44
+ --canvas-bg: #0f0f0f;
45
+ --canvas-panel: #1a1a1a;
46
+ --canvas-border: #2e2e2e;
47
+ --canvas-border-strong: #3a3a3a;
48
+ --canvas-text: #fafafa;
49
+ --canvas-text-dim: #a1a1aa;
50
+ --canvas-accent: #6be195;
51
+ --canvas-success: #6be195;
52
+ --canvas-escalation: #f59e0b;
53
+ --canvas-failure: #f87171;
54
+ }
55
+ @media (prefers-color-scheme: dark) {
56
+ :root:not([data-canvas-theme]) {
57
+ --canvas-bg: #0f0f0f;
58
+ --canvas-panel: #1a1a1a;
59
+ --canvas-border: #2e2e2e;
60
+ --canvas-border-strong: #3a3a3a;
61
+ --canvas-text: #fafafa;
62
+ --canvas-text-dim: #a1a1aa;
63
+ --canvas-accent: #6be195;
64
+ --canvas-success: #6be195;
65
+ --canvas-escalation: #f59e0b;
66
+ --canvas-failure: #f87171;
67
+ }
68
+ }
69
+ * { box-sizing: border-box; }
70
+ html, body {
71
+ margin: 0; min-height: 100%; background: var(--canvas-bg); color: var(--canvas-text);
72
+ font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
73
+ }
74
+ #canvas-root { max-width: 1100px; margin: 0 auto; padding: 24px 20px; display: flex; flex-direction: column; gap: 18px; }
75
+
76
+ /* --- structural classes: keep these, and their names, untouched --- */
77
+ .canvas-panel { background: var(--canvas-panel); border: 1px solid var(--canvas-border); border-radius: 16px; padding: 20px; }
78
+ .canvas-header { display: flex; flex-direction: column; gap: 10px; margin-bottom: 14px; }
79
+ .canvas-title-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
80
+ .canvas-title { margin: 0; font-size: 20px; font-weight: 600; letter-spacing: -0.01em; }
81
+ .canvas-badge {
82
+ font-size: 11px; padding: 3px 9px; border-radius: 999px; border: 1px solid var(--canvas-border-strong);
83
+ color: var(--canvas-text-dim);
84
+ }
85
+ .canvas-badge--stale { color: var(--canvas-escalation); border-color: var(--canvas-escalation); }
86
+ .canvas-subtitle { margin: 0; color: var(--canvas-text-dim); font-size: 12.5px; }
87
+ .canvas-stats { display: flex; gap: 22px; margin-top: 2px; }
88
+ .canvas-stat { display: flex; flex-direction: column; }
89
+ .canvas-stat-value { font-size: 17px; font-weight: 600; }
90
+ .canvas-stat-label { font-size: 10px; color: var(--canvas-text-dim); text-transform: uppercase; letter-spacing: 0.06em; }
91
+ .canvas-diagram-panel { overflow-x: auto; }
92
+ .canvas-empty-note { color: var(--canvas-text-dim); font-size: 13px; text-align: center; padding: 60px 20px; margin: 0; }
93
+ /* The <svg> carries explicit width/height, so it always renders at its
94
+ NATURAL (1×) size — a narrow single-column graph can never stretch to fill
95
+ the pane (the old width:100% did that, ballooning nodes ~4×). Centered when
96
+ it fits; a graph wider than the pane keeps full size and scrolls via
97
+ .canvas-diagram-panel's overflow-x, staying legible instead of shrinking. */
98
+ .canvas-graph-svg { display: block; margin: 0 auto; }
99
+
100
+ /* --- node kinds: entry | step | pause | terminal-success | terminal-warn | launched-workflow --- */
101
+ .canvas-node .canvas-node-rect { fill: var(--canvas-panel); stroke-width: 1.5; stroke: var(--canvas-border-strong); }
102
+ .node--entry .canvas-node-rect { stroke: var(--canvas-accent); }
103
+ .node--pause .canvas-node-rect { stroke: var(--canvas-text-dim); stroke-dasharray: 5 4; }
104
+ .node--terminal-success .canvas-node-rect { stroke: var(--canvas-success); }
105
+ .node--terminal-warn .canvas-node-rect { stroke: var(--canvas-escalation); }
106
+ .node--launched-workflow .canvas-node-rect { stroke: var(--canvas-accent); stroke-dasharray: 5 4; }
107
+ .canvas-node-title { fill: var(--canvas-text); font-size: 13px; font-weight: 600; text-anchor: middle; dominant-baseline: middle; }
108
+ .canvas-node-sub { fill: var(--canvas-text-dim); font-size: 9.5px; text-anchor: middle; dominant-baseline: middle; }
109
+
110
+ /* --- enrichment layout hints: group bands sit behind edges and nodes --- */
111
+ .canvas-group-band { fill: var(--canvas-accent); fill-opacity: 0.06; stroke: var(--canvas-border); stroke-dasharray: 3 5; }
112
+ .canvas-group-label { fill: var(--canvas-text-dim); font-size: 9px; text-transform: uppercase; letter-spacing: 0.06em; }
113
+
114
+ /* --- edge kinds: sequential (base) | branching (--success/--warn) | cross-workflow signal/handoff (--cross) --- */
115
+ .canvas-edge { fill: none; stroke-width: 1.8; stroke: var(--canvas-border-strong); }
116
+ .canvas-edge--success { stroke: var(--canvas-success); }
117
+ .canvas-edge--warn { stroke: var(--canvas-escalation); }
118
+ .canvas-edge--cross { stroke: var(--canvas-text-dim); stroke-dasharray: 4 4; }
119
+ .canvas-edge--launch { stroke: var(--canvas-accent); stroke-dasharray: 4 4; }
120
+ .canvas-edge-label { fill: var(--canvas-text-dim); font-size: 9px; }
121
+ .canvas-arrow-fill { fill: var(--canvas-border-strong); }
122
+ .canvas-arrow-fill--success { fill: var(--canvas-success); }
123
+ .canvas-arrow-fill--warn { fill: var(--canvas-escalation); }
124
+
125
+ /* --- legend + interconnections --- */
126
+ .canvas-legend { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; font-size: 11px; color: var(--canvas-text-dim); }
127
+ .canvas-legend-item { display: flex; align-items: center; gap: 6px; }
128
+ .canvas-legend-marker { width: 10px; height: 10px; border-radius: 50%; display: inline-block; flex: 0 0 auto; }
129
+ .canvas-legend-marker--entry { background: var(--canvas-accent); }
130
+ .canvas-legend-marker--step { border: 1.5px solid var(--canvas-border-strong); background: transparent; }
131
+ .canvas-legend-marker--pause { border: 1.5px dashed var(--canvas-text-dim); background: transparent; }
132
+ .canvas-legend-marker--terminal-success { background: var(--canvas-success); border-radius: 3px; }
133
+ .canvas-legend-marker--terminal-warn { background: var(--canvas-escalation); border-radius: 3px; }
134
+ .canvas-legend-marker--cross { border: 1.5px dashed var(--canvas-text-dim); border-radius: 2px; background: transparent; }
135
+ .canvas-legend-marker--launched-workflow { border: 1.5px dashed var(--canvas-accent); border-radius: 3px; background: transparent; }
136
+ .canvas-interconnections { display: flex; flex-direction: column; gap: 12px; }
137
+ .canvas-panel-title { margin: 0; font-size: 12px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--canvas-text-dim); }
138
+ .canvas-interconnection-row { display: grid; grid-template-columns: 12px 1fr auto; column-gap: 8px; row-gap: 2px; align-items: baseline; }
139
+ .canvas-interconnection-title { font-weight: 600; font-size: 13px; }
140
+ .canvas-interconnection-tag { font-size: 10px; color: var(--canvas-text-dim); border: 1px solid var(--canvas-border-strong); border-radius: 6px; padding: 1px 6px; }
141
+ .canvas-interconnection-desc { grid-column: 2 / -1; margin: 0; font-size: 11.5px; color: var(--canvas-text-dim); }
142
+ .canvas-footer { display: flex; flex-direction: column; gap: 10px; padding: 4px 2px 2px; }
143
+ .canvas-note { margin: 0; font-size: 11px; color: var(--canvas-text-dim); font-style: italic; }
144
+ .canvas-notes { margin: 0; padding-left: 16px; display: flex; flex-direction: column; gap: 3px; font-size: 11.5px; color: var(--canvas-text-dim); }
145
+ .canvas-cross-workflow { margin: 0; font-size: 11.5px; color: var(--canvas-text-dim); }
146
+ template { display: none; }
147
+ `.trim();
148
+ }
149
+ /** Reads the current theme from `?theme=light|dark`, falling back to
150
+ * `prefers-color-scheme` (the CSS `@media` block above) when the param is
151
+ * absent — the only script in the whole document. */
152
+ const THEME_SCRIPT = `
153
+ (function () {
154
+ var params = new URLSearchParams(location.search);
155
+ var theme = params.get("theme");
156
+ if (theme === "light" || theme === "dark") {
157
+ document.documentElement.setAttribute("data-canvas-theme", theme);
158
+ }
159
+ })();
160
+ `.trim();
161
+ /**
162
+ * One example of every markup building block, inside an inert `<template>`
163
+ * (never rendered, but real parsed DOM — not an HTML comment, so class names
164
+ * with `--` in them are never at risk of being mistaken for a comment
165
+ * terminator). An agent reads this once to learn the exact shape of a node,
166
+ * an edge of each kind, a stat, a badge, a legend item, and an
167
+ * interconnection row, then writes its own real ones using the same classes.
168
+ */
169
+ const PATTERNS_TEMPLATE = `
170
+ <template id="canvas-patterns">
171
+ <!-- copy the pattern you need; this whole block is never rendered -->
172
+ <span class="canvas-badge">standalone workflow</span>
173
+ <div class="canvas-stat"><span class="canvas-stat-value">5</span><span class="canvas-stat-label">steps</span></div>
174
+ <svg>
175
+ <g class="canvas-node node--entry" filter="url(#canvas-glow)" transform="translate(392,40)">
176
+ <rect class="canvas-node-rect" width="176" height="56" rx="14" />
177
+ <text class="canvas-node-title" x="88" y="24">step_name</text>
178
+ <text class="canvas-node-sub" x="88" y="40">short description</text>
179
+ </g>
180
+ <g class="canvas-node node--step" filter="url(#canvas-glow)" transform="translate(392,150)">
181
+ <rect class="canvas-node-rect" width="176" height="56" rx="14" />
182
+ <text class="canvas-node-title" x="88" y="28">step_name</text>
183
+ </g>
184
+ <g class="canvas-node node--pause" filter="url(#canvas-glow)" transform="translate(392,260)">
185
+ <rect class="canvas-node-rect" width="176" height="56" rx="14" />
186
+ <text class="canvas-node-title" x="88" y="24">step_name</text>
187
+ <text class="canvas-node-sub" x="88" y="40">waits for a human / event</text>
188
+ </g>
189
+ <g class="canvas-node node--terminal-success" filter="url(#canvas-glow)" transform="translate(200,410)">
190
+ <rect class="canvas-node-rect" width="176" height="56" rx="14" />
191
+ <text class="canvas-node-title" x="88" y="24">step_name</text>
192
+ <text class="canvas-node-sub" x="88" y="40">terminate({ resolved: true })</text>
193
+ </g>
194
+ <g class="canvas-node node--terminal-warn" filter="url(#canvas-glow)" transform="translate(600,410)">
195
+ <rect class="canvas-node-rect" width="176" height="56" rx="14" />
196
+ <text class="canvas-node-title" x="88" y="24">step_name</text>
197
+ <text class="canvas-node-sub" x="88" y="40">terminate({ escalated: true })</text>
198
+ </g>
199
+ <!-- sequential: a step with exactly one successor -> straight line -->
200
+ <path class="canvas-edge" d="M480,96 L480,150" marker-end="url(#canvas-arrow)" />
201
+ <!-- branching: a step with multiple successors -> curved, colored by the destination's outcome -->
202
+ <path class="canvas-edge canvas-edge--success" d="M480,320 C480,365 288,365 288,410" marker-end="url(#canvas-arrow-success)" />
203
+ <path class="canvas-edge canvas-edge--warn" d="M480,320 C480,365 688,365 688,410" marker-end="url(#canvas-arrow-warn)" />
204
+ <!-- cross-workflow signal/handoff: dashed, always neutral -->
205
+ <path class="canvas-edge canvas-edge--cross" d="M480,320 L480,410" marker-end="url(#canvas-arrow)" />
206
+ <!-- edge label, positioned near the branch point, anchored toward its own destination -->
207
+ <text class="canvas-edge-label" x="440" y="345" text-anchor="end">category == x</text>
208
+ </svg>
209
+ <div class="canvas-interconnection-row">
210
+ <span class="canvas-legend-marker canvas-legend-marker--entry"></span>
211
+ <span class="canvas-interconnection-title">external -&gt; intake</span>
212
+ <span class="canvas-interconnection-tag">signal</span>
213
+ <p class="canvas-interconnection-desc">an order object enters here</p>
214
+ </div>
215
+ <span class="canvas-legend-item"><span class="canvas-legend-marker canvas-legend-marker--entry"></span>entry / active step</span>
216
+ <span class="canvas-legend-item"><span class="canvas-legend-marker canvas-legend-marker--step"></span>step</span>
217
+ <span class="canvas-legend-item"><span class="canvas-legend-marker canvas-legend-marker--pause"></span>pause / waits for input</span>
218
+ <span class="canvas-legend-item"><span class="canvas-legend-marker canvas-legend-marker--terminal-success"></span>terminal &middot; success</span>
219
+ <span class="canvas-legend-item"><span class="canvas-legend-marker canvas-legend-marker--terminal-warn"></span>terminal &middot; escalation</span>
220
+ <span class="canvas-legend-item"><span class="canvas-legend-marker canvas-legend-marker--cross"></span>cross-workflow signal/handoff</span>
221
+ </template>
222
+ `.trim();
223
+ /** The `<defs>` every SVG graph needs — same glow filter and arrow markers
224
+ * (default/success/warn) referenced by every node/edge pattern above. */
225
+ const SVG_DEFS = `
226
+ <svg width="0" height="0" style="position: absolute;">
227
+ <defs>
228
+ <marker id="canvas-arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
229
+ <path class="canvas-arrow-fill" d="M0,0 L10,5 L0,10 z" />
230
+ </marker>
231
+ <marker id="canvas-arrow-success" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
232
+ <path class="canvas-arrow-fill canvas-arrow-fill--success" d="M0,0 L10,5 L0,10 z" />
233
+ </marker>
234
+ <marker id="canvas-arrow-warn" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
235
+ <path class="canvas-arrow-fill canvas-arrow-fill--warn" d="M0,0 L10,5 L0,10 z" />
236
+ </marker>
237
+ <filter id="canvas-glow" x="-40%" y="-40%" width="180%" height="180%">
238
+ <feDropShadow dx="0" dy="0" stdDeviation="3" flood-color="var(--canvas-accent)" flood-opacity="0.18" />
239
+ </filter>
240
+ </defs>
241
+ </svg>
242
+ `.trim();
243
+ /**
244
+ * Wraps `bodyHtml` in the shared canvas document shell: doctype, the theme
245
+ * switch script, and every CSS class an agent's markup can use. This is the
246
+ * single source both the pristine template and `scripts/seed-example.mjs`'s
247
+ * prefilled instance render through, so they can't drift from each other.
248
+ */
249
+ export function renderCanvasDocument(bodyHtml) {
250
+ return `<!DOCTYPE html>
251
+ <html lang="en">
252
+ <head>
253
+ <meta charset="UTF-8" />
254
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
255
+ <title>Sapiom workflow canvas</title>
256
+ <script>
257
+ ${THEME_SCRIPT}
258
+ </script>
259
+ <style>
260
+ ${themeStyleBlock()}
261
+ </style>
262
+ </head>
263
+ <body>
264
+ ${SVG_DEFS}
265
+ <div id="canvas-root">
266
+ ${bodyHtml}
267
+ </div>
268
+ </body>
269
+ </html>
270
+ `;
271
+ }
272
+ const TEMPLATE_BODY = `
273
+ <!--
274
+ ═══════════════════════════════════════════════════════════════════════
275
+ SAPIOM CANVAS TEMPLATE — fill in the sections below to visualize a
276
+ workflow. Keep the <style> block in <head> and the structural classes
277
+ you see here untouched; only add markup using the patterns in the
278
+ <template id="canvas-patterns"> block near the end of <body> (it is
279
+ never rendered — read it, copy from it, don't edit it). Delete the
280
+ empty-state note below and the patterns template once you're done
281
+ authoring. For a single bound workflow, one canvas-panel is enough; for
282
+ a workspace overview, add one canvas-panel per workflow plus an
283
+ Interconnections panel (see the pattern for its row shape).
284
+ ═══════════════════════════════════════════════════════════════════════
285
+ -->
286
+ <section class="canvas-panel">
287
+ <header class="canvas-header">
288
+ <div class="canvas-title-row">
289
+ <h1 class="canvas-title">Untitled workflow</h1>
290
+ </div>
291
+ <p class="canvas-subtitle">One-line description of what this workflow does.</p>
292
+ <div class="canvas-stats"></div>
293
+ </header>
294
+ <div class="canvas-diagram-panel">
295
+ <p class="canvas-empty-note">Nothing visualized yet — run Visualize on a workflow.</p>
296
+ </div>
297
+ </section>
298
+
299
+ ${PATTERNS_TEMPLATE}
300
+ `.trim();
301
+ /** The exact document written to both `_template.html` and the initial
302
+ * `index.html` — a friendly empty state plus the patterns reference. */
303
+ export const TEMPLATE_HTML = renderCanvasDocument(TEMPLATE_BODY);
304
+ async function writeIfMissing(filePath, content) {
305
+ try {
306
+ await fs.access(filePath);
307
+ return;
308
+ }
309
+ catch {
310
+ // Doesn't exist yet — fall through and write it.
311
+ }
312
+ try {
313
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
314
+ await fs.writeFile(filePath, content, "utf8");
315
+ }
316
+ catch (err) {
317
+ console.error(`[harness] failed to write canvas file ${filePath}:`, err);
318
+ }
319
+ }
320
+ /**
321
+ * Backfill-only: ensures both `<cwd>/.sapiom/canvas/_template.html` (a
322
+ * pristine copy, so "clone the template" always has a clean source — see
323
+ * the visualize macro's prompt) and `<cwd>/.sapiom/canvas/index.html` (the
324
+ * live canvas, seeded with the same empty-state content) exist. Never
325
+ * clobbers either file if something's already there — an earlier session,
326
+ * or the agent's own edits. Called from SessionManager's create()/resume()
327
+ * (see its `ensureCanvasTemplate` option) so the canvas pane never opens to
328
+ * a completely empty iframe. Best-effort, like the sibling
329
+ * `workspace-context.ts` writer: a session's cwd could be unwritable, and
330
+ * that must never fail session creation itself.
331
+ */
332
+ export async function ensureCanvasTemplate(cwd) {
333
+ await removeLegacyOverviewIndex(path.join(cwd, CANVAS_INDEX));
334
+ await writeIfMissing(path.join(cwd, CANVAS_TEMPLATE_FILE), TEMPLATE_HTML);
335
+ await writeIfMissing(path.join(cwd, CANVAS_INDEX), TEMPLATE_HTML);
336
+ }
337
+ /**
338
+ * Deletes `index.html` iff it's a legacy deterministic overview a pre-split
339
+ * server wrote there (detected by signature — see canvas-index-classify.ts).
340
+ * Such a file is a stale stacked all-workflows page with baked-in "render
341
+ * failed" panels; left in place it keeps getting served (or, at minimum,
342
+ * skipped by `writeIfMissing` so a fresh seed never replaces it). An
343
+ * agent-authored custom canvas and the seeded template are both left
344
+ * untouched. Best-effort — a read/unlink failure must never fail session
345
+ * creation. Once removed, the `writeIfMissing` below reseeds the clean
346
+ * template.
347
+ */
348
+ async function removeLegacyOverviewIndex(indexPath) {
349
+ let content;
350
+ try {
351
+ content = await fs.readFile(indexPath, "utf8");
352
+ }
353
+ catch {
354
+ return; // Missing (the common case) or unreadable — nothing to clean up.
355
+ }
356
+ if (!isLegacyDeterministicCanvas(content))
357
+ return;
358
+ try {
359
+ await fs.rm(indexPath, { force: true });
360
+ }
361
+ catch (err) {
362
+ console.error(`[harness] failed to remove legacy canvas overview ${indexPath}:`, err);
363
+ }
364
+ }
365
+ //# sourceMappingURL=canvas-template.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canvas-template.js","sourceRoot":"","sources":["../../src/core/canvas-template.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Cheap change fingerprint for a directory tree: sorted `path:mtime:size`
3
+ * entries. Used by the polling fallback (and directly testable on its own)
4
+ * — not a content hash, just enough to notice something moved.
5
+ */
6
+ export declare function snapshotCanvasDir(canvasDir: string): string;
7
+ export interface CanvasWatcherManagerDeps {
8
+ /** Debounced per-session change notification — the integrator broadcasts
9
+ * `{type: "canvas.reload", harnessSessionId}` on /ws/events from this. */
10
+ onChange(harnessSessionId: string): void;
11
+ }
12
+ /** Registry of one SessionCanvasWatcher per active harness session. */
13
+ export declare class CanvasWatcherManager {
14
+ private readonly deps;
15
+ private readonly watchers;
16
+ constructor(deps: CanvasWatcherManagerDeps);
17
+ /** Idempotent: replaces any existing watcher for this session (e.g. a resume
18
+ * into a different cwd). */
19
+ start(harnessSessionId: string, cwd: string): void;
20
+ stop(harnessSessionId: string): void;
21
+ stopAll(): void;
22
+ /** Test/debug helper — how many sessions currently have an active watcher. */
23
+ get size(): number;
24
+ }
25
+ //# sourceMappingURL=canvas-watcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canvas-watcher.d.ts","sourceRoot":"","sources":["../../src/core/canvas-watcher.ts"],"names":[],"mappings":"AAOA;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAiB3D;AA6FD,MAAM,WAAW,wBAAwB;IACvC;+EAC2E;IAC3E,QAAQ,CAAC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1C;AAED,uEAAuE;AACvE,qBAAa,oBAAoB;IAGnB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAFjC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA2C;gBAEvC,IAAI,EAAE,wBAAwB;IAE3D;iCAC6B;IAC7B,KAAK,CAAC,gBAAgB,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI;IAQlD,IAAI,CAAC,gBAAgB,EAAE,MAAM,GAAG,IAAI;IAKpC,OAAO,IAAI,IAAI;IAIf,8EAA8E;IAC9E,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF"}
@@ -0,0 +1,144 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { CANVAS_DIR } from "../shared/types.js";
4
+ const DEBOUNCE_MS = 150;
5
+ const POLL_INTERVAL_MS = 500;
6
+ /**
7
+ * Cheap change fingerprint for a directory tree: sorted `path:mtime:size`
8
+ * entries. Used by the polling fallback (and directly testable on its own)
9
+ * — not a content hash, just enough to notice something moved.
10
+ */
11
+ export function snapshotCanvasDir(canvasDir) {
12
+ let entries;
13
+ try {
14
+ entries = fs.readdirSync(canvasDir, { recursive: true });
15
+ }
16
+ catch {
17
+ return "";
18
+ }
19
+ const parts = entries.map((entry) => {
20
+ try {
21
+ const stat = fs.statSync(path.join(canvasDir, entry));
22
+ return `${entry}:${stat.mtimeMs}:${stat.size}`;
23
+ }
24
+ catch {
25
+ return `${entry}:gone`;
26
+ }
27
+ });
28
+ return parts.sort().join("|");
29
+ }
30
+ /**
31
+ * One session's watcher. Watches the whole project root (not the canvas dir
32
+ * directly) recursively and filters to CANVAS_DIR-prefixed changes — so
33
+ * there's no separate "wait for the dir to appear" phase, and it naturally
34
+ * survives the canvas dir itself being deleted/recreated. Falls back to
35
+ * polling a directory-tree fingerprint when recursive `fs.watch` isn't
36
+ * available (notably Linux) or the watcher errors out at runtime.
37
+ */
38
+ class SessionCanvasWatcher {
39
+ constructor(cwd, harnessSessionId, onChange) {
40
+ this.cwd = cwd;
41
+ this.harnessSessionId = harnessSessionId;
42
+ this.onChange = onChange;
43
+ this.watcher = null;
44
+ this.pollTimer = null;
45
+ this.debounceTimer = null;
46
+ this.closed = false;
47
+ this.lastSnapshot = "";
48
+ this.canvasDir = path.join(cwd, CANVAS_DIR);
49
+ this.canvasPrefix = CANVAS_DIR + path.sep;
50
+ this.arm();
51
+ }
52
+ scheduleChange() {
53
+ if (this.closed)
54
+ return;
55
+ if (this.debounceTimer)
56
+ clearTimeout(this.debounceTimer);
57
+ this.debounceTimer = setTimeout(() => this.onChange(this.harnessSessionId), DEBOUNCE_MS);
58
+ }
59
+ isCanvasPath(filename) {
60
+ // A `null` filename means the platform couldn't report which path
61
+ // changed — reload to be safe rather than miss an update.
62
+ if (!filename)
63
+ return true;
64
+ return filename === CANVAS_DIR || filename.startsWith(this.canvasPrefix);
65
+ }
66
+ arm() {
67
+ if (this.closed)
68
+ return;
69
+ try {
70
+ this.watcher = fs.watch(this.cwd, { recursive: true }, (event, filename) => {
71
+ if (!this.isCanvasPath(filename))
72
+ return;
73
+ this.scheduleChange();
74
+ // A rename at (or above) the canvas dir — e.g. an editor's atomic
75
+ // write-then-rename, or the agent `mkdir -p`'ing it for the first
76
+ // time — can leave a recursive watcher no longer covering the new
77
+ // inode on some platforms. Re-arm defensively rather than risk
78
+ // silently going deaf.
79
+ if (event === "rename" && (filename === CANVAS_DIR || filename === path.dirname(CANVAS_DIR))) {
80
+ this.rearm();
81
+ }
82
+ });
83
+ this.watcher.on("error", () => this.fallBackToPolling());
84
+ }
85
+ catch {
86
+ // `recursive` isn't supported on this platform (notably Linux).
87
+ this.fallBackToPolling();
88
+ }
89
+ }
90
+ rearm() {
91
+ this.watcher?.close();
92
+ this.watcher = null;
93
+ this.arm();
94
+ }
95
+ fallBackToPolling() {
96
+ if (this.closed || this.pollTimer)
97
+ return;
98
+ this.watcher?.close();
99
+ this.watcher = null;
100
+ this.lastSnapshot = snapshotCanvasDir(this.canvasDir);
101
+ this.pollTimer = setInterval(() => {
102
+ const snapshot = snapshotCanvasDir(this.canvasDir);
103
+ if (snapshot !== this.lastSnapshot) {
104
+ this.lastSnapshot = snapshot;
105
+ this.scheduleChange();
106
+ }
107
+ }, POLL_INTERVAL_MS);
108
+ }
109
+ close() {
110
+ this.closed = true;
111
+ if (this.debounceTimer)
112
+ clearTimeout(this.debounceTimer);
113
+ if (this.pollTimer)
114
+ clearInterval(this.pollTimer);
115
+ this.watcher?.close();
116
+ this.watcher = null;
117
+ }
118
+ }
119
+ /** Registry of one SessionCanvasWatcher per active harness session. */
120
+ export class CanvasWatcherManager {
121
+ constructor(deps) {
122
+ this.deps = deps;
123
+ this.watchers = new Map();
124
+ }
125
+ /** Idempotent: replaces any existing watcher for this session (e.g. a resume
126
+ * into a different cwd). */
127
+ start(harnessSessionId, cwd) {
128
+ this.stop(harnessSessionId);
129
+ this.watchers.set(harnessSessionId, new SessionCanvasWatcher(cwd, harnessSessionId, (id) => this.deps.onChange(id)));
130
+ }
131
+ stop(harnessSessionId) {
132
+ this.watchers.get(harnessSessionId)?.close();
133
+ this.watchers.delete(harnessSessionId);
134
+ }
135
+ stopAll() {
136
+ for (const harnessSessionId of [...this.watchers.keys()])
137
+ this.stop(harnessSessionId);
138
+ }
139
+ /** Test/debug helper — how many sessions currently have an active watcher. */
140
+ get size() {
141
+ return this.watchers.size;
142
+ }
143
+ }
144
+ //# sourceMappingURL=canvas-watcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canvas-watcher.js","sourceRoot":"","sources":["../../src/core/canvas-watcher.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * In-memory queue -> batched POST to the remote collector, at
3
+ * `${SAPIOM_COLLECTOR_URL}/v1/harness/events`.
4
+ *
5
+ * Gated by telemetry opt-in: when off (or when no collector URL is
6
+ * configured at all), `enqueue` is a no-op — events still land in the local
7
+ * ndjson store via a separate path, this is only the remote fan-out.
8
+ *
9
+ * Response contract: any 2xx = accepted, drop the batch from the queue.
10
+ * 4xx = permanent failure, drop without retry (a malformed batch will never
11
+ * succeed by resending it). 5xx or a network error = transient, retry with
12
+ * exponential backoff, then drop.
13
+ */
14
+ import { type AnalyticsEvent, type CollectorContext } from "../../shared/types.js";
15
+ type FetchLike = (url: string, init: RequestInit) => Promise<Response>;
16
+ export interface CollectorBatcherOptions {
17
+ machineId: string;
18
+ telemetryOptIn: boolean;
19
+ /** Static per-install/boot context sent at the batch level. */
20
+ context: CollectorContext;
21
+ /** Base URL; `/v1/harness/events` is appended. Defaults to `process.env[ENV.collectorUrl]`. */
22
+ collectorUrl?: string;
23
+ /** Sapiom API key; sent as `Authorization: Bearer <key>`. Omitted (anonymous) when null/unset. */
24
+ apiKey?: string | null;
25
+ flushIntervalMs?: number;
26
+ maxBatchSize?: number;
27
+ /** Injectable for tests. Defaults to the global `fetch`. */
28
+ fetchImpl?: FetchLike;
29
+ onDebug?: (message: string) => void;
30
+ }
31
+ export declare class CollectorBatcher {
32
+ private readonly machineId;
33
+ private readonly context;
34
+ private readonly eventsUrl;
35
+ private readonly maxBatchSize;
36
+ private readonly fetchImpl;
37
+ private readonly onDebug;
38
+ private readonly timer;
39
+ private queue;
40
+ private telemetryOptIn;
41
+ private apiKey;
42
+ private flushing;
43
+ private warnedNoCollectorUrl;
44
+ constructor(options: CollectorBatcherOptions);
45
+ setTelemetryOptIn(optIn: boolean): void;
46
+ /** Update the auth key used on subsequent flushes (e.g. after login/logout). */
47
+ setApiKey(apiKey: string | null): void;
48
+ /** Queue an event for the next batch. No-op unless opted in and configured. */
49
+ enqueue(event: AnalyticsEvent): void;
50
+ /** Send whatever is queued now, with retry/backoff, dropping on final failure. */
51
+ flush(): Promise<void>;
52
+ private sendWithRetry;
53
+ /** Flush everything queued, then stop the periodic timer. */
54
+ close(): Promise<void>;
55
+ }
56
+ export {};
57
+ //# sourceMappingURL=batcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"batcher.d.ts","sourceRoot":"","sources":["../../../src/core/collector/batcher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,OAAO,EAGL,KAAK,cAAc,EAEnB,KAAK,gBAAgB,EACtB,MAAM,uBAAuB,CAAC;AAS/B,KAAK,SAAS,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAEvE,MAAM,WAAW,uBAAuB;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,OAAO,CAAC;IACxB,+DAA+D;IAC/D,OAAO,EAAE,gBAAgB,CAAC;IAC1B,+FAA+F;IAC/F,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kGAAkG;IAClG,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,4DAA4D;IAC5D,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACrC;AAMD,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmB;IAC3C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAqB;IAC/C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IACtC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA4B;IACpD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAwC;IAC9D,OAAO,CAAC,KAAK,CAAwB;IACrC,OAAO,CAAC,cAAc,CAAU;IAChC,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,oBAAoB,CAAS;gBAEzB,OAAO,EAAE,uBAAuB;IAsB5C,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI;IAKvC,gFAAgF;IAChF,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAItC,+EAA+E;IAC/E,OAAO,CAAC,KAAK,EAAE,cAAc,GAAG,IAAI;IAsBpC,kFAAkF;IAC5E,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YAmBd,aAAa;IAuC3B,6DAA6D;IACvD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAM7B"}