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.
- package/LICENSE +21 -0
- package/README.md +1638 -0
- package/bin/cowork.js +42 -0
- package/build/entitlements.mac.plist +16 -0
- package/build/icon.icns +0 -0
- package/build/icon.png +0 -0
- package/dist/electron/electron/activity/ActivityRepository.js +190 -0
- package/dist/electron/electron/agent/browser/browser-service.js +639 -0
- package/dist/electron/electron/agent/context-manager.js +225 -0
- package/dist/electron/electron/agent/custom-skill-loader.js +566 -0
- package/dist/electron/electron/agent/daemon.js +975 -0
- package/dist/electron/electron/agent/executor.js +3561 -0
- package/dist/electron/electron/agent/llm/anthropic-provider.js +155 -0
- package/dist/electron/electron/agent/llm/bedrock-provider.js +202 -0
- package/dist/electron/electron/agent/llm/gemini-provider.js +375 -0
- package/dist/electron/electron/agent/llm/index.js +34 -0
- package/dist/electron/electron/agent/llm/ollama-provider.js +263 -0
- package/dist/electron/electron/agent/llm/openai-oauth.js +101 -0
- package/dist/electron/electron/agent/llm/openai-provider.js +657 -0
- package/dist/electron/electron/agent/llm/openrouter-provider.js +232 -0
- package/dist/electron/electron/agent/llm/pricing.js +160 -0
- package/dist/electron/electron/agent/llm/provider-factory.js +880 -0
- package/dist/electron/electron/agent/llm/types.js +178 -0
- package/dist/electron/electron/agent/queue-manager.js +378 -0
- package/dist/electron/electron/agent/sandbox/docker-sandbox.js +402 -0
- package/dist/electron/electron/agent/sandbox/macos-sandbox.js +407 -0
- package/dist/electron/electron/agent/sandbox/runner.js +410 -0
- package/dist/electron/electron/agent/sandbox/sandbox-factory.js +228 -0
- package/dist/electron/electron/agent/sandbox/security-utils.js +258 -0
- package/dist/electron/electron/agent/search/brave-provider.js +119 -0
- package/dist/electron/electron/agent/search/google-provider.js +100 -0
- package/dist/electron/electron/agent/search/index.js +28 -0
- package/dist/electron/electron/agent/search/provider-factory.js +395 -0
- package/dist/electron/electron/agent/search/serpapi-provider.js +112 -0
- package/dist/electron/electron/agent/search/tavily-provider.js +90 -0
- package/dist/electron/electron/agent/search/types.js +40 -0
- package/dist/electron/electron/agent/security/index.js +12 -0
- package/dist/electron/electron/agent/security/input-sanitizer.js +303 -0
- package/dist/electron/electron/agent/security/output-filter.js +217 -0
- package/dist/electron/electron/agent/skill-eligibility.js +281 -0
- package/dist/electron/electron/agent/skill-registry.js +396 -0
- package/dist/electron/electron/agent/skills/document.js +878 -0
- package/dist/electron/electron/agent/skills/image-generator.js +225 -0
- package/dist/electron/electron/agent/skills/organizer.js +141 -0
- package/dist/electron/electron/agent/skills/presentation.js +367 -0
- package/dist/electron/electron/agent/skills/spreadsheet.js +165 -0
- package/dist/electron/electron/agent/tools/browser-tools.js +523 -0
- package/dist/electron/electron/agent/tools/builtin-settings.js +384 -0
- package/dist/electron/electron/agent/tools/canvas-tools.js +530 -0
- package/dist/electron/electron/agent/tools/cron-tools.js +577 -0
- package/dist/electron/electron/agent/tools/edit-tools.js +194 -0
- package/dist/electron/electron/agent/tools/file-tools.js +719 -0
- package/dist/electron/electron/agent/tools/glob-tools.js +283 -0
- package/dist/electron/electron/agent/tools/grep-tools.js +387 -0
- package/dist/electron/electron/agent/tools/image-tools.js +111 -0
- package/dist/electron/electron/agent/tools/mention-tools.js +282 -0
- package/dist/electron/electron/agent/tools/node-tools.js +476 -0
- package/dist/electron/electron/agent/tools/registry.js +2719 -0
- package/dist/electron/electron/agent/tools/search-tools.js +91 -0
- package/dist/electron/electron/agent/tools/shell-tools.js +574 -0
- package/dist/electron/electron/agent/tools/skill-tools.js +274 -0
- package/dist/electron/electron/agent/tools/system-tools.js +578 -0
- package/dist/electron/electron/agent/tools/web-fetch-tools.js +444 -0
- package/dist/electron/electron/agent/tools/x-tools.js +264 -0
- package/dist/electron/electron/agents/AgentRoleRepository.js +420 -0
- package/dist/electron/electron/agents/HeartbeatService.js +356 -0
- package/dist/electron/electron/agents/MentionRepository.js +197 -0
- package/dist/electron/electron/agents/TaskSubscriptionRepository.js +168 -0
- package/dist/electron/electron/agents/WorkingStateRepository.js +229 -0
- package/dist/electron/electron/canvas/canvas-manager.js +714 -0
- package/dist/electron/electron/canvas/canvas-preload.js +53 -0
- package/dist/electron/electron/canvas/canvas-protocol.js +195 -0
- package/dist/electron/electron/canvas/canvas-store.js +174 -0
- package/dist/electron/electron/canvas/index.js +13 -0
- package/dist/electron/electron/control-plane/client.js +364 -0
- package/dist/electron/electron/control-plane/handlers.js +572 -0
- package/dist/electron/electron/control-plane/index.js +41 -0
- package/dist/electron/electron/control-plane/node-manager.js +264 -0
- package/dist/electron/electron/control-plane/protocol.js +194 -0
- package/dist/electron/electron/control-plane/remote-client.js +437 -0
- package/dist/electron/electron/control-plane/server.js +640 -0
- package/dist/electron/electron/control-plane/settings.js +369 -0
- package/dist/electron/electron/control-plane/ssh-tunnel.js +549 -0
- package/dist/electron/electron/cron/index.js +30 -0
- package/dist/electron/electron/cron/schedule.js +190 -0
- package/dist/electron/electron/cron/service.js +614 -0
- package/dist/electron/electron/cron/store.js +155 -0
- package/dist/electron/electron/cron/types.js +82 -0
- package/dist/electron/electron/cron/webhook.js +258 -0
- package/dist/electron/electron/database/SecureSettingsRepository.js +444 -0
- package/dist/electron/electron/database/TaskLabelRepository.js +120 -0
- package/dist/electron/electron/database/repositories.js +1781 -0
- package/dist/electron/electron/database/schema.js +978 -0
- package/dist/electron/electron/extensions/index.js +33 -0
- package/dist/electron/electron/extensions/loader.js +313 -0
- package/dist/electron/electron/extensions/registry.js +485 -0
- package/dist/electron/electron/extensions/types.js +11 -0
- package/dist/electron/electron/gateway/channel-registry.js +1102 -0
- package/dist/electron/electron/gateway/channels/bluebubbles-client.js +479 -0
- package/dist/electron/electron/gateway/channels/bluebubbles.js +432 -0
- package/dist/electron/electron/gateway/channels/discord.js +975 -0
- package/dist/electron/electron/gateway/channels/email-client.js +593 -0
- package/dist/electron/electron/gateway/channels/email.js +443 -0
- package/dist/electron/electron/gateway/channels/google-chat.js +631 -0
- package/dist/electron/electron/gateway/channels/imessage-client.js +363 -0
- package/dist/electron/electron/gateway/channels/imessage.js +465 -0
- package/dist/electron/electron/gateway/channels/index.js +36 -0
- package/dist/electron/electron/gateway/channels/line-client.js +470 -0
- package/dist/electron/electron/gateway/channels/line.js +479 -0
- package/dist/electron/electron/gateway/channels/matrix-client.js +432 -0
- package/dist/electron/electron/gateway/channels/matrix.js +592 -0
- package/dist/electron/electron/gateway/channels/mattermost-client.js +394 -0
- package/dist/electron/electron/gateway/channels/mattermost.js +496 -0
- package/dist/electron/electron/gateway/channels/signal-client.js +500 -0
- package/dist/electron/electron/gateway/channels/signal.js +582 -0
- package/dist/electron/electron/gateway/channels/slack.js +415 -0
- package/dist/electron/electron/gateway/channels/teams.js +596 -0
- package/dist/electron/electron/gateway/channels/telegram.js +1390 -0
- package/dist/electron/electron/gateway/channels/twitch-client.js +502 -0
- package/dist/electron/electron/gateway/channels/twitch.js +396 -0
- package/dist/electron/electron/gateway/channels/types.js +8 -0
- package/dist/electron/electron/gateway/channels/whatsapp.js +953 -0
- package/dist/electron/electron/gateway/context-policy.js +268 -0
- package/dist/electron/electron/gateway/index.js +1063 -0
- package/dist/electron/electron/gateway/infrastructure.js +496 -0
- package/dist/electron/electron/gateway/router.js +2700 -0
- package/dist/electron/electron/gateway/security.js +375 -0
- package/dist/electron/electron/gateway/session.js +115 -0
- package/dist/electron/electron/gateway/tunnel.js +503 -0
- package/dist/electron/electron/guardrails/guardrail-manager.js +348 -0
- package/dist/electron/electron/hooks/gmail-watcher.js +300 -0
- package/dist/electron/electron/hooks/index.js +46 -0
- package/dist/electron/electron/hooks/mappings.js +381 -0
- package/dist/electron/electron/hooks/server.js +480 -0
- package/dist/electron/electron/hooks/settings.js +447 -0
- package/dist/electron/electron/hooks/types.js +41 -0
- package/dist/electron/electron/ipc/canvas-handlers.js +158 -0
- package/dist/electron/electron/ipc/handlers.js +3138 -0
- package/dist/electron/electron/ipc/mission-control-handlers.js +141 -0
- package/dist/electron/electron/main.js +448 -0
- package/dist/electron/electron/mcp/client/MCPClientManager.js +330 -0
- package/dist/electron/electron/mcp/client/MCPServerConnection.js +437 -0
- package/dist/electron/electron/mcp/client/transports/SSETransport.js +304 -0
- package/dist/electron/electron/mcp/client/transports/StdioTransport.js +307 -0
- package/dist/electron/electron/mcp/client/transports/WebSocketTransport.js +329 -0
- package/dist/electron/electron/mcp/host/MCPHostServer.js +354 -0
- package/dist/electron/electron/mcp/host/ToolAdapter.js +100 -0
- package/dist/electron/electron/mcp/registry/MCPRegistryManager.js +497 -0
- package/dist/electron/electron/mcp/settings.js +446 -0
- package/dist/electron/electron/mcp/types.js +59 -0
- package/dist/electron/electron/memory/MemoryService.js +435 -0
- package/dist/electron/electron/notifications/index.js +17 -0
- package/dist/electron/electron/notifications/service.js +118 -0
- package/dist/electron/electron/notifications/store.js +144 -0
- package/dist/electron/electron/preload.js +842 -0
- package/dist/electron/electron/reports/StandupReportService.js +272 -0
- package/dist/electron/electron/security/concurrency.js +293 -0
- package/dist/electron/electron/security/index.js +15 -0
- package/dist/electron/electron/security/policy-manager.js +435 -0
- package/dist/electron/electron/settings/appearance-manager.js +193 -0
- package/dist/electron/electron/settings/personality-manager.js +724 -0
- package/dist/electron/electron/settings/x-manager.js +58 -0
- package/dist/electron/electron/tailscale/exposure.js +188 -0
- package/dist/electron/electron/tailscale/index.js +28 -0
- package/dist/electron/electron/tailscale/settings.js +205 -0
- package/dist/electron/electron/tailscale/tailscale.js +355 -0
- package/dist/electron/electron/tray/QuickInputWindow.js +568 -0
- package/dist/electron/electron/tray/TrayManager.js +895 -0
- package/dist/electron/electron/tray/index.js +9 -0
- package/dist/electron/electron/updater/index.js +6 -0
- package/dist/electron/electron/updater/update-manager.js +418 -0
- package/dist/electron/electron/utils/env-migration.js +209 -0
- package/dist/electron/electron/utils/process.js +102 -0
- package/dist/electron/electron/utils/rate-limiter.js +104 -0
- package/dist/electron/electron/utils/validation.js +419 -0
- package/dist/electron/electron/utils/x-cli.js +177 -0
- package/dist/electron/electron/voice/VoiceService.js +507 -0
- package/dist/electron/electron/voice/index.js +14 -0
- package/dist/electron/electron/voice/voice-settings-manager.js +359 -0
- package/dist/electron/shared/channelMessages.js +170 -0
- package/dist/electron/shared/types.js +1185 -0
- package/package.json +159 -0
- package/resources/skills/1password.json +10 -0
- package/resources/skills/add-documentation.json +31 -0
- package/resources/skills/analyze-csv.json +17 -0
- package/resources/skills/apple-notes.json +10 -0
- package/resources/skills/apple-reminders.json +10 -0
- package/resources/skills/auto-commenter.json +10 -0
- package/resources/skills/bear-notes.json +10 -0
- package/resources/skills/bird.json +35 -0
- package/resources/skills/blogwatcher.json +10 -0
- package/resources/skills/blucli.json +10 -0
- package/resources/skills/bluebubbles.json +10 -0
- package/resources/skills/camsnap.json +10 -0
- package/resources/skills/clean-imports.json +18 -0
- package/resources/skills/code-review.json +18 -0
- package/resources/skills/coding-agent.json +10 -0
- package/resources/skills/compare-files.json +23 -0
- package/resources/skills/convert-code.json +34 -0
- package/resources/skills/create-changelog.json +24 -0
- package/resources/skills/debug-error.json +17 -0
- package/resources/skills/dependency-check.json +10 -0
- package/resources/skills/discord.json +10 -0
- package/resources/skills/eightctl.json +10 -0
- package/resources/skills/explain-code.json +29 -0
- package/resources/skills/extract-todos.json +18 -0
- package/resources/skills/food-order.json +10 -0
- package/resources/skills/gemini.json +10 -0
- package/resources/skills/generate-readme.json +10 -0
- package/resources/skills/gifgrep.json +10 -0
- package/resources/skills/git-commit.json +10 -0
- package/resources/skills/github.json +10 -0
- package/resources/skills/gog.json +10 -0
- package/resources/skills/goplaces.json +10 -0
- package/resources/skills/himalaya.json +10 -0
- package/resources/skills/imsg.json +10 -0
- package/resources/skills/karpathy-guidelines.json +12 -0
- package/resources/skills/last30days.json +26 -0
- package/resources/skills/local-places.json +10 -0
- package/resources/skills/mcporter.json +10 -0
- package/resources/skills/model-usage.json +10 -0
- package/resources/skills/nano-banana-pro.json +10 -0
- package/resources/skills/nano-pdf.json +10 -0
- package/resources/skills/notion.json +10 -0
- package/resources/skills/obsidian.json +10 -0
- package/resources/skills/openai-image-gen.json +10 -0
- package/resources/skills/openai-whisper-api.json +10 -0
- package/resources/skills/openai-whisper.json +10 -0
- package/resources/skills/openhue.json +10 -0
- package/resources/skills/oracle.json +10 -0
- package/resources/skills/ordercli.json +10 -0
- package/resources/skills/peekaboo.json +10 -0
- package/resources/skills/project-structure.json +10 -0
- package/resources/skills/proofread.json +17 -0
- package/resources/skills/refactor-code.json +31 -0
- package/resources/skills/rename-symbol.json +23 -0
- package/resources/skills/sag.json +10 -0
- package/resources/skills/security-audit.json +18 -0
- package/resources/skills/session-logs.json +10 -0
- package/resources/skills/sherpa-onnx-tts.json +10 -0
- package/resources/skills/skill-creator.json +15 -0
- package/resources/skills/skill-hub.json +29 -0
- package/resources/skills/slack.json +10 -0
- package/resources/skills/songsee.json +10 -0
- package/resources/skills/sonoscli.json +10 -0
- package/resources/skills/spotify-player.json +10 -0
- package/resources/skills/startup-cfo.json +55 -0
- package/resources/skills/summarize-folder.json +18 -0
- package/resources/skills/summarize.json +10 -0
- package/resources/skills/things-mac.json +10 -0
- package/resources/skills/tmux.json +10 -0
- package/resources/skills/translate.json +36 -0
- package/resources/skills/trello.json +10 -0
- package/resources/skills/video-frames.json +10 -0
- package/resources/skills/voice-call.json +10 -0
- package/resources/skills/wacli.json +10 -0
- package/resources/skills/weather.json +10 -0
- package/resources/skills/write-tests.json +31 -0
- package/src/electron/activity/ActivityRepository.ts +238 -0
- package/src/electron/agent/browser/browser-service.ts +721 -0
- package/src/electron/agent/context-manager.ts +257 -0
- package/src/electron/agent/custom-skill-loader.ts +634 -0
- package/src/electron/agent/daemon.ts +1097 -0
- package/src/electron/agent/executor.ts +4017 -0
- package/src/electron/agent/llm/anthropic-provider.ts +175 -0
- package/src/electron/agent/llm/bedrock-provider.ts +236 -0
- package/src/electron/agent/llm/gemini-provider.ts +422 -0
- package/src/electron/agent/llm/index.ts +9 -0
- package/src/electron/agent/llm/ollama-provider.ts +347 -0
- package/src/electron/agent/llm/openai-oauth.ts +127 -0
- package/src/electron/agent/llm/openai-provider.ts +686 -0
- package/src/electron/agent/llm/openrouter-provider.ts +273 -0
- package/src/electron/agent/llm/pricing.ts +180 -0
- package/src/electron/agent/llm/provider-factory.ts +971 -0
- package/src/electron/agent/llm/types.ts +291 -0
- package/src/electron/agent/queue-manager.ts +408 -0
- package/src/electron/agent/sandbox/docker-sandbox.ts +453 -0
- package/src/electron/agent/sandbox/macos-sandbox.ts +426 -0
- package/src/electron/agent/sandbox/runner.ts +453 -0
- package/src/electron/agent/sandbox/sandbox-factory.ts +337 -0
- package/src/electron/agent/sandbox/security-utils.ts +251 -0
- package/src/electron/agent/search/brave-provider.ts +141 -0
- package/src/electron/agent/search/google-provider.ts +131 -0
- package/src/electron/agent/search/index.ts +6 -0
- package/src/electron/agent/search/provider-factory.ts +450 -0
- package/src/electron/agent/search/serpapi-provider.ts +138 -0
- package/src/electron/agent/search/tavily-provider.ts +108 -0
- package/src/electron/agent/search/types.ts +118 -0
- package/src/electron/agent/security/index.ts +20 -0
- package/src/electron/agent/security/input-sanitizer.ts +380 -0
- package/src/electron/agent/security/output-filter.ts +259 -0
- package/src/electron/agent/skill-eligibility.ts +334 -0
- package/src/electron/agent/skill-registry.ts +457 -0
- package/src/electron/agent/skills/document.ts +1070 -0
- package/src/electron/agent/skills/image-generator.ts +272 -0
- package/src/electron/agent/skills/organizer.ts +131 -0
- package/src/electron/agent/skills/presentation.ts +418 -0
- package/src/electron/agent/skills/spreadsheet.ts +166 -0
- package/src/electron/agent/tools/browser-tools.ts +546 -0
- package/src/electron/agent/tools/builtin-settings.ts +422 -0
- package/src/electron/agent/tools/canvas-tools.ts +572 -0
- package/src/electron/agent/tools/cron-tools.ts +723 -0
- package/src/electron/agent/tools/edit-tools.ts +196 -0
- package/src/electron/agent/tools/file-tools.ts +811 -0
- package/src/electron/agent/tools/glob-tools.ts +303 -0
- package/src/electron/agent/tools/grep-tools.ts +432 -0
- package/src/electron/agent/tools/image-tools.ts +126 -0
- package/src/electron/agent/tools/mention-tools.ts +371 -0
- package/src/electron/agent/tools/node-tools.ts +550 -0
- package/src/electron/agent/tools/registry.ts +3052 -0
- package/src/electron/agent/tools/search-tools.ts +111 -0
- package/src/electron/agent/tools/shell-tools.ts +651 -0
- package/src/electron/agent/tools/skill-tools.ts +340 -0
- package/src/electron/agent/tools/system-tools.ts +665 -0
- package/src/electron/agent/tools/web-fetch-tools.ts +528 -0
- package/src/electron/agent/tools/x-tools.ts +267 -0
- package/src/electron/agents/AgentRoleRepository.ts +557 -0
- package/src/electron/agents/HeartbeatService.ts +469 -0
- package/src/electron/agents/MentionRepository.ts +242 -0
- package/src/electron/agents/TaskSubscriptionRepository.ts +231 -0
- package/src/electron/agents/WorkingStateRepository.ts +278 -0
- package/src/electron/canvas/canvas-manager.ts +818 -0
- package/src/electron/canvas/canvas-preload.ts +102 -0
- package/src/electron/canvas/canvas-protocol.ts +174 -0
- package/src/electron/canvas/canvas-store.ts +200 -0
- package/src/electron/canvas/index.ts +8 -0
- package/src/electron/control-plane/client.ts +527 -0
- package/src/electron/control-plane/handlers.ts +723 -0
- package/src/electron/control-plane/index.ts +51 -0
- package/src/electron/control-plane/node-manager.ts +322 -0
- package/src/electron/control-plane/protocol.ts +269 -0
- package/src/electron/control-plane/remote-client.ts +517 -0
- package/src/electron/control-plane/server.ts +853 -0
- package/src/electron/control-plane/settings.ts +401 -0
- package/src/electron/control-plane/ssh-tunnel.ts +624 -0
- package/src/electron/cron/index.ts +9 -0
- package/src/electron/cron/schedule.ts +217 -0
- package/src/electron/cron/service.ts +743 -0
- package/src/electron/cron/store.ts +165 -0
- package/src/electron/cron/types.ts +291 -0
- package/src/electron/cron/webhook.ts +303 -0
- package/src/electron/database/SecureSettingsRepository.ts +514 -0
- package/src/electron/database/TaskLabelRepository.ts +148 -0
- package/src/electron/database/repositories.ts +2397 -0
- package/src/electron/database/schema.ts +1017 -0
- package/src/electron/extensions/index.ts +18 -0
- package/src/electron/extensions/loader.ts +336 -0
- package/src/electron/extensions/registry.ts +546 -0
- package/src/electron/extensions/types.ts +372 -0
- package/src/electron/gateway/channel-registry.ts +1267 -0
- package/src/electron/gateway/channels/bluebubbles-client.ts +641 -0
- package/src/electron/gateway/channels/bluebubbles.ts +509 -0
- package/src/electron/gateway/channels/discord.ts +1150 -0
- package/src/electron/gateway/channels/email-client.ts +708 -0
- package/src/electron/gateway/channels/email.ts +516 -0
- package/src/electron/gateway/channels/google-chat.ts +760 -0
- package/src/electron/gateway/channels/imessage-client.ts +473 -0
- package/src/electron/gateway/channels/imessage.ts +520 -0
- package/src/electron/gateway/channels/index.ts +21 -0
- package/src/electron/gateway/channels/line-client.ts +598 -0
- package/src/electron/gateway/channels/line.ts +559 -0
- package/src/electron/gateway/channels/matrix-client.ts +632 -0
- package/src/electron/gateway/channels/matrix.ts +655 -0
- package/src/electron/gateway/channels/mattermost-client.ts +526 -0
- package/src/electron/gateway/channels/mattermost.ts +550 -0
- package/src/electron/gateway/channels/signal-client.ts +722 -0
- package/src/electron/gateway/channels/signal.ts +666 -0
- package/src/electron/gateway/channels/slack.ts +458 -0
- package/src/electron/gateway/channels/teams.ts +681 -0
- package/src/electron/gateway/channels/telegram.ts +1727 -0
- package/src/electron/gateway/channels/twitch-client.ts +665 -0
- package/src/electron/gateway/channels/twitch.ts +468 -0
- package/src/electron/gateway/channels/types.ts +1002 -0
- package/src/electron/gateway/channels/whatsapp.ts +1101 -0
- package/src/electron/gateway/context-policy.ts +382 -0
- package/src/electron/gateway/index.ts +1274 -0
- package/src/electron/gateway/infrastructure.ts +645 -0
- package/src/electron/gateway/router.ts +3206 -0
- package/src/electron/gateway/security.ts +422 -0
- package/src/electron/gateway/session.ts +144 -0
- package/src/electron/gateway/tunnel.ts +626 -0
- package/src/electron/guardrails/guardrail-manager.ts +380 -0
- package/src/electron/hooks/gmail-watcher.ts +355 -0
- package/src/electron/hooks/index.ts +30 -0
- package/src/electron/hooks/mappings.ts +404 -0
- package/src/electron/hooks/server.ts +574 -0
- package/src/electron/hooks/settings.ts +466 -0
- package/src/electron/hooks/types.ts +245 -0
- package/src/electron/ipc/canvas-handlers.ts +223 -0
- package/src/electron/ipc/handlers.ts +3661 -0
- package/src/electron/ipc/mission-control-handlers.ts +182 -0
- package/src/electron/main.ts +496 -0
- package/src/electron/mcp/client/MCPClientManager.ts +406 -0
- package/src/electron/mcp/client/MCPServerConnection.ts +514 -0
- package/src/electron/mcp/client/transports/SSETransport.ts +360 -0
- package/src/electron/mcp/client/transports/StdioTransport.ts +355 -0
- package/src/electron/mcp/client/transports/WebSocketTransport.ts +384 -0
- package/src/electron/mcp/host/MCPHostServer.ts +388 -0
- package/src/electron/mcp/host/ToolAdapter.ts +140 -0
- package/src/electron/mcp/registry/MCPRegistryManager.ts +565 -0
- package/src/electron/mcp/settings.ts +468 -0
- package/src/electron/mcp/types.ts +371 -0
- package/src/electron/memory/MemoryService.ts +523 -0
- package/src/electron/notifications/index.ts +16 -0
- package/src/electron/notifications/service.ts +161 -0
- package/src/electron/notifications/store.ts +163 -0
- package/src/electron/preload.ts +2845 -0
- package/src/electron/reports/StandupReportService.ts +356 -0
- package/src/electron/security/concurrency.ts +333 -0
- package/src/electron/security/index.ts +17 -0
- package/src/electron/security/policy-manager.ts +539 -0
- package/src/electron/settings/appearance-manager.ts +182 -0
- package/src/electron/settings/personality-manager.ts +800 -0
- package/src/electron/settings/x-manager.ts +62 -0
- package/src/electron/tailscale/exposure.ts +262 -0
- package/src/electron/tailscale/index.ts +34 -0
- package/src/electron/tailscale/settings.ts +218 -0
- package/src/electron/tailscale/tailscale.ts +379 -0
- package/src/electron/tray/QuickInputWindow.ts +609 -0
- package/src/electron/tray/TrayManager.ts +1005 -0
- package/src/electron/tray/index.ts +6 -0
- package/src/electron/updater/index.ts +1 -0
- package/src/electron/updater/update-manager.ts +447 -0
- package/src/electron/utils/env-migration.ts +203 -0
- package/src/electron/utils/process.ts +124 -0
- package/src/electron/utils/rate-limiter.ts +130 -0
- package/src/electron/utils/validation.ts +493 -0
- package/src/electron/utils/x-cli.ts +198 -0
- package/src/electron/voice/VoiceService.ts +583 -0
- package/src/electron/voice/index.ts +9 -0
- package/src/electron/voice/voice-settings-manager.ts +403 -0
- package/src/renderer/App.tsx +775 -0
- package/src/renderer/components/ActivityFeed.tsx +407 -0
- package/src/renderer/components/ActivityFeedItem.tsx +285 -0
- package/src/renderer/components/AgentRoleCard.tsx +343 -0
- package/src/renderer/components/AgentRoleEditor.tsx +805 -0
- package/src/renderer/components/AgentSquadSettings.tsx +295 -0
- package/src/renderer/components/AgentWorkingStatePanel.tsx +411 -0
- package/src/renderer/components/AppearanceSettings.tsx +122 -0
- package/src/renderer/components/ApprovalDialog.tsx +100 -0
- package/src/renderer/components/BlueBubblesSettings.tsx +505 -0
- package/src/renderer/components/BuiltinToolsSettings.tsx +307 -0
- package/src/renderer/components/CanvasPreview.tsx +1189 -0
- package/src/renderer/components/CommandOutput.tsx +202 -0
- package/src/renderer/components/ContextPolicySettings.tsx +523 -0
- package/src/renderer/components/ControlPlaneSettings.tsx +1134 -0
- package/src/renderer/components/DisclaimerModal.tsx +124 -0
- package/src/renderer/components/DiscordSettings.tsx +436 -0
- package/src/renderer/components/EmailSettings.tsx +606 -0
- package/src/renderer/components/ExtensionsSettings.tsx +542 -0
- package/src/renderer/components/FileViewer.tsx +224 -0
- package/src/renderer/components/GoogleChatSettings.tsx +535 -0
- package/src/renderer/components/GuardrailSettings.tsx +487 -0
- package/src/renderer/components/HooksSettings.tsx +581 -0
- package/src/renderer/components/ImessageSettings.tsx +484 -0
- package/src/renderer/components/LineSettings.tsx +483 -0
- package/src/renderer/components/MCPRegistryBrowser.tsx +386 -0
- package/src/renderer/components/MCPSettings.tsx +943 -0
- package/src/renderer/components/MainContent.tsx +2433 -0
- package/src/renderer/components/MatrixSettings.tsx +510 -0
- package/src/renderer/components/MattermostSettings.tsx +473 -0
- package/src/renderer/components/MemorySettings.tsx +247 -0
- package/src/renderer/components/MentionBadge.tsx +87 -0
- package/src/renderer/components/MentionInput.tsx +409 -0
- package/src/renderer/components/MentionList.tsx +476 -0
- package/src/renderer/components/MissionControlPanel.tsx +1995 -0
- package/src/renderer/components/NodesSettings.tsx +316 -0
- package/src/renderer/components/NotificationPanel.tsx +481 -0
- package/src/renderer/components/Onboarding/AwakeningOrb.tsx +44 -0
- package/src/renderer/components/Onboarding/Onboarding.tsx +443 -0
- package/src/renderer/components/Onboarding/TypewriterText.tsx +102 -0
- package/src/renderer/components/Onboarding/index.ts +3 -0
- package/src/renderer/components/OnboardingModal.tsx +698 -0
- package/src/renderer/components/PairingCodeDisplay.tsx +324 -0
- package/src/renderer/components/PersonalitySettings.tsx +597 -0
- package/src/renderer/components/QueueSettings.tsx +119 -0
- package/src/renderer/components/QuickTaskFAB.tsx +71 -0
- package/src/renderer/components/RightPanel.tsx +413 -0
- package/src/renderer/components/ScheduledTasksSettings.tsx +1328 -0
- package/src/renderer/components/SearchSettings.tsx +328 -0
- package/src/renderer/components/Settings.tsx +1504 -0
- package/src/renderer/components/Sidebar.tsx +344 -0
- package/src/renderer/components/SignalSettings.tsx +673 -0
- package/src/renderer/components/SkillHubBrowser.tsx +458 -0
- package/src/renderer/components/SkillParameterModal.tsx +185 -0
- package/src/renderer/components/SkillsSettings.tsx +451 -0
- package/src/renderer/components/SlackSettings.tsx +442 -0
- package/src/renderer/components/StandupReportViewer.tsx +614 -0
- package/src/renderer/components/TaskBoard.tsx +498 -0
- package/src/renderer/components/TaskBoardCard.tsx +357 -0
- package/src/renderer/components/TaskBoardColumn.tsx +211 -0
- package/src/renderer/components/TaskLabelManager.tsx +472 -0
- package/src/renderer/components/TaskQueuePanel.tsx +144 -0
- package/src/renderer/components/TaskQuickActions.tsx +492 -0
- package/src/renderer/components/TaskTimeline.tsx +216 -0
- package/src/renderer/components/TaskView.tsx +162 -0
- package/src/renderer/components/TeamsSettings.tsx +518 -0
- package/src/renderer/components/TelegramSettings.tsx +421 -0
- package/src/renderer/components/Toast.tsx +76 -0
- package/src/renderer/components/TraySettings.tsx +189 -0
- package/src/renderer/components/TwitchSettings.tsx +511 -0
- package/src/renderer/components/UpdateSettings.tsx +295 -0
- package/src/renderer/components/VoiceIndicator.tsx +270 -0
- package/src/renderer/components/VoiceSettings.tsx +867 -0
- package/src/renderer/components/WhatsAppSettings.tsx +721 -0
- package/src/renderer/components/WorkingStateEditor.tsx +309 -0
- package/src/renderer/components/WorkingStateHistory.tsx +481 -0
- package/src/renderer/components/WorkspaceSelector.tsx +150 -0
- package/src/renderer/components/XSettings.tsx +311 -0
- package/src/renderer/global.d.ts +9 -0
- package/src/renderer/hooks/useAgentContext.ts +153 -0
- package/src/renderer/hooks/useOnboardingFlow.ts +548 -0
- package/src/renderer/hooks/useVoiceInput.ts +268 -0
- package/src/renderer/index.html +12 -0
- package/src/renderer/main.tsx +10 -0
- package/src/renderer/public/cowork-os-logo.png +0 -0
- package/src/renderer/quick-input.html +164 -0
- package/src/renderer/styles/index.css +14504 -0
- package/src/renderer/utils/agentMessages.ts +749 -0
- package/src/renderer/utils/voice-directives.ts +169 -0
- package/src/shared/channelMessages.ts +213 -0
- package/src/shared/types.ts +3608 -0
- package/tsconfig.electron.json +26 -0
- package/tsconfig.json +26 -0
- package/tsconfig.node.json +10 -0
- 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, '&')
|
|
618
|
+
.replace(/</g, '<')
|
|
619
|
+
.replace(/>/g, '>')
|
|
620
|
+
.replace(/"/g, '"')
|
|
621
|
+
.replace(/'/g, ''');
|
|
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
|
+
}
|