aicodeman 0.2.9 → 0.3.1
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/README.md +118 -4
- package/dist/ai-idle-checker.d.ts.map +1 -1
- package/dist/ai-idle-checker.js +3 -2
- package/dist/ai-idle-checker.js.map +1 -1
- package/dist/ai-plan-checker.d.ts.map +1 -1
- package/dist/ai-plan-checker.js +3 -2
- package/dist/ai-plan-checker.js.map +1 -1
- package/dist/bash-tool-parser.d.ts +2 -3
- package/dist/bash-tool-parser.d.ts.map +1 -1
- package/dist/bash-tool-parser.js +14 -31
- package/dist/bash-tool-parser.js.map +1 -1
- package/dist/config/ai-defaults.d.ts +16 -0
- package/dist/config/ai-defaults.d.ts.map +1 -0
- package/dist/config/ai-defaults.js +16 -0
- package/dist/config/ai-defaults.js.map +1 -0
- package/dist/config/auth-config.d.ts +19 -0
- package/dist/config/auth-config.d.ts.map +1 -0
- package/dist/config/auth-config.js +28 -0
- package/dist/config/auth-config.js.map +1 -0
- package/dist/config/exec-timeout.d.ts +10 -0
- package/dist/config/exec-timeout.d.ts.map +1 -0
- package/dist/config/exec-timeout.js +10 -0
- package/dist/config/exec-timeout.js.map +1 -0
- package/dist/config/map-limits.d.ts +4 -0
- package/dist/config/map-limits.d.ts.map +1 -1
- package/dist/config/map-limits.js +7 -0
- package/dist/config/map-limits.js.map +1 -1
- package/dist/config/server-timing.d.ts +42 -0
- package/dist/config/server-timing.d.ts.map +1 -0
- package/dist/config/server-timing.js +57 -0
- package/dist/config/server-timing.js.map +1 -0
- package/dist/config/team-config.d.ts +16 -0
- package/dist/config/team-config.d.ts.map +1 -0
- package/dist/config/team-config.js +16 -0
- package/dist/config/team-config.js.map +1 -0
- package/dist/config/terminal-limits.d.ts +18 -0
- package/dist/config/terminal-limits.d.ts.map +1 -0
- package/dist/config/terminal-limits.js +18 -0
- package/dist/config/terminal-limits.js.map +1 -0
- package/dist/config/tunnel-config.d.ts +27 -0
- package/dist/config/tunnel-config.d.ts.map +1 -0
- package/dist/config/tunnel-config.js +36 -0
- package/dist/config/tunnel-config.js.map +1 -0
- package/dist/hooks-config.d.ts +21 -6
- package/dist/hooks-config.d.ts.map +1 -1
- package/dist/hooks-config.js +28 -12
- package/dist/hooks-config.js.map +1 -1
- package/dist/image-watcher.d.ts +4 -4
- package/dist/image-watcher.d.ts.map +1 -1
- package/dist/image-watcher.js +17 -30
- package/dist/image-watcher.js.map +1 -1
- package/dist/index.js +1 -2
- package/dist/index.js.map +1 -1
- package/dist/plan-orchestrator.d.ts +2 -24
- package/dist/plan-orchestrator.d.ts.map +1 -1
- package/dist/plan-orchestrator.js.map +1 -1
- package/dist/prompts/planner.d.ts +7 -8
- package/dist/prompts/planner.d.ts.map +1 -1
- package/dist/prompts/planner.js +7 -8
- package/dist/prompts/planner.js.map +1 -1
- package/dist/prompts/research-agent.d.ts +6 -4
- package/dist/prompts/research-agent.d.ts.map +1 -1
- package/dist/prompts/research-agent.js +6 -4
- package/dist/prompts/research-agent.js.map +1 -1
- package/dist/push-store.d.ts +1 -1
- package/dist/push-store.d.ts.map +1 -1
- package/dist/push-store.js +4 -12
- package/dist/push-store.js.map +1 -1
- package/dist/ralph-fix-plan-watcher.d.ts +91 -0
- package/dist/ralph-fix-plan-watcher.d.ts.map +1 -0
- package/dist/ralph-fix-plan-watcher.js +326 -0
- package/dist/ralph-fix-plan-watcher.js.map +1 -0
- package/dist/ralph-loop.d.ts +14 -4
- package/dist/ralph-loop.d.ts.map +1 -1
- package/dist/ralph-loop.js +14 -4
- package/dist/ralph-loop.js.map +1 -1
- package/dist/ralph-plan-tracker.d.ts +201 -0
- package/dist/ralph-plan-tracker.d.ts.map +1 -0
- package/dist/ralph-plan-tracker.js +325 -0
- package/dist/ralph-plan-tracker.js.map +1 -0
- package/dist/ralph-stall-detector.d.ts +84 -0
- package/dist/ralph-stall-detector.d.ts.map +1 -0
- package/dist/ralph-stall-detector.js +139 -0
- package/dist/ralph-stall-detector.js.map +1 -0
- package/dist/ralph-status-parser.d.ts +141 -0
- package/dist/ralph-status-parser.d.ts.map +1 -0
- package/dist/ralph-status-parser.js +478 -0
- package/dist/ralph-status-parser.js.map +1 -0
- package/dist/ralph-tracker.d.ts +218 -692
- package/dist/ralph-tracker.d.ts.map +1 -1
- package/dist/ralph-tracker.js +389 -1723
- package/dist/ralph-tracker.js.map +1 -1
- package/dist/respawn-adaptive-timing.d.ts +61 -0
- package/dist/respawn-adaptive-timing.d.ts.map +1 -0
- package/dist/respawn-adaptive-timing.js +105 -0
- package/dist/respawn-adaptive-timing.js.map +1 -0
- package/dist/respawn-controller.d.ts +35 -115
- package/dist/respawn-controller.d.ts.map +1 -1
- package/dist/respawn-controller.js +167 -607
- package/dist/respawn-controller.js.map +1 -1
- package/dist/respawn-health.d.ts +54 -0
- package/dist/respawn-health.d.ts.map +1 -0
- package/dist/respawn-health.js +183 -0
- package/dist/respawn-health.js.map +1 -0
- package/dist/respawn-metrics.d.ts +81 -0
- package/dist/respawn-metrics.d.ts.map +1 -0
- package/dist/respawn-metrics.js +198 -0
- package/dist/respawn-metrics.js.map +1 -0
- package/dist/respawn-patterns.d.ts +45 -0
- package/dist/respawn-patterns.d.ts.map +1 -0
- package/dist/respawn-patterns.js +125 -0
- package/dist/respawn-patterns.js.map +1 -0
- package/dist/session-auto-ops.d.ts +89 -0
- package/dist/session-auto-ops.d.ts.map +1 -0
- package/dist/session-auto-ops.js +224 -0
- package/dist/session-auto-ops.js.map +1 -0
- package/dist/session-cli-builder.d.ts +62 -0
- package/dist/session-cli-builder.d.ts.map +1 -0
- package/dist/session-cli-builder.js +121 -0
- package/dist/session-cli-builder.js.map +1 -0
- package/dist/session-manager.d.ts +17 -5
- package/dist/session-manager.d.ts.map +1 -1
- package/dist/session-manager.js +17 -5
- package/dist/session-manager.js.map +1 -1
- package/dist/session-task-cache.d.ts +52 -0
- package/dist/session-task-cache.d.ts.map +1 -0
- package/dist/session-task-cache.js +90 -0
- package/dist/session-task-cache.js.map +1 -0
- package/dist/session.d.ts +23 -41
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +79 -317
- package/dist/session.js.map +1 -1
- package/dist/state-store.d.ts +19 -9
- package/dist/state-store.d.ts.map +1 -1
- package/dist/state-store.js +29 -30
- package/dist/state-store.js.map +1 -1
- package/dist/subagent-watcher.d.ts +26 -7
- package/dist/subagent-watcher.d.ts.map +1 -1
- package/dist/subagent-watcher.js +47 -64
- package/dist/subagent-watcher.js.map +1 -1
- package/dist/team-watcher.d.ts.map +1 -1
- package/dist/team-watcher.js +2 -5
- package/dist/team-watcher.js.map +1 -1
- package/dist/tmux-manager.d.ts.map +1 -1
- package/dist/tmux-manager.js +1 -2
- package/dist/tmux-manager.js.map +1 -1
- package/dist/tunnel-manager.d.ts +26 -0
- package/dist/tunnel-manager.d.ts.map +1 -1
- package/dist/tunnel-manager.js +126 -7
- package/dist/tunnel-manager.js.map +1 -1
- package/dist/types/api.d.ts +108 -0
- package/dist/types/api.d.ts.map +1 -0
- package/dist/types/api.js +98 -0
- package/dist/types/api.js.map +1 -0
- package/dist/types/app-state.d.ts +117 -0
- package/dist/types/app-state.d.ts.map +1 -0
- package/dist/types/app-state.js +76 -0
- package/dist/types/app-state.js.map +1 -0
- package/dist/types/common.d.ts +79 -0
- package/dist/types/common.d.ts.map +1 -0
- package/dist/types/common.js +17 -0
- package/dist/types/common.js.map +1 -0
- package/dist/types/index.d.ts +66 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +66 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/lifecycle.d.ts +28 -0
- package/dist/types/lifecycle.d.ts.map +1 -0
- package/dist/types/lifecycle.js +16 -0
- package/dist/types/lifecycle.js.map +1 -0
- package/dist/types/plan.d.ts +45 -0
- package/dist/types/plan.d.ts.map +1 -0
- package/dist/types/plan.js +18 -0
- package/dist/types/plan.js.map +1 -0
- package/dist/types/push.d.ts +36 -0
- package/dist/types/push.d.ts.map +1 -0
- package/dist/types/push.js +18 -0
- package/dist/types/push.js.map +1 -0
- package/dist/types/ralph.d.ts +262 -0
- package/dist/types/ralph.d.ts.map +1 -0
- package/dist/types/ralph.js +70 -0
- package/dist/types/ralph.js.map +1 -0
- package/dist/types/respawn.d.ts +271 -0
- package/dist/types/respawn.d.ts.map +1 -0
- package/dist/types/respawn.js +26 -0
- package/dist/types/respawn.js.map +1 -0
- package/dist/types/run-summary.d.ts +96 -0
- package/dist/types/run-summary.d.ts.map +1 -0
- package/dist/types/run-summary.js +37 -0
- package/dist/types/run-summary.js.map +1 -0
- package/dist/types/session.d.ts +152 -0
- package/dist/types/session.d.ts.map +1 -0
- package/dist/types/session.js +27 -0
- package/dist/types/session.js.map +1 -0
- package/dist/types/task.d.ts +72 -0
- package/dist/types/task.d.ts.map +1 -0
- package/dist/types/task.js +19 -0
- package/dist/types/task.js.map +1 -0
- package/dist/types/teams.d.ts +73 -0
- package/dist/types/teams.d.ts.map +1 -0
- package/dist/types/teams.js +23 -0
- package/dist/types/teams.js.map +1 -0
- package/dist/types/tools.d.ts +61 -0
- package/dist/types/tools.d.ts.map +1 -0
- package/dist/types/tools.js +20 -0
- package/dist/types/tools.js.map +1 -0
- package/dist/types.d.ts +8 -1134
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +8 -210
- package/dist/types.js.map +1 -1
- package/dist/utils/claude-cli-resolver.d.ts.map +1 -1
- package/dist/utils/claude-cli-resolver.js +1 -2
- package/dist/utils/claude-cli-resolver.js.map +1 -1
- package/dist/utils/debouncer.d.ts +111 -0
- package/dist/utils/debouncer.d.ts.map +1 -0
- package/dist/utils/debouncer.js +162 -0
- package/dist/utils/debouncer.js.map +1 -0
- package/dist/utils/index.d.ts +3 -2
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +3 -2
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/opencode-cli-resolver.d.ts.map +1 -1
- package/dist/utils/opencode-cli-resolver.js +1 -2
- package/dist/utils/opencode-cli-resolver.js.map +1 -1
- package/dist/utils/string-similarity.d.ts +0 -57
- package/dist/utils/string-similarity.d.ts.map +1 -1
- package/dist/utils/string-similarity.js +3 -18
- package/dist/utils/string-similarity.js.map +1 -1
- package/dist/web/middleware/auth.d.ts +31 -0
- package/dist/web/middleware/auth.d.ts.map +1 -0
- package/dist/web/middleware/auth.js +154 -0
- package/dist/web/middleware/auth.js.map +1 -0
- package/dist/web/ports/auth-port.d.ts +18 -0
- package/dist/web/ports/auth-port.d.ts.map +1 -0
- package/dist/web/ports/auth-port.js +6 -0
- package/dist/web/ports/auth-port.js.map +1 -0
- package/dist/web/ports/config-port.d.ts +28 -0
- package/dist/web/ports/config-port.d.ts.map +1 -0
- package/dist/web/ports/config-port.js +6 -0
- package/dist/web/ports/config-port.js.map +1 -0
- package/dist/web/ports/event-port.d.ts +13 -0
- package/dist/web/ports/event-port.d.ts.map +1 -0
- package/dist/web/ports/event-port.js +6 -0
- package/dist/web/ports/event-port.js.map +1 -0
- package/dist/web/ports/index.d.ts +14 -0
- package/dist/web/ports/index.d.ts.map +1 -0
- package/dist/web/ports/index.js +9 -0
- package/dist/web/ports/index.js.map +1 -0
- package/dist/web/ports/infra-port.d.ts +36 -0
- package/dist/web/ports/infra-port.d.ts.map +1 -0
- package/dist/web/ports/infra-port.js +6 -0
- package/dist/web/ports/infra-port.js.map +1 -0
- package/dist/web/ports/respawn-port.d.ts +20 -0
- package/dist/web/ports/respawn-port.d.ts.map +1 -0
- package/dist/web/ports/respawn-port.js +6 -0
- package/dist/web/ports/respawn-port.js.map +1 -0
- package/dist/web/ports/session-port.d.ts +15 -0
- package/dist/web/ports/session-port.d.ts.map +1 -0
- package/dist/web/ports/session-port.js +6 -0
- package/dist/web/ports/session-port.js.map +1 -0
- package/dist/web/public/api-client.js +82 -0
- package/dist/web/public/api-client.js.br +0 -0
- package/dist/web/public/api-client.js.gz +0 -0
- package/dist/web/public/app.js +117 -201
- package/dist/web/public/app.js.br +0 -0
- package/dist/web/public/app.js.gz +0 -0
- package/dist/web/public/constants.js +365 -0
- package/dist/web/public/constants.js.br +0 -0
- package/dist/web/public/constants.js.gz +0 -0
- package/dist/web/public/index.html +15 -3
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/public/keyboard-accessory.js +302 -0
- package/dist/web/public/keyboard-accessory.js.br +0 -0
- package/dist/web/public/keyboard-accessory.js.gz +0 -0
- package/dist/web/public/mobile-handlers.js +491 -0
- package/dist/web/public/mobile-handlers.js.br +0 -0
- package/dist/web/public/mobile-handlers.js.gz +0 -0
- package/dist/web/public/mobile.css.gz +0 -0
- package/dist/web/public/notification-manager.js +472 -0
- package/dist/web/public/notification-manager.js.br +0 -0
- package/dist/web/public/notification-manager.js.gz +0 -0
- package/dist/web/public/ralph-wizard.js +33 -9
- package/dist/web/public/ralph-wizard.js.br +0 -0
- package/dist/web/public/ralph-wizard.js.gz +0 -0
- package/dist/web/public/styles.css.gz +0 -0
- package/dist/web/public/subagent-windows.js +1149 -0
- package/dist/web/public/subagent-windows.js.br +0 -0
- package/dist/web/public/subagent-windows.js.gz +0 -0
- package/dist/web/public/sw.js +15 -0
- package/dist/web/public/sw.js.br +0 -0
- package/dist/web/public/sw.js.gz +0 -0
- package/dist/web/public/upload.html.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.js +4 -0
- package/dist/web/public/vendor/xterm-zerolag-input.js.br +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.js.gz +0 -0
- package/dist/web/public/vendor/xterm.css.gz +0 -0
- package/dist/web/public/vendor/xterm.min.js.gz +0 -0
- package/dist/web/public/voice-input.js +882 -0
- package/dist/web/public/voice-input.js.br +0 -0
- package/dist/web/public/voice-input.js.gz +0 -0
- package/dist/web/route-helpers.d.ts +38 -0
- package/dist/web/route-helpers.d.ts.map +1 -0
- package/dist/web/route-helpers.js +144 -0
- package/dist/web/route-helpers.js.map +1 -0
- package/dist/web/routes/case-routes.d.ts +9 -0
- package/dist/web/routes/case-routes.d.ts.map +1 -0
- package/dist/web/routes/case-routes.js +426 -0
- package/dist/web/routes/case-routes.js.map +1 -0
- package/dist/web/routes/file-routes.d.ts +8 -0
- package/dist/web/routes/file-routes.d.ts.map +1 -0
- package/dist/web/routes/file-routes.js +337 -0
- package/dist/web/routes/file-routes.js.map +1 -0
- package/dist/web/routes/hook-event-routes.d.ts +9 -0
- package/dist/web/routes/hook-event-routes.d.ts.map +1 -0
- package/dist/web/routes/hook-event-routes.js +57 -0
- package/dist/web/routes/hook-event-routes.js.map +1 -0
- package/dist/web/routes/index.d.ts +16 -0
- package/dist/web/routes/index.d.ts.map +1 -0
- package/dist/web/routes/index.js +16 -0
- package/dist/web/routes/index.js.map +1 -0
- package/dist/web/routes/mux-routes.d.ts +8 -0
- package/dist/web/routes/mux-routes.d.ts.map +1 -0
- package/dist/web/routes/mux-routes.js +32 -0
- package/dist/web/routes/mux-routes.js.map +1 -0
- package/dist/web/routes/plan-routes.d.ts +9 -0
- package/dist/web/routes/plan-routes.d.ts.map +1 -0
- package/dist/web/routes/plan-routes.js +385 -0
- package/dist/web/routes/plan-routes.js.map +1 -0
- package/dist/web/routes/push-routes.d.ts +8 -0
- package/dist/web/routes/push-routes.d.ts.map +1 -0
- package/dist/web/routes/push-routes.js +49 -0
- package/dist/web/routes/push-routes.js.map +1 -0
- package/dist/web/routes/ralph-routes.d.ts +9 -0
- package/dist/web/routes/ralph-routes.d.ts.map +1 -0
- package/dist/web/routes/ralph-routes.js +485 -0
- package/dist/web/routes/ralph-routes.js.map +1 -0
- package/dist/web/routes/respawn-routes.d.ts +8 -0
- package/dist/web/routes/respawn-routes.d.ts.map +1 -0
- package/dist/web/routes/respawn-routes.js +270 -0
- package/dist/web/routes/respawn-routes.js.map +1 -0
- package/dist/web/routes/scheduled-routes.d.ts +8 -0
- package/dist/web/routes/scheduled-routes.d.ts.map +1 -0
- package/dist/web/routes/scheduled-routes.js +51 -0
- package/dist/web/routes/scheduled-routes.js.map +1 -0
- package/dist/web/routes/session-routes.d.ts +9 -0
- package/dist/web/routes/session-routes.d.ts.map +1 -0
- package/dist/web/routes/session-routes.js +751 -0
- package/dist/web/routes/session-routes.js.map +1 -0
- package/dist/web/routes/system-routes.d.ts +9 -0
- package/dist/web/routes/system-routes.d.ts.map +1 -0
- package/dist/web/routes/system-routes.js +699 -0
- package/dist/web/routes/system-routes.js.map +1 -0
- package/dist/web/routes/team-routes.d.ts +8 -0
- package/dist/web/routes/team-routes.d.ts.map +1 -0
- package/dist/web/routes/team-routes.js +14 -0
- package/dist/web/routes/team-routes.js.map +1 -0
- package/dist/web/schemas.d.ts +43 -3
- package/dist/web/schemas.d.ts.map +1 -1
- package/dist/web/schemas.js +6 -2
- package/dist/web/schemas.js.map +1 -1
- package/dist/web/server.d.ts +35 -15
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +563 -3971
- package/dist/web/server.js.map +1 -1
- package/dist/web/sse-events.d.ts +361 -0
- package/dist/web/sse-events.d.ts.map +1 -0
- package/dist/web/sse-events.js +396 -0
- package/dist/web/sse-events.js.map +1 -0
- package/package.json +2 -1
- package/scripts/postinstall.js +58 -0
|
@@ -0,0 +1,751 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Session management routes.
|
|
3
|
+
* Covers session CRUD, input/output, terminal buffer, quick-start, quick-run,
|
|
4
|
+
* auto-clear, auto-compact, image watcher, flicker filter, and logout.
|
|
5
|
+
*/
|
|
6
|
+
import { join, dirname, resolve, relative, isAbsolute } from 'node:path';
|
|
7
|
+
import { existsSync, statSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import fs from 'node:fs/promises';
|
|
9
|
+
import { ApiErrorCode, createErrorResponse, getErrorMessage, } from '../../types.js';
|
|
10
|
+
import { Session } from '../../session.js';
|
|
11
|
+
import { SseEvent } from '../sse-events.js';
|
|
12
|
+
import { CreateSessionSchema, SessionNameSchema, SessionColorSchema, RunPromptSchema, SessionInputWithLimitSchema, ResizeSchema, AutoClearSchema, AutoCompactSchema, ImageWatcherSchema, FlickerFilterSchema, QuickRunSchema, QuickStartSchema, } from '../schemas.js';
|
|
13
|
+
import { autoConfigureRalph, CASES_DIR, SETTINGS_PATH } from '../route-helpers.js';
|
|
14
|
+
import { AUTH_COOKIE_NAME } from '../middleware/auth.js';
|
|
15
|
+
import { writeHooksConfig, updateCaseEnvVars } from '../../hooks-config.js';
|
|
16
|
+
import { generateClaudeMd } from '../../templates/claude-md.js';
|
|
17
|
+
import { imageWatcher } from '../../image-watcher.js';
|
|
18
|
+
import { getLifecycleLog } from '../../session-lifecycle-log.js';
|
|
19
|
+
import { MAX_CONCURRENT_SESSIONS } from '../../config/map-limits.js';
|
|
20
|
+
import { RunSummaryTracker } from '../../run-summary.js';
|
|
21
|
+
import { MAX_INPUT_LENGTH, MAX_SESSION_NAME_LENGTH } from '../../config/terminal-limits.js';
|
|
22
|
+
// Pre-compiled regex for terminal buffer cleaning (avoids per-request compilation)
|
|
23
|
+
// eslint-disable-next-line no-control-regex
|
|
24
|
+
const CLAUDE_BANNER_PATTERN = /\x1b\[1mClaud/;
|
|
25
|
+
// eslint-disable-next-line no-control-regex
|
|
26
|
+
const CTRL_L_PATTERN = /\x0c/g;
|
|
27
|
+
const LEADING_WHITESPACE_PATTERN = /^[\s\r\n]+/;
|
|
28
|
+
export function registerSessionRoutes(app, ctx) {
|
|
29
|
+
// ═══════════════════════════════════════════════════════════════
|
|
30
|
+
// Auth
|
|
31
|
+
// ═══════════════════════════════════════════════════════════════
|
|
32
|
+
// ========== Logout ==========
|
|
33
|
+
app.post('/api/logout', async (req, reply) => {
|
|
34
|
+
// Invalidate server-side session token (not just the browser cookie)
|
|
35
|
+
const sessionToken = req.cookies[AUTH_COOKIE_NAME];
|
|
36
|
+
if (sessionToken) {
|
|
37
|
+
ctx.authSessions?.delete(sessionToken);
|
|
38
|
+
}
|
|
39
|
+
reply.clearCookie(AUTH_COOKIE_NAME, { path: '/' });
|
|
40
|
+
return { success: true };
|
|
41
|
+
});
|
|
42
|
+
// ═══════════════════════════════════════════════════════════════
|
|
43
|
+
// Session CRUD (list, create, rename, color, delete, detail)
|
|
44
|
+
// ═══════════════════════════════════════════════════════════════
|
|
45
|
+
// ========== Session Listing ==========
|
|
46
|
+
app.get('/api/sessions', async () => {
|
|
47
|
+
return ctx.getLightSessionsState();
|
|
48
|
+
});
|
|
49
|
+
// ========== Session Creation ==========
|
|
50
|
+
app.post('/api/sessions', async (req) => {
|
|
51
|
+
// Prevent unbounded session creation
|
|
52
|
+
if (ctx.sessions.size >= MAX_CONCURRENT_SESSIONS) {
|
|
53
|
+
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, `Maximum concurrent sessions (${MAX_CONCURRENT_SESSIONS}) reached. Delete some sessions first.`);
|
|
54
|
+
}
|
|
55
|
+
const result = CreateSessionSchema.safeParse(req.body);
|
|
56
|
+
if (!result.success) {
|
|
57
|
+
return createErrorResponse(ApiErrorCode.INVALID_INPUT, result.error.issues[0]?.message ?? 'Validation failed');
|
|
58
|
+
}
|
|
59
|
+
const body = result.data;
|
|
60
|
+
const workingDir = body.workingDir || process.cwd();
|
|
61
|
+
// Validate workingDir exists and is a directory
|
|
62
|
+
if (body.workingDir) {
|
|
63
|
+
try {
|
|
64
|
+
const stat = statSync(workingDir);
|
|
65
|
+
if (!stat.isDirectory()) {
|
|
66
|
+
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'workingDir is not a directory');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'workingDir does not exist');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Write env overrides to .claude/settings.local.json if provided
|
|
74
|
+
if (body.envOverrides && Object.keys(body.envOverrides).length > 0) {
|
|
75
|
+
await updateCaseEnvVars(workingDir, body.envOverrides);
|
|
76
|
+
}
|
|
77
|
+
// Check OpenCode availability if requested
|
|
78
|
+
if (body.mode === 'opencode') {
|
|
79
|
+
const { isOpenCodeAvailable } = await import('../../utils/opencode-cli-resolver.js');
|
|
80
|
+
if (!isOpenCodeAvailable()) {
|
|
81
|
+
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, 'OpenCode CLI not found. Install with: curl -fsSL https://opencode.ai/install | bash');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const globalNice = await ctx.getGlobalNiceConfig();
|
|
85
|
+
const modelConfig = await ctx.getModelConfig();
|
|
86
|
+
const mode = body.mode || 'claude';
|
|
87
|
+
const model = mode === 'opencode' ? body.openCodeConfig?.model : mode !== 'shell' ? modelConfig?.defaultModel : undefined;
|
|
88
|
+
const claudeModeConfig = await ctx.getClaudeModeConfig();
|
|
89
|
+
const session = new Session({
|
|
90
|
+
workingDir,
|
|
91
|
+
mode,
|
|
92
|
+
name: body.name || '',
|
|
93
|
+
mux: ctx.mux,
|
|
94
|
+
useMux: true,
|
|
95
|
+
niceConfig: globalNice,
|
|
96
|
+
model,
|
|
97
|
+
claudeMode: claudeModeConfig.claudeMode,
|
|
98
|
+
allowedTools: claudeModeConfig.allowedTools,
|
|
99
|
+
openCodeConfig: mode === 'opencode' ? body.openCodeConfig : undefined,
|
|
100
|
+
});
|
|
101
|
+
ctx.addSession(session);
|
|
102
|
+
ctx.store.incrementSessionsCreated();
|
|
103
|
+
ctx.persistSessionState(session);
|
|
104
|
+
await ctx.setupSessionListeners(session);
|
|
105
|
+
getLifecycleLog().log({ event: 'created', sessionId: session.id, name: session.name });
|
|
106
|
+
// Use light state for broadcast + response — buffers are fetched on-demand via /terminal.
|
|
107
|
+
// Avoids serializing 2-3MB of terminal+text buffers per session creation.
|
|
108
|
+
const lightState = ctx.getSessionStateWithRespawn(session);
|
|
109
|
+
ctx.broadcast(SseEvent.SessionCreated, lightState);
|
|
110
|
+
return { success: true, session: lightState };
|
|
111
|
+
});
|
|
112
|
+
// ========== Rename Session ==========
|
|
113
|
+
app.put('/api/sessions/:id/name', async (req) => {
|
|
114
|
+
const { id } = req.params;
|
|
115
|
+
const result = SessionNameSchema.safeParse(req.body);
|
|
116
|
+
if (!result.success) {
|
|
117
|
+
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid request body');
|
|
118
|
+
}
|
|
119
|
+
const body = result.data;
|
|
120
|
+
const session = ctx.sessions.get(id);
|
|
121
|
+
if (!session) {
|
|
122
|
+
return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
|
|
123
|
+
}
|
|
124
|
+
const name = String(body.name || '').slice(0, MAX_SESSION_NAME_LENGTH);
|
|
125
|
+
session.name = name;
|
|
126
|
+
// Also update the mux session name if applicable
|
|
127
|
+
ctx.mux.updateSessionName(id, session.name);
|
|
128
|
+
ctx.persistSessionState(session);
|
|
129
|
+
ctx.broadcast(SseEvent.SessionUpdated, ctx.getSessionStateWithRespawn(session));
|
|
130
|
+
return { success: true, name: session.name };
|
|
131
|
+
});
|
|
132
|
+
// ========== Set Session Color ==========
|
|
133
|
+
app.put('/api/sessions/:id/color', async (req) => {
|
|
134
|
+
const { id } = req.params;
|
|
135
|
+
const result = SessionColorSchema.safeParse(req.body);
|
|
136
|
+
if (!result.success) {
|
|
137
|
+
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid request body');
|
|
138
|
+
}
|
|
139
|
+
const body = result.data;
|
|
140
|
+
const session = ctx.sessions.get(id);
|
|
141
|
+
if (!session) {
|
|
142
|
+
return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
|
|
143
|
+
}
|
|
144
|
+
const validColors = ['default', 'red', 'orange', 'yellow', 'green', 'blue', 'purple', 'pink'];
|
|
145
|
+
if (!validColors.includes(body.color)) {
|
|
146
|
+
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid color');
|
|
147
|
+
}
|
|
148
|
+
session.setColor(body.color);
|
|
149
|
+
ctx.persistSessionState(session);
|
|
150
|
+
ctx.broadcast(SseEvent.SessionUpdated, ctx.getSessionStateWithRespawn(session));
|
|
151
|
+
return { success: true, color: session.color };
|
|
152
|
+
});
|
|
153
|
+
// ========== Delete Session ==========
|
|
154
|
+
app.delete('/api/sessions/:id', async (req) => {
|
|
155
|
+
const { id } = req.params;
|
|
156
|
+
const query = req.query;
|
|
157
|
+
const killMux = query.killMux !== 'false'; // Default to true
|
|
158
|
+
if (!ctx.sessions.has(id)) {
|
|
159
|
+
return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
|
|
160
|
+
}
|
|
161
|
+
await ctx.cleanupSession(id, killMux, 'user_delete');
|
|
162
|
+
return { success: true };
|
|
163
|
+
});
|
|
164
|
+
// ========== Delete All Sessions ==========
|
|
165
|
+
app.delete('/api/sessions', async () => {
|
|
166
|
+
const sessionIds = Array.from(ctx.sessions.keys());
|
|
167
|
+
let killed = 0;
|
|
168
|
+
for (const id of sessionIds) {
|
|
169
|
+
if (ctx.sessions.has(id)) {
|
|
170
|
+
await ctx.cleanupSession(id, true, 'user_bulk_delete');
|
|
171
|
+
killed++;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return { success: true, data: { killed } };
|
|
175
|
+
});
|
|
176
|
+
// ========== Get Session Detail ==========
|
|
177
|
+
app.get('/api/sessions/:id', async (req) => {
|
|
178
|
+
const { id } = req.params;
|
|
179
|
+
const session = ctx.sessions.get(id);
|
|
180
|
+
if (!session) {
|
|
181
|
+
return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
|
|
182
|
+
}
|
|
183
|
+
// Use light state (no full buffers) — terminal buffer available via /terminal endpoint.
|
|
184
|
+
// Full buffers were 2-3MB and caused slowness when polled frequently (e.g. Ralph wizard).
|
|
185
|
+
return ctx.getSessionStateWithRespawn(session);
|
|
186
|
+
});
|
|
187
|
+
// ═══════════════════════════════════════════════════════════════
|
|
188
|
+
// Session Data (output, ralph state, run summary, active tools)
|
|
189
|
+
// ═══════════════════════════════════════════════════════════════
|
|
190
|
+
// ========== Get Session Output ==========
|
|
191
|
+
app.get('/api/sessions/:id/output', async (req) => {
|
|
192
|
+
const { id } = req.params;
|
|
193
|
+
const session = ctx.sessions.get(id);
|
|
194
|
+
if (!session) {
|
|
195
|
+
return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
success: true,
|
|
199
|
+
data: {
|
|
200
|
+
textOutput: session.textOutput,
|
|
201
|
+
messages: session.messages,
|
|
202
|
+
errorBuffer: session.errorBuffer,
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
});
|
|
206
|
+
// ========== Get Ralph State ==========
|
|
207
|
+
app.get('/api/sessions/:id/ralph-state', async (req) => {
|
|
208
|
+
const { id } = req.params;
|
|
209
|
+
const session = ctx.sessions.get(id);
|
|
210
|
+
if (!session) {
|
|
211
|
+
return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
success: true,
|
|
215
|
+
data: {
|
|
216
|
+
loop: session.ralphLoopState,
|
|
217
|
+
todos: session.ralphTodos,
|
|
218
|
+
todoStats: session.ralphTodoStats,
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
});
|
|
222
|
+
// ========== Get Run Summary ==========
|
|
223
|
+
app.get('/api/sessions/:id/run-summary', async (req) => {
|
|
224
|
+
const { id } = req.params;
|
|
225
|
+
const session = ctx.sessions.get(id);
|
|
226
|
+
if (!session) {
|
|
227
|
+
return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
|
|
228
|
+
}
|
|
229
|
+
const tracker = ctx.runSummaryTrackers.get(id);
|
|
230
|
+
if (!tracker) {
|
|
231
|
+
// Create a fresh tracker if one doesn't exist (shouldn't happen normally)
|
|
232
|
+
const newTracker = new RunSummaryTracker(id, session.name);
|
|
233
|
+
ctx.runSummaryTrackers.set(id, newTracker);
|
|
234
|
+
return { success: true, summary: newTracker.getSummary() };
|
|
235
|
+
}
|
|
236
|
+
// Update session name in case it changed
|
|
237
|
+
tracker.setSessionName(session.name);
|
|
238
|
+
return { success: true, summary: tracker.getSummary() };
|
|
239
|
+
});
|
|
240
|
+
// ========== Get Active Tools ==========
|
|
241
|
+
app.get('/api/sessions/:id/active-tools', async (req) => {
|
|
242
|
+
const { id } = req.params;
|
|
243
|
+
const session = ctx.sessions.get(id);
|
|
244
|
+
if (!session) {
|
|
245
|
+
return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
success: true,
|
|
249
|
+
data: {
|
|
250
|
+
tools: session.activeTools,
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
});
|
|
254
|
+
// ═══════════════════════════════════════════════════════════════
|
|
255
|
+
// Session Execution (run prompt, interactive mode, shell mode)
|
|
256
|
+
// ═══════════════════════════════════════════════════════════════
|
|
257
|
+
// ========== Run Prompt ==========
|
|
258
|
+
app.post('/api/sessions/:id/run', async (req) => {
|
|
259
|
+
const { id } = req.params;
|
|
260
|
+
const result = RunPromptSchema.safeParse(req.body);
|
|
261
|
+
if (!result.success) {
|
|
262
|
+
return createErrorResponse(ApiErrorCode.INVALID_INPUT, result.error.issues[0]?.message ?? 'Validation failed');
|
|
263
|
+
}
|
|
264
|
+
const { prompt } = result.data;
|
|
265
|
+
const session = ctx.sessions.get(id);
|
|
266
|
+
if (!session) {
|
|
267
|
+
return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
|
|
268
|
+
}
|
|
269
|
+
if (session.isBusy()) {
|
|
270
|
+
return createErrorResponse(ApiErrorCode.SESSION_BUSY, 'Session is busy');
|
|
271
|
+
}
|
|
272
|
+
// Run async, don't wait
|
|
273
|
+
session.runPrompt(prompt).catch((err) => {
|
|
274
|
+
ctx.broadcast(SseEvent.SessionError, { id, error: err.message });
|
|
275
|
+
});
|
|
276
|
+
ctx.broadcast(SseEvent.SessionRunning, { id, prompt });
|
|
277
|
+
return { success: true };
|
|
278
|
+
});
|
|
279
|
+
// ========== Start Interactive Mode ==========
|
|
280
|
+
app.post('/api/sessions/:id/interactive', async (req) => {
|
|
281
|
+
const { id } = req.params;
|
|
282
|
+
const session = ctx.sessions.get(id);
|
|
283
|
+
if (!session) {
|
|
284
|
+
return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
|
|
285
|
+
}
|
|
286
|
+
if (session.isBusy()) {
|
|
287
|
+
return createErrorResponse(ApiErrorCode.SESSION_BUSY, 'Session is busy');
|
|
288
|
+
}
|
|
289
|
+
try {
|
|
290
|
+
// Auto-detect completion phrase from CLAUDE.md BEFORE starting (only if globally enabled and not explicitly disabled by user)
|
|
291
|
+
// Ralph tracker is not supported for opencode sessions
|
|
292
|
+
if (session.mode !== 'opencode' &&
|
|
293
|
+
ctx.store.getConfig().ralphEnabled &&
|
|
294
|
+
!session.ralphTracker.autoEnableDisabled) {
|
|
295
|
+
autoConfigureRalph(session, session.workingDir, ctx);
|
|
296
|
+
if (!session.ralphTracker.enabled) {
|
|
297
|
+
session.ralphTracker.enable();
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
await session.startInteractive();
|
|
301
|
+
getLifecycleLog().log({
|
|
302
|
+
event: 'started',
|
|
303
|
+
sessionId: id,
|
|
304
|
+
name: session.name,
|
|
305
|
+
mode: session.mode,
|
|
306
|
+
});
|
|
307
|
+
ctx.broadcast(SseEvent.SessionInteractive, { id });
|
|
308
|
+
ctx.broadcast(SseEvent.SessionUpdated, { session: ctx.getSessionStateWithRespawn(session) });
|
|
309
|
+
return { success: true };
|
|
310
|
+
}
|
|
311
|
+
catch (err) {
|
|
312
|
+
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, getErrorMessage(err));
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
// ========== Start Shell Mode ==========
|
|
316
|
+
app.post('/api/sessions/:id/shell', async (req) => {
|
|
317
|
+
const { id } = req.params;
|
|
318
|
+
const session = ctx.sessions.get(id);
|
|
319
|
+
if (!session) {
|
|
320
|
+
return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
|
|
321
|
+
}
|
|
322
|
+
if (session.isBusy()) {
|
|
323
|
+
return createErrorResponse(ApiErrorCode.SESSION_BUSY, 'Session is busy');
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
await session.startShell();
|
|
327
|
+
getLifecycleLog().log({
|
|
328
|
+
event: 'started',
|
|
329
|
+
sessionId: id,
|
|
330
|
+
name: session.name,
|
|
331
|
+
mode: 'shell',
|
|
332
|
+
});
|
|
333
|
+
ctx.broadcast(SseEvent.SessionInteractive, { id, mode: 'shell' });
|
|
334
|
+
ctx.broadcast(SseEvent.SessionUpdated, { session: ctx.getSessionStateWithRespawn(session) });
|
|
335
|
+
return { success: true };
|
|
336
|
+
}
|
|
337
|
+
catch (err) {
|
|
338
|
+
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, getErrorMessage(err));
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
// ═══════════════════════════════════════════════════════════════
|
|
342
|
+
// Terminal I/O (input, resize, buffer)
|
|
343
|
+
// ═══════════════════════════════════════════════════════════════
|
|
344
|
+
// ========== Send Input ==========
|
|
345
|
+
app.post('/api/sessions/:id/input', async (req) => {
|
|
346
|
+
const { id } = req.params;
|
|
347
|
+
const result = SessionInputWithLimitSchema.safeParse(req.body);
|
|
348
|
+
if (!result.success) {
|
|
349
|
+
return createErrorResponse(ApiErrorCode.INVALID_INPUT, result.error.issues[0]?.message ?? 'Validation failed');
|
|
350
|
+
}
|
|
351
|
+
const { input, useMux } = result.data;
|
|
352
|
+
const session = ctx.sessions.get(id);
|
|
353
|
+
if (!session) {
|
|
354
|
+
return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
|
|
355
|
+
}
|
|
356
|
+
const inputStr = String(input);
|
|
357
|
+
if (inputStr.length > MAX_INPUT_LENGTH) {
|
|
358
|
+
return createErrorResponse(ApiErrorCode.INVALID_INPUT, `Input exceeds maximum length (${MAX_INPUT_LENGTH} bytes)`);
|
|
359
|
+
}
|
|
360
|
+
// Write input to PTY. Direct write is synchronous; writeViaMux
|
|
361
|
+
// (tmux send-keys) is fire-and-forget to avoid blocking the HTTP response.
|
|
362
|
+
if (useMux) {
|
|
363
|
+
// Fire-and-forget: don't block HTTP response on tmux child process.
|
|
364
|
+
// Fallback to direct write on failure.
|
|
365
|
+
session
|
|
366
|
+
.writeViaMux(inputStr)
|
|
367
|
+
.then((ok) => {
|
|
368
|
+
if (!ok) {
|
|
369
|
+
console.warn(`[Server] writeViaMux failed for session ${id}, falling back to direct write`);
|
|
370
|
+
session.write(inputStr);
|
|
371
|
+
}
|
|
372
|
+
})
|
|
373
|
+
.catch(() => {
|
|
374
|
+
session.write(inputStr);
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
session.write(inputStr);
|
|
379
|
+
}
|
|
380
|
+
return { success: true };
|
|
381
|
+
});
|
|
382
|
+
// ========== Resize Terminal ==========
|
|
383
|
+
app.post('/api/sessions/:id/resize', async (req) => {
|
|
384
|
+
const { id } = req.params;
|
|
385
|
+
const result = ResizeSchema.safeParse(req.body);
|
|
386
|
+
if (!result.success) {
|
|
387
|
+
return createErrorResponse(ApiErrorCode.INVALID_INPUT, result.error.issues[0]?.message ?? 'Validation failed');
|
|
388
|
+
}
|
|
389
|
+
const { cols, rows } = result.data;
|
|
390
|
+
const session = ctx.sessions.get(id);
|
|
391
|
+
if (!session) {
|
|
392
|
+
return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
|
|
393
|
+
}
|
|
394
|
+
session.resize(cols, rows);
|
|
395
|
+
return { success: true };
|
|
396
|
+
});
|
|
397
|
+
// ========== Get Terminal Buffer ==========
|
|
398
|
+
// Query params:
|
|
399
|
+
// tail=<bytes> - Only return last N bytes (faster initial load)
|
|
400
|
+
app.get('/api/sessions/:id/terminal', async (req) => {
|
|
401
|
+
const { id } = req.params;
|
|
402
|
+
const query = req.query;
|
|
403
|
+
const session = ctx.sessions.get(id);
|
|
404
|
+
if (!session) {
|
|
405
|
+
return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
|
|
406
|
+
}
|
|
407
|
+
const tailBytes = query.tail ? parseInt(query.tail, 10) : 0;
|
|
408
|
+
const fullSize = session.terminalBufferLength;
|
|
409
|
+
let truncated = false;
|
|
410
|
+
let cleanBuffer;
|
|
411
|
+
if (tailBytes > 0 && fullSize > tailBytes) {
|
|
412
|
+
// Fast path: tail from the end, skip expensive banner search on full 2MB buffer.
|
|
413
|
+
// Banner is near the top and gets discarded by tail anyway.
|
|
414
|
+
cleanBuffer = session.terminalBuffer.slice(-tailBytes);
|
|
415
|
+
truncated = true;
|
|
416
|
+
// Avoid starting mid-ANSI-escape: find first newline within the first 4KB
|
|
417
|
+
// and start from there. This prevents xterm.js from parsing a partial escape
|
|
418
|
+
// sequence which corrupts cursor position for all subsequent Ink redraws.
|
|
419
|
+
const firstNewline = cleanBuffer.indexOf('\n');
|
|
420
|
+
if (firstNewline > 0 && firstNewline < 4096) {
|
|
421
|
+
cleanBuffer = cleanBuffer.slice(firstNewline + 1);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
// Full buffer: clean junk before actual Claude content
|
|
426
|
+
cleanBuffer = session.terminalBuffer;
|
|
427
|
+
// Find where Claude banner starts (has color codes before "Claude")
|
|
428
|
+
const claudeMatch = cleanBuffer.match(CLAUDE_BANNER_PATTERN);
|
|
429
|
+
if (claudeMatch && claudeMatch.index !== undefined && claudeMatch.index > 0) {
|
|
430
|
+
let lineStart = claudeMatch.index;
|
|
431
|
+
while (lineStart > 0 && cleanBuffer[lineStart - 1] !== '\n') {
|
|
432
|
+
lineStart--;
|
|
433
|
+
}
|
|
434
|
+
cleanBuffer = cleanBuffer.slice(lineStart);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
// Remove Ctrl+L and leading whitespace (cheap on tailed subset)
|
|
438
|
+
cleanBuffer = cleanBuffer.replace(CTRL_L_PATTERN, '').replace(LEADING_WHITESPACE_PATTERN, '');
|
|
439
|
+
return {
|
|
440
|
+
terminalBuffer: cleanBuffer,
|
|
441
|
+
status: session.status,
|
|
442
|
+
fullSize,
|
|
443
|
+
truncated,
|
|
444
|
+
};
|
|
445
|
+
});
|
|
446
|
+
// ═══════════════════════════════════════════════════════════════
|
|
447
|
+
// Session Settings (auto-clear, auto-compact, image watcher, flicker filter)
|
|
448
|
+
// ═══════════════════════════════════════════════════════════════
|
|
449
|
+
// ========== Auto-Clear ==========
|
|
450
|
+
app.post('/api/sessions/:id/auto-clear', async (req) => {
|
|
451
|
+
const { id } = req.params;
|
|
452
|
+
const acResult = AutoClearSchema.safeParse(req.body);
|
|
453
|
+
if (!acResult.success) {
|
|
454
|
+
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid request body');
|
|
455
|
+
}
|
|
456
|
+
const body = acResult.data;
|
|
457
|
+
const session = ctx.sessions.get(id);
|
|
458
|
+
if (!session) {
|
|
459
|
+
return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
|
|
460
|
+
}
|
|
461
|
+
session.setAutoClear(body.enabled, body.threshold);
|
|
462
|
+
ctx.persistSessionState(session);
|
|
463
|
+
ctx.broadcast(SseEvent.SessionUpdated, ctx.getSessionStateWithRespawn(session));
|
|
464
|
+
return {
|
|
465
|
+
success: true,
|
|
466
|
+
data: {
|
|
467
|
+
autoClear: {
|
|
468
|
+
enabled: session.autoClearEnabled,
|
|
469
|
+
threshold: session.autoClearThreshold,
|
|
470
|
+
},
|
|
471
|
+
},
|
|
472
|
+
};
|
|
473
|
+
});
|
|
474
|
+
// ========== Auto-Compact ==========
|
|
475
|
+
app.post('/api/sessions/:id/auto-compact', async (req) => {
|
|
476
|
+
const { id } = req.params;
|
|
477
|
+
const compactResult = AutoCompactSchema.safeParse(req.body);
|
|
478
|
+
if (!compactResult.success) {
|
|
479
|
+
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid request body');
|
|
480
|
+
}
|
|
481
|
+
const body = compactResult.data;
|
|
482
|
+
const session = ctx.sessions.get(id);
|
|
483
|
+
if (!session) {
|
|
484
|
+
return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
|
|
485
|
+
}
|
|
486
|
+
session.setAutoCompact(body.enabled, body.threshold, body.prompt);
|
|
487
|
+
ctx.persistSessionState(session);
|
|
488
|
+
ctx.broadcast(SseEvent.SessionUpdated, ctx.getSessionStateWithRespawn(session));
|
|
489
|
+
return {
|
|
490
|
+
success: true,
|
|
491
|
+
data: {
|
|
492
|
+
autoCompact: {
|
|
493
|
+
enabled: session.autoCompactEnabled,
|
|
494
|
+
threshold: session.autoCompactThreshold,
|
|
495
|
+
prompt: session.autoCompactPrompt,
|
|
496
|
+
},
|
|
497
|
+
},
|
|
498
|
+
};
|
|
499
|
+
});
|
|
500
|
+
// ========== Image Watcher ==========
|
|
501
|
+
app.post('/api/sessions/:id/image-watcher', async (req) => {
|
|
502
|
+
const { id } = req.params;
|
|
503
|
+
const iwResult = ImageWatcherSchema.safeParse(req.body);
|
|
504
|
+
if (!iwResult.success) {
|
|
505
|
+
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid request body');
|
|
506
|
+
}
|
|
507
|
+
const body = iwResult.data;
|
|
508
|
+
const session = ctx.sessions.get(id);
|
|
509
|
+
if (!session) {
|
|
510
|
+
return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
|
|
511
|
+
}
|
|
512
|
+
if (body.enabled) {
|
|
513
|
+
imageWatcher.watchSession(session.id, session.workingDir);
|
|
514
|
+
}
|
|
515
|
+
else {
|
|
516
|
+
imageWatcher.unwatchSession(session.id);
|
|
517
|
+
}
|
|
518
|
+
// Store state on session for persistence
|
|
519
|
+
session.imageWatcherEnabled = body.enabled;
|
|
520
|
+
ctx.persistSessionState(session);
|
|
521
|
+
return {
|
|
522
|
+
success: true,
|
|
523
|
+
data: {
|
|
524
|
+
imageWatcherEnabled: body.enabled,
|
|
525
|
+
},
|
|
526
|
+
};
|
|
527
|
+
});
|
|
528
|
+
// ========== Flicker Filter ==========
|
|
529
|
+
app.post('/api/sessions/:id/flicker-filter', async (req) => {
|
|
530
|
+
const { id } = req.params;
|
|
531
|
+
const ffResult = FlickerFilterSchema.safeParse(req.body);
|
|
532
|
+
if (!ffResult.success) {
|
|
533
|
+
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid request body');
|
|
534
|
+
}
|
|
535
|
+
const body = ffResult.data;
|
|
536
|
+
const session = ctx.sessions.get(id);
|
|
537
|
+
if (!session) {
|
|
538
|
+
return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
|
|
539
|
+
}
|
|
540
|
+
session.flickerFilterEnabled = body.enabled;
|
|
541
|
+
ctx.persistSessionState(session);
|
|
542
|
+
ctx.broadcast(SseEvent.SessionUpdated, ctx.getSessionStateWithRespawn(session));
|
|
543
|
+
return {
|
|
544
|
+
success: true,
|
|
545
|
+
data: {
|
|
546
|
+
flickerFilterEnabled: body.enabled,
|
|
547
|
+
},
|
|
548
|
+
};
|
|
549
|
+
});
|
|
550
|
+
// ═══════════════════════════════════════════════════════════════
|
|
551
|
+
// Quick Actions (quick-run, quick-start)
|
|
552
|
+
// ═══════════════════════════════════════════════════════════════
|
|
553
|
+
// ========== Quick Run ==========
|
|
554
|
+
app.post('/api/run', async (req) => {
|
|
555
|
+
// Prevent unbounded session creation
|
|
556
|
+
if (ctx.sessions.size >= MAX_CONCURRENT_SESSIONS) {
|
|
557
|
+
return createErrorResponse(ApiErrorCode.SESSION_BUSY, `Maximum concurrent sessions (${MAX_CONCURRENT_SESSIONS}) reached`);
|
|
558
|
+
}
|
|
559
|
+
const qrResult = QuickRunSchema.safeParse(req.body);
|
|
560
|
+
if (!qrResult.success) {
|
|
561
|
+
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid request body');
|
|
562
|
+
}
|
|
563
|
+
const { prompt, workingDir } = qrResult.data;
|
|
564
|
+
if (!prompt.trim()) {
|
|
565
|
+
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'prompt is required');
|
|
566
|
+
}
|
|
567
|
+
const dir = workingDir || process.cwd();
|
|
568
|
+
// Validate workingDir exists and is a directory
|
|
569
|
+
if (workingDir) {
|
|
570
|
+
try {
|
|
571
|
+
const stat = statSync(dir);
|
|
572
|
+
if (!stat.isDirectory()) {
|
|
573
|
+
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'workingDir is not a directory');
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
catch {
|
|
577
|
+
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'workingDir does not exist');
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
const session = new Session({ workingDir: dir });
|
|
581
|
+
ctx.addSession(session);
|
|
582
|
+
ctx.store.incrementSessionsCreated();
|
|
583
|
+
ctx.persistSessionState(session);
|
|
584
|
+
await ctx.setupSessionListeners(session);
|
|
585
|
+
getLifecycleLog().log({
|
|
586
|
+
event: 'created',
|
|
587
|
+
sessionId: session.id,
|
|
588
|
+
name: session.name,
|
|
589
|
+
reason: 'run_prompt',
|
|
590
|
+
});
|
|
591
|
+
ctx.broadcast(SseEvent.SessionCreated, ctx.getSessionStateWithRespawn(session));
|
|
592
|
+
try {
|
|
593
|
+
const result = await session.runPrompt(prompt);
|
|
594
|
+
// Clean up session after completion to prevent memory leak
|
|
595
|
+
await ctx.cleanupSession(session.id, true, 'run_prompt_complete');
|
|
596
|
+
return { success: true, sessionId: session.id, ...result };
|
|
597
|
+
}
|
|
598
|
+
catch (err) {
|
|
599
|
+
// Clean up session on error too
|
|
600
|
+
await ctx.cleanupSession(session.id, true, 'run_prompt_error');
|
|
601
|
+
return { success: false, sessionId: session.id, error: getErrorMessage(err) };
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
// ========== Quick Start ==========
|
|
605
|
+
app.post('/api/quick-start', async (req) => {
|
|
606
|
+
// Prevent unbounded session creation
|
|
607
|
+
if (ctx.sessions.size >= MAX_CONCURRENT_SESSIONS) {
|
|
608
|
+
return createErrorResponse(ApiErrorCode.SESSION_BUSY, `Maximum concurrent sessions (${MAX_CONCURRENT_SESSIONS}) reached.`);
|
|
609
|
+
}
|
|
610
|
+
const result = QuickStartSchema.safeParse(req.body);
|
|
611
|
+
if (!result.success) {
|
|
612
|
+
return createErrorResponse(ApiErrorCode.INVALID_INPUT, result.error.issues[0]?.message ?? 'Validation failed');
|
|
613
|
+
}
|
|
614
|
+
const { caseName = 'testcase', mode = 'claude', openCodeConfig } = result.data;
|
|
615
|
+
// Check OpenCode availability if requested
|
|
616
|
+
if (mode === 'opencode') {
|
|
617
|
+
const { isOpenCodeAvailable } = await import('../../utils/opencode-cli-resolver.js');
|
|
618
|
+
if (!isOpenCodeAvailable()) {
|
|
619
|
+
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, 'OpenCode CLI not found. Install with: curl -fsSL https://opencode.ai/install | bash');
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
const casePath = join(CASES_DIR, caseName);
|
|
623
|
+
// Security: Path traversal protection - use relative path check
|
|
624
|
+
const resolvedPath = resolve(casePath);
|
|
625
|
+
const resolvedBase = resolve(CASES_DIR);
|
|
626
|
+
const relPath = relative(resolvedBase, resolvedPath);
|
|
627
|
+
if (relPath.startsWith('..') || isAbsolute(relPath)) {
|
|
628
|
+
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid case path');
|
|
629
|
+
}
|
|
630
|
+
// Create case folder and CLAUDE.md if it doesn't exist
|
|
631
|
+
if (!existsSync(casePath)) {
|
|
632
|
+
try {
|
|
633
|
+
mkdirSync(casePath, { recursive: true });
|
|
634
|
+
mkdirSync(join(casePath, 'src'), { recursive: true });
|
|
635
|
+
// Read settings to get custom template path
|
|
636
|
+
const templatePath = await ctx.getDefaultClaudeMdPath();
|
|
637
|
+
const claudeMd = generateClaudeMd(caseName, '', templatePath);
|
|
638
|
+
writeFileSync(join(casePath, 'CLAUDE.md'), claudeMd);
|
|
639
|
+
// Write .claude/settings.local.json with hooks for desktop notifications
|
|
640
|
+
// (Claude-specific — OpenCode uses its own plugin system)
|
|
641
|
+
if (mode !== 'opencode') {
|
|
642
|
+
await writeHooksConfig(casePath);
|
|
643
|
+
}
|
|
644
|
+
ctx.broadcast(SseEvent.CaseCreated, { name: caseName, path: casePath });
|
|
645
|
+
}
|
|
646
|
+
catch (err) {
|
|
647
|
+
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, `Failed to create case: ${getErrorMessage(err)}`);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
// Create a new session with the case as working directory
|
|
651
|
+
// Apply global Nice priority config and model config from settings
|
|
652
|
+
const niceConfig = await ctx.getGlobalNiceConfig();
|
|
653
|
+
const qsModelConfig = await ctx.getModelConfig();
|
|
654
|
+
const qsModel = mode === 'opencode' ? openCodeConfig?.model : mode !== 'shell' ? qsModelConfig?.defaultModel : undefined;
|
|
655
|
+
const qsClaudeModeConfig = await ctx.getClaudeModeConfig();
|
|
656
|
+
const session = new Session({
|
|
657
|
+
workingDir: casePath,
|
|
658
|
+
mux: ctx.mux,
|
|
659
|
+
useMux: true,
|
|
660
|
+
mode: mode,
|
|
661
|
+
niceConfig: niceConfig,
|
|
662
|
+
model: qsModel,
|
|
663
|
+
claudeMode: qsClaudeModeConfig.claudeMode,
|
|
664
|
+
allowedTools: qsClaudeModeConfig.allowedTools,
|
|
665
|
+
openCodeConfig: mode === 'opencode' ? openCodeConfig : undefined,
|
|
666
|
+
});
|
|
667
|
+
// Auto-detect completion phrase from CLAUDE.md BEFORE broadcasting
|
|
668
|
+
// so the initial state already has the phrase configured (only if globally enabled)
|
|
669
|
+
if (mode === 'claude' && ctx.store.getConfig().ralphEnabled) {
|
|
670
|
+
autoConfigureRalph(session, casePath, ctx);
|
|
671
|
+
if (!session.ralphTracker.enabled) {
|
|
672
|
+
session.ralphTracker.enable();
|
|
673
|
+
session.ralphTracker.enableAutoEnable(); // Allow re-enabling on restart
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
ctx.addSession(session);
|
|
677
|
+
ctx.store.incrementSessionsCreated();
|
|
678
|
+
ctx.persistSessionState(session);
|
|
679
|
+
await ctx.setupSessionListeners(session);
|
|
680
|
+
getLifecycleLog().log({
|
|
681
|
+
event: 'created',
|
|
682
|
+
sessionId: session.id,
|
|
683
|
+
name: session.name,
|
|
684
|
+
reason: 'quick_start',
|
|
685
|
+
});
|
|
686
|
+
ctx.broadcast(SseEvent.SessionCreated, ctx.getSessionStateWithRespawn(session));
|
|
687
|
+
// Start in the appropriate mode
|
|
688
|
+
try {
|
|
689
|
+
if (mode === 'shell') {
|
|
690
|
+
await session.startShell();
|
|
691
|
+
getLifecycleLog().log({
|
|
692
|
+
event: 'started',
|
|
693
|
+
sessionId: session.id,
|
|
694
|
+
name: session.name,
|
|
695
|
+
mode: 'shell',
|
|
696
|
+
});
|
|
697
|
+
ctx.broadcast(SseEvent.SessionInteractive, { id: session.id, mode: 'shell' });
|
|
698
|
+
}
|
|
699
|
+
else {
|
|
700
|
+
// Both 'claude' and 'opencode' modes use startInteractive()
|
|
701
|
+
await session.startInteractive();
|
|
702
|
+
getLifecycleLog().log({
|
|
703
|
+
event: 'started',
|
|
704
|
+
sessionId: session.id,
|
|
705
|
+
name: session.name,
|
|
706
|
+
mode,
|
|
707
|
+
});
|
|
708
|
+
ctx.broadcast(SseEvent.SessionInteractive, { id: session.id, mode });
|
|
709
|
+
}
|
|
710
|
+
ctx.broadcast(SseEvent.SessionUpdated, { session: ctx.getSessionStateWithRespawn(session) });
|
|
711
|
+
// Save lastUsedCase to settings for TUI/web sync
|
|
712
|
+
try {
|
|
713
|
+
const settingsFilePath = SETTINGS_PATH;
|
|
714
|
+
let settings = {};
|
|
715
|
+
try {
|
|
716
|
+
settings = JSON.parse(await fs.readFile(settingsFilePath, 'utf-8'));
|
|
717
|
+
}
|
|
718
|
+
catch (err) {
|
|
719
|
+
if (err.code !== 'ENOENT')
|
|
720
|
+
throw err;
|
|
721
|
+
}
|
|
722
|
+
settings.lastUsedCase = caseName;
|
|
723
|
+
const dir = dirname(settingsFilePath);
|
|
724
|
+
if (!existsSync(dir)) {
|
|
725
|
+
mkdirSync(dir, { recursive: true });
|
|
726
|
+
}
|
|
727
|
+
// Use async write to avoid blocking event loop
|
|
728
|
+
fs.writeFile(settingsFilePath, JSON.stringify(settings, null, 2)).catch((err) => {
|
|
729
|
+
// Non-critical but log for debugging
|
|
730
|
+
console.warn('[Server] Failed to save settings (lastUsedCase):', err);
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
catch (err) {
|
|
734
|
+
// Non-critical but log for debugging
|
|
735
|
+
console.warn('[Server] Failed to prepare settings update:', err);
|
|
736
|
+
}
|
|
737
|
+
return {
|
|
738
|
+
success: true,
|
|
739
|
+
sessionId: session.id,
|
|
740
|
+
casePath,
|
|
741
|
+
caseName,
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
catch (err) {
|
|
745
|
+
// Clean up session on error to prevent orphaned resources
|
|
746
|
+
await ctx.cleanupSession(session.id, true, 'quick_start_error');
|
|
747
|
+
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, getErrorMessage(err));
|
|
748
|
+
}
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
//# sourceMappingURL=session-routes.js.map
|