claude-code-kanban 7.5.0 → 7.6.1

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.
Files changed (40) hide show
  1. package/hub-app.json +5 -0
  2. package/lib/panes.js +16 -2
  3. package/lib/parsers.js +27 -2
  4. package/lib/retention.js +29 -6
  5. package/lib/session-events.js +154 -42
  6. package/lib/show.js +385 -0
  7. package/lib/terminal-client.js +14 -1
  8. package/lib/terminal-host.js +2 -1
  9. package/lib/terminal.js +27 -7
  10. package/package.json +1 -1
  11. package/plugin/plugins/claude-code-kanban/.claude-plugin/plugin.json +1 -1
  12. package/plugin/plugins/claude-code-kanban/hooks/doorbell.ts +66 -10
  13. package/plugin/plugins/claude-code-kanban/hooks/index.ts +2 -0
  14. package/plugin/plugins/claude-code-kanban/hooks/show.ts +120 -0
  15. package/plugin/plugins/claude-code-kanban/skills/dispatch/SKILL.md +2 -0
  16. package/plugin/plugins/claude-code-kanban/skills/kanban/SKILL.md +2 -0
  17. package/plugin/plugins/claude-code-kanban/skills/show/SKILL.md +45 -0
  18. package/plugin/plugins/claude-code-kanban/skills/show/references/NOTICE.md +27 -0
  19. package/plugin/plugins/claude-code-kanban/skills/show/references/chart.md +58 -0
  20. package/plugin/plugins/claude-code-kanban/skills/show/references/diagram/architecture.md +18 -0
  21. package/plugin/plugins/claude-code-kanban/skills/show/references/diagram/er.md +10 -0
  22. package/plugin/plugins/claude-code-kanban/skills/show/references/diagram/flow.md +13 -0
  23. package/plugin/plugins/claude-code-kanban/skills/show/references/diagram/layers.md +10 -0
  24. package/plugin/plugins/claude-code-kanban/skills/show/references/diagram/sequence.md +19 -0
  25. package/plugin/plugins/claude-code-kanban/skills/show/references/diagram/state.md +12 -0
  26. package/plugin/plugins/claude-code-kanban/skills/show/references/diagram/swimlane.md +10 -0
  27. package/plugin/plugins/claude-code-kanban/skills/show/references/diagram/timeline.md +9 -0
  28. package/plugin/plugins/claude-code-kanban/skills/show/references/diagram/tree.md +10 -0
  29. package/plugin/plugins/claude-code-kanban/skills/show/references/diagram.md +87 -0
  30. package/plugin/plugins/claude-code-kanban/skills/show/references/explain.md +98 -0
  31. package/plugin/plugins/claude-code-kanban/skills/show/references/mockup.md +30 -0
  32. package/plugin/plugins/claude-code-kanban/tests/doorbell.test.ts +69 -10
  33. package/plugin/plugins/claude-code-kanban/tests/show.test.ts +137 -0
  34. package/public/app.js +1149 -108
  35. package/public/index.html +7 -6
  36. package/public/style.css +770 -9
  37. package/public/terminal-frame.js +0 -2
  38. package/public/tree-highlight.js +137 -15
  39. package/server.js +71 -24
  40. package/lib/dispatch.js +0 -32
@@ -313,8 +313,6 @@
313
313
  return open(m);
314
314
  case 'detach':
315
315
  return detach();
316
- case 'send':
317
- return send(m.msg);
318
316
  case 'theme':
319
317
  return applyTheme(m.options);
320
318
  case 'font':
@@ -11,7 +11,8 @@ const treeHighlight = (() => {
11
11
  String.raw`^([+-](?=[ \t│├└▼]|$))?([ \t]*(?:[│├└─▼↓→ \t]*[│├└─▼↓→][ \t]*)?)(${NAME})?(.*)$`,
12
12
  'su',
13
13
  );
14
- const INDENT_NODE_LINE = new RegExp(String.raw`^([ \t]*)${NAME}(\([^()]*\))?( {2,}\S.*)?$`, 'u');
14
+ const FRAME = new RegExp(String.raw`^( *)(~.*?|${NAME}(?:\([^()]*\))?)( \*)?(?:( {2,})(\S(?:.*\S)?))?( *)$`, 'u');
15
+ const LOC = /^[^\s:]+:\d+(?::\d+)?$/;
15
16
 
16
17
  function escapeText(s) {
17
18
  return s
@@ -36,25 +37,61 @@ const treeHighlight = (() => {
36
37
  return false;
37
38
  }
38
39
 
39
- // A call tree drawn with indentation only: every line is one name or call, with an optional
40
- // note after two spaces. Prose and pseudocode have single-spaced words, so they never match.
41
- function looksLikeIndentTree(text, diff) {
42
- const lines = text.split('\n').filter((l) => l.trim());
43
- if (lines.length < 3) return false;
44
- const indents = new Set();
45
- for (const raw of lines) {
46
- if (diff && !/^[ +-]/.test(raw)) return false;
47
- const m = (diff ? raw.slice(1) : raw).match(INDENT_NODE_LINE);
48
- if (!m) return false;
49
- indents.add(m[1].length);
40
+ // The notation of skills/show/references/explain.md: frames indented by spaces, one name or
41
+ // call each, an optional ` *` focus mark, a note after 2+ spaces, `~` folds, and `+`/`-` in
42
+ // column 1 for a change. Prose and pseudocode have single-spaced words, so they never parse.
43
+ function parseStack(text) {
44
+ const lines = text.split('\n');
45
+ const diff = lines.some((l) => /^[+-]/.test(l));
46
+ const rows = [];
47
+ let roots = [];
48
+ let stack = [];
49
+ for (const line of lines) {
50
+ if (!line.trim()) {
51
+ rows.push({ blank: true, text: line });
52
+ roots = [];
53
+ stack = [];
54
+ continue;
55
+ }
56
+ const marker = diff ? line[0] : '';
57
+ if (diff && !/[ +-]/.test(marker)) return null;
58
+ const m = line.slice(marker.length).match(FRAME);
59
+ if (!m) return null;
60
+ const [, ind, label, star = '', gap = '', note = '', tail] = m;
61
+ while (stack.length && stack[stack.length - 1].indent >= ind.length) stack.pop();
62
+ const parent = stack[stack.length - 1] || null;
63
+ const sibs = parent ? parent.kids : roots;
64
+ const row = {
65
+ id: rows.length,
66
+ marker,
67
+ indent: ind.length,
68
+ parent,
69
+ sibs,
70
+ label,
71
+ fold: label.startsWith('~'),
72
+ star,
73
+ gap,
74
+ note,
75
+ tail,
76
+ kids: [],
77
+ };
78
+ sibs.push(row);
79
+ stack.push(row);
80
+ rows.push(row);
50
81
  }
51
- return indents.size >= 2;
82
+ return rows;
83
+ }
84
+
85
+ // An untagged call tree: 3 frames at least, on 2 indent levels at least.
86
+ function looksLikeIndentTree(text) {
87
+ const frames = parseStack(text)?.filter((r) => !r.blank) || [];
88
+ return frames.length >= 3 && new Set(frames.map((r) => r.indent)).size >= 2;
52
89
  }
53
90
 
54
91
  function wantsTree(lang, text) {
55
92
  const tag = (lang || '').trim().toLowerCase();
56
93
  if (TREE_TAGS.has(tag)) return true;
57
- return PLAIN_TAGS.has(tag) && (looksLikeTree(text) || looksLikeIndentTree(text, tag === 'diff'));
94
+ return PLAIN_TAGS.has(tag) && (looksLikeTree(text) || looksLikeIndentTree(text));
58
95
  }
59
96
 
60
97
  function highlightLine(line) {
@@ -68,7 +105,92 @@ const treeHighlight = (() => {
68
105
  return text.split('\n').map(highlightLine).join('\n');
69
106
  }
70
107
 
71
- return { wantsTree, highlightTree };
108
+ const isLast = (row) => row.sibs[row.sibs.length - 1] === row;
109
+
110
+ function guideCells(row) {
111
+ const g = Array(row.indent).fill('');
112
+ if (row.parent) {
113
+ for (let a = row.parent; a.parent; a = a.parent) if (!isLast(a)) g[a.parent.indent] = 'v';
114
+ g[row.parent.indent] = isLast(row) ? 'l' : 't';
115
+ if (row.parent.indent + 1 < row.indent) g[row.parent.indent + 1] = 'h';
116
+ }
117
+ return g.map((c) => (c ? `<span class="ct-g ct-${c}"> </span>` : ' ')).join('');
118
+ }
119
+
120
+ function renderRow(row) {
121
+ if (row.blank) return `<span class="ct-row">${row.text}</span>`;
122
+ const cls = ['ct-row'];
123
+ if (row.star) cls.push('ct-focus');
124
+ if (row.marker === '+') cls.push('ct-add');
125
+ if (row.marker === '-') cls.push('ct-del');
126
+ const above = [];
127
+ for (let a = row.parent; a; a = a.parent) above.push(a);
128
+ if (above.some((a) => !a.open)) cls.push('ct-hidden');
129
+ if (above.some((a) => a.fold)) cls.push('ct-lib');
130
+ let attrs = above.length ? ` data-ct-in="${above.map((a) => a.id).join(' ')}"` : '';
131
+ let label;
132
+ if (row.fold) {
133
+ cls.push('ct-fold');
134
+ label = `<span class="ct-tilde">~</span>${escapeText(row.label.slice(1))}`;
135
+ } else {
136
+ label = span('ct-name', row.label);
137
+ }
138
+ if (row.kids.length) {
139
+ cls.push('ct-has');
140
+ attrs += ` data-ct="${row.id}" role="button" tabindex="0" aria-expanded="${row.open}"`;
141
+ }
142
+ return (
143
+ `<span class="${cls.join(' ')}"${attrs}>` +
144
+ span('ct-m', row.marker) +
145
+ guideCells(row) +
146
+ label +
147
+ span('ct-star', row.star) +
148
+ row.gap +
149
+ span(LOC.test(row.note) ? 'ct-loc' : 'ct-note', row.note) +
150
+ row.tail +
151
+ '</span>'
152
+ );
153
+ }
154
+
155
+ // Rows are blocks, so the newlines between them sit in hidden spans: the text stays
156
+ // byte-identical and the rows draw no blank lines.
157
+ function renderTree(text) {
158
+ const rows = parseStack(text);
159
+ if (!rows) return `<pre><code class="hljs language-tree">${highlightTree(text)}</code></pre>`;
160
+ for (let i = rows.length - 1; i >= 0; i--) {
161
+ const r = rows[i];
162
+ if (r.parent && (r.star || r.marker === '+' || r.marker === '-' || r.holdsPoint)) r.parent.holdsPoint = true;
163
+ }
164
+ for (const r of rows) r.open = !r.fold || !!r.holdsPoint;
165
+ return `<pre><code class="hljs ct">${rows.map(renderRow).join('<span class="ct-nl">\n</span>')}</code></pre>`;
166
+ }
167
+
168
+ function toggleFold(fold) {
169
+ fold.setAttribute('aria-expanded', String(fold.getAttribute('aria-expanded') !== 'true'));
170
+ const code = fold.closest('code');
171
+ const closed = new Set();
172
+ for (const f of code.querySelectorAll('.ct-has[aria-expanded="false"]')) closed.add(f.dataset.ct);
173
+ for (const r of code.querySelectorAll('[data-ct-in]')) {
174
+ r.classList.toggle(
175
+ 'ct-hidden',
176
+ r.dataset.ctIn.split(' ').some((id) => closed.has(id)),
177
+ );
178
+ }
179
+ }
180
+
181
+ // A note in the LOC form → { path, line }, else null.
182
+ function parseLoc(text) {
183
+ if (!LOC.test(text)) return null;
184
+ const [path, line] = text.split(':');
185
+ return { path, line: Math.max(1, Number(line)) };
186
+ }
187
+
188
+ // A trailing newline does not start a line.
189
+ function lineCount(text) {
190
+ return Math.max(1, text.split('\n').length - (text.endsWith('\n') ? 1 : 0));
191
+ }
192
+
193
+ return { wantsTree, highlightTree, renderTree, toggleFold, parseLoc, lineCount };
72
194
  })();
73
195
 
74
196
  if (typeof module === 'object' && module.exports) module.exports = treeHighlight;
package/server.js CHANGED
@@ -28,6 +28,7 @@ const {
28
28
  modelDisplayName,
29
29
  buildSessionDigest,
30
30
  readCompactSummaries,
31
+ fillCompactSummaries,
31
32
  readArtifactLinks,
32
33
  readScratchpadCreations,
33
34
  extractPromptFromTranscript,
@@ -48,9 +49,9 @@ const { buildDecision, decisionFileName, isDecisionFile, approvalsFrom, boardRef
48
49
  const { getClaudeDir, getArgValue, storageNamespace, isDefaultClaudeDir } = require('./lib/claude-dir');
49
50
  const { readTerminalConfig } = require('./lib/terminal');
50
51
  const { createTerminalClient } = require('./lib/terminal-client');
52
+ const { createShowStore, mountShowRoutes, showBodyParser, SHOW_PATH } = require('./lib/show');
51
53
  const { readLiveSessions, isPidAlive, isSessionLive } = require('./lib/live-sessions');
52
54
  const { createProcStats } = require('./lib/proc-stats');
53
- const { createDispatchRegistry } = require('./lib/dispatch');
54
55
  const { createGroupStore, isGroupName, suggestGroupName } = require('./lib/dispatch-groups');
55
56
  const { createDispatchedStore, scanTranscripts, pruneSessionDirs, pruneContextStatus, pruneTaskMaps, retentionMs } = require('./lib/retention');
56
57
  const { freshRateLimits } = require('./lib/rate-limits');
@@ -660,6 +661,7 @@ app.param('name', (_req, res, next, val) => {
660
661
  next();
661
662
  });
662
663
 
664
+ app.use(SHOW_PATH, showBodyParser());
663
665
  // Parse JSON bodies. A review of 50 comments at the field caps is about 450 kB.
664
666
  app.use(express.json({ limit: '1mb' }));
665
667
  // #endregion
@@ -3045,7 +3047,10 @@ async function sendSessionMessages(req, res) {
3045
3047
  hasMore = messages.length > limit;
3046
3048
  if (hasMore) messages = messages.slice(-limit);
3047
3049
  }
3048
- const compactPromise = cachedByMtime(compactSummaryCache, jsonlPath, jsonlPath, () => readCompactSummaries(jsonlPath), []);
3050
+ const compactedMsgs = messages.filter(m => m.systemLabel === 'Compacted');
3051
+ const compactPromise = compactedMsgs.some(m => !m.compactSummary)
3052
+ ? cachedByMtime(compactSummaryCache, jsonlPath, jsonlPath, () => readCompactSummaries(jsonlPath), [])
3053
+ : null;
3049
3054
  const agentMessages = messages.filter(m => m.tool === 'Agent' && m.toolUseId);
3050
3055
  if (agentMessages.length) {
3051
3056
  const progressMap = await getProgressMap(jsonlPath);
@@ -3099,16 +3104,7 @@ async function sendSessionMessages(req, res) {
3099
3104
  }
3100
3105
  }
3101
3106
  }
3102
- const compactSummaries = await compactPromise;
3103
- // Match compaction messages to summaries by chronological order
3104
- const compactedMsgs = messages
3105
- .filter(m => m.systemLabel === 'Compacted')
3106
- .sort((a, b) => (a.timestamp || '').localeCompare(b.timestamp || ''));
3107
- for (let i = 0; i < compactedMsgs.length; i++) {
3108
- if (i < compactSummaries.length) {
3109
- compactedMsgs[i].compactSummary = compactSummaries[i].summary;
3110
- }
3111
- }
3107
+ if (compactPromise) fillCompactSummaries(compactedMsgs, await compactPromise);
3112
3108
  // The client keeps needing toolUseId when it builds a follow-up URL from it:
3113
3109
  // lazy-fetching a truncated tool result, or fetching each tool-result image.
3114
3110
  const clientNeedsToolUseId = (msg) => msg.toolResultTruncated || msg.toolResultImageCount;
@@ -3321,6 +3317,16 @@ function resolveSessionFolder(id) {
3321
3317
  return meta ? meta.project || meta.cwd || null : null;
3322
3318
  }
3323
3319
 
3320
+ // The host saves its terminals; cck adds the show map (terminal id → session id) beside them.
3321
+ const terminalsFile = jsonFile(TERMINALS_FILE);
3322
+ let savedTerminals = terminalsFile.load() || {};
3323
+
3324
+ function saveTerminals() {
3325
+ const kept = new Set(Object.values(savedTerminals.terminalIds || {}));
3326
+ const showSessions = show.prune((id) => kept.has(id) || terminal.hasTerminal(id));
3327
+ terminalsFile.save({ ...savedTerminals, showSessions });
3328
+ }
3329
+
3324
3330
  const terminal = createTerminalClient({
3325
3331
  config: readTerminalConfig({ getArgValue }),
3326
3332
  net,
@@ -3328,14 +3334,18 @@ const terminal = createTerminalClient({
3328
3334
  isDefaultDir: isDefaultClaudeDir(CLAUDE_DIR),
3329
3335
  sessionsDir: SESSIONS_DIR,
3330
3336
  token: process.env.CCK_TERMINAL_TOKEN,
3331
- ...jsonFile(TERMINALS_FILE),
3337
+ load: terminalsFile.load,
3338
+ save: (data) => {
3339
+ savedTerminals = data;
3340
+ saveTerminals();
3341
+ },
3332
3342
  // The project, not the last cwd: `claude --resume` finds a session under the
3333
3343
  // project dir it started in, and cwd drifts into subdirectories.
3334
3344
  resolveCwd: resolveSessionFolder,
3335
3345
  isAllowedFolder,
3336
3346
  onChange: () => broadcast({ type: 'terminals-update', ids: terminal.ids() }),
3337
3347
  onExit: (id) => {
3338
- if (dispatches.remove(id)) broadcast({ type: 'dispatch-update' });
3348
+ if (dispatched.get(id)) broadcast({ type: 'dispatch-update' });
3339
3349
  },
3340
3350
  });
3341
3351
 
@@ -3396,6 +3406,25 @@ app.delete('/api/terminals/:id', terminalRoute(async (req, res) => {
3396
3406
  res.status(204).end();
3397
3407
  }));
3398
3408
 
3409
+ const show = createShowStore({
3410
+ load: () => savedTerminals.showSessions,
3411
+ onChange: saveTerminals,
3412
+ onPosted: (e) => broadcast({ type: 'show:posted', ...e }),
3413
+ startedSession: terminal.sessionOfTerminal,
3414
+ resolveDir: (id) => {
3415
+ const meta = sessionMetaFor(id);
3416
+ return meta ? getScratchpadDir(id, meta) : null;
3417
+ },
3418
+ });
3419
+ mountShowRoutes(app, {
3420
+ store: show,
3421
+ authorized: (token) => terminal.authorized(token),
3422
+ hasTerminal: (id) => terminal.hasTerminal(id),
3423
+ readPreviewFile,
3424
+ broadcast,
3425
+ onRemoved: (sessionId, paths) => panes.removeShow(sessionId, paths),
3426
+ });
3427
+
3399
3428
  // Served from node_modules, never a CDN: any script on this page can use the token.
3400
3429
  const XTERM_FILES = {
3401
3430
  'xterm.js': '@xterm/xterm/lib/xterm.js',
@@ -3417,8 +3446,6 @@ app.get('/vendor/xterm/:file', (req, res) => {
3417
3446
  // #region DISPATCH
3418
3447
  const dispatched = createDispatchedStore(jsonFile(DISPATCHED_FILE));
3419
3448
 
3420
- const dispatches = createDispatchRegistry();
3421
-
3422
3449
  const dispatchGroups = createGroupStore({
3423
3450
  ...jsonFile(DISPATCH_GROUPS_FILE),
3424
3451
  isAlive: (id) => terminal.isRunning(id) || isSessionLive(loadLiveSessions(), id),
@@ -3454,8 +3481,7 @@ app.post('/api/dispatch', terminalRoute(async (req, res) => {
3454
3481
  }
3455
3482
  const started = await terminal.startNew({ cwd, name, model, worktree, taskList, prompt: spec.trim(), extraArgs: claudeArgs });
3456
3483
  if (started.error) return res.status(started.status).json({ error: started.error });
3457
- dispatches.add({ session: started.id, parent, cwd: started.cwd, name, group: group || null, worktree });
3458
- dispatched.record(started.id, parent);
3484
+ dispatched.record(started.id, { parent, cwd: started.cwd, name, group, worktree });
3459
3485
  // The starter stays where it is: moving it would jump it under the user.
3460
3486
  if (group) dispatchGroups.join(group, [started.id]);
3461
3487
  broadcast({ type: 'dispatch-update' });
@@ -3465,7 +3491,7 @@ app.post('/api/dispatch', terminalRoute(async (req, res) => {
3465
3491
  app.get('/api/dispatch', (req, res) => {
3466
3492
  res.setHeader('Cache-Control', 'no-store');
3467
3493
  const parent = typeof req.query.parent === 'string' && req.query.parent ? req.query.parent : null;
3468
- res.json({ running: dispatches.list({ parent }) });
3494
+ res.json({ running: dispatched.running(terminal.isRunning, { parent }) });
3469
3495
  });
3470
3496
  // #endregion
3471
3497
 
@@ -3524,7 +3550,8 @@ const {
3524
3550
  formatReviewSubmitted,
3525
3551
  formatTaskMoved,
3526
3552
  handleSessionEvents,
3527
- hasSessionListener,
3553
+ hasDoorbell,
3554
+ configureSessionEvents,
3528
3555
  } = require('./lib/session-events');
3529
3556
  app.get('/api/sessions/:sessionId/events', handleSessionEvents);
3530
3557
 
@@ -4092,11 +4119,11 @@ app.get('/api/panes/:sessionId', (req, res) => {
4092
4119
 
4093
4120
  app.post('/api/panes/:sessionId', async (req, res) => {
4094
4121
  try {
4095
- const { target, base, title, kind } = req.body || {};
4122
+ const { target, base, title, kind, show } = req.body || {};
4096
4123
  if (typeof target !== 'string' || !target) throw previewError(400, 'target is required', 'bad_target');
4097
4124
  // A message target is a session log message id, which the board resolves when it shows the pane.
4098
4125
  const resolved = kind === 'message' ? { kind, target } : await resolvePaneTarget(target, base);
4099
- res.json(panes.add(req.params.sessionId, { ...resolved, title }));
4126
+ res.json(panes.add(req.params.sessionId, { ...resolved, title, show: show === true }));
4100
4127
  } catch (error) {
4101
4128
  paneRouteError(res, error, 'POST /api/panes');
4102
4129
  }
@@ -4159,6 +4186,23 @@ app.delete('/api/panes/:sessionId/:paneId', (req, res) => {
4159
4186
  // carries one line, and the file is what each route points at.
4160
4187
  const REVIEW_DIR = path.join(CCK_DIR, 'reviews');
4161
4188
  const REVIEW_KIND_RE = /^[a-z]{1,20}$/;
4189
+
4190
+ // `<ts>.pending` holds the doorbell line of a review the mod has not acked yet.
4191
+ configureSessionEvents({
4192
+ onDelivered: (entry) => fs.unlink(entry.marker).catch(() => {}),
4193
+ restore: async (sessionId) => {
4194
+ if (!isUUID(sessionId)) return [];
4195
+ const dir = path.join(REVIEW_DIR, sessionId);
4196
+ const names = (await fs.readdir(dir).catch(() => [])).filter((n) => n.endsWith('.pending')).sort();
4197
+ const lines = [];
4198
+ for (const name of names) {
4199
+ const marker = path.join(dir, name);
4200
+ const text = await fs.readFile(marker, 'utf8').catch(() => '');
4201
+ if (text) lines.push({ text, marker });
4202
+ }
4203
+ return lines;
4204
+ },
4205
+ });
4162
4206
  const REVIEW_MAX_COMMENTS = 50;
4163
4207
  const REVIEW_MAX_CHARS = 4000;
4164
4208
 
@@ -4227,8 +4271,11 @@ app.post('/api/sessions/:sessionId/review', async (req, res) => {
4227
4271
  await fs.writeFile(file, markdown);
4228
4272
 
4229
4273
  let delivered = null;
4230
- if (boardEventsEnabled() && hasSessionListener(sessionId)) {
4231
- enqueueSessionEvent(sessionId, formatReviewSubmitted(src.items.length, src.label, file));
4274
+ if (boardEventsEnabled() && hasDoorbell(sessionId)) {
4275
+ const line = formatReviewSubmitted(src.items.length, src.label, file);
4276
+ const marker = file.replace(/\.md$/, '.pending');
4277
+ await fs.writeFile(marker, line);
4278
+ enqueueSessionEvent(sessionId, line, { marker });
4232
4279
  delivered = 'doorbell';
4233
4280
  } else if (
4234
4281
  terminal.authorized(req.get('x-terminal-token')) &&
package/lib/dispatch.js DELETED
@@ -1,32 +0,0 @@
1
- // Dispatch: a Claude Code session one session started in cck's terminal. In memory on
2
- // purpose: an entry lives as long as its PTY, which dies with the server.
3
-
4
- function createDispatchRegistry({ now = Date.now } = {}) {
5
- const entries = new Map();
6
-
7
- function add({ session, parent, cwd, name, group, worktree }) {
8
- entries.set(session, {
9
- session,
10
- parent: parent || null,
11
- cwd,
12
- name: name || null,
13
- group: group || null,
14
- worktree: worktree || null,
15
- startedAt: now(),
16
- });
17
- }
18
-
19
- function list({ parent } = {}) {
20
- const out = [...entries.values()];
21
- return parent ? out.filter((e) => e.parent === parent) : out;
22
- }
23
-
24
- return {
25
- add,
26
- list,
27
- has: (session) => entries.has(session),
28
- remove: (session) => entries.delete(session),
29
- };
30
- }
31
-
32
- module.exports = { createDispatchRegistry };