@aion0/forge 0.5.26 → 0.5.27
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/.forge/worktrees/pipeline-4dd8dc2d/CLAUDE.md +86 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/README.md +136 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/RELEASE_NOTES.md +36 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/agents/route.ts +17 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/auth/[...nextauth]/route.ts +3 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/auth/verify/route.ts +46 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude/[id]/route.ts +31 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude/[id]/stream/route.ts +63 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude/route.ts +28 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-sessions/[projectName]/entries/route.ts +23 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-sessions/[projectName]/live/route.ts +72 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-sessions/[projectName]/route.ts +37 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-sessions/sync/route.ts +17 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-templates/route.ts +145 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/code/route.ts +299 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/delivery/[id]/route.ts +62 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/delivery/route.ts +40 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/detect-cli/route.ts +46 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/docs/route.ts +176 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/docs/sessions/route.ts +54 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/favorites/route.ts +26 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/flows/route.ts +6 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/flows/run/route.ts +19 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/git/route.ts +149 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/help/route.ts +84 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/issue-scanner/route.ts +116 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/logs/route.ts +100 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/mobile-chat/route.ts +115 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/monitor/route.ts +74 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/notifications/route.ts +42 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/notify/test/route.ts +33 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/online/route.ts +40 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/pipelines/[id]/route.ts +41 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/pipelines/route.ts +90 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/plugins/route.ts +75 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/preview/[...path]/route.ts +64 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/preview/route.ts +156 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/project-pipelines/route.ts +91 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/project-sessions/route.ts +61 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/projects/route.ts +26 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/sessions/[id]/chat/route.ts +64 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/sessions/[id]/messages/route.ts +9 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/sessions/[id]/route.ts +17 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/sessions/route.ts +20 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/settings/route.ts +64 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/skills/local/route.ts +228 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/skills/route.ts +182 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/smith-templates/route.ts +81 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/status/route.ts +12 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tabs/route.ts +25 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/[id]/route.ts +51 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/[id]/stream/route.ts +77 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/link/route.ts +37 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/route.ts +44 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/session/route.ts +14 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/telegram/route.ts +23 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/templates/route.ts +6 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/terminal-bell/route.ts +39 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/terminal-cwd/route.ts +19 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/terminal-state/route.ts +15 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tunnel/route.ts +26 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/upgrade/route.ts +43 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/usage/route.ts +20 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/version/route.ts +78 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/watchers/route.ts +33 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/[id]/agents/route.ts +35 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/[id]/memory/route.ts +23 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/[id]/smith/route.ts +22 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/[id]/stream/route.ts +31 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/route.ts +79 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/global-error.tsx +21 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/globals.css +52 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/icon.ico +0 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/icon.png +0 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/icon.svg +106 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/layout.tsx +17 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/login/LoginForm.tsx +96 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/login/page.tsx +10 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/mobile/page.tsx +10 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/page.tsx +22 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/bin/forge-server.mjs +484 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/check-forge-status.sh +71 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/cli/mw.ts +579 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/BrowserPanel.tsx +175 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ChatPanel.tsx +191 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ClaudeTerminal.tsx +267 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/CodeViewer.tsx +787 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ConversationEditor.tsx +411 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ConversationGraphView.tsx +347 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ConversationTerminalView.tsx +303 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/Dashboard.tsx +807 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/DashboardWrapper.tsx +9 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/DeliveryFlowEditor.tsx +491 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/DeliveryList.tsx +230 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/DeliveryWorkspace.tsx +589 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/DocTerminal.tsx +187 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/DocsViewer.tsx +574 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/HelpDialog.tsx +169 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/HelpTerminal.tsx +141 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/InlinePipelineView.tsx +111 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/LogViewer.tsx +194 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/MarkdownContent.tsx +73 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/MobileView.tsx +385 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/MonitorPanel.tsx +122 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/NewSessionModal.tsx +93 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/NewTaskModal.tsx +492 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/PipelineEditor.tsx +570 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/PipelineView.tsx +1018 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/PluginsPanel.tsx +472 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ProjectDetail.tsx +1618 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ProjectList.tsx +108 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ProjectManager.tsx +401 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/SessionList.tsx +74 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/SessionView.tsx +726 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/SettingsModal.tsx +1647 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/SkillsPanel.tsx +969 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/StatusBar.tsx +99 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/TabBar.tsx +46 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/TaskBoard.tsx +113 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/TaskDetail.tsx +372 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/TerminalLauncher.tsx +398 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/TunnelToggle.tsx +206 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/UsagePanel.tsx +207 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/WebTerminal.tsx +1743 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/WorkspaceTree.tsx +221 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/WorkspaceView.tsx +4048 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/dev-test.sh +5 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/docs/Forge_Memory_Layer_Design.docx +0 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/docs/Forge_Strategy_Research_2026.docx +0 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/docs/LOCAL-DEPLOY.md +144 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/docs/roadmap-multi-agent-workflow.md +330 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/forge-logo.png +0 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/forge-logo.svg +106 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/hooks/useSidebarResize.ts +52 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/install.sh +29 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/instrumentation.ts +35 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/agents/claude-adapter.ts +104 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/agents/generic-adapter.ts +64 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/agents/index.ts +245 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/agents/types.ts +70 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/artifacts.ts +106 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/auth.ts +62 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/docker.yaml +70 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/http.yaml +66 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/jenkins.yaml +92 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/llm-vision.yaml +85 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/playwright.yaml +111 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/shell-command.yaml +60 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/slack.yaml +48 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/webhook.yaml +56 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/claude-process.ts +361 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/claude-sessions.ts +266 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/claude-templates.ts +227 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/cloudflared.ts +424 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/crypto.ts +67 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/delivery.ts +787 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/dirs.ts +99 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/flows.ts +86 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-mcp-server.ts +732 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-skills/forge-inbox.md +38 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-skills/forge-send.md +47 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-skills/forge-status.md +32 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-skills/forge-workspace-sync.md +37 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/00-overview.md +40 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/01-settings.md +194 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/02-telegram.md +41 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/03-tunnel.md +31 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/04-tasks.md +52 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/05-pipelines.md +460 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/06-skills.md +43 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/07-projects.md +73 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/08-rules.md +53 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/09-issue-autofix.md +55 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/10-troubleshooting.md +89 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/11-workspace.md +810 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/CLAUDE.md +62 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/init.ts +266 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/issue-scanner.ts +298 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/logger.ts +79 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/notifications.ts +75 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/notify.ts +108 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/password.ts +97 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/pipeline-scheduler.ts +373 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/pipeline.ts +1565 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/plugins/executor.ts +347 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/plugins/registry.ts +228 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/plugins/types.ts +103 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/project-sessions.ts +53 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/projects.ts +86 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/session-manager.ts +156 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/session-utils.ts +53 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/session-watcher.ts +345 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/settings.ts +195 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/skills.ts +458 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/task-manager.ts +951 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/telegram-bot.ts +1477 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/telegram-standalone.ts +83 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/terminal-server.ts +70 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/terminal-standalone.ts +438 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/usage-scanner.ts +249 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/__tests__/state-machine.test.ts +388 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/__tests__/workspace.test.ts +311 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/agent-bus.ts +416 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/agent-worker.ts +655 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/backends/api-backend.ts +262 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/backends/cli-backend.ts +491 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/index.ts +84 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/manager.ts +136 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/orchestrator.ts +3415 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/persistence.ts +309 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/presets.ts +649 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/requests.ts +287 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/session-monitor.ts +240 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/skill-installer.ts +275 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/smith-memory.ts +498 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/types.ts +241 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/watch-manager.ts +560 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace-standalone.ts +978 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/middleware.ts +51 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/next.config.ts +26 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/package.json +74 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/pnpm-lock.yaml +3719 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/pnpm-workspace.yaml +1 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/postcss.config.mjs +7 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/publish.sh +133 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/README.md +66 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/results/.gitignore +2 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/run.ts +635 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/01-text-utils/task.md +26 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/01-text-utils/validator.sh +46 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/02-pagination/setup.sh +19 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/02-pagination/task.md +48 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/02-pagination/validator.sh +69 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/03-bug-fix/setup.sh +82 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/03-bug-fix/task.md +30 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/03-bug-fix/validator.sh +29 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/verify-usage.ts +178 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/config/index.ts +129 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/core/db/database.ts +259 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/core/memory/strategy.ts +32 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/core/providers/chat.ts +65 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/core/providers/registry.ts +60 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/core/session/manager.ts +190 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/types/index.ts +129 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/start.sh +32 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/templates/smith-lead.json +45 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/tsconfig.json +42 -0
- package/RELEASE_NOTES.md +11 -28
- package/app/api/terminal-bell/route.ts +6 -2
- package/components/WebTerminal.tsx +36 -2
- package/lib/terminal-standalone.ts +19 -2
- package/next-env.d.ts +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,951 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task Manager — persistent task queue backed by SQLite.
|
|
3
|
+
* Tasks survive server restarts. Background runner picks up queued tasks.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { randomUUID } from 'node:crypto';
|
|
7
|
+
import { spawn, execSync } from 'node:child_process';
|
|
8
|
+
import { realpathSync } from 'node:fs';
|
|
9
|
+
import { getDb } from '@/src/core/db/database';
|
|
10
|
+
import { getDbPath } from '@/src/config';
|
|
11
|
+
import { loadSettings } from './settings';
|
|
12
|
+
import { notifyTaskComplete, notifyTaskFailed } from './notify';
|
|
13
|
+
import type { Task, TaskLogEntry, TaskStatus, TaskMode, WatchConfig } from '@/src/types';
|
|
14
|
+
|
|
15
|
+
/** Normalize SQLite datetime('now') → ISO 8601 UTC string. */
|
|
16
|
+
function toIsoUTC(s: string | null | undefined): string | null {
|
|
17
|
+
if (!s) return null;
|
|
18
|
+
if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(s)) return s.replace(' ', 'T') + 'Z';
|
|
19
|
+
return s;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const runnerKey = Symbol.for('mw-task-runner');
|
|
23
|
+
const gRunner = globalThis as any;
|
|
24
|
+
if (!gRunner[runnerKey]) gRunner[runnerKey] = { runner: null, currentTaskId: null };
|
|
25
|
+
const runnerState: { runner: ReturnType<typeof setInterval> | null; currentTaskId: string | null } = gRunner[runnerKey];
|
|
26
|
+
|
|
27
|
+
// Per-project concurrency: track which projects have a running prompt task
|
|
28
|
+
const runningProjects = new Set<string>();
|
|
29
|
+
|
|
30
|
+
// Event listeners for real-time updates
|
|
31
|
+
type TaskListener = (taskId: string, event: 'log' | 'status', data?: any) => void;
|
|
32
|
+
const listeners = new Set<TaskListener>();
|
|
33
|
+
|
|
34
|
+
export function onTaskEvent(fn: TaskListener): () => void {
|
|
35
|
+
listeners.add(fn);
|
|
36
|
+
return () => listeners.delete(fn);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function emit(taskId: string, event: 'log' | 'status', data?: any) {
|
|
40
|
+
for (const fn of listeners) {
|
|
41
|
+
try { fn(taskId, event, data); } catch {}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function db() {
|
|
46
|
+
return getDb(getDbPath());
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Per-task model overrides (used by pipeline to set pipelineModel)
|
|
50
|
+
export const taskModelOverrides = new Map<string, string>();
|
|
51
|
+
|
|
52
|
+
// ─── CRUD ────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
export function createTask(opts: {
|
|
55
|
+
projectName: string;
|
|
56
|
+
projectPath: string;
|
|
57
|
+
prompt: string;
|
|
58
|
+
mode?: TaskMode;
|
|
59
|
+
priority?: number;
|
|
60
|
+
conversationId?: string; // Explicit override; otherwise auto-inherits from project
|
|
61
|
+
scheduledAt?: string; // ISO timestamp — task won't run until this time
|
|
62
|
+
watchConfig?: WatchConfig;
|
|
63
|
+
agent?: string; // Agent ID (default: from settings)
|
|
64
|
+
}): Task {
|
|
65
|
+
const id = randomUUID().slice(0, 8);
|
|
66
|
+
const mode = opts.mode || 'prompt';
|
|
67
|
+
const agent = opts.agent || '';
|
|
68
|
+
|
|
69
|
+
// For prompt mode: auto-inherit conversation_id
|
|
70
|
+
// For monitor mode: conversationId is required (the session to watch)
|
|
71
|
+
const convId = opts.conversationId === ''
|
|
72
|
+
? null
|
|
73
|
+
: (opts.conversationId || (mode === 'prompt' ? getProjectConversationId(opts.projectName) : null));
|
|
74
|
+
|
|
75
|
+
db().prepare(`
|
|
76
|
+
INSERT INTO tasks (id, project_name, project_path, prompt, mode, status, priority, conversation_id, log, scheduled_at, watch_config, agent)
|
|
77
|
+
VALUES (?, ?, ?, ?, ?, 'queued', ?, ?, '[]', ?, ?, ?)
|
|
78
|
+
`).run(
|
|
79
|
+
id, opts.projectName, opts.projectPath, opts.prompt, mode,
|
|
80
|
+
opts.priority || 0, convId || null, opts.scheduledAt || null,
|
|
81
|
+
opts.watchConfig ? JSON.stringify(opts.watchConfig) : null,
|
|
82
|
+
agent || null,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// Kick the runner
|
|
86
|
+
ensureRunnerStarted();
|
|
87
|
+
|
|
88
|
+
return getTask(id)!;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get the most recent conversation_id for a project.
|
|
93
|
+
* This allows all tasks for the same project to share one Claude session.
|
|
94
|
+
*/
|
|
95
|
+
export function getProjectConversationId(projectName: string): string | null {
|
|
96
|
+
const row = db().prepare(`
|
|
97
|
+
SELECT conversation_id FROM tasks
|
|
98
|
+
WHERE project_name = ? AND conversation_id IS NOT NULL AND status = 'done'
|
|
99
|
+
ORDER BY completed_at DESC LIMIT 1
|
|
100
|
+
`).get(projectName) as any;
|
|
101
|
+
return row?.conversation_id || null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function getTask(id: string): Task | null {
|
|
105
|
+
const row = db().prepare('SELECT * FROM tasks WHERE id = ?').get(id) as any;
|
|
106
|
+
if (!row) return null;
|
|
107
|
+
return rowToTask(row);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function listTasks(status?: TaskStatus): Task[] {
|
|
111
|
+
let query = 'SELECT * FROM tasks';
|
|
112
|
+
const params: string[] = [];
|
|
113
|
+
if (status) {
|
|
114
|
+
query += ' WHERE status = ?';
|
|
115
|
+
params.push(status);
|
|
116
|
+
}
|
|
117
|
+
query += ' ORDER BY created_at DESC';
|
|
118
|
+
const rows = db().prepare(query).all(...params) as any[];
|
|
119
|
+
return rows.map(rowToTask);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function cancelTask(id: string): boolean {
|
|
123
|
+
const task = getTask(id);
|
|
124
|
+
if (!task) return false;
|
|
125
|
+
if (task.status === 'done' || task.status === 'failed') return false;
|
|
126
|
+
|
|
127
|
+
// Cancel monitor tasks
|
|
128
|
+
if (task.mode === 'monitor' && activeMonitors.has(id)) {
|
|
129
|
+
cancelMonitor(id);
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
updateTaskStatus(id, 'cancelled');
|
|
134
|
+
|
|
135
|
+
// Clean up project lock if this was a running prompt task
|
|
136
|
+
if (task.status === 'running') {
|
|
137
|
+
runningProjects.delete(task.projectName);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function deleteTask(id: string): boolean {
|
|
144
|
+
const task = getTask(id);
|
|
145
|
+
if (!task) return false;
|
|
146
|
+
if (task.status === 'running') cancelTask(id);
|
|
147
|
+
db().prepare('DELETE FROM tasks WHERE id = ?').run(id);
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function updateTask(id: string, updates: { prompt?: string; projectName?: string; projectPath?: string; priority?: number; scheduledAt?: string; restart?: boolean }): Task | null {
|
|
152
|
+
const task = getTask(id);
|
|
153
|
+
if (!task) return null;
|
|
154
|
+
|
|
155
|
+
// If running, cancel first
|
|
156
|
+
if (task.status === 'running') cancelTask(id);
|
|
157
|
+
|
|
158
|
+
const fields: string[] = [];
|
|
159
|
+
const values: any[] = [];
|
|
160
|
+
if (updates.prompt !== undefined) { fields.push('prompt = ?'); values.push(updates.prompt); }
|
|
161
|
+
if (updates.projectName !== undefined) { fields.push('project_name = ?'); values.push(updates.projectName); }
|
|
162
|
+
if (updates.projectPath !== undefined) { fields.push('project_path = ?'); values.push(updates.projectPath); }
|
|
163
|
+
if (updates.priority !== undefined) { fields.push('priority = ?'); values.push(updates.priority); }
|
|
164
|
+
if (updates.scheduledAt !== undefined) { fields.push('scheduled_at = ?'); values.push(updates.scheduledAt || null); }
|
|
165
|
+
|
|
166
|
+
// Reset to queued so it runs again
|
|
167
|
+
if (updates.restart) {
|
|
168
|
+
fields.push("status = 'queued'", 'started_at = NULL', 'completed_at = NULL', 'error = NULL', "log = '[]'", 'result_summary = NULL', 'git_diff = NULL', 'cost_usd = NULL');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (fields.length === 0) return task;
|
|
172
|
+
|
|
173
|
+
values.push(id);
|
|
174
|
+
db().prepare(`UPDATE tasks SET ${fields.join(', ')} WHERE id = ?`).run(...values);
|
|
175
|
+
|
|
176
|
+
if (updates.restart) ensureRunnerStarted();
|
|
177
|
+
|
|
178
|
+
return getTask(id);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function retryTask(id: string): Task | null {
|
|
182
|
+
const task = getTask(id);
|
|
183
|
+
if (!task) return null;
|
|
184
|
+
if (task.status !== 'failed' && task.status !== 'cancelled') return null;
|
|
185
|
+
|
|
186
|
+
// Create a new task with same params (including agent)
|
|
187
|
+
return createTask({
|
|
188
|
+
projectName: task.projectName,
|
|
189
|
+
projectPath: task.projectPath,
|
|
190
|
+
prompt: task.prompt,
|
|
191
|
+
priority: task.priority,
|
|
192
|
+
agent: (task as any).agent || undefined,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ─── Background Runner ───────────────────────────────────────
|
|
197
|
+
|
|
198
|
+
export function ensureRunnerStarted() {
|
|
199
|
+
if (runnerState.runner) return;
|
|
200
|
+
runnerState.runner = setInterval(processNextTask, 3000);
|
|
201
|
+
// Also try immediately
|
|
202
|
+
processNextTask();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function stopRunner() {
|
|
206
|
+
if (runnerState.runner) {
|
|
207
|
+
clearInterval(runnerState.runner);
|
|
208
|
+
runnerState.runner = null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function processNextTask() {
|
|
213
|
+
// Find all queued tasks ready to run
|
|
214
|
+
const queued = db().prepare(`
|
|
215
|
+
SELECT * FROM tasks WHERE status = 'queued'
|
|
216
|
+
AND (scheduled_at IS NULL OR replace(replace(scheduled_at, 'T', ' '), 'Z', '') <= datetime('now'))
|
|
217
|
+
ORDER BY priority DESC, created_at ASC
|
|
218
|
+
`).all() as any[];
|
|
219
|
+
|
|
220
|
+
for (const next of queued) {
|
|
221
|
+
const task = rowToTask(next);
|
|
222
|
+
|
|
223
|
+
if (task.mode === 'monitor') {
|
|
224
|
+
// Monitor tasks run in background, don't block the runner
|
|
225
|
+
startMonitorTask(task);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Skip if this project already has a running prompt task
|
|
230
|
+
if (runningProjects.has(task.projectName)) continue;
|
|
231
|
+
|
|
232
|
+
// Run this task
|
|
233
|
+
runningProjects.add(task.projectName);
|
|
234
|
+
runnerState.currentTaskId = task.id;
|
|
235
|
+
|
|
236
|
+
// Execute async — don't await so we can process tasks for other projects in parallel
|
|
237
|
+
executeTask(task)
|
|
238
|
+
.catch((err: any) => {
|
|
239
|
+
appendLog(task.id, { type: 'system', subtype: 'error', content: err.message, timestamp: new Date().toISOString() });
|
|
240
|
+
updateTaskStatus(task.id, 'failed', err.message);
|
|
241
|
+
})
|
|
242
|
+
.finally(() => {
|
|
243
|
+
runningProjects.delete(task.projectName);
|
|
244
|
+
if (runnerState.currentTaskId === task.id) runnerState.currentTaskId = null;
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function executeShellTask(task: Task): Promise<void> {
|
|
250
|
+
return new Promise((resolve) => {
|
|
251
|
+
updateTaskStatus(task.id, 'running');
|
|
252
|
+
db().prepare('UPDATE tasks SET started_at = datetime(\'now\') WHERE id = ?').run(task.id);
|
|
253
|
+
console.log(`[task:shell] ${task.projectName}: "${task.prompt.slice(0, 80)}"`);
|
|
254
|
+
|
|
255
|
+
const child = spawn('bash', ['-c', task.prompt], {
|
|
256
|
+
cwd: task.projectPath,
|
|
257
|
+
env: { ...process.env },
|
|
258
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
let stdout = '';
|
|
262
|
+
let stderr = '';
|
|
263
|
+
child.stdout.on('data', (chunk: Buffer) => {
|
|
264
|
+
const text = chunk.toString();
|
|
265
|
+
stdout += text;
|
|
266
|
+
appendLog(task.id, { type: 'system', subtype: 'text', content: text, timestamp: new Date().toISOString() });
|
|
267
|
+
});
|
|
268
|
+
child.stderr.on('data', (chunk: Buffer) => { stderr += chunk.toString(); });
|
|
269
|
+
|
|
270
|
+
child.on('exit', (code) => {
|
|
271
|
+
if (code === 0) {
|
|
272
|
+
db().prepare('UPDATE tasks SET status = ?, result_summary = ?, completed_at = datetime(\'now\') WHERE id = ?')
|
|
273
|
+
.run('done', stdout.trim(), task.id);
|
|
274
|
+
emit(task.id, 'status', 'done');
|
|
275
|
+
} else {
|
|
276
|
+
const errMsg = stderr.trim() || `Exit code ${code}`;
|
|
277
|
+
db().prepare('UPDATE tasks SET status = ?, error = ?, completed_at = datetime(\'now\') WHERE id = ?')
|
|
278
|
+
.run('failed', errMsg, task.id);
|
|
279
|
+
emit(task.id, 'status', 'failed');
|
|
280
|
+
}
|
|
281
|
+
resolve();
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function executeTask(task: Task): Promise<void> {
|
|
287
|
+
if (task.mode === 'shell') return executeShellTask(task);
|
|
288
|
+
|
|
289
|
+
return new Promise((resolve, reject) => {
|
|
290
|
+
const settings = loadSettings();
|
|
291
|
+
const { getAgent } = require('./agents');
|
|
292
|
+
const agentId = (task as any).agent || settings.defaultAgent || 'claude';
|
|
293
|
+
const adapter = getAgent(agentId);
|
|
294
|
+
|
|
295
|
+
// Model priority: per-task override > agent scene model > global taskModel
|
|
296
|
+
// "default" means "no override" — fall through to next level
|
|
297
|
+
const agentCfg = settings.agents?.[agentId];
|
|
298
|
+
const agentModel = agentCfg?.models?.task;
|
|
299
|
+
const effectiveAgentModel = agentModel && agentModel !== 'default' ? agentModel : null;
|
|
300
|
+
const model = taskModelOverrides.get(task.id) || effectiveAgentModel || settings.taskModel;
|
|
301
|
+
const supportsModel = adapter.config.capabilities?.supportsModel;
|
|
302
|
+
const spawnOpts = adapter.buildTaskSpawn({
|
|
303
|
+
projectPath: task.projectPath,
|
|
304
|
+
prompt: task.prompt,
|
|
305
|
+
model: supportsModel && model && model !== 'default' ? model : undefined,
|
|
306
|
+
conversationId: task.conversationId || undefined,
|
|
307
|
+
skipPermissions: true,
|
|
308
|
+
outputFormat: adapter.config.capabilities?.supportsStreamJson ? 'stream-json' : undefined,
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
const env = { ...process.env, ...(spawnOpts.env || {}) };
|
|
312
|
+
delete env.CLAUDECODE;
|
|
313
|
+
|
|
314
|
+
updateTaskStatus(task.id, 'running');
|
|
315
|
+
db().prepare('UPDATE tasks SET started_at = datetime(\'now\') WHERE id = ?').run(task.id);
|
|
316
|
+
|
|
317
|
+
const agentName = adapter.config.name || agentId;
|
|
318
|
+
console.log(`[task] ${task.projectName} [${agentName}${supportsModel && model ? '/' + model : ''}]: "${task.prompt.slice(0, 60)}..."`);
|
|
319
|
+
|
|
320
|
+
// Log agent info as first entry
|
|
321
|
+
appendLog(task.id, { type: 'system', subtype: 'init', content: `Agent: ${agentName}${supportsModel && model && model !== 'default' ? ` | Model: ${model}` : ''}`, timestamp: new Date().toISOString() });
|
|
322
|
+
|
|
323
|
+
const needsTTY = adapter.config.capabilities?.requiresTTY;
|
|
324
|
+
let child: any;
|
|
325
|
+
let ptyProcess: any = null;
|
|
326
|
+
|
|
327
|
+
if (needsTTY) {
|
|
328
|
+
// Use node-pty for agents that require a terminal environment
|
|
329
|
+
const pty = require('node-pty');
|
|
330
|
+
ptyProcess = pty.spawn(spawnOpts.cmd, spawnOpts.args, {
|
|
331
|
+
name: 'xterm-256color',
|
|
332
|
+
cols: 120,
|
|
333
|
+
rows: 40,
|
|
334
|
+
cwd: task.projectPath,
|
|
335
|
+
env,
|
|
336
|
+
});
|
|
337
|
+
// Strip terminal control codes from PTY output for clean logging
|
|
338
|
+
const stripAnsi = (s: string) => s
|
|
339
|
+
.replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, '') // CSI sequences
|
|
340
|
+
.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, '') // OSC sequences
|
|
341
|
+
.replace(/\x1b[()][0-9A-B]/g, '') // charset
|
|
342
|
+
.replace(/\x1b[=>]/g, '') // keypad
|
|
343
|
+
.replace(/\r/g, '') // carriage return
|
|
344
|
+
.replace(/\x07/g, ''); // bell
|
|
345
|
+
|
|
346
|
+
// Auto-kill PTY after idle (interactive agents don't exit on their own)
|
|
347
|
+
let ptyBytes = 0;
|
|
348
|
+
let ptyIdleTimer: any = null;
|
|
349
|
+
const PTY_IDLE_MS = 15000; // 15s idle = done
|
|
350
|
+
|
|
351
|
+
// Create a child-like interface for pty
|
|
352
|
+
let exitCb: Function | null = null;
|
|
353
|
+
|
|
354
|
+
ptyProcess.onData((data: string) => {
|
|
355
|
+
const clean = stripAnsi(data);
|
|
356
|
+
ptyBytes += clean.length;
|
|
357
|
+
if (dataCallback) dataCallback(Buffer.from(clean));
|
|
358
|
+
// Reset idle timer
|
|
359
|
+
if (ptyIdleTimer) clearTimeout(ptyIdleTimer);
|
|
360
|
+
if (ptyBytes > 500) {
|
|
361
|
+
ptyIdleTimer = setTimeout(() => {
|
|
362
|
+
console.log(`[task] PTY idle timeout — killing process (${ptyBytes} bytes received)`);
|
|
363
|
+
try { ptyProcess.kill(); } catch {}
|
|
364
|
+
}, PTY_IDLE_MS);
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
ptyProcess.onExit(({ exitCode }: any) => {
|
|
369
|
+
if (ptyIdleTimer) clearTimeout(ptyIdleTimer);
|
|
370
|
+
if (exitCb) exitCb(exitCode, null);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
let dataCallback: Function | null = null;
|
|
374
|
+
child = {
|
|
375
|
+
stdout: { on: (evt: string, cb: Function) => { if (evt === 'data') dataCallback = cb; } },
|
|
376
|
+
stderr: { on: (_evt: string, _cb: Function) => {} },
|
|
377
|
+
on: (evt: string, cb: Function) => { if (evt === 'exit') exitCb = cb; if (evt === 'error') {} },
|
|
378
|
+
kill: (sig: string) => { if (ptyIdleTimer) clearTimeout(ptyIdleTimer); try { ptyProcess.kill(sig); } catch {} },
|
|
379
|
+
stdin: null,
|
|
380
|
+
pid: ptyProcess.pid,
|
|
381
|
+
};
|
|
382
|
+
} else {
|
|
383
|
+
child = spawn(spawnOpts.cmd, spawnOpts.args, {
|
|
384
|
+
cwd: task.projectPath,
|
|
385
|
+
env,
|
|
386
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
387
|
+
});
|
|
388
|
+
child.stdin?.end();
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
let buffer = '';
|
|
392
|
+
let resultText = '';
|
|
393
|
+
let totalCost = 0;
|
|
394
|
+
let sessionId = '';
|
|
395
|
+
let modelUsed = '';
|
|
396
|
+
let totalInputTokens = 0;
|
|
397
|
+
let totalOutputTokens = 0;
|
|
398
|
+
|
|
399
|
+
child.on('error', (err: any) => {
|
|
400
|
+
console.error(`[task-runner] Spawn error:`, err.message);
|
|
401
|
+
updateTaskStatus(task.id, 'failed', err.message);
|
|
402
|
+
reject(err);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
child.stdout?.on('data', (data: Buffer) => {
|
|
406
|
+
// stdout chunk processing (silent)
|
|
407
|
+
|
|
408
|
+
// Check if cancelled
|
|
409
|
+
if (getTask(task.id)?.status === 'cancelled') {
|
|
410
|
+
child.kill('SIGTERM');
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
buffer += data.toString();
|
|
415
|
+
const lines = buffer.split('\n');
|
|
416
|
+
buffer = lines.pop() || '';
|
|
417
|
+
|
|
418
|
+
for (const line of lines) {
|
|
419
|
+
if (!line.trim()) continue;
|
|
420
|
+
let jsonParsed = false;
|
|
421
|
+
try {
|
|
422
|
+
const parsed = JSON.parse(line);
|
|
423
|
+
jsonParsed = true;
|
|
424
|
+
const entries = parseStreamJson(parsed);
|
|
425
|
+
for (const entry of entries) {
|
|
426
|
+
// Skip Claude's Model init line for non-claude agents (we already logged our own)
|
|
427
|
+
if (entry.subtype === 'init' && agentId !== 'claude' && entry.content?.startsWith('Model:')) continue;
|
|
428
|
+
appendLog(task.id, entry);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (parsed.session_id) sessionId = parsed.session_id;
|
|
432
|
+
if (parsed.type === 'system' && parsed.subtype === 'init' && parsed.model) {
|
|
433
|
+
modelUsed = parsed.model;
|
|
434
|
+
}
|
|
435
|
+
// Accumulate token usage from assistant messages
|
|
436
|
+
if (parsed.type === 'assistant' && parsed.message?.usage) {
|
|
437
|
+
totalInputTokens += parsed.message.usage.input_tokens || 0;
|
|
438
|
+
totalOutputTokens += parsed.message.usage.output_tokens || 0;
|
|
439
|
+
}
|
|
440
|
+
if (parsed.type === 'result') {
|
|
441
|
+
resultText = typeof parsed.result === 'string' ? parsed.result : JSON.stringify(parsed.result);
|
|
442
|
+
totalCost = parsed.total_cost_usd || 0;
|
|
443
|
+
if (parsed.total_input_tokens) totalInputTokens = parsed.total_input_tokens;
|
|
444
|
+
if (parsed.total_output_tokens) totalOutputTokens = parsed.total_output_tokens;
|
|
445
|
+
}
|
|
446
|
+
} catch {
|
|
447
|
+
// Non-JSON output (generic agents) — log as raw text
|
|
448
|
+
if (!jsonParsed) {
|
|
449
|
+
resultText += (resultText ? '\n' : '') + line;
|
|
450
|
+
appendLog(task.id, { type: 'system', subtype: 'text', content: line, timestamp: new Date().toISOString() });
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
child.stderr?.on('data', (data: Buffer) => {
|
|
457
|
+
const text = data.toString().trim();
|
|
458
|
+
// stderr logged to task log only
|
|
459
|
+
if (text) {
|
|
460
|
+
appendLog(task.id, { type: 'system', subtype: 'error', content: text, timestamp: new Date().toISOString() });
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
child.on('exit', (code: any, signal: any) => {
|
|
465
|
+
// Process exit handled below
|
|
466
|
+
// Process remaining buffer
|
|
467
|
+
if (buffer.trim()) {
|
|
468
|
+
try {
|
|
469
|
+
const parsed = JSON.parse(buffer);
|
|
470
|
+
const entries = parseStreamJson(parsed);
|
|
471
|
+
for (const entry of entries) appendLog(task.id, entry);
|
|
472
|
+
if (parsed.type === 'result') {
|
|
473
|
+
resultText = typeof parsed.result === 'string' ? parsed.result : JSON.stringify(parsed.result);
|
|
474
|
+
totalCost = parsed.total_cost_usd || 0;
|
|
475
|
+
}
|
|
476
|
+
} catch {}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Save conversation ID for follow-up
|
|
480
|
+
if (sessionId) {
|
|
481
|
+
db().prepare('UPDATE tasks SET conversation_id = ? WHERE id = ?').run(sessionId, task.id);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Capture git diff
|
|
485
|
+
try {
|
|
486
|
+
const { execSync } = require('node:child_process');
|
|
487
|
+
const diff = execSync('git diff HEAD', { cwd: task.projectPath, timeout: 5000 }).toString();
|
|
488
|
+
if (diff.trim()) {
|
|
489
|
+
db().prepare('UPDATE tasks SET git_diff = ? WHERE id = ?').run(diff, task.id);
|
|
490
|
+
}
|
|
491
|
+
} catch {}
|
|
492
|
+
|
|
493
|
+
const currentStatus = getTask(task.id)?.status;
|
|
494
|
+
if (currentStatus === 'cancelled') {
|
|
495
|
+
resolve();
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (code === 0) {
|
|
500
|
+
db().prepare(`
|
|
501
|
+
UPDATE tasks SET status = 'done', result_summary = ?, cost_usd = ?, completed_at = datetime('now')
|
|
502
|
+
WHERE id = ?
|
|
503
|
+
`).run(resultText, totalCost, task.id);
|
|
504
|
+
emit(task.id, 'status', 'done');
|
|
505
|
+
console.log(`[task] Done: ${task.id} ${task.projectName} (cost: $${totalCost?.toFixed(4) || '0'}, ${totalInputTokens}in/${totalOutputTokens}out)`);
|
|
506
|
+
// Record usage
|
|
507
|
+
try {
|
|
508
|
+
const { recordUsage } = require('./usage-scanner');
|
|
509
|
+
let isPipeline = false;
|
|
510
|
+
try { const { pipelineTaskIds: ptids } = require('./pipeline'); isPipeline = ptids.has(task.id); } catch {}
|
|
511
|
+
recordUsage({
|
|
512
|
+
sessionId: sessionId || task.id,
|
|
513
|
+
source: isPipeline ? 'pipeline' : 'task',
|
|
514
|
+
projectPath: task.projectPath,
|
|
515
|
+
projectName: task.projectName,
|
|
516
|
+
model: modelUsed || 'unknown',
|
|
517
|
+
inputTokens: totalInputTokens,
|
|
518
|
+
outputTokens: totalOutputTokens,
|
|
519
|
+
taskId: task.id,
|
|
520
|
+
});
|
|
521
|
+
} catch {}
|
|
522
|
+
const doneTask = getTask(task.id);
|
|
523
|
+
if (doneTask) notifyTaskComplete(doneTask).catch(() => {});
|
|
524
|
+
notifyTerminalSession(task, 'done', sessionId);
|
|
525
|
+
resolve();
|
|
526
|
+
} else {
|
|
527
|
+
const errMsg = `Process exited with code ${code}`;
|
|
528
|
+
console.error(`[task] Failed: ${task.id} ${task.projectName} — ${errMsg}`);
|
|
529
|
+
updateTaskStatus(task.id, 'failed', errMsg);
|
|
530
|
+
const failedTask = getTask(task.id);
|
|
531
|
+
if (failedTask) notifyTaskFailed(failedTask).catch(() => {});
|
|
532
|
+
notifyTerminalSession(task, 'failed', sessionId);
|
|
533
|
+
reject(new Error(errMsg));
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
child.on('error', (err: any) => {
|
|
538
|
+
updateTaskStatus(task.id, 'failed', err.message);
|
|
539
|
+
reject(err);
|
|
540
|
+
});
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// ─── Terminal notification ────────────────────────────────────
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Notify tmux terminal sessions in the same project directory that a task completed.
|
|
548
|
+
* Sends a visible bell character so the user knows to resume.
|
|
549
|
+
*/
|
|
550
|
+
function notifyTerminalSession(task: Task, status: 'done' | 'failed', sessionId?: string) {
|
|
551
|
+
// Skip pipeline tasks — they have their own notification system
|
|
552
|
+
try {
|
|
553
|
+
const { pipelineTaskIds } = require('./pipeline');
|
|
554
|
+
if (pipelineTaskIds.has(task.id)) return;
|
|
555
|
+
} catch {}
|
|
556
|
+
|
|
557
|
+
try {
|
|
558
|
+
const out = execSync(
|
|
559
|
+
`tmux list-sessions -F "#{session_name}" 2>/dev/null`,
|
|
560
|
+
{ encoding: 'utf-8', timeout: 3000 }
|
|
561
|
+
).trim();
|
|
562
|
+
if (!out) return;
|
|
563
|
+
|
|
564
|
+
for (const name of out.split('\n')) {
|
|
565
|
+
if (!name.startsWith('mw-')) continue;
|
|
566
|
+
try {
|
|
567
|
+
const cwd = execSync(
|
|
568
|
+
`tmux display-message -p -t ${name} '#{pane_current_path}'`,
|
|
569
|
+
{ encoding: 'utf-8', timeout: 2000 }
|
|
570
|
+
).trim();
|
|
571
|
+
|
|
572
|
+
// Match: same dir, parent dir, or child dir
|
|
573
|
+
const match = cwd && (
|
|
574
|
+
cwd === task.projectPath ||
|
|
575
|
+
cwd.startsWith(task.projectPath + '/') ||
|
|
576
|
+
task.projectPath.startsWith(cwd + '/')
|
|
577
|
+
);
|
|
578
|
+
if (!match) continue;
|
|
579
|
+
|
|
580
|
+
const paneCmd = execSync(
|
|
581
|
+
`tmux display-message -p -t ${name} '#{pane_current_command}'`,
|
|
582
|
+
{ encoding: 'utf-8', timeout: 2000 }
|
|
583
|
+
).trim();
|
|
584
|
+
|
|
585
|
+
if (status === 'done') {
|
|
586
|
+
const summary = task.prompt.slice(0, 80).replace(/"/g, "'");
|
|
587
|
+
const msg = `A background task just completed. Task: "${summary}". Please check git diff and continue.`;
|
|
588
|
+
|
|
589
|
+
// If a process is running (claude/node), send as input
|
|
590
|
+
if (paneCmd !== 'zsh' && paneCmd !== 'bash' && paneCmd !== 'fish') {
|
|
591
|
+
execSync(`tmux send-keys -t ${name} -- "${msg.replace(/"/g, '\\"')}" Enter`, { timeout: 2000 });
|
|
592
|
+
} else {
|
|
593
|
+
execSync(`tmux display-message -t ${name} "✅ Task ${task.id} done — changes ready"`, { timeout: 2000 });
|
|
594
|
+
}
|
|
595
|
+
} else {
|
|
596
|
+
execSync(`tmux display-message -t ${name} "❌ Task ${task.id} failed"`, { timeout: 2000 });
|
|
597
|
+
}
|
|
598
|
+
} catch {}
|
|
599
|
+
}
|
|
600
|
+
} catch {}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// ─── Helpers ─────────────────────────────────────────────────
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Resolve the claude binary path. `claude` is typically a symlink to a .js file,
|
|
607
|
+
* which can't be spawned directly without a shell. We resolve to the real .js path
|
|
608
|
+
* and run it with `node`.
|
|
609
|
+
*/
|
|
610
|
+
function resolveClaudePath(claudePath: string): { cmd: string; prefix: string[] } {
|
|
611
|
+
try {
|
|
612
|
+
// Try to find the real path
|
|
613
|
+
let resolved = claudePath;
|
|
614
|
+
try {
|
|
615
|
+
const which = execSync(`which ${claudePath}`, { encoding: 'utf-8' }).trim();
|
|
616
|
+
resolved = realpathSync(which);
|
|
617
|
+
} catch {
|
|
618
|
+
resolved = realpathSync(claudePath);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// If it's a .js file, run with node
|
|
622
|
+
if (resolved.endsWith('.js') || resolved.endsWith('.mjs')) {
|
|
623
|
+
return { cmd: process.execPath, prefix: [resolved] };
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
return { cmd: resolved, prefix: [] };
|
|
627
|
+
} catch {
|
|
628
|
+
// Fallback: use node to run it
|
|
629
|
+
return { cmd: process.execPath, prefix: [claudePath] };
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function parseStreamJson(parsed: any): TaskLogEntry[] {
|
|
634
|
+
const entries: TaskLogEntry[] = [];
|
|
635
|
+
const ts = new Date().toISOString();
|
|
636
|
+
|
|
637
|
+
if (parsed.type === 'system' && parsed.subtype === 'init') {
|
|
638
|
+
entries.push({ type: 'system', subtype: 'init', content: `Model: ${parsed.model || 'unknown'}`, timestamp: ts });
|
|
639
|
+
return entries;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
if (parsed.type === 'assistant' && parsed.message?.content) {
|
|
643
|
+
for (const block of parsed.message.content) {
|
|
644
|
+
if (block.type === 'text' && block.text) {
|
|
645
|
+
entries.push({ type: 'assistant', subtype: 'text', content: block.text, timestamp: ts });
|
|
646
|
+
} else if (block.type === 'tool_use') {
|
|
647
|
+
entries.push({
|
|
648
|
+
type: 'assistant',
|
|
649
|
+
subtype: 'tool_use',
|
|
650
|
+
content: typeof block.input === 'string' ? block.input : JSON.stringify(block.input || {}),
|
|
651
|
+
tool: block.name,
|
|
652
|
+
timestamp: ts,
|
|
653
|
+
});
|
|
654
|
+
} else if (block.type === 'tool_result') {
|
|
655
|
+
entries.push({
|
|
656
|
+
type: 'assistant',
|
|
657
|
+
subtype: 'tool_result',
|
|
658
|
+
content: typeof block.content === 'string' ? block.content : JSON.stringify(block.content || ''),
|
|
659
|
+
timestamp: ts,
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
return entries;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (parsed.type === 'result') {
|
|
667
|
+
entries.push({
|
|
668
|
+
type: 'result',
|
|
669
|
+
subtype: parsed.subtype || 'success',
|
|
670
|
+
content: typeof parsed.result === 'string' ? parsed.result : JSON.stringify(parsed.result || ''),
|
|
671
|
+
timestamp: ts,
|
|
672
|
+
});
|
|
673
|
+
return entries;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
if (parsed.type === 'rate_limit_event') return entries;
|
|
677
|
+
|
|
678
|
+
entries.push({ type: 'assistant', subtype: parsed.type || 'unknown', content: JSON.stringify(parsed), timestamp: ts });
|
|
679
|
+
return entries;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function appendLog(taskId: string, entry: TaskLogEntry) {
|
|
683
|
+
const row = db().prepare('SELECT log FROM tasks WHERE id = ?').get(taskId) as any;
|
|
684
|
+
if (!row) return;
|
|
685
|
+
const log: TaskLogEntry[] = JSON.parse(row.log);
|
|
686
|
+
log.push(entry);
|
|
687
|
+
db().prepare('UPDATE tasks SET log = ? WHERE id = ?').run(JSON.stringify(log), taskId);
|
|
688
|
+
emit(taskId, 'log', entry);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function updateTaskStatus(id: string, status: TaskStatus, error?: string) {
|
|
692
|
+
if (status === 'failed' || status === 'cancelled') {
|
|
693
|
+
db().prepare('UPDATE tasks SET status = ?, error = ?, completed_at = datetime(\'now\') WHERE id = ?').run(status, error || null, id);
|
|
694
|
+
} else {
|
|
695
|
+
db().prepare('UPDATE tasks SET status = ? WHERE id = ?').run(status, id);
|
|
696
|
+
}
|
|
697
|
+
emit(id, 'status', status);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function rowToTask(row: any): Task {
|
|
701
|
+
return {
|
|
702
|
+
id: row.id,
|
|
703
|
+
projectName: row.project_name,
|
|
704
|
+
projectPath: row.project_path,
|
|
705
|
+
prompt: row.prompt,
|
|
706
|
+
mode: row.mode || 'prompt',
|
|
707
|
+
status: row.status,
|
|
708
|
+
priority: row.priority,
|
|
709
|
+
conversationId: row.conversation_id || undefined,
|
|
710
|
+
watchConfig: row.watch_config ? JSON.parse(row.watch_config) : undefined,
|
|
711
|
+
log: JSON.parse(row.log || '[]'),
|
|
712
|
+
resultSummary: row.result_summary || undefined,
|
|
713
|
+
gitDiff: row.git_diff || undefined,
|
|
714
|
+
gitBranch: row.git_branch || undefined,
|
|
715
|
+
costUSD: row.cost_usd || undefined,
|
|
716
|
+
error: row.error || undefined,
|
|
717
|
+
createdAt: toIsoUTC(row.created_at) ?? row.created_at,
|
|
718
|
+
startedAt: toIsoUTC(row.started_at) ?? undefined,
|
|
719
|
+
completedAt: toIsoUTC(row.completed_at) ?? undefined,
|
|
720
|
+
scheduledAt: toIsoUTC(row.scheduled_at) ?? undefined,
|
|
721
|
+
agent: row.agent || undefined,
|
|
722
|
+
} as Task;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// ─── Monitor task execution ──────────────────────────────────
|
|
726
|
+
|
|
727
|
+
import { getSessionFilePath, readSessionEntries, tailSessionFile, type SessionEntry } from './claude-sessions';
|
|
728
|
+
|
|
729
|
+
const activeMonitors = new Map<string, () => void>(); // taskId → cleanup fn
|
|
730
|
+
|
|
731
|
+
function startMonitorTask(task: Task) {
|
|
732
|
+
if (!task.conversationId || !task.watchConfig) {
|
|
733
|
+
updateTaskStatus(task.id, 'failed', 'Monitor task requires a session and watch config');
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const config = task.watchConfig;
|
|
738
|
+
const fp = getSessionFilePath(task.projectName, task.conversationId);
|
|
739
|
+
if (!fp) {
|
|
740
|
+
updateTaskStatus(task.id, 'failed', `Session file not found: ${task.conversationId}`);
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
console.log(`[monitor] Starting monitor ${task.id} for ${task.projectName}/${task.conversationId.slice(0, 8)} — condition: ${config.condition}, action: ${config.action}, file: ${fp}`);
|
|
745
|
+
|
|
746
|
+
updateTaskStatus(task.id, 'running');
|
|
747
|
+
appendLog(task.id, {
|
|
748
|
+
type: 'system', subtype: 'init',
|
|
749
|
+
content: `Monitoring session ${task.conversationId.slice(0, 12)} — condition: ${config.condition}, action: ${config.action}`,
|
|
750
|
+
timestamp: new Date().toISOString(),
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
// Read initial state
|
|
754
|
+
const initialEntries = readSessionEntries(fp);
|
|
755
|
+
let lastEntryCount = initialEntries.length;
|
|
756
|
+
let lastActivityTime = Date.now();
|
|
757
|
+
|
|
758
|
+
// Idle check timer
|
|
759
|
+
let idleTimer: ReturnType<typeof setInterval> | null = null;
|
|
760
|
+
if (config.condition === 'idle') {
|
|
761
|
+
const idleMs = (config.idleMinutes || 10) * 60_000;
|
|
762
|
+
idleTimer = setInterval(() => {
|
|
763
|
+
if (Date.now() - lastActivityTime > idleMs) {
|
|
764
|
+
triggerMonitorAction(task, `Session idle for ${config.idleMinutes || 10} minutes`);
|
|
765
|
+
if (!config.repeat) stopMonitor(task.id);
|
|
766
|
+
}
|
|
767
|
+
}, 30_000);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// Notification throttling: batch updates and send at most once per interval
|
|
771
|
+
const notifyInterval = (config.notifyIntervalSeconds || 60) * 1000;
|
|
772
|
+
let lastNotifyTime = 0;
|
|
773
|
+
let pendingContext: string[] = [];
|
|
774
|
+
let notifyTimer: ReturnType<typeof setTimeout> | null = null;
|
|
775
|
+
|
|
776
|
+
function scheduleNotify(context: string, immediate?: boolean) {
|
|
777
|
+
pendingContext.push(context);
|
|
778
|
+
if (immediate) {
|
|
779
|
+
flushNotify();
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
if (notifyTimer) return; // already scheduled
|
|
783
|
+
const elapsed = Date.now() - lastNotifyTime;
|
|
784
|
+
const delay = Math.max(0, notifyInterval - elapsed);
|
|
785
|
+
notifyTimer = setTimeout(flushNotify, delay);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function flushNotify() {
|
|
789
|
+
if (notifyTimer) { clearTimeout(notifyTimer); notifyTimer = null; }
|
|
790
|
+
if (pendingContext.length === 0) return;
|
|
791
|
+
const summary = pendingContext.length === 1
|
|
792
|
+
? pendingContext[0]
|
|
793
|
+
: `${pendingContext.length} updates:\n\n${pendingContext.slice(-5).join('\n\n')}`;
|
|
794
|
+
pendingContext = [];
|
|
795
|
+
lastNotifyTime = Date.now();
|
|
796
|
+
triggerMonitorAction(task, summary);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// Tail the file for changes (uses fs.watch + 5s polling fallback)
|
|
800
|
+
const stopTail = tailSessionFile(fp, (newEntries) => {
|
|
801
|
+
lastActivityTime = Date.now();
|
|
802
|
+
lastEntryCount += newEntries.length;
|
|
803
|
+
|
|
804
|
+
// Check conditions
|
|
805
|
+
if (config.condition === 'change') {
|
|
806
|
+
scheduleNotify(summarizeNewEntries(newEntries));
|
|
807
|
+
if (!config.repeat) stopMonitor(task.id);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
if (config.condition === 'keyword' && config.keyword) {
|
|
811
|
+
const kw = config.keyword.toLowerCase();
|
|
812
|
+
const matched = newEntries.find(e => e.content.toLowerCase().includes(kw));
|
|
813
|
+
if (matched) {
|
|
814
|
+
scheduleNotify(`Keyword "${config.keyword}" found: ${matched.content.slice(0, 200)}`, true);
|
|
815
|
+
if (!config.repeat) stopMonitor(task.id);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
if (config.condition === 'error') {
|
|
820
|
+
const errors = newEntries.filter(e =>
|
|
821
|
+
e.type === 'system' && e.content.toLowerCase().includes('error')
|
|
822
|
+
);
|
|
823
|
+
if (errors.length > 0) {
|
|
824
|
+
scheduleNotify(`Error detected: ${errors[0].content.slice(0, 200)}`, true);
|
|
825
|
+
if (!config.repeat) stopMonitor(task.id);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
if (config.condition === 'complete') {
|
|
830
|
+
// Check if last assistant entry looks like completion
|
|
831
|
+
const lastAssistant = [...newEntries].reverse().find(e => e.type === 'assistant_text');
|
|
832
|
+
if (lastAssistant) {
|
|
833
|
+
// Heuristic: check if there are no more tool calls after the last text
|
|
834
|
+
const lastIdx = newEntries.lastIndexOf(lastAssistant);
|
|
835
|
+
const afterToolUse = newEntries.slice(lastIdx + 1).some(e => e.type === 'tool_use');
|
|
836
|
+
if (!afterToolUse && newEntries.length > 2) {
|
|
837
|
+
// Wait a bit to see if more entries come
|
|
838
|
+
setTimeout(() => {
|
|
839
|
+
if (Date.now() - lastActivityTime > 30_000) {
|
|
840
|
+
scheduleNotify(`Session appears complete.\n\nLast: ${lastAssistant.content.slice(0, 300)}`, true);
|
|
841
|
+
if (!config.repeat) stopMonitor(task.id);
|
|
842
|
+
}
|
|
843
|
+
}, 35_000);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}, (err) => {
|
|
848
|
+
console.error(`[monitor] ${task.id} tail error:`, err.message);
|
|
849
|
+
appendLog(task.id, {
|
|
850
|
+
type: 'system', subtype: 'error',
|
|
851
|
+
content: `File watch error: ${err.message}`,
|
|
852
|
+
timestamp: new Date().toISOString(),
|
|
853
|
+
});
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
const cleanup = () => {
|
|
857
|
+
stopTail();
|
|
858
|
+
if (idleTimer) clearInterval(idleTimer);
|
|
859
|
+
flushNotify(); // send any remaining batched notifications
|
|
860
|
+
};
|
|
861
|
+
|
|
862
|
+
activeMonitors.set(task.id, cleanup);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function stopMonitor(taskId: string) {
|
|
866
|
+
const cleanup = activeMonitors.get(taskId);
|
|
867
|
+
if (cleanup) {
|
|
868
|
+
cleanup();
|
|
869
|
+
activeMonitors.delete(taskId);
|
|
870
|
+
}
|
|
871
|
+
updateTaskStatus(taskId, 'done');
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// Also export for cancel
|
|
875
|
+
export function cancelMonitor(taskId: string) {
|
|
876
|
+
stopMonitor(taskId);
|
|
877
|
+
updateTaskStatus(taskId, 'cancelled');
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
async function triggerMonitorAction(task: Task, context: string) {
|
|
881
|
+
const config = task.watchConfig!;
|
|
882
|
+
|
|
883
|
+
appendLog(task.id, {
|
|
884
|
+
type: 'system', subtype: 'text',
|
|
885
|
+
content: `⚡ Triggered: ${context}`,
|
|
886
|
+
timestamp: new Date().toISOString(),
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
if (config.action === 'notify') {
|
|
890
|
+
// Send Telegram notification
|
|
891
|
+
const settings = loadSettings();
|
|
892
|
+
if (settings.telegramBotToken && settings.telegramChatId) {
|
|
893
|
+
const msg = config.actionPrompt
|
|
894
|
+
? config.actionPrompt.replace('{{context}}', context)
|
|
895
|
+
: `📋 Monitor: ${task.projectName}/${task.conversationId?.slice(0, 8)}\n\n${context}`;
|
|
896
|
+
await sendTelegramDirect(settings.telegramBotToken, settings.telegramChatId, msg);
|
|
897
|
+
}
|
|
898
|
+
} else if (config.action === 'message' && config.actionPrompt && task.conversationId) {
|
|
899
|
+
// Send a message to the session by creating a prompt task (will queue if project is busy)
|
|
900
|
+
const newTask = createTask({
|
|
901
|
+
projectName: task.projectName,
|
|
902
|
+
projectPath: task.projectPath,
|
|
903
|
+
prompt: config.actionPrompt,
|
|
904
|
+
conversationId: task.conversationId,
|
|
905
|
+
});
|
|
906
|
+
const queued = runningProjects.has(task.projectName) ? ' (queued — project busy)' : '';
|
|
907
|
+
appendLog(task.id, {
|
|
908
|
+
type: 'system', subtype: 'text',
|
|
909
|
+
content: `Created follow-up task ${newTask.id}${queued}: ${config.actionPrompt.slice(0, 100)}`,
|
|
910
|
+
timestamp: new Date().toISOString(),
|
|
911
|
+
});
|
|
912
|
+
} else if (config.action === 'task' && config.actionPrompt) {
|
|
913
|
+
const project = config.actionProject || task.projectName;
|
|
914
|
+
createTask({
|
|
915
|
+
projectName: project,
|
|
916
|
+
projectPath: task.projectPath,
|
|
917
|
+
prompt: config.actionPrompt,
|
|
918
|
+
});
|
|
919
|
+
appendLog(task.id, {
|
|
920
|
+
type: 'system', subtype: 'text',
|
|
921
|
+
content: `Created new task for ${project}: ${config.actionPrompt.slice(0, 100)}`,
|
|
922
|
+
timestamp: new Date().toISOString(),
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
async function sendTelegramDirect(token: string, chatId: string, text: string) {
|
|
928
|
+
try {
|
|
929
|
+
const res = await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
|
|
930
|
+
method: 'POST',
|
|
931
|
+
headers: { 'Content-Type': 'application/json' },
|
|
932
|
+
body: JSON.stringify({ chat_id: chatId, text, disable_web_page_preview: true }),
|
|
933
|
+
});
|
|
934
|
+
if (!res.ok) {
|
|
935
|
+
const body = await res.text();
|
|
936
|
+
console.error(`[monitor] Telegram send failed: ${res.status} ${body}`);
|
|
937
|
+
}
|
|
938
|
+
} catch (err) {
|
|
939
|
+
console.error('[monitor] Telegram send error:', err);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
function summarizeNewEntries(entries: SessionEntry[]): string {
|
|
944
|
+
const parts: string[] = [];
|
|
945
|
+
for (const e of entries) {
|
|
946
|
+
if (e.type === 'user') parts.push(`👤 ${e.content.slice(0, 100)}`);
|
|
947
|
+
else if (e.type === 'assistant_text') parts.push(`🤖 ${e.content.slice(0, 150)}`);
|
|
948
|
+
else if (e.type === 'tool_use') parts.push(`🔧 ${e.toolName || 'tool'}`);
|
|
949
|
+
}
|
|
950
|
+
return parts.slice(0, 5).join('\n') || 'Activity detected';
|
|
951
|
+
}
|