codekanban 0.25.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 CHANGED
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "codekanban",
3
- "version": "0.25.0",
3
+ "version": "0.27.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.25.0",
10
- "@codekanban/darwin-x64": "0.25.0",
11
- "@codekanban/darwin-arm64": "0.25.0",
12
- "@codekanban/linux-x64": "0.25.0",
13
- "@codekanban/linux-arm64": "0.25.0"
9
+ "@codekanban/win32-x64": "0.27.0",
10
+ "@codekanban/darwin-x64": "0.27.0",
11
+ "@codekanban/darwin-arm64": "0.27.0",
12
+ "@codekanban/linux-x64": "0.27.0",
13
+ "@codekanban/linux-arm64": "0.27.0"
14
14
  },
15
15
  "keywords": [
16
16
  "ai",
@@ -11,6 +11,12 @@ Node SDK and CLI for CodeKanban project workflows.
11
11
  - Support `standard`, `plan`, and `yolo` Codex profiles
12
12
  - Read terminal session lists and AI session summaries
13
13
  - Read AI conversations and continue existing terminal sessions
14
+ - Control CodeKanban `web session` runs over HTTP and WebSocket
15
+ - Read `web session` snapshots, history, runtime config, command groups, and archived sessions
16
+ - Send `web session` messages, approvals, user-input answers, model/workflow/permission updates, and move operations
17
+ - Receive normalized `web session` events through `on/off`, `waitFor`, or `for await`
18
+ - Poll a `web session` for a derived actionable state instead of reconstructing history logic yourself
19
+ - Execute the latest plan or answer the active structured prompt with one SDK call
14
20
 
15
21
  ## CLI
16
22
 
@@ -21,6 +27,19 @@ node packages/node-sdk/bin/codekanban-sdk.js session conversation --base-url htt
21
27
  node packages/node-sdk/bin/codekanban-sdk.js terminal continue --base-url http://127.0.0.1:3000 --session-id <terminal-session-id> --prompt "Continue from the previous plan"
22
28
  ```
23
29
 
30
+ ### Web Session CLI
31
+
32
+ ```bash
33
+ node packages/node-sdk/bin/codekanban-sdk.js web-session create --base-url http://127.0.0.1:3000 --path D:\repo --agent codex --workflow-mode plan --permission-level elevated --title "Planning session"
34
+ node packages/node-sdk/bin/codekanban-sdk.js web-session snapshot --base-url http://127.0.0.1:3000 --project-id <project-id> --session-id <session-id> --limit 80
35
+ node packages/node-sdk/bin/codekanban-sdk.js web-session send --base-url http://127.0.0.1:3000 --session-id <session-id> --text "Continue from the last plan"
36
+ node packages/node-sdk/bin/codekanban-sdk.js web-session approve --base-url http://127.0.0.1:3000 --session-id <session-id>
37
+ node packages/node-sdk/bin/codekanban-sdk.js web-session user-input --base-url http://127.0.0.1:3000 --session-id <session-id> --item-id <item-id> --answers-json '{"scope":["full repo"]}'
38
+ node packages/node-sdk/bin/codekanban-sdk.js web-session watch --base-url http://127.0.0.1:3000 --session-id <session-id> --max-events 20
39
+ ```
40
+
41
+ `web-session watch` writes NDJSON. Add `--raw` to print raw short-key protocol frames.
42
+
24
43
  ## Library
25
44
 
26
45
  ```js
@@ -37,3 +56,117 @@ const result = await client.startWorkflow({
37
56
  },
38
57
  });
39
58
  ```
59
+
60
+ ### Web Session HTTP
61
+
62
+ ```js
63
+ const client = new CodeKanbanClient({ baseURL: 'http://127.0.0.1:3000' });
64
+
65
+ const session = await client.createWebSession({
66
+ path: 'D:/repo',
67
+ agent: 'codex',
68
+ workflowMode: 'plan',
69
+ permissionLevel: 'elevated',
70
+ title: 'Planning session',
71
+ });
72
+
73
+ const snapshot = await client.getWebSessionSnapshot({
74
+ projectId: session.projectId,
75
+ sessionId: session.id,
76
+ });
77
+ ```
78
+
79
+ ### Polling-Friendly Web Session State
80
+
81
+ ```js
82
+ const state = await client.getWebSessionState({
83
+ projectId: session.projectId,
84
+ sessionId: session.id,
85
+ });
86
+
87
+ if (state.phase === 'running') {
88
+ return;
89
+ }
90
+
91
+ if (state.nextAction?.type === 'answer_user_input') {
92
+ await client.answerPendingUserInput({
93
+ projectId: session.projectId,
94
+ sessionId: session.id,
95
+ answers: {
96
+ scope: ['full repo'],
97
+ },
98
+ });
99
+ }
100
+
101
+ if (state.nextAction?.type === 'execute_plan') {
102
+ await client.executeLatestPlan({
103
+ projectId: session.projectId,
104
+ sessionId: session.id,
105
+ });
106
+ }
107
+ ```
108
+
109
+ `getWebSessionState()` derives:
110
+
111
+ - `phase`
112
+ - `canSend`
113
+ - `needsAction`
114
+ - `nextAction`
115
+ - `pendingApproval`
116
+ - `pendingUserInput`
117
+ - `latestPlan`
118
+ - `lastAssistantMessage`
119
+ - `snapshot`
120
+
121
+ ### Waiting With Polling
122
+
123
+ ```js
124
+ const doneState = await client.waitForWebSessionState({
125
+ projectId: session.projectId,
126
+ sessionId: session.id,
127
+ until: 'done',
128
+ intervalMs: 5000,
129
+ timeoutMs: 120000,
130
+ });
131
+
132
+ console.log(doneState.lastAssistantMessage?.text);
133
+ ```
134
+
135
+ ### Web Session Command Channel
136
+
137
+ ```js
138
+ const channel = client.openWebSessionCommandChannel();
139
+ await channel.waitForOpen();
140
+
141
+ await channel.sendMessage(session.id, {
142
+ text: 'Continue from the latest review.',
143
+ });
144
+
145
+ await channel.updateWorkflowMode(session.id, {
146
+ workflowMode: 'plan',
147
+ });
148
+
149
+ channel.close();
150
+ ```
151
+
152
+ ### Web Session Event Stream
153
+
154
+ ```js
155
+ const events = client.openWebSessionEventStream({ sessionId: session.id });
156
+ await events.waitForOpen();
157
+
158
+ events.on('snapshot', event => {
159
+ console.log('snapshot', event.snapshot.session.title);
160
+ });
161
+
162
+ const nextApproval = events.waitFor(event => event.type === 'historyItem' && event.item.detail?.type === 'approval_request');
163
+
164
+ for await (const event of events) {
165
+ if (event.type === 'historyItem') {
166
+ console.log(event.item.itemType, event.item.text);
167
+ }
168
+ }
169
+
170
+ await nextApproval;
171
+ events.close();
172
+ ```
@@ -10,14 +10,39 @@ function readFlagValue(argv, index, flag) {
10
10
  return value;
11
11
  }
12
12
 
13
+ function parseIntegerFlag(value, fieldName) {
14
+ if (value == null || value === '') {
15
+ return undefined;
16
+ }
17
+ const parsed = Number(value);
18
+ if (!Number.isFinite(parsed)) {
19
+ throw new Error(`${fieldName} must be a number`);
20
+ }
21
+ return Math.trunc(parsed);
22
+ }
23
+
24
+ function parseJsonFlag(value, fieldName) {
25
+ if (!value) {
26
+ throw new Error(`${fieldName} is required`);
27
+ }
28
+ try {
29
+ return JSON.parse(value);
30
+ } catch (error) {
31
+ throw new Error(`${fieldName} must be valid JSON`);
32
+ }
33
+ }
34
+
13
35
  function parseCliArgs(argv) {
14
36
  const positionals = [];
15
37
  const flags = {
16
38
  addDirs: [],
39
+ attachmentIds: [],
17
40
  extraArgs: [],
18
41
  includeTerminal: true,
19
42
  includeAI: true,
20
43
  refresh: false,
44
+ clearExisting: false,
45
+ raw: false,
21
46
  };
22
47
 
23
48
  for (let index = 0; index < argv.length; index += 1) {
@@ -44,10 +69,18 @@ function parseCliArgs(argv) {
44
69
  flags.prompt = readFlagValue(argv, index, token);
45
70
  index += 1;
46
71
  break;
72
+ case '--text':
73
+ flags.text = readFlagValue(argv, index, token);
74
+ index += 1;
75
+ break;
47
76
  case '--agent':
48
77
  flags.agent = readFlagValue(argv, index, token);
49
78
  index += 1;
50
79
  break;
80
+ case '--model':
81
+ flags.model = readFlagValue(argv, index, token);
82
+ index += 1;
83
+ break;
51
84
  case '--profile':
52
85
  flags.profile = readFlagValue(argv, index, token);
53
86
  index += 1;
@@ -64,6 +97,10 @@ function parseCliArgs(argv) {
64
97
  flags.addDirs.push(readFlagValue(argv, index, token));
65
98
  index += 1;
66
99
  break;
100
+ case '--attachment-id':
101
+ flags.attachmentIds.push(readFlagValue(argv, index, token));
102
+ index += 1;
103
+ break;
67
104
  case '--extra-arg':
68
105
  flags.extraArgs.push(readFlagValue(argv, index, token));
69
106
  index += 1;
@@ -96,9 +133,75 @@ function parseCliArgs(argv) {
96
133
  flags.toolUseId = readFlagValue(argv, index, token);
97
134
  index += 1;
98
135
  break;
136
+ case '--reasoning-effort':
137
+ flags.reasoningEffort = readFlagValue(argv, index, token);
138
+ index += 1;
139
+ break;
140
+ case '--workflow-mode':
141
+ flags.workflowMode = readFlagValue(argv, index, token);
142
+ index += 1;
143
+ break;
144
+ case '--permission-level':
145
+ flags.permissionLevel = readFlagValue(argv, index, token);
146
+ index += 1;
147
+ break;
148
+ case '--permission-mode':
149
+ flags.permissionMode = readFlagValue(argv, index, token);
150
+ index += 1;
151
+ break;
152
+ case '--limit':
153
+ flags.limit = readFlagValue(argv, index, token);
154
+ index += 1;
155
+ break;
156
+ case '--before-cursor':
157
+ flags.beforeCursor = readFlagValue(argv, index, token);
158
+ index += 1;
159
+ break;
160
+ case '--mode':
161
+ flags.mode = readFlagValue(argv, index, token);
162
+ index += 1;
163
+ break;
164
+ case '--group-id':
165
+ flags.groupId = readFlagValue(argv, index, token);
166
+ index += 1;
167
+ break;
168
+ case '--file':
169
+ flags.file = readFlagValue(argv, index, token);
170
+ index += 1;
171
+ break;
172
+ case '--item-id':
173
+ flags.itemId = readFlagValue(argv, index, token);
174
+ index += 1;
175
+ break;
176
+ case '--answers-json':
177
+ flags.answersJson = readFlagValue(argv, index, token);
178
+ index += 1;
179
+ break;
180
+ case '--prev-session-id':
181
+ flags.prevSessionId = readFlagValue(argv, index, token);
182
+ index += 1;
183
+ break;
184
+ case '--next-session-id':
185
+ flags.nextSessionId = readFlagValue(argv, index, token);
186
+ index += 1;
187
+ break;
188
+ case '--idle-timeout-ms':
189
+ flags.idleTimeoutMs = readFlagValue(argv, index, token);
190
+ index += 1;
191
+ break;
192
+ case '--max-events':
193
+ flags.maxEvents = readFlagValue(argv, index, token);
194
+ index += 1;
195
+ break;
99
196
  case '--refresh':
100
197
  flags.refresh = true;
101
198
  break;
199
+ case '--clear-existing':
200
+ flags.clearExisting = true;
201
+ break;
202
+ case '--raw':
203
+ flags.raw = true;
204
+ break;
102
205
  case '--no-terminal':
103
206
  flags.includeTerminal = false;
104
207
  break;
@@ -136,6 +239,9 @@ function sanitizeConnection(value) {
136
239
  }
137
240
  const next = { ...value };
138
241
  delete next.connection;
242
+ delete next.commandChannel;
243
+ delete next.eventStream;
244
+ delete next.socket;
139
245
  return next;
140
246
  }
141
247
 
@@ -143,12 +249,88 @@ function printJson(stream, payload) {
143
249
  stream.write(createJsonOutput(payload));
144
250
  }
145
251
 
146
- export async function runCli(argv) {
252
+ function writeJsonLine(stream, payload) {
253
+ stream.write(`${JSON.stringify(payload)}\n`);
254
+ }
255
+
256
+ async function withWebSessionCommandChannel(client, handler) {
257
+ const channel = client.openWebSessionCommandChannel();
258
+ try {
259
+ await channel.waitForOpen();
260
+ return await handler(channel);
261
+ } finally {
262
+ channel.close();
263
+ }
264
+ }
265
+
266
+ async function watchWebSession(client, flags, stdout) {
267
+ const eventStream = client.openWebSessionEventStream({
268
+ sessionId: flags.sessionId,
269
+ });
270
+ const maxEvents = parseIntegerFlag(flags.maxEvents, 'maxEvents');
271
+ const idleTimeoutMs = parseIntegerFlag(flags.idleTimeoutMs, 'idleTimeoutMs');
272
+ let eventCount = 0;
273
+ let idleTimer = null;
274
+ let closedByPolicy = false;
275
+
276
+ const armIdleTimer = () => {
277
+ if (idleTimer) {
278
+ clearTimeout(idleTimer);
279
+ idleTimer = null;
280
+ }
281
+ if (!Number.isFinite(idleTimeoutMs) || idleTimeoutMs == null || idleTimeoutMs <= 0) {
282
+ return;
283
+ }
284
+ idleTimer = setTimeout(() => {
285
+ closedByPolicy = true;
286
+ eventStream.close();
287
+ }, idleTimeoutMs);
288
+ };
289
+
290
+ try {
291
+ await eventStream.waitForOpen();
292
+ armIdleTimer();
293
+
294
+ for await (const event of eventStream) {
295
+ if (event.type === 'open' || event.type === 'close') {
296
+ continue;
297
+ }
298
+
299
+ if (event.type === 'error' && event.errorType !== 'frame') {
300
+ throw event.error instanceof Error ? event.error : new Error(event.message || 'event stream failed');
301
+ }
302
+
303
+ const payload = flags.raw && event.raw ? event.raw : event;
304
+ writeJsonLine(stdout, payload);
305
+ eventCount += 1;
306
+ armIdleTimer();
307
+
308
+ if (maxEvents != null && maxEvents > 0 && eventCount >= maxEvents) {
309
+ closedByPolicy = true;
310
+ eventStream.close();
311
+ }
312
+ }
313
+
314
+ return 0;
315
+ } finally {
316
+ if (idleTimer) {
317
+ clearTimeout(idleTimer);
318
+ }
319
+ if (!closedByPolicy) {
320
+ eventStream.close();
321
+ }
322
+ }
323
+ }
324
+
325
+ export async function runCli(argv, options = {}) {
326
+ const stdout = options.stdout || process.stdout;
327
+ const stderr = options.stderr || process.stderr;
328
+
147
329
  try {
148
330
  const { positionals, flags } = parseCliArgs(argv);
149
331
  const [scope, action] = positionals;
150
332
  if (!scope || !action) {
151
- throw new Error('usage: <workflow|session|terminal> <action> --base-url <url> [...]');
333
+ throw new Error('usage: <workflow|session|terminal|web-session> <action> --base-url <url> [...]');
152
334
  }
153
335
 
154
336
  if (scope === 'workflow' && action === 'command') {
@@ -159,7 +341,7 @@ export async function runCli(argv) {
159
341
  extraArgs: flags.extraArgs,
160
342
  prompt: flags.prompt || 'Inspect the project and respond.',
161
343
  });
162
- printJson(process.stdout, sanitizeConnection(result));
344
+ printJson(stdout, sanitizeConnection(result));
163
345
  return 0;
164
346
  }
165
347
 
@@ -210,11 +392,155 @@ export async function runCli(argv) {
210
392
  sessionId: flags.sessionId,
211
393
  prompt: flags.prompt,
212
394
  });
395
+ } else if (scope === 'web-session' && action === 'list') {
396
+ result = await client.listWebSessions({
397
+ projectId: flags.projectId,
398
+ path: flags.path,
399
+ });
400
+ } else if (scope === 'web-session' && action === 'create') {
401
+ result = await client.createWebSession({
402
+ projectId: flags.projectId,
403
+ path: flags.path,
404
+ worktreeId: flags.worktreeId,
405
+ agent: flags.agent,
406
+ model: flags.model,
407
+ reasoningEffort: flags.reasoningEffort,
408
+ workflowMode: flags.workflowMode,
409
+ permissionLevel: flags.permissionLevel,
410
+ permissionMode: flags.permissionMode,
411
+ title: flags.title,
412
+ });
413
+ } else if (scope === 'web-session' && action === 'connect') {
414
+ result = await withWebSessionCommandChannel(client, channel => channel.connect(flags.sessionId));
415
+ } else if (scope === 'web-session' && action === 'snapshot') {
416
+ result = await client.getWebSessionSnapshot({
417
+ projectId: flags.projectId,
418
+ path: flags.path,
419
+ sessionId: flags.sessionId,
420
+ limit: parseIntegerFlag(flags.limit, 'limit'),
421
+ });
422
+ } else if (scope === 'web-session' && action === 'history') {
423
+ result = await client.getWebSessionHistory({
424
+ projectId: flags.projectId,
425
+ path: flags.path,
426
+ sessionId: flags.sessionId,
427
+ beforeCursor: flags.beforeCursor,
428
+ limit: parseIntegerFlag(flags.limit, 'limit'),
429
+ });
430
+ } else if (scope === 'web-session' && action === 'sync') {
431
+ result = await client.syncWebSession({
432
+ projectId: flags.projectId,
433
+ path: flags.path,
434
+ sessionId: flags.sessionId,
435
+ mode: flags.mode,
436
+ clearExisting: flags.clearExisting,
437
+ });
438
+ } else if (scope === 'web-session' && action === 'archive') {
439
+ result = await client.archiveWebSession({
440
+ projectId: flags.projectId,
441
+ path: flags.path,
442
+ sessionId: flags.sessionId,
443
+ });
444
+ } else if (scope === 'web-session' && action === 'unarchive') {
445
+ result = await client.unarchiveWebSession({
446
+ projectId: flags.projectId,
447
+ path: flags.path,
448
+ sessionId: flags.sessionId,
449
+ });
450
+ } else if (scope === 'web-session' && action === 'rename') {
451
+ result = await client.renameWebSession({
452
+ projectId: flags.projectId,
453
+ path: flags.path,
454
+ sessionId: flags.sessionId,
455
+ title: flags.title,
456
+ });
457
+ } else if (scope === 'web-session' && action === 'close') {
458
+ result = await client.closeWebSession({
459
+ projectId: flags.projectId,
460
+ path: flags.path,
461
+ sessionId: flags.sessionId,
462
+ });
463
+ } else if (scope === 'web-session' && action === 'delete') {
464
+ result = await client.deleteWebSession({
465
+ projectId: flags.projectId,
466
+ path: flags.path,
467
+ sessionId: flags.sessionId,
468
+ });
469
+ } else if (scope === 'web-session' && action === 'runtime-config') {
470
+ result = await client.getWebSessionRuntimeConfig();
471
+ } else if (scope === 'web-session' && action === 'command-group') {
472
+ result = await client.getWebSessionCommandGroup({
473
+ projectId: flags.projectId,
474
+ path: flags.path,
475
+ sessionId: flags.sessionId,
476
+ groupId: flags.groupId,
477
+ });
478
+ } else if (scope === 'web-session' && action === 'attach') {
479
+ result = await client.uploadWebSessionAttachment({
480
+ projectId: flags.projectId,
481
+ path: flags.path,
482
+ filePath: flags.file,
483
+ });
484
+ } else if (scope === 'web-session' && action === 'send') {
485
+ result = await withWebSessionCommandChannel(client, channel =>
486
+ channel.sendMessage(flags.sessionId, {
487
+ text: flags.text || flags.prompt,
488
+ attachmentIds: flags.attachmentIds,
489
+ }),
490
+ );
491
+ } else if (scope === 'web-session' && action === 'approve') {
492
+ result = await withWebSessionCommandChannel(client, channel => channel.approve(flags.sessionId));
493
+ } else if (scope === 'web-session' && action === 'reject') {
494
+ result = await withWebSessionCommandChannel(client, channel => channel.reject(flags.sessionId));
495
+ } else if (scope === 'web-session' && action === 'user-input') {
496
+ result = await withWebSessionCommandChannel(client, channel =>
497
+ channel.answerUserInput(flags.sessionId, {
498
+ itemId: flags.itemId,
499
+ answers: parseJsonFlag(flags.answersJson, 'answersJson'),
500
+ }),
501
+ );
502
+ } else if (scope === 'web-session' && action === 'set-model') {
503
+ result = await withWebSessionCommandChannel(client, channel =>
504
+ channel.updateModel(flags.sessionId, { model: flags.model }),
505
+ );
506
+ } else if (scope === 'web-session' && action === 'set-reasoning') {
507
+ result = await withWebSessionCommandChannel(client, channel =>
508
+ channel.updateReasoningEffort(flags.sessionId, {
509
+ reasoningEffort: flags.reasoningEffort,
510
+ }),
511
+ );
512
+ } else if (scope === 'web-session' && action === 'set-workflow') {
513
+ result = await withWebSessionCommandChannel(client, channel =>
514
+ channel.updateWorkflowMode(flags.sessionId, {
515
+ workflowMode: flags.workflowMode,
516
+ }),
517
+ );
518
+ } else if (scope === 'web-session' && action === 'set-permission') {
519
+ result = await withWebSessionCommandChannel(client, channel =>
520
+ channel.updatePermissionLevel(flags.sessionId, {
521
+ permissionLevel: flags.permissionLevel,
522
+ }),
523
+ );
524
+ } else if (scope === 'web-session' && action === 'set-agent') {
525
+ result = await withWebSessionCommandChannel(client, channel =>
526
+ channel.updateAgent(flags.sessionId, {
527
+ agent: flags.agent,
528
+ }),
529
+ );
530
+ } else if (scope === 'web-session' && action === 'move') {
531
+ result = await withWebSessionCommandChannel(client, channel =>
532
+ channel.move(flags.sessionId, {
533
+ prevSessionId: flags.prevSessionId,
534
+ nextSessionId: flags.nextSessionId,
535
+ }),
536
+ );
537
+ } else if (scope === 'web-session' && action === 'watch') {
538
+ return await watchWebSession(client, flags, stdout);
213
539
  } else {
214
540
  throw new Error(`unsupported command: ${scope} ${action}`);
215
541
  }
216
542
 
217
- printJson(process.stdout, sanitizeConnection(result));
543
+ printJson(stdout, sanitizeConnection(result));
218
544
  return 0;
219
545
  } catch (error) {
220
546
  const payload = {
@@ -223,7 +549,7 @@ export async function runCli(argv) {
223
549
  message: error instanceof Error ? error.message : String(error),
224
550
  },
225
551
  };
226
- printJson(process.stderr, payload);
552
+ printJson(stderr, payload);
227
553
  return 1;
228
554
  }
229
555
  }