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,811 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { shell } from 'electron';
|
|
4
|
+
import { Workspace } from '../../../shared/types';
|
|
5
|
+
import { AgentDaemon } from '../daemon';
|
|
6
|
+
import { GuardrailManager } from '../../guardrails/guardrail-manager';
|
|
7
|
+
import mammoth from 'mammoth';
|
|
8
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
9
|
+
const pdfParseModule = require('pdf-parse');
|
|
10
|
+
// Handle both ESM default export and CommonJS module.exports
|
|
11
|
+
const pdfParse = (typeof pdfParseModule === 'function' ? pdfParseModule : pdfParseModule.default) as (dataBuffer: Buffer) => Promise<{
|
|
12
|
+
numpages: number;
|
|
13
|
+
info: { Title?: string; Author?: string };
|
|
14
|
+
text: string;
|
|
15
|
+
}>;
|
|
16
|
+
|
|
17
|
+
// Limits to prevent context overflow
|
|
18
|
+
const MAX_FILE_SIZE = 100 * 1024; // 100KB max for file reads
|
|
19
|
+
const MAX_DIR_ENTRIES = 100; // Max files to list per directory
|
|
20
|
+
const MAX_SEARCH_RESULTS = 50; // Max search results
|
|
21
|
+
const MAX_NAME_PAD = 48; // Cap for aligned directory listings
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* FileTools implements safe file operations within the workspace
|
|
25
|
+
*/
|
|
26
|
+
export class FileTools {
|
|
27
|
+
constructor(
|
|
28
|
+
private workspace: Workspace,
|
|
29
|
+
private daemon: AgentDaemon,
|
|
30
|
+
private taskId: string
|
|
31
|
+
) {}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Update the workspace for this tool
|
|
35
|
+
*/
|
|
36
|
+
setWorkspace(workspace: Workspace): void {
|
|
37
|
+
this.workspace = workspace;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Dangerous paths that should never be written to, even with unrestricted access
|
|
42
|
+
*/
|
|
43
|
+
private static readonly PROTECTED_PATHS = [
|
|
44
|
+
'/System',
|
|
45
|
+
'/Library',
|
|
46
|
+
'/usr',
|
|
47
|
+
'/bin',
|
|
48
|
+
'/sbin',
|
|
49
|
+
'/etc',
|
|
50
|
+
'/var',
|
|
51
|
+
'/private',
|
|
52
|
+
'C:\\Windows',
|
|
53
|
+
'C:\\Program Files',
|
|
54
|
+
'C:\\Program Files (x86)',
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Check if a path is in a protected system location
|
|
59
|
+
*/
|
|
60
|
+
private isProtectedPath(absolutePath: string): boolean {
|
|
61
|
+
const normalizedPath = path.normalize(absolutePath).toLowerCase();
|
|
62
|
+
return FileTools.PROTECTED_PATHS.some(protectedPath =>
|
|
63
|
+
normalizedPath.startsWith(protectedPath.toLowerCase())
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Check if path is allowed based on allowedPaths configuration
|
|
69
|
+
*/
|
|
70
|
+
private isPathAllowed(absolutePath: string): boolean {
|
|
71
|
+
const allowedPaths = this.workspace.permissions.allowedPaths;
|
|
72
|
+
if (!allowedPaths || allowedPaths.length === 0) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const normalizedPath = path.normalize(absolutePath);
|
|
77
|
+
return allowedPaths.some(allowed => {
|
|
78
|
+
const normalizedAllowed = path.normalize(allowed);
|
|
79
|
+
// Check if the path starts with or equals an allowed path
|
|
80
|
+
return normalizedPath === normalizedAllowed ||
|
|
81
|
+
normalizedPath.startsWith(normalizedAllowed + path.sep);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Resolve path, supporting both workspace-relative and absolute paths
|
|
87
|
+
* When unrestrictedFileAccess is enabled, allows absolute paths anywhere (except protected locations)
|
|
88
|
+
* When allowedPaths is configured, allows specific paths outside workspace
|
|
89
|
+
*/
|
|
90
|
+
private resolvePath(inputPath: string, operation: 'read' | 'write' | 'delete' = 'read'): string {
|
|
91
|
+
const normalizedWorkspace = path.resolve(this.workspace.path);
|
|
92
|
+
|
|
93
|
+
// Handle absolute paths
|
|
94
|
+
if (path.isAbsolute(inputPath)) {
|
|
95
|
+
const absolutePath = path.normalize(inputPath);
|
|
96
|
+
|
|
97
|
+
// Check if it's inside workspace (always allowed)
|
|
98
|
+
const relativeToWorkspace = path.relative(normalizedWorkspace, absolutePath);
|
|
99
|
+
if (!relativeToWorkspace.startsWith('..') && !path.isAbsolute(relativeToWorkspace)) {
|
|
100
|
+
return absolutePath;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Outside workspace - check permissions
|
|
104
|
+
if (this.workspace.isTemp || this.workspace.permissions.unrestrictedFileAccess) {
|
|
105
|
+
// With unrestricted access, block protected paths for writes
|
|
106
|
+
if (operation !== 'read' && this.isProtectedPath(absolutePath)) {
|
|
107
|
+
throw new Error(`Cannot ${operation} protected system path: ${absolutePath}`);
|
|
108
|
+
}
|
|
109
|
+
return absolutePath;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Check if in allowed paths
|
|
113
|
+
if (this.isPathAllowed(absolutePath)) {
|
|
114
|
+
if (operation !== 'read' && this.isProtectedPath(absolutePath)) {
|
|
115
|
+
throw new Error(`Cannot ${operation} protected system path: ${absolutePath}`);
|
|
116
|
+
}
|
|
117
|
+
return absolutePath;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
throw new Error(
|
|
121
|
+
'Path is outside workspace boundary. Enable "Unrestricted File Access" in workspace settings ' +
|
|
122
|
+
'or add specific paths to "Allowed Paths" to access files outside the workspace.'
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Handle relative paths (relative to workspace)
|
|
127
|
+
const resolved = path.resolve(normalizedWorkspace, inputPath);
|
|
128
|
+
const relative = path.relative(normalizedWorkspace, resolved);
|
|
129
|
+
|
|
130
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
131
|
+
// Path escapes workspace via ../ traversal
|
|
132
|
+
if (this.workspace.isTemp || this.workspace.permissions.unrestrictedFileAccess) {
|
|
133
|
+
if (operation !== 'read' && this.isProtectedPath(resolved)) {
|
|
134
|
+
throw new Error(`Cannot ${operation} protected system path: ${resolved}`);
|
|
135
|
+
}
|
|
136
|
+
return resolved;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (this.isPathAllowed(resolved)) {
|
|
140
|
+
if (operation !== 'read' && this.isProtectedPath(resolved)) {
|
|
141
|
+
throw new Error(`Cannot ${operation} protected system path: ${resolved}`);
|
|
142
|
+
}
|
|
143
|
+
return resolved;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
throw new Error(
|
|
147
|
+
'Path traversal outside workspace is not allowed. Enable "Unrestricted File Access" ' +
|
|
148
|
+
'in workspace settings to access files outside the workspace.'
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return resolved;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Check if operation is allowed based on permissions
|
|
157
|
+
*/
|
|
158
|
+
private checkPermission(operation: 'read' | 'write' | 'delete'): void {
|
|
159
|
+
if (operation === 'read' && !this.workspace.permissions.read) {
|
|
160
|
+
throw new Error('Read permission not granted');
|
|
161
|
+
}
|
|
162
|
+
if (operation === 'write' && !this.workspace.permissions.write) {
|
|
163
|
+
throw new Error('Write permission not granted');
|
|
164
|
+
}
|
|
165
|
+
if (operation === 'delete' && !this.workspace.permissions.delete) {
|
|
166
|
+
throw new Error('Delete permission not granted');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Read file contents (with size limit to prevent context overflow)
|
|
172
|
+
* Supports plain text, DOCX, and PDF files
|
|
173
|
+
*/
|
|
174
|
+
async readFile(relativePath: string): Promise<{ content: string; size: number; truncated?: boolean; format?: string }> {
|
|
175
|
+
// Validate input
|
|
176
|
+
if (!relativePath || typeof relativePath !== 'string') {
|
|
177
|
+
throw new Error('Invalid path: path must be a non-empty string');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
this.checkPermission('read');
|
|
181
|
+
const fullPath = this.resolvePath(relativePath, 'read');
|
|
182
|
+
const ext = path.extname(fullPath).toLowerCase();
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
const stats = await fs.stat(fullPath);
|
|
186
|
+
|
|
187
|
+
// Handle DOCX files
|
|
188
|
+
if (ext === '.docx') {
|
|
189
|
+
return await this.readDocxFile(fullPath, stats.size);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Handle PDF files
|
|
193
|
+
if (ext === '.pdf') {
|
|
194
|
+
return await this.readPdfFile(fullPath, stats.size);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Handle plain text files
|
|
198
|
+
// Check file size before reading
|
|
199
|
+
if (stats.size > MAX_FILE_SIZE) {
|
|
200
|
+
// Read only the first portion of large files
|
|
201
|
+
const fileHandle = await fs.open(fullPath, 'r');
|
|
202
|
+
try {
|
|
203
|
+
const buffer = Buffer.alloc(MAX_FILE_SIZE);
|
|
204
|
+
await fileHandle.read(buffer, 0, MAX_FILE_SIZE, 0);
|
|
205
|
+
|
|
206
|
+
const content = buffer.toString('utf-8');
|
|
207
|
+
return {
|
|
208
|
+
content: content + `\n\n[... File truncated. Showing first ${Math.round(MAX_FILE_SIZE / 1024)}KB of ${Math.round(stats.size / 1024)}KB ...]`,
|
|
209
|
+
size: stats.size,
|
|
210
|
+
truncated: true,
|
|
211
|
+
};
|
|
212
|
+
} finally {
|
|
213
|
+
await fileHandle.close();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
218
|
+
return {
|
|
219
|
+
content,
|
|
220
|
+
size: stats.size,
|
|
221
|
+
};
|
|
222
|
+
} catch (error: any) {
|
|
223
|
+
throw new Error(`Failed to read file: ${error.message}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Read DOCX file and extract text content
|
|
229
|
+
*/
|
|
230
|
+
private async readDocxFile(fullPath: string, size: number): Promise<{ content: string; size: number; truncated?: boolean; format: string }> {
|
|
231
|
+
try {
|
|
232
|
+
const result = await mammoth.extractRawText({ path: fullPath });
|
|
233
|
+
let content = result.value;
|
|
234
|
+
|
|
235
|
+
// Check if extracted text exceeds limit
|
|
236
|
+
const truncated = content.length > MAX_FILE_SIZE;
|
|
237
|
+
if (truncated) {
|
|
238
|
+
content = content.slice(0, MAX_FILE_SIZE) +
|
|
239
|
+
`\n\n[... Content truncated. Showing first ${Math.round(MAX_FILE_SIZE / 1024)}KB of extracted text ...]`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Add any warnings from mammoth
|
|
243
|
+
if (result.messages && result.messages.length > 0) {
|
|
244
|
+
const warnings = result.messages.map(m => m.message).join('\n');
|
|
245
|
+
content = `[Document warnings: ${warnings}]\n\n${content}`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
content,
|
|
250
|
+
size,
|
|
251
|
+
truncated,
|
|
252
|
+
format: 'docx',
|
|
253
|
+
};
|
|
254
|
+
} catch (error: any) {
|
|
255
|
+
throw new Error(`Failed to read DOCX file: ${error.message}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Read PDF file and extract text content
|
|
261
|
+
*/
|
|
262
|
+
private async readPdfFile(fullPath: string, size: number): Promise<{ content: string; size: number; truncated?: boolean; format: string }> {
|
|
263
|
+
try {
|
|
264
|
+
const dataBuffer = await fs.readFile(fullPath);
|
|
265
|
+
const data = await pdfParse(dataBuffer);
|
|
266
|
+
|
|
267
|
+
let content = data.text;
|
|
268
|
+
|
|
269
|
+
// Add metadata header
|
|
270
|
+
const metadata: string[] = [];
|
|
271
|
+
if (data.numpages) metadata.push(`Pages: ${data.numpages}`);
|
|
272
|
+
if (data.info?.Title) metadata.push(`Title: ${data.info.Title}`);
|
|
273
|
+
if (data.info?.Author) metadata.push(`Author: ${data.info.Author}`);
|
|
274
|
+
|
|
275
|
+
if (metadata.length > 0) {
|
|
276
|
+
content = `[PDF Metadata: ${metadata.join(' | ')}]\n\n${content}`;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Check if extracted text exceeds limit
|
|
280
|
+
const truncated = content.length > MAX_FILE_SIZE;
|
|
281
|
+
if (truncated) {
|
|
282
|
+
content = content.slice(0, MAX_FILE_SIZE) +
|
|
283
|
+
`\n\n[... Content truncated. Showing first ${Math.round(MAX_FILE_SIZE / 1024)}KB of extracted text ...]`;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
content,
|
|
288
|
+
size,
|
|
289
|
+
truncated,
|
|
290
|
+
format: 'pdf',
|
|
291
|
+
};
|
|
292
|
+
} catch (error: any) {
|
|
293
|
+
throw new Error(`Failed to read PDF file: ${error.message}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Write file contents
|
|
299
|
+
*/
|
|
300
|
+
async writeFile(relativePath: string, content: string): Promise<{ success: boolean; path: string }> {
|
|
301
|
+
// Validate inputs before proceeding
|
|
302
|
+
if (!relativePath || typeof relativePath !== 'string') {
|
|
303
|
+
throw new Error('Invalid path: path must be a non-empty string');
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Check for binary file extensions that shouldn't be written with write_file
|
|
307
|
+
const ext = path.extname(relativePath).toLowerCase();
|
|
308
|
+
const binaryExtensions = ['.docx', '.xlsx', '.pptx', '.pdf', '.zip', '.png', '.jpg', '.jpeg', '.gif', '.mp3', '.mp4', '.exe', '.dmg'];
|
|
309
|
+
if (binaryExtensions.includes(ext)) {
|
|
310
|
+
const suggestions: Record<string, string> = {
|
|
311
|
+
'.docx': 'Use "create_document" or "edit_document" tool instead',
|
|
312
|
+
'.xlsx': 'Use "create_spreadsheet" tool instead',
|
|
313
|
+
'.pptx': 'Use "create_presentation" tool instead',
|
|
314
|
+
'.pdf': 'Use "create_document" with format="pdf" instead',
|
|
315
|
+
};
|
|
316
|
+
const suggestion = suggestions[ext] || 'Use the appropriate skill tool for binary files';
|
|
317
|
+
throw new Error(
|
|
318
|
+
`Cannot use write_file for binary file type "${ext}". ` +
|
|
319
|
+
`The write_file tool is for text files only. ${suggestion}.`
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (content === undefined || content === null) {
|
|
324
|
+
throw new Error('Invalid content: content parameter is required but was not provided');
|
|
325
|
+
}
|
|
326
|
+
if (typeof content !== 'string') {
|
|
327
|
+
throw new Error(`Invalid content: expected string but received ${typeof content}`);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
this.checkPermission('write');
|
|
331
|
+
const fullPath = this.resolvePath(relativePath, 'write');
|
|
332
|
+
|
|
333
|
+
// Check file size against guardrail limits
|
|
334
|
+
const contentSizeBytes = Buffer.byteLength(content, 'utf-8');
|
|
335
|
+
const sizeCheck = GuardrailManager.isFileSizeExceeded(contentSizeBytes);
|
|
336
|
+
if (sizeCheck.exceeded) {
|
|
337
|
+
throw new Error(
|
|
338
|
+
`File size limit exceeded: ${sizeCheck.sizeMB.toFixed(2)}MB exceeds limit of ${sizeCheck.limitMB}MB.\n` +
|
|
339
|
+
`You can adjust this limit in Settings > Guardrails.`
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
try {
|
|
344
|
+
// Ensure directory exists
|
|
345
|
+
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
|
346
|
+
|
|
347
|
+
// Write file
|
|
348
|
+
await fs.writeFile(fullPath, content, 'utf-8');
|
|
349
|
+
|
|
350
|
+
// Log artifact
|
|
351
|
+
this.daemon.logEvent(this.taskId, 'file_created', {
|
|
352
|
+
path: relativePath,
|
|
353
|
+
size: content.length,
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
return {
|
|
357
|
+
success: true,
|
|
358
|
+
path: relativePath,
|
|
359
|
+
};
|
|
360
|
+
} catch (error: any) {
|
|
361
|
+
throw new Error(`Failed to write file: ${error.message}`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* List directory contents (limited to prevent context overflow)
|
|
367
|
+
*/
|
|
368
|
+
async listDirectory(relativePath: string = '.'): Promise<{
|
|
369
|
+
files: Array<{ name: string; type: 'file' | 'directory'; size: number }>;
|
|
370
|
+
totalCount: number;
|
|
371
|
+
truncated?: boolean;
|
|
372
|
+
}> {
|
|
373
|
+
// Validate and normalize input (use default if null/undefined)
|
|
374
|
+
const pathToUse = (relativePath && typeof relativePath === 'string') ? relativePath : '.';
|
|
375
|
+
|
|
376
|
+
this.checkPermission('read');
|
|
377
|
+
const fullPath = this.resolvePath(pathToUse, 'read');
|
|
378
|
+
|
|
379
|
+
try {
|
|
380
|
+
const entries = await fs.readdir(fullPath, { withFileTypes: true });
|
|
381
|
+
const totalCount = entries.length;
|
|
382
|
+
|
|
383
|
+
// Limit entries to prevent large responses
|
|
384
|
+
const limitedEntries = entries.slice(0, MAX_DIR_ENTRIES);
|
|
385
|
+
|
|
386
|
+
const files = await Promise.all(
|
|
387
|
+
limitedEntries.map(async entry => {
|
|
388
|
+
const entryPath = path.join(fullPath, entry.name);
|
|
389
|
+
try {
|
|
390
|
+
const stats = await fs.stat(entryPath);
|
|
391
|
+
return {
|
|
392
|
+
name: entry.name,
|
|
393
|
+
type: entry.isDirectory() ? 'directory' as const : 'file' as const,
|
|
394
|
+
size: stats.size,
|
|
395
|
+
};
|
|
396
|
+
} catch {
|
|
397
|
+
return {
|
|
398
|
+
name: entry.name,
|
|
399
|
+
type: 'file' as const,
|
|
400
|
+
size: 0,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
})
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
return {
|
|
407
|
+
files,
|
|
408
|
+
totalCount,
|
|
409
|
+
truncated: totalCount > MAX_DIR_ENTRIES,
|
|
410
|
+
};
|
|
411
|
+
} catch (error: any) {
|
|
412
|
+
throw new Error(`Failed to list directory: ${error.message}`);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* List directory contents in a compact, size-aware format
|
|
418
|
+
* Mirrors MCP filesystem output for easier agent consumption.
|
|
419
|
+
*/
|
|
420
|
+
async listDirectoryWithSizes(relativePath: string = '.'): Promise<{
|
|
421
|
+
output: string;
|
|
422
|
+
files: Array<{ name: string; type: 'file' | 'directory'; size: number }>;
|
|
423
|
+
totalCount: number;
|
|
424
|
+
truncated?: boolean;
|
|
425
|
+
combinedSize: number;
|
|
426
|
+
}> {
|
|
427
|
+
const pathToUse = (relativePath && typeof relativePath === 'string') ? relativePath : '.';
|
|
428
|
+
|
|
429
|
+
this.checkPermission('read');
|
|
430
|
+
const fullPath = this.resolvePath(pathToUse, 'read');
|
|
431
|
+
|
|
432
|
+
try {
|
|
433
|
+
const entries = await fs.readdir(fullPath, { withFileTypes: true });
|
|
434
|
+
const totalCount = entries.length;
|
|
435
|
+
const limitedEntries = entries.slice(0, MAX_DIR_ENTRIES);
|
|
436
|
+
|
|
437
|
+
const files = await Promise.all(
|
|
438
|
+
limitedEntries.map(async entry => {
|
|
439
|
+
const entryPath = path.join(fullPath, entry.name);
|
|
440
|
+
try {
|
|
441
|
+
const stats = await fs.stat(entryPath);
|
|
442
|
+
return {
|
|
443
|
+
name: entry.name,
|
|
444
|
+
type: entry.isDirectory() ? 'directory' as const : 'file' as const,
|
|
445
|
+
size: stats.size,
|
|
446
|
+
};
|
|
447
|
+
} catch {
|
|
448
|
+
return {
|
|
449
|
+
name: entry.name,
|
|
450
|
+
type: entry.isDirectory() ? 'directory' as const : 'file' as const,
|
|
451
|
+
size: 0,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
})
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
const combinedSize = files.reduce((sum, entry) => sum + (entry.type === 'file' ? entry.size : 0), 0);
|
|
458
|
+
const output = this.formatDirectoryListing(files, combinedSize);
|
|
459
|
+
|
|
460
|
+
return {
|
|
461
|
+
output,
|
|
462
|
+
files,
|
|
463
|
+
totalCount,
|
|
464
|
+
truncated: totalCount > MAX_DIR_ENTRIES,
|
|
465
|
+
combinedSize,
|
|
466
|
+
};
|
|
467
|
+
} catch (error: any) {
|
|
468
|
+
throw new Error(`Failed to list directory: ${error.message}`);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Get file or directory metadata
|
|
474
|
+
*/
|
|
475
|
+
async getFileInfo(relativePath: string): Promise<{
|
|
476
|
+
size: number;
|
|
477
|
+
created: string;
|
|
478
|
+
modified: string;
|
|
479
|
+
accessed: string;
|
|
480
|
+
isDirectory: boolean;
|
|
481
|
+
isFile: boolean;
|
|
482
|
+
permissions: string;
|
|
483
|
+
}> {
|
|
484
|
+
if (!relativePath || typeof relativePath !== 'string') {
|
|
485
|
+
throw new Error('Invalid path: path must be a non-empty string');
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
this.checkPermission('read');
|
|
489
|
+
const fullPath = this.resolvePath(relativePath, 'read');
|
|
490
|
+
|
|
491
|
+
try {
|
|
492
|
+
const stats = await fs.stat(fullPath);
|
|
493
|
+
const permissions = (stats.mode & 0o777).toString(8);
|
|
494
|
+
return {
|
|
495
|
+
size: stats.size,
|
|
496
|
+
created: stats.birthtime.toISOString(),
|
|
497
|
+
modified: stats.mtime.toISOString(),
|
|
498
|
+
accessed: stats.atime.toISOString(),
|
|
499
|
+
isDirectory: stats.isDirectory(),
|
|
500
|
+
isFile: stats.isFile(),
|
|
501
|
+
permissions,
|
|
502
|
+
};
|
|
503
|
+
} catch (error: any) {
|
|
504
|
+
throw new Error(`Failed to get file info: ${error.message}`);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Rename or move file
|
|
510
|
+
*/
|
|
511
|
+
async renameFile(oldPath: string, newPath: string): Promise<{ success: boolean }> {
|
|
512
|
+
// Validate inputs
|
|
513
|
+
if (!oldPath || typeof oldPath !== 'string') {
|
|
514
|
+
throw new Error('Invalid oldPath: must be a non-empty string');
|
|
515
|
+
}
|
|
516
|
+
if (!newPath || typeof newPath !== 'string') {
|
|
517
|
+
throw new Error('Invalid newPath: must be a non-empty string');
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
this.checkPermission('write');
|
|
521
|
+
const oldFullPath = this.resolvePath(oldPath, 'write');
|
|
522
|
+
const newFullPath = this.resolvePath(newPath, 'write');
|
|
523
|
+
|
|
524
|
+
try {
|
|
525
|
+
// Ensure target directory exists
|
|
526
|
+
await fs.mkdir(path.dirname(newFullPath), { recursive: true });
|
|
527
|
+
|
|
528
|
+
await fs.rename(oldFullPath, newFullPath);
|
|
529
|
+
|
|
530
|
+
this.daemon.logEvent(this.taskId, 'file_modified', {
|
|
531
|
+
action: 'rename',
|
|
532
|
+
from: oldPath,
|
|
533
|
+
to: newPath,
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
return { success: true };
|
|
537
|
+
} catch (error: any) {
|
|
538
|
+
throw new Error(`Failed to rename file: ${error.message}`);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Copy file (supports binary files like DOCX, PDF, images, etc.)
|
|
544
|
+
*/
|
|
545
|
+
async copyFile(sourcePath: string, destPath: string): Promise<{ success: boolean; path: string }> {
|
|
546
|
+
// Validate inputs
|
|
547
|
+
if (!sourcePath || typeof sourcePath !== 'string') {
|
|
548
|
+
throw new Error('Invalid sourcePath: must be a non-empty string');
|
|
549
|
+
}
|
|
550
|
+
if (!destPath || typeof destPath !== 'string') {
|
|
551
|
+
throw new Error('Invalid destPath: must be a non-empty string');
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
this.checkPermission('read');
|
|
555
|
+
this.checkPermission('write');
|
|
556
|
+
const sourceFullPath = this.resolvePath(sourcePath, 'read');
|
|
557
|
+
const destFullPath = this.resolvePath(destPath, 'write');
|
|
558
|
+
|
|
559
|
+
try {
|
|
560
|
+
// Ensure target directory exists
|
|
561
|
+
await fs.mkdir(path.dirname(destFullPath), { recursive: true });
|
|
562
|
+
|
|
563
|
+
// Copy file using binary buffer (preserves exact content)
|
|
564
|
+
await fs.copyFile(sourceFullPath, destFullPath);
|
|
565
|
+
|
|
566
|
+
this.daemon.logEvent(this.taskId, 'file_created', {
|
|
567
|
+
path: destPath,
|
|
568
|
+
copiedFrom: sourcePath,
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
return {
|
|
572
|
+
success: true,
|
|
573
|
+
path: destPath,
|
|
574
|
+
};
|
|
575
|
+
} catch (error: any) {
|
|
576
|
+
throw new Error(`Failed to copy file: ${error.message}`);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Delete file (requires approval)
|
|
582
|
+
* Uses shell.trashItem() for protected locations like /Applications
|
|
583
|
+
* Note: We don't check workspace.permissions.delete here because
|
|
584
|
+
* delete operations always require explicit user approval via requestApproval()
|
|
585
|
+
*/
|
|
586
|
+
async deleteFile(relativePath: string): Promise<{ success: boolean; movedToTrash?: boolean }> {
|
|
587
|
+
// Validate input
|
|
588
|
+
if (!relativePath || typeof relativePath !== 'string') {
|
|
589
|
+
throw new Error('Invalid path: path must be a non-empty string');
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const fullPath = this.resolvePath(relativePath, 'delete');
|
|
593
|
+
|
|
594
|
+
// Request user approval
|
|
595
|
+
const approved = await this.daemon.requestApproval(
|
|
596
|
+
this.taskId,
|
|
597
|
+
'delete_file',
|
|
598
|
+
`Delete file: ${relativePath}`,
|
|
599
|
+
{ path: relativePath }
|
|
600
|
+
);
|
|
601
|
+
|
|
602
|
+
if (!approved) {
|
|
603
|
+
throw new Error('User denied file deletion');
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
try {
|
|
607
|
+
// For .app bundles on macOS, use shell.trashItem directly (safer and expected behavior)
|
|
608
|
+
if (fullPath.endsWith('.app')) {
|
|
609
|
+
await shell.trashItem(fullPath);
|
|
610
|
+
|
|
611
|
+
this.daemon.logEvent(this.taskId, 'file_deleted', {
|
|
612
|
+
path: relativePath,
|
|
613
|
+
movedToTrash: true,
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
return { success: true, movedToTrash: true };
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// For other files/directories, try direct deletion
|
|
620
|
+
const stats = await fs.stat(fullPath);
|
|
621
|
+
if (stats.isDirectory()) {
|
|
622
|
+
// Use force: true to handle read-only files and special cases
|
|
623
|
+
await fs.rm(fullPath, { recursive: true, force: true });
|
|
624
|
+
} else {
|
|
625
|
+
await fs.unlink(fullPath);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
this.daemon.logEvent(this.taskId, 'file_deleted', {
|
|
629
|
+
path: relativePath,
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
return { success: true };
|
|
633
|
+
} catch (error: any) {
|
|
634
|
+
// If deletion fails, try moving to Trash as fallback
|
|
635
|
+
// This handles EPERM, EACCES, ENOTEMPTY and other filesystem errors
|
|
636
|
+
if (error.code === 'EPERM' || error.code === 'EACCES' || error.code === 'ENOTEMPTY' || error.code === 'EBUSY') {
|
|
637
|
+
try {
|
|
638
|
+
await shell.trashItem(fullPath);
|
|
639
|
+
|
|
640
|
+
this.daemon.logEvent(this.taskId, 'file_deleted', {
|
|
641
|
+
path: relativePath,
|
|
642
|
+
movedToTrash: true,
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
return { success: true, movedToTrash: true };
|
|
646
|
+
} catch (trashError: any) {
|
|
647
|
+
throw new Error(`Failed to delete file: ${error.code}. Could not move to Trash: ${trashError.message}`);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
throw new Error(`Failed to delete file: ${error.message}`);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Create directory
|
|
656
|
+
*/
|
|
657
|
+
async createDirectory(relativePath: string): Promise<{ success: boolean }> {
|
|
658
|
+
// Validate input
|
|
659
|
+
if (!relativePath || typeof relativePath !== 'string') {
|
|
660
|
+
throw new Error('Invalid path: path must be a non-empty string');
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
this.checkPermission('write');
|
|
664
|
+
const fullPath = this.resolvePath(relativePath, 'write');
|
|
665
|
+
|
|
666
|
+
try {
|
|
667
|
+
await fs.mkdir(fullPath, { recursive: true });
|
|
668
|
+
|
|
669
|
+
this.daemon.logEvent(this.taskId, 'file_created', {
|
|
670
|
+
path: relativePath,
|
|
671
|
+
type: 'directory',
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
return { success: true };
|
|
675
|
+
} catch (error: any) {
|
|
676
|
+
throw new Error(`Failed to create directory: ${error.message}`);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Search files by name or content (limited to prevent context overflow)
|
|
682
|
+
*/
|
|
683
|
+
async searchFiles(
|
|
684
|
+
query: string,
|
|
685
|
+
relativePath: string = '.'
|
|
686
|
+
): Promise<{
|
|
687
|
+
matches: Array<{ path: string; type: 'filename' | 'content' }>;
|
|
688
|
+
totalFound: number;
|
|
689
|
+
truncated?: boolean;
|
|
690
|
+
}> {
|
|
691
|
+
// Validate input
|
|
692
|
+
if (!query || typeof query !== 'string') {
|
|
693
|
+
throw new Error('Invalid query: query must be a non-empty string');
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
this.checkPermission('read');
|
|
697
|
+
const fullPath = this.resolvePath(relativePath, 'read');
|
|
698
|
+
const matches: Array<{ path: string; type: 'filename' | 'content' }> = [];
|
|
699
|
+
let filesSearched = 0;
|
|
700
|
+
const maxFilesToSearch = 500; // Limit files to search for performance
|
|
701
|
+
|
|
702
|
+
const searchRecursive = async (dir: string) => {
|
|
703
|
+
if (matches.length >= MAX_SEARCH_RESULTS || filesSearched >= maxFilesToSearch) {
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
let entries;
|
|
708
|
+
try {
|
|
709
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
710
|
+
} catch {
|
|
711
|
+
return; // Skip directories we can't read
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
for (const entry of entries) {
|
|
715
|
+
if (matches.length >= MAX_SEARCH_RESULTS || filesSearched >= maxFilesToSearch) {
|
|
716
|
+
break;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
const entryPath = path.join(dir, entry.name);
|
|
720
|
+
const relPath = path.relative(this.workspace.path, entryPath);
|
|
721
|
+
|
|
722
|
+
// Skip hidden files/directories and node_modules
|
|
723
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules') {
|
|
724
|
+
continue;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// Check filename match
|
|
728
|
+
if (entry.name.toLowerCase().includes(query.toLowerCase())) {
|
|
729
|
+
matches.push({
|
|
730
|
+
path: relPath,
|
|
731
|
+
type: 'filename',
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Search content for small files only
|
|
736
|
+
if (entry.isFile()) {
|
|
737
|
+
filesSearched++;
|
|
738
|
+
try {
|
|
739
|
+
const stats = await fs.stat(entryPath);
|
|
740
|
+
// Only search small text files
|
|
741
|
+
if (stats.size < 50 * 1024) {
|
|
742
|
+
const content = await fs.readFile(entryPath, 'utf-8');
|
|
743
|
+
if (content.toLowerCase().includes(query.toLowerCase())) {
|
|
744
|
+
if (!matches.some(m => m.path === relPath)) {
|
|
745
|
+
matches.push({
|
|
746
|
+
path: relPath,
|
|
747
|
+
type: 'content',
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
} catch {
|
|
753
|
+
// Skip binary files or files that can't be read
|
|
754
|
+
}
|
|
755
|
+
} else if (entry.isDirectory()) {
|
|
756
|
+
await searchRecursive(entryPath);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
};
|
|
760
|
+
|
|
761
|
+
try {
|
|
762
|
+
await searchRecursive(fullPath);
|
|
763
|
+
return {
|
|
764
|
+
matches: matches.slice(0, MAX_SEARCH_RESULTS),
|
|
765
|
+
totalFound: matches.length,
|
|
766
|
+
truncated: matches.length >= MAX_SEARCH_RESULTS,
|
|
767
|
+
};
|
|
768
|
+
} catch (error: any) {
|
|
769
|
+
throw new Error(`Search failed: ${error.message}`);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Format directory listing to match MCP-style output
|
|
775
|
+
*/
|
|
776
|
+
private formatDirectoryListing(
|
|
777
|
+
entries: Array<{ name: string; type: 'file' | 'directory'; size: number }>,
|
|
778
|
+
combinedSize: number
|
|
779
|
+
): string {
|
|
780
|
+
const maxNameLength = entries.reduce((max, entry) => Math.max(max, entry.name.length), 0);
|
|
781
|
+
const namePad = Math.min(Math.max(maxNameLength + 2, 16), MAX_NAME_PAD);
|
|
782
|
+
|
|
783
|
+
const lines = entries.map(entry => {
|
|
784
|
+
const label = entry.type === 'directory' ? '[DIR]' : '[FILE]';
|
|
785
|
+
const name = entry.name.padEnd(namePad, ' ');
|
|
786
|
+
const size = entry.type === 'file' ? this.formatBytes(entry.size) : '';
|
|
787
|
+
return `${label} ${name}${size}`.trimEnd();
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
const fileCount = entries.filter(entry => entry.type === 'file').length;
|
|
791
|
+
const dirCount = entries.filter(entry => entry.type === 'directory').length;
|
|
792
|
+
lines.push('');
|
|
793
|
+
lines.push(`Total: ${fileCount} files, ${dirCount} directories`);
|
|
794
|
+
lines.push(`Combined size: ${this.formatBytes(combinedSize)}`);
|
|
795
|
+
|
|
796
|
+
return lines.join('\n');
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* Human-readable byte formatting
|
|
801
|
+
*/
|
|
802
|
+
private formatBytes(bytes: number): string {
|
|
803
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
804
|
+
const kb = bytes / 1024;
|
|
805
|
+
if (kb < 1024) return `${kb.toFixed(2)} KB`;
|
|
806
|
+
const mb = kb / 1024;
|
|
807
|
+
if (mb < 1024) return `${mb.toFixed(2)} MB`;
|
|
808
|
+
const gb = mb / 1024;
|
|
809
|
+
return `${gb.toFixed(2)} GB`;
|
|
810
|
+
}
|
|
811
|
+
}
|