aicodeman 0.4.7 → 0.5.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.
Files changed (91) hide show
  1. package/dist/bash-tool-parser.d.ts.map +1 -1
  2. package/dist/bash-tool-parser.js +11 -4
  3. package/dist/bash-tool-parser.js.map +1 -1
  4. package/dist/web/public/api-client.3adebdc2.js.gz +0 -0
  5. package/dist/web/public/app.916baeb9.js +26 -0
  6. package/dist/web/public/app.916baeb9.js.br +0 -0
  7. package/dist/web/public/app.916baeb9.js.gz +0 -0
  8. package/dist/web/public/constants.64161167.js.gz +0 -0
  9. package/dist/web/public/index.html +11 -11
  10. package/dist/web/public/index.html.br +0 -0
  11. package/dist/web/public/index.html.gz +0 -0
  12. package/dist/web/public/input-cjk.92544c51.js.gz +0 -0
  13. package/dist/web/public/keyboard-accessory.9fb81db6.js.gz +0 -0
  14. package/dist/web/public/mobile-handlers.1e2a8ef8.js.gz +0 -0
  15. package/dist/web/public/mobile.fdd28a54.css.gz +0 -0
  16. package/dist/web/public/notification-manager.2d5ea8ec.js.gz +0 -0
  17. package/dist/web/public/orchestrator-panel.js.gz +0 -0
  18. package/dist/web/public/{panels-ui.3dd2e29b.js → panels-ui.8204db1e.js} +2 -2
  19. package/dist/web/public/panels-ui.8204db1e.js.br +0 -0
  20. package/dist/web/public/panels-ui.8204db1e.js.gz +0 -0
  21. package/dist/web/public/{ralph-panel.7b014f16.js → ralph-panel.a2733fd5.js} +1 -1
  22. package/dist/web/public/ralph-panel.a2733fd5.js.br +0 -0
  23. package/dist/web/public/ralph-panel.a2733fd5.js.gz +0 -0
  24. package/dist/web/public/ralph-wizard.f31ab90e.js.gz +0 -0
  25. package/dist/web/public/respawn-ui.372c6ea7.js.gz +0 -0
  26. package/dist/web/public/session-ui.0a07c3b7.js.gz +0 -0
  27. package/dist/web/public/settings-ui.94c57184.js.gz +0 -0
  28. package/dist/web/public/styles.5c87d847.css +1 -0
  29. package/dist/web/public/styles.5c87d847.css.br +0 -0
  30. package/dist/web/public/styles.5c87d847.css.gz +0 -0
  31. package/dist/web/public/subagent-windows.a366a4ad.js.gz +0 -0
  32. package/dist/web/public/sw.js.gz +0 -0
  33. package/dist/web/public/terminal-ui.b66dbf4e.js +3 -0
  34. package/dist/web/public/terminal-ui.b66dbf4e.js.br +0 -0
  35. package/dist/web/public/terminal-ui.b66dbf4e.js.gz +0 -0
  36. package/dist/web/public/upload.html.gz +0 -0
  37. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  38. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  39. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  40. package/dist/web/public/vendor/xterm-zerolag-input.137ad9f0.js.gz +0 -0
  41. package/dist/web/public/vendor/xterm.css.gz +0 -0
  42. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  43. package/dist/web/public/voice-input.085e9e73.js.gz +0 -0
  44. package/dist/web/route-helpers.d.ts +11 -0
  45. package/dist/web/route-helpers.d.ts.map +1 -1
  46. package/dist/web/route-helpers.js +23 -0
  47. package/dist/web/route-helpers.js.map +1 -1
  48. package/dist/web/routes/case-routes.d.ts.map +1 -1
  49. package/dist/web/routes/case-routes.js +3 -11
  50. package/dist/web/routes/case-routes.js.map +1 -1
  51. package/dist/web/routes/hook-event-routes.d.ts.map +1 -1
  52. package/dist/web/routes/hook-event-routes.js +2 -6
  53. package/dist/web/routes/hook-event-routes.js.map +1 -1
  54. package/dist/web/routes/orchestrator-routes.d.ts.map +1 -1
  55. package/dist/web/routes/orchestrator-routes.js +4 -10
  56. package/dist/web/routes/orchestrator-routes.js.map +1 -1
  57. package/dist/web/routes/plan-routes.d.ts.map +1 -1
  58. package/dist/web/routes/plan-routes.js +6 -26
  59. package/dist/web/routes/plan-routes.js.map +1 -1
  60. package/dist/web/routes/push-routes.d.ts.map +1 -1
  61. package/dist/web/routes/push-routes.js +4 -10
  62. package/dist/web/routes/push-routes.js.map +1 -1
  63. package/dist/web/routes/ralph-routes.d.ts.map +1 -1
  64. package/dist/web/routes/ralph-routes.js +13 -53
  65. package/dist/web/routes/ralph-routes.js.map +1 -1
  66. package/dist/web/routes/respawn-routes.d.ts.map +1 -1
  67. package/dist/web/routes/respawn-routes.js +3 -11
  68. package/dist/web/routes/respawn-routes.js.map +1 -1
  69. package/dist/web/routes/scheduled-routes.d.ts.map +1 -1
  70. package/dist/web/routes/scheduled-routes.js +2 -5
  71. package/dist/web/routes/scheduled-routes.js.map +1 -1
  72. package/dist/web/routes/session-routes.d.ts.map +1 -1
  73. package/dist/web/routes/session-routes.js +113 -169
  74. package/dist/web/routes/session-routes.js.map +1 -1
  75. package/dist/web/routes/system-routes.d.ts.map +1 -1
  76. package/dist/web/routes/system-routes.js +8 -31
  77. package/dist/web/routes/system-routes.js.map +1 -1
  78. package/package.json +1 -1
  79. package/dist/web/public/app.cbf6e9e8.js +0 -26
  80. package/dist/web/public/app.cbf6e9e8.js.br +0 -0
  81. package/dist/web/public/app.cbf6e9e8.js.gz +0 -0
  82. package/dist/web/public/panels-ui.3dd2e29b.js.br +0 -0
  83. package/dist/web/public/panels-ui.3dd2e29b.js.gz +0 -0
  84. package/dist/web/public/ralph-panel.7b014f16.js.br +0 -0
  85. package/dist/web/public/ralph-panel.7b014f16.js.gz +0 -0
  86. package/dist/web/public/styles.8e110d27.css +0 -1
  87. package/dist/web/public/styles.8e110d27.css.br +0 -0
  88. package/dist/web/public/styles.8e110d27.css.gz +0 -0
  89. package/dist/web/public/terminal-ui.9b40798a.js +0 -3
  90. package/dist/web/public/terminal-ui.9b40798a.js.br +0 -0
  91. package/dist/web/public/terminal-ui.9b40798a.js.gz +0 -0
@@ -5,16 +5,13 @@
5
5
  import { statSync } from 'node:fs';
6
6
  import { ApiErrorCode, createErrorResponse } from '../../types.js';
7
7
  import { ScheduledRunSchema } from '../schemas.js';
8
+ import { parseBody } from '../route-helpers.js';
8
9
  export function registerScheduledRoutes(app, ctx) {
9
10
  app.get('/api/scheduled', async () => {
10
11
  return Array.from(ctx.scheduledRuns.values());
11
12
  });
12
13
  app.post('/api/scheduled', async (req) => {
13
- const srResult = ScheduledRunSchema.safeParse(req.body);
14
- if (!srResult.success) {
15
- return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid request body');
16
- }
17
- const { prompt, workingDir, durationMinutes } = srResult.data;
14
+ const { prompt, workingDir, durationMinutes } = parseBody(ScheduledRunSchema, req.body, 'Invalid request body');
18
15
  // Validate workingDir exists and is a directory
19
16
  if (workingDir) {
20
17
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"scheduled-routes.js","sourceRoot":"","sources":["../../../src/web/routes/scheduled-routes.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAoB,MAAM,gBAAgB,CAAC;AACrF,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAGnD,MAAM,UAAU,uBAAuB,CAAC,GAAoB,EAAE,GAAwC;IACpG,GAAG,CAAC,GAAG,CAAC,gBAAgB,EAAE,KAAK,IAAI,EAAE;QACnC,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,aAAa,CAAC,MAAM,EAAE,CAAC,CAAC;IAChD,CAAC,CAAC,CAAC;IAEH,GAAG,CAAC,IAAI,CAAC,gBAAgB,EAAE,KAAK,EAAE,GAAG,EAAyE,EAAE;QAC9G,MAAM,QAAQ,GAAG,kBAAkB,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACxD,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC;YACtB,OAAO,mBAAmB,CAAC,YAAY,CAAC,aAAa,EAAE,sBAAsB,CAAC,CAAC;QACjF,CAAC;QACD,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,eAAe,EAAE,GAAG,QAAQ,CAAC,IAAI,CAAC;QAE9D,gDAAgD;QAChD,IAAI,UAAU,EAAE,CAAC;YACf,IAAI,CAAC;gBACH,MAAM,IAAI,GAAG,QAAQ,CAAC,UAAU,CAAC,CAAC;gBAClC,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;oBACxB,OAAO,mBAAmB,CAAC,YAAY,CAAC,aAAa,EAAE,+BAA+B,CAAC,CAAC;gBAC1F,CAAC;YACH,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,mBAAmB,CAAC,YAAY,CAAC,aAAa,EAAE,2BAA2B,CAAC,CAAC;YACtF,CAAC;QACH,CAAC;QAED,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,iBAAiB,CAAC,MAAM,EAAE,UAAU,IAAI,OAAO,CAAC,GAAG,EAAE,EAAE,eAAe,IAAI,EAAE,CAAC,CAAC;QACpG,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC;IAChC,CAAC,CAAC,CAAC;IAEH,GAAG,CAAC,MAAM,CAAC,oBAAoB,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;QAC7C,MAAM,EAAE,EAAE,EAAE,GAAG,GAAG,CAAC,MAAwB,CAAC;QAC5C,MAAM,GAAG,GAAG,GAAG,CAAC,aAAa,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAEtC,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,OAAO,mBAAmB,CAAC,YAAY,CAAC,SAAS,EAAE,yBAAyB,CAAC,CAAC;QAChF,CAAC;QAED,MAAM,GAAG,CAAC,gBAAgB,CAAC,EAAE,CAAC,CAAC;QAC/B,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAC3B,CAAC,CAAC,CAAC;IAEH,GAAG,CAAC,GAAG,CAAC,oBAAoB,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;QAC1C,MAAM,EAAE,EAAE,EAAE,GAAG,GAAG,CAAC,MAAwB,CAAC;QAC5C,MAAM,GAAG,GAAG,GAAG,CAAC,aAAa,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAEtC,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,OAAO,mBAAmB,CAAC,YAAY,CAAC,SAAS,EAAE,yBAAyB,CAAC,CAAC;QAChF,CAAC;QAED,OAAO,GAAG,CAAC;IACb,CAAC,CAAC,CAAC;AACL,CAAC"}
1
+ {"version":3,"file":"scheduled-routes.js","sourceRoot":"","sources":["../../../src/web/routes/scheduled-routes.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAoB,MAAM,gBAAgB,CAAC;AACrF,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAGhD,MAAM,UAAU,uBAAuB,CAAC,GAAoB,EAAE,GAAwC;IACpG,GAAG,CAAC,GAAG,CAAC,gBAAgB,EAAE,KAAK,IAAI,EAAE;QACnC,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,aAAa,CAAC,MAAM,EAAE,CAAC,CAAC;IAChD,CAAC,CAAC,CAAC;IAEH,GAAG,CAAC,IAAI,CAAC,gBAAgB,EAAE,KAAK,EAAE,GAAG,EAAyE,EAAE;QAC9G,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,eAAe,EAAE,GAAG,SAAS,CAAC,kBAAkB,EAAE,GAAG,CAAC,IAAI,EAAE,sBAAsB,CAAC,CAAC;QAEhH,gDAAgD;QAChD,IAAI,UAAU,EAAE,CAAC;YACf,IAAI,CAAC;gBACH,MAAM,IAAI,GAAG,QAAQ,CAAC,UAAU,CAAC,CAAC;gBAClC,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;oBACxB,OAAO,mBAAmB,CAAC,YAAY,CAAC,aAAa,EAAE,+BAA+B,CAAC,CAAC;gBAC1F,CAAC;YACH,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,mBAAmB,CAAC,YAAY,CAAC,aAAa,EAAE,2BAA2B,CAAC,CAAC;YACtF,CAAC;QACH,CAAC;QAED,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,iBAAiB,CAAC,MAAM,EAAE,UAAU,IAAI,OAAO,CAAC,GAAG,EAAE,EAAE,eAAe,IAAI,EAAE,CAAC,CAAC;QACpG,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC;IAChC,CAAC,CAAC,CAAC;IAEH,GAAG,CAAC,MAAM,CAAC,oBAAoB,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;QAC7C,MAAM,EAAE,EAAE,EAAE,GAAG,GAAG,CAAC,MAAwB,CAAC;QAC5C,MAAM,GAAG,GAAG,GAAG,CAAC,aAAa,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAEtC,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,OAAO,mBAAmB,CAAC,YAAY,CAAC,SAAS,EAAE,yBAAyB,CAAC,CAAC;QAChF,CAAC;QAED,MAAM,GAAG,CAAC,gBAAgB,CAAC,EAAE,CAAC,CAAC;QAC/B,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAC3B,CAAC,CAAC,CAAC;IAEH,GAAG,CAAC,GAAG,CAAC,oBAAoB,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;QAC1C,MAAM,EAAE,EAAE,EAAE,GAAG,GAAG,CAAC,MAAwB,CAAC;QAC5C,MAAM,GAAG,GAAG,GAAG,CAAC,aAAa,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAEtC,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,OAAO,mBAAmB,CAAC,YAAY,CAAC,SAAS,EAAE,yBAAyB,CAAC,CAAC;QAChF,CAAC;QAED,OAAO,GAAG,CAAC;IACb,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"session-routes.d.ts","sourceRoot":"","sources":["../../../src/web/routes/session-routes.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAkC1C,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AA8CjG,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,WAAW,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,GAAG,QAAQ,GAC/D,IAAI,CA2+BN"}
1
+ {"version":3,"file":"session-routes.d.ts","sourceRoot":"","sources":["../../../src/web/routes/session-routes.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AA0C1C,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AA2DjG,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,WAAW,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,GAAG,QAAQ,GAC/D,IAAI,CA25BN"}
@@ -10,7 +10,7 @@ import { ApiErrorCode, createErrorResponse, getErrorMessage, } from '../../types
10
10
  import { Session } from '../../session.js';
11
11
  import { SseEvent } from '../sse-events.js';
12
12
  import { CreateSessionSchema, SessionNameSchema, SessionColorSchema, RunPromptSchema, SessionInputWithLimitSchema, ResizeSchema, AutoClearSchema, AutoCompactSchema, ImageWatcherSchema, FlickerFilterSchema, QuickRunSchema, QuickStartSchema, } from '../schemas.js';
13
- import { autoConfigureRalph, CASES_DIR, SETTINGS_PATH, validatePathWithinBase } from '../route-helpers.js';
13
+ import { autoConfigureRalph, CASES_DIR, findSessionOrFail, parseBody, persistAndBroadcastSession, SETTINGS_PATH, validatePathWithinBase, } from '../route-helpers.js';
14
14
  import { AUTH_COOKIE_NAME } from '../middleware/auth.js';
15
15
  import { writeHooksConfig, updateCaseEnvVars } from '../../hooks-config.js';
16
16
  import { generateClaudeMd } from '../../templates/claude-md.js';
@@ -46,14 +46,26 @@ function stripInkRedrawBloat(buffer) {
46
46
  // If the redraw section is small (<16KB), not worth stripping
47
47
  if (redrawPart.length < 16384)
48
48
  return buffer;
49
- // Keep only the last 4KB of redraw frames this preserves the final visual state
50
- // (spinner position, status bar text, token count, etc.)
51
- const tail = redrawPart.slice(-4096);
52
- // Avoid starting mid-escape: find first complete frame boundary
49
+ // Find the last complete Ink frame by searching for where the VPA row
50
+ // number drops (cursor jumps back to viewport top for a new render cycle).
51
+ // Search the last 64KB — a single Ink frame with response content can be
52
+ // 10-20KB, so 4KB was too small and caused partial frames (blank gap).
53
+ const searchLen = Math.min(redrawPart.length, 65536);
54
+ const searchWindow = redrawPart.slice(-searchLen);
53
55
  // eslint-disable-next-line no-control-regex
54
- const frameStart = tail.search(/\x1b\(B\x1b\[m|\x1b\[\d+d|\x1b\[\d+;\d+H/);
55
- const cleanTail = frameStart > 0 ? tail.slice(frameStart) : tail;
56
- return contentPart + cleanTail;
56
+ const vpaRe = /\x1b\[(\d+)d/g;
57
+ let lastFrameStart = 0;
58
+ let prevRow = -1;
59
+ let match;
60
+ while ((match = vpaRe.exec(searchWindow)) !== null) {
61
+ const row = parseInt(match[1], 10);
62
+ // Row number dropped significantly — Ink started a new frame
63
+ if (prevRow > 0 && row < prevRow - 5) {
64
+ lastFrameStart = match.index;
65
+ }
66
+ prevRow = row;
67
+ }
68
+ return contentPart + searchWindow.slice(lastFrameStart);
57
69
  }
58
70
  export function registerSessionRoutes(app, ctx) {
59
71
  // ═══════════════════════════════════════════════════════════════
@@ -82,11 +94,7 @@ export function registerSessionRoutes(app, ctx) {
82
94
  if (ctx.sessions.size >= MAX_CONCURRENT_SESSIONS) {
83
95
  return createErrorResponse(ApiErrorCode.OPERATION_FAILED, `Maximum concurrent sessions (${MAX_CONCURRENT_SESSIONS}) reached. Delete some sessions first.`);
84
96
  }
85
- const result = CreateSessionSchema.safeParse(req.body);
86
- if (!result.success) {
87
- return createErrorResponse(ApiErrorCode.INVALID_INPUT, result.error.issues[0]?.message ?? 'Validation failed');
88
- }
89
- const body = result.data;
97
+ const body = parseBody(CreateSessionSchema, req.body);
90
98
  const workingDir = body.workingDir || process.cwd();
91
99
  // Validate workingDir exists and is a directory
92
100
  if (body.workingDir) {
@@ -143,42 +151,26 @@ export function registerSessionRoutes(app, ctx) {
143
151
  // ========== Rename Session ==========
144
152
  app.put('/api/sessions/:id/name', async (req) => {
145
153
  const { id } = req.params;
146
- const result = SessionNameSchema.safeParse(req.body);
147
- if (!result.success) {
148
- return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid request body');
149
- }
150
- const body = result.data;
151
- const session = ctx.sessions.get(id);
152
- if (!session) {
153
- return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
154
- }
154
+ const body = parseBody(SessionNameSchema, req.body, 'Invalid request body');
155
+ const session = findSessionOrFail(ctx, id);
155
156
  const name = String(body.name || '').slice(0, MAX_SESSION_NAME_LENGTH);
156
157
  session.name = name;
157
158
  // Also update the mux session name if applicable
158
159
  ctx.mux.updateSessionName(id, session.name);
159
- ctx.persistSessionState(session);
160
- ctx.broadcast(SseEvent.SessionUpdated, ctx.getSessionStateWithRespawn(session));
160
+ persistAndBroadcastSession(ctx, session);
161
161
  return { success: true, name: session.name };
162
162
  });
163
163
  // ========== Set Session Color ==========
164
164
  app.put('/api/sessions/:id/color', async (req) => {
165
165
  const { id } = req.params;
166
- const result = SessionColorSchema.safeParse(req.body);
167
- if (!result.success) {
168
- return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid request body');
169
- }
170
- const body = result.data;
171
- const session = ctx.sessions.get(id);
172
- if (!session) {
173
- return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
174
- }
166
+ const body = parseBody(SessionColorSchema, req.body, 'Invalid request body');
167
+ const session = findSessionOrFail(ctx, id);
175
168
  const validColors = ['default', 'red', 'orange', 'yellow', 'green', 'blue', 'purple', 'pink'];
176
169
  if (!validColors.includes(body.color)) {
177
170
  return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid color');
178
171
  }
179
172
  session.setColor(body.color);
180
- ctx.persistSessionState(session);
181
- ctx.broadcast(SseEvent.SessionUpdated, ctx.getSessionStateWithRespawn(session));
173
+ persistAndBroadcastSession(ctx, session);
182
174
  return { success: true, color: session.color };
183
175
  });
184
176
  // ========== Delete Session ==========
@@ -207,10 +199,7 @@ export function registerSessionRoutes(app, ctx) {
207
199
  // ========== Get Session Detail ==========
208
200
  app.get('/api/sessions/:id', async (req) => {
209
201
  const { id } = req.params;
210
- const session = ctx.sessions.get(id);
211
- if (!session) {
212
- return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
213
- }
202
+ const session = findSessionOrFail(ctx, id);
214
203
  // Use light state (no full buffers) — terminal buffer available via /terminal endpoint.
215
204
  // Full buffers were 2-3MB and caused slowness when polled frequently (e.g. Ralph wizard).
216
205
  return ctx.getSessionStateWithRespawn(session);
@@ -221,10 +210,7 @@ export function registerSessionRoutes(app, ctx) {
221
210
  // ========== Get Session Output ==========
222
211
  app.get('/api/sessions/:id/output', async (req) => {
223
212
  const { id } = req.params;
224
- const session = ctx.sessions.get(id);
225
- if (!session) {
226
- return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
227
- }
213
+ const session = findSessionOrFail(ctx, id);
228
214
  return {
229
215
  success: true,
230
216
  data: {
@@ -237,10 +223,7 @@ export function registerSessionRoutes(app, ctx) {
237
223
  // ========== Get Ralph State ==========
238
224
  app.get('/api/sessions/:id/ralph-state', async (req) => {
239
225
  const { id } = req.params;
240
- const session = ctx.sessions.get(id);
241
- if (!session) {
242
- return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
243
- }
226
+ const session = findSessionOrFail(ctx, id);
244
227
  return {
245
228
  success: true,
246
229
  data: {
@@ -253,10 +236,7 @@ export function registerSessionRoutes(app, ctx) {
253
236
  // ========== Get Run Summary ==========
254
237
  app.get('/api/sessions/:id/run-summary', async (req) => {
255
238
  const { id } = req.params;
256
- const session = ctx.sessions.get(id);
257
- if (!session) {
258
- return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
259
- }
239
+ const session = findSessionOrFail(ctx, id);
260
240
  const tracker = ctx.runSummaryTrackers.get(id);
261
241
  if (!tracker) {
262
242
  // Create a fresh tracker if one doesn't exist (shouldn't happen normally)
@@ -271,10 +251,7 @@ export function registerSessionRoutes(app, ctx) {
271
251
  // ========== Get Active Tools ==========
272
252
  app.get('/api/sessions/:id/active-tools', async (req) => {
273
253
  const { id } = req.params;
274
- const session = ctx.sessions.get(id);
275
- if (!session) {
276
- return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
277
- }
254
+ const session = findSessionOrFail(ctx, id);
278
255
  return {
279
256
  success: true,
280
257
  data: {
@@ -288,15 +265,8 @@ export function registerSessionRoutes(app, ctx) {
288
265
  // ========== Run Prompt ==========
289
266
  app.post('/api/sessions/:id/run', async (req) => {
290
267
  const { id } = req.params;
291
- const result = RunPromptSchema.safeParse(req.body);
292
- if (!result.success) {
293
- return createErrorResponse(ApiErrorCode.INVALID_INPUT, result.error.issues[0]?.message ?? 'Validation failed');
294
- }
295
- const { prompt } = result.data;
296
- const session = ctx.sessions.get(id);
297
- if (!session) {
298
- return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
299
- }
268
+ const { prompt } = parseBody(RunPromptSchema, req.body);
269
+ const session = findSessionOrFail(ctx, id);
300
270
  if (session.isBusy()) {
301
271
  return createErrorResponse(ApiErrorCode.SESSION_BUSY, 'Session is busy');
302
272
  }
@@ -310,10 +280,7 @@ export function registerSessionRoutes(app, ctx) {
310
280
  // ========== Start Interactive Mode ==========
311
281
  app.post('/api/sessions/:id/interactive', async (req) => {
312
282
  const { id } = req.params;
313
- const session = ctx.sessions.get(id);
314
- if (!session) {
315
- return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
316
- }
283
+ const session = findSessionOrFail(ctx, id);
317
284
  if (session.isBusy()) {
318
285
  return createErrorResponse(ApiErrorCode.SESSION_BUSY, 'Session is busy');
319
286
  }
@@ -346,10 +313,7 @@ export function registerSessionRoutes(app, ctx) {
346
313
  // ========== Start Shell Mode ==========
347
314
  app.post('/api/sessions/:id/shell', async (req) => {
348
315
  const { id } = req.params;
349
- const session = ctx.sessions.get(id);
350
- if (!session) {
351
- return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
352
- }
316
+ const session = findSessionOrFail(ctx, id);
353
317
  if (session.isBusy()) {
354
318
  return createErrorResponse(ApiErrorCode.SESSION_BUSY, 'Session is busy');
355
319
  }
@@ -375,15 +339,8 @@ export function registerSessionRoutes(app, ctx) {
375
339
  // ========== Send Input ==========
376
340
  app.post('/api/sessions/:id/input', async (req) => {
377
341
  const { id } = req.params;
378
- const result = SessionInputWithLimitSchema.safeParse(req.body);
379
- if (!result.success) {
380
- return createErrorResponse(ApiErrorCode.INVALID_INPUT, result.error.issues[0]?.message ?? 'Validation failed');
381
- }
382
- const { input, useMux } = result.data;
383
- const session = ctx.sessions.get(id);
384
- if (!session) {
385
- return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
386
- }
342
+ const { input, useMux } = parseBody(SessionInputWithLimitSchema, req.body);
343
+ const session = findSessionOrFail(ctx, id);
387
344
  const inputStr = String(input);
388
345
  if (inputStr.length > MAX_INPUT_LENGTH) {
389
346
  return createErrorResponse(ApiErrorCode.INVALID_INPUT, `Input exceeds maximum length (${MAX_INPUT_LENGTH} bytes)`);
@@ -413,15 +370,8 @@ export function registerSessionRoutes(app, ctx) {
413
370
  // ========== Resize Terminal ==========
414
371
  app.post('/api/sessions/:id/resize', async (req) => {
415
372
  const { id } = req.params;
416
- const result = ResizeSchema.safeParse(req.body);
417
- if (!result.success) {
418
- return createErrorResponse(ApiErrorCode.INVALID_INPUT, result.error.issues[0]?.message ?? 'Validation failed');
419
- }
420
- const { cols, rows } = result.data;
421
- const session = ctx.sessions.get(id);
422
- if (!session) {
423
- return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
424
- }
373
+ const { cols, rows } = parseBody(ResizeSchema, req.body);
374
+ const session = findSessionOrFail(ctx, id);
425
375
  session.resize(cols, rows);
426
376
  return { success: true };
427
377
  });
@@ -431,10 +381,7 @@ export function registerSessionRoutes(app, ctx) {
431
381
  app.get('/api/sessions/:id/terminal', async (req) => {
432
382
  const { id } = req.params;
433
383
  const query = req.query;
434
- const session = ctx.sessions.get(id);
435
- if (!session) {
436
- return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
437
- }
384
+ const session = findSessionOrFail(ctx, id);
438
385
  const tailBytes = query.tail ? parseInt(query.tail, 10) : 0;
439
386
  const fullSize = session.terminalBufferLength;
440
387
  let truncated = false;
@@ -485,18 +432,10 @@ export function registerSessionRoutes(app, ctx) {
485
432
  // ========== Auto-Clear ==========
486
433
  app.post('/api/sessions/:id/auto-clear', async (req) => {
487
434
  const { id } = req.params;
488
- const acResult = AutoClearSchema.safeParse(req.body);
489
- if (!acResult.success) {
490
- return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid request body');
491
- }
492
- const body = acResult.data;
493
- const session = ctx.sessions.get(id);
494
- if (!session) {
495
- return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
496
- }
435
+ const body = parseBody(AutoClearSchema, req.body, 'Invalid request body');
436
+ const session = findSessionOrFail(ctx, id);
497
437
  session.setAutoClear(body.enabled, body.threshold);
498
- ctx.persistSessionState(session);
499
- ctx.broadcast(SseEvent.SessionUpdated, ctx.getSessionStateWithRespawn(session));
438
+ persistAndBroadcastSession(ctx, session);
500
439
  return {
501
440
  success: true,
502
441
  data: {
@@ -510,18 +449,10 @@ export function registerSessionRoutes(app, ctx) {
510
449
  // ========== Auto-Compact ==========
511
450
  app.post('/api/sessions/:id/auto-compact', async (req) => {
512
451
  const { id } = req.params;
513
- const compactResult = AutoCompactSchema.safeParse(req.body);
514
- if (!compactResult.success) {
515
- return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid request body');
516
- }
517
- const body = compactResult.data;
518
- const session = ctx.sessions.get(id);
519
- if (!session) {
520
- return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
521
- }
452
+ const body = parseBody(AutoCompactSchema, req.body, 'Invalid request body');
453
+ const session = findSessionOrFail(ctx, id);
522
454
  session.setAutoCompact(body.enabled, body.threshold, body.prompt);
523
- ctx.persistSessionState(session);
524
- ctx.broadcast(SseEvent.SessionUpdated, ctx.getSessionStateWithRespawn(session));
455
+ persistAndBroadcastSession(ctx, session);
525
456
  return {
526
457
  success: true,
527
458
  data: {
@@ -536,15 +467,8 @@ export function registerSessionRoutes(app, ctx) {
536
467
  // ========== Image Watcher ==========
537
468
  app.post('/api/sessions/:id/image-watcher', async (req) => {
538
469
  const { id } = req.params;
539
- const iwResult = ImageWatcherSchema.safeParse(req.body);
540
- if (!iwResult.success) {
541
- return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid request body');
542
- }
543
- const body = iwResult.data;
544
- const session = ctx.sessions.get(id);
545
- if (!session) {
546
- return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
547
- }
470
+ const body = parseBody(ImageWatcherSchema, req.body, 'Invalid request body');
471
+ const session = findSessionOrFail(ctx, id);
548
472
  if (body.enabled) {
549
473
  imageWatcher.watchSession(session.id, session.workingDir);
550
474
  }
@@ -564,18 +488,10 @@ export function registerSessionRoutes(app, ctx) {
564
488
  // ========== Flicker Filter ==========
565
489
  app.post('/api/sessions/:id/flicker-filter', async (req) => {
566
490
  const { id } = req.params;
567
- const ffResult = FlickerFilterSchema.safeParse(req.body);
568
- if (!ffResult.success) {
569
- return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid request body');
570
- }
571
- const body = ffResult.data;
572
- const session = ctx.sessions.get(id);
573
- if (!session) {
574
- return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
575
- }
491
+ const body = parseBody(FlickerFilterSchema, req.body, 'Invalid request body');
492
+ const session = findSessionOrFail(ctx, id);
576
493
  session.flickerFilterEnabled = body.enabled;
577
- ctx.persistSessionState(session);
578
- ctx.broadcast(SseEvent.SessionUpdated, ctx.getSessionStateWithRespawn(session));
494
+ persistAndBroadcastSession(ctx, session);
579
495
  return {
580
496
  success: true,
581
497
  data: {
@@ -592,11 +508,7 @@ export function registerSessionRoutes(app, ctx) {
592
508
  if (ctx.sessions.size >= MAX_CONCURRENT_SESSIONS) {
593
509
  return createErrorResponse(ApiErrorCode.SESSION_BUSY, `Maximum concurrent sessions (${MAX_CONCURRENT_SESSIONS}) reached`);
594
510
  }
595
- const qrResult = QuickRunSchema.safeParse(req.body);
596
- if (!qrResult.success) {
597
- return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid request body');
598
- }
599
- const { prompt, workingDir } = qrResult.data;
511
+ const { prompt, workingDir } = parseBody(QuickRunSchema, req.body, 'Invalid request body');
600
512
  if (!prompt.trim()) {
601
513
  return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'prompt is required');
602
514
  }
@@ -643,11 +555,7 @@ export function registerSessionRoutes(app, ctx) {
643
555
  if (ctx.sessions.size >= MAX_CONCURRENT_SESSIONS) {
644
556
  return createErrorResponse(ApiErrorCode.SESSION_BUSY, `Maximum concurrent sessions (${MAX_CONCURRENT_SESSIONS}) reached.`);
645
557
  }
646
- const result = QuickStartSchema.safeParse(req.body);
647
- if (!result.success) {
648
- return createErrorResponse(ApiErrorCode.INVALID_INPUT, result.error.issues[0]?.message ?? 'Validation failed');
649
- }
650
- const { caseName = 'testcase', mode = 'claude', openCodeConfig } = result.data;
558
+ const { caseName = 'testcase', mode = 'claude', openCodeConfig } = parseBody(QuickStartSchema, req.body);
651
559
  // Check OpenCode availability if requested
652
560
  if (mode === 'opencode') {
653
561
  const { isOpenCodeAvailable } = await import('../../utils/opencode-cli-resolver.js');
@@ -785,7 +693,12 @@ export function registerSessionRoutes(app, ctx) {
785
693
  /** Extract the text of the first user message from a JSONL transcript head. */
786
694
  function extractFirstUserPrompt(head) {
787
695
  const MAX_PROMPT_LEN = 120;
788
- for (const line of head.split('\n')) {
696
+ // Iterate lines without allocating a full split array
697
+ let start = 0;
698
+ while (start < head.length) {
699
+ const end = head.indexOf('\n', start);
700
+ const line = end === -1 ? head.slice(start) : head.slice(start, end);
701
+ start = end === -1 ? head.length : end + 1;
789
702
  if (!line.includes('"type":"user"'))
790
703
  continue;
791
704
  try {
@@ -804,15 +717,23 @@ export function registerSessionRoutes(app, ctx) {
804
717
  }
805
718
  if (!text)
806
719
  continue;
807
- // Strip XML-like system/command tags that Claude Code injects into transcripts
720
+ // Strip XML-like system/command tags and ANSI escapes from transcripts
808
721
  text = text
809
722
  .replace(/<[^>]+>/g, '')
723
+ .replace(new RegExp(String.raw `\x1b\[[0-9;]*[a-zA-Z]`, 'g'), '')
810
724
  .trim()
811
725
  .replace(/\s+/g, ' ');
812
726
  if (!text)
813
727
  continue;
814
- // Skip system-injected messages and slash command artifacts (not real user prompts)
815
- if (/^(Caveat:|init\b|clear\b|\/\w+ \w+$|You are a )/i.test(text))
728
+ // Skip system-injected messages, slash command artifacts, and expanded skill prompts
729
+ if (/^(Caveat:|init\b|clear\b|resume\b|\/[a-z][\w-]*\b|You are a |\[Request |Set model to )/i.test(text) ||
730
+ /^(Please )?(analyze|review) this codebase/i.test(text) ||
731
+ /^(Read|Implement the following) .+, then (search|list|check) /i.test(text) ||
732
+ /^\d+ vulnerabilit/i.test(text) ||
733
+ /\btoolu_/.test(text) ||
734
+ /^[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]+/.test(text) ||
735
+ /\b(sk-ant-|ANTHROPIC_API_KEY|API_KEY=|SECRET|TOKEN=)/i.test(text) ||
736
+ text.length < 8)
816
737
  continue;
817
738
  return text.length > MAX_PROMPT_LEN ? text.slice(0, MAX_PROMPT_LEN) + '…' : text;
818
739
  }
@@ -822,9 +743,41 @@ export function registerSessionRoutes(app, ctx) {
822
743
  }
823
744
  return undefined;
824
745
  }
746
+ /** Read the first 16KB of a file for content sniffing. */
747
+ async function readFileHead(path, buf) {
748
+ try {
749
+ const fd = await fs.open(path, 'r');
750
+ const { bytesRead } = await fd.read(buf, 0, buf.length, 0);
751
+ await fd.close();
752
+ return buf.toString('utf8', 0, bytesRead);
753
+ }
754
+ catch {
755
+ return null;
756
+ }
757
+ }
758
+ /** Read the last `buf.length` bytes of a file (for tail-scanning user prompts). */
759
+ async function readFileTail(path, buf, fileSize) {
760
+ try {
761
+ const fd = await fs.open(path, 'r');
762
+ const offset = Math.max(0, fileSize - buf.length);
763
+ const { bytesRead } = await fd.read(buf, 0, buf.length, offset);
764
+ await fd.close();
765
+ const text = buf.toString('utf8', 0, bytesRead);
766
+ // Skip first partial line when we didn't read from the start
767
+ if (offset > 0) {
768
+ const nl = text.indexOf('\n');
769
+ return nl >= 0 ? text.slice(nl + 1) : null;
770
+ }
771
+ return text;
772
+ }
773
+ catch {
774
+ return null;
775
+ }
776
+ }
825
777
  app.get('/api/history/sessions', async () => {
826
778
  const projectsDir = join(process.env.HOME || '/tmp', '.claude', 'projects');
827
779
  const results = [];
780
+ const headBuf = Buffer.alloc(16384);
828
781
  try {
829
782
  const projectDirs = await fs.readdir(projectsDir);
830
783
  for (const projDir of projectDirs) {
@@ -862,33 +815,24 @@ export function registerSessionRoutes(app, ctx) {
862
815
  // no "user"/"assistant" messages and will fail claude --resume.
863
816
  // Read first 16KB to check content and extract first user prompt.
864
817
  let firstPrompt;
865
- const readHead = async () => {
866
- try {
867
- const fd = await fs.open(filePath, 'r');
868
- const buf = Buffer.alloc(16384);
869
- const { bytesRead } = await fd.read(buf, 0, 16384, 0);
870
- await fd.close();
871
- return buf.toString('utf8', 0, bytesRead);
872
- }
873
- catch {
874
- return null;
875
- }
876
- };
818
+ const head = await readFileHead(filePath, headBuf);
877
819
  if (fileStat.size < 50000) {
878
- const head = await readHead();
879
820
  if (!head ||
880
821
  (!head.includes('"type":"user"') &&
881
822
  !head.includes('"type":"assistant"') &&
882
823
  !head.includes('"type":"summary"'))) {
883
824
  continue; // No conversation content — skip
884
825
  }
885
- firstPrompt = extractFirstUserPrompt(head);
886
826
  }
887
- else {
888
- // Large files are almost certainly real conversations; still read head for prompt
889
- const head = await readHead();
890
- if (head)
891
- firstPrompt = extractFirstUserPrompt(head);
827
+ if (head)
828
+ firstPrompt = extractFirstUserPrompt(head);
829
+ // If head scan found no usable prompt (e.g. session started with /init),
830
+ // try reading the tail for a recent user message.
831
+ if (!firstPrompt && fileStat.size > 65536) {
832
+ const tailBuf = Buffer.alloc(32768);
833
+ const tail = await readFileTail(filePath, tailBuf, fileStat.size);
834
+ if (tail)
835
+ firstPrompt = extractFirstUserPrompt(tail);
892
836
  }
893
837
  results.push({
894
838
  sessionId,