whale-code 6.4.0 → 6.5.1

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 (187) hide show
  1. package/bin/swagmanager-mcp.js +51 -0
  2. package/dist/cli/app.js +30 -2
  3. package/dist/cli/chat/ChatApp.d.ts +4 -4
  4. package/dist/cli/chat/ChatApp.js +114 -44
  5. package/dist/cli/chat/ChatInput.d.ts +13 -6
  6. package/dist/cli/chat/ChatInput.js +433 -89
  7. package/dist/cli/chat/MemoryManager.d.ts +15 -0
  8. package/dist/cli/chat/MemoryManager.js +61 -0
  9. package/dist/cli/chat/MessageList.d.ts +8 -0
  10. package/dist/cli/chat/MessageList.js +1 -1
  11. package/dist/cli/chat/NodeManager.d.ts +30 -0
  12. package/dist/cli/chat/NodeManager.js +89 -0
  13. package/dist/cli/chat/NodeSelector.d.ts +19 -0
  14. package/dist/cli/chat/NodeSelector.js +37 -0
  15. package/dist/cli/chat/PlanApproval.d.ts +17 -0
  16. package/dist/cli/chat/PlanApproval.js +82 -0
  17. package/dist/cli/chat/SessionManager.d.ts +16 -0
  18. package/dist/cli/chat/SessionManager.js +43 -0
  19. package/dist/cli/chat/SlashMenu.d.ts +38 -0
  20. package/dist/cli/chat/SlashMenu.js +208 -0
  21. package/dist/cli/chat/StatusBar.d.ts +16 -0
  22. package/dist/cli/chat/StatusBar.js +22 -0
  23. package/dist/cli/chat/ThemeSelector.d.ts +14 -0
  24. package/dist/cli/chat/ThemeSelector.js +29 -0
  25. package/dist/cli/chat/ToolIndicator.d.ts +8 -0
  26. package/dist/cli/chat/ToolIndicator.js +33 -9
  27. package/dist/cli/chat/hooks/useAgentLoop.d.ts +2 -1
  28. package/dist/cli/chat/hooks/useAgentLoop.js +22 -17
  29. package/dist/cli/chat/hooks/useSlashCommands.d.ts +19 -0
  30. package/dist/cli/chat/hooks/useSlashCommands.js +254 -15
  31. package/dist/cli/commands/config-cmd.js +4 -25
  32. package/dist/cli/commands/db.d.ts +13 -0
  33. package/dist/cli/commands/db.js +243 -0
  34. package/dist/cli/commands/doctor.js +6 -9
  35. package/dist/cli/commands/mcp.js +1 -20
  36. package/dist/cli/services/agent-events.d.ts +22 -1
  37. package/dist/cli/services/agent-events.js +9 -0
  38. package/dist/cli/services/agent-loop.js +65 -8
  39. package/dist/cli/services/agent-worker-base.js +21 -6
  40. package/dist/cli/services/api-retry.d.ts +25 -0
  41. package/dist/cli/services/api-retry.js +91 -0
  42. package/dist/cli/services/auth-service.d.ts +1 -1
  43. package/dist/cli/services/auth-service.js +40 -19
  44. package/dist/cli/services/background-processes.js +26 -2
  45. package/dist/cli/services/config-store.d.ts +13 -1
  46. package/dist/cli/services/config-store.js +116 -13
  47. package/dist/cli/services/format-server-response.js +12 -6
  48. package/dist/cli/services/ink-resize-fix.d.ts +18 -0
  49. package/dist/cli/services/ink-resize-fix.js +66 -0
  50. package/dist/cli/services/interactive-tools.d.ts +14 -0
  51. package/dist/cli/services/interactive-tools.js +47 -2
  52. package/dist/cli/services/keybinding-manager.js +1 -1
  53. package/dist/cli/services/local-tools.js +35 -2
  54. package/dist/cli/services/server-tools.js +175 -3
  55. package/dist/cli/services/subagent.js +7 -6
  56. package/dist/cli/services/system-prompt.js +5 -3
  57. package/dist/cli/services/task-decomposer.d.ts +35 -0
  58. package/dist/cli/services/task-decomposer.js +199 -0
  59. package/dist/cli/services/team-lead.d.ts +18 -0
  60. package/dist/cli/services/team-lead.js +80 -0
  61. package/dist/cli/services/teammate.js +5 -5
  62. package/dist/cli/services/telemetry.d.ts +8 -2
  63. package/dist/cli/services/telemetry.js +116 -92
  64. package/dist/cli/services/tools/agent-tools.d.ts +1 -0
  65. package/dist/cli/services/tools/agent-tools.js +50 -4
  66. package/dist/cli/services/tools/file-ops.d.ts +2 -0
  67. package/dist/cli/services/tools/file-ops.js +85 -19
  68. package/dist/cli/services/tools/shell-exec.js +22 -12
  69. package/dist/cli/shared/Theme.d.ts +1 -2
  70. package/dist/cli/shared/Theme.js +1 -1
  71. package/dist/cli/shared/WhaleBanner.d.ts +4 -1
  72. package/dist/cli/shared/WhaleBanner.js +12 -8
  73. package/dist/cli/shared/markdown.d.ts +5 -4
  74. package/dist/cli/shared/markdown.js +376 -334
  75. package/dist/cli/shared/theme-manager.d.ts +27 -0
  76. package/dist/cli/shared/theme-manager.js +178 -0
  77. package/dist/cli/shared/theme-presets.d.ts +16 -0
  78. package/dist/cli/shared/theme-presets.js +265 -0
  79. package/dist/index.js +0 -51
  80. package/dist/node/adapters/imessage.d.ts +10 -0
  81. package/dist/node/adapters/imessage.js +45 -6
  82. package/dist/node/cli.js +459 -8
  83. package/dist/node/config.d.ts +17 -0
  84. package/dist/node/gateway-client.d.ts +55 -0
  85. package/dist/node/gateway-client.js +201 -0
  86. package/dist/node/portal/clipboard.d.ts +28 -0
  87. package/dist/node/portal/clipboard.js +183 -0
  88. package/dist/node/portal/discovery.d.ts +29 -0
  89. package/dist/node/portal/discovery.js +61 -0
  90. package/dist/node/portal/forward.d.ts +30 -0
  91. package/dist/node/portal/forward.js +90 -0
  92. package/dist/node/portal/index.d.ts +47 -0
  93. package/dist/node/portal/index.js +250 -0
  94. package/dist/node/portal/multiplexer.d.ts +48 -0
  95. package/dist/node/portal/multiplexer.js +207 -0
  96. package/dist/node/portal/permissions.d.ts +36 -0
  97. package/dist/node/portal/permissions.js +131 -0
  98. package/dist/node/portal/protocol.d.ts +140 -0
  99. package/dist/node/portal/protocol.js +193 -0
  100. package/dist/node/portal/screen.d.ts +18 -0
  101. package/dist/node/portal/screen.js +93 -0
  102. package/dist/node/portal/session.d.ts +68 -0
  103. package/dist/node/portal/session.js +127 -0
  104. package/dist/node/portal/shell.d.ts +26 -0
  105. package/dist/node/portal/shell.js +142 -0
  106. package/dist/node/portal/stream.d.ts +43 -0
  107. package/dist/node/portal/stream.js +90 -0
  108. package/dist/node/portal/transfer.d.ts +33 -0
  109. package/dist/node/portal/transfer.js +231 -0
  110. package/dist/node/portal/ui.d.ts +16 -0
  111. package/dist/node/portal/ui.js +148 -0
  112. package/dist/node/remote-desktop/compile-helper.d.ts +13 -0
  113. package/dist/node/remote-desktop/compile-helper.js +73 -0
  114. package/dist/node/remote-desktop/index.d.ts +67 -0
  115. package/dist/node/remote-desktop/index.js +220 -0
  116. package/dist/node/remote-desktop/protocol.d.ts +96 -0
  117. package/dist/node/remote-desktop/protocol.js +67 -0
  118. package/dist/node/runtime.d.ts +8 -1
  119. package/dist/node/runtime.js +117 -9
  120. package/dist/server/handlers/__test-utils__/test-db.d.ts +25 -0
  121. package/dist/server/handlers/__test-utils__/test-db.js +128 -0
  122. package/dist/server/handlers/api-keys.js +26 -2
  123. package/dist/server/handlers/browser.d.ts +0 -4
  124. package/dist/server/handlers/browser.js +0 -46
  125. package/dist/server/handlers/catalog.js +37 -14
  126. package/dist/server/handlers/clickhouse.d.ts +10 -0
  127. package/dist/server/handlers/clickhouse.js +215 -0
  128. package/dist/server/handlers/comms.d.ts +308 -4
  129. package/dist/server/handlers/comms.js +444 -11
  130. package/dist/server/handlers/creations.js +1 -1
  131. package/dist/server/handlers/crm.d.ts +54 -8
  132. package/dist/server/handlers/crm.js +353 -68
  133. package/dist/server/handlers/embeddings.js +3 -3
  134. package/dist/server/handlers/enrichment.js +39 -55
  135. package/dist/server/handlers/inventory.js +1 -1
  136. package/dist/server/handlers/kali.d.ts +9 -1
  137. package/dist/server/handlers/kali.js +50 -1
  138. package/dist/server/handlers/media.d.ts +8 -0
  139. package/dist/server/handlers/media.js +902 -0
  140. package/dist/server/handlers/meta-ads.js +6 -3
  141. package/dist/server/handlers/nodes.d.ts +2 -0
  142. package/dist/server/handlers/nodes.js +331 -40
  143. package/dist/server/handlers/operations.d.ts +4 -6
  144. package/dist/server/handlers/operations.js +99 -38
  145. package/dist/server/handlers/platform.js +224 -107
  146. package/dist/server/handlers/remove-bg.d.ts +6 -0
  147. package/dist/server/handlers/remove-bg.js +96 -0
  148. package/dist/server/handlers/storefront.d.ts +6 -0
  149. package/dist/server/handlers/storefront.js +477 -0
  150. package/dist/server/handlers/supply-chain.js +21 -3
  151. package/dist/server/handlers/workflow-steps.js +87 -31
  152. package/dist/server/handlers/workflows.js +4 -1
  153. package/dist/server/index.js +334 -88
  154. package/dist/server/lib/clickhouse-buffer.d.ts +48 -0
  155. package/dist/server/lib/clickhouse-buffer.js +175 -0
  156. package/dist/server/lib/clickhouse-client.d.ts +112 -0
  157. package/dist/server/lib/clickhouse-client.js +141 -0
  158. package/dist/server/lib/coa-renderer.d.ts +91 -0
  159. package/dist/server/lib/coa-renderer.js +411 -0
  160. package/dist/server/lib/compaction-service.js +46 -1
  161. package/dist/server/lib/pdf-renderer.d.ts +143 -0
  162. package/dist/server/lib/pdf-renderer.js +867 -0
  163. package/dist/server/lib/react-pdf-layout.d.ts +40 -0
  164. package/dist/server/lib/react-pdf-layout.js +437 -0
  165. package/dist/server/lib/server-agent-loop.d.ts +2 -0
  166. package/dist/server/lib/server-agent-loop.js +36 -17
  167. package/dist/server/lib/server-subagent.d.ts +3 -0
  168. package/dist/server/lib/server-subagent.js +9 -6
  169. package/dist/server/lib/supabase-client.js +51 -3
  170. package/dist/server/lib/template-resolver.js +14 -4
  171. package/dist/server/lib/utils.js +15 -0
  172. package/dist/server/local-agent-gateway.d.ts +44 -0
  173. package/dist/server/local-agent-gateway.js +389 -49
  174. package/dist/server/providers/anthropic.js +12 -2
  175. package/dist/server/providers/gemini.js +17 -2
  176. package/dist/server/proxy-handlers.js +151 -0
  177. package/dist/server/tool-router.d.ts +2 -2
  178. package/dist/server/tool-router.js +25 -35
  179. package/dist/shared/agent-core.d.ts +25 -2
  180. package/dist/shared/agent-core.js +66 -5
  181. package/dist/shared/api-client.js +54 -3
  182. package/dist/shared/sse-parser.d.ts +1 -1
  183. package/dist/shared/sse-parser.js +5 -2
  184. package/dist/shared/tool-dispatch.js +15 -1
  185. package/package.json +16 -10
  186. package/dist/server/handlers/__test-utils__/mock-supabase.d.ts +0 -11
  187. package/dist/server/handlers/__test-utils__/mock-supabase.js +0 -393
@@ -0,0 +1,40 @@
1
+ import { type LayoutElement, type PageConfig, type StyleConfig } from "./pdf-renderer.js";
2
+ export declare function renderLayoutToPdf(layout: LayoutElement[] | Record<string, unknown> | undefined | null, pageConfig: PageConfig | undefined | null, styles: StyleConfig | undefined | null, data: Record<string, unknown>): Promise<Buffer>;
3
+ interface LabelPageConfig {
4
+ cols: number;
5
+ rows: number;
6
+ pageWidth: number;
7
+ pageHeight: number;
8
+ labelWidth: number;
9
+ labelHeight: number;
10
+ margins: {
11
+ top: number;
12
+ left: number;
13
+ };
14
+ gutters?: {
15
+ horizontal: number;
16
+ vertical: number;
17
+ };
18
+ }
19
+ interface LabelElement {
20
+ type: string;
21
+ field?: string;
22
+ x: number;
23
+ y: number;
24
+ width: number;
25
+ height: number;
26
+ style?: Record<string, unknown>;
27
+ text?: string;
28
+ format?: string;
29
+ color?: string;
30
+ backgroundColor?: string;
31
+ }
32
+ export declare function renderLabelToPdf(layout: {
33
+ version: number;
34
+ elements: LabelElement[];
35
+ }, pageConfig: LabelPageConfig | any, items: Record<string, unknown>[]): Promise<Buffer>;
36
+ export declare function renderHtmlToPdf(html: string, options?: {
37
+ format?: string;
38
+ landscape?: boolean;
39
+ }): Promise<Buffer>;
40
+ export {};
@@ -0,0 +1,437 @@
1
+ // server/lib/react-pdf-layout.ts — Generic React-PDF layout engine
2
+ // Replaces Playwright HTML→PDF with pure Node.js React-PDF rendering.
3
+ import React from "react";
4
+ import { Document, Page, Text, View, Image, renderToBuffer, } from "@react-pdf/renderer";
5
+ import { resolveBinding, resolveText, generateQRSvg, } from "./pdf-renderer.js";
6
+ const e = React.createElement;
7
+ // ============================================================================
8
+ // Unit Helpers
9
+ // ============================================================================
10
+ /** Convert margin string (e.g. "15mm", "1cm", "72pt", number) to points */
11
+ function toPoints(val, fallback = 42.5) {
12
+ if (val === undefined || val === null)
13
+ return fallback;
14
+ if (typeof val === "number")
15
+ return val;
16
+ const num = parseFloat(val);
17
+ if (isNaN(num))
18
+ return fallback;
19
+ if (val.endsWith("mm"))
20
+ return num * 2.835;
21
+ if (val.endsWith("cm"))
22
+ return num * 28.35;
23
+ if (val.endsWith("in"))
24
+ return num * 72;
25
+ if (val.endsWith("pt"))
26
+ return num;
27
+ if (val.endsWith("px"))
28
+ return num * 0.75;
29
+ return num; // default to points
30
+ }
31
+ /** Map page size string to React-PDF page size */
32
+ function mapPageSize(size) {
33
+ if (!size)
34
+ return "A4";
35
+ const s = size.toUpperCase();
36
+ if (s === "LETTER")
37
+ return "LETTER";
38
+ if (s === "LEGAL")
39
+ return "LEGAL";
40
+ if (s === "TABLOID")
41
+ return "TABLOID";
42
+ return s; // A4, A3, etc. — React-PDF supports these directly
43
+ }
44
+ // ============================================================================
45
+ // Element Renderer — ports renderElement() from pdf-renderer.ts
46
+ // ============================================================================
47
+ function renderElement(el, data, key) {
48
+ switch (el.type) {
49
+ case "text": {
50
+ const text = resolveText(el.content || el.text || "", data);
51
+ return e(View, { key, style: { marginBottom: 2 } }, e(Text, {
52
+ style: {
53
+ fontSize: el.fontSize || undefined,
54
+ color: el.color || undefined,
55
+ fontFamily: el.bold ? "Helvetica-Bold" : undefined,
56
+ textAlign: el.align || undefined,
57
+ },
58
+ }, text));
59
+ }
60
+ case "header": {
61
+ const text = resolveText(el.content || el.text || "", data);
62
+ return e(View, {
63
+ key,
64
+ style: {
65
+ backgroundColor: el.backgroundColor || undefined,
66
+ padding: typeof el.padding === "number" ? el.padding : el.padding ? toPoints(el.padding, 0) : undefined,
67
+ marginBottom: 4,
68
+ },
69
+ }, e(Text, {
70
+ style: {
71
+ fontFamily: "Helvetica-Bold",
72
+ fontSize: el.fontSize || undefined,
73
+ color: el.color || undefined,
74
+ textAlign: el.align || undefined,
75
+ },
76
+ }, text));
77
+ }
78
+ case "image": {
79
+ let src = el.src || el.url || "";
80
+ if (el.bind) {
81
+ const resolved = resolveBinding(el.bind, data);
82
+ if (typeof resolved === "string")
83
+ src = resolved;
84
+ }
85
+ if (!src)
86
+ return e(View, { key });
87
+ const style = {};
88
+ if (el.width)
89
+ style.width = typeof el.width === "number" ? el.width : toPoints(el.width, 100);
90
+ if (el.height)
91
+ style.height = typeof el.height === "number" ? el.height : toPoints(el.height, 100);
92
+ return e(Image, { key, src, style });
93
+ }
94
+ case "qrcode": {
95
+ let qrData = el.data || el.content || "";
96
+ if (el.bind) {
97
+ const resolved = resolveBinding(el.bind, data);
98
+ if (typeof resolved === "string")
99
+ qrData = resolved;
100
+ }
101
+ qrData = resolveText(qrData, data);
102
+ const qrSize = el.size || (typeof el.width === "number" ? el.width : 100);
103
+ const svg = generateQRSvg(qrData, qrSize);
104
+ const dataUri = `data:image/svg+xml;base64,${Buffer.from(svg).toString("base64")}`;
105
+ return e(Image, { key, src: dataUri, style: { width: qrSize, height: qrSize } });
106
+ }
107
+ case "spacer": {
108
+ const h = el.height || 10;
109
+ return e(View, { key, style: { height: typeof h === "number" ? h : toPoints(h, 10) } });
110
+ }
111
+ case "line": {
112
+ const color = el.color || "#000";
113
+ const thickness = el.thickness || 1;
114
+ return e(View, {
115
+ key,
116
+ style: {
117
+ borderTopWidth: thickness,
118
+ borderTopColor: color,
119
+ borderTopStyle: "solid",
120
+ marginVertical: 8,
121
+ },
122
+ });
123
+ }
124
+ case "columns": {
125
+ const cols = el.columns || el.children || [];
126
+ const children = cols.map((child, i) => {
127
+ if (Array.isArray(child)) {
128
+ return e(View, { key: `col-${i}`, style: { flex: 1 } }, ...child.map((c, j) => renderElement(c, data, `col-${i}-${j}`)));
129
+ }
130
+ return e(View, { key: `col-${i}`, style: { flex: 1 } }, renderElement(child, data, `col-${i}-inner`));
131
+ });
132
+ return e(View, { key, style: { flexDirection: "row", gap: 8 } }, ...children);
133
+ }
134
+ case "page_break": {
135
+ return e(View, { key, break: true });
136
+ }
137
+ case "grid": {
138
+ const items = (Array.isArray(el.items) ? el.items : Array.isArray(el.content) ? el.content : []);
139
+ const rows = items.map((item, i) => {
140
+ const label = item.label || item.value || "";
141
+ let value = "";
142
+ if (item.bind) {
143
+ const resolved = resolveBinding(item.bind, data);
144
+ value = resolved !== undefined && resolved !== null ? String(resolved) : "";
145
+ }
146
+ value = resolveText(value || "", data);
147
+ return e(View, { key: `row-${i}`, style: { flexDirection: "row", borderBottomWidth: 0.5, borderBottomColor: "#ddd" } }, e(View, { style: { width: "40%", padding: 4 } }, e(Text, { style: { fontFamily: "Helvetica-Bold", fontSize: 9 } }, label)), e(View, { style: { width: "60%", padding: 4 } }, e(Text, { style: { fontSize: 9 } }, value)));
148
+ });
149
+ return e(View, { key, style: { width: "100%" } }, ...rows);
150
+ }
151
+ case "table": {
152
+ const headers = el.headers || [];
153
+ const rowFields = el.row_fields;
154
+ let bodyRows = el.rows || [];
155
+ if (el.bind) {
156
+ const resolved = resolveBinding(el.bind, data);
157
+ if (Array.isArray(resolved))
158
+ bodyRows = resolved;
159
+ }
160
+ const colCount = headers.length || (rowFields?.length || 0);
161
+ const colWidth = colCount > 0 ? `${(100 / colCount).toFixed(1)}%` : "100%";
162
+ const cellStyle = { padding: 5, borderWidth: 0.5, borderColor: "#ddd" };
163
+ const headerCellStyle = { ...cellStyle, backgroundColor: "#f5f5f5" };
164
+ const headerRow = headers.length
165
+ ? e(View, { key: "thead", style: { flexDirection: "row" } }, ...headers.map((h, i) => e(View, { key: `th-${i}`, style: { ...headerCellStyle, width: colWidth } }, e(Text, { style: { fontSize: 8, fontFamily: "Helvetica-Bold" } }, h))))
166
+ : null;
167
+ const dataRows = bodyRows.map((row, ri) => {
168
+ let cells;
169
+ if (Array.isArray(row)) {
170
+ cells = row.map((c) => String(c ?? ""));
171
+ }
172
+ else if (rowFields && rowFields.length) {
173
+ cells = rowFields.map(field => String(row[field] ?? ""));
174
+ }
175
+ else {
176
+ cells = headers.map((h) => {
177
+ const key2 = h.toLowerCase().replace(/[^a-z0-9]/g, "");
178
+ return String(row[h] ?? row[key2] ?? row[h.replace(/\s+/g, "")] ?? "");
179
+ });
180
+ }
181
+ return e(View, { key: `tr-${ri}`, style: { flexDirection: "row" } }, ...cells.map((cell, ci) => e(View, { key: `td-${ri}-${ci}`, style: { ...cellStyle, width: colWidth } }, e(Text, { style: { fontSize: 8 } }, cell))));
182
+ });
183
+ const tableChildren = [];
184
+ if (headerRow)
185
+ tableChildren.push(headerRow);
186
+ tableChildren.push(...dataRows);
187
+ return e(View, { key, style: { width: "100%" } }, ...tableChildren);
188
+ }
189
+ case "box": {
190
+ const inner = (el.children || []).map((child, i) => renderElement(child, data, `box-${i}`));
191
+ const text = el.content || el.text ? resolveText(el.content || el.text || "", data) : "";
192
+ const boxChildren = [];
193
+ if (text)
194
+ boxChildren.push(e(Text, { key: "box-text", style: { textAlign: "center" } }, text));
195
+ boxChildren.push(...inner);
196
+ return e(View, {
197
+ key,
198
+ style: {
199
+ textAlign: "center",
200
+ border: el.border ? undefined : undefined,
201
+ borderWidth: el.border ? 1 : undefined,
202
+ borderColor: el.border ? (el.border.includes("#") ? el.border.split(" ").find(s => s.startsWith("#")) : "#000") : undefined,
203
+ backgroundColor: el.background || el.backgroundColor || undefined,
204
+ padding: typeof el.padding === "number" ? el.padding : el.padding ? toPoints(el.padding, 0) : undefined,
205
+ },
206
+ }, ...boxChildren);
207
+ }
208
+ default: {
209
+ if (el.children && Array.isArray(el.children)) {
210
+ return e(View, { key }, ...el.children.map((child, i) => renderElement(child, data, `child-${i}`)));
211
+ }
212
+ return e(View, { key });
213
+ }
214
+ }
215
+ }
216
+ // ============================================================================
217
+ // Layout Normalization — same logic as renderLayoutToHtml lines 896-924
218
+ // ============================================================================
219
+ function normalizeLayout(layout) {
220
+ if (!layout)
221
+ return [];
222
+ if (Array.isArray(layout))
223
+ return layout;
224
+ const obj = layout;
225
+ const elements = [];
226
+ if (Array.isArray(obj.pages)) {
227
+ for (const page of obj.pages) {
228
+ if (page.header)
229
+ elements.push(page.header);
230
+ if (Array.isArray(page.body))
231
+ elements.push(...page.body);
232
+ else if (page.body)
233
+ elements.push(page.body);
234
+ if (Array.isArray(page.sections))
235
+ elements.push(...page.sections);
236
+ if (Array.isArray(page.elements))
237
+ elements.push(...page.elements);
238
+ if (page.footer)
239
+ elements.push(page.footer);
240
+ }
241
+ }
242
+ else if (Array.isArray(obj.sections)) {
243
+ elements.push(...obj.sections);
244
+ }
245
+ else if (Array.isArray(obj.children)) {
246
+ elements.push(...obj.children);
247
+ }
248
+ else {
249
+ if (obj.header)
250
+ elements.push(obj.header);
251
+ if (obj.body) {
252
+ if (Array.isArray(obj.body))
253
+ elements.push(...obj.body);
254
+ else
255
+ elements.push(obj.body);
256
+ }
257
+ if (obj.footer)
258
+ elements.push(obj.footer);
259
+ }
260
+ return elements;
261
+ }
262
+ // ============================================================================
263
+ // renderLayoutToPdf — Generic element-based layout → PDF buffer
264
+ // ============================================================================
265
+ export async function renderLayoutToPdf(layout, pageConfig, styles, data) {
266
+ const elements = normalizeLayout(layout);
267
+ const pageSize = mapPageSize(pageConfig?.size);
268
+ const margins = pageConfig?.margins || {};
269
+ const orientation = pageConfig?.orientation === "landscape" ? "landscape" : "portrait";
270
+ const fontFamily = styles?.fontFamily?.split(",")[0]?.trim() || "Helvetica";
271
+ const fontSize = styles?.fontSize || 12;
272
+ const color = styles?.color || "#000";
273
+ const pageStyle = {
274
+ paddingTop: toPoints(margins.top, 42.5),
275
+ paddingRight: toPoints(margins.right, 42.5),
276
+ paddingBottom: toPoints(margins.bottom, 42.5),
277
+ paddingLeft: toPoints(margins.left, 42.5),
278
+ fontFamily,
279
+ fontSize,
280
+ color,
281
+ lineHeight: 1.4,
282
+ };
283
+ const rendered = elements.map((el, i) => renderElement(el, data, `el-${i}`));
284
+ const doc = e(Document, {}, e(Page, { size: pageSize, orientation, style: pageStyle }, ...rendered));
285
+ const buffer = await renderToBuffer(doc);
286
+ return Buffer.from(buffer);
287
+ }
288
+ export async function renderLabelToPdf(layout, pageConfig, items) {
289
+ const IN = 72; // 1 inch = 72 points
290
+ const cols = pageConfig.cols || 2;
291
+ const rows = pageConfig.rows || 5;
292
+ const pageW = (pageConfig.pageWidth || 8.5) * IN;
293
+ const pageH = (pageConfig.pageHeight || 11) * IN;
294
+ const labelW = (pageConfig.labelWidth || 4) * IN;
295
+ const labelH = (pageConfig.labelHeight || 2) * IN;
296
+ const mTop = (pageConfig.margins?.top || 0.5) * IN;
297
+ const mLeft = (pageConfig.margins?.left || 0.15625) * IN;
298
+ const gH = (pageConfig.gutters?.horizontal || 0.125) * IN;
299
+ const gV = (pageConfig.gutters?.vertical || 0) * IN;
300
+ const labelsPerPage = cols * rows;
301
+ const pageCount = Math.ceil(items.length / labelsPerPage);
302
+ const pages = [];
303
+ for (let p = 0; p < pageCount; p++) {
304
+ const labelViews = [];
305
+ for (let slot = 0; slot < labelsPerPage; slot++) {
306
+ const itemIdx = p * labelsPerPage + slot;
307
+ if (itemIdx >= items.length)
308
+ break;
309
+ const item = items[itemIdx];
310
+ const row = Math.floor(slot / cols);
311
+ const col = slot % cols;
312
+ const x = mLeft + col * (labelW + gH);
313
+ const y = mTop + row * (labelH + gV);
314
+ const labelElements = layout.elements.map((le, ei) => {
315
+ const elX = le.x * labelW;
316
+ const elY = le.y * labelH;
317
+ const elW = le.width * labelW;
318
+ const elH = le.height * labelH;
319
+ const value = le.field ? String(item[le.field] ?? "") : (le.text || "");
320
+ switch (le.type) {
321
+ case "text":
322
+ return e(View, {
323
+ key: `le-${ei}`,
324
+ style: { position: "absolute", left: elX, top: elY, width: elW, height: elH },
325
+ }, e(Text, {
326
+ style: {
327
+ fontSize: le.style?.fontSize || 8,
328
+ color: le.color || "#000",
329
+ fontFamily: (le.style?.bold || le.style?.fontWeight === "bold") ? "Helvetica-Bold" : "Helvetica",
330
+ },
331
+ }, value));
332
+ case "badge":
333
+ return e(View, {
334
+ key: `le-${ei}`,
335
+ style: {
336
+ position: "absolute", left: elX, top: elY, width: elW, height: elH,
337
+ backgroundColor: le.backgroundColor || "#4CAF50",
338
+ borderRadius: 4,
339
+ justifyContent: "center",
340
+ alignItems: "center",
341
+ },
342
+ }, e(Text, { style: { fontSize: 7, color: le.color || "#fff", fontFamily: "Helvetica-Bold" } }, value));
343
+ case "image": {
344
+ const src = le.field ? String(item[le.field] ?? "") : "";
345
+ if (!src)
346
+ return e(View, { key: `le-${ei}` });
347
+ return e(Image, {
348
+ key: `le-${ei}`,
349
+ src,
350
+ style: { position: "absolute", left: elX, top: elY, width: elW, height: elH },
351
+ });
352
+ }
353
+ case "qr_code": {
354
+ const svg = generateQRSvg(value, Math.min(elW, elH));
355
+ const dataUri = `data:image/svg+xml;base64,${Buffer.from(svg).toString("base64")}`;
356
+ return e(Image, {
357
+ key: `le-${ei}`,
358
+ src: dataUri,
359
+ style: { position: "absolute", left: elX, top: elY, width: Math.min(elW, elH), height: Math.min(elW, elH) },
360
+ });
361
+ }
362
+ case "date": {
363
+ const dateVal = le.field ? item[le.field] : null;
364
+ const formatted = dateVal ? new Date(String(dateVal)).toLocaleDateString() : value;
365
+ return e(View, {
366
+ key: `le-${ei}`,
367
+ style: { position: "absolute", left: elX, top: elY, width: elW, height: elH },
368
+ }, e(Text, { style: { fontSize: le.style?.fontSize || 8, color: le.color || "#000" } }, formatted));
369
+ }
370
+ default:
371
+ return e(View, { key: `le-${ei}` });
372
+ }
373
+ });
374
+ labelViews.push(e(View, {
375
+ key: `label-${slot}`,
376
+ style: {
377
+ position: "absolute",
378
+ left: x,
379
+ top: y,
380
+ width: labelW,
381
+ height: labelH,
382
+ },
383
+ }, ...labelElements));
384
+ }
385
+ pages.push(e(Page, {
386
+ key: `page-${p}`,
387
+ size: [pageW, pageH],
388
+ style: { position: "relative" },
389
+ }, ...labelViews));
390
+ }
391
+ const doc = e(Document, {}, ...pages);
392
+ const buffer = await renderToBuffer(doc);
393
+ return Buffer.from(buffer);
394
+ }
395
+ // ============================================================================
396
+ // renderHtmlToPdf — Direct HTML string → PDF (replaces Playwright)
397
+ // ============================================================================
398
+ export async function renderHtmlToPdf(html, options) {
399
+ // Sanitize: strip dangerous tags (same P0 security as the old Playwright path)
400
+ const sanitizedHtml = html
401
+ .replace(/<script[\s\S]*?<\/script>/gi, "")
402
+ .replace(/<script[^>]*\/?>/gi, "")
403
+ .replace(/<iframe[\s\S]*?<\/iframe>/gi, "")
404
+ .replace(/<iframe[^>]*\/?>/gi, "")
405
+ .replace(/<object[\s\S]*?<\/object>/gi, "")
406
+ .replace(/<object[^>]*\/?>/gi, "")
407
+ .replace(/<embed[^>]*\/?>/gi, "")
408
+ .replace(/<link[^>]*\/?>/gi, "");
409
+ // Use react-pdf-html to render HTML inside React-PDF
410
+ let Html;
411
+ try {
412
+ const mod = await import("react-pdf-html");
413
+ Html = mod.default || mod.Html || mod;
414
+ }
415
+ catch {
416
+ // Fallback: extract text content and render as plain text
417
+ const textContent = sanitizedHtml.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
418
+ const pageSize = mapPageSize(options?.format);
419
+ const orientation = options?.landscape ? "landscape" : "portrait";
420
+ const doc = e(Document, {}, e(Page, {
421
+ size: pageSize,
422
+ orientation,
423
+ style: { padding: 28, fontFamily: "Helvetica", fontSize: 12 },
424
+ }, e(Text, {}, textContent)));
425
+ const buffer = await renderToBuffer(doc);
426
+ return Buffer.from(buffer);
427
+ }
428
+ const pageSize = mapPageSize(options?.format);
429
+ const orientation = options?.landscape ? "landscape" : "portrait";
430
+ const doc = e(Document, {}, e(Page, {
431
+ size: pageSize,
432
+ orientation,
433
+ style: { padding: 28, fontFamily: "Helvetica", fontSize: 12, lineHeight: 1.4 },
434
+ }, e(Html, { style: { fontSize: 12 } }, sanitizedHtml)));
435
+ const buffer = await renderToBuffer(doc);
436
+ return Buffer.from(buffer);
437
+ }
@@ -102,6 +102,8 @@ export interface ServerAgentLoopResult {
102
102
  consecutiveFailedTurns: number;
103
103
  };
104
104
  turns: TurnMetrics[];
105
+ /** Final stop reason from the last API response (end_turn, tool_use, max_tokens) */
106
+ stopReason: string;
105
107
  }
106
108
  export type { SubagentProgressCallback, SubagentProgressEvent };
107
109
  export declare function runServerAgentLoop(opts: ServerAgentLoopOptions): Promise<ServerAgentLoopResult>;
@@ -8,11 +8,12 @@
8
8
  * Consolidates: streaming, prompt caching, context management betas, compaction,
9
9
  * loop detection, parallel tool execution, subagent delegation, retry, cost tracking.
10
10
  */
11
- import { LoopDetector, getContextManagement, getMaxOutputTokens, getThinkingConfig, addPromptCaching, estimateCostUsd, isRetryableError, sanitizeError, routeModel, resolveToolChoice, COMPACTION_TRIGGER_TOKENS, COMPACTION_TOTAL_BUDGET, DEFAULT_SESSION_COST_BUDGET_USD, } from "../../shared/agent-core.js";
11
+ import { LoopDetector, getContextManagement, getMaxOutputTokens, getThinkingConfig, addPromptCaching, estimateCostUsd, isRetryableError, sanitizeError, routeModel, resolveToolChoice, emitCostWarningIfNeeded, demoteSubagentModel, COMPACTION_TRIGGER_TOKENS, COMPACTION_TOTAL_BUDGET, DEFAULT_SESSION_COST_BUDGET_USD, } from "../../shared/agent-core.js";
12
12
  import { processStreamWithCallbacks } from "../../shared/sse-parser.js";
13
13
  import { MODELS } from "../../shared/constants.js";
14
14
  import { dispatchTools, buildAssistantContent } from "../../shared/tool-dispatch.js";
15
15
  import { getCachedToolDefs, getFullToolSchemas } from "../tool-router.js";
16
+ import { queueSpan, auditRowToSpan } from "./clickhouse-buffer.js";
16
17
  import { DELEGATE_TASK_TOOL_DEF, runServerSubagent, } from "./server-subagent.js";
17
18
  import { handleTranscribe } from "../handlers/transcription.js";
18
19
  import { preCompact } from "./compaction-service.js";
@@ -49,7 +50,10 @@ function mapToolChoiceForAnthropic(tc) {
49
50
  // UNIFIED AGENT LOOP
50
51
  // ============================================================================
51
52
  export async function runServerAgentLoop(opts) {
52
- const { anthropic, model, systemPrompt, messages, tools: inputTools, maxTurns, temperature, enableDelegation = true, enablePromptCaching = true, enableStreaming = true, maxConcurrentTools = DEFAULT_MAX_CONCURRENT_TOOLS, maxCostUsd = DEFAULT_SESSION_COST_BUDGET_USD, onText, onToolStart, onCitation, documents, clientDisconnected = { value: false }, startedAt = Date.now(), maxDurationMs = 5 * 60 * 1000, } = opts;
53
+ const { anthropic, model, systemPrompt, messages, tools: inputTools, maxTurns, temperature, enableDelegation = true, enablePromptCaching = true, enableStreaming = true, maxConcurrentTools = DEFAULT_MAX_CONCURRENT_TOOLS, maxCostUsd: maxCostUsdOpt, onText, onToolStart, onCitation, documents, clientDisconnected = { value: false }, startedAt = Date.now(), maxDurationMs = 15 * 60 * 1000, } = opts;
54
+ // Resolve cost budget: explicit opt > env var > default
55
+ const envBudget = parseFloat(process.env.WHALE_COST_BUDGET_USD || "");
56
+ const maxCostUsd = maxCostUsdOpt ?? (isFinite(envBudget) ? envBudget : DEFAULT_SESSION_COST_BUDGET_USD);
53
57
  // Auto-inject delegate_task for all models (subagents always use Claude Haiku/Sonnet)
54
58
  // activeTools is mutable — discover_tools adds to it during the session
55
59
  const activeTools = [...inputTools];
@@ -92,10 +96,12 @@ export async function runServerAgentLoop(opts) {
92
96
  let sessionCostUsd = 0;
93
97
  let compactionCount = 0;
94
98
  let finalResponse = "";
99
+ let lastStopReason = "end_turn";
95
100
  const allTextResponses = [];
96
101
  const allToolNames = [];
97
102
  const allCitations = [];
98
103
  const turnMetrics = [];
104
+ const costWarningsEmitted = new Set();
99
105
  while (turnCount < maxTurns) {
100
106
  // Abort checks
101
107
  if (clientDisconnected.value) {
@@ -164,13 +170,17 @@ export async function runServerAgentLoop(opts) {
164
170
  ];
165
171
  // Resolve tool_choice for this turn
166
172
  const recentToolUses = turnMetrics.slice(-3).flatMap(t => t.toolsUsed);
167
- const resolvedToolChoice = resolveToolChoice({
173
+ let resolvedToolChoice = resolveToolChoice({
168
174
  toolChoice: opts.toolChoice,
169
175
  turnCount,
170
176
  recentToolUses,
171
177
  availableToolNames: tools.map(t => t.name),
172
178
  userMessage: firstUserText,
173
179
  });
180
+ // Anthropic API: forced tool_choice ("any" or specific tool) is incompatible with thinking — downgrade to "auto"
181
+ if (thinkingCfg.thinking.type !== "disabled" && resolvedToolChoice !== "auto" && resolvedToolChoice !== "none") {
182
+ resolvedToolChoice = "auto";
183
+ }
174
184
  const { toolChoice: anthropicToolChoice, omitTools } = mapToolChoiceForAnthropic(resolvedToolChoice);
175
185
  if (omitTools) {
176
186
  log.info({ turn: turnCount, resolvedToolChoice }, "tool_choice=none — omitting tools");
@@ -254,6 +264,8 @@ export async function runServerAgentLoop(opts) {
254
264
  cacheReadTokens += turnCacheRead;
255
265
  // Update cost (include cache tokens for accurate pricing)
256
266
  sessionCostUsd = estimateCostUsd(totalIn, totalOut, model, 0, cacheReadTokens, cacheCreationTokens);
267
+ // Graduated cost warnings — give the LLM visibility into spend
268
+ emitCostWarningIfNeeded(sessionCostUsd, maxCostUsd, costWarningsEmitted, onText);
257
269
  // Record per-turn metrics for observability
258
270
  const turnToolNames = toolUseBlocks.map(b => b.name);
259
271
  turnMetrics.push({
@@ -271,6 +283,7 @@ export async function runServerAgentLoop(opts) {
271
283
  // Compaction handling — API paused after generating summary.
272
284
  // Preserve last 2 messages (1 user + 1 assistant turn) for continuity,
273
285
  // then resume. This is NOT a new turn — just context compression.
286
+ lastStopReason = streamResult.stopReason || "end_turn";
274
287
  if (streamResult.stopReason === "compaction" && compactionContent) {
275
288
  compactionCount++;
276
289
  log.info({ compactionCount }, "compaction — preserving last 2 messages, resuming");
@@ -331,6 +344,8 @@ export async function runServerAgentLoop(opts) {
331
344
  totalIn += subagentTokens.input;
332
345
  totalOut += subagentTokens.output;
333
346
  sessionCostUsd = estimateCostUsd(totalIn, totalOut, model, 0, cacheReadTokens, cacheCreationTokens) + subagentTokens.costUsd;
347
+ // Cost warnings after subagent aggregation (subagents can be expensive)
348
+ emitCostWarningIfNeeded(sessionCostUsd, maxCostUsd, costWarningsEmitted, onText);
334
349
  const assistantContent = buildAssistantContent({ text: currentText, toolUseBlocks, compactionContent });
335
350
  messages.push({ role: "assistant", content: assistantContent });
336
351
  messages.push({ role: "user", content: toolResults });
@@ -426,6 +441,8 @@ export async function runServerAgentLoop(opts) {
426
441
  }
427
442
  }
428
443
  sessionCostUsd = estimateCostUsd(totalIn, totalOut, model, 0, cacheReadTokens, cacheCreationTokens);
444
+ // Graduated cost warnings (non-streaming path)
445
+ emitCostWarningIfNeeded(sessionCostUsd, maxCostUsd, costWarningsEmitted, onText);
429
446
  // Record per-turn metrics (non-streaming)
430
447
  const nsTurnToolNames = toolUseBlocks.map(b => b.name);
431
448
  turnMetrics.push({
@@ -440,6 +457,7 @@ export async function runServerAgentLoop(opts) {
440
457
  });
441
458
  if (currentText)
442
459
  allTextResponses.push(currentText);
460
+ lastStopReason = response.stop_reason || "end_turn";
443
461
  // Compaction handling (non-streaming) — same logic as streaming path
444
462
  if (response.stop_reason === "compaction" && nsCompactionContent !== null) {
445
463
  compactionCount++;
@@ -488,6 +506,8 @@ export async function runServerAgentLoop(opts) {
488
506
  totalIn += nonStreamSubTokens.input;
489
507
  totalOut += nonStreamSubTokens.output;
490
508
  sessionCostUsd = estimateCostUsd(totalIn, totalOut, model, 0, cacheReadTokens, cacheCreationTokens) + nonStreamSubTokens.costUsd;
509
+ // Cost warnings after subagent aggregation (non-streaming)
510
+ emitCostWarningIfNeeded(sessionCostUsd, maxCostUsd, costWarningsEmitted, onText);
491
511
  const assistantContent = buildAssistantContent({ text: currentText, toolUseBlocks });
492
512
  messages.push({ role: "assistant", content: assistantContent });
493
513
  messages.push({ role: "user", content: toolResults });
@@ -514,21 +534,20 @@ export async function runServerAgentLoop(opts) {
514
534
  loopDetectorStats: loopDetector.getSessionStats(),
515
535
  turns: turnMetrics,
516
536
  citations: allCitations,
537
+ stopReason: lastStopReason,
517
538
  };
518
539
  }
519
540
  // ============================================================================
520
541
  // TOOL EXECUTOR FACTORY — creates executor for dispatchTools with delegation
521
542
  // ============================================================================
522
543
  function makeToolExecutor(opts, tools, allToolNames, subagentTokens, discoveredToolNames) {
523
- const { anthropic, supabase, storeId, traceId, userId, userEmail, conversationId, agentId, executeTool, onToolResult, onToolProgress, onSubagentProgress, clientDisconnected = { value: false }, startedAt = Date.now(), maxDurationMs = 5 * 60 * 1000, } = opts;
544
+ const { anthropic, supabase, storeId, traceId, userId, userEmail, conversationId, agentId, executeTool, onToolResult, onToolProgress, onSubagentProgress, clientDisconnected = { value: false }, startedAt = Date.now(), maxDurationMs = 15 * 60 * 1000, } = opts;
524
545
  return async (name, input) => {
525
546
  allToolNames.push(name);
526
- // Subagent delegation
547
+ // Subagent delegation — demote models to control cost (sub-agents should never run Opus)
527
548
  if (name === "delegate_task") {
528
549
  const subPrompt = String(input.prompt || "");
529
- const subModelInput = String(input.model || "haiku");
530
- const subModel = (subModelInput === "opus" ? "opus" :
531
- subModelInput === "sonnet" ? "sonnet" : "haiku");
550
+ const subModel = demoteSubagentModel(input.model ? String(input.model) : undefined);
532
551
  const subMaxTurns = Math.min(Math.max(1, Number(input.max_turns) || 6), 12);
533
552
  const subTools = tools.filter((t) => t.name !== "delegate_task");
534
553
  const subId = `sub-${Date.now().toString(36)}`;
@@ -547,37 +566,37 @@ function makeToolExecutor(opts, tools, allToolNames, subagentTokens, discoveredT
547
566
  : subModel === "sonnet" ? MODELS.SONNET : MODELS.HAIKU;
548
567
  try {
549
568
  const subEndTime = Date.now();
550
- const subBytes = new Uint8Array(8);
551
- crypto.getRandomValues(subBytes);
552
- const subSpanId = Array.from(subBytes).map(b => b.toString(16).padStart(2, "0")).join("");
553
- await supabase.from("audit_logs").insert({
569
+ queueSpan(auditRowToSpan({
554
570
  action: "chat.subagent_complete", severity: "info",
555
571
  store_id: storeId || null, resource_type: "chat_subagent",
556
572
  resource_id: agentId || null, request_id: traceId || null,
557
573
  conversation_id: conversationId || null, source: "server_subagent",
558
- user_id: userId || null, user_email: userEmail || null,
574
+ user_id: userId || null,
575
+ user_email: userEmail || null,
559
576
  input_tokens: subResult.tokensUsed.input, output_tokens: subResult.tokensUsed.output,
560
577
  total_cost: subResult.costUsd, model: subModelId, duration_ms: subDurationMs,
561
- // OTEL fields
562
578
  trace_id: traceId || null,
563
- span_id: subSpanId,
564
579
  span_kind: "INTERNAL",
565
580
  service_name: "agent-server",
566
581
  status_code: subResult.success ? "OK" : "ERROR",
567
582
  start_time: new Date(subEndTime - subDurationMs).toISOString(),
568
583
  end_time: new Date(subEndTime).toISOString(),
584
+ stop_reason: subResult.stopReason || undefined,
585
+ turn_number: subResult.turnCount || 1,
586
+ parent_conversation_id: conversationId || undefined,
569
587
  details: {
570
588
  subagent_model: subModel, turn_count: subResult.turnCount,
571
589
  tool_calls: subResult.toolsUsed.length, tool_names: subResult.toolsUsed,
572
590
  cost_usd: subResult.costUsd, success: subResult.success,
573
591
  prompt_preview: subPrompt.substring(0, 200),
574
- // gen_ai fields for SwiftUI cost display
575
592
  "gen_ai.request.model": subModelId,
576
593
  "gen_ai.usage.input_tokens": subResult.tokensUsed.input,
577
594
  "gen_ai.usage.output_tokens": subResult.tokensUsed.output,
595
+ "gen_ai.usage.cache_read_tokens": subResult.tokensUsed.cacheRead || 0,
596
+ "gen_ai.usage.cache_creation_tokens": subResult.tokensUsed.cacheCreation || 0,
578
597
  "gen_ai.usage.cost": subResult.costUsd,
579
598
  },
580
- });
599
+ }));
581
600
  }
582
601
  catch (err) {
583
602
  log.error({ err: err.message }, "failed to log subagent delegation audit");