codekanban 0.31.0 → 0.33.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.
@@ -17,6 +17,7 @@ import {
17
17
  normalizeWebSessionAttachment,
18
18
  } from "./web-session-shared.js";
19
19
  import {
20
+ ensureArrayOfStrings,
20
21
  ensureOptionalString,
21
22
  ensureString,
22
23
  normalizeBaseUrl,
@@ -143,12 +144,274 @@ function getRetryDelayMs(retryConfig, attemptIndex) {
143
144
  );
144
145
  }
145
146
 
147
+ function normalizeWebSessionQuestionChoice(value) {
148
+ return String(value || "").trim();
149
+ }
150
+
151
+ function buildAutoWebSessionAnswers(
152
+ questions = [],
153
+ strategy = "prefer-second-or-text",
154
+ ) {
155
+ const normalizedStrategy = String(
156
+ strategy || "prefer-second-or-text",
157
+ ).trim().toLowerCase();
158
+ if (
159
+ !["prefer-second-or-text", "prefer-second-or-first"].includes(
160
+ normalizedStrategy,
161
+ )
162
+ ) {
163
+ throw new CodeKanbanValidationError(
164
+ `unsupported answer strategy: ${strategy}`,
165
+ );
166
+ }
167
+
168
+ const answers = {};
169
+ for (const question of Array.isArray(questions) ? questions : []) {
170
+ const questionId = normalizeWebSessionQuestionChoice(question?.id);
171
+ if (!questionId) {
172
+ continue;
173
+ }
174
+ const options = Array.isArray(question?.options)
175
+ ? question.options
176
+ .map((option) => normalizeWebSessionQuestionChoice(option?.label))
177
+ .filter(Boolean)
178
+ : [];
179
+ if (options[1]) {
180
+ answers[questionId] = [options[1]];
181
+ continue;
182
+ }
183
+ if (normalizedStrategy === "prefer-second-or-first" && options[0]) {
184
+ answers[questionId] = [options[0]];
185
+ continue;
186
+ }
187
+ answers[questionId] = [question?.isSecret ? "redacted" : "continue"];
188
+ }
189
+ return answers;
190
+ }
191
+
192
+ function normalizeWebSessionStateMatcher(until) {
193
+ if (!until) {
194
+ throw new CodeKanbanValidationError("until is required");
195
+ }
196
+ return typeof until === "function"
197
+ ? until
198
+ : Array.isArray(until)
199
+ ? (state) => until.includes(state.phase)
200
+ : (state) => state.phase === until;
201
+ }
202
+
203
+ function getWebSessionPauseReason(state, untilMatcher) {
204
+ if (!state) {
205
+ return null;
206
+ }
207
+ if (state.phase === "done") {
208
+ return "done";
209
+ }
210
+ if (state.phase === "error") {
211
+ return "error";
212
+ }
213
+ if (typeof untilMatcher === "function" && untilMatcher(state)) {
214
+ return "until";
215
+ }
216
+ if (state.nextAction?.type === "approval") {
217
+ return "approval";
218
+ }
219
+ if (state.nextAction?.type === "answer_user_input") {
220
+ return "user_input";
221
+ }
222
+ if (
223
+ state.nextAction?.type === "execute_plan" ||
224
+ (state.latestPlan?.awaitingExecution && state.canSend)
225
+ ) {
226
+ return "execute_plan";
227
+ }
228
+ return null;
229
+ }
230
+
231
+ function isDebouncedPauseReason(reason) {
232
+ return reason === "done" || reason === "error" || reason === "until";
233
+ }
234
+
235
+ function hasOwnField(value, fieldName) {
236
+ return Boolean(
237
+ value &&
238
+ typeof value === "object" &&
239
+ Object.prototype.hasOwnProperty.call(value, fieldName),
240
+ );
241
+ }
242
+
243
+ function extractPayloadItem(payload) {
244
+ if (hasOwnField(payload?.body, "item")) {
245
+ return payload.body.item;
246
+ }
247
+ if (hasOwnField(payload, "item")) {
248
+ return payload.item;
249
+ }
250
+ return null;
251
+ }
252
+
253
+ function extractPayloadItems(payload) {
254
+ if (Array.isArray(payload?.body?.items)) {
255
+ return payload.body.items;
256
+ }
257
+ if (Array.isArray(payload?.items)) {
258
+ return payload.items;
259
+ }
260
+ return [];
261
+ }
262
+
263
+ function extractPayloadMessage(payload) {
264
+ if (typeof payload?.body?.message === "string") {
265
+ return payload.body.message;
266
+ }
267
+ if (typeof payload?.message === "string") {
268
+ return payload.message;
269
+ }
270
+ return "";
271
+ }
272
+
273
+ function normalizeProjectSearchToken(value) {
274
+ return ensureOptionalString(value).toLowerCase();
275
+ }
276
+
277
+ function uniqueProjectsById(projects) {
278
+ const seen = new Set();
279
+ return projects.filter((project) => {
280
+ const id = ensureOptionalString(project?.id);
281
+ if (!id || seen.has(id)) {
282
+ return false;
283
+ }
284
+ seen.add(id);
285
+ return true;
286
+ });
287
+ }
288
+
289
+ function projectPathBaseName(project) {
290
+ const projectPath = ensureOptionalString(project?.path);
291
+ return projectPath ? normalizeProjectSearchToken(pathBasename(projectPath)) : "";
292
+ }
293
+
294
+ function normalizeProjectIndex(projectIndex) {
295
+ if (projectIndex == null || projectIndex === "") {
296
+ return null;
297
+ }
298
+ const normalized = Number(projectIndex);
299
+ if (!Number.isInteger(normalized) || normalized < 1) {
300
+ throw new CodeKanbanValidationError(
301
+ "projectIndex must be a positive integer",
302
+ );
303
+ }
304
+ return normalized;
305
+ }
306
+
307
+ function selectProjectCandidate(
308
+ projectName,
309
+ candidates,
310
+ projectIndex,
311
+ reason,
312
+ matchedBy,
313
+ ) {
314
+ const normalizedProjectIndex = normalizeProjectIndex(projectIndex);
315
+ if (normalizedProjectIndex != null) {
316
+ const selected = candidates[normalizedProjectIndex - 1];
317
+ if (!selected) {
318
+ throw new CodeKanbanValidationError(
319
+ `projectIndex ${normalizedProjectIndex} is out of range for projectName ${projectName}`,
320
+ );
321
+ }
322
+ return {
323
+ project: selected,
324
+ matchedBy,
325
+ };
326
+ }
327
+ throw new CodeKanbanValidationError(
328
+ `projectName ${projectName} ${reason}; provide projectIndex or projectId`,
329
+ );
330
+ }
331
+
332
+ function resolveProjectByName(projects, projectName, projectIndex) {
333
+ const needle = normalizeProjectSearchToken(projectName);
334
+ if (!needle) {
335
+ throw new CodeKanbanValidationError("projectName is required");
336
+ }
337
+
338
+ const exactName = uniqueProjectsById(
339
+ projects.filter(
340
+ (project) => normalizeProjectSearchToken(project?.name) === needle,
341
+ ),
342
+ );
343
+ if (exactName.length === 1) {
344
+ return {
345
+ project: exactName[0],
346
+ matchedBy: "projectName",
347
+ };
348
+ }
349
+ if (exactName.length > 1) {
350
+ return selectProjectCandidate(
351
+ projectName,
352
+ exactName,
353
+ projectIndex,
354
+ "matches multiple projects",
355
+ "projectName",
356
+ );
357
+ }
358
+
359
+ const exactBaseName = uniqueProjectsById(
360
+ projects.filter((project) => projectPathBaseName(project) === needle),
361
+ );
362
+ if (exactBaseName.length === 1) {
363
+ return {
364
+ project: exactBaseName[0],
365
+ matchedBy: "projectPathBaseName",
366
+ };
367
+ }
368
+ if (exactBaseName.length > 1) {
369
+ return selectProjectCandidate(
370
+ projectName,
371
+ exactBaseName,
372
+ projectIndex,
373
+ "matches multiple project paths",
374
+ "projectPathBaseName",
375
+ );
376
+ }
377
+
378
+ const fuzzy = uniqueProjectsById(
379
+ projects.filter((project) => {
380
+ const normalizedName = normalizeProjectSearchToken(project?.name);
381
+ const normalizedBaseName = projectPathBaseName(project);
382
+ return (
383
+ normalizedName.includes(needle) || normalizedBaseName.includes(needle)
384
+ );
385
+ }),
386
+ );
387
+ if (fuzzy.length === 1) {
388
+ return {
389
+ project: fuzzy[0],
390
+ matchedBy: "projectNameFuzzy",
391
+ };
392
+ }
393
+ if (fuzzy.length > 1) {
394
+ return selectProjectCandidate(
395
+ projectName,
396
+ fuzzy,
397
+ projectIndex,
398
+ "is ambiguous",
399
+ "projectNameFuzzy",
400
+ );
401
+ }
402
+
403
+ throw new CodeKanbanValidationError(
404
+ `no CodeKanban project is registered for projectName: ${projectName}`,
405
+ );
406
+ }
407
+
146
408
  export class CodeKanbanClient {
147
409
  constructor(options = {}) {
148
410
  this.baseURL = normalizeBaseUrl(options.baseURL);
149
411
  this.headers = { ...(options.headers || {}) };
150
412
  this.fetchImpl = options.fetchImpl || globalThis.fetch;
151
413
  this.WebSocketImpl = options.WebSocketImpl || globalThis.WebSocket;
414
+ this.webSocketOptions = options.webSocketOptions || null;
152
415
  this.requestRetry = normalizeRequestRetryConfig(options.requestRetry);
153
416
  if (!this.fetchImpl) {
154
417
  throw new CodeKanbanConfigError("fetch implementation is unavailable");
@@ -215,22 +478,104 @@ export class CodeKanbanClient {
215
478
  );
216
479
  }
217
480
 
218
- async resolveProjectReference({ projectId, path, ensureProject = true }) {
481
+ async requestText(path, options = {}) {
482
+ const method = String(options.method || "GET").toUpperCase();
483
+ const headers = {
484
+ ...this.headers,
485
+ ...(options.headers || {}),
486
+ };
487
+ const request = {
488
+ method,
489
+ headers,
490
+ };
491
+
492
+ const retryConfig = resolveRequestRetryConfig(
493
+ method,
494
+ this.requestRetry,
495
+ options.retry,
496
+ );
497
+ const maxAttempts = retryConfig?.attempts ?? 1;
498
+
499
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
500
+ try {
501
+ const response = await this.fetchImpl(
502
+ new URL(path, this.baseURL),
503
+ request,
504
+ );
505
+ const text = await response.text();
506
+ if (!response.ok) {
507
+ let body = null;
508
+ try {
509
+ body = text ? JSON.parse(text) : null;
510
+ } catch {
511
+ body = text || null;
512
+ }
513
+ throw new CodeKanbanHttpError(
514
+ `request failed with ${response.status}`,
515
+ {
516
+ status: response.status,
517
+ method,
518
+ path,
519
+ body,
520
+ },
521
+ );
522
+ }
523
+ return {
524
+ text,
525
+ contentType:
526
+ typeof response.headers?.get === "function"
527
+ ? response.headers.get("content-type") || ""
528
+ : "",
529
+ };
530
+ } catch (error) {
531
+ const canRetry =
532
+ retryConfig &&
533
+ attempt + 1 < maxAttempts &&
534
+ isRetryableRequestError(error);
535
+ if (!canRetry) {
536
+ throw error;
537
+ }
538
+ await sleep(getRetryDelayMs(retryConfig, attempt));
539
+ }
540
+ }
541
+
542
+ throw new CodeKanbanValidationError(
543
+ `request retry loop exited unexpectedly for ${method} ${path}`,
544
+ );
545
+ }
546
+
547
+ async resolveProjectReference({
548
+ projectId,
549
+ projectName,
550
+ projectIndex,
551
+ path,
552
+ ensureProject = true,
553
+ }) {
219
554
  const { project } = await this.resolveProject({
220
555
  projectId,
556
+ projectName,
557
+ projectIndex,
221
558
  path,
222
559
  ensureProject,
223
560
  });
224
561
  return project;
225
562
  }
226
563
 
227
- async resolveProjectId({ projectId, path, ensureProject = true }) {
564
+ async resolveProjectId({
565
+ projectId,
566
+ projectName,
567
+ projectIndex,
568
+ path,
569
+ ensureProject = true,
570
+ }) {
228
571
  const resolvedProjectId = ensureOptionalString(projectId);
229
572
  if (resolvedProjectId) {
230
573
  return resolvedProjectId;
231
574
  }
232
575
  const { project } = await this.resolveProject({
233
576
  projectId,
577
+ projectName,
578
+ projectIndex,
234
579
  path,
235
580
  ensureProject,
236
581
  });
@@ -239,13 +584,13 @@ export class CodeKanbanClient {
239
584
 
240
585
  async listProjects() {
241
586
  const response = await this.requestJson("/api/v1/projects");
242
- return response?.items || [];
587
+ return extractPayloadItems(response);
243
588
  }
244
589
 
245
590
  async getProject(projectId) {
246
591
  ensureString(projectId, "projectId");
247
592
  const response = await this.requestJson(`/api/v1/projects/${projectId}`);
248
- return response?.item;
593
+ return extractPayloadItem(response);
249
594
  }
250
595
 
251
596
  async createProject({
@@ -267,7 +612,7 @@ export class CodeKanbanClient {
267
612
  hidePath,
268
613
  },
269
614
  });
270
- return response?.item;
615
+ return extractPayloadItem(response);
271
616
  }
272
617
 
273
618
  async listWorktrees(projectId) {
@@ -275,7 +620,7 @@ export class CodeKanbanClient {
275
620
  const response = await this.requestJson(
276
621
  `/api/v1/projects/${projectId}/worktrees`,
277
622
  );
278
- return response?.items || [];
623
+ return extractPayloadItems(response);
279
624
  }
280
625
 
281
626
  async listTerminalSessions(projectId) {
@@ -283,7 +628,7 @@ export class CodeKanbanClient {
283
628
  const response = await this.requestJson(
284
629
  `/api/v1/projects/${projectId}/terminals`,
285
630
  );
286
- return response?.items || [];
631
+ return extractPayloadItems(response);
287
632
  }
288
633
 
289
634
  async listAISessionsByProject(projectId) {
@@ -291,7 +636,7 @@ export class CodeKanbanClient {
291
636
  const response = await this.requestJson(
292
637
  `/api/v1/projects/${projectId}/ai-sessions`,
293
638
  );
294
- return response?.item || null;
639
+ return extractPayloadItem(response);
295
640
  }
296
641
 
297
642
  async listAISessionsByPath(projectPath) {
@@ -300,7 +645,7 @@ export class CodeKanbanClient {
300
645
  method: "POST",
301
646
  body: { path: projectPath },
302
647
  });
303
- return response?.item || null;
648
+ return extractPayloadItem(response);
304
649
  }
305
650
 
306
651
  async getAISessionConversation({ id, sessionId, refresh = false }) {
@@ -322,13 +667,13 @@ export class CodeKanbanClient {
322
667
  const response = await this.requestJson(path, {
323
668
  method: refresh ? "POST" : "GET",
324
669
  });
325
- return response?.item || null;
670
+ return extractPayloadItem(response);
326
671
  }
327
672
 
328
673
  const response = await this.requestJson(
329
674
  `/api/v1/ai-sessions/by-session-id/${rawSessionId}/conversation`,
330
675
  );
331
- return response?.item || null;
676
+ return extractPayloadItem(response);
332
677
  }
333
678
 
334
679
  async getAISessionToolResult({ id, sessionId, toolUseId }) {
@@ -342,7 +687,7 @@ export class CodeKanbanClient {
342
687
  ? `/api/v1/ai-sessions/${dbId}/conversation/tool-results/${resolvedToolUseId}`
343
688
  : `/api/v1/ai-sessions/by-session-id/${rawSessionId}/conversation/tool-results/${resolvedToolUseId}`;
344
689
  const response = await this.requestJson(path);
345
- return response?.item || null;
690
+ return extractPayloadItem(response);
346
691
  }
347
692
 
348
693
  async createTerminalSession({
@@ -367,11 +712,18 @@ export class CodeKanbanClient {
367
712
  },
368
713
  },
369
714
  );
370
- return response?.item;
715
+ return extractPayloadItem(response);
371
716
  }
372
717
 
373
- async resolveProject({ projectId, path, ensureProject = true }) {
718
+ async resolveProject({
719
+ projectId,
720
+ projectName,
721
+ projectIndex,
722
+ path,
723
+ ensureProject = true,
724
+ }) {
374
725
  const resolvedProjectId = ensureOptionalString(projectId);
726
+ const resolvedProjectName = ensureOptionalString(projectName);
375
727
  const resolvedPath = ensureOptionalString(path);
376
728
 
377
729
  if (resolvedProjectId) {
@@ -382,12 +734,20 @@ export class CodeKanbanClient {
382
734
  };
383
735
  }
384
736
 
737
+ const projects =
738
+ resolvedProjectName || resolvedPath ? await this.listProjects() : [];
739
+
740
+ if (resolvedProjectName) {
741
+ return resolveProjectByName(projects, resolvedProjectName, projectIndex);
742
+ }
743
+
385
744
  if (!resolvedPath) {
386
- throw new CodeKanbanValidationError("projectId or path is required");
745
+ throw new CodeKanbanValidationError(
746
+ "projectId, projectName, or path is required",
747
+ );
387
748
  }
388
749
 
389
750
  const target = normalizeFsPath(resolvedPath);
390
- const projects = await this.listProjects();
391
751
  const project = projects.find(
392
752
  (item) => normalizeFsPath(item.path) === target,
393
753
  );
@@ -451,6 +811,7 @@ export class CodeKanbanClient {
451
811
  sessionId: resolvedSessionId,
452
812
  url,
453
813
  WebSocketImpl: this.WebSocketImpl,
814
+ webSocketOptions: this.webSocketOptions,
454
815
  });
455
816
  }
456
817
 
@@ -458,6 +819,7 @@ export class CodeKanbanClient {
458
819
  return new WebSessionCommandChannel({
459
820
  url: toWsUrl(this.baseURL, WEB_SESSION_COMMAND_WS_PATH),
460
821
  WebSocketImpl: this.WebSocketImpl,
822
+ webSocketOptions: this.webSocketOptions,
461
823
  });
462
824
  }
463
825
 
@@ -466,6 +828,7 @@ export class CodeKanbanClient {
466
828
  url: toWsUrl(this.baseURL, WEB_SESSION_EVENTS_WS_PATH),
467
829
  sessionId: ensureOptionalString(options.sessionId),
468
830
  WebSocketImpl: this.WebSocketImpl,
831
+ webSocketOptions: this.webSocketOptions,
469
832
  });
470
833
  }
471
834
 
@@ -481,6 +844,8 @@ export class CodeKanbanClient {
481
844
 
482
845
  async listSessions({
483
846
  projectId,
847
+ projectName,
848
+ projectIndex,
484
849
  path,
485
850
  includeTerminal = true,
486
851
  includeAI = true,
@@ -488,6 +853,8 @@ export class CodeKanbanClient {
488
853
  }) {
489
854
  const { project, matchedBy } = await this.resolveProject({
490
855
  projectId,
856
+ projectName,
857
+ projectIndex,
491
858
  path,
492
859
  ensureProject,
493
860
  });
@@ -514,21 +881,108 @@ export class CodeKanbanClient {
514
881
  };
515
882
  }
516
883
 
517
- async listWebSessions({ projectId, path, ensureProject = true } = {}) {
884
+ async listProjectFileScopes({ projectId, projectName, projectIndex, path, ensureProject = true } = {}) {
518
885
  const resolvedProjectId = await this.resolveProjectId({
519
886
  projectId,
887
+ projectName,
888
+ projectIndex,
889
+ path,
890
+ ensureProject,
891
+ });
892
+ const response = await this.requestJson(
893
+ `/api/v1/projects/${resolvedProjectId}/files/scopes`,
894
+ );
895
+ return extractPayloadItems(response);
896
+ }
897
+
898
+ async readProjectFileText({
899
+ projectId,
900
+ projectName,
901
+ projectIndex,
902
+ path,
903
+ scopeId,
904
+ filePath,
905
+ ensureProject = true,
906
+ } = {}) {
907
+ const resolvedProjectId = await this.resolveProjectId({
908
+ projectId,
909
+ projectName,
910
+ projectIndex,
911
+ path,
912
+ ensureProject,
913
+ });
914
+ const params = new URLSearchParams();
915
+ if (ensureOptionalString(scopeId)) {
916
+ params.set("scopeId", ensureOptionalString(scopeId));
917
+ }
918
+ params.set("path", ensureString(filePath, "filePath"));
919
+ params.set("disposition", "inline");
920
+ const { text, contentType } = await this.requestText(
921
+ `/api/v1/projects/${resolvedProjectId}/files/content?${params.toString()}`,
922
+ );
923
+ return {
924
+ projectId: resolvedProjectId,
925
+ scopeId: ensureOptionalString(scopeId) || null,
926
+ path: ensureString(filePath, "filePath"),
927
+ contentType,
928
+ text,
929
+ };
930
+ }
931
+
932
+ async deleteProjectFiles({
933
+ projectId,
934
+ projectName,
935
+ projectIndex,
936
+ path,
937
+ scopeId,
938
+ paths,
939
+ ensureProject = true,
940
+ } = {}) {
941
+ const resolvedProjectId = await this.resolveProjectId({
942
+ projectId,
943
+ projectName,
944
+ projectIndex,
945
+ path,
946
+ ensureProject,
947
+ });
948
+ const normalizedPaths = ensureArrayOfStrings(paths, "paths");
949
+ if (normalizedPaths.length === 0) {
950
+ throw new CodeKanbanValidationError("paths is required");
951
+ }
952
+ const response = await this.requestJson(
953
+ `/api/v1/projects/${resolvedProjectId}/files/delete`,
954
+ {
955
+ method: "POST",
956
+ body: {
957
+ ...(ensureOptionalString(scopeId)
958
+ ? { scopeId: ensureOptionalString(scopeId) }
959
+ : {}),
960
+ paths: normalizedPaths,
961
+ },
962
+ },
963
+ );
964
+ return extractPayloadItem(response);
965
+ }
966
+
967
+ async listWebSessions({ projectId, projectName, projectIndex, path, ensureProject = true } = {}) {
968
+ const resolvedProjectId = await this.resolveProjectId({
969
+ projectId,
970
+ projectName,
971
+ projectIndex,
520
972
  path,
521
973
  ensureProject,
522
974
  });
523
975
  const response = await this.requestJson(
524
976
  `/api/v1/projects/${resolvedProjectId}/web-sessions`,
525
977
  );
526
- return response?.items || [];
978
+ return extractPayloadItems(response);
527
979
  }
528
980
 
529
981
  async createWebSession(input = {}) {
530
982
  const projectId = await this.resolveProjectId({
531
983
  projectId: input.projectId,
984
+ projectName: input.projectName,
985
+ projectIndex: input.projectIndex,
532
986
  path: input.path,
533
987
  ensureProject: true,
534
988
  });
@@ -572,12 +1026,14 @@ export class CodeKanbanClient {
572
1026
  },
573
1027
  },
574
1028
  );
575
- return response?.item || null;
1029
+ return extractPayloadItem(response);
576
1030
  }
577
1031
 
578
- async getWebSessionSnapshot({ projectId, path, sessionId, limit = 80 }) {
1032
+ async getWebSessionSnapshot({ projectId, projectName, projectIndex, path, sessionId, limit = 80 }) {
579
1033
  const resolvedProjectId = await this.resolveProjectId({
580
1034
  projectId,
1035
+ projectName,
1036
+ projectIndex,
581
1037
  path,
582
1038
  ensureProject: true,
583
1039
  });
@@ -588,11 +1044,13 @@ export class CodeKanbanClient {
588
1044
  const response = await this.requestJson(
589
1045
  `/api/v1/projects/${resolvedProjectId}/web-sessions/${resolvedSessionId}/snapshot?limit=${normalizedLimit}`,
590
1046
  );
591
- return response?.item || null;
1047
+ return extractPayloadItem(response);
592
1048
  }
593
1049
 
594
1050
  async getWebSessionHistory({
595
1051
  projectId,
1052
+ projectName,
1053
+ projectIndex,
596
1054
  path,
597
1055
  sessionId,
598
1056
  beforeCursor,
@@ -600,6 +1058,8 @@ export class CodeKanbanClient {
600
1058
  }) {
601
1059
  const resolvedProjectId = await this.resolveProjectId({
602
1060
  projectId,
1061
+ projectName,
1062
+ projectIndex,
603
1063
  path,
604
1064
  ensureProject: true,
605
1065
  });
@@ -615,11 +1075,13 @@ export class CodeKanbanClient {
615
1075
  const response = await this.requestJson(
616
1076
  `/api/v1/projects/${resolvedProjectId}/web-sessions/${resolvedSessionId}/history${suffix ? `?${suffix}` : ""}`,
617
1077
  );
618
- return response?.item || null;
1078
+ return extractPayloadItem(response);
619
1079
  }
620
1080
 
621
1081
  async syncWebSession({
622
1082
  projectId,
1083
+ projectName,
1084
+ projectIndex,
623
1085
  path,
624
1086
  sessionId,
625
1087
  mode,
@@ -627,6 +1089,8 @@ export class CodeKanbanClient {
627
1089
  }) {
628
1090
  const resolvedProjectId = await this.resolveProjectId({
629
1091
  projectId,
1092
+ projectName,
1093
+ projectIndex,
630
1094
  path,
631
1095
  ensureProject: true,
632
1096
  });
@@ -643,12 +1107,14 @@ export class CodeKanbanClient {
643
1107
  },
644
1108
  },
645
1109
  );
646
- return response?.item || null;
1110
+ return extractPayloadItem(response);
647
1111
  }
648
1112
 
649
- async archiveWebSession({ projectId, path, sessionId }) {
1113
+ async archiveWebSession({ projectId, projectName, projectIndex, path, sessionId }) {
650
1114
  const resolvedProjectId = await this.resolveProjectId({
651
1115
  projectId,
1116
+ projectName,
1117
+ projectIndex,
652
1118
  path,
653
1119
  ensureProject: true,
654
1120
  });
@@ -659,12 +1125,14 @@ export class CodeKanbanClient {
659
1125
  method: "POST",
660
1126
  },
661
1127
  );
662
- return response?.item || null;
1128
+ return extractPayloadItem(response);
663
1129
  }
664
1130
 
665
- async unarchiveWebSession({ projectId, path, sessionId }) {
1131
+ async unarchiveWebSession({ projectId, projectName, projectIndex, path, sessionId }) {
666
1132
  const resolvedProjectId = await this.resolveProjectId({
667
1133
  projectId,
1134
+ projectName,
1135
+ projectIndex,
668
1136
  path,
669
1137
  ensureProject: true,
670
1138
  });
@@ -675,12 +1143,14 @@ export class CodeKanbanClient {
675
1143
  method: "POST",
676
1144
  },
677
1145
  );
678
- return response?.item || null;
1146
+ return extractPayloadItem(response);
679
1147
  }
680
1148
 
681
- async renameWebSession({ projectId, path, sessionId, title }) {
1149
+ async renameWebSession({ projectId, projectName, projectIndex, path, sessionId, title }) {
682
1150
  const resolvedProjectId = await this.resolveProjectId({
683
1151
  projectId,
1152
+ projectName,
1153
+ projectIndex,
684
1154
  path,
685
1155
  ensureProject: true,
686
1156
  });
@@ -694,12 +1164,14 @@ export class CodeKanbanClient {
694
1164
  },
695
1165
  },
696
1166
  );
697
- return response?.item || null;
1167
+ return extractPayloadItem(response);
698
1168
  }
699
1169
 
700
- async closeWebSession({ projectId, path, sessionId }) {
1170
+ async closeWebSession({ projectId, projectName, projectIndex, path, sessionId }) {
701
1171
  const resolvedProjectId = await this.resolveProjectId({
702
1172
  projectId,
1173
+ projectName,
1174
+ projectIndex,
703
1175
  path,
704
1176
  ensureProject: true,
705
1177
  });
@@ -711,13 +1183,15 @@ export class CodeKanbanClient {
711
1183
  },
712
1184
  );
713
1185
  return {
714
- message: response?.message || "session aborted",
1186
+ message: extractPayloadMessage(response) || "session aborted",
715
1187
  };
716
1188
  }
717
1189
 
718
- async deleteWebSession({ projectId, path, sessionId }) {
1190
+ async deleteWebSession({ projectId, projectName, projectIndex, path, sessionId }) {
719
1191
  const resolvedProjectId = await this.resolveProjectId({
720
1192
  projectId,
1193
+ projectName,
1194
+ projectIndex,
721
1195
  path,
722
1196
  ensureProject: true,
723
1197
  });
@@ -729,7 +1203,7 @@ export class CodeKanbanClient {
729
1203
  },
730
1204
  );
731
1205
  return {
732
- message: response?.message || "session deleted",
1206
+ message: extractPayloadMessage(response) || "session deleted",
733
1207
  };
734
1208
  }
735
1209
 
@@ -746,13 +1220,15 @@ export class CodeKanbanClient {
746
1220
  },
747
1221
  );
748
1222
  return (
749
- response?.item || { items: [], total: 0, hasMore: false, nextOffset: 0 }
1223
+ extractPayloadItem(response) || { items: [], total: 0, hasMore: false, nextOffset: 0 }
750
1224
  );
751
1225
  }
752
1226
 
753
- async getWebSessionCommandGroup({ projectId, path, sessionId, groupId }) {
1227
+ async getWebSessionCommandGroup({ projectId, projectName, projectIndex, path, sessionId, groupId }) {
754
1228
  const resolvedProjectId = await this.resolveProjectId({
755
1229
  projectId,
1230
+ projectName,
1231
+ projectIndex,
756
1232
  path,
757
1233
  ensureProject: true,
758
1234
  });
@@ -761,18 +1237,20 @@ export class CodeKanbanClient {
761
1237
  const response = await this.requestJson(
762
1238
  `/api/v1/projects/${resolvedProjectId}/web-sessions/${resolvedSessionId}/command-groups/${resolvedGroupId}`,
763
1239
  );
764
- return response?.item || null;
1240
+ return extractPayloadItem(response);
765
1241
  }
766
1242
 
767
1243
  async getWebSessionRuntimeConfig() {
768
1244
  const response = await this.requestJson(
769
1245
  "/api/v1/web-sessions/runtime-config",
770
1246
  );
771
- return response?.item || null;
1247
+ return extractPayloadItem(response);
772
1248
  }
773
1249
 
774
1250
  async uploadWebSessionAttachment({
775
1251
  projectId,
1252
+ projectName,
1253
+ projectIndex,
776
1254
  path,
777
1255
  filePath,
778
1256
  fileName,
@@ -780,6 +1258,8 @@ export class CodeKanbanClient {
780
1258
  }) {
781
1259
  const resolvedProjectId = await this.resolveProjectId({
782
1260
  projectId,
1261
+ projectName,
1262
+ projectIndex,
783
1263
  path,
784
1264
  ensureProject: true,
785
1265
  });
@@ -821,16 +1301,18 @@ export class CodeKanbanClient {
821
1301
  body,
822
1302
  });
823
1303
  }
824
- return normalizeWebSessionAttachment(body?.item);
1304
+ return normalizeWebSessionAttachment(extractPayloadItem(body));
825
1305
  }
826
1306
 
827
1307
  analyzeWebSession(snapshot) {
828
1308
  return analyzeWebSession(snapshot);
829
1309
  }
830
1310
 
831
- async getWebSessionState({ projectId, path, sessionId, limit = 120 }) {
1311
+ async getWebSessionState({ projectId, projectName, projectIndex, path, sessionId, limit = 120 }) {
832
1312
  const snapshot = await this.getWebSessionSnapshot({
833
1313
  projectId,
1314
+ projectName,
1315
+ projectIndex,
834
1316
  path,
835
1317
  sessionId,
836
1318
  limit,
@@ -887,6 +1369,8 @@ export class CodeKanbanClient {
887
1369
 
888
1370
  async answerPendingUserInput({
889
1371
  projectId,
1372
+ projectName,
1373
+ projectIndex,
890
1374
  path,
891
1375
  sessionId,
892
1376
  answers,
@@ -894,6 +1378,8 @@ export class CodeKanbanClient {
894
1378
  }) {
895
1379
  const state = await this.getWebSessionState({
896
1380
  projectId,
1381
+ projectName,
1382
+ projectIndex,
897
1383
  path,
898
1384
  sessionId,
899
1385
  limit,
@@ -917,9 +1403,11 @@ export class CodeKanbanClient {
917
1403
  };
918
1404
  }
919
1405
 
920
- async approvePending({ projectId, path, sessionId, limit = 120 }) {
1406
+ async approvePending({ projectId, projectName, projectIndex, path, sessionId, limit = 120 }) {
921
1407
  const state = await this.getWebSessionState({
922
1408
  projectId,
1409
+ projectName,
1410
+ projectIndex,
923
1411
  path,
924
1412
  sessionId,
925
1413
  limit,
@@ -938,9 +1426,11 @@ export class CodeKanbanClient {
938
1426
  };
939
1427
  }
940
1428
 
941
- async rejectPending({ projectId, path, sessionId, limit = 120 }) {
1429
+ async rejectPending({ projectId, projectName, projectIndex, path, sessionId, limit = 120 }) {
942
1430
  const state = await this.getWebSessionState({
943
1431
  projectId,
1432
+ projectName,
1433
+ projectIndex,
944
1434
  path,
945
1435
  sessionId,
946
1436
  limit,
@@ -961,6 +1451,8 @@ export class CodeKanbanClient {
961
1451
 
962
1452
  async executeLatestPlan({
963
1453
  projectId,
1454
+ projectName,
1455
+ projectIndex,
964
1456
  path,
965
1457
  sessionId,
966
1458
  prompt = "Implement the plan.",
@@ -968,6 +1460,8 @@ export class CodeKanbanClient {
968
1460
  }) {
969
1461
  const state = await this.getWebSessionState({
970
1462
  projectId,
1463
+ projectName,
1464
+ projectIndex,
971
1465
  path,
972
1466
  sessionId,
973
1467
  limit,
@@ -1030,64 +1524,322 @@ export class CodeKanbanClient {
1030
1524
 
1031
1525
  async waitForWebSessionState({
1032
1526
  projectId,
1527
+ projectName,
1528
+ projectIndex,
1033
1529
  path,
1034
1530
  sessionId,
1035
1531
  until,
1036
1532
  intervalMs = 5000,
1037
1533
  timeoutMs = 60000,
1038
1534
  limit = 120,
1535
+ settleMs = 0,
1039
1536
  }) {
1040
- if (!until) {
1041
- throw new CodeKanbanValidationError("until is required");
1042
- }
1043
-
1044
- const matches =
1045
- typeof until === "function"
1046
- ? until
1047
- : Array.isArray(until)
1048
- ? (state) => until.includes(state.phase)
1049
- : (state) => state.phase === until;
1537
+ const matches = normalizeWebSessionStateMatcher(until);
1538
+ const normalizedIntervalMs = Math.max(1, Math.trunc(intervalMs));
1539
+ const normalizedSettleMs = Number.isFinite(settleMs)
1540
+ ? Math.max(0, Math.trunc(settleMs))
1541
+ : 0;
1050
1542
 
1051
1543
  const startedAt = Date.now();
1052
1544
  let lastRetryableError = null;
1545
+ let matchedAt = null;
1053
1546
  while (Date.now() - startedAt <= timeoutMs) {
1054
1547
  try {
1055
1548
  const state = await this.getWebSessionState({
1056
1549
  projectId,
1550
+ projectName,
1551
+ projectIndex,
1057
1552
  path,
1058
1553
  sessionId,
1059
1554
  limit,
1060
1555
  });
1061
1556
  lastRetryableError = null;
1062
1557
  if (matches(state)) {
1063
- return state;
1558
+ if (normalizedSettleMs <= 0) {
1559
+ return state;
1560
+ }
1561
+ if (matchedAt == null) {
1562
+ matchedAt = Date.now();
1563
+ }
1564
+ if (Date.now() - matchedAt >= normalizedSettleMs) {
1565
+ return state;
1566
+ }
1567
+ } else {
1568
+ matchedAt = null;
1064
1569
  }
1065
1570
  } catch (error) {
1066
1571
  if (!isRetryableRequestError(error)) {
1067
1572
  throw error;
1068
1573
  }
1069
1574
  lastRetryableError = error;
1575
+ matchedAt = null;
1070
1576
  }
1071
1577
 
1072
- await sleep(Math.max(1, Math.trunc(intervalMs)));
1578
+ const remainingSettleMs =
1579
+ matchedAt == null || normalizedSettleMs <= 0
1580
+ ? normalizedIntervalMs
1581
+ : Math.max(1, normalizedSettleMs - (Date.now() - matchedAt));
1582
+ await sleep(Math.min(normalizedIntervalMs, remainingSettleMs));
1073
1583
  }
1074
1584
 
1075
1585
  if (lastRetryableError) {
1076
1586
  throw new CodeKanbanValidationError(
1077
1587
  `web session ${sessionId} did not reach the requested state within ${timeoutMs}ms (last transient error: ${lastRetryableError.message})`,
1078
- { cause: lastRetryableError },
1588
+ { cause: lastRetryableError, reason: "timeout" },
1079
1589
  );
1080
1590
  }
1081
1591
 
1082
1592
  throw new CodeKanbanValidationError(
1083
1593
  `web session ${sessionId} did not reach the requested state within ${timeoutMs}ms`,
1594
+ { reason: "timeout" },
1595
+ );
1596
+ }
1597
+
1598
+ async waitForWebSessionPause({
1599
+ projectId,
1600
+ projectName,
1601
+ projectIndex,
1602
+ path,
1603
+ sessionId,
1604
+ until,
1605
+ intervalMs = 5000,
1606
+ timeoutMs = 60000,
1607
+ limit = 120,
1608
+ settleMs = 0,
1609
+ }) {
1610
+ const untilMatcher = until ? normalizeWebSessionStateMatcher(until) : null;
1611
+ const normalizedIntervalMs = Math.max(1, Math.trunc(intervalMs));
1612
+ const normalizedSettleMs = Number.isFinite(settleMs)
1613
+ ? Math.max(0, Math.trunc(settleMs))
1614
+ : 0;
1615
+
1616
+ const startedAt = Date.now();
1617
+ let lastRetryableError = null;
1618
+ let settledReason = null;
1619
+ let settledAt = null;
1620
+ while (Date.now() - startedAt <= timeoutMs) {
1621
+ try {
1622
+ const state = await this.getWebSessionState({
1623
+ projectId,
1624
+ projectName,
1625
+ projectIndex,
1626
+ path,
1627
+ sessionId,
1628
+ limit,
1629
+ });
1630
+ lastRetryableError = null;
1631
+ const reason = getWebSessionPauseReason(state, untilMatcher);
1632
+ if (reason) {
1633
+ if (
1634
+ normalizedSettleMs > 0 &&
1635
+ isDebouncedPauseReason(reason)
1636
+ ) {
1637
+ if (settledReason !== reason) {
1638
+ settledReason = reason;
1639
+ settledAt = Date.now();
1640
+ }
1641
+ if (Date.now() - settledAt >= normalizedSettleMs) {
1642
+ return { reason, state };
1643
+ }
1644
+ } else {
1645
+ return { reason, state };
1646
+ }
1647
+ } else {
1648
+ settledReason = null;
1649
+ settledAt = null;
1650
+ }
1651
+ } catch (error) {
1652
+ if (!isRetryableRequestError(error)) {
1653
+ throw error;
1654
+ }
1655
+ lastRetryableError = error;
1656
+ settledReason = null;
1657
+ settledAt = null;
1658
+ }
1659
+
1660
+ const remainingSettleMs =
1661
+ settledAt == null || normalizedSettleMs <= 0
1662
+ ? normalizedIntervalMs
1663
+ : Math.max(1, normalizedSettleMs - (Date.now() - settledAt));
1664
+ await sleep(Math.min(normalizedIntervalMs, remainingSettleMs));
1665
+ }
1666
+
1667
+ if (lastRetryableError) {
1668
+ throw new CodeKanbanValidationError(
1669
+ `web session ${sessionId} did not reach a pause state within ${timeoutMs}ms (last transient error: ${lastRetryableError.message})`,
1670
+ { cause: lastRetryableError, reason: "timeout" },
1671
+ );
1672
+ }
1673
+
1674
+ throw new CodeKanbanValidationError(
1675
+ `web session ${sessionId} did not reach a pause state within ${timeoutMs}ms`,
1676
+ { reason: "timeout" },
1084
1677
  );
1085
1678
  }
1086
1679
 
1680
+ async runWebSessionUntilDone({
1681
+ projectId,
1682
+ projectName,
1683
+ projectIndex,
1684
+ path,
1685
+ sessionId,
1686
+ until,
1687
+ intervalMs = 2000,
1688
+ timeoutMs = 120000,
1689
+ limit = 120,
1690
+ settleMs = 2000,
1691
+ terminalDebounceMs,
1692
+ answerStrategy = "prefer-second-or-text",
1693
+ autoExecutePlan = true,
1694
+ executePlanPrompt = "Implement the plan.",
1695
+ }) {
1696
+ const normalizedTerminalDebounceMs = Number.isFinite(terminalDebounceMs)
1697
+ ? Math.max(0, Math.trunc(terminalDebounceMs))
1698
+ : Number.isFinite(settleMs)
1699
+ ? Math.max(0, Math.trunc(settleMs))
1700
+ : 2000;
1701
+ const actions = [];
1702
+ const answeredItemIds = new Set();
1703
+ const executedPlanIds = new Set();
1704
+ let lastExecuteMode = null;
1705
+ let lastState = null;
1706
+ const startedAt = Date.now();
1707
+
1708
+ while (Date.now() - startedAt <= timeoutMs) {
1709
+ const remainingTimeoutMs = Math.max(1, timeoutMs - (Date.now() - startedAt));
1710
+ let pause = null;
1711
+ try {
1712
+ pause = await this.waitForWebSessionPause({
1713
+ projectId,
1714
+ projectName,
1715
+ projectIndex,
1716
+ path,
1717
+ sessionId,
1718
+ until,
1719
+ intervalMs,
1720
+ timeoutMs: remainingTimeoutMs,
1721
+ limit,
1722
+ settleMs: normalizedTerminalDebounceMs,
1723
+ });
1724
+ } catch (error) {
1725
+ if (error?.reason !== "timeout") {
1726
+ throw error;
1727
+ }
1728
+ try {
1729
+ lastState = await this.getWebSessionState({
1730
+ projectId,
1731
+ projectName,
1732
+ projectIndex,
1733
+ path,
1734
+ sessionId,
1735
+ limit,
1736
+ });
1737
+ } catch {
1738
+ // Ignore a best-effort state refresh on timeout.
1739
+ }
1740
+ return {
1741
+ stopReason: "timeout",
1742
+ finalState: lastState,
1743
+ actions,
1744
+ lastExecuteMode,
1745
+ };
1746
+ }
1747
+
1748
+ lastState = pause.state;
1749
+ if (pause.reason === "done" || pause.reason === "error" || pause.reason === "until") {
1750
+ return {
1751
+ stopReason: pause.reason,
1752
+ finalState: pause.state,
1753
+ actions,
1754
+ lastExecuteMode,
1755
+ };
1756
+ }
1757
+ if (pause.reason === "approval") {
1758
+ return {
1759
+ stopReason: "needs_approval",
1760
+ finalState: pause.state,
1761
+ actions,
1762
+ lastExecuteMode,
1763
+ };
1764
+ }
1765
+ if (pause.reason === "user_input") {
1766
+ const itemId = pause.state.pendingUserInput?.itemId || null;
1767
+ if (!itemId || answeredItemIds.has(itemId) || !answerStrategy) {
1768
+ return {
1769
+ stopReason: "needs_user_input",
1770
+ finalState: pause.state,
1771
+ actions,
1772
+ lastExecuteMode,
1773
+ };
1774
+ }
1775
+ const answers = buildAutoWebSessionAnswers(
1776
+ pause.state.pendingUserInput?.questions,
1777
+ answerStrategy,
1778
+ );
1779
+ await this.answerPendingUserInput({
1780
+ projectId,
1781
+ projectName,
1782
+ projectIndex,
1783
+ path,
1784
+ sessionId,
1785
+ answers,
1786
+ limit,
1787
+ });
1788
+ answeredItemIds.add(itemId);
1789
+ actions.push({
1790
+ type: "answer_user_input",
1791
+ at: new Date().toISOString(),
1792
+ itemId,
1793
+ answers,
1794
+ });
1795
+ continue;
1796
+ }
1797
+ if (pause.reason === "execute_plan") {
1798
+ const planId = pause.state.latestPlan?.itemId || pause.state.latestPlan?.id || null;
1799
+ if (!autoExecutePlan || (planId && executedPlanIds.has(planId))) {
1800
+ return {
1801
+ stopReason: "needs_execute_plan",
1802
+ finalState: pause.state,
1803
+ actions,
1804
+ lastExecuteMode,
1805
+ };
1806
+ }
1807
+ const result = await this.executeLatestPlan({
1808
+ projectId,
1809
+ projectName,
1810
+ projectIndex,
1811
+ path,
1812
+ sessionId,
1813
+ prompt: executePlanPrompt,
1814
+ limit,
1815
+ });
1816
+ if (planId) {
1817
+ executedPlanIds.add(planId);
1818
+ }
1819
+ lastExecuteMode = result.mode;
1820
+ actions.push({
1821
+ type: "execute_plan",
1822
+ at: new Date().toISOString(),
1823
+ itemId: planId,
1824
+ mode: result.mode,
1825
+ });
1826
+ }
1827
+ }
1828
+
1829
+ return {
1830
+ stopReason: "timeout",
1831
+ finalState: lastState,
1832
+ actions,
1833
+ lastExecuteMode,
1834
+ };
1835
+ }
1836
+
1087
1837
  async startWorkflow(input = {}) {
1088
1838
  const launch = buildAgentLaunchSpec(input);
1089
1839
  const { project, matchedBy } = await this.resolveProject({
1090
1840
  projectId: input.projectId,
1841
+ projectName: input.projectName,
1842
+ projectIndex: input.projectIndex,
1091
1843
  path: input.path,
1092
1844
  ensureProject: true,
1093
1845
  });
@@ -1135,14 +1887,16 @@ export class CodeKanbanClient {
1135
1887
  };
1136
1888
  }
1137
1889
 
1138
- async continueTerminalSession({ projectId, path, sessionId, prompt }) {
1890
+ async continueTerminalSession({ projectId, projectName, projectIndex, path, sessionId, prompt }) {
1139
1891
  const resolvedSessionId = ensureString(sessionId, "sessionId");
1140
1892
  const resolvedPrompt = ensureString(prompt, "prompt");
1141
1893
 
1142
1894
  let project;
1143
- if (projectId || path) {
1895
+ if (projectId || projectName || path) {
1144
1896
  ({ project } = await this.resolveProject({
1145
1897
  projectId,
1898
+ projectName,
1899
+ projectIndex,
1146
1900
  path,
1147
1901
  ensureProject: true,
1148
1902
  }));