codekanban 0.29.0 → 0.30.0
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.
|
@@ -1,17 +1,21 @@
|
|
|
1
|
-
import { readFile } from
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
2
|
|
|
3
|
-
import { buildAgentLaunchSpec } from
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
import { buildAgentLaunchSpec } from "./command-builder.js";
|
|
4
|
+
import {
|
|
5
|
+
CodeKanbanConfigError,
|
|
6
|
+
CodeKanbanHttpError,
|
|
7
|
+
CodeKanbanValidationError,
|
|
8
|
+
} from "./errors.js";
|
|
9
|
+
import { TerminalConnection } from "./terminal-connection.js";
|
|
10
|
+
import { WebSessionCommandChannel } from "./web-session-command-channel.js";
|
|
11
|
+
import { WebSessionEventStream } from "./web-session-event-stream.js";
|
|
8
12
|
import {
|
|
9
13
|
WEB_SESSION_COMMAND_WS_PATH,
|
|
10
14
|
WEB_SESSION_EVENTS_WS_PATH,
|
|
11
15
|
analyzeWebSession,
|
|
12
16
|
ensureImageMimeType,
|
|
13
17
|
normalizeWebSessionAttachment,
|
|
14
|
-
} from
|
|
18
|
+
} from "./web-session-shared.js";
|
|
15
19
|
import {
|
|
16
20
|
ensureOptionalString,
|
|
17
21
|
ensureString,
|
|
@@ -21,7 +25,7 @@ import {
|
|
|
21
25
|
pathBasename,
|
|
22
26
|
sleep,
|
|
23
27
|
toWsUrl,
|
|
24
|
-
} from
|
|
28
|
+
} from "./utils.js";
|
|
25
29
|
|
|
26
30
|
const DEFAULT_REQUEST_RETRY = Object.freeze({
|
|
27
31
|
attempts: 2,
|
|
@@ -29,30 +33,36 @@ const DEFAULT_REQUEST_RETRY = Object.freeze({
|
|
|
29
33
|
maxDelayMs: 1000,
|
|
30
34
|
});
|
|
31
35
|
|
|
32
|
-
const RETRYABLE_HTTP_STATUS_CODES = new Set([
|
|
36
|
+
const RETRYABLE_HTTP_STATUS_CODES = new Set([
|
|
37
|
+
408, 425, 429, 500, 502, 503, 504,
|
|
38
|
+
]);
|
|
33
39
|
|
|
34
|
-
const DEFAULT_WEB_SESSION_AUTO_RETRY_SCOPE =
|
|
35
|
-
const DEFAULT_WEB_SESSION_AUTO_RETRY_PRESET =
|
|
40
|
+
const DEFAULT_WEB_SESSION_AUTO_RETRY_SCOPE = "network_only";
|
|
41
|
+
const DEFAULT_WEB_SESSION_AUTO_RETRY_PRESET = "gentle_stop";
|
|
36
42
|
|
|
37
43
|
function defaultWebSessionModel(agent) {
|
|
38
|
-
return agent ===
|
|
44
|
+
return agent === "claude" ? "opus" : "gpt-5.4";
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
function defaultWebSessionReasoningEffort(agent) {
|
|
42
|
-
return agent ===
|
|
48
|
+
return agent === "claude" ? "default" : "xhigh";
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
function defaultWebSessionWorkflowMode(permissionMode) {
|
|
46
|
-
return permissionMode ===
|
|
52
|
+
return permissionMode === "plan" ? "plan" : "default";
|
|
47
53
|
}
|
|
48
54
|
|
|
49
55
|
function defaultWebSessionPermissionLevel(permissionMode) {
|
|
50
|
-
return permissionMode ===
|
|
56
|
+
return permissionMode === "yolo" ? "yolo" : "elevated";
|
|
51
57
|
}
|
|
52
58
|
|
|
53
59
|
function normalizeWebSessionAutoRetryScope(scope) {
|
|
54
60
|
const normalized = ensureOptionalString(scope);
|
|
55
|
-
if (
|
|
61
|
+
if (
|
|
62
|
+
["network_only", "network_and_rate_limit", "all_failures"].includes(
|
|
63
|
+
normalized,
|
|
64
|
+
)
|
|
65
|
+
) {
|
|
56
66
|
return normalized;
|
|
57
67
|
}
|
|
58
68
|
return DEFAULT_WEB_SESSION_AUTO_RETRY_SCOPE;
|
|
@@ -60,14 +70,16 @@ function normalizeWebSessionAutoRetryScope(scope) {
|
|
|
60
70
|
|
|
61
71
|
function normalizeWebSessionAutoRetryPreset(preset) {
|
|
62
72
|
const normalized = ensureOptionalString(preset);
|
|
63
|
-
if ([
|
|
73
|
+
if (["gentle_stop", "aggressive_stop", "sustain_60s"].includes(normalized)) {
|
|
64
74
|
return normalized;
|
|
65
75
|
}
|
|
66
76
|
return DEFAULT_WEB_SESSION_AUTO_RETRY_PRESET;
|
|
67
77
|
}
|
|
68
78
|
|
|
69
79
|
function normalizeRequestRetryConfig(value = {}) {
|
|
70
|
-
const attempts = Number.isFinite(value.attempts)
|
|
80
|
+
const attempts = Number.isFinite(value.attempts)
|
|
81
|
+
? Math.max(1, Math.trunc(value.attempts))
|
|
82
|
+
: DEFAULT_REQUEST_RETRY.attempts;
|
|
71
83
|
const baseDelayMs = Number.isFinite(value.baseDelayMs)
|
|
72
84
|
? Math.max(1, Math.trunc(value.baseDelayMs))
|
|
73
85
|
: DEFAULT_REQUEST_RETRY.baseDelayMs;
|
|
@@ -86,12 +98,12 @@ function resolveRequestRetryConfig(method, defaults, override) {
|
|
|
86
98
|
return null;
|
|
87
99
|
}
|
|
88
100
|
|
|
89
|
-
const normalizedMethod = String(method ||
|
|
90
|
-
const source = override && typeof override ===
|
|
101
|
+
const normalizedMethod = String(method || "GET").toUpperCase();
|
|
102
|
+
const source = override && typeof override === "object" ? override : {};
|
|
91
103
|
const enabled =
|
|
92
|
-
typeof source.enabled ===
|
|
104
|
+
typeof source.enabled === "boolean"
|
|
93
105
|
? source.enabled
|
|
94
|
-
: [
|
|
106
|
+
: ["GET", "HEAD", "OPTIONS"].includes(normalizedMethod);
|
|
95
107
|
if (!enabled) {
|
|
96
108
|
return null;
|
|
97
109
|
}
|
|
@@ -106,13 +118,16 @@ function isRetryableRequestError(error) {
|
|
|
106
118
|
if (error instanceof CodeKanbanHttpError) {
|
|
107
119
|
return RETRYABLE_HTTP_STATUS_CODES.has(error.status);
|
|
108
120
|
}
|
|
109
|
-
if (
|
|
121
|
+
if (
|
|
122
|
+
error instanceof CodeKanbanValidationError ||
|
|
123
|
+
error instanceof CodeKanbanConfigError
|
|
124
|
+
) {
|
|
110
125
|
return false;
|
|
111
126
|
}
|
|
112
127
|
if (error instanceof SyntaxError) {
|
|
113
128
|
return false;
|
|
114
129
|
}
|
|
115
|
-
if (error?.name ===
|
|
130
|
+
if (error?.name === "AbortError") {
|
|
116
131
|
return true;
|
|
117
132
|
}
|
|
118
133
|
if (error instanceof TypeError) {
|
|
@@ -122,7 +137,10 @@ function isRetryableRequestError(error) {
|
|
|
122
137
|
}
|
|
123
138
|
|
|
124
139
|
function getRetryDelayMs(retryConfig, attemptIndex) {
|
|
125
|
-
return Math.min(
|
|
140
|
+
return Math.min(
|
|
141
|
+
retryConfig.maxDelayMs,
|
|
142
|
+
retryConfig.baseDelayMs * 2 ** attemptIndex,
|
|
143
|
+
);
|
|
126
144
|
}
|
|
127
145
|
|
|
128
146
|
export class CodeKanbanClient {
|
|
@@ -133,41 +151,58 @@ export class CodeKanbanClient {
|
|
|
133
151
|
this.WebSocketImpl = options.WebSocketImpl || globalThis.WebSocket;
|
|
134
152
|
this.requestRetry = normalizeRequestRetryConfig(options.requestRetry);
|
|
135
153
|
if (!this.fetchImpl) {
|
|
136
|
-
throw new CodeKanbanConfigError(
|
|
154
|
+
throw new CodeKanbanConfigError("fetch implementation is unavailable");
|
|
137
155
|
}
|
|
138
156
|
}
|
|
139
157
|
|
|
140
158
|
async requestJson(path, options = {}) {
|
|
141
|
-
const method = String(options.method ||
|
|
142
|
-
const headers = {
|
|
159
|
+
const method = String(options.method || "GET").toUpperCase();
|
|
160
|
+
const headers = {
|
|
161
|
+
Accept: "application/json",
|
|
162
|
+
...this.headers,
|
|
163
|
+
...(options.headers || {}),
|
|
164
|
+
};
|
|
143
165
|
const request = {
|
|
144
166
|
method,
|
|
145
167
|
headers,
|
|
146
168
|
};
|
|
147
169
|
if (options.body !== undefined) {
|
|
148
170
|
request.body = JSON.stringify(options.body);
|
|
149
|
-
request.headers[
|
|
171
|
+
request.headers["Content-Type"] = "application/json";
|
|
150
172
|
}
|
|
151
173
|
|
|
152
|
-
const retryConfig = resolveRequestRetryConfig(
|
|
174
|
+
const retryConfig = resolveRequestRetryConfig(
|
|
175
|
+
method,
|
|
176
|
+
this.requestRetry,
|
|
177
|
+
options.retry,
|
|
178
|
+
);
|
|
153
179
|
const maxAttempts = retryConfig?.attempts ?? 1;
|
|
154
180
|
|
|
155
181
|
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
156
182
|
try {
|
|
157
|
-
const response = await this.fetchImpl(
|
|
183
|
+
const response = await this.fetchImpl(
|
|
184
|
+
new URL(path, this.baseURL),
|
|
185
|
+
request,
|
|
186
|
+
);
|
|
158
187
|
const text = await response.text();
|
|
159
188
|
const body = text ? JSON.parse(text) : null;
|
|
160
189
|
if (!response.ok) {
|
|
161
|
-
throw new CodeKanbanHttpError(
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
190
|
+
throw new CodeKanbanHttpError(
|
|
191
|
+
`request failed with ${response.status}`,
|
|
192
|
+
{
|
|
193
|
+
status: response.status,
|
|
194
|
+
method,
|
|
195
|
+
path,
|
|
196
|
+
body,
|
|
197
|
+
},
|
|
198
|
+
);
|
|
167
199
|
}
|
|
168
200
|
return body;
|
|
169
201
|
} catch (error) {
|
|
170
|
-
const canRetry =
|
|
202
|
+
const canRetry =
|
|
203
|
+
retryConfig &&
|
|
204
|
+
attempt + 1 < maxAttempts &&
|
|
205
|
+
isRetryableRequestError(error);
|
|
171
206
|
if (!canRetry) {
|
|
172
207
|
throw error;
|
|
173
208
|
}
|
|
@@ -175,7 +210,9 @@ export class CodeKanbanClient {
|
|
|
175
210
|
}
|
|
176
211
|
}
|
|
177
212
|
|
|
178
|
-
throw new CodeKanbanValidationError(
|
|
213
|
+
throw new CodeKanbanValidationError(
|
|
214
|
+
`request retry loop exited unexpectedly for ${method} ${path}`,
|
|
215
|
+
);
|
|
179
216
|
}
|
|
180
217
|
|
|
181
218
|
async resolveProjectReference({ projectId, path, ensureProject = true }) {
|
|
@@ -197,25 +234,31 @@ export class CodeKanbanClient {
|
|
|
197
234
|
path,
|
|
198
235
|
ensureProject,
|
|
199
236
|
});
|
|
200
|
-
return ensureString(project?.id,
|
|
237
|
+
return ensureString(project?.id, "projectId");
|
|
201
238
|
}
|
|
202
239
|
|
|
203
240
|
async listProjects() {
|
|
204
|
-
const response = await this.requestJson(
|
|
241
|
+
const response = await this.requestJson("/api/v1/projects");
|
|
205
242
|
return response?.items || [];
|
|
206
243
|
}
|
|
207
244
|
|
|
208
245
|
async getProject(projectId) {
|
|
209
|
-
ensureString(projectId,
|
|
246
|
+
ensureString(projectId, "projectId");
|
|
210
247
|
const response = await this.requestJson(`/api/v1/projects/${projectId}`);
|
|
211
248
|
return response?.item;
|
|
212
249
|
}
|
|
213
250
|
|
|
214
|
-
async createProject({
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
251
|
+
async createProject({
|
|
252
|
+
path,
|
|
253
|
+
name,
|
|
254
|
+
description = "",
|
|
255
|
+
worktreeBasePath,
|
|
256
|
+
hidePath,
|
|
257
|
+
}) {
|
|
258
|
+
ensureString(path, "path");
|
|
259
|
+
ensureString(name, "name");
|
|
260
|
+
const response = await this.requestJson("/api/v1/projects/create", {
|
|
261
|
+
method: "POST",
|
|
219
262
|
body: {
|
|
220
263
|
path,
|
|
221
264
|
name,
|
|
@@ -228,27 +271,33 @@ export class CodeKanbanClient {
|
|
|
228
271
|
}
|
|
229
272
|
|
|
230
273
|
async listWorktrees(projectId) {
|
|
231
|
-
ensureString(projectId,
|
|
232
|
-
const response = await this.requestJson(
|
|
274
|
+
ensureString(projectId, "projectId");
|
|
275
|
+
const response = await this.requestJson(
|
|
276
|
+
`/api/v1/projects/${projectId}/worktrees`,
|
|
277
|
+
);
|
|
233
278
|
return response?.items || [];
|
|
234
279
|
}
|
|
235
280
|
|
|
236
281
|
async listTerminalSessions(projectId) {
|
|
237
|
-
ensureString(projectId,
|
|
238
|
-
const response = await this.requestJson(
|
|
282
|
+
ensureString(projectId, "projectId");
|
|
283
|
+
const response = await this.requestJson(
|
|
284
|
+
`/api/v1/projects/${projectId}/terminals`,
|
|
285
|
+
);
|
|
239
286
|
return response?.items || [];
|
|
240
287
|
}
|
|
241
288
|
|
|
242
289
|
async listAISessionsByProject(projectId) {
|
|
243
|
-
ensureString(projectId,
|
|
244
|
-
const response = await this.requestJson(
|
|
290
|
+
ensureString(projectId, "projectId");
|
|
291
|
+
const response = await this.requestJson(
|
|
292
|
+
`/api/v1/projects/${projectId}/ai-sessions`,
|
|
293
|
+
);
|
|
245
294
|
return response?.item || null;
|
|
246
295
|
}
|
|
247
296
|
|
|
248
297
|
async listAISessionsByPath(projectPath) {
|
|
249
|
-
ensureString(projectPath,
|
|
250
|
-
const response = await this.requestJson(
|
|
251
|
-
method:
|
|
298
|
+
ensureString(projectPath, "path");
|
|
299
|
+
const response = await this.requestJson("/api/v1/ai-sessions/by-path", {
|
|
300
|
+
method: "POST",
|
|
252
301
|
body: { path: projectPath },
|
|
253
302
|
});
|
|
254
303
|
return response?.item || null;
|
|
@@ -258,28 +307,36 @@ export class CodeKanbanClient {
|
|
|
258
307
|
const dbId = ensureOptionalString(id);
|
|
259
308
|
const rawSessionId = ensureOptionalString(sessionId);
|
|
260
309
|
if (!dbId && !rawSessionId) {
|
|
261
|
-
throw new CodeKanbanValidationError(
|
|
310
|
+
throw new CodeKanbanValidationError("id or sessionId is required");
|
|
262
311
|
}
|
|
263
312
|
if (refresh && !dbId) {
|
|
264
|
-
throw new CodeKanbanValidationError(
|
|
313
|
+
throw new CodeKanbanValidationError(
|
|
314
|
+
"refresh currently requires a database id",
|
|
315
|
+
);
|
|
265
316
|
}
|
|
266
317
|
|
|
267
318
|
if (dbId) {
|
|
268
|
-
const path = refresh
|
|
269
|
-
|
|
319
|
+
const path = refresh
|
|
320
|
+
? `/api/v1/ai-sessions/${dbId}/refresh`
|
|
321
|
+
: `/api/v1/ai-sessions/${dbId}/conversation`;
|
|
322
|
+
const response = await this.requestJson(path, {
|
|
323
|
+
method: refresh ? "POST" : "GET",
|
|
324
|
+
});
|
|
270
325
|
return response?.item || null;
|
|
271
326
|
}
|
|
272
327
|
|
|
273
|
-
const response = await this.requestJson(
|
|
328
|
+
const response = await this.requestJson(
|
|
329
|
+
`/api/v1/ai-sessions/by-session-id/${rawSessionId}/conversation`,
|
|
330
|
+
);
|
|
274
331
|
return response?.item || null;
|
|
275
332
|
}
|
|
276
333
|
|
|
277
334
|
async getAISessionToolResult({ id, sessionId, toolUseId }) {
|
|
278
335
|
const dbId = ensureOptionalString(id);
|
|
279
336
|
const rawSessionId = ensureOptionalString(sessionId);
|
|
280
|
-
const resolvedToolUseId = ensureString(toolUseId,
|
|
337
|
+
const resolvedToolUseId = ensureString(toolUseId, "toolUseId");
|
|
281
338
|
if (!dbId && !rawSessionId) {
|
|
282
|
-
throw new CodeKanbanValidationError(
|
|
339
|
+
throw new CodeKanbanValidationError("id or sessionId is required");
|
|
283
340
|
}
|
|
284
341
|
const path = dbId
|
|
285
342
|
? `/api/v1/ai-sessions/${dbId}/conversation/tool-results/${resolvedToolUseId}`
|
|
@@ -288,18 +345,28 @@ export class CodeKanbanClient {
|
|
|
288
345
|
return response?.item || null;
|
|
289
346
|
}
|
|
290
347
|
|
|
291
|
-
async createTerminalSession({
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
348
|
+
async createTerminalSession({
|
|
349
|
+
projectId,
|
|
350
|
+
worktreeId,
|
|
351
|
+
workingDir = "",
|
|
352
|
+
title = "",
|
|
353
|
+
rows = 0,
|
|
354
|
+
cols = 0,
|
|
355
|
+
}) {
|
|
356
|
+
ensureString(projectId, "projectId");
|
|
357
|
+
ensureString(worktreeId, "worktreeId");
|
|
358
|
+
const response = await this.requestJson(
|
|
359
|
+
`/api/v1/projects/${projectId}/worktrees/${worktreeId}/terminals`,
|
|
360
|
+
{
|
|
361
|
+
method: "POST",
|
|
362
|
+
body: {
|
|
363
|
+
workingDir,
|
|
364
|
+
title,
|
|
365
|
+
rows,
|
|
366
|
+
cols,
|
|
367
|
+
},
|
|
301
368
|
},
|
|
302
|
-
|
|
369
|
+
);
|
|
303
370
|
return response?.item;
|
|
304
371
|
}
|
|
305
372
|
|
|
@@ -311,61 +378,74 @@ export class CodeKanbanClient {
|
|
|
311
378
|
const project = await this.getProject(resolvedProjectId);
|
|
312
379
|
return {
|
|
313
380
|
project,
|
|
314
|
-
matchedBy:
|
|
381
|
+
matchedBy: "projectId",
|
|
315
382
|
};
|
|
316
383
|
}
|
|
317
384
|
|
|
318
385
|
if (!resolvedPath) {
|
|
319
|
-
throw new CodeKanbanValidationError(
|
|
386
|
+
throw new CodeKanbanValidationError("projectId or path is required");
|
|
320
387
|
}
|
|
321
388
|
|
|
322
389
|
const target = normalizeFsPath(resolvedPath);
|
|
323
390
|
const projects = await this.listProjects();
|
|
324
|
-
const project = projects.find(
|
|
391
|
+
const project = projects.find(
|
|
392
|
+
(item) => normalizeFsPath(item.path) === target,
|
|
393
|
+
);
|
|
325
394
|
if (project) {
|
|
326
395
|
return {
|
|
327
396
|
project,
|
|
328
|
-
matchedBy:
|
|
397
|
+
matchedBy: "path",
|
|
329
398
|
};
|
|
330
399
|
}
|
|
331
400
|
|
|
332
401
|
if (!ensureProject) {
|
|
333
|
-
throw new CodeKanbanValidationError(
|
|
402
|
+
throw new CodeKanbanValidationError(
|
|
403
|
+
`no CodeKanban project is registered for path: ${resolvedPath}`,
|
|
404
|
+
);
|
|
334
405
|
}
|
|
335
406
|
|
|
336
407
|
const created = await this.createProject({
|
|
337
408
|
path: resolvedPath,
|
|
338
409
|
name: pathBasename(resolvedPath),
|
|
339
|
-
description:
|
|
410
|
+
description: "",
|
|
340
411
|
});
|
|
341
412
|
return {
|
|
342
413
|
project: created,
|
|
343
|
-
matchedBy:
|
|
414
|
+
matchedBy: "created",
|
|
344
415
|
};
|
|
345
416
|
}
|
|
346
417
|
|
|
347
418
|
async resolveWorktree({ projectId, worktreeId }) {
|
|
348
|
-
const project = ensureString(projectId,
|
|
419
|
+
const project = ensureString(projectId, "projectId");
|
|
349
420
|
const preferredWorktreeId = ensureOptionalString(worktreeId);
|
|
350
421
|
const worktrees = await this.listWorktrees(project);
|
|
351
422
|
if (worktrees.length === 0) {
|
|
352
|
-
throw new CodeKanbanValidationError(
|
|
423
|
+
throw new CodeKanbanValidationError(
|
|
424
|
+
`no worktrees are available for project ${project}`,
|
|
425
|
+
);
|
|
353
426
|
}
|
|
354
427
|
if (preferredWorktreeId) {
|
|
355
|
-
const direct = worktrees.find(item => item.id === preferredWorktreeId);
|
|
428
|
+
const direct = worktrees.find((item) => item.id === preferredWorktreeId);
|
|
356
429
|
if (!direct) {
|
|
357
|
-
throw new CodeKanbanValidationError(
|
|
430
|
+
throw new CodeKanbanValidationError(
|
|
431
|
+
`worktree ${preferredWorktreeId} was not found in project ${project}`,
|
|
432
|
+
);
|
|
358
433
|
}
|
|
359
434
|
return direct;
|
|
360
435
|
}
|
|
361
|
-
return worktrees.find(item => item.isMain) || worktrees[0];
|
|
436
|
+
return worktrees.find((item) => item.isMain) || worktrees[0];
|
|
362
437
|
}
|
|
363
438
|
|
|
364
439
|
connectTerminal({ sessionId, wsPath, wsUrl }) {
|
|
365
|
-
const resolvedSessionId = ensureString(sessionId,
|
|
440
|
+
const resolvedSessionId = ensureString(sessionId, "sessionId");
|
|
366
441
|
const resolvedPath =
|
|
367
|
-
ensureOptionalString(wsUrl) ||
|
|
368
|
-
|
|
442
|
+
ensureOptionalString(wsUrl) ||
|
|
443
|
+
ensureOptionalString(wsPath) ||
|
|
444
|
+
`/api/v1/terminal/ws?sessionId=${resolvedSessionId}`;
|
|
445
|
+
const url =
|
|
446
|
+
resolvedPath.startsWith("ws://") || resolvedPath.startsWith("wss://")
|
|
447
|
+
? resolvedPath
|
|
448
|
+
: toWsUrl(this.baseURL, resolvedPath);
|
|
369
449
|
|
|
370
450
|
return new TerminalConnection({
|
|
371
451
|
sessionId: resolvedSessionId,
|
|
@@ -399,11 +479,23 @@ export class CodeKanbanClient {
|
|
|
399
479
|
}
|
|
400
480
|
}
|
|
401
481
|
|
|
402
|
-
async listSessions({
|
|
403
|
-
|
|
482
|
+
async listSessions({
|
|
483
|
+
projectId,
|
|
484
|
+
path,
|
|
485
|
+
includeTerminal = true,
|
|
486
|
+
includeAI = true,
|
|
487
|
+
ensureProject = true,
|
|
488
|
+
}) {
|
|
489
|
+
const { project, matchedBy } = await this.resolveProject({
|
|
490
|
+
projectId,
|
|
491
|
+
path,
|
|
492
|
+
ensureProject,
|
|
493
|
+
});
|
|
404
494
|
|
|
405
495
|
const [terminalSessions, aiSessions] = await Promise.all([
|
|
406
|
-
includeTerminal
|
|
496
|
+
includeTerminal
|
|
497
|
+
? this.listTerminalSessions(project.id)
|
|
498
|
+
: Promise.resolve([]),
|
|
407
499
|
includeAI
|
|
408
500
|
? this.listAISessionsByProject(project.id)
|
|
409
501
|
: Promise.resolve({
|
|
@@ -423,8 +515,14 @@ export class CodeKanbanClient {
|
|
|
423
515
|
}
|
|
424
516
|
|
|
425
517
|
async listWebSessions({ projectId, path, ensureProject = true } = {}) {
|
|
426
|
-
const resolvedProjectId = await this.resolveProjectId({
|
|
427
|
-
|
|
518
|
+
const resolvedProjectId = await this.resolveProjectId({
|
|
519
|
+
projectId,
|
|
520
|
+
path,
|
|
521
|
+
ensureProject,
|
|
522
|
+
});
|
|
523
|
+
const response = await this.requestJson(
|
|
524
|
+
`/api/v1/projects/${resolvedProjectId}/web-sessions`,
|
|
525
|
+
);
|
|
428
526
|
return response?.items || [];
|
|
429
527
|
}
|
|
430
528
|
|
|
@@ -435,142 +533,231 @@ export class CodeKanbanClient {
|
|
|
435
533
|
ensureProject: true,
|
|
436
534
|
});
|
|
437
535
|
|
|
438
|
-
const agent = ensureString(input.agent,
|
|
439
|
-
const permissionMode = ensureOptionalString(
|
|
536
|
+
const agent = ensureString(input.agent, "agent");
|
|
537
|
+
const permissionMode = ensureOptionalString(
|
|
538
|
+
input.permissionMode,
|
|
539
|
+
).toLowerCase();
|
|
440
540
|
const worktree = await this.resolveWorktree({
|
|
441
541
|
projectId,
|
|
442
542
|
worktreeId: input.worktreeId,
|
|
443
543
|
});
|
|
444
544
|
|
|
445
|
-
const response = await this.requestJson(
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
545
|
+
const response = await this.requestJson(
|
|
546
|
+
`/api/v1/projects/${projectId}/web-sessions`,
|
|
547
|
+
{
|
|
548
|
+
method: "POST",
|
|
549
|
+
body: {
|
|
550
|
+
worktreeId: ensureString(worktree?.id, "worktreeId"),
|
|
551
|
+
agent,
|
|
552
|
+
model:
|
|
553
|
+
ensureOptionalString(input.model) || defaultWebSessionModel(agent),
|
|
554
|
+
reasoningEffort:
|
|
555
|
+
ensureOptionalString(input.reasoningEffort) ||
|
|
556
|
+
defaultWebSessionReasoningEffort(agent),
|
|
557
|
+
workflowMode:
|
|
558
|
+
ensureOptionalString(input.workflowMode) ||
|
|
559
|
+
defaultWebSessionWorkflowMode(permissionMode),
|
|
560
|
+
permissionLevel:
|
|
561
|
+
ensureOptionalString(input.permissionLevel) ||
|
|
562
|
+
defaultWebSessionPermissionLevel(permissionMode),
|
|
563
|
+
autoRetryEnabled: input.autoRetryEnabled === true,
|
|
564
|
+
autoRetryScope: normalizeWebSessionAutoRetryScope(
|
|
565
|
+
input.autoRetryScope,
|
|
566
|
+
),
|
|
567
|
+
autoRetryPreset: normalizeWebSessionAutoRetryPreset(
|
|
568
|
+
input.autoRetryPreset,
|
|
569
|
+
),
|
|
570
|
+
permissionMode,
|
|
571
|
+
title: ensureOptionalString(input.title),
|
|
572
|
+
},
|
|
461
573
|
},
|
|
462
|
-
|
|
574
|
+
);
|
|
463
575
|
return response?.item || null;
|
|
464
576
|
}
|
|
465
577
|
|
|
466
578
|
async getWebSessionSnapshot({ projectId, path, sessionId, limit = 80 }) {
|
|
467
|
-
const resolvedProjectId = await this.resolveProjectId({
|
|
468
|
-
|
|
469
|
-
|
|
579
|
+
const resolvedProjectId = await this.resolveProjectId({
|
|
580
|
+
projectId,
|
|
581
|
+
path,
|
|
582
|
+
ensureProject: true,
|
|
583
|
+
});
|
|
584
|
+
const resolvedSessionId = ensureString(sessionId, "sessionId");
|
|
585
|
+
const normalizedLimit = Number.isFinite(limit)
|
|
586
|
+
? Math.max(1, Math.trunc(limit))
|
|
587
|
+
: 80;
|
|
470
588
|
const response = await this.requestJson(
|
|
471
589
|
`/api/v1/projects/${resolvedProjectId}/web-sessions/${resolvedSessionId}/snapshot?limit=${normalizedLimit}`,
|
|
472
590
|
);
|
|
473
591
|
return response?.item || null;
|
|
474
592
|
}
|
|
475
593
|
|
|
476
|
-
async getWebSessionHistory({
|
|
477
|
-
|
|
478
|
-
|
|
594
|
+
async getWebSessionHistory({
|
|
595
|
+
projectId,
|
|
596
|
+
path,
|
|
597
|
+
sessionId,
|
|
598
|
+
beforeCursor,
|
|
599
|
+
limit = 80,
|
|
600
|
+
}) {
|
|
601
|
+
const resolvedProjectId = await this.resolveProjectId({
|
|
602
|
+
projectId,
|
|
603
|
+
path,
|
|
604
|
+
ensureProject: true,
|
|
605
|
+
});
|
|
606
|
+
const resolvedSessionId = ensureString(sessionId, "sessionId");
|
|
479
607
|
const params = new URLSearchParams();
|
|
480
608
|
if (ensureOptionalString(beforeCursor)) {
|
|
481
|
-
params.set(
|
|
609
|
+
params.set("beforeCursor", ensureOptionalString(beforeCursor));
|
|
482
610
|
}
|
|
483
611
|
if (Number.isFinite(limit)) {
|
|
484
|
-
params.set(
|
|
612
|
+
params.set("limit", String(Math.max(1, Math.trunc(limit))));
|
|
485
613
|
}
|
|
486
614
|
const suffix = params.toString();
|
|
487
615
|
const response = await this.requestJson(
|
|
488
|
-
`/api/v1/projects/${resolvedProjectId}/web-sessions/${resolvedSessionId}/history${suffix ? `?${suffix}` :
|
|
616
|
+
`/api/v1/projects/${resolvedProjectId}/web-sessions/${resolvedSessionId}/history${suffix ? `?${suffix}` : ""}`,
|
|
489
617
|
);
|
|
490
618
|
return response?.item || null;
|
|
491
619
|
}
|
|
492
620
|
|
|
493
|
-
async syncWebSession({
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
621
|
+
async syncWebSession({
|
|
622
|
+
projectId,
|
|
623
|
+
path,
|
|
624
|
+
sessionId,
|
|
625
|
+
mode,
|
|
626
|
+
clearExisting = false,
|
|
627
|
+
}) {
|
|
628
|
+
const resolvedProjectId = await this.resolveProjectId({
|
|
629
|
+
projectId,
|
|
630
|
+
path,
|
|
631
|
+
ensureProject: true,
|
|
502
632
|
});
|
|
633
|
+
const resolvedSessionId = ensureString(sessionId, "sessionId");
|
|
634
|
+
const response = await this.requestJson(
|
|
635
|
+
`/api/v1/projects/${resolvedProjectId}/web-sessions/${resolvedSessionId}/sync`,
|
|
636
|
+
{
|
|
637
|
+
method: "POST",
|
|
638
|
+
body: {
|
|
639
|
+
...(ensureOptionalString(mode)
|
|
640
|
+
? { mode: ensureOptionalString(mode) }
|
|
641
|
+
: {}),
|
|
642
|
+
clearExisting: clearExisting === true,
|
|
643
|
+
},
|
|
644
|
+
},
|
|
645
|
+
);
|
|
503
646
|
return response?.item || null;
|
|
504
647
|
}
|
|
505
648
|
|
|
506
649
|
async archiveWebSession({ projectId, path, sessionId }) {
|
|
507
|
-
const resolvedProjectId = await this.resolveProjectId({
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
650
|
+
const resolvedProjectId = await this.resolveProjectId({
|
|
651
|
+
projectId,
|
|
652
|
+
path,
|
|
653
|
+
ensureProject: true,
|
|
511
654
|
});
|
|
655
|
+
const resolvedSessionId = ensureString(sessionId, "sessionId");
|
|
656
|
+
const response = await this.requestJson(
|
|
657
|
+
`/api/v1/projects/${resolvedProjectId}/web-sessions/${resolvedSessionId}/archive`,
|
|
658
|
+
{
|
|
659
|
+
method: "POST",
|
|
660
|
+
},
|
|
661
|
+
);
|
|
512
662
|
return response?.item || null;
|
|
513
663
|
}
|
|
514
664
|
|
|
515
665
|
async unarchiveWebSession({ projectId, path, sessionId }) {
|
|
516
|
-
const resolvedProjectId = await this.resolveProjectId({
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
666
|
+
const resolvedProjectId = await this.resolveProjectId({
|
|
667
|
+
projectId,
|
|
668
|
+
path,
|
|
669
|
+
ensureProject: true,
|
|
520
670
|
});
|
|
671
|
+
const resolvedSessionId = ensureString(sessionId, "sessionId");
|
|
672
|
+
const response = await this.requestJson(
|
|
673
|
+
`/api/v1/projects/${resolvedProjectId}/web-sessions/${resolvedSessionId}/unarchive`,
|
|
674
|
+
{
|
|
675
|
+
method: "POST",
|
|
676
|
+
},
|
|
677
|
+
);
|
|
521
678
|
return response?.item || null;
|
|
522
679
|
}
|
|
523
680
|
|
|
524
681
|
async renameWebSession({ projectId, path, sessionId, title }) {
|
|
525
|
-
const resolvedProjectId = await this.resolveProjectId({
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
body: {
|
|
530
|
-
title: ensureString(title, 'title'),
|
|
531
|
-
},
|
|
682
|
+
const resolvedProjectId = await this.resolveProjectId({
|
|
683
|
+
projectId,
|
|
684
|
+
path,
|
|
685
|
+
ensureProject: true,
|
|
532
686
|
});
|
|
687
|
+
const resolvedSessionId = ensureString(sessionId, "sessionId");
|
|
688
|
+
const response = await this.requestJson(
|
|
689
|
+
`/api/v1/projects/${resolvedProjectId}/web-sessions/${resolvedSessionId}/rename`,
|
|
690
|
+
{
|
|
691
|
+
method: "POST",
|
|
692
|
+
body: {
|
|
693
|
+
title: ensureString(title, "title"),
|
|
694
|
+
},
|
|
695
|
+
},
|
|
696
|
+
);
|
|
533
697
|
return response?.item || null;
|
|
534
698
|
}
|
|
535
699
|
|
|
536
700
|
async closeWebSession({ projectId, path, sessionId }) {
|
|
537
|
-
const resolvedProjectId = await this.resolveProjectId({
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
701
|
+
const resolvedProjectId = await this.resolveProjectId({
|
|
702
|
+
projectId,
|
|
703
|
+
path,
|
|
704
|
+
ensureProject: true,
|
|
541
705
|
});
|
|
706
|
+
const resolvedSessionId = ensureString(sessionId, "sessionId");
|
|
707
|
+
const response = await this.requestJson(
|
|
708
|
+
`/api/v1/projects/${resolvedProjectId}/web-sessions/${resolvedSessionId}/close`,
|
|
709
|
+
{
|
|
710
|
+
method: "POST",
|
|
711
|
+
},
|
|
712
|
+
);
|
|
542
713
|
return {
|
|
543
|
-
message: response?.message ||
|
|
714
|
+
message: response?.message || "session aborted",
|
|
544
715
|
};
|
|
545
716
|
}
|
|
546
717
|
|
|
547
718
|
async deleteWebSession({ projectId, path, sessionId }) {
|
|
548
|
-
const resolvedProjectId = await this.resolveProjectId({
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
719
|
+
const resolvedProjectId = await this.resolveProjectId({
|
|
720
|
+
projectId,
|
|
721
|
+
path,
|
|
722
|
+
ensureProject: true,
|
|
552
723
|
});
|
|
724
|
+
const resolvedSessionId = ensureString(sessionId, "sessionId");
|
|
725
|
+
const response = await this.requestJson(
|
|
726
|
+
`/api/v1/projects/${resolvedProjectId}/web-sessions/${resolvedSessionId}`,
|
|
727
|
+
{
|
|
728
|
+
method: "DELETE",
|
|
729
|
+
},
|
|
730
|
+
);
|
|
553
731
|
return {
|
|
554
|
-
message: response?.message ||
|
|
732
|
+
message: response?.message || "session deleted",
|
|
555
733
|
};
|
|
556
734
|
}
|
|
557
735
|
|
|
558
736
|
async queryArchivedWebSessions({ projectIds, offset = 0, limit = 20 }) {
|
|
559
|
-
const response = await this.requestJson(
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
737
|
+
const response = await this.requestJson(
|
|
738
|
+
"/api/v1/web-sessions/archived/query",
|
|
739
|
+
{
|
|
740
|
+
method: "POST",
|
|
741
|
+
body: {
|
|
742
|
+
projectIds: Array.isArray(projectIds) ? projectIds : [],
|
|
743
|
+
offset: Number.isFinite(offset) ? Math.max(0, Math.trunc(offset)) : 0,
|
|
744
|
+
limit: Number.isFinite(limit) ? Math.max(1, Math.trunc(limit)) : 20,
|
|
745
|
+
},
|
|
565
746
|
},
|
|
566
|
-
|
|
567
|
-
return
|
|
747
|
+
);
|
|
748
|
+
return (
|
|
749
|
+
response?.item || { items: [], total: 0, hasMore: false, nextOffset: 0 }
|
|
750
|
+
);
|
|
568
751
|
}
|
|
569
752
|
|
|
570
753
|
async getWebSessionCommandGroup({ projectId, path, sessionId, groupId }) {
|
|
571
|
-
const resolvedProjectId = await this.resolveProjectId({
|
|
572
|
-
|
|
573
|
-
|
|
754
|
+
const resolvedProjectId = await this.resolveProjectId({
|
|
755
|
+
projectId,
|
|
756
|
+
path,
|
|
757
|
+
ensureProject: true,
|
|
758
|
+
});
|
|
759
|
+
const resolvedSessionId = ensureString(sessionId, "sessionId");
|
|
760
|
+
const resolvedGroupId = ensureString(groupId, "groupId");
|
|
574
761
|
const response = await this.requestJson(
|
|
575
762
|
`/api/v1/projects/${resolvedProjectId}/web-sessions/${resolvedSessionId}/command-groups/${resolvedGroupId}`,
|
|
576
763
|
);
|
|
@@ -578,29 +765,48 @@ export class CodeKanbanClient {
|
|
|
578
765
|
}
|
|
579
766
|
|
|
580
767
|
async getWebSessionRuntimeConfig() {
|
|
581
|
-
const response = await this.requestJson(
|
|
768
|
+
const response = await this.requestJson(
|
|
769
|
+
"/api/v1/web-sessions/runtime-config",
|
|
770
|
+
);
|
|
582
771
|
return response?.item || null;
|
|
583
772
|
}
|
|
584
773
|
|
|
585
|
-
async uploadWebSessionAttachment({
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
774
|
+
async uploadWebSessionAttachment({
|
|
775
|
+
projectId,
|
|
776
|
+
path,
|
|
777
|
+
filePath,
|
|
778
|
+
fileName,
|
|
779
|
+
mimeType,
|
|
780
|
+
}) {
|
|
781
|
+
const resolvedProjectId = await this.resolveProjectId({
|
|
782
|
+
projectId,
|
|
783
|
+
path,
|
|
784
|
+
ensureProject: true,
|
|
785
|
+
});
|
|
786
|
+
const resolvedFilePath = ensureString(filePath, "filePath");
|
|
787
|
+
const resolvedFileName =
|
|
788
|
+
ensureOptionalString(fileName) || pathBasename(resolvedFilePath);
|
|
589
789
|
const resolvedMimeType = ensureImageMimeType(mimeType, resolvedFileName);
|
|
590
790
|
const fileBuffer = await readFile(resolvedFilePath);
|
|
591
791
|
const formData = new FormData();
|
|
592
|
-
formData.append(
|
|
792
|
+
formData.append(
|
|
793
|
+
"file",
|
|
794
|
+
new File([fileBuffer], resolvedFileName, { type: resolvedMimeType }),
|
|
795
|
+
);
|
|
593
796
|
|
|
594
797
|
const headers = {
|
|
595
|
-
Accept:
|
|
798
|
+
Accept: "application/json",
|
|
596
799
|
...this.headers,
|
|
597
800
|
};
|
|
598
|
-
delete headers[
|
|
801
|
+
delete headers["Content-Type"];
|
|
599
802
|
|
|
600
803
|
const response = await this.fetchImpl(
|
|
601
|
-
new URL(
|
|
804
|
+
new URL(
|
|
805
|
+
`/api/v1/projects/${resolvedProjectId}/web-sessions/attachments`,
|
|
806
|
+
this.baseURL,
|
|
807
|
+
),
|
|
602
808
|
{
|
|
603
|
-
method:
|
|
809
|
+
method: "POST",
|
|
604
810
|
headers,
|
|
605
811
|
body: formData,
|
|
606
812
|
},
|
|
@@ -610,7 +816,7 @@ export class CodeKanbanClient {
|
|
|
610
816
|
if (!response.ok) {
|
|
611
817
|
throw new CodeKanbanHttpError(`request failed with ${response.status}`, {
|
|
612
818
|
status: response.status,
|
|
613
|
-
method:
|
|
819
|
+
method: "POST",
|
|
614
820
|
path: `/api/v1/projects/${resolvedProjectId}/web-sessions/attachments`,
|
|
615
821
|
body,
|
|
616
822
|
});
|
|
@@ -632,17 +838,26 @@ export class CodeKanbanClient {
|
|
|
632
838
|
return this.analyzeWebSession(snapshot);
|
|
633
839
|
}
|
|
634
840
|
|
|
635
|
-
async sendWebSessionMessage({ sessionId, text, attachmentIds = [] }) {
|
|
636
|
-
return await this.withWebSessionCommandChannel(channel =>
|
|
841
|
+
async sendWebSessionMessage({ sessionId, text, attachmentIds = [], mode }) {
|
|
842
|
+
return await this.withWebSessionCommandChannel((channel) =>
|
|
637
843
|
channel.sendMessage(sessionId, {
|
|
638
844
|
text,
|
|
639
845
|
attachmentIds,
|
|
846
|
+
mode: ensureOptionalString(mode),
|
|
847
|
+
}),
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
async removeWebSessionPendingInput({ sessionId, pendingId }) {
|
|
852
|
+
return await this.withWebSessionCommandChannel((channel) =>
|
|
853
|
+
channel.removePendingInput(sessionId, {
|
|
854
|
+
pendingId,
|
|
640
855
|
}),
|
|
641
856
|
);
|
|
642
857
|
}
|
|
643
858
|
|
|
644
859
|
async updateWebSessionWorkflowMode({ sessionId, workflowMode }) {
|
|
645
|
-
return await this.withWebSessionCommandChannel(channel =>
|
|
860
|
+
return await this.withWebSessionCommandChannel((channel) =>
|
|
646
861
|
channel.updateWorkflowMode(sessionId, {
|
|
647
862
|
workflowMode,
|
|
648
863
|
}),
|
|
@@ -650,7 +865,7 @@ export class CodeKanbanClient {
|
|
|
650
865
|
}
|
|
651
866
|
|
|
652
867
|
async answerWebSessionUserInput({ sessionId, itemId, answers }) {
|
|
653
|
-
return await this.withWebSessionCommandChannel(channel =>
|
|
868
|
+
return await this.withWebSessionCommandChannel((channel) =>
|
|
654
869
|
channel.answerUserInput(sessionId, {
|
|
655
870
|
itemId,
|
|
656
871
|
answers,
|
|
@@ -659,14 +874,24 @@ export class CodeKanbanClient {
|
|
|
659
874
|
}
|
|
660
875
|
|
|
661
876
|
async approveWebSession({ sessionId }) {
|
|
662
|
-
return await this.withWebSessionCommandChannel(channel =>
|
|
877
|
+
return await this.withWebSessionCommandChannel((channel) =>
|
|
878
|
+
channel.approve(sessionId),
|
|
879
|
+
);
|
|
663
880
|
}
|
|
664
881
|
|
|
665
882
|
async rejectWebSession({ sessionId }) {
|
|
666
|
-
return await this.withWebSessionCommandChannel(channel =>
|
|
883
|
+
return await this.withWebSessionCommandChannel((channel) =>
|
|
884
|
+
channel.reject(sessionId),
|
|
885
|
+
);
|
|
667
886
|
}
|
|
668
887
|
|
|
669
|
-
async answerPendingUserInput({
|
|
888
|
+
async answerPendingUserInput({
|
|
889
|
+
projectId,
|
|
890
|
+
path,
|
|
891
|
+
sessionId,
|
|
892
|
+
answers,
|
|
893
|
+
limit = 120,
|
|
894
|
+
}) {
|
|
670
895
|
const state = await this.getWebSessionState({
|
|
671
896
|
projectId,
|
|
672
897
|
path,
|
|
@@ -674,7 +899,9 @@ export class CodeKanbanClient {
|
|
|
674
899
|
limit,
|
|
675
900
|
});
|
|
676
901
|
if (!state.pendingUserInput?.itemId) {
|
|
677
|
-
throw new CodeKanbanValidationError(
|
|
902
|
+
throw new CodeKanbanValidationError(
|
|
903
|
+
`web session ${sessionId} has no pending user input`,
|
|
904
|
+
);
|
|
678
905
|
}
|
|
679
906
|
const ack = await this.answerWebSessionUserInput({
|
|
680
907
|
sessionId,
|
|
@@ -698,7 +925,9 @@ export class CodeKanbanClient {
|
|
|
698
925
|
limit,
|
|
699
926
|
});
|
|
700
927
|
if (!state.pendingApproval) {
|
|
701
|
-
throw new CodeKanbanValidationError(
|
|
928
|
+
throw new CodeKanbanValidationError(
|
|
929
|
+
`web session ${sessionId} has no pending approval`,
|
|
930
|
+
);
|
|
702
931
|
}
|
|
703
932
|
const ack = await this.approveWebSession({ sessionId });
|
|
704
933
|
return {
|
|
@@ -717,7 +946,9 @@ export class CodeKanbanClient {
|
|
|
717
946
|
limit,
|
|
718
947
|
});
|
|
719
948
|
if (!state.pendingApproval) {
|
|
720
|
-
throw new CodeKanbanValidationError(
|
|
949
|
+
throw new CodeKanbanValidationError(
|
|
950
|
+
`web session ${sessionId} has no pending approval`,
|
|
951
|
+
);
|
|
721
952
|
}
|
|
722
953
|
const ack = await this.rejectWebSession({ sessionId });
|
|
723
954
|
return {
|
|
@@ -728,7 +959,13 @@ export class CodeKanbanClient {
|
|
|
728
959
|
};
|
|
729
960
|
}
|
|
730
961
|
|
|
731
|
-
async executeLatestPlan({
|
|
962
|
+
async executeLatestPlan({
|
|
963
|
+
projectId,
|
|
964
|
+
path,
|
|
965
|
+
sessionId,
|
|
966
|
+
prompt = "Implement the plan.",
|
|
967
|
+
limit = 120,
|
|
968
|
+
}) {
|
|
732
969
|
const state = await this.getWebSessionState({
|
|
733
970
|
projectId,
|
|
734
971
|
path,
|
|
@@ -736,16 +973,20 @@ export class CodeKanbanClient {
|
|
|
736
973
|
limit,
|
|
737
974
|
});
|
|
738
975
|
if (!state.latestPlan) {
|
|
739
|
-
throw new CodeKanbanValidationError(
|
|
976
|
+
throw new CodeKanbanValidationError(
|
|
977
|
+
`web session ${sessionId} has no latest plan to execute`,
|
|
978
|
+
);
|
|
740
979
|
}
|
|
741
|
-
if (!state.canSend && state.nextAction?.type !==
|
|
742
|
-
throw new CodeKanbanValidationError(
|
|
980
|
+
if (!state.canSend && state.nextAction?.type !== "execute_plan") {
|
|
981
|
+
throw new CodeKanbanValidationError(
|
|
982
|
+
`web session ${sessionId} is not ready to execute the latest plan`,
|
|
983
|
+
);
|
|
743
984
|
}
|
|
744
985
|
|
|
745
|
-
return await this.withWebSessionCommandChannel(async channel => {
|
|
746
|
-
if (state.session?.workflowMode ===
|
|
986
|
+
return await this.withWebSessionCommandChannel(async (channel) => {
|
|
987
|
+
if (state.session?.workflowMode === "plan") {
|
|
747
988
|
await channel.updateWorkflowMode(sessionId, {
|
|
748
|
-
workflowMode:
|
|
989
|
+
workflowMode: "default",
|
|
749
990
|
});
|
|
750
991
|
}
|
|
751
992
|
|
|
@@ -758,12 +999,14 @@ export class CodeKanbanClient {
|
|
|
758
999
|
const ack = await channel.answerUserInput(sessionId, {
|
|
759
1000
|
itemId: state.pendingUserInput.itemId,
|
|
760
1001
|
answers: {
|
|
761
|
-
[state.pendingUserInput.questionId]: [
|
|
1002
|
+
[state.pendingUserInput.questionId]: [
|
|
1003
|
+
state.pendingUserInput.executeOptionLabel,
|
|
1004
|
+
],
|
|
762
1005
|
},
|
|
763
1006
|
});
|
|
764
1007
|
return {
|
|
765
1008
|
sessionId,
|
|
766
|
-
mode:
|
|
1009
|
+
mode: "plan_choice",
|
|
767
1010
|
latestPlan: state.latestPlan,
|
|
768
1011
|
ack,
|
|
769
1012
|
state,
|
|
@@ -776,7 +1019,7 @@ export class CodeKanbanClient {
|
|
|
776
1019
|
});
|
|
777
1020
|
return {
|
|
778
1021
|
sessionId,
|
|
779
|
-
mode:
|
|
1022
|
+
mode: "followup_message",
|
|
780
1023
|
prompt,
|
|
781
1024
|
latestPlan: state.latestPlan,
|
|
782
1025
|
ack,
|
|
@@ -795,15 +1038,15 @@ export class CodeKanbanClient {
|
|
|
795
1038
|
limit = 120,
|
|
796
1039
|
}) {
|
|
797
1040
|
if (!until) {
|
|
798
|
-
throw new CodeKanbanValidationError(
|
|
1041
|
+
throw new CodeKanbanValidationError("until is required");
|
|
799
1042
|
}
|
|
800
1043
|
|
|
801
1044
|
const matches =
|
|
802
|
-
typeof until ===
|
|
1045
|
+
typeof until === "function"
|
|
803
1046
|
? until
|
|
804
1047
|
: Array.isArray(until)
|
|
805
|
-
? state => until.includes(state.phase)
|
|
806
|
-
: state => state.phase === until;
|
|
1048
|
+
? (state) => until.includes(state.phase)
|
|
1049
|
+
: (state) => state.phase === until;
|
|
807
1050
|
|
|
808
1051
|
const startedAt = Date.now();
|
|
809
1052
|
let lastRetryableError = null;
|
|
@@ -836,7 +1079,9 @@ export class CodeKanbanClient {
|
|
|
836
1079
|
);
|
|
837
1080
|
}
|
|
838
1081
|
|
|
839
|
-
throw new CodeKanbanValidationError(
|
|
1082
|
+
throw new CodeKanbanValidationError(
|
|
1083
|
+
`web session ${sessionId} did not reach the requested state within ${timeoutMs}ms`,
|
|
1084
|
+
);
|
|
840
1085
|
}
|
|
841
1086
|
|
|
842
1087
|
async startWorkflow(input = {}) {
|
|
@@ -855,7 +1100,10 @@ export class CodeKanbanClient {
|
|
|
855
1100
|
projectId: project.id,
|
|
856
1101
|
worktreeId: worktree.id,
|
|
857
1102
|
workingDir: ensureOptionalString(input.workingDir) || worktree.path,
|
|
858
|
-
title:
|
|
1103
|
+
title:
|
|
1104
|
+
ensureOptionalString(input.title) ||
|
|
1105
|
+
ensureOptionalString(input.prompt) ||
|
|
1106
|
+
"AI workflow",
|
|
859
1107
|
rows: Number.isFinite(input.rows) ? input.rows : 0,
|
|
860
1108
|
cols: Number.isFinite(input.cols) ? input.cols : 0,
|
|
861
1109
|
});
|
|
@@ -888,16 +1136,22 @@ export class CodeKanbanClient {
|
|
|
888
1136
|
}
|
|
889
1137
|
|
|
890
1138
|
async continueTerminalSession({ projectId, path, sessionId, prompt }) {
|
|
891
|
-
const resolvedSessionId = ensureString(sessionId,
|
|
892
|
-
const resolvedPrompt = ensureString(prompt,
|
|
1139
|
+
const resolvedSessionId = ensureString(sessionId, "sessionId");
|
|
1140
|
+
const resolvedPrompt = ensureString(prompt, "prompt");
|
|
893
1141
|
|
|
894
1142
|
let project;
|
|
895
1143
|
if (projectId || path) {
|
|
896
|
-
({ project } = await this.resolveProject({
|
|
1144
|
+
({ project } = await this.resolveProject({
|
|
1145
|
+
projectId,
|
|
1146
|
+
path,
|
|
1147
|
+
ensureProject: true,
|
|
1148
|
+
}));
|
|
897
1149
|
const sessions = await this.listTerminalSessions(project.id);
|
|
898
|
-
const exists = sessions.some(item => item.id === resolvedSessionId);
|
|
1150
|
+
const exists = sessions.some((item) => item.id === resolvedSessionId);
|
|
899
1151
|
if (!exists) {
|
|
900
|
-
throw new CodeKanbanValidationError(
|
|
1152
|
+
throw new CodeKanbanValidationError(
|
|
1153
|
+
`terminal session ${resolvedSessionId} does not belong to project ${project.id}`,
|
|
1154
|
+
);
|
|
901
1155
|
}
|
|
902
1156
|
}
|
|
903
1157
|
|