cowork-os 0.3.21

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 (526) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1638 -0
  3. package/bin/cowork.js +42 -0
  4. package/build/entitlements.mac.plist +16 -0
  5. package/build/icon.icns +0 -0
  6. package/build/icon.png +0 -0
  7. package/dist/electron/electron/activity/ActivityRepository.js +190 -0
  8. package/dist/electron/electron/agent/browser/browser-service.js +639 -0
  9. package/dist/electron/electron/agent/context-manager.js +225 -0
  10. package/dist/electron/electron/agent/custom-skill-loader.js +566 -0
  11. package/dist/electron/electron/agent/daemon.js +975 -0
  12. package/dist/electron/electron/agent/executor.js +3561 -0
  13. package/dist/electron/electron/agent/llm/anthropic-provider.js +155 -0
  14. package/dist/electron/electron/agent/llm/bedrock-provider.js +202 -0
  15. package/dist/electron/electron/agent/llm/gemini-provider.js +375 -0
  16. package/dist/electron/electron/agent/llm/index.js +34 -0
  17. package/dist/electron/electron/agent/llm/ollama-provider.js +263 -0
  18. package/dist/electron/electron/agent/llm/openai-oauth.js +101 -0
  19. package/dist/electron/electron/agent/llm/openai-provider.js +657 -0
  20. package/dist/electron/electron/agent/llm/openrouter-provider.js +232 -0
  21. package/dist/electron/electron/agent/llm/pricing.js +160 -0
  22. package/dist/electron/electron/agent/llm/provider-factory.js +880 -0
  23. package/dist/electron/electron/agent/llm/types.js +178 -0
  24. package/dist/electron/electron/agent/queue-manager.js +378 -0
  25. package/dist/electron/electron/agent/sandbox/docker-sandbox.js +402 -0
  26. package/dist/electron/electron/agent/sandbox/macos-sandbox.js +407 -0
  27. package/dist/electron/electron/agent/sandbox/runner.js +410 -0
  28. package/dist/electron/electron/agent/sandbox/sandbox-factory.js +228 -0
  29. package/dist/electron/electron/agent/sandbox/security-utils.js +258 -0
  30. package/dist/electron/electron/agent/search/brave-provider.js +119 -0
  31. package/dist/electron/electron/agent/search/google-provider.js +100 -0
  32. package/dist/electron/electron/agent/search/index.js +28 -0
  33. package/dist/electron/electron/agent/search/provider-factory.js +395 -0
  34. package/dist/electron/electron/agent/search/serpapi-provider.js +112 -0
  35. package/dist/electron/electron/agent/search/tavily-provider.js +90 -0
  36. package/dist/electron/electron/agent/search/types.js +40 -0
  37. package/dist/electron/electron/agent/security/index.js +12 -0
  38. package/dist/electron/electron/agent/security/input-sanitizer.js +303 -0
  39. package/dist/electron/electron/agent/security/output-filter.js +217 -0
  40. package/dist/electron/electron/agent/skill-eligibility.js +281 -0
  41. package/dist/electron/electron/agent/skill-registry.js +396 -0
  42. package/dist/electron/electron/agent/skills/document.js +878 -0
  43. package/dist/electron/electron/agent/skills/image-generator.js +225 -0
  44. package/dist/electron/electron/agent/skills/organizer.js +141 -0
  45. package/dist/electron/electron/agent/skills/presentation.js +367 -0
  46. package/dist/electron/electron/agent/skills/spreadsheet.js +165 -0
  47. package/dist/electron/electron/agent/tools/browser-tools.js +523 -0
  48. package/dist/electron/electron/agent/tools/builtin-settings.js +384 -0
  49. package/dist/electron/electron/agent/tools/canvas-tools.js +530 -0
  50. package/dist/electron/electron/agent/tools/cron-tools.js +577 -0
  51. package/dist/electron/electron/agent/tools/edit-tools.js +194 -0
  52. package/dist/electron/electron/agent/tools/file-tools.js +719 -0
  53. package/dist/electron/electron/agent/tools/glob-tools.js +283 -0
  54. package/dist/electron/electron/agent/tools/grep-tools.js +387 -0
  55. package/dist/electron/electron/agent/tools/image-tools.js +111 -0
  56. package/dist/electron/electron/agent/tools/mention-tools.js +282 -0
  57. package/dist/electron/electron/agent/tools/node-tools.js +476 -0
  58. package/dist/electron/electron/agent/tools/registry.js +2719 -0
  59. package/dist/electron/electron/agent/tools/search-tools.js +91 -0
  60. package/dist/electron/electron/agent/tools/shell-tools.js +574 -0
  61. package/dist/electron/electron/agent/tools/skill-tools.js +274 -0
  62. package/dist/electron/electron/agent/tools/system-tools.js +578 -0
  63. package/dist/electron/electron/agent/tools/web-fetch-tools.js +444 -0
  64. package/dist/electron/electron/agent/tools/x-tools.js +264 -0
  65. package/dist/electron/electron/agents/AgentRoleRepository.js +420 -0
  66. package/dist/electron/electron/agents/HeartbeatService.js +356 -0
  67. package/dist/electron/electron/agents/MentionRepository.js +197 -0
  68. package/dist/electron/electron/agents/TaskSubscriptionRepository.js +168 -0
  69. package/dist/electron/electron/agents/WorkingStateRepository.js +229 -0
  70. package/dist/electron/electron/canvas/canvas-manager.js +714 -0
  71. package/dist/electron/electron/canvas/canvas-preload.js +53 -0
  72. package/dist/electron/electron/canvas/canvas-protocol.js +195 -0
  73. package/dist/electron/electron/canvas/canvas-store.js +174 -0
  74. package/dist/electron/electron/canvas/index.js +13 -0
  75. package/dist/electron/electron/control-plane/client.js +364 -0
  76. package/dist/electron/electron/control-plane/handlers.js +572 -0
  77. package/dist/electron/electron/control-plane/index.js +41 -0
  78. package/dist/electron/electron/control-plane/node-manager.js +264 -0
  79. package/dist/electron/electron/control-plane/protocol.js +194 -0
  80. package/dist/electron/electron/control-plane/remote-client.js +437 -0
  81. package/dist/electron/electron/control-plane/server.js +640 -0
  82. package/dist/electron/electron/control-plane/settings.js +369 -0
  83. package/dist/electron/electron/control-plane/ssh-tunnel.js +549 -0
  84. package/dist/electron/electron/cron/index.js +30 -0
  85. package/dist/electron/electron/cron/schedule.js +190 -0
  86. package/dist/electron/electron/cron/service.js +614 -0
  87. package/dist/electron/electron/cron/store.js +155 -0
  88. package/dist/electron/electron/cron/types.js +82 -0
  89. package/dist/electron/electron/cron/webhook.js +258 -0
  90. package/dist/electron/electron/database/SecureSettingsRepository.js +444 -0
  91. package/dist/electron/electron/database/TaskLabelRepository.js +120 -0
  92. package/dist/electron/electron/database/repositories.js +1781 -0
  93. package/dist/electron/electron/database/schema.js +978 -0
  94. package/dist/electron/electron/extensions/index.js +33 -0
  95. package/dist/electron/electron/extensions/loader.js +313 -0
  96. package/dist/electron/electron/extensions/registry.js +485 -0
  97. package/dist/electron/electron/extensions/types.js +11 -0
  98. package/dist/electron/electron/gateway/channel-registry.js +1102 -0
  99. package/dist/electron/electron/gateway/channels/bluebubbles-client.js +479 -0
  100. package/dist/electron/electron/gateway/channels/bluebubbles.js +432 -0
  101. package/dist/electron/electron/gateway/channels/discord.js +975 -0
  102. package/dist/electron/electron/gateway/channels/email-client.js +593 -0
  103. package/dist/electron/electron/gateway/channels/email.js +443 -0
  104. package/dist/electron/electron/gateway/channels/google-chat.js +631 -0
  105. package/dist/electron/electron/gateway/channels/imessage-client.js +363 -0
  106. package/dist/electron/electron/gateway/channels/imessage.js +465 -0
  107. package/dist/electron/electron/gateway/channels/index.js +36 -0
  108. package/dist/electron/electron/gateway/channels/line-client.js +470 -0
  109. package/dist/electron/electron/gateway/channels/line.js +479 -0
  110. package/dist/electron/electron/gateway/channels/matrix-client.js +432 -0
  111. package/dist/electron/electron/gateway/channels/matrix.js +592 -0
  112. package/dist/electron/electron/gateway/channels/mattermost-client.js +394 -0
  113. package/dist/electron/electron/gateway/channels/mattermost.js +496 -0
  114. package/dist/electron/electron/gateway/channels/signal-client.js +500 -0
  115. package/dist/electron/electron/gateway/channels/signal.js +582 -0
  116. package/dist/electron/electron/gateway/channels/slack.js +415 -0
  117. package/dist/electron/electron/gateway/channels/teams.js +596 -0
  118. package/dist/electron/electron/gateway/channels/telegram.js +1390 -0
  119. package/dist/electron/electron/gateway/channels/twitch-client.js +502 -0
  120. package/dist/electron/electron/gateway/channels/twitch.js +396 -0
  121. package/dist/electron/electron/gateway/channels/types.js +8 -0
  122. package/dist/electron/electron/gateway/channels/whatsapp.js +953 -0
  123. package/dist/electron/electron/gateway/context-policy.js +268 -0
  124. package/dist/electron/electron/gateway/index.js +1063 -0
  125. package/dist/electron/electron/gateway/infrastructure.js +496 -0
  126. package/dist/electron/electron/gateway/router.js +2700 -0
  127. package/dist/electron/electron/gateway/security.js +375 -0
  128. package/dist/electron/electron/gateway/session.js +115 -0
  129. package/dist/electron/electron/gateway/tunnel.js +503 -0
  130. package/dist/electron/electron/guardrails/guardrail-manager.js +348 -0
  131. package/dist/electron/electron/hooks/gmail-watcher.js +300 -0
  132. package/dist/electron/electron/hooks/index.js +46 -0
  133. package/dist/electron/electron/hooks/mappings.js +381 -0
  134. package/dist/electron/electron/hooks/server.js +480 -0
  135. package/dist/electron/electron/hooks/settings.js +447 -0
  136. package/dist/electron/electron/hooks/types.js +41 -0
  137. package/dist/electron/electron/ipc/canvas-handlers.js +158 -0
  138. package/dist/electron/electron/ipc/handlers.js +3138 -0
  139. package/dist/electron/electron/ipc/mission-control-handlers.js +141 -0
  140. package/dist/electron/electron/main.js +448 -0
  141. package/dist/electron/electron/mcp/client/MCPClientManager.js +330 -0
  142. package/dist/electron/electron/mcp/client/MCPServerConnection.js +437 -0
  143. package/dist/electron/electron/mcp/client/transports/SSETransport.js +304 -0
  144. package/dist/electron/electron/mcp/client/transports/StdioTransport.js +307 -0
  145. package/dist/electron/electron/mcp/client/transports/WebSocketTransport.js +329 -0
  146. package/dist/electron/electron/mcp/host/MCPHostServer.js +354 -0
  147. package/dist/electron/electron/mcp/host/ToolAdapter.js +100 -0
  148. package/dist/electron/electron/mcp/registry/MCPRegistryManager.js +497 -0
  149. package/dist/electron/electron/mcp/settings.js +446 -0
  150. package/dist/electron/electron/mcp/types.js +59 -0
  151. package/dist/electron/electron/memory/MemoryService.js +435 -0
  152. package/dist/electron/electron/notifications/index.js +17 -0
  153. package/dist/electron/electron/notifications/service.js +118 -0
  154. package/dist/electron/electron/notifications/store.js +144 -0
  155. package/dist/electron/electron/preload.js +842 -0
  156. package/dist/electron/electron/reports/StandupReportService.js +272 -0
  157. package/dist/electron/electron/security/concurrency.js +293 -0
  158. package/dist/electron/electron/security/index.js +15 -0
  159. package/dist/electron/electron/security/policy-manager.js +435 -0
  160. package/dist/electron/electron/settings/appearance-manager.js +193 -0
  161. package/dist/electron/electron/settings/personality-manager.js +724 -0
  162. package/dist/electron/electron/settings/x-manager.js +58 -0
  163. package/dist/electron/electron/tailscale/exposure.js +188 -0
  164. package/dist/electron/electron/tailscale/index.js +28 -0
  165. package/dist/electron/electron/tailscale/settings.js +205 -0
  166. package/dist/electron/electron/tailscale/tailscale.js +355 -0
  167. package/dist/electron/electron/tray/QuickInputWindow.js +568 -0
  168. package/dist/electron/electron/tray/TrayManager.js +895 -0
  169. package/dist/electron/electron/tray/index.js +9 -0
  170. package/dist/electron/electron/updater/index.js +6 -0
  171. package/dist/electron/electron/updater/update-manager.js +418 -0
  172. package/dist/electron/electron/utils/env-migration.js +209 -0
  173. package/dist/electron/electron/utils/process.js +102 -0
  174. package/dist/electron/electron/utils/rate-limiter.js +104 -0
  175. package/dist/electron/electron/utils/validation.js +419 -0
  176. package/dist/electron/electron/utils/x-cli.js +177 -0
  177. package/dist/electron/electron/voice/VoiceService.js +507 -0
  178. package/dist/electron/electron/voice/index.js +14 -0
  179. package/dist/electron/electron/voice/voice-settings-manager.js +359 -0
  180. package/dist/electron/shared/channelMessages.js +170 -0
  181. package/dist/electron/shared/types.js +1185 -0
  182. package/package.json +159 -0
  183. package/resources/skills/1password.json +10 -0
  184. package/resources/skills/add-documentation.json +31 -0
  185. package/resources/skills/analyze-csv.json +17 -0
  186. package/resources/skills/apple-notes.json +10 -0
  187. package/resources/skills/apple-reminders.json +10 -0
  188. package/resources/skills/auto-commenter.json +10 -0
  189. package/resources/skills/bear-notes.json +10 -0
  190. package/resources/skills/bird.json +35 -0
  191. package/resources/skills/blogwatcher.json +10 -0
  192. package/resources/skills/blucli.json +10 -0
  193. package/resources/skills/bluebubbles.json +10 -0
  194. package/resources/skills/camsnap.json +10 -0
  195. package/resources/skills/clean-imports.json +18 -0
  196. package/resources/skills/code-review.json +18 -0
  197. package/resources/skills/coding-agent.json +10 -0
  198. package/resources/skills/compare-files.json +23 -0
  199. package/resources/skills/convert-code.json +34 -0
  200. package/resources/skills/create-changelog.json +24 -0
  201. package/resources/skills/debug-error.json +17 -0
  202. package/resources/skills/dependency-check.json +10 -0
  203. package/resources/skills/discord.json +10 -0
  204. package/resources/skills/eightctl.json +10 -0
  205. package/resources/skills/explain-code.json +29 -0
  206. package/resources/skills/extract-todos.json +18 -0
  207. package/resources/skills/food-order.json +10 -0
  208. package/resources/skills/gemini.json +10 -0
  209. package/resources/skills/generate-readme.json +10 -0
  210. package/resources/skills/gifgrep.json +10 -0
  211. package/resources/skills/git-commit.json +10 -0
  212. package/resources/skills/github.json +10 -0
  213. package/resources/skills/gog.json +10 -0
  214. package/resources/skills/goplaces.json +10 -0
  215. package/resources/skills/himalaya.json +10 -0
  216. package/resources/skills/imsg.json +10 -0
  217. package/resources/skills/karpathy-guidelines.json +12 -0
  218. package/resources/skills/last30days.json +26 -0
  219. package/resources/skills/local-places.json +10 -0
  220. package/resources/skills/mcporter.json +10 -0
  221. package/resources/skills/model-usage.json +10 -0
  222. package/resources/skills/nano-banana-pro.json +10 -0
  223. package/resources/skills/nano-pdf.json +10 -0
  224. package/resources/skills/notion.json +10 -0
  225. package/resources/skills/obsidian.json +10 -0
  226. package/resources/skills/openai-image-gen.json +10 -0
  227. package/resources/skills/openai-whisper-api.json +10 -0
  228. package/resources/skills/openai-whisper.json +10 -0
  229. package/resources/skills/openhue.json +10 -0
  230. package/resources/skills/oracle.json +10 -0
  231. package/resources/skills/ordercli.json +10 -0
  232. package/resources/skills/peekaboo.json +10 -0
  233. package/resources/skills/project-structure.json +10 -0
  234. package/resources/skills/proofread.json +17 -0
  235. package/resources/skills/refactor-code.json +31 -0
  236. package/resources/skills/rename-symbol.json +23 -0
  237. package/resources/skills/sag.json +10 -0
  238. package/resources/skills/security-audit.json +18 -0
  239. package/resources/skills/session-logs.json +10 -0
  240. package/resources/skills/sherpa-onnx-tts.json +10 -0
  241. package/resources/skills/skill-creator.json +15 -0
  242. package/resources/skills/skill-hub.json +29 -0
  243. package/resources/skills/slack.json +10 -0
  244. package/resources/skills/songsee.json +10 -0
  245. package/resources/skills/sonoscli.json +10 -0
  246. package/resources/skills/spotify-player.json +10 -0
  247. package/resources/skills/startup-cfo.json +55 -0
  248. package/resources/skills/summarize-folder.json +18 -0
  249. package/resources/skills/summarize.json +10 -0
  250. package/resources/skills/things-mac.json +10 -0
  251. package/resources/skills/tmux.json +10 -0
  252. package/resources/skills/translate.json +36 -0
  253. package/resources/skills/trello.json +10 -0
  254. package/resources/skills/video-frames.json +10 -0
  255. package/resources/skills/voice-call.json +10 -0
  256. package/resources/skills/wacli.json +10 -0
  257. package/resources/skills/weather.json +10 -0
  258. package/resources/skills/write-tests.json +31 -0
  259. package/src/electron/activity/ActivityRepository.ts +238 -0
  260. package/src/electron/agent/browser/browser-service.ts +721 -0
  261. package/src/electron/agent/context-manager.ts +257 -0
  262. package/src/electron/agent/custom-skill-loader.ts +634 -0
  263. package/src/electron/agent/daemon.ts +1097 -0
  264. package/src/electron/agent/executor.ts +4017 -0
  265. package/src/electron/agent/llm/anthropic-provider.ts +175 -0
  266. package/src/electron/agent/llm/bedrock-provider.ts +236 -0
  267. package/src/electron/agent/llm/gemini-provider.ts +422 -0
  268. package/src/electron/agent/llm/index.ts +9 -0
  269. package/src/electron/agent/llm/ollama-provider.ts +347 -0
  270. package/src/electron/agent/llm/openai-oauth.ts +127 -0
  271. package/src/electron/agent/llm/openai-provider.ts +686 -0
  272. package/src/electron/agent/llm/openrouter-provider.ts +273 -0
  273. package/src/electron/agent/llm/pricing.ts +180 -0
  274. package/src/electron/agent/llm/provider-factory.ts +971 -0
  275. package/src/electron/agent/llm/types.ts +291 -0
  276. package/src/electron/agent/queue-manager.ts +408 -0
  277. package/src/electron/agent/sandbox/docker-sandbox.ts +453 -0
  278. package/src/electron/agent/sandbox/macos-sandbox.ts +426 -0
  279. package/src/electron/agent/sandbox/runner.ts +453 -0
  280. package/src/electron/agent/sandbox/sandbox-factory.ts +337 -0
  281. package/src/electron/agent/sandbox/security-utils.ts +251 -0
  282. package/src/electron/agent/search/brave-provider.ts +141 -0
  283. package/src/electron/agent/search/google-provider.ts +131 -0
  284. package/src/electron/agent/search/index.ts +6 -0
  285. package/src/electron/agent/search/provider-factory.ts +450 -0
  286. package/src/electron/agent/search/serpapi-provider.ts +138 -0
  287. package/src/electron/agent/search/tavily-provider.ts +108 -0
  288. package/src/electron/agent/search/types.ts +118 -0
  289. package/src/electron/agent/security/index.ts +20 -0
  290. package/src/electron/agent/security/input-sanitizer.ts +380 -0
  291. package/src/electron/agent/security/output-filter.ts +259 -0
  292. package/src/electron/agent/skill-eligibility.ts +334 -0
  293. package/src/electron/agent/skill-registry.ts +457 -0
  294. package/src/electron/agent/skills/document.ts +1070 -0
  295. package/src/electron/agent/skills/image-generator.ts +272 -0
  296. package/src/electron/agent/skills/organizer.ts +131 -0
  297. package/src/electron/agent/skills/presentation.ts +418 -0
  298. package/src/electron/agent/skills/spreadsheet.ts +166 -0
  299. package/src/electron/agent/tools/browser-tools.ts +546 -0
  300. package/src/electron/agent/tools/builtin-settings.ts +422 -0
  301. package/src/electron/agent/tools/canvas-tools.ts +572 -0
  302. package/src/electron/agent/tools/cron-tools.ts +723 -0
  303. package/src/electron/agent/tools/edit-tools.ts +196 -0
  304. package/src/electron/agent/tools/file-tools.ts +811 -0
  305. package/src/electron/agent/tools/glob-tools.ts +303 -0
  306. package/src/electron/agent/tools/grep-tools.ts +432 -0
  307. package/src/electron/agent/tools/image-tools.ts +126 -0
  308. package/src/electron/agent/tools/mention-tools.ts +371 -0
  309. package/src/electron/agent/tools/node-tools.ts +550 -0
  310. package/src/electron/agent/tools/registry.ts +3052 -0
  311. package/src/electron/agent/tools/search-tools.ts +111 -0
  312. package/src/electron/agent/tools/shell-tools.ts +651 -0
  313. package/src/electron/agent/tools/skill-tools.ts +340 -0
  314. package/src/electron/agent/tools/system-tools.ts +665 -0
  315. package/src/electron/agent/tools/web-fetch-tools.ts +528 -0
  316. package/src/electron/agent/tools/x-tools.ts +267 -0
  317. package/src/electron/agents/AgentRoleRepository.ts +557 -0
  318. package/src/electron/agents/HeartbeatService.ts +469 -0
  319. package/src/electron/agents/MentionRepository.ts +242 -0
  320. package/src/electron/agents/TaskSubscriptionRepository.ts +231 -0
  321. package/src/electron/agents/WorkingStateRepository.ts +278 -0
  322. package/src/electron/canvas/canvas-manager.ts +818 -0
  323. package/src/electron/canvas/canvas-preload.ts +102 -0
  324. package/src/electron/canvas/canvas-protocol.ts +174 -0
  325. package/src/electron/canvas/canvas-store.ts +200 -0
  326. package/src/electron/canvas/index.ts +8 -0
  327. package/src/electron/control-plane/client.ts +527 -0
  328. package/src/electron/control-plane/handlers.ts +723 -0
  329. package/src/electron/control-plane/index.ts +51 -0
  330. package/src/electron/control-plane/node-manager.ts +322 -0
  331. package/src/electron/control-plane/protocol.ts +269 -0
  332. package/src/electron/control-plane/remote-client.ts +517 -0
  333. package/src/electron/control-plane/server.ts +853 -0
  334. package/src/electron/control-plane/settings.ts +401 -0
  335. package/src/electron/control-plane/ssh-tunnel.ts +624 -0
  336. package/src/electron/cron/index.ts +9 -0
  337. package/src/electron/cron/schedule.ts +217 -0
  338. package/src/electron/cron/service.ts +743 -0
  339. package/src/electron/cron/store.ts +165 -0
  340. package/src/electron/cron/types.ts +291 -0
  341. package/src/electron/cron/webhook.ts +303 -0
  342. package/src/electron/database/SecureSettingsRepository.ts +514 -0
  343. package/src/electron/database/TaskLabelRepository.ts +148 -0
  344. package/src/electron/database/repositories.ts +2397 -0
  345. package/src/electron/database/schema.ts +1017 -0
  346. package/src/electron/extensions/index.ts +18 -0
  347. package/src/electron/extensions/loader.ts +336 -0
  348. package/src/electron/extensions/registry.ts +546 -0
  349. package/src/electron/extensions/types.ts +372 -0
  350. package/src/electron/gateway/channel-registry.ts +1267 -0
  351. package/src/electron/gateway/channels/bluebubbles-client.ts +641 -0
  352. package/src/electron/gateway/channels/bluebubbles.ts +509 -0
  353. package/src/electron/gateway/channels/discord.ts +1150 -0
  354. package/src/electron/gateway/channels/email-client.ts +708 -0
  355. package/src/electron/gateway/channels/email.ts +516 -0
  356. package/src/electron/gateway/channels/google-chat.ts +760 -0
  357. package/src/electron/gateway/channels/imessage-client.ts +473 -0
  358. package/src/electron/gateway/channels/imessage.ts +520 -0
  359. package/src/electron/gateway/channels/index.ts +21 -0
  360. package/src/electron/gateway/channels/line-client.ts +598 -0
  361. package/src/electron/gateway/channels/line.ts +559 -0
  362. package/src/electron/gateway/channels/matrix-client.ts +632 -0
  363. package/src/electron/gateway/channels/matrix.ts +655 -0
  364. package/src/electron/gateway/channels/mattermost-client.ts +526 -0
  365. package/src/electron/gateway/channels/mattermost.ts +550 -0
  366. package/src/electron/gateway/channels/signal-client.ts +722 -0
  367. package/src/electron/gateway/channels/signal.ts +666 -0
  368. package/src/electron/gateway/channels/slack.ts +458 -0
  369. package/src/electron/gateway/channels/teams.ts +681 -0
  370. package/src/electron/gateway/channels/telegram.ts +1727 -0
  371. package/src/electron/gateway/channels/twitch-client.ts +665 -0
  372. package/src/electron/gateway/channels/twitch.ts +468 -0
  373. package/src/electron/gateway/channels/types.ts +1002 -0
  374. package/src/electron/gateway/channels/whatsapp.ts +1101 -0
  375. package/src/electron/gateway/context-policy.ts +382 -0
  376. package/src/electron/gateway/index.ts +1274 -0
  377. package/src/electron/gateway/infrastructure.ts +645 -0
  378. package/src/electron/gateway/router.ts +3206 -0
  379. package/src/electron/gateway/security.ts +422 -0
  380. package/src/electron/gateway/session.ts +144 -0
  381. package/src/electron/gateway/tunnel.ts +626 -0
  382. package/src/electron/guardrails/guardrail-manager.ts +380 -0
  383. package/src/electron/hooks/gmail-watcher.ts +355 -0
  384. package/src/electron/hooks/index.ts +30 -0
  385. package/src/electron/hooks/mappings.ts +404 -0
  386. package/src/electron/hooks/server.ts +574 -0
  387. package/src/electron/hooks/settings.ts +466 -0
  388. package/src/electron/hooks/types.ts +245 -0
  389. package/src/electron/ipc/canvas-handlers.ts +223 -0
  390. package/src/electron/ipc/handlers.ts +3661 -0
  391. package/src/electron/ipc/mission-control-handlers.ts +182 -0
  392. package/src/electron/main.ts +496 -0
  393. package/src/electron/mcp/client/MCPClientManager.ts +406 -0
  394. package/src/electron/mcp/client/MCPServerConnection.ts +514 -0
  395. package/src/electron/mcp/client/transports/SSETransport.ts +360 -0
  396. package/src/electron/mcp/client/transports/StdioTransport.ts +355 -0
  397. package/src/electron/mcp/client/transports/WebSocketTransport.ts +384 -0
  398. package/src/electron/mcp/host/MCPHostServer.ts +388 -0
  399. package/src/electron/mcp/host/ToolAdapter.ts +140 -0
  400. package/src/electron/mcp/registry/MCPRegistryManager.ts +565 -0
  401. package/src/electron/mcp/settings.ts +468 -0
  402. package/src/electron/mcp/types.ts +371 -0
  403. package/src/electron/memory/MemoryService.ts +523 -0
  404. package/src/electron/notifications/index.ts +16 -0
  405. package/src/electron/notifications/service.ts +161 -0
  406. package/src/electron/notifications/store.ts +163 -0
  407. package/src/electron/preload.ts +2845 -0
  408. package/src/electron/reports/StandupReportService.ts +356 -0
  409. package/src/electron/security/concurrency.ts +333 -0
  410. package/src/electron/security/index.ts +17 -0
  411. package/src/electron/security/policy-manager.ts +539 -0
  412. package/src/electron/settings/appearance-manager.ts +182 -0
  413. package/src/electron/settings/personality-manager.ts +800 -0
  414. package/src/electron/settings/x-manager.ts +62 -0
  415. package/src/electron/tailscale/exposure.ts +262 -0
  416. package/src/electron/tailscale/index.ts +34 -0
  417. package/src/electron/tailscale/settings.ts +218 -0
  418. package/src/electron/tailscale/tailscale.ts +379 -0
  419. package/src/electron/tray/QuickInputWindow.ts +609 -0
  420. package/src/electron/tray/TrayManager.ts +1005 -0
  421. package/src/electron/tray/index.ts +6 -0
  422. package/src/electron/updater/index.ts +1 -0
  423. package/src/electron/updater/update-manager.ts +447 -0
  424. package/src/electron/utils/env-migration.ts +203 -0
  425. package/src/electron/utils/process.ts +124 -0
  426. package/src/electron/utils/rate-limiter.ts +130 -0
  427. package/src/electron/utils/validation.ts +493 -0
  428. package/src/electron/utils/x-cli.ts +198 -0
  429. package/src/electron/voice/VoiceService.ts +583 -0
  430. package/src/electron/voice/index.ts +9 -0
  431. package/src/electron/voice/voice-settings-manager.ts +403 -0
  432. package/src/renderer/App.tsx +775 -0
  433. package/src/renderer/components/ActivityFeed.tsx +407 -0
  434. package/src/renderer/components/ActivityFeedItem.tsx +285 -0
  435. package/src/renderer/components/AgentRoleCard.tsx +343 -0
  436. package/src/renderer/components/AgentRoleEditor.tsx +805 -0
  437. package/src/renderer/components/AgentSquadSettings.tsx +295 -0
  438. package/src/renderer/components/AgentWorkingStatePanel.tsx +411 -0
  439. package/src/renderer/components/AppearanceSettings.tsx +122 -0
  440. package/src/renderer/components/ApprovalDialog.tsx +100 -0
  441. package/src/renderer/components/BlueBubblesSettings.tsx +505 -0
  442. package/src/renderer/components/BuiltinToolsSettings.tsx +307 -0
  443. package/src/renderer/components/CanvasPreview.tsx +1189 -0
  444. package/src/renderer/components/CommandOutput.tsx +202 -0
  445. package/src/renderer/components/ContextPolicySettings.tsx +523 -0
  446. package/src/renderer/components/ControlPlaneSettings.tsx +1134 -0
  447. package/src/renderer/components/DisclaimerModal.tsx +124 -0
  448. package/src/renderer/components/DiscordSettings.tsx +436 -0
  449. package/src/renderer/components/EmailSettings.tsx +606 -0
  450. package/src/renderer/components/ExtensionsSettings.tsx +542 -0
  451. package/src/renderer/components/FileViewer.tsx +224 -0
  452. package/src/renderer/components/GoogleChatSettings.tsx +535 -0
  453. package/src/renderer/components/GuardrailSettings.tsx +487 -0
  454. package/src/renderer/components/HooksSettings.tsx +581 -0
  455. package/src/renderer/components/ImessageSettings.tsx +484 -0
  456. package/src/renderer/components/LineSettings.tsx +483 -0
  457. package/src/renderer/components/MCPRegistryBrowser.tsx +386 -0
  458. package/src/renderer/components/MCPSettings.tsx +943 -0
  459. package/src/renderer/components/MainContent.tsx +2433 -0
  460. package/src/renderer/components/MatrixSettings.tsx +510 -0
  461. package/src/renderer/components/MattermostSettings.tsx +473 -0
  462. package/src/renderer/components/MemorySettings.tsx +247 -0
  463. package/src/renderer/components/MentionBadge.tsx +87 -0
  464. package/src/renderer/components/MentionInput.tsx +409 -0
  465. package/src/renderer/components/MentionList.tsx +476 -0
  466. package/src/renderer/components/MissionControlPanel.tsx +1995 -0
  467. package/src/renderer/components/NodesSettings.tsx +316 -0
  468. package/src/renderer/components/NotificationPanel.tsx +481 -0
  469. package/src/renderer/components/Onboarding/AwakeningOrb.tsx +44 -0
  470. package/src/renderer/components/Onboarding/Onboarding.tsx +443 -0
  471. package/src/renderer/components/Onboarding/TypewriterText.tsx +102 -0
  472. package/src/renderer/components/Onboarding/index.ts +3 -0
  473. package/src/renderer/components/OnboardingModal.tsx +698 -0
  474. package/src/renderer/components/PairingCodeDisplay.tsx +324 -0
  475. package/src/renderer/components/PersonalitySettings.tsx +597 -0
  476. package/src/renderer/components/QueueSettings.tsx +119 -0
  477. package/src/renderer/components/QuickTaskFAB.tsx +71 -0
  478. package/src/renderer/components/RightPanel.tsx +413 -0
  479. package/src/renderer/components/ScheduledTasksSettings.tsx +1328 -0
  480. package/src/renderer/components/SearchSettings.tsx +328 -0
  481. package/src/renderer/components/Settings.tsx +1504 -0
  482. package/src/renderer/components/Sidebar.tsx +344 -0
  483. package/src/renderer/components/SignalSettings.tsx +673 -0
  484. package/src/renderer/components/SkillHubBrowser.tsx +458 -0
  485. package/src/renderer/components/SkillParameterModal.tsx +185 -0
  486. package/src/renderer/components/SkillsSettings.tsx +451 -0
  487. package/src/renderer/components/SlackSettings.tsx +442 -0
  488. package/src/renderer/components/StandupReportViewer.tsx +614 -0
  489. package/src/renderer/components/TaskBoard.tsx +498 -0
  490. package/src/renderer/components/TaskBoardCard.tsx +357 -0
  491. package/src/renderer/components/TaskBoardColumn.tsx +211 -0
  492. package/src/renderer/components/TaskLabelManager.tsx +472 -0
  493. package/src/renderer/components/TaskQueuePanel.tsx +144 -0
  494. package/src/renderer/components/TaskQuickActions.tsx +492 -0
  495. package/src/renderer/components/TaskTimeline.tsx +216 -0
  496. package/src/renderer/components/TaskView.tsx +162 -0
  497. package/src/renderer/components/TeamsSettings.tsx +518 -0
  498. package/src/renderer/components/TelegramSettings.tsx +421 -0
  499. package/src/renderer/components/Toast.tsx +76 -0
  500. package/src/renderer/components/TraySettings.tsx +189 -0
  501. package/src/renderer/components/TwitchSettings.tsx +511 -0
  502. package/src/renderer/components/UpdateSettings.tsx +295 -0
  503. package/src/renderer/components/VoiceIndicator.tsx +270 -0
  504. package/src/renderer/components/VoiceSettings.tsx +867 -0
  505. package/src/renderer/components/WhatsAppSettings.tsx +721 -0
  506. package/src/renderer/components/WorkingStateEditor.tsx +309 -0
  507. package/src/renderer/components/WorkingStateHistory.tsx +481 -0
  508. package/src/renderer/components/WorkspaceSelector.tsx +150 -0
  509. package/src/renderer/components/XSettings.tsx +311 -0
  510. package/src/renderer/global.d.ts +9 -0
  511. package/src/renderer/hooks/useAgentContext.ts +153 -0
  512. package/src/renderer/hooks/useOnboardingFlow.ts +548 -0
  513. package/src/renderer/hooks/useVoiceInput.ts +268 -0
  514. package/src/renderer/index.html +12 -0
  515. package/src/renderer/main.tsx +10 -0
  516. package/src/renderer/public/cowork-os-logo.png +0 -0
  517. package/src/renderer/quick-input.html +164 -0
  518. package/src/renderer/styles/index.css +14504 -0
  519. package/src/renderer/utils/agentMessages.ts +749 -0
  520. package/src/renderer/utils/voice-directives.ts +169 -0
  521. package/src/shared/channelMessages.ts +213 -0
  522. package/src/shared/types.ts +3608 -0
  523. package/tsconfig.electron.json +26 -0
  524. package/tsconfig.json +26 -0
  525. package/tsconfig.node.json +10 -0
  526. package/vite.config.ts +23 -0
@@ -0,0 +1,1070 @@
1
+ import * as fs from 'fs';
2
+ import * as fsPromises from 'fs/promises';
3
+ import * as path from 'path';
4
+ import {
5
+ Document,
6
+ Packer,
7
+ Paragraph,
8
+ TextRun,
9
+ HeadingLevel,
10
+ AlignmentType,
11
+ Table,
12
+ TableRow,
13
+ TableCell,
14
+ WidthType,
15
+ BorderStyle
16
+ } from 'docx';
17
+ import PDFDocument from 'pdfkit';
18
+ import * as mammoth from 'mammoth';
19
+ import JSZip from 'jszip';
20
+ import { Workspace } from '../../../shared/types';
21
+
22
+ export interface ContentBlock {
23
+ type: string; // 'heading' | 'paragraph' | 'list' | 'table' | 'code'
24
+ text: string;
25
+ level?: number; // For headings: 1-6
26
+ items?: string[]; // For lists
27
+ rows?: string[][]; // For tables
28
+ language?: string; // For code blocks
29
+ }
30
+
31
+ export interface DocumentOptions {
32
+ title?: string;
33
+ author?: string;
34
+ subject?: string;
35
+ /** Font size in points (default: 12) */
36
+ fontSize?: number;
37
+ /** Page margins in inches */
38
+ margins?: {
39
+ top?: number;
40
+ bottom?: number;
41
+ left?: number;
42
+ right?: number;
43
+ };
44
+ }
45
+
46
+ /**
47
+ * Represents a document section identified by a heading
48
+ */
49
+ interface DocumentSection {
50
+ headingLevel: number;
51
+ headingText: string;
52
+ sectionNumber?: string;
53
+ startIndex: number;
54
+ endIndex: number;
55
+ xmlContent: string;
56
+ }
57
+
58
+ /**
59
+ * DocumentBuilder creates Word documents (.docx) and PDFs using docx and pdfkit
60
+ */
61
+ export class DocumentBuilder {
62
+ constructor(private workspace: Workspace) {}
63
+
64
+ async create(
65
+ outputPath: string,
66
+ format: 'docx' | 'pdf' | 'md',
67
+ content: ContentBlock[] | ContentBlock | string | undefined,
68
+ options: DocumentOptions = {}
69
+ ): Promise<void> {
70
+ // Normalize content to always be an array
71
+ const normalizedContent = this.normalizeContent(content);
72
+ const ext = path.extname(outputPath).toLowerCase();
73
+
74
+ // Allow format override via extension
75
+ if (ext === '.md' || format === 'md') {
76
+ await this.createMarkdown(outputPath, normalizedContent);
77
+ return;
78
+ }
79
+
80
+ if (ext === '.pdf' || format === 'pdf') {
81
+ await this.createPDF(outputPath, normalizedContent, options);
82
+ return;
83
+ }
84
+
85
+ // Default to Word document
86
+ await this.createDocx(outputPath, normalizedContent, options);
87
+ }
88
+
89
+ /**
90
+ * Normalizes content input to always be an array of ContentBlocks
91
+ * Throws an error if content is empty or invalid to prevent creating empty documents
92
+ */
93
+ private normalizeContent(content: ContentBlock[] | ContentBlock | string | undefined): ContentBlock[] {
94
+ // Handle undefined/null - FAIL instead of creating empty document
95
+ if (!content) {
96
+ throw new Error(
97
+ 'Document content is required. Please provide content as an array of blocks ' +
98
+ '(e.g., [{ type: "paragraph", text: "Your text here" }]) or as a string.'
99
+ );
100
+ }
101
+
102
+ // Handle string input - convert to a single paragraph
103
+ if (typeof content === 'string') {
104
+ if (content.trim().length === 0) {
105
+ throw new Error('Document content cannot be empty. Please provide text content.');
106
+ }
107
+ return [{ type: 'paragraph', text: content }];
108
+ }
109
+
110
+ // Handle single object (not an array)
111
+ if (!Array.isArray(content)) {
112
+ if (!content.text || content.text.trim().length === 0) {
113
+ throw new Error(
114
+ 'Content block must have non-empty text. ' +
115
+ `Received block with type "${content.type}" but empty or missing text.`
116
+ );
117
+ }
118
+ return [content];
119
+ }
120
+
121
+ // Already an array - ensure it's not empty
122
+ if (content.length === 0) {
123
+ throw new Error(
124
+ 'Document content array cannot be empty. ' +
125
+ 'Please provide at least one content block (e.g., [{ type: "paragraph", text: "Your text" }]).'
126
+ );
127
+ }
128
+
129
+ // Validate each block has content
130
+ const emptyBlocks = content.filter(block => !block.text || block.text.trim().length === 0);
131
+ if (emptyBlocks.length > 0) {
132
+ console.warn(`[DocumentBuilder] Found ${emptyBlocks.length} empty content blocks, filtering them out`);
133
+ const validBlocks = content.filter(block => block.text && block.text.trim().length > 0);
134
+ if (validBlocks.length === 0) {
135
+ throw new Error(
136
+ 'All content blocks have empty text. Please provide content blocks with actual text. ' +
137
+ `Received ${content.length} blocks but all had empty or missing text fields.`
138
+ );
139
+ }
140
+ return validBlocks;
141
+ }
142
+
143
+ return content;
144
+ }
145
+
146
+ /**
147
+ * Creates a Word document (.docx)
148
+ */
149
+ private async createDocx(
150
+ outputPath: string,
151
+ content: ContentBlock[],
152
+ options: DocumentOptions
153
+ ): Promise<void> {
154
+ const children: Paragraph[] = [];
155
+
156
+ for (const block of content) {
157
+ switch (block.type) {
158
+ case 'heading': {
159
+ const level = Math.min(Math.max(block.level || 1, 1), 6);
160
+ const headingLevel = this.getHeadingLevel(level);
161
+ children.push(
162
+ new Paragraph({
163
+ text: block.text,
164
+ heading: headingLevel,
165
+ spacing: { before: 240, after: 120 }
166
+ })
167
+ );
168
+ break;
169
+ }
170
+
171
+ case 'paragraph':
172
+ children.push(
173
+ new Paragraph({
174
+ children: [new TextRun({ text: block.text, size: (options.fontSize || 12) * 2 })],
175
+ spacing: { after: 200 }
176
+ })
177
+ );
178
+ break;
179
+
180
+ case 'list': {
181
+ const items = block.items || block.text.split('\n').filter(line => line.trim());
182
+ for (const item of items) {
183
+ children.push(
184
+ new Paragraph({
185
+ children: [new TextRun({ text: item, size: (options.fontSize || 12) * 2 })],
186
+ bullet: { level: 0 },
187
+ spacing: { after: 100 }
188
+ })
189
+ );
190
+ }
191
+ break;
192
+ }
193
+
194
+ case 'table': {
195
+ if (block.rows && block.rows.length > 0) {
196
+ const table = new Table({
197
+ width: { size: 100, type: WidthType.PERCENTAGE },
198
+ rows: block.rows.map((row, rowIndex) =>
199
+ new TableRow({
200
+ children: row.map(
201
+ cell =>
202
+ new TableCell({
203
+ children: [
204
+ new Paragraph({
205
+ children: [
206
+ new TextRun({
207
+ text: cell,
208
+ bold: rowIndex === 0,
209
+ size: (options.fontSize || 12) * 2
210
+ })
211
+ ]
212
+ })
213
+ ],
214
+ borders: {
215
+ top: { style: BorderStyle.SINGLE, size: 1 },
216
+ bottom: { style: BorderStyle.SINGLE, size: 1 },
217
+ left: { style: BorderStyle.SINGLE, size: 1 },
218
+ right: { style: BorderStyle.SINGLE, size: 1 }
219
+ }
220
+ })
221
+ )
222
+ })
223
+ )
224
+ });
225
+ children.push(new Paragraph({ children: [] })); // Spacing before table
226
+ children.push(table as any);
227
+ children.push(new Paragraph({ children: [] })); // Spacing after table
228
+ }
229
+ break;
230
+ }
231
+
232
+ case 'code':
233
+ children.push(
234
+ new Paragraph({
235
+ children: [
236
+ new TextRun({
237
+ text: block.text,
238
+ font: 'Courier New',
239
+ size: 20, // 10pt
240
+ shading: { fill: 'F0F0F0' }
241
+ })
242
+ ],
243
+ spacing: { before: 200, after: 200 }
244
+ })
245
+ );
246
+ break;
247
+
248
+ default:
249
+ children.push(
250
+ new Paragraph({
251
+ children: [new TextRun({ text: block.text, size: (options.fontSize || 12) * 2 })]
252
+ })
253
+ );
254
+ }
255
+ }
256
+
257
+ const doc = new Document({
258
+ creator: options.author || 'CoWork OS',
259
+ title: options.title,
260
+ subject: options.subject,
261
+ sections: [
262
+ {
263
+ properties: {
264
+ page: {
265
+ margin: {
266
+ top: (options.margins?.top || 1) * 1440, // Convert inches to twips
267
+ bottom: (options.margins?.bottom || 1) * 1440,
268
+ left: (options.margins?.left || 1) * 1440,
269
+ right: (options.margins?.right || 1) * 1440
270
+ }
271
+ }
272
+ },
273
+ children
274
+ }
275
+ ]
276
+ });
277
+
278
+ const buffer = await Packer.toBuffer(doc);
279
+ await fsPromises.writeFile(outputPath, buffer);
280
+ }
281
+
282
+ /**
283
+ * Creates a PDF document
284
+ */
285
+ private async createPDF(
286
+ outputPath: string,
287
+ content: ContentBlock[],
288
+ options: DocumentOptions
289
+ ): Promise<void> {
290
+ return new Promise((resolve, reject) => {
291
+ const doc = new PDFDocument({
292
+ size: 'LETTER',
293
+ margins: {
294
+ top: (options.margins?.top || 1) * 72,
295
+ bottom: (options.margins?.bottom || 1) * 72,
296
+ left: (options.margins?.left || 1) * 72,
297
+ right: (options.margins?.right || 1) * 72
298
+ },
299
+ info: {
300
+ Title: options.title || '',
301
+ Author: options.author || 'CoWork OS',
302
+ Subject: options.subject || ''
303
+ }
304
+ });
305
+
306
+ const stream = fs.createWriteStream(outputPath);
307
+ doc.pipe(stream);
308
+
309
+ const baseFontSize = options.fontSize || 12;
310
+
311
+ for (const block of content) {
312
+ switch (block.type) {
313
+ case 'heading': {
314
+ const level = Math.min(Math.max(block.level || 1, 1), 6);
315
+ const fontSize = baseFontSize + (7 - level) * 2; // h1 = base+12, h6 = base+2
316
+ doc
317
+ .font('Helvetica-Bold')
318
+ .fontSize(fontSize)
319
+ .text(block.text, { paragraphGap: 10 });
320
+ doc.moveDown(0.5);
321
+ break;
322
+ }
323
+
324
+ case 'paragraph':
325
+ doc
326
+ .font('Helvetica')
327
+ .fontSize(baseFontSize)
328
+ .text(block.text, { paragraphGap: 8, lineGap: 4 });
329
+ doc.moveDown(0.5);
330
+ break;
331
+
332
+ case 'list': {
333
+ const items = block.items || block.text.split('\n').filter(line => line.trim());
334
+ doc.font('Helvetica').fontSize(baseFontSize);
335
+ for (const item of items) {
336
+ doc.text(`• ${item}`, { indent: 20, paragraphGap: 4 });
337
+ }
338
+ doc.moveDown(0.5);
339
+ break;
340
+ }
341
+
342
+ case 'table': {
343
+ if (block.rows && block.rows.length > 0) {
344
+ doc.font('Helvetica').fontSize(baseFontSize - 1);
345
+ const columnCount = block.rows[0].length;
346
+ const pageWidth = doc.page.width - doc.page.margins.left - doc.page.margins.right;
347
+ const colWidth = pageWidth / columnCount;
348
+
349
+ for (let rowIndex = 0; rowIndex < block.rows.length; rowIndex++) {
350
+ const row = block.rows[rowIndex];
351
+ const startY = doc.y;
352
+
353
+ // Draw cells
354
+ for (let colIndex = 0; colIndex < row.length; colIndex++) {
355
+ const x = doc.page.margins.left + colIndex * colWidth;
356
+ doc.font(rowIndex === 0 ? 'Helvetica-Bold' : 'Helvetica');
357
+ doc.text(row[colIndex], x, startY, {
358
+ width: colWidth - 10,
359
+ continued: false
360
+ });
361
+ }
362
+
363
+ // Draw horizontal line
364
+ doc
365
+ .moveTo(doc.page.margins.left, doc.y + 5)
366
+ .lineTo(doc.page.margins.left + pageWidth, doc.y + 5)
367
+ .stroke();
368
+
369
+ doc.moveDown(0.3);
370
+ }
371
+ doc.moveDown(0.5);
372
+ }
373
+ break;
374
+ }
375
+
376
+ case 'code':
377
+ doc
378
+ .font('Courier')
379
+ .fontSize(baseFontSize - 2)
380
+ .fillColor('#333333')
381
+ .text(block.text, { paragraphGap: 8 });
382
+ doc.fillColor('#000000');
383
+ doc.moveDown(0.5);
384
+ break;
385
+
386
+ default:
387
+ doc
388
+ .font('Helvetica')
389
+ .fontSize(baseFontSize)
390
+ .text(block.text);
391
+ doc.moveDown(0.5);
392
+ }
393
+ }
394
+
395
+ doc.end();
396
+
397
+ stream.on('finish', resolve);
398
+ stream.on('error', reject);
399
+ });
400
+ }
401
+
402
+ /**
403
+ * Creates a Markdown document (fallback)
404
+ */
405
+ private async createMarkdown(outputPath: string, content: ContentBlock[]): Promise<void> {
406
+ const markdown = content
407
+ .map(block => {
408
+ switch (block.type) {
409
+ case 'heading': {
410
+ const level = Math.min(Math.max(block.level || 1, 1), 6);
411
+ return `${'#'.repeat(level)} ${block.text}\n`;
412
+ }
413
+ case 'paragraph':
414
+ return `${block.text}\n`;
415
+ case 'list': {
416
+ const items = block.items || block.text.split('\n').filter(line => line.trim());
417
+ return items.map(item => `- ${item}`).join('\n') + '\n';
418
+ }
419
+ case 'table': {
420
+ if (!block.rows || block.rows.length === 0) return '';
421
+ const header = block.rows[0];
422
+ const separator = header.map(() => '---').join(' | ');
423
+ const rows = block.rows.map(row => row.join(' | ')).join('\n');
424
+ return `${header.join(' | ')}\n${separator}\n${block.rows.slice(1).map(row => row.join(' | ')).join('\n')}\n`;
425
+ }
426
+ case 'code':
427
+ return `\`\`\`${block.language || ''}\n${block.text}\n\`\`\`\n`;
428
+ default:
429
+ return `${block.text}\n`;
430
+ }
431
+ })
432
+ .join('\n');
433
+
434
+ await fsPromises.writeFile(outputPath, markdown, 'utf-8');
435
+ }
436
+
437
+ private getHeadingLevel(level: number): (typeof HeadingLevel)[keyof typeof HeadingLevel] {
438
+ switch (level) {
439
+ case 1: return HeadingLevel.HEADING_1;
440
+ case 2: return HeadingLevel.HEADING_2;
441
+ case 3: return HeadingLevel.HEADING_3;
442
+ case 4: return HeadingLevel.HEADING_4;
443
+ case 5: return HeadingLevel.HEADING_5;
444
+ case 6: return HeadingLevel.HEADING_6;
445
+ default: return HeadingLevel.HEADING_1;
446
+ }
447
+ }
448
+
449
+ /**
450
+ * Reads an existing DOCX file and extracts its content as HTML
451
+ */
452
+ async readDocument(inputPath: string): Promise<{ html: string; text: string; messages: string[] }> {
453
+ const buffer = await fsPromises.readFile(inputPath);
454
+ const result = await mammoth.convertToHtml({ buffer });
455
+ const textResult = await mammoth.extractRawText({ buffer });
456
+
457
+ return {
458
+ html: result.value,
459
+ text: textResult.value,
460
+ messages: result.messages.map(m => m.message)
461
+ };
462
+ }
463
+
464
+ /**
465
+ * Appends new content sections to an existing DOCX file.
466
+ * This method directly manipulates the DOCX XML structure to preserve
467
+ * the original document formatting while adding new content at the end.
468
+ */
469
+ async appendToDocument(
470
+ inputPath: string,
471
+ outputPath: string,
472
+ newContent: ContentBlock[],
473
+ options: DocumentOptions = {}
474
+ ): Promise<{ success: boolean; sectionsAdded: number }> {
475
+ console.log(`[DocumentBuilder] appendToDocument: ${inputPath} -> ${outputPath}, ${newContent.length} blocks`);
476
+
477
+ // Read the DOCX file as a ZIP
478
+ const docxBuffer = await fsPromises.readFile(inputPath);
479
+ const zip = await JSZip.loadAsync(docxBuffer);
480
+
481
+ // Get the main document.xml
482
+ const documentXml = zip.file('word/document.xml');
483
+ if (!documentXml) {
484
+ throw new Error('Invalid DOCX file: missing word/document.xml');
485
+ }
486
+
487
+ let xmlContent = await documentXml.async('text');
488
+
489
+ // Generate OOXML for the new content
490
+ const newXmlContent = this.contentBlocksToOoxml(newContent);
491
+
492
+ // Find the insertion point - before </w:body> or before <w:sectPr
493
+ // The sectPr element contains section properties and must stay at the end
494
+ const sectPrMatch = xmlContent.match(/<w:sectPr[^>]*>[\s\S]*?<\/w:sectPr>/);
495
+ const bodyEndMatch = xmlContent.match(/<\/w:body>/);
496
+
497
+ if (sectPrMatch && sectPrMatch.index !== undefined) {
498
+ // Insert before sectPr
499
+ xmlContent =
500
+ xmlContent.slice(0, sectPrMatch.index) +
501
+ newXmlContent +
502
+ xmlContent.slice(sectPrMatch.index);
503
+ console.log(`[DocumentBuilder] Inserted content before <w:sectPr>`);
504
+ } else if (bodyEndMatch && bodyEndMatch.index !== undefined) {
505
+ // Insert before </w:body>
506
+ xmlContent =
507
+ xmlContent.slice(0, bodyEndMatch.index) +
508
+ newXmlContent +
509
+ xmlContent.slice(bodyEndMatch.index);
510
+ console.log(`[DocumentBuilder] Inserted content before </w:body>`);
511
+ } else {
512
+ throw new Error('Could not find insertion point in document.xml');
513
+ }
514
+
515
+ // Update the document.xml in the ZIP
516
+ zip.file('word/document.xml', xmlContent);
517
+
518
+ // Write the modified DOCX
519
+ const outputBuffer = await zip.generateAsync({
520
+ type: 'nodebuffer',
521
+ compression: 'DEFLATE',
522
+ compressionOptions: { level: 9 }
523
+ });
524
+ await fsPromises.writeFile(outputPath, outputBuffer);
525
+
526
+ console.log(`[DocumentBuilder] Successfully appended ${newContent.length} sections to ${outputPath}`);
527
+
528
+ return {
529
+ success: true,
530
+ sectionsAdded: newContent.length
531
+ };
532
+ }
533
+
534
+ /**
535
+ * Converts ContentBlocks to OOXML (Office Open XML) format
536
+ * This creates proper Word paragraph/table elements
537
+ */
538
+ private contentBlocksToOoxml(blocks: ContentBlock[]): string {
539
+ const xmlParts: string[] = [];
540
+
541
+ for (const block of blocks) {
542
+ switch (block.type) {
543
+ case 'heading': {
544
+ const level = Math.min(Math.max(block.level || 1, 1), 6);
545
+ // Word heading styles are "Heading1" through "Heading6"
546
+ const styleId = `Heading${level}`;
547
+ xmlParts.push(this.createOoxmlParagraph(block.text, styleId));
548
+ break;
549
+ }
550
+
551
+ case 'paragraph':
552
+ xmlParts.push(this.createOoxmlParagraph(block.text));
553
+ break;
554
+
555
+ case 'list': {
556
+ const items = block.items || block.text.split('\n').filter(line => line.trim());
557
+ for (const item of items) {
558
+ xmlParts.push(this.createOoxmlListItem(item));
559
+ }
560
+ break;
561
+ }
562
+
563
+ case 'table': {
564
+ if (block.rows && block.rows.length > 0) {
565
+ xmlParts.push(this.createOoxmlTable(block.rows));
566
+ }
567
+ break;
568
+ }
569
+
570
+ default:
571
+ xmlParts.push(this.createOoxmlParagraph(block.text));
572
+ }
573
+ }
574
+
575
+ return xmlParts.join('\n');
576
+ }
577
+
578
+ /**
579
+ * Creates an OOXML paragraph element
580
+ */
581
+ private createOoxmlParagraph(text: string, styleId?: string): string {
582
+ const escapedText = this.escapeXml(text);
583
+ const styleXml = styleId ? `<w:pPr><w:pStyle w:val="${styleId}"/></w:pPr>` : '';
584
+ return `<w:p>${styleXml}<w:r><w:t>${escapedText}</w:t></w:r></w:p>`;
585
+ }
586
+
587
+ /**
588
+ * Creates an OOXML list item (bullet point)
589
+ */
590
+ private createOoxmlListItem(text: string): string {
591
+ const escapedText = this.escapeXml(text);
592
+ // Simple bullet using a bullet character - more compatible than numPr
593
+ return `<w:p><w:pPr><w:ind w:left="720"/></w:pPr><w:r><w:t>• ${escapedText}</w:t></w:r></w:p>`;
594
+ }
595
+
596
+ /**
597
+ * Creates an OOXML table element
598
+ */
599
+ private createOoxmlTable(rows: string[][]): string {
600
+ const tableRows = rows.map((row, rowIndex) => {
601
+ const cells = row.map(cellText => {
602
+ const escapedText = this.escapeXml(cellText);
603
+ const boldStyle = rowIndex === 0 ? '<w:rPr><w:b/></w:rPr>' : '';
604
+ return `<w:tc><w:tcPr><w:tcW w:w="0" w:type="auto"/><w:tcBorders><w:top w:val="single" w:sz="4"/><w:left w:val="single" w:sz="4"/><w:bottom w:val="single" w:sz="4"/><w:right w:val="single" w:sz="4"/></w:tcBorders></w:tcPr><w:p><w:r>${boldStyle}<w:t>${escapedText}</w:t></w:r></w:p></w:tc>`;
605
+ }).join('');
606
+ return `<w:tr>${cells}</w:tr>`;
607
+ }).join('');
608
+
609
+ return `<w:tbl><w:tblPr><w:tblW w:w="5000" w:type="pct"/><w:tblBorders><w:top w:val="single" w:sz="4"/><w:left w:val="single" w:sz="4"/><w:bottom w:val="single" w:sz="4"/><w:right w:val="single" w:sz="4"/><w:insideH w:val="single" w:sz="4"/><w:insideV w:val="single" w:sz="4"/></w:tblBorders></w:tblPr>${tableRows}</w:tbl>`;
610
+ }
611
+
612
+ /**
613
+ * Escapes special XML characters
614
+ */
615
+ private escapeXml(text: string): string {
616
+ return text
617
+ .replace(/&/g, '&amp;')
618
+ .replace(/</g, '&lt;')
619
+ .replace(/>/g, '&gt;')
620
+ .replace(/"/g, '&quot;')
621
+ .replace(/'/g, '&apos;');
622
+ }
623
+
624
+ /**
625
+ * Parses the document.xml content and identifies sections based on headings.
626
+ * Sections are delimited by heading paragraphs (Heading1, Heading2, etc.)
627
+ */
628
+ private parseSections(xmlContent: string): DocumentSection[] {
629
+ const sections: DocumentSection[] = [];
630
+
631
+ // Find all paragraphs that are headings (have w:pStyle with Heading1-6)
632
+ // Pattern: <w:p ...>...<w:pStyle w:val="Heading[1-6]"/>...</w:p>
633
+ const paragraphRegex = /<w:p\b[^>]*>[\s\S]*?<\/w:p>/g;
634
+ const headingStyleRegex = /<w:pStyle\s+w:val="Heading([1-6])"\s*\/>/;
635
+ const textRegex = /<w:t[^>]*>([^<]*)<\/w:t>/g;
636
+
637
+ let match;
638
+ const headingPositions: Array<{
639
+ level: number;
640
+ text: string;
641
+ sectionNumber?: string;
642
+ startIndex: number;
643
+ endIndex: number;
644
+ }> = [];
645
+
646
+ // Find all heading paragraphs
647
+ while ((match = paragraphRegex.exec(xmlContent)) !== null) {
648
+ const paragraph = match[0];
649
+ const styleMatch = paragraph.match(headingStyleRegex);
650
+
651
+ if (styleMatch) {
652
+ const level = parseInt(styleMatch[1], 10);
653
+
654
+ // Extract text from the paragraph
655
+ let text = '';
656
+ let textMatch;
657
+ const textRegexLocal = /<w:t[^>]*>([^<]*)<\/w:t>/g;
658
+ while ((textMatch = textRegexLocal.exec(paragraph)) !== null) {
659
+ text += textMatch[1];
660
+ }
661
+
662
+ // Try to extract section number (e.g., "8. " or "8 ")
663
+ const sectionNumMatch = text.match(/^(\d+(?:\.\d+)*)[.\s]/);
664
+ const sectionNumber = sectionNumMatch ? sectionNumMatch[1] : undefined;
665
+
666
+ headingPositions.push({
667
+ level,
668
+ text: text.trim(),
669
+ sectionNumber,
670
+ startIndex: match.index,
671
+ endIndex: match.index + paragraph.length
672
+ });
673
+ }
674
+ }
675
+
676
+ // Now create sections from heading positions
677
+ // Each section spans from its heading to the next same-level or higher-level heading
678
+ for (let i = 0; i < headingPositions.length; i++) {
679
+ const current = headingPositions[i];
680
+ let endIndex: number;
681
+
682
+ // Find the end of this section
683
+ // It ends at the next heading of same or higher level (lower number)
684
+ // Or at the sectPr element, or end of body
685
+ let nextSectionStart: number | undefined;
686
+
687
+ for (let j = i + 1; j < headingPositions.length; j++) {
688
+ if (headingPositions[j].level <= current.level) {
689
+ nextSectionStart = headingPositions[j].startIndex;
690
+ break;
691
+ }
692
+ }
693
+
694
+ if (nextSectionStart !== undefined) {
695
+ endIndex = nextSectionStart;
696
+ } else {
697
+ // This is the last section at this level
698
+ // End at sectPr or end of body
699
+ const sectPrMatch = xmlContent.match(/<w:sectPr[^>]*>/);
700
+ const bodyEndMatch = xmlContent.match(/<\/w:body>/);
701
+
702
+ if (sectPrMatch && sectPrMatch.index !== undefined) {
703
+ endIndex = sectPrMatch.index;
704
+ } else if (bodyEndMatch && bodyEndMatch.index !== undefined) {
705
+ endIndex = bodyEndMatch.index;
706
+ } else {
707
+ endIndex = xmlContent.length;
708
+ }
709
+ }
710
+
711
+ sections.push({
712
+ headingLevel: current.level,
713
+ headingText: current.text,
714
+ sectionNumber: current.sectionNumber,
715
+ startIndex: current.startIndex,
716
+ endIndex,
717
+ xmlContent: xmlContent.slice(current.startIndex, endIndex)
718
+ });
719
+ }
720
+
721
+ return sections;
722
+ }
723
+
724
+ /**
725
+ * Moves a section to a new position in the document.
726
+ * @param inputPath Path to the source DOCX file
727
+ * @param outputPath Path to save the modified DOCX file
728
+ * @param sectionIdentifier The section to move (can be section number like "8" or heading text)
729
+ * @param afterSection The section after which to place it (section number or heading text)
730
+ */
731
+ async moveSectionAfter(
732
+ inputPath: string,
733
+ outputPath: string,
734
+ sectionIdentifier: string,
735
+ afterSection: string
736
+ ): Promise<{ success: boolean; message: string }> {
737
+ console.log(`[DocumentBuilder] moveSectionAfter: Moving "${sectionIdentifier}" after "${afterSection}"`);
738
+
739
+ // Read the DOCX file
740
+ const docxBuffer = await fsPromises.readFile(inputPath);
741
+ const zip = await JSZip.loadAsync(docxBuffer);
742
+
743
+ const documentXml = zip.file('word/document.xml');
744
+ if (!documentXml) {
745
+ throw new Error('Invalid DOCX file: missing word/document.xml');
746
+ }
747
+
748
+ let xmlContent = await documentXml.async('text');
749
+
750
+ // Parse sections
751
+ const sections = this.parseSections(xmlContent);
752
+ console.log(`[DocumentBuilder] Found ${sections.length} sections:`,
753
+ sections.map(s => `${s.sectionNumber || 'N/A'}: ${s.headingText.substring(0, 50)}`));
754
+
755
+ // Find the section to move
756
+ const sectionToMove = this.findSection(sections, sectionIdentifier);
757
+ if (!sectionToMove) {
758
+ return {
759
+ success: false,
760
+ message: `Could not find section "${sectionIdentifier}". Available sections: ${sections.map(s => s.sectionNumber || s.headingText).join(', ')}`
761
+ };
762
+ }
763
+
764
+ // Find the target section (after which to insert)
765
+ const targetSection = this.findSection(sections, afterSection);
766
+ if (!targetSection) {
767
+ return {
768
+ success: false,
769
+ message: `Could not find target section "${afterSection}". Available sections: ${sections.map(s => s.sectionNumber || s.headingText).join(', ')}`
770
+ };
771
+ }
772
+
773
+ // Check if move is needed
774
+ if (sectionToMove.startIndex === targetSection.endIndex) {
775
+ return { success: true, message: 'Section is already in the correct position' };
776
+ }
777
+
778
+ // Perform the move
779
+ const sectionContent = sectionToMove.xmlContent;
780
+
781
+ // Remove the section from its current position
782
+ let newXmlContent: string;
783
+
784
+ if (sectionToMove.startIndex > targetSection.endIndex) {
785
+ // Section is after target - remove it first, then insert
786
+ newXmlContent =
787
+ xmlContent.slice(0, sectionToMove.startIndex) +
788
+ xmlContent.slice(sectionToMove.endIndex);
789
+
790
+ // Insert at target position (unchanged since it's before the removed section)
791
+ newXmlContent =
792
+ newXmlContent.slice(0, targetSection.endIndex) +
793
+ sectionContent +
794
+ newXmlContent.slice(targetSection.endIndex);
795
+ } else {
796
+ // Section is before target - need to adjust indices
797
+ // First, calculate where target ends after section removal
798
+ const sectionLength = sectionToMove.endIndex - sectionToMove.startIndex;
799
+ const adjustedTargetEnd = targetSection.endIndex - sectionLength;
800
+
801
+ // Remove section first
802
+ newXmlContent =
803
+ xmlContent.slice(0, sectionToMove.startIndex) +
804
+ xmlContent.slice(sectionToMove.endIndex);
805
+
806
+ // Insert at adjusted target position
807
+ newXmlContent =
808
+ newXmlContent.slice(0, adjustedTargetEnd) +
809
+ sectionContent +
810
+ newXmlContent.slice(adjustedTargetEnd);
811
+ }
812
+
813
+ // Update the document.xml in the ZIP
814
+ zip.file('word/document.xml', newXmlContent);
815
+
816
+ // Write the modified DOCX
817
+ const outputBuffer = await zip.generateAsync({
818
+ type: 'nodebuffer',
819
+ compression: 'DEFLATE',
820
+ compressionOptions: { level: 9 }
821
+ });
822
+ await fsPromises.writeFile(outputPath, outputBuffer);
823
+
824
+ console.log(`[DocumentBuilder] Successfully moved section "${sectionIdentifier}" after "${afterSection}"`);
825
+
826
+ return {
827
+ success: true,
828
+ message: `Moved section "${sectionToMove.headingText}" after "${targetSection.headingText}"`
829
+ };
830
+ }
831
+
832
+ /**
833
+ * Finds a section by its number or heading text
834
+ */
835
+ private findSection(sections: DocumentSection[], identifier: string): DocumentSection | undefined {
836
+ const normalizedId = identifier.trim().toLowerCase();
837
+
838
+ // First try exact section number match
839
+ const byNumber = sections.find(s =>
840
+ s.sectionNumber === identifier ||
841
+ s.sectionNumber === normalizedId
842
+ );
843
+ if (byNumber) return byNumber;
844
+
845
+ // Try with "Section " prefix
846
+ const withPrefix = sections.find(s =>
847
+ s.headingText.toLowerCase().startsWith(`section ${normalizedId}`) ||
848
+ s.headingText.toLowerCase().startsWith(`${normalizedId}.`) ||
849
+ s.headingText.toLowerCase().startsWith(`${normalizedId} `)
850
+ );
851
+ if (withPrefix) return withPrefix;
852
+
853
+ // Try partial heading text match
854
+ const byText = sections.find(s =>
855
+ s.headingText.toLowerCase().includes(normalizedId)
856
+ );
857
+ if (byText) return byText;
858
+
859
+ return undefined;
860
+ }
861
+
862
+ /**
863
+ * Inserts new content after a specific section in the document.
864
+ * @param inputPath Path to the source DOCX file
865
+ * @param outputPath Path to save the modified DOCX file
866
+ * @param afterSection Section identifier (number or heading text) after which to insert
867
+ * @param newContent Content blocks to insert
868
+ */
869
+ async insertAfterSection(
870
+ inputPath: string,
871
+ outputPath: string,
872
+ afterSection: string,
873
+ newContent: ContentBlock[]
874
+ ): Promise<{ success: boolean; message: string; sectionsAdded: number }> {
875
+ console.log(`[DocumentBuilder] insertAfterSection: After "${afterSection}", inserting ${newContent.length} blocks`);
876
+
877
+ // Read the DOCX file
878
+ const docxBuffer = await fsPromises.readFile(inputPath);
879
+ const zip = await JSZip.loadAsync(docxBuffer);
880
+
881
+ const documentXml = zip.file('word/document.xml');
882
+ if (!documentXml) {
883
+ throw new Error('Invalid DOCX file: missing word/document.xml');
884
+ }
885
+
886
+ let xmlContent = await documentXml.async('text');
887
+
888
+ // Parse sections
889
+ const sections = this.parseSections(xmlContent);
890
+
891
+ // Find the target section
892
+ const targetSection = this.findSection(sections, afterSection);
893
+ if (!targetSection) {
894
+ return {
895
+ success: false,
896
+ message: `Could not find section "${afterSection}". Available sections: ${sections.map(s => s.sectionNumber || s.headingText).join(', ')}`,
897
+ sectionsAdded: 0
898
+ };
899
+ }
900
+
901
+ // Generate OOXML for the new content
902
+ const newXmlContent = this.contentBlocksToOoxml(newContent);
903
+
904
+ // Insert after the target section
905
+ const insertionPoint = targetSection.endIndex;
906
+ xmlContent =
907
+ xmlContent.slice(0, insertionPoint) +
908
+ newXmlContent +
909
+ xmlContent.slice(insertionPoint);
910
+
911
+ // Update the document.xml in the ZIP
912
+ zip.file('word/document.xml', xmlContent);
913
+
914
+ // Write the modified DOCX
915
+ const outputBuffer = await zip.generateAsync({
916
+ type: 'nodebuffer',
917
+ compression: 'DEFLATE',
918
+ compressionOptions: { level: 9 }
919
+ });
920
+ await fsPromises.writeFile(outputPath, outputBuffer);
921
+
922
+ console.log(`[DocumentBuilder] Successfully inserted ${newContent.length} blocks after section "${afterSection}"`);
923
+
924
+ return {
925
+ success: true,
926
+ message: `Inserted ${newContent.length} content blocks after "${targetSection.headingText}"`,
927
+ sectionsAdded: newContent.length
928
+ };
929
+ }
930
+
931
+ /**
932
+ * Lists all sections in a document
933
+ */
934
+ async listSections(inputPath: string): Promise<Array<{
935
+ number?: string;
936
+ title: string;
937
+ level: number;
938
+ }>> {
939
+ const docxBuffer = await fsPromises.readFile(inputPath);
940
+ const zip = await JSZip.loadAsync(docxBuffer);
941
+
942
+ const documentXml = zip.file('word/document.xml');
943
+ if (!documentXml) {
944
+ throw new Error('Invalid DOCX file: missing word/document.xml');
945
+ }
946
+
947
+ const xmlContent = await documentXml.async('text');
948
+ const sections = this.parseSections(xmlContent);
949
+
950
+ return sections.map(s => ({
951
+ number: s.sectionNumber,
952
+ title: s.headingText,
953
+ level: s.headingLevel
954
+ }));
955
+ }
956
+
957
+ /**
958
+ * Converts HTML from mammoth to ContentBlocks
959
+ * This is a simplified conversion that preserves basic structure
960
+ */
961
+ private htmlToContentBlocks(html: string): ContentBlock[] {
962
+ const blocks: ContentBlock[] = [];
963
+
964
+ // Simple regex-based HTML parsing for common elements
965
+ // Match headings
966
+ const headingRegex = /<h([1-6])[^>]*>([\s\S]*?)<\/h\1>/gi;
967
+ // Match paragraphs
968
+ const paragraphRegex = /<p[^>]*>([\s\S]*?)<\/p>/gi;
969
+ // Match list items
970
+ const listRegex = /<ul[^>]*>([\s\S]*?)<\/ul>/gi;
971
+ const listItemRegex = /<li[^>]*>([\s\S]*?)<\/li>/gi;
972
+ // Match tables
973
+ const tableRegex = /<table[^>]*>([\s\S]*?)<\/table>/gi;
974
+ const trRegex = /<tr[^>]*>([\s\S]*?)<\/tr>/gi;
975
+ const tdThRegex = /<t[dh][^>]*>([\s\S]*?)<\/t[dh]>/gi;
976
+
977
+ // Helper to strip HTML tags
978
+ const stripTags = (str: string): string => str.replace(/<[^>]*>/g, '').trim();
979
+
980
+ // Process in order of appearance
981
+ let lastIndex = 0;
982
+ const processedRanges: Array<{start: number; end: number}> = [];
983
+
984
+ // Find all headings
985
+ let match;
986
+ while ((match = headingRegex.exec(html)) !== null) {
987
+ const text = stripTags(match[2]);
988
+ if (text) {
989
+ blocks.push({
990
+ type: 'heading',
991
+ text,
992
+ level: parseInt(match[1], 10)
993
+ });
994
+ processedRanges.push({ start: match.index, end: match.index + match[0].length });
995
+ }
996
+ }
997
+
998
+ // Find all paragraphs
999
+ paragraphRegex.lastIndex = 0;
1000
+ while ((match = paragraphRegex.exec(html)) !== null) {
1001
+ // Skip if this range overlaps with an already processed element
1002
+ const overlaps = processedRanges.some(r =>
1003
+ (match!.index >= r.start && match!.index < r.end) ||
1004
+ (match!.index + match![0].length > r.start && match!.index + match![0].length <= r.end)
1005
+ );
1006
+ if (overlaps) continue;
1007
+
1008
+ const text = stripTags(match[1]);
1009
+ if (text) {
1010
+ blocks.push({
1011
+ type: 'paragraph',
1012
+ text
1013
+ });
1014
+ processedRanges.push({ start: match.index, end: match.index + match[0].length });
1015
+ }
1016
+ }
1017
+
1018
+ // Find all lists
1019
+ listRegex.lastIndex = 0;
1020
+ while ((match = listRegex.exec(html)) !== null) {
1021
+ const listHtml = match[1];
1022
+ const items: string[] = [];
1023
+ let itemMatch;
1024
+ const itemRegex = /<li[^>]*>([\s\S]*?)<\/li>/gi;
1025
+ while ((itemMatch = itemRegex.exec(listHtml)) !== null) {
1026
+ const itemText = stripTags(itemMatch[1]);
1027
+ if (itemText) items.push(itemText);
1028
+ }
1029
+ if (items.length > 0) {
1030
+ blocks.push({
1031
+ type: 'list',
1032
+ text: items.join('\n'),
1033
+ items
1034
+ });
1035
+ }
1036
+ }
1037
+
1038
+ // Find all tables
1039
+ tableRegex.lastIndex = 0;
1040
+ while ((match = tableRegex.exec(html)) !== null) {
1041
+ const tableHtml = match[1];
1042
+ const rows: string[][] = [];
1043
+ let rowMatch;
1044
+ const rowRegex = /<tr[^>]*>([\s\S]*?)<\/tr>/gi;
1045
+ while ((rowMatch = rowRegex.exec(tableHtml)) !== null) {
1046
+ const rowHtml = rowMatch[1];
1047
+ const cells: string[] = [];
1048
+ let cellMatch;
1049
+ const cellRegex = /<t[dh][^>]*>([\s\S]*?)<\/t[dh]>/gi;
1050
+ while ((cellMatch = cellRegex.exec(rowHtml)) !== null) {
1051
+ cells.push(stripTags(cellMatch[1]));
1052
+ }
1053
+ if (cells.length > 0) rows.push(cells);
1054
+ }
1055
+ if (rows.length > 0) {
1056
+ blocks.push({
1057
+ type: 'table',
1058
+ text: '',
1059
+ rows
1060
+ });
1061
+ }
1062
+ }
1063
+
1064
+ // Sort blocks by their original position would require more complex tracking
1065
+ // For now, we return them in the order found (headings, then paragraphs, then lists, then tables)
1066
+ // This may not preserve exact document order
1067
+
1068
+ return blocks;
1069
+ }
1070
+ }