codekanban 0.27.0 → 0.28.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.
- package/package.json +6 -6
- package/packages/node-sdk/README.md +4 -2
- package/packages/node-sdk/package.json +1 -1
- package/packages/node-sdk/src/cli.js +0 -5
- package/packages/node-sdk/src/client.js +215 -55
- package/packages/node-sdk/src/web-session-command-channel.js +8 -0
- package/packages/node-sdk/src/web-session-event-stream.js +11 -1
- package/packages/node-sdk/src/web-session-shared.js +26 -1
package/package.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codekanban",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
4
4
|
"description": "An auxiliary programming tool for the AI era, helping you speed up 10x.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"codekanban": "npm-bin/codekanban.js"
|
|
7
7
|
},
|
|
8
8
|
"optionalDependencies": {
|
|
9
|
-
"@codekanban/win32-x64": "0.
|
|
10
|
-
"@codekanban/darwin-x64": "0.
|
|
11
|
-
"@codekanban/darwin-arm64": "0.
|
|
12
|
-
"@codekanban/linux-x64": "0.
|
|
13
|
-
"@codekanban/linux-arm64": "0.
|
|
9
|
+
"@codekanban/win32-x64": "0.28.0",
|
|
10
|
+
"@codekanban/darwin-x64": "0.28.0",
|
|
11
|
+
"@codekanban/darwin-arm64": "0.28.0",
|
|
12
|
+
"@codekanban/linux-x64": "0.28.0",
|
|
13
|
+
"@codekanban/linux-arm64": "0.28.0"
|
|
14
14
|
},
|
|
15
15
|
"keywords": [
|
|
16
16
|
"ai",
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
# `@codekanban/sdk`
|
|
2
2
|
|
|
3
|
-
Node SDK and CLI for CodeKanban
|
|
3
|
+
Node SDK and CLI for CodeKanban coding workflows, terminal sessions, and web sessions.
|
|
4
|
+
|
|
5
|
+
Use it as a programming-assistant toolkit for launching, observing, and steering CodeKanban work in a registered project or local path, with `web session` as the default structured path and `terminal session` for PTY-style flows.
|
|
4
6
|
|
|
5
7
|
## Features
|
|
6
8
|
|
|
7
9
|
- Resolve CodeKanban projects by `projectId` or local `path`
|
|
8
10
|
- Auto-create projects for new paths
|
|
9
11
|
- Select the main worktree automatically
|
|
10
|
-
- Launch
|
|
12
|
+
- Launch coding workflows for `codex` or `claude`
|
|
11
13
|
- Support `standard`, `plan`, and `yolo` Codex profiles
|
|
12
14
|
- Read terminal session lists and AI session summaries
|
|
13
15
|
- Read AI conversations and continue existing terminal sessions
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codekanban/sdk",
|
|
3
3
|
"version": "0.1.0",
|
|
4
|
-
"description": "Node SDK and CLI for launching CodeKanban
|
|
4
|
+
"description": "Node SDK and CLI for launching CodeKanban coding workflows, terminal sessions, and web sessions.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -113,10 +113,6 @@ function parseCliArgs(argv) {
|
|
|
113
113
|
flags.workingDir = readFlagValue(argv, index, token);
|
|
114
114
|
index += 1;
|
|
115
115
|
break;
|
|
116
|
-
case '--task-id':
|
|
117
|
-
flags.taskId = readFlagValue(argv, index, token);
|
|
118
|
-
index += 1;
|
|
119
|
-
break;
|
|
120
116
|
case '--worktree-id':
|
|
121
117
|
flags.worktreeId = readFlagValue(argv, index, token);
|
|
122
118
|
index += 1;
|
|
@@ -364,7 +360,6 @@ export async function runCli(argv, options = {}) {
|
|
|
364
360
|
extraArgs: flags.extraArgs,
|
|
365
361
|
title: flags.title,
|
|
366
362
|
workingDir: flags.workingDir,
|
|
367
|
-
taskId: flags.taskId,
|
|
368
363
|
});
|
|
369
364
|
} else if (scope === 'session' && action === 'list') {
|
|
370
365
|
result = await client.listSessions({
|
|
@@ -23,19 +23,122 @@ import {
|
|
|
23
23
|
toWsUrl,
|
|
24
24
|
} from './utils.js';
|
|
25
25
|
|
|
26
|
+
const DEFAULT_REQUEST_RETRY = Object.freeze({
|
|
27
|
+
attempts: 2,
|
|
28
|
+
baseDelayMs: 250,
|
|
29
|
+
maxDelayMs: 1000,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const RETRYABLE_HTTP_STATUS_CODES = new Set([408, 425, 429, 500, 502, 503, 504]);
|
|
33
|
+
|
|
34
|
+
const DEFAULT_WEB_SESSION_AUTO_RETRY_SCOPE = 'network_only';
|
|
35
|
+
const DEFAULT_WEB_SESSION_AUTO_RETRY_PRESET = 'gentle_stop';
|
|
36
|
+
|
|
37
|
+
function defaultWebSessionModel(agent) {
|
|
38
|
+
return agent === 'claude' ? 'opus' : 'gpt-5.4';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function defaultWebSessionReasoningEffort(agent) {
|
|
42
|
+
return agent === 'claude' ? 'default' : 'xhigh';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function defaultWebSessionWorkflowMode(permissionMode) {
|
|
46
|
+
return permissionMode === 'plan' ? 'plan' : 'default';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function defaultWebSessionPermissionLevel(permissionMode) {
|
|
50
|
+
return permissionMode === 'yolo' ? 'yolo' : 'elevated';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function normalizeWebSessionAutoRetryScope(scope) {
|
|
54
|
+
const normalized = ensureOptionalString(scope);
|
|
55
|
+
if (['network_only', 'network_and_rate_limit', 'all_failures'].includes(normalized)) {
|
|
56
|
+
return normalized;
|
|
57
|
+
}
|
|
58
|
+
return DEFAULT_WEB_SESSION_AUTO_RETRY_SCOPE;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function normalizeWebSessionAutoRetryPreset(preset) {
|
|
62
|
+
const normalized = ensureOptionalString(preset);
|
|
63
|
+
if (['gentle_stop', 'aggressive_stop', 'sustain_60s'].includes(normalized)) {
|
|
64
|
+
return normalized;
|
|
65
|
+
}
|
|
66
|
+
return DEFAULT_WEB_SESSION_AUTO_RETRY_PRESET;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function normalizeRequestRetryConfig(value = {}) {
|
|
70
|
+
const attempts = Number.isFinite(value.attempts) ? Math.max(1, Math.trunc(value.attempts)) : DEFAULT_REQUEST_RETRY.attempts;
|
|
71
|
+
const baseDelayMs = Number.isFinite(value.baseDelayMs)
|
|
72
|
+
? Math.max(1, Math.trunc(value.baseDelayMs))
|
|
73
|
+
: DEFAULT_REQUEST_RETRY.baseDelayMs;
|
|
74
|
+
const maxDelayMs = Number.isFinite(value.maxDelayMs)
|
|
75
|
+
? Math.max(baseDelayMs, Math.trunc(value.maxDelayMs))
|
|
76
|
+
: DEFAULT_REQUEST_RETRY.maxDelayMs;
|
|
77
|
+
return {
|
|
78
|
+
attempts,
|
|
79
|
+
baseDelayMs,
|
|
80
|
+
maxDelayMs,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function resolveRequestRetryConfig(method, defaults, override) {
|
|
85
|
+
if (override === false) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const normalizedMethod = String(method || 'GET').toUpperCase();
|
|
90
|
+
const source = override && typeof override === 'object' ? override : {};
|
|
91
|
+
const enabled =
|
|
92
|
+
typeof source.enabled === 'boolean'
|
|
93
|
+
? source.enabled
|
|
94
|
+
: ['GET', 'HEAD', 'OPTIONS'].includes(normalizedMethod);
|
|
95
|
+
if (!enabled) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return normalizeRequestRetryConfig({
|
|
100
|
+
...defaults,
|
|
101
|
+
...source,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function isRetryableRequestError(error) {
|
|
106
|
+
if (error instanceof CodeKanbanHttpError) {
|
|
107
|
+
return RETRYABLE_HTTP_STATUS_CODES.has(error.status);
|
|
108
|
+
}
|
|
109
|
+
if (error instanceof CodeKanbanValidationError || error instanceof CodeKanbanConfigError) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
if (error instanceof SyntaxError) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
if (error?.name === 'AbortError') {
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
if (error instanceof TypeError) {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function getRetryDelayMs(retryConfig, attemptIndex) {
|
|
125
|
+
return Math.min(retryConfig.maxDelayMs, retryConfig.baseDelayMs * 2 ** attemptIndex);
|
|
126
|
+
}
|
|
127
|
+
|
|
26
128
|
export class CodeKanbanClient {
|
|
27
129
|
constructor(options = {}) {
|
|
28
130
|
this.baseURL = normalizeBaseUrl(options.baseURL);
|
|
29
131
|
this.headers = { ...(options.headers || {}) };
|
|
30
132
|
this.fetchImpl = options.fetchImpl || globalThis.fetch;
|
|
31
133
|
this.WebSocketImpl = options.WebSocketImpl || globalThis.WebSocket;
|
|
134
|
+
this.requestRetry = normalizeRequestRetryConfig(options.requestRetry);
|
|
32
135
|
if (!this.fetchImpl) {
|
|
33
136
|
throw new CodeKanbanConfigError('fetch implementation is unavailable');
|
|
34
137
|
}
|
|
35
138
|
}
|
|
36
139
|
|
|
37
140
|
async requestJson(path, options = {}) {
|
|
38
|
-
const method = options.method || 'GET';
|
|
141
|
+
const method = String(options.method || 'GET').toUpperCase();
|
|
39
142
|
const headers = { Accept: 'application/json', ...this.headers, ...(options.headers || {}) };
|
|
40
143
|
const request = {
|
|
41
144
|
method,
|
|
@@ -45,18 +148,34 @@ export class CodeKanbanClient {
|
|
|
45
148
|
request.body = JSON.stringify(options.body);
|
|
46
149
|
request.headers['Content-Type'] = 'application/json';
|
|
47
150
|
}
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
body
|
|
57
|
-
|
|
151
|
+
|
|
152
|
+
const retryConfig = resolveRequestRetryConfig(method, this.requestRetry, options.retry);
|
|
153
|
+
const maxAttempts = retryConfig?.attempts ?? 1;
|
|
154
|
+
|
|
155
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
156
|
+
try {
|
|
157
|
+
const response = await this.fetchImpl(new URL(path, this.baseURL), request);
|
|
158
|
+
const text = await response.text();
|
|
159
|
+
const body = text ? JSON.parse(text) : null;
|
|
160
|
+
if (!response.ok) {
|
|
161
|
+
throw new CodeKanbanHttpError(`request failed with ${response.status}`, {
|
|
162
|
+
status: response.status,
|
|
163
|
+
method,
|
|
164
|
+
path,
|
|
165
|
+
body,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return body;
|
|
169
|
+
} catch (error) {
|
|
170
|
+
const canRetry = retryConfig && attempt + 1 < maxAttempts && isRetryableRequestError(error);
|
|
171
|
+
if (!canRetry) {
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
await sleep(getRetryDelayMs(retryConfig, attempt));
|
|
175
|
+
}
|
|
58
176
|
}
|
|
59
|
-
|
|
177
|
+
|
|
178
|
+
throw new CodeKanbanValidationError(`request retry loop exited unexpectedly for ${method} ${path}`);
|
|
60
179
|
}
|
|
61
180
|
|
|
62
181
|
async resolveProjectReference({ projectId, path, ensureProject = true }) {
|
|
@@ -68,6 +187,19 @@ export class CodeKanbanClient {
|
|
|
68
187
|
return project;
|
|
69
188
|
}
|
|
70
189
|
|
|
190
|
+
async resolveProjectId({ projectId, path, ensureProject = true }) {
|
|
191
|
+
const resolvedProjectId = ensureOptionalString(projectId);
|
|
192
|
+
if (resolvedProjectId) {
|
|
193
|
+
return resolvedProjectId;
|
|
194
|
+
}
|
|
195
|
+
const { project } = await this.resolveProject({
|
|
196
|
+
projectId,
|
|
197
|
+
path,
|
|
198
|
+
ensureProject,
|
|
199
|
+
});
|
|
200
|
+
return ensureString(project?.id, 'projectId');
|
|
201
|
+
}
|
|
202
|
+
|
|
71
203
|
async listProjects() {
|
|
72
204
|
const response = await this.requestJson('/api/v1/projects');
|
|
73
205
|
return response?.items || [];
|
|
@@ -156,7 +288,7 @@ export class CodeKanbanClient {
|
|
|
156
288
|
return response?.item || null;
|
|
157
289
|
}
|
|
158
290
|
|
|
159
|
-
async createTerminalSession({ projectId, worktreeId, workingDir = '', title = '', rows = 0, cols = 0
|
|
291
|
+
async createTerminalSession({ projectId, worktreeId, workingDir = '', title = '', rows = 0, cols = 0 }) {
|
|
160
292
|
ensureString(projectId, 'projectId');
|
|
161
293
|
ensureString(worktreeId, 'worktreeId');
|
|
162
294
|
const response = await this.requestJson(`/api/v1/projects/${projectId}/worktrees/${worktreeId}/terminals`, {
|
|
@@ -166,7 +298,6 @@ export class CodeKanbanClient {
|
|
|
166
298
|
title,
|
|
167
299
|
rows,
|
|
168
300
|
cols,
|
|
169
|
-
taskId,
|
|
170
301
|
},
|
|
171
302
|
});
|
|
172
303
|
return response?.item;
|
|
@@ -292,27 +423,40 @@ export class CodeKanbanClient {
|
|
|
292
423
|
}
|
|
293
424
|
|
|
294
425
|
async listWebSessions({ projectId, path, ensureProject = true } = {}) {
|
|
295
|
-
const
|
|
296
|
-
const response = await this.requestJson(`/api/v1/projects/${
|
|
426
|
+
const resolvedProjectId = await this.resolveProjectId({ projectId, path, ensureProject });
|
|
427
|
+
const response = await this.requestJson(`/api/v1/projects/${resolvedProjectId}/web-sessions`);
|
|
297
428
|
return response?.items || [];
|
|
298
429
|
}
|
|
299
430
|
|
|
300
431
|
async createWebSession(input = {}) {
|
|
301
|
-
const
|
|
432
|
+
const projectId = await this.resolveProjectId({
|
|
302
433
|
projectId: input.projectId,
|
|
303
434
|
path: input.path,
|
|
304
435
|
ensureProject: true,
|
|
305
436
|
});
|
|
306
|
-
|
|
437
|
+
|
|
438
|
+
const agent = ensureString(input.agent, 'agent');
|
|
439
|
+
const permissionMode = ensureOptionalString(input.permissionMode).toLowerCase();
|
|
440
|
+
const worktree = await this.resolveWorktree({
|
|
441
|
+
projectId,
|
|
442
|
+
worktreeId: input.worktreeId,
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
const response = await this.requestJson(`/api/v1/projects/${projectId}/web-sessions`, {
|
|
307
446
|
method: 'POST',
|
|
308
447
|
body: {
|
|
309
|
-
worktreeId:
|
|
310
|
-
agent
|
|
311
|
-
model: ensureOptionalString(input.model),
|
|
312
|
-
reasoningEffort:
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
448
|
+
worktreeId: ensureString(worktree?.id, 'worktreeId'),
|
|
449
|
+
agent,
|
|
450
|
+
model: ensureOptionalString(input.model) || defaultWebSessionModel(agent),
|
|
451
|
+
reasoningEffort:
|
|
452
|
+
ensureOptionalString(input.reasoningEffort) || defaultWebSessionReasoningEffort(agent),
|
|
453
|
+
workflowMode: ensureOptionalString(input.workflowMode) || defaultWebSessionWorkflowMode(permissionMode),
|
|
454
|
+
permissionLevel:
|
|
455
|
+
ensureOptionalString(input.permissionLevel) || defaultWebSessionPermissionLevel(permissionMode),
|
|
456
|
+
autoRetryEnabled: input.autoRetryEnabled === true,
|
|
457
|
+
autoRetryScope: normalizeWebSessionAutoRetryScope(input.autoRetryScope),
|
|
458
|
+
autoRetryPreset: normalizeWebSessionAutoRetryPreset(input.autoRetryPreset),
|
|
459
|
+
permissionMode,
|
|
316
460
|
title: ensureOptionalString(input.title),
|
|
317
461
|
},
|
|
318
462
|
});
|
|
@@ -320,17 +464,17 @@ export class CodeKanbanClient {
|
|
|
320
464
|
}
|
|
321
465
|
|
|
322
466
|
async getWebSessionSnapshot({ projectId, path, sessionId, limit = 80 }) {
|
|
323
|
-
const
|
|
467
|
+
const resolvedProjectId = await this.resolveProjectId({ projectId, path, ensureProject: true });
|
|
324
468
|
const resolvedSessionId = ensureString(sessionId, 'sessionId');
|
|
325
469
|
const normalizedLimit = Number.isFinite(limit) ? Math.max(1, Math.trunc(limit)) : 80;
|
|
326
470
|
const response = await this.requestJson(
|
|
327
|
-
`/api/v1/projects/${
|
|
471
|
+
`/api/v1/projects/${resolvedProjectId}/web-sessions/${resolvedSessionId}/snapshot?limit=${normalizedLimit}`,
|
|
328
472
|
);
|
|
329
473
|
return response?.item || null;
|
|
330
474
|
}
|
|
331
475
|
|
|
332
476
|
async getWebSessionHistory({ projectId, path, sessionId, beforeCursor, limit = 80 }) {
|
|
333
|
-
const
|
|
477
|
+
const resolvedProjectId = await this.resolveProjectId({ projectId, path, ensureProject: true });
|
|
334
478
|
const resolvedSessionId = ensureString(sessionId, 'sessionId');
|
|
335
479
|
const params = new URLSearchParams();
|
|
336
480
|
if (ensureOptionalString(beforeCursor)) {
|
|
@@ -341,15 +485,15 @@ export class CodeKanbanClient {
|
|
|
341
485
|
}
|
|
342
486
|
const suffix = params.toString();
|
|
343
487
|
const response = await this.requestJson(
|
|
344
|
-
`/api/v1/projects/${
|
|
488
|
+
`/api/v1/projects/${resolvedProjectId}/web-sessions/${resolvedSessionId}/history${suffix ? `?${suffix}` : ''}`,
|
|
345
489
|
);
|
|
346
490
|
return response?.item || null;
|
|
347
491
|
}
|
|
348
492
|
|
|
349
493
|
async syncWebSession({ projectId, path, sessionId, mode, clearExisting = false }) {
|
|
350
|
-
const
|
|
494
|
+
const resolvedProjectId = await this.resolveProjectId({ projectId, path, ensureProject: true });
|
|
351
495
|
const resolvedSessionId = ensureString(sessionId, 'sessionId');
|
|
352
|
-
const response = await this.requestJson(`/api/v1/projects/${
|
|
496
|
+
const response = await this.requestJson(`/api/v1/projects/${resolvedProjectId}/web-sessions/${resolvedSessionId}/sync`, {
|
|
353
497
|
method: 'POST',
|
|
354
498
|
body: {
|
|
355
499
|
...(ensureOptionalString(mode) ? { mode: ensureOptionalString(mode) } : {}),
|
|
@@ -360,27 +504,27 @@ export class CodeKanbanClient {
|
|
|
360
504
|
}
|
|
361
505
|
|
|
362
506
|
async archiveWebSession({ projectId, path, sessionId }) {
|
|
363
|
-
const
|
|
507
|
+
const resolvedProjectId = await this.resolveProjectId({ projectId, path, ensureProject: true });
|
|
364
508
|
const resolvedSessionId = ensureString(sessionId, 'sessionId');
|
|
365
|
-
const response = await this.requestJson(`/api/v1/projects/${
|
|
509
|
+
const response = await this.requestJson(`/api/v1/projects/${resolvedProjectId}/web-sessions/${resolvedSessionId}/archive`, {
|
|
366
510
|
method: 'POST',
|
|
367
511
|
});
|
|
368
512
|
return response?.item || null;
|
|
369
513
|
}
|
|
370
514
|
|
|
371
515
|
async unarchiveWebSession({ projectId, path, sessionId }) {
|
|
372
|
-
const
|
|
516
|
+
const resolvedProjectId = await this.resolveProjectId({ projectId, path, ensureProject: true });
|
|
373
517
|
const resolvedSessionId = ensureString(sessionId, 'sessionId');
|
|
374
|
-
const response = await this.requestJson(`/api/v1/projects/${
|
|
518
|
+
const response = await this.requestJson(`/api/v1/projects/${resolvedProjectId}/web-sessions/${resolvedSessionId}/unarchive`, {
|
|
375
519
|
method: 'POST',
|
|
376
520
|
});
|
|
377
521
|
return response?.item || null;
|
|
378
522
|
}
|
|
379
523
|
|
|
380
524
|
async renameWebSession({ projectId, path, sessionId, title }) {
|
|
381
|
-
const
|
|
525
|
+
const resolvedProjectId = await this.resolveProjectId({ projectId, path, ensureProject: true });
|
|
382
526
|
const resolvedSessionId = ensureString(sessionId, 'sessionId');
|
|
383
|
-
const response = await this.requestJson(`/api/v1/projects/${
|
|
527
|
+
const response = await this.requestJson(`/api/v1/projects/${resolvedProjectId}/web-sessions/${resolvedSessionId}/rename`, {
|
|
384
528
|
method: 'POST',
|
|
385
529
|
body: {
|
|
386
530
|
title: ensureString(title, 'title'),
|
|
@@ -390,9 +534,9 @@ export class CodeKanbanClient {
|
|
|
390
534
|
}
|
|
391
535
|
|
|
392
536
|
async closeWebSession({ projectId, path, sessionId }) {
|
|
393
|
-
const
|
|
537
|
+
const resolvedProjectId = await this.resolveProjectId({ projectId, path, ensureProject: true });
|
|
394
538
|
const resolvedSessionId = ensureString(sessionId, 'sessionId');
|
|
395
|
-
const response = await this.requestJson(`/api/v1/projects/${
|
|
539
|
+
const response = await this.requestJson(`/api/v1/projects/${resolvedProjectId}/web-sessions/${resolvedSessionId}/close`, {
|
|
396
540
|
method: 'POST',
|
|
397
541
|
});
|
|
398
542
|
return {
|
|
@@ -401,9 +545,9 @@ export class CodeKanbanClient {
|
|
|
401
545
|
}
|
|
402
546
|
|
|
403
547
|
async deleteWebSession({ projectId, path, sessionId }) {
|
|
404
|
-
const
|
|
548
|
+
const resolvedProjectId = await this.resolveProjectId({ projectId, path, ensureProject: true });
|
|
405
549
|
const resolvedSessionId = ensureString(sessionId, 'sessionId');
|
|
406
|
-
const response = await this.requestJson(`/api/v1/projects/${
|
|
550
|
+
const response = await this.requestJson(`/api/v1/projects/${resolvedProjectId}/web-sessions/${resolvedSessionId}`, {
|
|
407
551
|
method: 'DELETE',
|
|
408
552
|
});
|
|
409
553
|
return {
|
|
@@ -424,11 +568,11 @@ export class CodeKanbanClient {
|
|
|
424
568
|
}
|
|
425
569
|
|
|
426
570
|
async getWebSessionCommandGroup({ projectId, path, sessionId, groupId }) {
|
|
427
|
-
const
|
|
571
|
+
const resolvedProjectId = await this.resolveProjectId({ projectId, path, ensureProject: true });
|
|
428
572
|
const resolvedSessionId = ensureString(sessionId, 'sessionId');
|
|
429
573
|
const resolvedGroupId = ensureString(groupId, 'groupId');
|
|
430
574
|
const response = await this.requestJson(
|
|
431
|
-
`/api/v1/projects/${
|
|
575
|
+
`/api/v1/projects/${resolvedProjectId}/web-sessions/${resolvedSessionId}/command-groups/${resolvedGroupId}`,
|
|
432
576
|
);
|
|
433
577
|
return response?.item || null;
|
|
434
578
|
}
|
|
@@ -439,7 +583,7 @@ export class CodeKanbanClient {
|
|
|
439
583
|
}
|
|
440
584
|
|
|
441
585
|
async uploadWebSessionAttachment({ projectId, path, filePath, fileName, mimeType }) {
|
|
442
|
-
const
|
|
586
|
+
const resolvedProjectId = await this.resolveProjectId({ projectId, path, ensureProject: true });
|
|
443
587
|
const resolvedFilePath = ensureString(filePath, 'filePath');
|
|
444
588
|
const resolvedFileName = ensureOptionalString(fileName) || pathBasename(resolvedFilePath);
|
|
445
589
|
const resolvedMimeType = ensureImageMimeType(mimeType, resolvedFileName);
|
|
@@ -454,7 +598,7 @@ export class CodeKanbanClient {
|
|
|
454
598
|
delete headers['Content-Type'];
|
|
455
599
|
|
|
456
600
|
const response = await this.fetchImpl(
|
|
457
|
-
new URL(`/api/v1/projects/${
|
|
601
|
+
new URL(`/api/v1/projects/${resolvedProjectId}/web-sessions/attachments`, this.baseURL),
|
|
458
602
|
{
|
|
459
603
|
method: 'POST',
|
|
460
604
|
headers,
|
|
@@ -467,7 +611,7 @@ export class CodeKanbanClient {
|
|
|
467
611
|
throw new CodeKanbanHttpError(`request failed with ${response.status}`, {
|
|
468
612
|
status: response.status,
|
|
469
613
|
method: 'POST',
|
|
470
|
-
path: `/api/v1/projects/${
|
|
614
|
+
path: `/api/v1/projects/${resolvedProjectId}/web-sessions/attachments`,
|
|
471
615
|
body,
|
|
472
616
|
});
|
|
473
617
|
}
|
|
@@ -662,19 +806,36 @@ export class CodeKanbanClient {
|
|
|
662
806
|
: state => state.phase === until;
|
|
663
807
|
|
|
664
808
|
const startedAt = Date.now();
|
|
809
|
+
let lastRetryableError = null;
|
|
665
810
|
while (Date.now() - startedAt <= timeoutMs) {
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
811
|
+
try {
|
|
812
|
+
const state = await this.getWebSessionState({
|
|
813
|
+
projectId,
|
|
814
|
+
path,
|
|
815
|
+
sessionId,
|
|
816
|
+
limit,
|
|
817
|
+
});
|
|
818
|
+
lastRetryableError = null;
|
|
819
|
+
if (matches(state)) {
|
|
820
|
+
return state;
|
|
821
|
+
}
|
|
822
|
+
} catch (error) {
|
|
823
|
+
if (!isRetryableRequestError(error)) {
|
|
824
|
+
throw error;
|
|
825
|
+
}
|
|
826
|
+
lastRetryableError = error;
|
|
674
827
|
}
|
|
828
|
+
|
|
675
829
|
await sleep(Math.max(1, Math.trunc(intervalMs)));
|
|
676
830
|
}
|
|
677
831
|
|
|
832
|
+
if (lastRetryableError) {
|
|
833
|
+
throw new CodeKanbanValidationError(
|
|
834
|
+
`web session ${sessionId} did not reach the requested state within ${timeoutMs}ms (last transient error: ${lastRetryableError.message})`,
|
|
835
|
+
{ cause: lastRetryableError },
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
|
|
678
839
|
throw new CodeKanbanValidationError(`web session ${sessionId} did not reach the requested state within ${timeoutMs}ms`);
|
|
679
840
|
}
|
|
680
841
|
|
|
@@ -695,7 +856,6 @@ export class CodeKanbanClient {
|
|
|
695
856
|
worktreeId: worktree.id,
|
|
696
857
|
workingDir: ensureOptionalString(input.workingDir) || worktree.path,
|
|
697
858
|
title: ensureOptionalString(input.title) || ensureOptionalString(input.prompt) || 'AI workflow',
|
|
698
|
-
taskId: ensureOptionalString(input.taskId),
|
|
699
859
|
rows: Number.isFinite(input.rows) ? input.rows : 0,
|
|
700
860
|
cols: Number.isFinite(input.cols) ? input.cols : 0,
|
|
701
861
|
});
|
|
@@ -2,7 +2,9 @@ import { CodeKanbanConfigError, CodeKanbanError, CodeKanbanValidationError } fro
|
|
|
2
2
|
import {
|
|
3
3
|
WEB_SESSION_PROTOCOL_VERSION,
|
|
4
4
|
buildWebSessionCommandFrame,
|
|
5
|
+
buildWebSessionHeartbeatFrame,
|
|
5
6
|
decodeWebSessionSocketMessage,
|
|
7
|
+
isWebSessionHeartbeatFrame,
|
|
6
8
|
normalizeWebSessionFrame,
|
|
7
9
|
} from './web-session-shared.js';
|
|
8
10
|
import { ensureArrayOfStrings, ensureOptionalString, ensureString } from './utils.js';
|
|
@@ -307,6 +309,12 @@ export class WebSessionCommandChannel {
|
|
|
307
309
|
|
|
308
310
|
_handleMessage(data) {
|
|
309
311
|
const rawFrame = decodeWebSessionSocketMessage(data);
|
|
312
|
+
if (isWebSessionHeartbeatFrame(rawFrame)) {
|
|
313
|
+
if (rawFrame?.op === 'ping' && this.socket.readyState === SOCKET_OPEN) {
|
|
314
|
+
this.socket.send(JSON.stringify(buildWebSessionHeartbeatFrame('pong')));
|
|
315
|
+
}
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
310
318
|
const frame = normalizeWebSessionFrame(rawFrame);
|
|
311
319
|
|
|
312
320
|
if (frame.type === 'error' && frame.requestId && this._pendingRequests.has(frame.requestId)) {
|
|
@@ -2,7 +2,9 @@ import { EventEmitter } from 'node:events';
|
|
|
2
2
|
|
|
3
3
|
import { CodeKanbanConfigError, CodeKanbanError, CodeKanbanValidationError } from './errors.js';
|
|
4
4
|
import {
|
|
5
|
+
buildWebSessionHeartbeatFrame,
|
|
5
6
|
decodeWebSessionSocketMessage,
|
|
7
|
+
isWebSessionHeartbeatFrame,
|
|
6
8
|
normalizeWebSessionFrame,
|
|
7
9
|
shouldEmitWebSessionFrame,
|
|
8
10
|
} from './web-session-shared.js';
|
|
@@ -158,9 +160,17 @@ export class WebSessionEventStream {
|
|
|
158
160
|
}
|
|
159
161
|
|
|
160
162
|
_handleMessage(data) {
|
|
163
|
+
let rawFrame;
|
|
161
164
|
let frame;
|
|
162
165
|
try {
|
|
163
|
-
|
|
166
|
+
rawFrame = decodeWebSessionSocketMessage(data);
|
|
167
|
+
if (isWebSessionHeartbeatFrame(rawFrame)) {
|
|
168
|
+
if (rawFrame?.op === 'ping' && this.socket.readyState === SOCKET_OPEN) {
|
|
169
|
+
this.socket.send(JSON.stringify(buildWebSessionHeartbeatFrame('pong')));
|
|
170
|
+
}
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
frame = normalizeWebSessionFrame(rawFrame);
|
|
164
174
|
} catch (error) {
|
|
165
175
|
const payload = {
|
|
166
176
|
type: 'error',
|
|
@@ -6,6 +6,7 @@ import { ensureArrayOfStrings, ensureOptionalString, ensureString } from './util
|
|
|
6
6
|
export const WEB_SESSION_PROTOCOL_VERSION = 1;
|
|
7
7
|
export const WEB_SESSION_COMMAND_WS_PATH = '/api/v1/web-sessions/ws';
|
|
8
8
|
export const WEB_SESSION_EVENTS_WS_PATH = '/api/v1/web-sessions/events';
|
|
9
|
+
export const WEB_SESSION_HEARTBEAT_KIND = 'hb';
|
|
9
10
|
|
|
10
11
|
const IMAGE_MIME_BY_EXT = new Map([
|
|
11
12
|
['.apng', 'image/apng'],
|
|
@@ -504,6 +505,14 @@ export function normalizeWebSessionFrame(rawFrame) {
|
|
|
504
505
|
};
|
|
505
506
|
}
|
|
506
507
|
|
|
508
|
+
if (rawFrame?.k === WEB_SESSION_HEARTBEAT_KIND) {
|
|
509
|
+
return {
|
|
510
|
+
...base,
|
|
511
|
+
type: 'heartbeat',
|
|
512
|
+
heartbeatType: trimmedString(rawFrame?.op) || null,
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
|
|
507
516
|
if (rawFrame?.k === 'snap') {
|
|
508
517
|
return {
|
|
509
518
|
...base,
|
|
@@ -560,6 +569,19 @@ export function buildWebSessionCommandFrame({ requestId, sessionId, operation, p
|
|
|
560
569
|
};
|
|
561
570
|
}
|
|
562
571
|
|
|
572
|
+
export function buildWebSessionHeartbeatFrame(operation) {
|
|
573
|
+
return {
|
|
574
|
+
v: WEB_SESSION_PROTOCOL_VERSION,
|
|
575
|
+
k: WEB_SESSION_HEARTBEAT_KIND,
|
|
576
|
+
ts: Date.now(),
|
|
577
|
+
op: ensureString(operation, 'operation'),
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
export function isWebSessionHeartbeatFrame(rawFrame) {
|
|
582
|
+
return rawFrame?.k === WEB_SESSION_HEARTBEAT_KIND;
|
|
583
|
+
}
|
|
584
|
+
|
|
563
585
|
export function normalizeWebSessionAttachment(value) {
|
|
564
586
|
if (!value || typeof value !== 'object') {
|
|
565
587
|
return null;
|
|
@@ -592,7 +614,7 @@ export function ensureImageMimeType(value, fileName) {
|
|
|
592
614
|
export function shouldEmitWebSessionFrame(frame, sessionIdFilter) {
|
|
593
615
|
const normalizedFilter = ensureOptionalString(sessionIdFilter);
|
|
594
616
|
if (!normalizedFilter) {
|
|
595
|
-
return
|
|
617
|
+
return frame?.type !== 'heartbeat';
|
|
596
618
|
}
|
|
597
619
|
if (!frame || typeof frame !== 'object') {
|
|
598
620
|
return false;
|
|
@@ -600,5 +622,8 @@ export function shouldEmitWebSessionFrame(frame, sessionIdFilter) {
|
|
|
600
622
|
if (frame.type === 'open' || frame.type === 'close' || frame.type === 'error') {
|
|
601
623
|
return true;
|
|
602
624
|
}
|
|
625
|
+
if (frame.type === 'heartbeat') {
|
|
626
|
+
return false;
|
|
627
|
+
}
|
|
603
628
|
return ensureOptionalString(frame.sessionId) === normalizedFilter;
|
|
604
629
|
}
|