@aion0/forge 0.5.26 → 0.5.28
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 +10 -29
- package/app/api/terminal-bell/route.ts +6 -2
- package/app/api/terminal-cwd/route.ts +7 -4
- package/components/CodeViewer.tsx +3 -31
- package/components/Dashboard.tsx +34 -20
- package/components/WebTerminal.tsx +36 -2
- package/lib/terminal-standalone.ts +19 -2
- package/package.json +1 -1
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* Standalone Telegram bot process.
|
|
4
|
+
* Runs as a single process — no duplication from Next.js workers.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { loadSettings } from './settings';
|
|
8
|
+
|
|
9
|
+
const settings = loadSettings();
|
|
10
|
+
if (!settings.telegramBotToken || !settings.telegramChatId) {
|
|
11
|
+
console.log('[telegram] No token or chatId configured, exiting');
|
|
12
|
+
process.exit(0);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const TOKEN = settings.telegramBotToken;
|
|
16
|
+
const ALLOWED_IDS = settings.telegramChatId.split(',').map(s => s.trim()).filter(Boolean);
|
|
17
|
+
let lastUpdateId = 0;
|
|
18
|
+
let polling = true;
|
|
19
|
+
const processedMsgIds = new Set<number>();
|
|
20
|
+
|
|
21
|
+
// Skip stale messages on startup
|
|
22
|
+
async function init() {
|
|
23
|
+
try {
|
|
24
|
+
const res = await fetch(`https://api.telegram.org/bot${TOKEN}/getUpdates?offset=-1`);
|
|
25
|
+
const data = await res.json();
|
|
26
|
+
if (data.ok && data.result?.length > 0) {
|
|
27
|
+
lastUpdateId = data.result[data.result.length - 1].update_id;
|
|
28
|
+
}
|
|
29
|
+
} catch {}
|
|
30
|
+
console.log('[telegram] Bot started (standalone)');
|
|
31
|
+
poll();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function poll() {
|
|
35
|
+
if (!polling) return;
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const controller = new AbortController();
|
|
39
|
+
const timeout = setTimeout(() => controller.abort(), 35000);
|
|
40
|
+
|
|
41
|
+
const res = await fetch(
|
|
42
|
+
`https://api.telegram.org/bot${TOKEN}/getUpdates?offset=${lastUpdateId + 1}&timeout=30`,
|
|
43
|
+
{ signal: controller.signal }
|
|
44
|
+
);
|
|
45
|
+
clearTimeout(timeout);
|
|
46
|
+
|
|
47
|
+
const data = await res.json();
|
|
48
|
+
if (data.ok && data.result) {
|
|
49
|
+
for (const update of data.result) {
|
|
50
|
+
if (update.update_id <= lastUpdateId) continue;
|
|
51
|
+
lastUpdateId = update.update_id;
|
|
52
|
+
|
|
53
|
+
if (update.message?.text) {
|
|
54
|
+
const msgId = update.message.message_id;
|
|
55
|
+
if (processedMsgIds.has(msgId)) continue;
|
|
56
|
+
processedMsgIds.add(msgId);
|
|
57
|
+
if (processedMsgIds.size > 200) {
|
|
58
|
+
const oldest = [...processedMsgIds].slice(0, 100);
|
|
59
|
+
oldest.forEach(id => processedMsgIds.delete(id));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Forward to Next.js API for processing
|
|
63
|
+
try {
|
|
64
|
+
await fetch(`http://localhost:${process.env.PORT || 8403}/api/telegram`, {
|
|
65
|
+
method: 'POST',
|
|
66
|
+
headers: { 'Content-Type': 'application/json', 'x-telegram-secret': TOKEN },
|
|
67
|
+
body: JSON.stringify(update.message),
|
|
68
|
+
});
|
|
69
|
+
} catch {}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
} catch {
|
|
74
|
+
// Network error — silent retry
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
setTimeout(poll, 1000);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
process.on('SIGTERM', () => { polling = false; process.exit(0); });
|
|
81
|
+
process.on('SIGINT', () => { polling = false; process.exit(0); });
|
|
82
|
+
|
|
83
|
+
init();
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal Server — standalone WebSocket PTY server.
|
|
3
|
+
* Runs on port 8404 alongside the Next.js server on 8403.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { WebSocketServer, WebSocket } from 'ws';
|
|
7
|
+
import * as pty from 'node-pty';
|
|
8
|
+
import { homedir } from 'node:os';
|
|
9
|
+
|
|
10
|
+
let wss: WebSocketServer | null = null;
|
|
11
|
+
|
|
12
|
+
export function startTerminalServer(port = 8404) {
|
|
13
|
+
if (wss) return;
|
|
14
|
+
|
|
15
|
+
wss = new WebSocketServer({ port });
|
|
16
|
+
console.log(`[terminal] WebSocket server on ws://localhost:${port}`);
|
|
17
|
+
|
|
18
|
+
wss.on('connection', (ws: WebSocket) => {
|
|
19
|
+
const shell = process.env.SHELL || '/bin/zsh';
|
|
20
|
+
const term = pty.spawn(shell, [], {
|
|
21
|
+
name: 'xterm-256color',
|
|
22
|
+
cols: 120,
|
|
23
|
+
rows: 30,
|
|
24
|
+
cwd: homedir(),
|
|
25
|
+
env: {
|
|
26
|
+
...process.env,
|
|
27
|
+
TERM: 'xterm-256color',
|
|
28
|
+
COLORTERM: 'truecolor',
|
|
29
|
+
} as Record<string, string>,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
console.log(`[terminal] New session (pid: ${term.pid})`);
|
|
33
|
+
|
|
34
|
+
term.onData((data: string) => {
|
|
35
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
36
|
+
ws.send(JSON.stringify({ type: 'output', data }));
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
term.onExit(({ exitCode }) => {
|
|
41
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
42
|
+
ws.send(JSON.stringify({ type: 'exit', code: exitCode }));
|
|
43
|
+
ws.close();
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
ws.on('message', (msg: Buffer) => {
|
|
48
|
+
try {
|
|
49
|
+
const parsed = JSON.parse(msg.toString());
|
|
50
|
+
if (parsed.type === 'input') {
|
|
51
|
+
term.write(parsed.data);
|
|
52
|
+
} else if (parsed.type === 'resize') {
|
|
53
|
+
term.resize(parsed.cols, parsed.rows);
|
|
54
|
+
}
|
|
55
|
+
} catch {}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
ws.on('close', () => {
|
|
59
|
+
term.kill();
|
|
60
|
+
console.log(`[terminal] Session closed (pid: ${term.pid})`);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function stopTerminalServer() {
|
|
66
|
+
if (wss) {
|
|
67
|
+
wss.close();
|
|
68
|
+
wss = null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* Standalone terminal WebSocket server with tmux-backed persistent sessions.
|
|
4
|
+
* Sessions survive browser close and app restart.
|
|
5
|
+
*
|
|
6
|
+
* Protocol:
|
|
7
|
+
* Client → Server:
|
|
8
|
+
* { type: 'create', cols, rows } — create new tmux session
|
|
9
|
+
* { type: 'attach', sessionName, cols, rows } — attach to existing
|
|
10
|
+
* { type: 'list' } — list all mw-* sessions
|
|
11
|
+
* { type: 'input', data } — stdin
|
|
12
|
+
* { type: 'resize', cols, rows } — resize
|
|
13
|
+
* { type: 'kill', sessionName } — kill a session
|
|
14
|
+
* { type: 'load-state' } — load shared terminal state
|
|
15
|
+
* { type: 'save-state', data } — save shared terminal state
|
|
16
|
+
*
|
|
17
|
+
* Server → Client:
|
|
18
|
+
* { type: 'sessions', sessions: [{name, created, attached, windows}] }
|
|
19
|
+
* { type: 'connected', sessionName }
|
|
20
|
+
* { type: 'output', data }
|
|
21
|
+
* { type: 'exit', code }
|
|
22
|
+
* { type: 'terminal-state', data } — loaded state (or null)
|
|
23
|
+
*
|
|
24
|
+
* Usage: npx tsx lib/terminal-standalone.ts
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { WebSocketServer, WebSocket } from 'ws';
|
|
28
|
+
import * as pty from 'node-pty';
|
|
29
|
+
import { execSync } from 'node:child_process';
|
|
30
|
+
import { homedir } from 'node:os';
|
|
31
|
+
import { createHash } from 'node:crypto';
|
|
32
|
+
import { getDataDir } from './dirs';
|
|
33
|
+
import { readFileSync, writeFileSync, mkdirSync, readdirSync } from 'node:fs';
|
|
34
|
+
import { join } from 'node:path';
|
|
35
|
+
|
|
36
|
+
const PORT = Number(process.env.TERMINAL_PORT) || 8404;
|
|
37
|
+
// Session prefix based on DATA_DIR hash — default instance keeps 'mw-' for backward compat
|
|
38
|
+
const _dataDir = process.env.FORGE_DATA_DIR || '';
|
|
39
|
+
const _isDefault = !_dataDir || _dataDir.endsWith('/data') || _dataDir.endsWith('/.forge');
|
|
40
|
+
const SESSION_PREFIX = _isDefault ? 'mw-' : `mw${createHash('md5').update(_dataDir).digest('hex').slice(0, 6)}-`;
|
|
41
|
+
|
|
42
|
+
// Remove CLAUDECODE env so Claude Code can run inside terminal sessions
|
|
43
|
+
delete process.env.CLAUDECODE;
|
|
44
|
+
|
|
45
|
+
// ─── Shared state persistence ─────────────────────────────────
|
|
46
|
+
|
|
47
|
+
const STATE_DIR = getDataDir();
|
|
48
|
+
const STATE_FILE = join(STATE_DIR, 'terminal-state.json');
|
|
49
|
+
|
|
50
|
+
function loadTerminalState(): unknown {
|
|
51
|
+
try {
|
|
52
|
+
return JSON.parse(readFileSync(STATE_FILE, 'utf-8'));
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function saveTerminalState(data: unknown): void {
|
|
59
|
+
try {
|
|
60
|
+
mkdirSync(STATE_DIR, { recursive: true });
|
|
61
|
+
writeFileSync(STATE_FILE, JSON.stringify(data, null, 2));
|
|
62
|
+
} catch (e) {
|
|
63
|
+
console.error('[terminal] Failed to save state:', e);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
// ─── tmux helpers ──────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
function tmuxBin(): string {
|
|
71
|
+
try {
|
|
72
|
+
return execSync('which tmux', { encoding: 'utf-8' }).trim();
|
|
73
|
+
} catch {
|
|
74
|
+
return 'tmux';
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const TMUX = tmuxBin();
|
|
79
|
+
|
|
80
|
+
function listTmuxSessions(): { name: string; created: string; attached: boolean; windows: number }[] {
|
|
81
|
+
try {
|
|
82
|
+
const out = execSync(
|
|
83
|
+
`${TMUX} list-sessions -F "#{session_name}||#{session_created}||#{session_attached}||#{session_windows}" 2>/dev/null`,
|
|
84
|
+
{ encoding: 'utf-8' }
|
|
85
|
+
);
|
|
86
|
+
return out
|
|
87
|
+
.trim()
|
|
88
|
+
.split('\n')
|
|
89
|
+
.filter(line => line.startsWith(SESSION_PREFIX))
|
|
90
|
+
.map(line => {
|
|
91
|
+
const [name, created, attached, windows] = line.split('||');
|
|
92
|
+
return {
|
|
93
|
+
name,
|
|
94
|
+
created: new Date(Number(created) * 1000).toISOString(),
|
|
95
|
+
attached: attached !== '0',
|
|
96
|
+
windows: Number(windows) || 1,
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
} catch {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const MAX_SESSIONS = 10;
|
|
105
|
+
|
|
106
|
+
function getDefaultCwd(): string {
|
|
107
|
+
try {
|
|
108
|
+
const settingsPath = join(getDataDir(), 'settings.yaml');
|
|
109
|
+
const raw = readFileSync(settingsPath, 'utf-8');
|
|
110
|
+
const match = raw.match(/projectRoots:\s*\n((?:\s+-\s+.+\n?)*)/);
|
|
111
|
+
if (match) {
|
|
112
|
+
const first = match[1].split('\n').map(l => l.replace(/^\s+-\s+/, '').trim()).filter(Boolean)[0];
|
|
113
|
+
if (first) return first.replace(/^~/, homedir());
|
|
114
|
+
}
|
|
115
|
+
} catch {}
|
|
116
|
+
return homedir();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function createTmuxSession(cols: number, rows: number): string {
|
|
120
|
+
// Auto-cleanup: if too many sessions, kill the oldest idle ones
|
|
121
|
+
const existing = listTmuxSessions();
|
|
122
|
+
if (existing.length >= MAX_SESSIONS) {
|
|
123
|
+
const idle = existing.filter(s => !s.attached);
|
|
124
|
+
// Kill oldest idle sessions to make room
|
|
125
|
+
const toKill = idle.slice(0, Math.max(1, idle.length - Math.floor(MAX_SESSIONS / 2)));
|
|
126
|
+
for (const s of toKill) {
|
|
127
|
+
console.log(`[terminal] Auto-cleanup: killing idle session "${s.name}"`);
|
|
128
|
+
killTmuxSession(s.name);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const id = Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
|
133
|
+
const name = `${SESSION_PREFIX}${id}`;
|
|
134
|
+
try {
|
|
135
|
+
execSync(`${TMUX} new-session -d -s ${name} -x ${cols} -y ${rows}`, {
|
|
136
|
+
cwd: getDefaultCwd(),
|
|
137
|
+
env: { ...process.env, TERM: 'xterm-256color' },
|
|
138
|
+
});
|
|
139
|
+
} catch (e: any) {
|
|
140
|
+
const msg = e.stderr?.toString() || e.message || '';
|
|
141
|
+
if (msg.includes('posix_spawn') || msg.includes('fork failed') || msg.includes('No such file')) {
|
|
142
|
+
// PTY exhausted — aggressive cleanup: kill ALL idle sessions
|
|
143
|
+
console.error(`[terminal] PTY exhausted, cleaning up all idle sessions...`);
|
|
144
|
+
const all = listTmuxSessions();
|
|
145
|
+
for (const s of all) {
|
|
146
|
+
if (!s.attached) {
|
|
147
|
+
killTmuxSession(s.name);
|
|
148
|
+
console.log(`[terminal] Killed idle session: ${s.name}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// Retry once
|
|
152
|
+
try {
|
|
153
|
+
execSync(`${TMUX} new-session -d -s ${name} -x ${cols} -y ${rows}`, {
|
|
154
|
+
cwd: getDefaultCwd(),
|
|
155
|
+
env: { ...process.env, TERM: 'xterm-256color' },
|
|
156
|
+
});
|
|
157
|
+
} catch {
|
|
158
|
+
throw new Error('Failed to create terminal session. PTY devices exhausted. Run: sudo sysctl kern.tty.ptmx_max=2048');
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
throw e;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// Mouse and scrollback are set in attachToTmux (always called after create)
|
|
165
|
+
return name;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function killTmuxSession(name: string): boolean {
|
|
169
|
+
if (!name.startsWith(SESSION_PREFIX)) return false;
|
|
170
|
+
try {
|
|
171
|
+
execSync(`${TMUX} kill-session -t ${name} 2>/dev/null`);
|
|
172
|
+
return true;
|
|
173
|
+
} catch {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function tmuxSessionExists(name: string): boolean {
|
|
179
|
+
try {
|
|
180
|
+
execSync(`${TMUX} has-session -t ${name} 2>/dev/null`);
|
|
181
|
+
return true;
|
|
182
|
+
} catch {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ─── Connection tracking (for orphan cleanup) ──────────────────
|
|
188
|
+
|
|
189
|
+
/** Map from tmux session name → Set of WebSocket clients attached to it */
|
|
190
|
+
const sessionClients = new Map<string, Set<WebSocket>>();
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
function trackAttach(ws: WebSocket, sessionName: string) {
|
|
194
|
+
if (!sessionClients.has(sessionName)) sessionClients.set(sessionName, new Set());
|
|
195
|
+
sessionClients.get(sessionName)!.add(ws);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function trackDetach(ws: WebSocket, sessionName: string) {
|
|
199
|
+
sessionClients.get(sessionName)?.delete(ws);
|
|
200
|
+
if (sessionClients.get(sessionName)?.size === 0) sessionClients.delete(sessionName);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ─── Periodic orphan cleanup ─────────────────────────────────
|
|
204
|
+
|
|
205
|
+
/** Clean up detached tmux sessions that are not tracked in terminal-state.json */
|
|
206
|
+
function cleanupOrphanedSessions() {
|
|
207
|
+
const knownSessions = getKnownSessions();
|
|
208
|
+
const sessions = listTmuxSessions();
|
|
209
|
+
for (const s of sessions) {
|
|
210
|
+
if (s.attached) continue;
|
|
211
|
+
if (s.name.startsWith(`${SESSION_PREFIX}forge-`)) continue; // workspace agent session — managed by orchestrator
|
|
212
|
+
if (knownSessions.has(s.name)) continue; // saved in terminal state — preserve
|
|
213
|
+
const clients = sessionClients.get(s.name)?.size ?? 0;
|
|
214
|
+
if (clients === 0) {
|
|
215
|
+
console.log(`[terminal] Orphan cleanup: killing "${s.name}"`);
|
|
216
|
+
killTmuxSession(s.name);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Get all session names referenced in terminal-state.json (tabs + labels) */
|
|
222
|
+
function getKnownSessions(): Set<string> {
|
|
223
|
+
try {
|
|
224
|
+
const state = loadTerminalState() as any;
|
|
225
|
+
if (!state) return new Set();
|
|
226
|
+
const known = new Set<string>();
|
|
227
|
+
// From sessionLabels
|
|
228
|
+
if (state.sessionLabels) {
|
|
229
|
+
for (const name of Object.keys(state.sessionLabels)) known.add(name);
|
|
230
|
+
}
|
|
231
|
+
// From tab trees
|
|
232
|
+
if (state.tabs) {
|
|
233
|
+
for (const tab of state.tabs) {
|
|
234
|
+
collectTreeSessions(tab.tree, known);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return known;
|
|
238
|
+
} catch {
|
|
239
|
+
return new Set();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function collectTreeSessions(node: any, set: Set<string>) {
|
|
244
|
+
if (!node) return;
|
|
245
|
+
if (node.type === 'terminal' && node.sessionName) set.add(node.sessionName);
|
|
246
|
+
if (node.first) collectTreeSessions(node.first, set);
|
|
247
|
+
if (node.second) collectTreeSessions(node.second, set);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Run cleanup every 60 seconds, with a 60s initial delay to let clients reconnect after restart
|
|
251
|
+
setTimeout(() => setInterval(cleanupOrphanedSessions, 60_000), 60_000);
|
|
252
|
+
|
|
253
|
+
// ─── WebSocket server ──────────────────────────────────────────
|
|
254
|
+
|
|
255
|
+
const wss = new WebSocketServer({ port: PORT });
|
|
256
|
+
console.log(`[terminal] WebSocket server on ws://0.0.0.0:${PORT} (tmux-backed)`);
|
|
257
|
+
|
|
258
|
+
wss.on('connection', (ws: WebSocket) => {
|
|
259
|
+
let term: pty.IPty | null = null;
|
|
260
|
+
let sessionName: string | null = null;
|
|
261
|
+
|
|
262
|
+
function attachToTmux(name: string, cols: number, rows: number) {
|
|
263
|
+
if (!tmuxSessionExists(name)) {
|
|
264
|
+
ws.send(JSON.stringify({ type: 'error', message: `session "${name}" no longer exists` }));
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Kill previous pty process before attaching to new session (prevents PTY leak)
|
|
269
|
+
if (term) {
|
|
270
|
+
try { term.kill(); } catch {}
|
|
271
|
+
term = null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Ensure mouse mode is on (enables trackpad scrolling) and scrollback is set
|
|
275
|
+
try {
|
|
276
|
+
execSync(`${TMUX} set-option -t ${name} mouse on 2>/dev/null`);
|
|
277
|
+
execSync(`${TMUX} set-option -t ${name} history-limit 50000 2>/dev/null`);
|
|
278
|
+
} catch {}
|
|
279
|
+
|
|
280
|
+
// Detach from previous session if switching
|
|
281
|
+
if (sessionName) trackDetach(ws, sessionName);
|
|
282
|
+
sessionName = name;
|
|
283
|
+
trackAttach(ws, name);
|
|
284
|
+
|
|
285
|
+
// Attach to tmux session via pty
|
|
286
|
+
term = pty.spawn(TMUX, ['attach-session', '-t', name], {
|
|
287
|
+
name: 'xterm-256color',
|
|
288
|
+
cols,
|
|
289
|
+
rows,
|
|
290
|
+
cwd: homedir(),
|
|
291
|
+
env: {
|
|
292
|
+
...process.env,
|
|
293
|
+
TERM: 'xterm-256color',
|
|
294
|
+
COLORTERM: 'truecolor',
|
|
295
|
+
} as Record<string, string>,
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// Attached to tmux session (silent)
|
|
299
|
+
ws.send(JSON.stringify({ type: 'connected', sessionName: name }));
|
|
300
|
+
|
|
301
|
+
term.onData((data: string) => {
|
|
302
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
303
|
+
ws.send(JSON.stringify({ type: 'output', data }));
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
term.onExit(({ exitCode }) => {
|
|
308
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
309
|
+
ws.send(JSON.stringify({ type: 'exit', code: exitCode }));
|
|
310
|
+
}
|
|
311
|
+
term = null;
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
ws.on('message', (msg: Buffer) => {
|
|
316
|
+
try {
|
|
317
|
+
const parsed = JSON.parse(msg.toString());
|
|
318
|
+
|
|
319
|
+
switch (parsed.type) {
|
|
320
|
+
case 'list': {
|
|
321
|
+
const sessions = listTmuxSessions();
|
|
322
|
+
ws.send(JSON.stringify({ type: 'sessions', sessions }));
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
case 'create': {
|
|
327
|
+
const cols = parsed.cols || 120;
|
|
328
|
+
const rows = parsed.rows || 30;
|
|
329
|
+
try {
|
|
330
|
+
// Support fixed session name (e.g. mw-docs-claude)
|
|
331
|
+
let name: string;
|
|
332
|
+
if (parsed.sessionName && parsed.sessionName.startsWith(SESSION_PREFIX)) {
|
|
333
|
+
// Create with fixed name if it doesn't exist, otherwise attach
|
|
334
|
+
if (tmuxSessionExists(parsed.sessionName)) {
|
|
335
|
+
attachToTmux(parsed.sessionName, cols, rows);
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
name = parsed.sessionName;
|
|
339
|
+
execSync(`${TMUX} new-session -d -s ${name} -x ${cols} -y ${rows}`, {
|
|
340
|
+
cwd: homedir(),
|
|
341
|
+
env: { ...process.env, TERM: 'xterm-256color' },
|
|
342
|
+
});
|
|
343
|
+
// Mouse and scrollback are set in attachToTmux (always called after create)
|
|
344
|
+
} else {
|
|
345
|
+
name = createTmuxSession(cols, rows);
|
|
346
|
+
}
|
|
347
|
+
attachToTmux(name, cols, rows);
|
|
348
|
+
} catch (e: unknown) {
|
|
349
|
+
const errMsg = e instanceof Error ? e.message : 'unknown error';
|
|
350
|
+
console.error(`[terminal] Failed to create tmux session:`, errMsg);
|
|
351
|
+
ws.send(JSON.stringify({ type: 'error', message: `failed to create session: ${errMsg}` }));
|
|
352
|
+
}
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
case 'attach': {
|
|
357
|
+
const cols = parsed.cols || 120;
|
|
358
|
+
const rows = parsed.rows || 30;
|
|
359
|
+
try {
|
|
360
|
+
attachToTmux(parsed.sessionName, cols, rows);
|
|
361
|
+
} catch (e: unknown) {
|
|
362
|
+
const errMsg = e instanceof Error ? e.message : 'unknown error';
|
|
363
|
+
console.error(`[terminal] Failed to attach to session:`, errMsg);
|
|
364
|
+
ws.send(JSON.stringify({ type: 'error', message: `failed to attach: ${errMsg}` }));
|
|
365
|
+
}
|
|
366
|
+
break;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
case 'input': {
|
|
370
|
+
if (term) term.write(parsed.data);
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
case 'resize': {
|
|
375
|
+
if (term) term.resize(parsed.cols, parsed.rows);
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
case 'kill': {
|
|
380
|
+
if (parsed.sessionName) {
|
|
381
|
+
killTmuxSession(parsed.sessionName);
|
|
382
|
+
ws.send(JSON.stringify({ type: 'sessions', sessions: listTmuxSessions() }));
|
|
383
|
+
}
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
case 'tmux-mouse': {
|
|
388
|
+
// Toggle mouse for ALL tmux sessions (global + per-session)
|
|
389
|
+
const val = parsed.mouse ? 'on' : 'off';
|
|
390
|
+
try {
|
|
391
|
+
execSync(`${TMUX} set -g mouse ${val}`, { timeout: 3000 });
|
|
392
|
+
// Also apply to every existing session (overrides per-session setting)
|
|
393
|
+
const sessions = listTmuxSessions();
|
|
394
|
+
for (const s of sessions) {
|
|
395
|
+
try { execSync(`${TMUX} set-option -t "${s.name}" mouse ${val}`, { timeout: 1000 }); } catch {}
|
|
396
|
+
}
|
|
397
|
+
ws.send(JSON.stringify({ type: 'tmux-mouse-result', ok: true, mouse: parsed.mouse }));
|
|
398
|
+
} catch (e: any) {
|
|
399
|
+
ws.send(JSON.stringify({ type: 'tmux-mouse-result', ok: false, error: e.message }));
|
|
400
|
+
}
|
|
401
|
+
break;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
case 'load-state': {
|
|
405
|
+
const state = loadTerminalState();
|
|
406
|
+
ws.send(JSON.stringify({ type: 'terminal-state', data: state }));
|
|
407
|
+
break;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
case 'save-state': {
|
|
411
|
+
if (parsed.data) {
|
|
412
|
+
saveTerminalState(parsed.data);
|
|
413
|
+
}
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
} catch (e) {
|
|
418
|
+
console.error('[terminal] Error handling message:', e);
|
|
419
|
+
try {
|
|
420
|
+
ws.send(JSON.stringify({ type: 'error', message: 'internal server error' }));
|
|
421
|
+
} catch {}
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
ws.on('close', () => {
|
|
426
|
+
// Only kill the pty attach process, NOT the tmux session — it persists
|
|
427
|
+
if (term) {
|
|
428
|
+
term.kill();
|
|
429
|
+
// Detached from tmux session (silent)
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Untrack this client
|
|
433
|
+
if (sessionName) trackDetach(ws, sessionName);
|
|
434
|
+
|
|
435
|
+
// Orphan cleanup is handled by the periodic cleanupOrphanedSessions() (every 60s)
|
|
436
|
+
// which checks sessionClients and getKnownSessions() from terminal-state.json
|
|
437
|
+
});
|
|
438
|
+
});
|