@sweny-ai/core 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 (93) hide show
  1. package/dist/__tests__/claude.test.d.ts +1 -0
  2. package/dist/__tests__/claude.test.js +328 -0
  3. package/dist/__tests__/executor.test.d.ts +1 -0
  4. package/dist/__tests__/executor.test.js +296 -0
  5. package/dist/__tests__/integration/datadog.integration.test.d.ts +1 -0
  6. package/dist/__tests__/integration/datadog.integration.test.js +23 -0
  7. package/dist/__tests__/integration/e2e-workflow.integration.test.d.ts +1 -0
  8. package/dist/__tests__/integration/e2e-workflow.integration.test.js +75 -0
  9. package/dist/__tests__/integration/github.integration.test.d.ts +1 -0
  10. package/dist/__tests__/integration/github.integration.test.js +37 -0
  11. package/dist/__tests__/integration/harness.d.ts +24 -0
  12. package/dist/__tests__/integration/harness.js +34 -0
  13. package/dist/__tests__/integration/linear.integration.test.d.ts +1 -0
  14. package/dist/__tests__/integration/linear.integration.test.js +15 -0
  15. package/dist/__tests__/integration/sentry.integration.test.d.ts +1 -0
  16. package/dist/__tests__/integration/sentry.integration.test.js +20 -0
  17. package/dist/__tests__/integration/slack.integration.test.d.ts +1 -0
  18. package/dist/__tests__/integration/slack.integration.test.js +22 -0
  19. package/dist/__tests__/schema.test.d.ts +1 -0
  20. package/dist/__tests__/schema.test.js +239 -0
  21. package/dist/__tests__/skills-index.test.d.ts +1 -0
  22. package/dist/__tests__/skills-index.test.js +122 -0
  23. package/dist/__tests__/skills.test.d.ts +1 -0
  24. package/dist/__tests__/skills.test.js +296 -0
  25. package/dist/__tests__/studio.test.d.ts +1 -0
  26. package/dist/__tests__/studio.test.js +172 -0
  27. package/dist/__tests__/testing.test.d.ts +1 -0
  28. package/dist/__tests__/testing.test.js +224 -0
  29. package/dist/browser.d.ts +17 -0
  30. package/dist/browser.js +22 -0
  31. package/dist/claude.d.ts +48 -0
  32. package/dist/claude.js +293 -0
  33. package/dist/cli/check.d.ts +11 -0
  34. package/dist/cli/check.js +237 -0
  35. package/dist/cli/config-file.d.ts +12 -0
  36. package/dist/cli/config-file.js +208 -0
  37. package/dist/cli/config.d.ts +77 -0
  38. package/dist/cli/config.js +565 -0
  39. package/dist/cli/main.d.ts +10 -0
  40. package/dist/cli/main.js +744 -0
  41. package/dist/cli/output.d.ts +26 -0
  42. package/dist/cli/output.js +357 -0
  43. package/dist/cli/renderer.d.ts +33 -0
  44. package/dist/cli/renderer.js +423 -0
  45. package/dist/cli/renderer.test.d.ts +1 -0
  46. package/dist/cli/renderer.test.js +302 -0
  47. package/dist/cli/setup.d.ts +11 -0
  48. package/dist/cli/setup.js +310 -0
  49. package/dist/executor.d.ts +29 -0
  50. package/dist/executor.js +173 -0
  51. package/dist/executor.test.d.ts +1 -0
  52. package/dist/executor.test.js +314 -0
  53. package/dist/index.d.ts +37 -0
  54. package/dist/index.js +36 -0
  55. package/dist/mcp.d.ts +11 -0
  56. package/dist/mcp.js +183 -0
  57. package/dist/mcp.test.d.ts +1 -0
  58. package/dist/mcp.test.js +334 -0
  59. package/dist/schema.d.ts +318 -0
  60. package/dist/schema.js +207 -0
  61. package/dist/skills/betterstack.d.ts +7 -0
  62. package/dist/skills/betterstack.js +114 -0
  63. package/dist/skills/datadog.d.ts +7 -0
  64. package/dist/skills/datadog.js +107 -0
  65. package/dist/skills/github.d.ts +8 -0
  66. package/dist/skills/github.js +155 -0
  67. package/dist/skills/index.d.ts +68 -0
  68. package/dist/skills/index.js +134 -0
  69. package/dist/skills/linear.d.ts +7 -0
  70. package/dist/skills/linear.js +89 -0
  71. package/dist/skills/notification.d.ts +11 -0
  72. package/dist/skills/notification.js +142 -0
  73. package/dist/skills/sentry.d.ts +7 -0
  74. package/dist/skills/sentry.js +105 -0
  75. package/dist/skills/slack.d.ts +8 -0
  76. package/dist/skills/slack.js +115 -0
  77. package/dist/studio.d.ts +124 -0
  78. package/dist/studio.js +174 -0
  79. package/dist/testing.d.ts +88 -0
  80. package/dist/testing.js +253 -0
  81. package/dist/types.d.ts +144 -0
  82. package/dist/types.js +11 -0
  83. package/dist/workflow-builder.d.ts +45 -0
  84. package/dist/workflow-builder.js +120 -0
  85. package/dist/workflow-builder.test.d.ts +1 -0
  86. package/dist/workflow-builder.test.js +117 -0
  87. package/dist/workflows/implement.d.ts +11 -0
  88. package/dist/workflows/implement.js +83 -0
  89. package/dist/workflows/index.d.ts +2 -0
  90. package/dist/workflows/index.js +2 -0
  91. package/dist/workflows/triage.d.ts +18 -0
  92. package/dist/workflows/triage.js +108 -0
  93. package/package.json +83 -0
@@ -0,0 +1,423 @@
1
+ import chalk from "chalk";
2
+ // ── Helpers ──────────────────────────────────────────────────────
3
+ /** Strip ANSI escape codes for accurate visible-width calculations. */
4
+ export function stripAnsi(str) {
5
+ return str.replace(/\x1B\[[0-9;]*m/g, "");
6
+ }
7
+ function visLen(str) {
8
+ return stripAnsi(str).length;
9
+ }
10
+ function formatElapsed(ms) {
11
+ const s = Math.round(ms / 1000);
12
+ if (s < 60)
13
+ return `${s}s`;
14
+ const m = Math.floor(s / 60);
15
+ return `${m}m ${s % 60}s`;
16
+ }
17
+ // ── Topological sort ─────────────────────────────────────────────
18
+ /**
19
+ * Returns node IDs in topological order starting from the entry node.
20
+ * Uses Kahn's algorithm (BFS from entry) to respect edge ordering.
21
+ */
22
+ function topologicalOrder(workflow) {
23
+ const nodeIds = Object.keys(workflow.nodes);
24
+ const inDegree = new Map(nodeIds.map((id) => [id, 0]));
25
+ const children = new Map(nodeIds.map((id) => [id, []]));
26
+ for (const edge of workflow.edges) {
27
+ inDegree.set(edge.to, (inDegree.get(edge.to) ?? 0) + 1);
28
+ children.get(edge.from)?.push(edge.to);
29
+ }
30
+ const queue = [workflow.entry];
31
+ const visited = new Set([workflow.entry]);
32
+ const order = [];
33
+ while (queue.length > 0) {
34
+ const current = queue.shift();
35
+ order.push(current);
36
+ for (const child of children.get(current) ?? []) {
37
+ if (!visited.has(child)) {
38
+ visited.add(child);
39
+ queue.push(child);
40
+ }
41
+ }
42
+ }
43
+ // Append any nodes not reachable from entry (disconnected subgraphs)
44
+ for (const id of nodeIds) {
45
+ if (!visited.has(id)) {
46
+ order.push(id);
47
+ }
48
+ }
49
+ return order;
50
+ }
51
+ // ── Status icons + colors ────────────────────────────────────────
52
+ function statusIcon(state) {
53
+ switch (state) {
54
+ case "completed":
55
+ return chalk.green("●");
56
+ case "running":
57
+ return chalk.yellow("◉");
58
+ case "pending":
59
+ return chalk.gray("○");
60
+ case "failed":
61
+ return chalk.red("✕");
62
+ }
63
+ }
64
+ // ── Graph helpers ────────────────────────────────────────────────
65
+ function getChildren(workflow) {
66
+ const children = new Map(Object.keys(workflow.nodes).map((id) => [id, []]));
67
+ for (const edge of workflow.edges) {
68
+ children.get(edge.from)?.push(edge.to);
69
+ }
70
+ return children;
71
+ }
72
+ // ── Box rendering ────────────────────────────────────────────────
73
+ function getNodeDetail(nodeRenderState) {
74
+ const { state, toolCallCount, startedAt, finishedAt } = nodeRenderState;
75
+ if (state === "running" && startedAt != null) {
76
+ const elapsed = formatElapsed(Date.now() - startedAt);
77
+ return chalk.dim(` ${elapsed}`);
78
+ }
79
+ else if (state === "completed" && toolCallCount > 0) {
80
+ return chalk.dim(` ${toolCallCount} call${toolCallCount === 1 ? "" : "s"}`);
81
+ }
82
+ else if (state === "failed" && startedAt != null && finishedAt != null) {
83
+ const elapsed = formatElapsed(finishedAt - startedAt);
84
+ return chalk.dim(` ${elapsed}`);
85
+ }
86
+ return "";
87
+ }
88
+ /**
89
+ * Word-wrap a node name into lines that fit within maxChars.
90
+ * Splits on spaces. If a single word exceeds maxChars, it's placed on its own line.
91
+ */
92
+ function wrapName(name, maxChars) {
93
+ const words = name.split(" ");
94
+ const lines = [];
95
+ let current = "";
96
+ for (const word of words) {
97
+ if (current.length === 0) {
98
+ current = word;
99
+ }
100
+ else if (current.length + 1 + word.length <= maxChars) {
101
+ current += " " + word;
102
+ }
103
+ else {
104
+ lines.push(current);
105
+ current = word;
106
+ }
107
+ }
108
+ if (current.length > 0)
109
+ lines.push(current);
110
+ return lines;
111
+ }
112
+ /**
113
+ * Render a single node as a box.
114
+ * @param innerWidth - the uniform inner width for all boxes
115
+ * @param indent - number of leading spaces
116
+ * @param topConnector - "plain" (┌───┐), "arrow" (┌───▼───┐)
117
+ * @param bottomConnector - "plain" (└───┘), "tee" (└───┬───┘)
118
+ * @param connectorCol - absolute column for ▼/┬ (default: centered in the box)
119
+ */
120
+ function renderNodeBox(nodeName, nodeRenderState, innerWidth, indent, topConnector, bottomConnector, connectorCol) {
121
+ const icon = statusIcon(nodeRenderState.state);
122
+ const detail = getNodeDetail(nodeRenderState);
123
+ const pad = " ".repeat(indent);
124
+ // Position of connector character within the inner width (offset from ┌/└)
125
+ const mid = connectorCol != null ? connectorCol - indent - 1 : Math.floor(innerWidth / 2);
126
+ // Top border
127
+ let top;
128
+ if (topConnector === "arrow") {
129
+ const leftDashes = "─".repeat(mid);
130
+ const rightDashes = "─".repeat(innerWidth - mid - 1);
131
+ top = `${pad}┌${leftDashes}▼${rightDashes}┐`;
132
+ }
133
+ else {
134
+ top = `${pad}┌${"─".repeat(innerWidth)}┐`;
135
+ }
136
+ // Bottom border
137
+ let bottom;
138
+ if (bottomConnector === "tee") {
139
+ const leftDashes = "─".repeat(mid);
140
+ const rightDashes = "─".repeat(innerWidth - mid - 1);
141
+ bottom = `${pad}└${leftDashes}┬${rightDashes}┘`;
142
+ }
143
+ else {
144
+ bottom = `${pad}└${"─".repeat(innerWidth)}┘`;
145
+ }
146
+ // Content lines — icon on first line, wrapped name
147
+ // Available space for text: innerWidth - 4 (1 space + icon + 1 space + text + padding + 1 space)
148
+ // "│ ○ Name...padding │" → icon(1) + spaces(2) + name + pad
149
+ const iconVis = stripAnsi(icon); // 1 char
150
+ const detailVis = stripAnsi(detail);
151
+ // Max text width for name: innerWidth - 2 (margins) - 2 (icon + space)
152
+ const maxNameWidth = innerWidth - 4; // "│ X name_here... │" → 1+1+1+1 = 4 overhead
153
+ const nameLines = wrapName(nodeName, maxNameWidth);
154
+ const contentLines = [];
155
+ for (let li = 0; li < nameLines.length; li++) {
156
+ const isFirst = li === 0;
157
+ let lineText;
158
+ let lineVisLen;
159
+ if (isFirst) {
160
+ // First line includes icon and possibly detail
161
+ const namePart = nameLines[li];
162
+ // Check if detail fits on first line
163
+ if (nameLines.length === 1 && namePart.length + detailVis.length <= maxNameWidth) {
164
+ lineText = `${icon} ${namePart}${detail}`;
165
+ lineVisLen = iconVis.length + 1 + namePart.length + detailVis.length;
166
+ }
167
+ else {
168
+ lineText = `${icon} ${namePart}`;
169
+ lineVisLen = iconVis.length + 1 + namePart.length;
170
+ }
171
+ }
172
+ else {
173
+ // Continuation lines indented to align with name start (after "X ")
174
+ lineText = ` ${nameLines[li]}`;
175
+ lineVisLen = 2 + nameLines[li].length;
176
+ }
177
+ const rightPad = Math.max(0, innerWidth - 2 - lineVisLen);
178
+ contentLines.push(`${pad}│ ${lineText}${" ".repeat(rightPad)} │`);
179
+ }
180
+ // If detail didn't fit on the first line of a multi-line name, add it to last line
181
+ // Actually, for simplicity, detail goes on the first line only when name is single-line
182
+ return [top, ...contentLines, bottom];
183
+ }
184
+ // ── Legend ───────────────────────────────────────────────────────
185
+ function renderLegend() {
186
+ const items = [
187
+ `${chalk.green("●")} completed`,
188
+ `${chalk.yellow("◉")} running`,
189
+ `${chalk.gray("○")} pending`,
190
+ `${chalk.red("✕")} failed`,
191
+ ];
192
+ return `\n ${chalk.dim(items.join(" "))}`;
193
+ }
194
+ export class DagRenderer {
195
+ workflow;
196
+ options;
197
+ nodeStates;
198
+ topoOrder;
199
+ /** Number of lines written in the last render pass (for cursor repositioning). */
200
+ lastLineCount = 0;
201
+ constructor(workflow, options = {}) {
202
+ this.workflow = workflow;
203
+ this.options = {
204
+ animate: options.animate ?? false,
205
+ stream: options.stream ??
206
+ (typeof process !== "undefined" ? process.stderr : undefined),
207
+ };
208
+ this.nodeStates = new Map(Object.keys(workflow.nodes).map((id) => [id, { state: "pending", toolCallCount: 0 }]));
209
+ this.topoOrder = topologicalOrder(workflow);
210
+ }
211
+ // ── Public API ─────────────────────────────────────────────────
212
+ /** Update renderer state from an execution event. Re-renders if animate=true. */
213
+ update(event) {
214
+ switch (event.type) {
215
+ case "node:enter": {
216
+ const s = this.getOrCreate(event.node);
217
+ s.state = "running";
218
+ s.startedAt = Date.now();
219
+ break;
220
+ }
221
+ case "tool:call": {
222
+ const s = this.getOrCreate(event.node);
223
+ s.toolCallCount++;
224
+ break;
225
+ }
226
+ case "node:exit": {
227
+ const s = this.getOrCreate(event.node);
228
+ s.state = event.result.status === "failed" ? "failed" : "completed";
229
+ s.finishedAt = Date.now();
230
+ break;
231
+ }
232
+ // These events don't affect per-node rendering state
233
+ case "workflow:start":
234
+ case "workflow:end":
235
+ case "tool:result":
236
+ case "route":
237
+ break;
238
+ }
239
+ if (this.options.animate) {
240
+ this.render();
241
+ }
242
+ }
243
+ /** Returns the current state of a node. Returns "pending" for unknown nodes. */
244
+ getNodeState(nodeId) {
245
+ return this.nodeStates.get(nodeId)?.state ?? "pending";
246
+ }
247
+ /** Returns the number of tool calls for a node. Returns 0 for unknown nodes. */
248
+ getToolCallCount(nodeId) {
249
+ return this.nodeStates.get(nodeId)?.toolCallCount ?? 0;
250
+ }
251
+ /** Renders the DAG to a string (no side effects). */
252
+ renderToString() {
253
+ const children = getChildren(this.workflow);
254
+ // Pre-compute uniform inner width from the max node name across ALL nodes
255
+ const MIN_INNER = 20;
256
+ let maxNameLen = 0;
257
+ for (const nodeId of this.topoOrder) {
258
+ const node = this.workflow.nodes[nodeId];
259
+ if (node) {
260
+ maxNameLen = Math.max(maxNameLen, node.name.length);
261
+ }
262
+ }
263
+ // innerWidth = "│ X " + name + " │" → name + 4 + some padding
264
+ const boxInnerWidth = Math.max(maxNameLen + 4, MIN_INNER);
265
+ const indent = 2; // global left indent for main-column boxes
266
+ const midCol = indent + 1 + Math.floor(boxInnerWidth / 2); // center column for connectors (indent + border + half inner)
267
+ const lines = [];
268
+ lines.push(` ${chalk.bold(this.workflow.name)}`);
269
+ lines.push("");
270
+ // Build a set of nodes that have been rendered (to handle branches)
271
+ const rendered = new Set();
272
+ // Track the column where the incoming connector arrives from (for merge nodes after branches)
273
+ // When set, the next node's ▼ is placed at this column instead of centered
274
+ let incomingConnectorCol;
275
+ for (let i = 0; i < this.topoOrder.length; i++) {
276
+ const nodeId = this.topoOrder[i];
277
+ if (rendered.has(nodeId))
278
+ continue;
279
+ const node = this.workflow.nodes[nodeId];
280
+ if (!node)
281
+ continue;
282
+ const state = this.nodeStates.get(nodeId) ?? { state: "pending", toolCallCount: 0 };
283
+ const kids = children.get(nodeId) ?? [];
284
+ // Check if this node has exactly 2 children → render fork
285
+ if (kids.length === 2) {
286
+ // Render this node with a tee bottom
287
+ const isFirstRendered = rendered.size === 0;
288
+ const boxLines = renderNodeBox(node.name, state, boxInnerWidth, indent, isFirstRendered ? "plain" : "arrow", "tee", incomingConnectorCol);
289
+ lines.push(...boxLines);
290
+ rendered.add(nodeId);
291
+ incomingConnectorCol = undefined;
292
+ // Render fork connector and side-by-side children
293
+ const leftId = kids[0];
294
+ const rightId = kids[1];
295
+ const leftNode = this.workflow.nodes[leftId];
296
+ const rightNode = this.workflow.nodes[rightId];
297
+ if (!leftNode || !rightNode)
298
+ continue;
299
+ const leftState = this.nodeStates.get(leftId) ?? { state: "pending", toolCallCount: 0 };
300
+ const rightState = this.nodeStates.get(rightId) ?? { state: "pending", toolCallCount: 0 };
301
+ // Determine child box widths — each child gets its own width based on its name
302
+ const leftKids = children.get(leftId) ?? [];
303
+ const rightKids = children.get(rightId) ?? [];
304
+ // For side-by-side, compute widths that fit
305
+ const leftInner = Math.max(leftNode.name.length + 4, 14);
306
+ const rightInner = Math.max(rightNode.name.length + 4, 14);
307
+ // Gap between the two child boxes: 1 space
308
+ const gap = 1;
309
+ const leftBoxOuter = leftInner + 2; // +2 for ┌/│ and ┐/│
310
+ const rightBoxOuter = rightInner + 2;
311
+ // Center the pair around midCol
312
+ const totalWidth = leftBoxOuter + gap + rightBoxOuter;
313
+ const pairStart = Math.max(0, midCol - Math.floor(totalWidth / 2));
314
+ const leftIndent = pairStart;
315
+ const rightIndent = pairStart + leftBoxOuter + gap;
316
+ // Left box center and right box center (absolute columns)
317
+ const leftCenter = leftIndent + 1 + Math.floor(leftInner / 2);
318
+ const rightCenter = rightIndent + 1 + Math.floor(rightInner / 2);
319
+ // Vertical connector from parent ┬
320
+ lines.push(" ".repeat(midCol) + "│");
321
+ // Fork line: ┌───┴───┐ with horizontal line from leftCenter to rightCenter
322
+ const forkLineChars = new Array(Math.max(rightCenter, midCol) + 1).fill(" ");
323
+ for (let c = leftCenter; c <= rightCenter; c++) {
324
+ forkLineChars[c] = "─";
325
+ }
326
+ forkLineChars[leftCenter] = "┌";
327
+ forkLineChars[rightCenter] = "┐";
328
+ forkLineChars[midCol] = "┴";
329
+ lines.push(forkLineChars.join(""));
330
+ // Vertical connectors from fork to child boxes
331
+ const vertLineChars = new Array(Math.max(rightCenter, midCol) + 1).fill(" ");
332
+ vertLineChars[leftCenter] = "│";
333
+ vertLineChars[rightCenter] = "│";
334
+ lines.push(vertLineChars.join(""));
335
+ // Render left and right child boxes side by side
336
+ const leftBox = renderNodeBox(leftNode.name, leftState, leftInner, leftIndent, "arrow", leftKids.length > 0 ? "tee" : "plain", leftCenter);
337
+ const rightBox = renderNodeBox(rightNode.name, rightState, rightInner, rightIndent, "arrow", rightKids.length > 0 ? "tee" : "plain", rightCenter);
338
+ // Merge left and right box lines side-by-side
339
+ const maxBoxLines = Math.max(leftBox.length, rightBox.length);
340
+ for (let li = 0; li < maxBoxLines; li++) {
341
+ const leftLine = li < leftBox.length ? leftBox[li] : "";
342
+ const rightLine = li < rightBox.length ? rightBox[li] : "";
343
+ const leftVisLen = visLen(leftLine);
344
+ if (rightLine.length > 0) {
345
+ const rightContent = rightLine.trimStart();
346
+ const rightStartCol = visLen(rightLine) - visLen(rightLine.trimStart());
347
+ const neededPad = Math.max(0, rightStartCol - leftVisLen);
348
+ lines.push(leftLine + " ".repeat(neededPad) + rightContent);
349
+ }
350
+ else {
351
+ lines.push(leftLine);
352
+ }
353
+ }
354
+ rendered.add(leftId);
355
+ rendered.add(rightId);
356
+ // Find the continuation/merge node
357
+ const leftNextIds = leftKids;
358
+ const rightNextIds = rightKids;
359
+ // Find merge point: a node reachable from either branch
360
+ let mergeNodeId;
361
+ const leftNextSet = new Set(leftNextIds);
362
+ for (const rn of rightNextIds) {
363
+ if (leftNextSet.has(rn)) {
364
+ mergeNodeId = rn;
365
+ break;
366
+ }
367
+ }
368
+ // If only one branch has a continuation, use that
369
+ if (!mergeNodeId && leftNextIds.length > 0) {
370
+ mergeNodeId = leftNextIds[0];
371
+ }
372
+ // Render connector from left child down to merge node
373
+ if (mergeNodeId && leftKids.length > 0) {
374
+ // The left child's ┬ is at leftCenter
375
+ lines.push(" ".repeat(leftCenter) + "│");
376
+ // Tell the merge node to place its ▼ at leftCenter
377
+ incomingConnectorCol = leftCenter;
378
+ }
379
+ }
380
+ else {
381
+ // Regular sequential node
382
+ const isFirstRendered = rendered.size === 0;
383
+ const hasDownstream = kids.length > 0;
384
+ const boxLines = renderNodeBox(node.name, state, boxInnerWidth, indent, isFirstRendered ? "plain" : "arrow", hasDownstream ? "tee" : "plain", incomingConnectorCol);
385
+ lines.push(...boxLines);
386
+ rendered.add(nodeId);
387
+ incomingConnectorCol = undefined;
388
+ // Add vertical connector if there's a next node
389
+ if (hasDownstream) {
390
+ lines.push(" ".repeat(midCol) + "│");
391
+ }
392
+ }
393
+ }
394
+ lines.push(renderLegend());
395
+ return lines.join("\n");
396
+ }
397
+ /**
398
+ * Writes the current DAG render to the stream.
399
+ * When animate=true, uses cursor manipulation to update in place.
400
+ */
401
+ render() {
402
+ const output = this.renderToString();
403
+ const lineCount = output.split("\n").length;
404
+ const stream = this.options.stream;
405
+ if (!stream)
406
+ return;
407
+ if (this.options.animate && this.lastLineCount > 0) {
408
+ // Move cursor up to overwrite previous render
409
+ stream.write(`\x1B[${this.lastLineCount}A`);
410
+ }
411
+ stream.write(output + "\n");
412
+ this.lastLineCount = lineCount + 1; // +1 for the trailing newline
413
+ }
414
+ // ── Private helpers ────────────────────────────────────────────
415
+ getOrCreate(nodeId) {
416
+ let s = this.nodeStates.get(nodeId);
417
+ if (!s) {
418
+ s = { state: "pending", toolCallCount: 0 };
419
+ this.nodeStates.set(nodeId, s);
420
+ }
421
+ return s;
422
+ }
423
+ }
@@ -0,0 +1 @@
1
+ export {};