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,699 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview System, config, settings, subagent, and debug routes.
|
|
3
|
+
* Covers status, stats, config CRUD, settings, subagent monitoring,
|
|
4
|
+
* debug/memory, lifecycle logs, screenshots, and various persistence endpoints.
|
|
5
|
+
*/
|
|
6
|
+
import { join, dirname } from 'node:path';
|
|
7
|
+
import { existsSync, mkdirSync, readdirSync } from 'node:fs';
|
|
8
|
+
import fs from 'node:fs/promises';
|
|
9
|
+
import { homedir, totalmem, freemem, loadavg, cpus } from 'node:os';
|
|
10
|
+
import { execSync } from 'node:child_process';
|
|
11
|
+
import { randomBytes } from 'node:crypto';
|
|
12
|
+
import { ApiErrorCode, createErrorResponse, getErrorMessage } from '../../types.js';
|
|
13
|
+
import { ConfigUpdateSchema, SettingsUpdateSchema, ModelConfigUpdateSchema, CpuLimitSchema, SubagentWindowStatesSchema, SubagentParentMapSchema, RevokeSessionSchema, } from '../schemas.js';
|
|
14
|
+
import { subagentWatcher } from '../../subagent-watcher.js';
|
|
15
|
+
import { imageWatcher } from '../../image-watcher.js';
|
|
16
|
+
import { getLifecycleLog } from '../../session-lifecycle-log.js';
|
|
17
|
+
import { findSessionOrFail, formatUptime, SETTINGS_PATH } from '../route-helpers.js';
|
|
18
|
+
import { SseEvent } from '../sse-events.js';
|
|
19
|
+
import { AUTH_COOKIE_NAME } from '../middleware/auth.js';
|
|
20
|
+
import { QR_AUTH_FAILURE_MAX } from '../../config/tunnel-config.js';
|
|
21
|
+
import { AUTH_SESSION_TTL_MS } from '../../config/auth-config.js';
|
|
22
|
+
// Maximum screenshot upload size (10MB)
|
|
23
|
+
const MAX_SCREENSHOT_SIZE = 10 * 1024 * 1024;
|
|
24
|
+
// Screenshots directory
|
|
25
|
+
const SCREENSHOTS_DIR = join(homedir(), '.codeman', 'screenshots');
|
|
26
|
+
/** Cached CPU count — doesn't change at runtime */
|
|
27
|
+
const CPU_COUNT = cpus().length;
|
|
28
|
+
/** Get system CPU and memory usage */
|
|
29
|
+
function getSystemStats() {
|
|
30
|
+
try {
|
|
31
|
+
const totalMem = totalmem();
|
|
32
|
+
// macOS: os.freemem() only returns truly free pages, not cached/purgeable memory.
|
|
33
|
+
// Use vm_stat to get accurate used memory (wired + active + compressed).
|
|
34
|
+
let usedMem;
|
|
35
|
+
if (process.platform === 'darwin') {
|
|
36
|
+
try {
|
|
37
|
+
const vmstat = execSync('vm_stat', { encoding: 'utf-8', timeout: 2000 });
|
|
38
|
+
const pageSize = parseInt(vmstat.match(/page size of (\d+)/)?.[1] || '4096', 10);
|
|
39
|
+
const wired = parseInt(vmstat.match(/Pages wired down:\s+(\d+)/)?.[1] || '0', 10);
|
|
40
|
+
const active = parseInt(vmstat.match(/Pages active:\s+(\d+)/)?.[1] || '0', 10);
|
|
41
|
+
const compressed = parseInt(vmstat.match(/Pages occupied by compressor:\s+(\d+)/)?.[1] || '0', 10);
|
|
42
|
+
usedMem = (wired + active + compressed) * pageSize;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
usedMem = totalMem - freemem();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
usedMem = totalMem - freemem();
|
|
50
|
+
}
|
|
51
|
+
// CPU load average (1 min) as percentage (rough approximation)
|
|
52
|
+
const load = loadavg()[0];
|
|
53
|
+
const cpuPercent = Math.min(100, Math.round((load / CPU_COUNT) * 100));
|
|
54
|
+
return {
|
|
55
|
+
cpu: cpuPercent,
|
|
56
|
+
memory: {
|
|
57
|
+
usedMB: Math.round(usedMem / (1024 * 1024)),
|
|
58
|
+
totalMB: Math.round(totalMem / (1024 * 1024)),
|
|
59
|
+
percent: Math.round((usedMem / totalMem) * 100),
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return {
|
|
65
|
+
cpu: 0,
|
|
66
|
+
memory: { usedMB: 0, totalMB: 0, percent: 0 },
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export function registerSystemRoutes(app, ctx) {
|
|
71
|
+
const windowStatesPath = join(homedir(), '.codeman', 'subagent-window-states.json');
|
|
72
|
+
const parentMapPath = join(homedir(), '.codeman', 'subagent-parents.json');
|
|
73
|
+
// ═══════════════════════════════════════════════════════════════
|
|
74
|
+
// System Status & Health
|
|
75
|
+
// ═══════════════════════════════════════════════════════════════
|
|
76
|
+
// ========== Status ==========
|
|
77
|
+
app.get('/api/status', async () => ctx.getLightState());
|
|
78
|
+
// ========== Tunnel ==========
|
|
79
|
+
app.get('/api/tunnel/status', async () => ctx.tunnelManager.getStatus());
|
|
80
|
+
app.get('/api/tunnel/qr', async (_req, reply) => {
|
|
81
|
+
const url = ctx.tunnelManager.getUrl();
|
|
82
|
+
if (!url) {
|
|
83
|
+
return reply.code(404).send(createErrorResponse(ApiErrorCode.NOT_FOUND, 'Tunnel not running'));
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
const authPassword = process.env.CODEMAN_PASSWORD;
|
|
87
|
+
if (authPassword) {
|
|
88
|
+
// Auth enabled — use cached SVG with embedded short code
|
|
89
|
+
const svg = await ctx.tunnelManager.getQrSvg(url);
|
|
90
|
+
return { svg, authEnabled: true };
|
|
91
|
+
}
|
|
92
|
+
// No auth — just encode the raw tunnel URL
|
|
93
|
+
const QRCode = await import('qrcode');
|
|
94
|
+
const svg = await QRCode.toString(url, { type: 'svg', margin: 2, width: 256 });
|
|
95
|
+
return { svg, authEnabled: false };
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
return reply.code(500).send(createErrorResponse(ApiErrorCode.OPERATION_FAILED, getErrorMessage(err)));
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
// ═══════════════════════════════════════════════════════════════
|
|
102
|
+
// Authentication (QR auth, session revocation)
|
|
103
|
+
// ═══════════════════════════════════════════════════════════════
|
|
104
|
+
// ========== QR Auth Route ==========
|
|
105
|
+
app.get('/q/:code', async (req, reply) => {
|
|
106
|
+
const shortCode = req.params.code;
|
|
107
|
+
const authPassword = process.env.CODEMAN_PASSWORD;
|
|
108
|
+
// No point if auth isn't enabled — just redirect
|
|
109
|
+
if (!authPassword) {
|
|
110
|
+
return reply.redirect('/');
|
|
111
|
+
}
|
|
112
|
+
const clientIp = req.ip;
|
|
113
|
+
// Per-IP rate limit (separate counter from Basic Auth failures)
|
|
114
|
+
const qrFailures = ctx.qrAuthFailures?.get(clientIp) ?? 0;
|
|
115
|
+
if (qrFailures >= QR_AUTH_FAILURE_MAX) {
|
|
116
|
+
return reply.code(429).send('Too Many Requests');
|
|
117
|
+
}
|
|
118
|
+
// Validate and atomically consume the token
|
|
119
|
+
if (!shortCode || !ctx.tunnelManager.consumeToken(shortCode)) {
|
|
120
|
+
ctx.qrAuthFailures?.set(clientIp, qrFailures + 1);
|
|
121
|
+
return reply.code(401).send('Invalid or expired QR code');
|
|
122
|
+
}
|
|
123
|
+
// Issue session cookie (same pattern as Basic Auth success path)
|
|
124
|
+
const sessionToken = randomBytes(32).toString('hex');
|
|
125
|
+
const clientUA = req.headers['user-agent'] ?? '';
|
|
126
|
+
ctx.authSessions?.set(sessionToken, {
|
|
127
|
+
ip: clientIp,
|
|
128
|
+
ua: clientUA,
|
|
129
|
+
createdAt: Date.now(),
|
|
130
|
+
method: 'qr',
|
|
131
|
+
});
|
|
132
|
+
ctx.qrAuthFailures?.delete(clientIp);
|
|
133
|
+
// Audit log
|
|
134
|
+
const lifecycleLog = getLifecycleLog();
|
|
135
|
+
lifecycleLog.log({
|
|
136
|
+
event: 'qr_auth',
|
|
137
|
+
sessionId: 'system',
|
|
138
|
+
extra: {
|
|
139
|
+
ip: clientIp,
|
|
140
|
+
ua: clientUA,
|
|
141
|
+
shortCodePrefix: shortCode.slice(0, 3) + '***',
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
reply.setCookie(AUTH_COOKIE_NAME, sessionToken, {
|
|
145
|
+
httpOnly: true,
|
|
146
|
+
secure: ctx.https,
|
|
147
|
+
sameSite: 'lax',
|
|
148
|
+
maxAge: AUTH_SESSION_TTL_MS / 1000,
|
|
149
|
+
path: '/',
|
|
150
|
+
});
|
|
151
|
+
// Broadcast auth notification — desktop sees who authenticated
|
|
152
|
+
ctx.broadcast(SseEvent.TunnelQrAuthUsed, {
|
|
153
|
+
ip: clientIp,
|
|
154
|
+
ua: clientUA,
|
|
155
|
+
timestamp: Date.now(),
|
|
156
|
+
});
|
|
157
|
+
return reply.redirect('/');
|
|
158
|
+
});
|
|
159
|
+
// ========== QR Regeneration ==========
|
|
160
|
+
app.post('/api/tunnel/qr/regenerate', async () => {
|
|
161
|
+
ctx.tunnelManager.regenerateQrToken();
|
|
162
|
+
return { success: true };
|
|
163
|
+
});
|
|
164
|
+
// ========== Auth Session Revocation ==========
|
|
165
|
+
app.post('/api/auth/revoke', async (req) => {
|
|
166
|
+
const result = RevokeSessionSchema.safeParse(req.body);
|
|
167
|
+
if (result.success && result.data.sessionToken) {
|
|
168
|
+
ctx.authSessions?.delete(result.data.sessionToken);
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
// Revoke all sessions (nuclear option)
|
|
172
|
+
ctx.authSessions?.clear();
|
|
173
|
+
}
|
|
174
|
+
return { success: true };
|
|
175
|
+
});
|
|
176
|
+
// ═══════════════════════════════════════════════════════════════
|
|
177
|
+
// CLI Integrations (OpenCode)
|
|
178
|
+
// ═══════════════════════════════════════════════════════════════
|
|
179
|
+
// ========== OpenCode ==========
|
|
180
|
+
app.get('/api/opencode/status', async () => {
|
|
181
|
+
const { isOpenCodeAvailable, resolveOpenCodeDir } = await import('../../utils/opencode-cli-resolver.js');
|
|
182
|
+
return {
|
|
183
|
+
available: isOpenCodeAvailable(),
|
|
184
|
+
path: resolveOpenCodeDir(),
|
|
185
|
+
};
|
|
186
|
+
});
|
|
187
|
+
// ═══════════════════════════════════════════════════════════════
|
|
188
|
+
// State & Lifecycle (cleanup, lifecycle log, stats)
|
|
189
|
+
// ═══════════════════════════════════════════════════════════════
|
|
190
|
+
// ========== State & Lifecycle ==========
|
|
191
|
+
app.post('/api/cleanup-state', async () => {
|
|
192
|
+
const activeSessionIds = new Set(ctx.sessions.keys());
|
|
193
|
+
const result = ctx.store.cleanupStaleSessions(activeSessionIds);
|
|
194
|
+
const lifecycleLog = getLifecycleLog();
|
|
195
|
+
for (const s of result.cleaned) {
|
|
196
|
+
lifecycleLog.log({ event: 'stale_cleaned', sessionId: s.id, name: s.name });
|
|
197
|
+
}
|
|
198
|
+
return { success: true, cleanedSessions: result.count };
|
|
199
|
+
});
|
|
200
|
+
app.get('/api/session-lifecycle', async (req) => {
|
|
201
|
+
const query = req.query;
|
|
202
|
+
const lifecycleLog = getLifecycleLog();
|
|
203
|
+
const entries = await lifecycleLog.query({
|
|
204
|
+
sessionId: query.sessionId,
|
|
205
|
+
event: query.event,
|
|
206
|
+
since: query.since ? Number(query.since) : undefined,
|
|
207
|
+
limit: query.limit ? Math.min(Number(query.limit), 1000) : 200,
|
|
208
|
+
});
|
|
209
|
+
return { success: true, entries };
|
|
210
|
+
});
|
|
211
|
+
// ========== Stats ==========
|
|
212
|
+
app.get('/api/stats', async () => {
|
|
213
|
+
const activeSessionTokens = {};
|
|
214
|
+
for (const [sessionId, session] of ctx.sessions) {
|
|
215
|
+
activeSessionTokens[sessionId] = {
|
|
216
|
+
inputTokens: session.inputTokens,
|
|
217
|
+
outputTokens: session.outputTokens,
|
|
218
|
+
totalCost: session.totalCost,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
success: true,
|
|
223
|
+
stats: ctx.store.getAggregateStats(activeSessionTokens),
|
|
224
|
+
raw: ctx.store.getGlobalStats(),
|
|
225
|
+
};
|
|
226
|
+
});
|
|
227
|
+
app.get('/api/token-stats', async () => {
|
|
228
|
+
const activeSessionTokens = {};
|
|
229
|
+
for (const [sessionId, session] of ctx.sessions) {
|
|
230
|
+
activeSessionTokens[sessionId] = {
|
|
231
|
+
inputTokens: session.inputTokens,
|
|
232
|
+
outputTokens: session.outputTokens,
|
|
233
|
+
totalCost: session.totalCost,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
return {
|
|
237
|
+
success: true,
|
|
238
|
+
daily: ctx.store.getDailyStats(30),
|
|
239
|
+
totals: ctx.store.getAggregateStats(activeSessionTokens),
|
|
240
|
+
};
|
|
241
|
+
});
|
|
242
|
+
// ═══════════════════════════════════════════════════════════════
|
|
243
|
+
// Configuration & Settings (config, settings, model config, CPU priority)
|
|
244
|
+
// ═══════════════════════════════════════════════════════════════
|
|
245
|
+
// ========== Config ==========
|
|
246
|
+
app.get('/api/config', async () => {
|
|
247
|
+
return { success: true, config: ctx.store.getConfig() };
|
|
248
|
+
});
|
|
249
|
+
app.put('/api/config', async (req) => {
|
|
250
|
+
const parseResult = ConfigUpdateSchema.safeParse(req.body);
|
|
251
|
+
if (!parseResult.success) {
|
|
252
|
+
return createErrorResponse(ApiErrorCode.INVALID_INPUT, `Invalid config: ${parseResult.error.message}`);
|
|
253
|
+
}
|
|
254
|
+
ctx.store.setConfig(parseResult.data);
|
|
255
|
+
return { success: true, config: ctx.store.getConfig() };
|
|
256
|
+
});
|
|
257
|
+
// ========== Debug/Memory ==========
|
|
258
|
+
app.get('/api/debug/memory', async () => {
|
|
259
|
+
const mem = process.memoryUsage();
|
|
260
|
+
const subagentStats = subagentWatcher.getStats();
|
|
261
|
+
const serverMapSizes = {
|
|
262
|
+
sessions: ctx.sessions.size,
|
|
263
|
+
runSummaryTrackers: ctx.runSummaryTrackers.size,
|
|
264
|
+
scheduledRuns: ctx.scheduledRuns.size,
|
|
265
|
+
activePlanOrchestrators: ctx.activePlanOrchestrators.size,
|
|
266
|
+
};
|
|
267
|
+
const totalServerMapEntries = Object.values(serverMapSizes).reduce((a, b) => a + b, 0);
|
|
268
|
+
const totalSubagentMapEntries = Object.values(subagentStats).reduce((a, b) => a + b, 0);
|
|
269
|
+
return {
|
|
270
|
+
memory: {
|
|
271
|
+
rss: mem.rss,
|
|
272
|
+
rssMB: Math.round((mem.rss / 1024 / 1024) * 10) / 10,
|
|
273
|
+
heapUsed: mem.heapUsed,
|
|
274
|
+
heapUsedMB: Math.round((mem.heapUsed / 1024 / 1024) * 10) / 10,
|
|
275
|
+
heapTotal: mem.heapTotal,
|
|
276
|
+
heapTotalMB: Math.round((mem.heapTotal / 1024 / 1024) * 10) / 10,
|
|
277
|
+
external: mem.external,
|
|
278
|
+
externalMB: Math.round((mem.external / 1024 / 1024) * 10) / 10,
|
|
279
|
+
arrayBuffers: mem.arrayBuffers,
|
|
280
|
+
arrayBuffersMB: Math.round((mem.arrayBuffers / 1024 / 1024) * 10) / 10,
|
|
281
|
+
},
|
|
282
|
+
mapSizes: {
|
|
283
|
+
server: serverMapSizes,
|
|
284
|
+
subagentWatcher: subagentStats,
|
|
285
|
+
totals: {
|
|
286
|
+
serverEntries: totalServerMapEntries,
|
|
287
|
+
subagentEntries: totalSubagentMapEntries,
|
|
288
|
+
allEntries: totalServerMapEntries + totalSubagentMapEntries,
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
watchers: {
|
|
292
|
+
fileDebouncers: subagentStats.fileDebouncerCount,
|
|
293
|
+
dirWatchers: subagentStats.dirWatcherCount,
|
|
294
|
+
total: subagentStats.fileDebouncerCount + subagentStats.dirWatcherCount,
|
|
295
|
+
},
|
|
296
|
+
timers: {
|
|
297
|
+
subagentIdleTimers: subagentStats.idleTimerCount,
|
|
298
|
+
total: subagentStats.idleTimerCount,
|
|
299
|
+
},
|
|
300
|
+
uptime: {
|
|
301
|
+
seconds: Math.round(process.uptime()),
|
|
302
|
+
formatted: formatUptime(process.uptime()),
|
|
303
|
+
},
|
|
304
|
+
timestamp: Date.now(),
|
|
305
|
+
};
|
|
306
|
+
});
|
|
307
|
+
// ========== System Stats ==========
|
|
308
|
+
app.get('/api/system/stats', async () => {
|
|
309
|
+
return getSystemStats();
|
|
310
|
+
});
|
|
311
|
+
// ========== Settings ==========
|
|
312
|
+
app.get('/api/settings', async () => {
|
|
313
|
+
try {
|
|
314
|
+
const content = await fs.readFile(SETTINGS_PATH, 'utf-8');
|
|
315
|
+
return JSON.parse(content);
|
|
316
|
+
}
|
|
317
|
+
catch (err) {
|
|
318
|
+
if (err.code !== 'ENOENT') {
|
|
319
|
+
console.error('Failed to read settings:', err);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return {};
|
|
323
|
+
});
|
|
324
|
+
app.put('/api/settings', async (req) => {
|
|
325
|
+
const settingsResult = SettingsUpdateSchema.safeParse(req.body);
|
|
326
|
+
if (!settingsResult.success) {
|
|
327
|
+
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid settings');
|
|
328
|
+
}
|
|
329
|
+
const settings = settingsResult.data;
|
|
330
|
+
try {
|
|
331
|
+
const dir = dirname(SETTINGS_PATH);
|
|
332
|
+
if (!existsSync(dir)) {
|
|
333
|
+
mkdirSync(dir, { recursive: true });
|
|
334
|
+
}
|
|
335
|
+
let existing = {};
|
|
336
|
+
try {
|
|
337
|
+
existing = JSON.parse(await fs.readFile(SETTINGS_PATH, 'utf-8'));
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
/* ignore */
|
|
341
|
+
}
|
|
342
|
+
const merged = { ...existing, ...settings };
|
|
343
|
+
await fs.writeFile(SETTINGS_PATH, JSON.stringify(merged, null, 2));
|
|
344
|
+
// Handle subagent tracking toggle dynamically
|
|
345
|
+
const subagentEnabled = settings.subagentTrackingEnabled ?? true;
|
|
346
|
+
if (subagentEnabled && !subagentWatcher.isRunning()) {
|
|
347
|
+
subagentWatcher.start();
|
|
348
|
+
console.log('Subagent watcher started via settings change');
|
|
349
|
+
}
|
|
350
|
+
else if (!subagentEnabled && subagentWatcher.isRunning()) {
|
|
351
|
+
subagentWatcher.stop();
|
|
352
|
+
console.log('Subagent watcher stopped via settings change');
|
|
353
|
+
}
|
|
354
|
+
// Handle image watcher toggle dynamically
|
|
355
|
+
const imageWatcherEnabled = settings.imageWatcherEnabled ?? false;
|
|
356
|
+
if (imageWatcherEnabled && !imageWatcher.isRunning()) {
|
|
357
|
+
imageWatcher.start();
|
|
358
|
+
// Re-watch all active sessions that have image watcher enabled
|
|
359
|
+
for (const session of ctx.sessions.values()) {
|
|
360
|
+
if (session.imageWatcherEnabled) {
|
|
361
|
+
imageWatcher.watchSession(session.id, session.workingDir);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
console.log('Image watcher started via settings change');
|
|
365
|
+
}
|
|
366
|
+
else if (!imageWatcherEnabled && imageWatcher.isRunning()) {
|
|
367
|
+
imageWatcher.stop();
|
|
368
|
+
console.log('Image watcher stopped via settings change');
|
|
369
|
+
}
|
|
370
|
+
// Handle tunnel toggle dynamically
|
|
371
|
+
if ('tunnelEnabled' in settings) {
|
|
372
|
+
const tunnelEnabled = settings.tunnelEnabled;
|
|
373
|
+
if (tunnelEnabled && !ctx.tunnelManager.isRunning()) {
|
|
374
|
+
ctx.tunnelManager.start(ctx.port, ctx.https);
|
|
375
|
+
console.log('Tunnel started via settings change');
|
|
376
|
+
}
|
|
377
|
+
else if (tunnelEnabled && ctx.tunnelManager.isRunning() && ctx.tunnelManager.getUrl()) {
|
|
378
|
+
// Tunnel already running — re-emit so the client gets the URL
|
|
379
|
+
ctx.broadcast(SseEvent.TunnelStarted, { url: ctx.tunnelManager.getUrl() });
|
|
380
|
+
console.log('Tunnel already running, re-broadcast URL to client');
|
|
381
|
+
}
|
|
382
|
+
else if (!tunnelEnabled && ctx.tunnelManager.isRunning()) {
|
|
383
|
+
ctx.tunnelManager.stop();
|
|
384
|
+
console.log('Tunnel stopped via settings change');
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return { success: true };
|
|
388
|
+
}
|
|
389
|
+
catch (err) {
|
|
390
|
+
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, getErrorMessage(err));
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
// ========== Model Configuration ==========
|
|
394
|
+
app.get('/api/execution/model-config', async () => {
|
|
395
|
+
try {
|
|
396
|
+
const content = await fs.readFile(SETTINGS_PATH, 'utf-8');
|
|
397
|
+
const settings = JSON.parse(content);
|
|
398
|
+
return { success: true, data: settings.modelConfig || {} };
|
|
399
|
+
}
|
|
400
|
+
catch (err) {
|
|
401
|
+
if (err.code !== 'ENOENT') {
|
|
402
|
+
console.error('Failed to read model config:', err);
|
|
403
|
+
}
|
|
404
|
+
return { success: true, data: {} };
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
app.put('/api/execution/model-config', async (req) => {
|
|
408
|
+
const mcResult = ModelConfigUpdateSchema.safeParse(req.body);
|
|
409
|
+
if (!mcResult.success) {
|
|
410
|
+
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid model config');
|
|
411
|
+
}
|
|
412
|
+
const modelConfig = mcResult.data;
|
|
413
|
+
try {
|
|
414
|
+
let existingSettings = {};
|
|
415
|
+
try {
|
|
416
|
+
const content = await fs.readFile(SETTINGS_PATH, 'utf-8');
|
|
417
|
+
existingSettings = JSON.parse(content);
|
|
418
|
+
}
|
|
419
|
+
catch {
|
|
420
|
+
// File doesn't exist yet, start fresh
|
|
421
|
+
}
|
|
422
|
+
existingSettings.modelConfig = modelConfig;
|
|
423
|
+
const dir = dirname(SETTINGS_PATH);
|
|
424
|
+
if (!existsSync(dir)) {
|
|
425
|
+
mkdirSync(dir, { recursive: true });
|
|
426
|
+
}
|
|
427
|
+
await fs.writeFile(SETTINGS_PATH, JSON.stringify(existingSettings, null, 2));
|
|
428
|
+
return { success: true };
|
|
429
|
+
}
|
|
430
|
+
catch (err) {
|
|
431
|
+
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, getErrorMessage(err));
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
// ========== CPU Priority ==========
|
|
435
|
+
app.get('/api/sessions/:id/cpu-limit', async (req) => {
|
|
436
|
+
const { id } = req.params;
|
|
437
|
+
const session = findSessionOrFail(ctx, id);
|
|
438
|
+
return {
|
|
439
|
+
success: true,
|
|
440
|
+
nice: session.niceConfig,
|
|
441
|
+
};
|
|
442
|
+
});
|
|
443
|
+
app.post('/api/sessions/:id/cpu-limit', async (req) => {
|
|
444
|
+
const { id } = req.params;
|
|
445
|
+
const session = findSessionOrFail(ctx, id);
|
|
446
|
+
const clResult = CpuLimitSchema.safeParse(req.body);
|
|
447
|
+
if (!clResult.success) {
|
|
448
|
+
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid request body');
|
|
449
|
+
}
|
|
450
|
+
const body = clResult.data;
|
|
451
|
+
session.setNice(body);
|
|
452
|
+
ctx.persistSessionState(session);
|
|
453
|
+
ctx.broadcast(SseEvent.SessionUpdated, { session: ctx.getSessionStateWithRespawn(session) });
|
|
454
|
+
return {
|
|
455
|
+
success: true,
|
|
456
|
+
nice: session.niceConfig,
|
|
457
|
+
note: 'Nice priority only affects newly created mux sessions, not currently running ones.',
|
|
458
|
+
};
|
|
459
|
+
});
|
|
460
|
+
// ═══════════════════════════════════════════════════════════════
|
|
461
|
+
// Subagent Management (window states, parents, monitoring, transcripts)
|
|
462
|
+
// ═══════════════════════════════════════════════════════════════
|
|
463
|
+
// ========== Subagent Window State Persistence ==========
|
|
464
|
+
app.get('/api/subagent-window-states', async () => {
|
|
465
|
+
try {
|
|
466
|
+
const content = await fs.readFile(windowStatesPath, 'utf-8');
|
|
467
|
+
return JSON.parse(content);
|
|
468
|
+
}
|
|
469
|
+
catch (err) {
|
|
470
|
+
if (err.code !== 'ENOENT') {
|
|
471
|
+
console.error('Failed to read subagent window states:', err);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return { minimized: {}, open: [] };
|
|
475
|
+
});
|
|
476
|
+
app.put('/api/subagent-window-states', async (req) => {
|
|
477
|
+
const swResult = SubagentWindowStatesSchema.safeParse(req.body);
|
|
478
|
+
if (!swResult.success) {
|
|
479
|
+
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid window states');
|
|
480
|
+
}
|
|
481
|
+
const states = swResult.data;
|
|
482
|
+
try {
|
|
483
|
+
const dir = dirname(windowStatesPath);
|
|
484
|
+
if (!existsSync(dir)) {
|
|
485
|
+
mkdirSync(dir, { recursive: true });
|
|
486
|
+
}
|
|
487
|
+
await fs.writeFile(windowStatesPath, JSON.stringify(states, null, 2));
|
|
488
|
+
return { success: true };
|
|
489
|
+
}
|
|
490
|
+
catch (err) {
|
|
491
|
+
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, getErrorMessage(err));
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
// ========== Subagent Parent Associations ==========
|
|
495
|
+
app.get('/api/subagent-parents', async () => {
|
|
496
|
+
try {
|
|
497
|
+
const content = await fs.readFile(parentMapPath, 'utf-8');
|
|
498
|
+
return JSON.parse(content);
|
|
499
|
+
}
|
|
500
|
+
catch (err) {
|
|
501
|
+
if (err.code !== 'ENOENT') {
|
|
502
|
+
console.error('Failed to read subagent parent map:', err);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
return {};
|
|
506
|
+
});
|
|
507
|
+
app.put('/api/subagent-parents', async (req) => {
|
|
508
|
+
const spResult = SubagentParentMapSchema.safeParse(req.body);
|
|
509
|
+
if (!spResult.success) {
|
|
510
|
+
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid parent map');
|
|
511
|
+
}
|
|
512
|
+
const parentMap = spResult.data;
|
|
513
|
+
try {
|
|
514
|
+
const dir = dirname(parentMapPath);
|
|
515
|
+
if (!existsSync(dir)) {
|
|
516
|
+
mkdirSync(dir, { recursive: true });
|
|
517
|
+
}
|
|
518
|
+
await fs.writeFile(parentMapPath, JSON.stringify(parentMap, null, 2));
|
|
519
|
+
return { success: true };
|
|
520
|
+
}
|
|
521
|
+
catch (err) {
|
|
522
|
+
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, getErrorMessage(err));
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
// ========== Subagent Monitoring ==========
|
|
526
|
+
app.get('/api/subagents', async (req) => {
|
|
527
|
+
const { minutes } = req.query;
|
|
528
|
+
const subagents = minutes
|
|
529
|
+
? subagentWatcher.getRecentSubagents(parseInt(minutes, 10))
|
|
530
|
+
: subagentWatcher.getSubagents();
|
|
531
|
+
return { success: true, data: subagents };
|
|
532
|
+
});
|
|
533
|
+
app.get('/api/sessions/:id/subagents', async (req) => {
|
|
534
|
+
const { id } = req.params;
|
|
535
|
+
const session = findSessionOrFail(ctx, id);
|
|
536
|
+
const subagents = subagentWatcher.getSubagentsForSession(session.workingDir);
|
|
537
|
+
return { success: true, data: subagents };
|
|
538
|
+
});
|
|
539
|
+
app.get('/api/subagents/:agentId', async (req) => {
|
|
540
|
+
const { agentId } = req.params;
|
|
541
|
+
const info = subagentWatcher.getSubagent(agentId);
|
|
542
|
+
if (!info) {
|
|
543
|
+
return createErrorResponse(ApiErrorCode.NOT_FOUND, `Subagent ${agentId} not found`);
|
|
544
|
+
}
|
|
545
|
+
return { success: true, data: info };
|
|
546
|
+
});
|
|
547
|
+
app.get('/api/subagents/:agentId/transcript', async (req) => {
|
|
548
|
+
const { agentId } = req.params;
|
|
549
|
+
const { limit, format } = req.query;
|
|
550
|
+
const limitNum = limit ? parseInt(limit, 10) : undefined;
|
|
551
|
+
const transcript = await subagentWatcher.getTranscript(agentId, limitNum);
|
|
552
|
+
if (format === 'formatted') {
|
|
553
|
+
const formatted = subagentWatcher.formatTranscript(transcript);
|
|
554
|
+
return { success: true, data: { formatted, entryCount: transcript.length } };
|
|
555
|
+
}
|
|
556
|
+
return { success: true, data: transcript };
|
|
557
|
+
});
|
|
558
|
+
app.delete('/api/subagents/:agentId', async (req) => {
|
|
559
|
+
const { agentId } = req.params;
|
|
560
|
+
const info = subagentWatcher.getSubagent(agentId);
|
|
561
|
+
if (!info) {
|
|
562
|
+
return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Subagent not found');
|
|
563
|
+
}
|
|
564
|
+
const killed = await subagentWatcher.killSubagent(agentId);
|
|
565
|
+
if (killed) {
|
|
566
|
+
return { success: true, data: { agentId, status: 'killed' } };
|
|
567
|
+
}
|
|
568
|
+
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, 'Subagent not found or already completed');
|
|
569
|
+
});
|
|
570
|
+
app.post('/api/subagents/cleanup', async () => {
|
|
571
|
+
const removed = subagentWatcher.cleanupNow();
|
|
572
|
+
return { success: true, data: { removed, remaining: subagentWatcher.getSubagents().length } };
|
|
573
|
+
});
|
|
574
|
+
app.delete('/api/subagents', async () => {
|
|
575
|
+
const cleared = subagentWatcher.clearAll();
|
|
576
|
+
return { success: true, data: { cleared } };
|
|
577
|
+
});
|
|
578
|
+
// ═══════════════════════════════════════════════════════════════
|
|
579
|
+
// Screenshots (upload, list, serve)
|
|
580
|
+
// ═══════════════════════════════════════════════════════════════
|
|
581
|
+
// ========== Screenshots ==========
|
|
582
|
+
app.post('/api/screenshots', async (req, reply) => {
|
|
583
|
+
const contentType = req.headers['content-type'] ?? '';
|
|
584
|
+
if (!contentType.includes('multipart/form-data')) {
|
|
585
|
+
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Expected multipart/form-data');
|
|
586
|
+
}
|
|
587
|
+
// Parse multipart boundary
|
|
588
|
+
const boundaryMatch = contentType.match(/boundary=(.+?)(?:;|$)/);
|
|
589
|
+
if (!boundaryMatch) {
|
|
590
|
+
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Missing boundary');
|
|
591
|
+
}
|
|
592
|
+
// Collect raw body
|
|
593
|
+
const chunks = [];
|
|
594
|
+
let totalSize = 0;
|
|
595
|
+
for await (const chunk of req.raw) {
|
|
596
|
+
totalSize += chunk.length;
|
|
597
|
+
if (totalSize > MAX_SCREENSHOT_SIZE) {
|
|
598
|
+
reply.status(413);
|
|
599
|
+
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'File too large (max 10MB)');
|
|
600
|
+
}
|
|
601
|
+
chunks.push(chunk);
|
|
602
|
+
}
|
|
603
|
+
const body = Buffer.concat(chunks);
|
|
604
|
+
// Extract file from multipart body
|
|
605
|
+
const boundary = '--' + boundaryMatch[1];
|
|
606
|
+
const boundaryBuf = Buffer.from(boundary);
|
|
607
|
+
const parts = [];
|
|
608
|
+
let pos = 0;
|
|
609
|
+
// Find each part between boundaries
|
|
610
|
+
while (pos < body.length) {
|
|
611
|
+
const start = body.indexOf(boundaryBuf, pos);
|
|
612
|
+
if (start === -1)
|
|
613
|
+
break;
|
|
614
|
+
const afterBoundary = start + boundaryBuf.length;
|
|
615
|
+
// Check for closing boundary (--)
|
|
616
|
+
if (body[afterBoundary] === 0x2d && body[afterBoundary + 1] === 0x2d)
|
|
617
|
+
break;
|
|
618
|
+
// Skip \r\n after boundary
|
|
619
|
+
const headerStart = afterBoundary + 2;
|
|
620
|
+
const headerEnd = body.indexOf(Buffer.from('\r\n\r\n'), headerStart);
|
|
621
|
+
if (headerEnd === -1)
|
|
622
|
+
break;
|
|
623
|
+
const headers = body.subarray(headerStart, headerEnd).toString();
|
|
624
|
+
const dataStart = headerEnd + 4;
|
|
625
|
+
const nextBoundary = body.indexOf(boundaryBuf, dataStart);
|
|
626
|
+
// Data ends 2 bytes before next boundary (\r\n)
|
|
627
|
+
const dataEnd = nextBoundary === -1 ? body.length : nextBoundary - 2;
|
|
628
|
+
parts.push({ headers, data: body.subarray(dataStart, dataEnd) });
|
|
629
|
+
pos = nextBoundary === -1 ? body.length : nextBoundary;
|
|
630
|
+
}
|
|
631
|
+
const filePart = parts.find((p) => p.headers.includes('name="file"'));
|
|
632
|
+
if (!filePart || filePart.data.length === 0) {
|
|
633
|
+
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'No file uploaded');
|
|
634
|
+
}
|
|
635
|
+
// Determine extension from Content-Type or filename
|
|
636
|
+
let ext = '.png';
|
|
637
|
+
const filenameMatch = filePart.headers.match(/filename="(.+?)"/);
|
|
638
|
+
if (filenameMatch) {
|
|
639
|
+
const origExt = filenameMatch[1].match(/\.(png|jpg|jpeg|webp|gif)$/i);
|
|
640
|
+
if (origExt)
|
|
641
|
+
ext = origExt[0].toLowerCase();
|
|
642
|
+
}
|
|
643
|
+
const ctMatch = filePart.headers.match(/Content-Type:\s*image\/(png|jpeg|webp|gif)/i);
|
|
644
|
+
if (ctMatch) {
|
|
645
|
+
const map = {
|
|
646
|
+
png: '.png',
|
|
647
|
+
jpeg: '.jpg',
|
|
648
|
+
webp: '.webp',
|
|
649
|
+
gif: '.gif',
|
|
650
|
+
};
|
|
651
|
+
ext = map[ctMatch[1].toLowerCase()] ?? ext;
|
|
652
|
+
}
|
|
653
|
+
// Save to ~/.codeman/screenshots/
|
|
654
|
+
if (!existsSync(SCREENSHOTS_DIR)) {
|
|
655
|
+
mkdirSync(SCREENSHOTS_DIR, { recursive: true });
|
|
656
|
+
}
|
|
657
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').replace('T', '_').slice(0, 19);
|
|
658
|
+
const filename = `screenshot_${timestamp}${ext}`;
|
|
659
|
+
const filepath = join(SCREENSHOTS_DIR, filename);
|
|
660
|
+
await fs.writeFile(filepath, filePart.data);
|
|
661
|
+
return { success: true, path: filepath, filename };
|
|
662
|
+
});
|
|
663
|
+
app.get('/api/screenshots', async () => {
|
|
664
|
+
if (!existsSync(SCREENSHOTS_DIR)) {
|
|
665
|
+
return { files: [] };
|
|
666
|
+
}
|
|
667
|
+
const files = readdirSync(SCREENSHOTS_DIR)
|
|
668
|
+
.filter((f) => /\.(png|jpg|jpeg|webp|gif)$/i.test(f))
|
|
669
|
+
.sort()
|
|
670
|
+
.reverse()
|
|
671
|
+
.slice(0, 50)
|
|
672
|
+
.map((name) => ({ name, path: join(SCREENSHOTS_DIR, name) }));
|
|
673
|
+
return { files };
|
|
674
|
+
});
|
|
675
|
+
app.get('/api/screenshots/:name', async (req, reply) => {
|
|
676
|
+
const { name } = req.params;
|
|
677
|
+
// Prevent path traversal
|
|
678
|
+
if (name.includes('/') || name.includes('\\') || name.includes('..')) {
|
|
679
|
+
reply.status(400);
|
|
680
|
+
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid filename');
|
|
681
|
+
}
|
|
682
|
+
const filepath = join(SCREENSHOTS_DIR, name);
|
|
683
|
+
if (!existsSync(filepath)) {
|
|
684
|
+
reply.status(404);
|
|
685
|
+
return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Screenshot not found');
|
|
686
|
+
}
|
|
687
|
+
const ext = name.match(/\.(png|jpg|jpeg|webp|gif)$/i)?.[1]?.toLowerCase() ?? 'png';
|
|
688
|
+
const mimeMap = {
|
|
689
|
+
png: 'image/png',
|
|
690
|
+
jpg: 'image/jpeg',
|
|
691
|
+
jpeg: 'image/jpeg',
|
|
692
|
+
webp: 'image/webp',
|
|
693
|
+
gif: 'image/gif',
|
|
694
|
+
};
|
|
695
|
+
reply.type(mimeMap[ext] ?? 'image/png');
|
|
696
|
+
return fs.readFile(filepath);
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
//# sourceMappingURL=system-routes.js.map
|