codekanban 0.26.0 → 0.27.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 +133 -0
- package/packages/node-sdk/src/cli.js +331 -5
- package/packages/node-sdk/src/client.js +432 -0
- package/packages/node-sdk/src/index.js +3 -0
- package/packages/node-sdk/src/web-session-command-channel.js +370 -0
- package/packages/node-sdk/src/web-session-event-stream.js +206 -0
- package/packages/node-sdk/src/web-session-shared.js +604 -0
- package/skills/codekanban-project-session/agents/openai.yaml +2 -2
|
@@ -1,6 +1,17 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
|
|
1
3
|
import { buildAgentLaunchSpec } from './command-builder.js';
|
|
2
4
|
import { CodeKanbanConfigError, CodeKanbanHttpError, CodeKanbanValidationError } from './errors.js';
|
|
3
5
|
import { TerminalConnection } from './terminal-connection.js';
|
|
6
|
+
import { WebSessionCommandChannel } from './web-session-command-channel.js';
|
|
7
|
+
import { WebSessionEventStream } from './web-session-event-stream.js';
|
|
8
|
+
import {
|
|
9
|
+
WEB_SESSION_COMMAND_WS_PATH,
|
|
10
|
+
WEB_SESSION_EVENTS_WS_PATH,
|
|
11
|
+
analyzeWebSession,
|
|
12
|
+
ensureImageMimeType,
|
|
13
|
+
normalizeWebSessionAttachment,
|
|
14
|
+
} from './web-session-shared.js';
|
|
4
15
|
import {
|
|
5
16
|
ensureOptionalString,
|
|
6
17
|
ensureString,
|
|
@@ -48,6 +59,15 @@ export class CodeKanbanClient {
|
|
|
48
59
|
return body;
|
|
49
60
|
}
|
|
50
61
|
|
|
62
|
+
async resolveProjectReference({ projectId, path, ensureProject = true }) {
|
|
63
|
+
const { project } = await this.resolveProject({
|
|
64
|
+
projectId,
|
|
65
|
+
path,
|
|
66
|
+
ensureProject,
|
|
67
|
+
});
|
|
68
|
+
return project;
|
|
69
|
+
}
|
|
70
|
+
|
|
51
71
|
async listProjects() {
|
|
52
72
|
const response = await this.requestJson('/api/v1/projects');
|
|
53
73
|
return response?.items || [];
|
|
@@ -223,6 +243,31 @@ export class CodeKanbanClient {
|
|
|
223
243
|
});
|
|
224
244
|
}
|
|
225
245
|
|
|
246
|
+
openWebSessionCommandChannel() {
|
|
247
|
+
return new WebSessionCommandChannel({
|
|
248
|
+
url: toWsUrl(this.baseURL, WEB_SESSION_COMMAND_WS_PATH),
|
|
249
|
+
WebSocketImpl: this.WebSocketImpl,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
openWebSessionEventStream(options = {}) {
|
|
254
|
+
return new WebSessionEventStream({
|
|
255
|
+
url: toWsUrl(this.baseURL, WEB_SESSION_EVENTS_WS_PATH),
|
|
256
|
+
sessionId: ensureOptionalString(options.sessionId),
|
|
257
|
+
WebSocketImpl: this.WebSocketImpl,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async withWebSessionCommandChannel(handler) {
|
|
262
|
+
const channel = this.openWebSessionCommandChannel();
|
|
263
|
+
try {
|
|
264
|
+
await channel.waitForOpen();
|
|
265
|
+
return await handler(channel);
|
|
266
|
+
} finally {
|
|
267
|
+
channel.close();
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
226
271
|
async listSessions({ projectId, path, includeTerminal = true, includeAI = true, ensureProject = true }) {
|
|
227
272
|
const { project, matchedBy } = await this.resolveProject({ projectId, path, ensureProject });
|
|
228
273
|
|
|
@@ -246,6 +291,393 @@ export class CodeKanbanClient {
|
|
|
246
291
|
};
|
|
247
292
|
}
|
|
248
293
|
|
|
294
|
+
async listWebSessions({ projectId, path, ensureProject = true } = {}) {
|
|
295
|
+
const project = await this.resolveProjectReference({ projectId, path, ensureProject });
|
|
296
|
+
const response = await this.requestJson(`/api/v1/projects/${project.id}/web-sessions`);
|
|
297
|
+
return response?.items || [];
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async createWebSession(input = {}) {
|
|
301
|
+
const project = await this.resolveProjectReference({
|
|
302
|
+
projectId: input.projectId,
|
|
303
|
+
path: input.path,
|
|
304
|
+
ensureProject: true,
|
|
305
|
+
});
|
|
306
|
+
const response = await this.requestJson(`/api/v1/projects/${project.id}/web-sessions`, {
|
|
307
|
+
method: 'POST',
|
|
308
|
+
body: {
|
|
309
|
+
worktreeId: ensureOptionalString(input.worktreeId),
|
|
310
|
+
agent: ensureString(input.agent, 'agent'),
|
|
311
|
+
model: ensureOptionalString(input.model),
|
|
312
|
+
reasoningEffort: ensureOptionalString(input.reasoningEffort),
|
|
313
|
+
workflowMode: ensureOptionalString(input.workflowMode),
|
|
314
|
+
permissionLevel: ensureOptionalString(input.permissionLevel),
|
|
315
|
+
permissionMode: ensureOptionalString(input.permissionMode),
|
|
316
|
+
title: ensureOptionalString(input.title),
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
return response?.item || null;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async getWebSessionSnapshot({ projectId, path, sessionId, limit = 80 }) {
|
|
323
|
+
const project = await this.resolveProjectReference({ projectId, path, ensureProject: true });
|
|
324
|
+
const resolvedSessionId = ensureString(sessionId, 'sessionId');
|
|
325
|
+
const normalizedLimit = Number.isFinite(limit) ? Math.max(1, Math.trunc(limit)) : 80;
|
|
326
|
+
const response = await this.requestJson(
|
|
327
|
+
`/api/v1/projects/${project.id}/web-sessions/${resolvedSessionId}/snapshot?limit=${normalizedLimit}`,
|
|
328
|
+
);
|
|
329
|
+
return response?.item || null;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async getWebSessionHistory({ projectId, path, sessionId, beforeCursor, limit = 80 }) {
|
|
333
|
+
const project = await this.resolveProjectReference({ projectId, path, ensureProject: true });
|
|
334
|
+
const resolvedSessionId = ensureString(sessionId, 'sessionId');
|
|
335
|
+
const params = new URLSearchParams();
|
|
336
|
+
if (ensureOptionalString(beforeCursor)) {
|
|
337
|
+
params.set('beforeCursor', ensureOptionalString(beforeCursor));
|
|
338
|
+
}
|
|
339
|
+
if (Number.isFinite(limit)) {
|
|
340
|
+
params.set('limit', String(Math.max(1, Math.trunc(limit))));
|
|
341
|
+
}
|
|
342
|
+
const suffix = params.toString();
|
|
343
|
+
const response = await this.requestJson(
|
|
344
|
+
`/api/v1/projects/${project.id}/web-sessions/${resolvedSessionId}/history${suffix ? `?${suffix}` : ''}`,
|
|
345
|
+
);
|
|
346
|
+
return response?.item || null;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async syncWebSession({ projectId, path, sessionId, mode, clearExisting = false }) {
|
|
350
|
+
const project = await this.resolveProjectReference({ projectId, path, ensureProject: true });
|
|
351
|
+
const resolvedSessionId = ensureString(sessionId, 'sessionId');
|
|
352
|
+
const response = await this.requestJson(`/api/v1/projects/${project.id}/web-sessions/${resolvedSessionId}/sync`, {
|
|
353
|
+
method: 'POST',
|
|
354
|
+
body: {
|
|
355
|
+
...(ensureOptionalString(mode) ? { mode: ensureOptionalString(mode) } : {}),
|
|
356
|
+
clearExisting: clearExisting === true,
|
|
357
|
+
},
|
|
358
|
+
});
|
|
359
|
+
return response?.item || null;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async archiveWebSession({ projectId, path, sessionId }) {
|
|
363
|
+
const project = await this.resolveProjectReference({ projectId, path, ensureProject: true });
|
|
364
|
+
const resolvedSessionId = ensureString(sessionId, 'sessionId');
|
|
365
|
+
const response = await this.requestJson(`/api/v1/projects/${project.id}/web-sessions/${resolvedSessionId}/archive`, {
|
|
366
|
+
method: 'POST',
|
|
367
|
+
});
|
|
368
|
+
return response?.item || null;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async unarchiveWebSession({ projectId, path, sessionId }) {
|
|
372
|
+
const project = await this.resolveProjectReference({ projectId, path, ensureProject: true });
|
|
373
|
+
const resolvedSessionId = ensureString(sessionId, 'sessionId');
|
|
374
|
+
const response = await this.requestJson(`/api/v1/projects/${project.id}/web-sessions/${resolvedSessionId}/unarchive`, {
|
|
375
|
+
method: 'POST',
|
|
376
|
+
});
|
|
377
|
+
return response?.item || null;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async renameWebSession({ projectId, path, sessionId, title }) {
|
|
381
|
+
const project = await this.resolveProjectReference({ projectId, path, ensureProject: true });
|
|
382
|
+
const resolvedSessionId = ensureString(sessionId, 'sessionId');
|
|
383
|
+
const response = await this.requestJson(`/api/v1/projects/${project.id}/web-sessions/${resolvedSessionId}/rename`, {
|
|
384
|
+
method: 'POST',
|
|
385
|
+
body: {
|
|
386
|
+
title: ensureString(title, 'title'),
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
return response?.item || null;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async closeWebSession({ projectId, path, sessionId }) {
|
|
393
|
+
const project = await this.resolveProjectReference({ projectId, path, ensureProject: true });
|
|
394
|
+
const resolvedSessionId = ensureString(sessionId, 'sessionId');
|
|
395
|
+
const response = await this.requestJson(`/api/v1/projects/${project.id}/web-sessions/${resolvedSessionId}/close`, {
|
|
396
|
+
method: 'POST',
|
|
397
|
+
});
|
|
398
|
+
return {
|
|
399
|
+
message: response?.message || 'session aborted',
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
async deleteWebSession({ projectId, path, sessionId }) {
|
|
404
|
+
const project = await this.resolveProjectReference({ projectId, path, ensureProject: true });
|
|
405
|
+
const resolvedSessionId = ensureString(sessionId, 'sessionId');
|
|
406
|
+
const response = await this.requestJson(`/api/v1/projects/${project.id}/web-sessions/${resolvedSessionId}`, {
|
|
407
|
+
method: 'DELETE',
|
|
408
|
+
});
|
|
409
|
+
return {
|
|
410
|
+
message: response?.message || 'session deleted',
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async queryArchivedWebSessions({ projectIds, offset = 0, limit = 20 }) {
|
|
415
|
+
const response = await this.requestJson('/api/v1/web-sessions/archived/query', {
|
|
416
|
+
method: 'POST',
|
|
417
|
+
body: {
|
|
418
|
+
projectIds: Array.isArray(projectIds) ? projectIds : [],
|
|
419
|
+
offset: Number.isFinite(offset) ? Math.max(0, Math.trunc(offset)) : 0,
|
|
420
|
+
limit: Number.isFinite(limit) ? Math.max(1, Math.trunc(limit)) : 20,
|
|
421
|
+
},
|
|
422
|
+
});
|
|
423
|
+
return response?.item || { items: [], total: 0, hasMore: false, nextOffset: 0 };
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async getWebSessionCommandGroup({ projectId, path, sessionId, groupId }) {
|
|
427
|
+
const project = await this.resolveProjectReference({ projectId, path, ensureProject: true });
|
|
428
|
+
const resolvedSessionId = ensureString(sessionId, 'sessionId');
|
|
429
|
+
const resolvedGroupId = ensureString(groupId, 'groupId');
|
|
430
|
+
const response = await this.requestJson(
|
|
431
|
+
`/api/v1/projects/${project.id}/web-sessions/${resolvedSessionId}/command-groups/${resolvedGroupId}`,
|
|
432
|
+
);
|
|
433
|
+
return response?.item || null;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async getWebSessionRuntimeConfig() {
|
|
437
|
+
const response = await this.requestJson('/api/v1/web-sessions/runtime-config');
|
|
438
|
+
return response?.item || null;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async uploadWebSessionAttachment({ projectId, path, filePath, fileName, mimeType }) {
|
|
442
|
+
const project = await this.resolveProjectReference({ projectId, path, ensureProject: true });
|
|
443
|
+
const resolvedFilePath = ensureString(filePath, 'filePath');
|
|
444
|
+
const resolvedFileName = ensureOptionalString(fileName) || pathBasename(resolvedFilePath);
|
|
445
|
+
const resolvedMimeType = ensureImageMimeType(mimeType, resolvedFileName);
|
|
446
|
+
const fileBuffer = await readFile(resolvedFilePath);
|
|
447
|
+
const formData = new FormData();
|
|
448
|
+
formData.append('file', new File([fileBuffer], resolvedFileName, { type: resolvedMimeType }));
|
|
449
|
+
|
|
450
|
+
const headers = {
|
|
451
|
+
Accept: 'application/json',
|
|
452
|
+
...this.headers,
|
|
453
|
+
};
|
|
454
|
+
delete headers['Content-Type'];
|
|
455
|
+
|
|
456
|
+
const response = await this.fetchImpl(
|
|
457
|
+
new URL(`/api/v1/projects/${project.id}/web-sessions/attachments`, this.baseURL),
|
|
458
|
+
{
|
|
459
|
+
method: 'POST',
|
|
460
|
+
headers,
|
|
461
|
+
body: formData,
|
|
462
|
+
},
|
|
463
|
+
);
|
|
464
|
+
const text = await response.text();
|
|
465
|
+
const body = text ? JSON.parse(text) : null;
|
|
466
|
+
if (!response.ok) {
|
|
467
|
+
throw new CodeKanbanHttpError(`request failed with ${response.status}`, {
|
|
468
|
+
status: response.status,
|
|
469
|
+
method: 'POST',
|
|
470
|
+
path: `/api/v1/projects/${project.id}/web-sessions/attachments`,
|
|
471
|
+
body,
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
return normalizeWebSessionAttachment(body?.item);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
analyzeWebSession(snapshot) {
|
|
478
|
+
return analyzeWebSession(snapshot);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
async getWebSessionState({ projectId, path, sessionId, limit = 120 }) {
|
|
482
|
+
const snapshot = await this.getWebSessionSnapshot({
|
|
483
|
+
projectId,
|
|
484
|
+
path,
|
|
485
|
+
sessionId,
|
|
486
|
+
limit,
|
|
487
|
+
});
|
|
488
|
+
return this.analyzeWebSession(snapshot);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
async sendWebSessionMessage({ sessionId, text, attachmentIds = [] }) {
|
|
492
|
+
return await this.withWebSessionCommandChannel(channel =>
|
|
493
|
+
channel.sendMessage(sessionId, {
|
|
494
|
+
text,
|
|
495
|
+
attachmentIds,
|
|
496
|
+
}),
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
async updateWebSessionWorkflowMode({ sessionId, workflowMode }) {
|
|
501
|
+
return await this.withWebSessionCommandChannel(channel =>
|
|
502
|
+
channel.updateWorkflowMode(sessionId, {
|
|
503
|
+
workflowMode,
|
|
504
|
+
}),
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
async answerWebSessionUserInput({ sessionId, itemId, answers }) {
|
|
509
|
+
return await this.withWebSessionCommandChannel(channel =>
|
|
510
|
+
channel.answerUserInput(sessionId, {
|
|
511
|
+
itemId,
|
|
512
|
+
answers,
|
|
513
|
+
}),
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
async approveWebSession({ sessionId }) {
|
|
518
|
+
return await this.withWebSessionCommandChannel(channel => channel.approve(sessionId));
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
async rejectWebSession({ sessionId }) {
|
|
522
|
+
return await this.withWebSessionCommandChannel(channel => channel.reject(sessionId));
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
async answerPendingUserInput({ projectId, path, sessionId, answers, limit = 120 }) {
|
|
526
|
+
const state = await this.getWebSessionState({
|
|
527
|
+
projectId,
|
|
528
|
+
path,
|
|
529
|
+
sessionId,
|
|
530
|
+
limit,
|
|
531
|
+
});
|
|
532
|
+
if (!state.pendingUserInput?.itemId) {
|
|
533
|
+
throw new CodeKanbanValidationError(`web session ${sessionId} has no pending user input`);
|
|
534
|
+
}
|
|
535
|
+
const ack = await this.answerWebSessionUserInput({
|
|
536
|
+
sessionId,
|
|
537
|
+
itemId: state.pendingUserInput.itemId,
|
|
538
|
+
answers,
|
|
539
|
+
});
|
|
540
|
+
return {
|
|
541
|
+
sessionId,
|
|
542
|
+
itemId: state.pendingUserInput.itemId,
|
|
543
|
+
prompt: state.pendingUserInput.prompt,
|
|
544
|
+
ack,
|
|
545
|
+
state,
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
async approvePending({ projectId, path, sessionId, limit = 120 }) {
|
|
550
|
+
const state = await this.getWebSessionState({
|
|
551
|
+
projectId,
|
|
552
|
+
path,
|
|
553
|
+
sessionId,
|
|
554
|
+
limit,
|
|
555
|
+
});
|
|
556
|
+
if (!state.pendingApproval) {
|
|
557
|
+
throw new CodeKanbanValidationError(`web session ${sessionId} has no pending approval`);
|
|
558
|
+
}
|
|
559
|
+
const ack = await this.approveWebSession({ sessionId });
|
|
560
|
+
return {
|
|
561
|
+
sessionId,
|
|
562
|
+
prompt: state.pendingApproval.prompt,
|
|
563
|
+
ack,
|
|
564
|
+
state,
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
async rejectPending({ projectId, path, sessionId, limit = 120 }) {
|
|
569
|
+
const state = await this.getWebSessionState({
|
|
570
|
+
projectId,
|
|
571
|
+
path,
|
|
572
|
+
sessionId,
|
|
573
|
+
limit,
|
|
574
|
+
});
|
|
575
|
+
if (!state.pendingApproval) {
|
|
576
|
+
throw new CodeKanbanValidationError(`web session ${sessionId} has no pending approval`);
|
|
577
|
+
}
|
|
578
|
+
const ack = await this.rejectWebSession({ sessionId });
|
|
579
|
+
return {
|
|
580
|
+
sessionId,
|
|
581
|
+
prompt: state.pendingApproval.prompt,
|
|
582
|
+
ack,
|
|
583
|
+
state,
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
async executeLatestPlan({ projectId, path, sessionId, prompt = 'Implement the plan.', limit = 120 }) {
|
|
588
|
+
const state = await this.getWebSessionState({
|
|
589
|
+
projectId,
|
|
590
|
+
path,
|
|
591
|
+
sessionId,
|
|
592
|
+
limit,
|
|
593
|
+
});
|
|
594
|
+
if (!state.latestPlan) {
|
|
595
|
+
throw new CodeKanbanValidationError(`web session ${sessionId} has no latest plan to execute`);
|
|
596
|
+
}
|
|
597
|
+
if (!state.canSend && state.nextAction?.type !== 'execute_plan') {
|
|
598
|
+
throw new CodeKanbanValidationError(`web session ${sessionId} is not ready to execute the latest plan`);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
return await this.withWebSessionCommandChannel(async channel => {
|
|
602
|
+
if (state.session?.workflowMode === 'plan') {
|
|
603
|
+
await channel.updateWorkflowMode(sessionId, {
|
|
604
|
+
workflowMode: 'default',
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
if (
|
|
609
|
+
state.pendingUserInput?.isPlanChoice &&
|
|
610
|
+
state.pendingUserInput.itemId &&
|
|
611
|
+
state.pendingUserInput.questionId &&
|
|
612
|
+
state.pendingUserInput.executeOptionLabel
|
|
613
|
+
) {
|
|
614
|
+
const ack = await channel.answerUserInput(sessionId, {
|
|
615
|
+
itemId: state.pendingUserInput.itemId,
|
|
616
|
+
answers: {
|
|
617
|
+
[state.pendingUserInput.questionId]: [state.pendingUserInput.executeOptionLabel],
|
|
618
|
+
},
|
|
619
|
+
});
|
|
620
|
+
return {
|
|
621
|
+
sessionId,
|
|
622
|
+
mode: 'plan_choice',
|
|
623
|
+
latestPlan: state.latestPlan,
|
|
624
|
+
ack,
|
|
625
|
+
state,
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const ack = await channel.sendMessage(sessionId, {
|
|
630
|
+
text: prompt,
|
|
631
|
+
attachmentIds: [],
|
|
632
|
+
});
|
|
633
|
+
return {
|
|
634
|
+
sessionId,
|
|
635
|
+
mode: 'followup_message',
|
|
636
|
+
prompt,
|
|
637
|
+
latestPlan: state.latestPlan,
|
|
638
|
+
ack,
|
|
639
|
+
state,
|
|
640
|
+
};
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
async waitForWebSessionState({
|
|
645
|
+
projectId,
|
|
646
|
+
path,
|
|
647
|
+
sessionId,
|
|
648
|
+
until,
|
|
649
|
+
intervalMs = 5000,
|
|
650
|
+
timeoutMs = 60000,
|
|
651
|
+
limit = 120,
|
|
652
|
+
}) {
|
|
653
|
+
if (!until) {
|
|
654
|
+
throw new CodeKanbanValidationError('until is required');
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const matches =
|
|
658
|
+
typeof until === 'function'
|
|
659
|
+
? until
|
|
660
|
+
: Array.isArray(until)
|
|
661
|
+
? state => until.includes(state.phase)
|
|
662
|
+
: state => state.phase === until;
|
|
663
|
+
|
|
664
|
+
const startedAt = Date.now();
|
|
665
|
+
while (Date.now() - startedAt <= timeoutMs) {
|
|
666
|
+
const state = await this.getWebSessionState({
|
|
667
|
+
projectId,
|
|
668
|
+
path,
|
|
669
|
+
sessionId,
|
|
670
|
+
limit,
|
|
671
|
+
});
|
|
672
|
+
if (matches(state)) {
|
|
673
|
+
return state;
|
|
674
|
+
}
|
|
675
|
+
await sleep(Math.max(1, Math.trunc(intervalMs)));
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
throw new CodeKanbanValidationError(`web session ${sessionId} did not reach the requested state within ${timeoutMs}ms`);
|
|
679
|
+
}
|
|
680
|
+
|
|
249
681
|
async startWorkflow(input = {}) {
|
|
250
682
|
const launch = buildAgentLaunchSpec(input);
|
|
251
683
|
const { project, matchedBy } = await this.resolveProject({
|
|
@@ -15,3 +15,6 @@ export {
|
|
|
15
15
|
CodeKanbanValidationError,
|
|
16
16
|
} from './errors.js';
|
|
17
17
|
export { TerminalConnection } from './terminal-connection.js';
|
|
18
|
+
export { WebSessionCommandChannel } from './web-session-command-channel.js';
|
|
19
|
+
export { WebSessionEventStream } from './web-session-event-stream.js';
|
|
20
|
+
export { analyzeWebSession } from './web-session-shared.js';
|