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.
- package/dist/bash-tool-parser.d.ts.map +1 -1
- package/dist/bash-tool-parser.js +11 -4
- package/dist/bash-tool-parser.js.map +1 -1
- package/dist/web/public/api-client.3adebdc2.js.gz +0 -0
- package/dist/web/public/app.916baeb9.js +26 -0
- package/dist/web/public/app.916baeb9.js.br +0 -0
- package/dist/web/public/app.916baeb9.js.gz +0 -0
- package/dist/web/public/constants.64161167.js.gz +0 -0
- package/dist/web/public/index.html +11 -11
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/public/input-cjk.92544c51.js.gz +0 -0
- package/dist/web/public/keyboard-accessory.9fb81db6.js.gz +0 -0
- package/dist/web/public/mobile-handlers.1e2a8ef8.js.gz +0 -0
- package/dist/web/public/mobile.fdd28a54.css.gz +0 -0
- package/dist/web/public/notification-manager.2d5ea8ec.js.gz +0 -0
- package/dist/web/public/orchestrator-panel.js.gz +0 -0
- package/dist/web/public/{panels-ui.3dd2e29b.js → panels-ui.8204db1e.js} +2 -2
- package/dist/web/public/panels-ui.8204db1e.js.br +0 -0
- package/dist/web/public/panels-ui.8204db1e.js.gz +0 -0
- package/dist/web/public/{ralph-panel.7b014f16.js → ralph-panel.a2733fd5.js} +1 -1
- package/dist/web/public/ralph-panel.a2733fd5.js.br +0 -0
- package/dist/web/public/ralph-panel.a2733fd5.js.gz +0 -0
- package/dist/web/public/ralph-wizard.f31ab90e.js.gz +0 -0
- package/dist/web/public/respawn-ui.372c6ea7.js.gz +0 -0
- package/dist/web/public/session-ui.0a07c3b7.js.gz +0 -0
- package/dist/web/public/settings-ui.94c57184.js.gz +0 -0
- package/dist/web/public/styles.5c87d847.css +1 -0
- package/dist/web/public/styles.5c87d847.css.br +0 -0
- package/dist/web/public/styles.5c87d847.css.gz +0 -0
- package/dist/web/public/subagent-windows.a366a4ad.js.gz +0 -0
- package/dist/web/public/sw.js.gz +0 -0
- package/dist/web/public/terminal-ui.b66dbf4e.js +3 -0
- package/dist/web/public/terminal-ui.b66dbf4e.js.br +0 -0
- package/dist/web/public/terminal-ui.b66dbf4e.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.137ad9f0.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.085e9e73.js.gz +0 -0
- package/dist/web/route-helpers.d.ts +11 -0
- package/dist/web/route-helpers.d.ts.map +1 -1
- package/dist/web/route-helpers.js +23 -0
- package/dist/web/route-helpers.js.map +1 -1
- package/dist/web/routes/case-routes.d.ts.map +1 -1
- package/dist/web/routes/case-routes.js +3 -11
- package/dist/web/routes/case-routes.js.map +1 -1
- package/dist/web/routes/hook-event-routes.d.ts.map +1 -1
- package/dist/web/routes/hook-event-routes.js +2 -6
- package/dist/web/routes/hook-event-routes.js.map +1 -1
- package/dist/web/routes/orchestrator-routes.d.ts.map +1 -1
- package/dist/web/routes/orchestrator-routes.js +4 -10
- package/dist/web/routes/orchestrator-routes.js.map +1 -1
- package/dist/web/routes/plan-routes.d.ts.map +1 -1
- package/dist/web/routes/plan-routes.js +6 -26
- package/dist/web/routes/plan-routes.js.map +1 -1
- package/dist/web/routes/push-routes.d.ts.map +1 -1
- package/dist/web/routes/push-routes.js +4 -10
- package/dist/web/routes/push-routes.js.map +1 -1
- package/dist/web/routes/ralph-routes.d.ts.map +1 -1
- package/dist/web/routes/ralph-routes.js +13 -53
- package/dist/web/routes/ralph-routes.js.map +1 -1
- package/dist/web/routes/respawn-routes.d.ts.map +1 -1
- package/dist/web/routes/respawn-routes.js +3 -11
- package/dist/web/routes/respawn-routes.js.map +1 -1
- package/dist/web/routes/scheduled-routes.d.ts.map +1 -1
- package/dist/web/routes/scheduled-routes.js +2 -5
- package/dist/web/routes/scheduled-routes.js.map +1 -1
- package/dist/web/routes/session-routes.d.ts.map +1 -1
- package/dist/web/routes/session-routes.js +113 -169
- package/dist/web/routes/session-routes.js.map +1 -1
- package/dist/web/routes/system-routes.d.ts.map +1 -1
- package/dist/web/routes/system-routes.js +8 -31
- package/dist/web/routes/system-routes.js.map +1 -1
- package/package.json +1 -1
- package/dist/web/public/app.cbf6e9e8.js +0 -26
- package/dist/web/public/app.cbf6e9e8.js.br +0 -0
- package/dist/web/public/app.cbf6e9e8.js.gz +0 -0
- package/dist/web/public/panels-ui.3dd2e29b.js.br +0 -0
- package/dist/web/public/panels-ui.3dd2e29b.js.gz +0 -0
- package/dist/web/public/ralph-panel.7b014f16.js.br +0 -0
- package/dist/web/public/ralph-panel.7b014f16.js.gz +0 -0
- package/dist/web/public/styles.8e110d27.css +0 -1
- package/dist/web/public/styles.8e110d27.css.br +0 -0
- package/dist/web/public/styles.8e110d27.css.gz +0 -0
- package/dist/web/public/terminal-ui.9b40798a.js +0 -3
- package/dist/web/public/terminal-ui.9b40798a.js.br +0 -0
- 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
|
|
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;
|
|
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;
|
|
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
|
-
//
|
|
50
|
-
// (
|
|
51
|
-
|
|
52
|
-
//
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
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
|
|
147
|
-
|
|
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
|
|
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
|
|
167
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
292
|
-
|
|
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
|
|
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
|
|
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
|
|
379
|
-
|
|
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
|
|
417
|
-
|
|
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
|
|
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
|
|
489
|
-
|
|
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
|
|
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
|
|
514
|
-
|
|
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
|
|
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
|
|
540
|
-
|
|
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
|
|
568
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
815
|
-
if (/^(Caveat:|init\b|clear\b
|
|
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
|
|
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
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
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,
|