claude-code-kanban 5.1.1 → 5.2.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/README.md +3 -2
- package/lib/auto-compact.js +42 -0
- package/lib/claude-settings.js +21 -0
- package/lib/dispatch.js +11 -4
- package/lib/retention.js +137 -0
- package/lib/session-events.js +30 -7
- package/lib/terminal.js +23 -3
- package/package.json +1 -1
- package/plugin/plugins/claude-code-kanban/.claude-plugin/plugin.json +1 -1
- package/plugin/plugins/claude-code-kanban/hooks/hooks.json +14 -7
- package/plugin/plugins/claude-code-kanban/monitors.json +3 -3
- package/plugin/plugins/claude-code-kanban/scripts/postman.js +2 -2
- package/plugin/plugins/claude-code-kanban/skills/{kanban-dispatch → dispatch}/SKILL.md +2 -2
- package/plugin/plugins/claude-code-kanban/skills/{kanban-follow → follow}/SKILL.md +17 -9
- package/plugin/plugins/claude-code-kanban/skills/kanban/SKILL.md +1 -1
- package/public/app.js +699 -138
- package/public/fonts/OFL-IBM-Plex.txt +93 -0
- package/public/fonts/OFL-Playfair-Display.txt +93 -0
- package/public/fonts/fonts.css +244 -0
- package/public/fonts/ibm-plex-mono-cyrillic-400.woff2 +0 -0
- package/public/fonts/ibm-plex-mono-cyrillic-500.woff2 +0 -0
- package/public/fonts/ibm-plex-mono-cyrillic-600.woff2 +0 -0
- package/public/fonts/ibm-plex-mono-cyrillic-ext-400.woff2 +0 -0
- package/public/fonts/ibm-plex-mono-cyrillic-ext-500.woff2 +0 -0
- package/public/fonts/ibm-plex-mono-cyrillic-ext-600.woff2 +0 -0
- package/public/fonts/ibm-plex-mono-latin-400.woff2 +0 -0
- package/public/fonts/ibm-plex-mono-latin-500.woff2 +0 -0
- package/public/fonts/ibm-plex-mono-latin-600.woff2 +0 -0
- package/public/fonts/ibm-plex-mono-latin-ext-400.woff2 +0 -0
- package/public/fonts/ibm-plex-mono-latin-ext-500.woff2 +0 -0
- package/public/fonts/ibm-plex-mono-latin-ext-600.woff2 +0 -0
- package/public/fonts/ibm-plex-mono-vietnamese-400.woff2 +0 -0
- package/public/fonts/ibm-plex-mono-vietnamese-500.woff2 +0 -0
- package/public/fonts/ibm-plex-mono-vietnamese-600.woff2 +0 -0
- package/public/fonts/playfair-display-cyrillic-400.woff2 +0 -0
- package/public/fonts/playfair-display-latin-400.woff2 +0 -0
- package/public/fonts/playfair-display-latin-ext-400.woff2 +0 -0
- package/public/fonts/playfair-display-vietnamese-400.woff2 +0 -0
- package/public/index.html +20 -14
- package/public/style.css +277 -44
- package/server.js +152 -15
- package/skill-guides/dispatch.md +1 -1
package/README.md
CHANGED
|
@@ -71,8 +71,9 @@ Run `claude` in any project. You do not configure anything per project. Claude C
|
|
|
71
71
|
- **Answer prompts from the board.** When Claude asks for permission, asks a question or waits for plan approval, the session gets an amber highlight and the ask shows with Allow and Deny buttons or an answer form. The terminal prompt stays open, and the first answer wins. [Answer prompts from the board](https://nikiforovall.blog/claude-code-kanban/guides/waiting-prompts/)
|
|
72
72
|
- **Embedded terminal.** Run a real Claude Code process for any session next to its board (<kbd>Ctrl</kbd>+<kbd>`</kbd>). <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>R</kbd> resumes a past session and <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>S</kbd> swaps to the previous one. The terminal is off by default when the board runs alone. Start it with `--enable-terminal` and open the `#t=<token>` link the server prints. [Embedded terminal](https://nikiforovall.blog/claude-code-kanban/guides/embedded-terminal/)
|
|
73
73
|
- **New session.** <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>N</kbd> opens a dialog to pick a folder, a name, a model, an optional git worktree and a first prompt. Needs the terminal.
|
|
74
|
-
- **Dispatch.** Hand a written task to a new session with `claude-code-kanban dispatch start`, or ask Claude to do it with the `
|
|
75
|
-
- **Steer with card moves.** Run `/claude-code-kanban:
|
|
74
|
+
- **Dispatch.** Hand a written task to a new session with `claude-code-kanban dispatch start`, or ask Claude to do it with the `dispatch` skill. Add `--report` to get the outcome back. Needs the terminal. [Dispatch tasks to other sessions](https://nikiforovall.blog/claude-code-kanban/guides/dispatch/)
|
|
75
|
+
- **Steer with card moves.** Run `/claude-code-kanban:follow` in a session, then drag its cards. Claude starts, parks or stops the task. [Claude Code plugin skills](https://nikiforovall.blog/claude-code-kanban/guides/plugin-skills/)
|
|
76
|
+
- **Review comments.** Select text in a previewed file or the plan, add comments and send them to the session in one step. [Review comments](https://nikiforovall.blog/claude-code-kanban/guides/review-comments/)
|
|
76
77
|
|
|
77
78
|
<picture>
|
|
78
79
|
<source media="(prefers-color-scheme: dark)" srcset="website/public/shots/themes/ember-11-waiting-prompt-dark.webp">
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const path = require('node:path');
|
|
2
|
+
const { readSettings } = require('./claude-settings');
|
|
3
|
+
|
|
4
|
+
const MIN_WINDOW = 100000;
|
|
5
|
+
const MAX_WINDOW = 1000000;
|
|
6
|
+
|
|
7
|
+
function toInt(v) {
|
|
8
|
+
const n = typeof v === 'number' ? v : typeof v === 'string' && /^\d+$/.test(v.trim()) ? Number(v) : NaN;
|
|
9
|
+
return Number.isFinite(n) ? n : null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Claude Code merges settings as project local > project > user, and an env var beats the
|
|
13
|
+
// `autoCompactWindow` key. Shell env and managed settings are not visible from here.
|
|
14
|
+
// https://code.claude.com/docs/en/env-vars.md, https://code.claude.com/docs/en/settings-reference.md
|
|
15
|
+
function getAutoCompact(claudeDir, projectPath) {
|
|
16
|
+
const files = [path.join(claudeDir, 'settings.json')];
|
|
17
|
+
if (projectPath) {
|
|
18
|
+
files.push(path.join(projectPath, '.claude', 'settings.json'), path.join(projectPath, '.claude', 'settings.local.json'));
|
|
19
|
+
}
|
|
20
|
+
let enabled = true;
|
|
21
|
+
let envWindow = null;
|
|
22
|
+
let keyWindow = null;
|
|
23
|
+
let pct = null;
|
|
24
|
+
for (const file of files) {
|
|
25
|
+
const s = readSettings(file);
|
|
26
|
+
if (!s || typeof s !== 'object') continue;
|
|
27
|
+
if (typeof s.autoCompactEnabled === 'boolean') enabled = s.autoCompactEnabled;
|
|
28
|
+
keyWindow = toInt(s.autoCompactWindow) ?? keyWindow;
|
|
29
|
+
envWindow = toInt(s.env?.CLAUDE_CODE_AUTO_COMPACT_WINDOW) ?? envWindow;
|
|
30
|
+
pct = toInt(s.env?.CLAUDE_AUTOCOMPACT_PCT_OVERRIDE) ?? pct;
|
|
31
|
+
}
|
|
32
|
+
if (!enabled) return null;
|
|
33
|
+
const window = envWindow ?? keyWindow;
|
|
34
|
+
const validPct = pct != null && pct >= 1 && pct <= 100 ? pct : null;
|
|
35
|
+
if (window == null && validPct == null) return null;
|
|
36
|
+
return {
|
|
37
|
+
window: window == null ? null : Math.min(MAX_WINDOW, Math.max(MIN_WINDOW, window)),
|
|
38
|
+
pct: validPct,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = { getAutoCompact };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
|
|
3
|
+
const fileCache = new Map();
|
|
4
|
+
|
|
5
|
+
// Parsed Claude Code settings file, or null when it is missing or invalid. Re-read only
|
|
6
|
+
// after its mtime changes.
|
|
7
|
+
function readSettings(file) {
|
|
8
|
+
const mtime = fs.statSync(file, { throwIfNoEntry: false })?.mtimeMs ?? null;
|
|
9
|
+
const hit = fileCache.get(file);
|
|
10
|
+
if (hit && hit.mtime === mtime) return hit.data;
|
|
11
|
+
let data = null;
|
|
12
|
+
if (mtime !== null) {
|
|
13
|
+
try {
|
|
14
|
+
data = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
15
|
+
} catch {}
|
|
16
|
+
}
|
|
17
|
+
fileCache.set(file, { mtime, data });
|
|
18
|
+
return data;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = { readSettings };
|
package/lib/dispatch.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
const crypto = require('node:crypto');
|
|
10
10
|
const { tokenMatches } = require('./terminal');
|
|
11
|
-
const { clampWait } = require('./session-events');
|
|
11
|
+
const { clampWait, EVENT_PREFIX } = require('./session-events');
|
|
12
12
|
|
|
13
13
|
const OUTCOMES = new Set(['succeeded', 'failed']);
|
|
14
14
|
const MAX_SUMMARY = 4000;
|
|
@@ -45,9 +45,16 @@ function formatPreamble(record, cli = 'claude-code-kanban') {
|
|
|
45
45
|
|
|
46
46
|
// The line reaches the parent through the postman at hook trust level; the summary is
|
|
47
47
|
// last so no summary text can pose as a further field.
|
|
48
|
+
const DISPATCH_OUTCOME = {
|
|
49
|
+
succeeded: 'reported success',
|
|
50
|
+
failed: 'reported failure',
|
|
51
|
+
exited: 'ended without a report',
|
|
52
|
+
};
|
|
53
|
+
|
|
48
54
|
function formatDispatchLine(r) {
|
|
49
|
-
const
|
|
50
|
-
|
|
55
|
+
const outcome = DISPATCH_OUTCOME[r.status] || r.status;
|
|
56
|
+
const head = `${EVENT_PREFIX} Dispatch ${r.id} (session ${r.session}) ${outcome}.`;
|
|
57
|
+
return r.summary ? `${head} Summary: ${r.summary}` : head;
|
|
51
58
|
}
|
|
52
59
|
|
|
53
60
|
function publicView(r) {
|
|
@@ -169,4 +176,4 @@ function createDispatchRegistry({ onSettle, now = Date.now } = {}) {
|
|
|
169
176
|
return { create, attach, discard, settle, sessionExited, list, wait };
|
|
170
177
|
}
|
|
171
178
|
|
|
172
|
-
module.exports = { createDispatchRegistry, formatPreamble, formatDispatchLine, isPeerName };
|
|
179
|
+
module.exports = { createDispatchRegistry, formatPreamble, formatDispatchLine, isPeerName, DISPATCH_OUTCOME };
|
package/lib/retention.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// Expiry of per-session state cck writes. The rule and the sweep: docs/retention.md.
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs/promises');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { readSettings } = require('./claude-settings');
|
|
6
|
+
const { DISPATCH_OUTCOME } = require('./dispatch');
|
|
7
|
+
|
|
8
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
9
|
+
const DEFAULT_CLEANUP_DAYS = 30;
|
|
10
|
+
// A started session writes its transcript a moment after the record; the sweep must not
|
|
11
|
+
// read that gap as a deleted transcript.
|
|
12
|
+
const GRACE_MS = 60 * 60 * 1000;
|
|
13
|
+
const MAX_DISPATCHED = 500;
|
|
14
|
+
const OUTCOMES = new Set(Object.keys(DISPATCH_OUTCOME));
|
|
15
|
+
|
|
16
|
+
// Only the config dir's settings: retention is per config dir, not per project.
|
|
17
|
+
function retentionMs(claudeDir) {
|
|
18
|
+
const days = readSettings(path.join(claudeDir, 'settings.json'))?.cleanupPeriodDays;
|
|
19
|
+
return (Number.isInteger(days) && days > 0 ? days : DEFAULT_CLEANUP_DAYS) * DAY_MS;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// `known` is the set of session ids with a transcript, or null when the scan found none,
|
|
23
|
+
// so a failed scan never reads as every transcript gone.
|
|
24
|
+
function isExpired(id, at, { known, maxAgeMs, now }) {
|
|
25
|
+
const age = now - at;
|
|
26
|
+
if (age < GRACE_MS) return false;
|
|
27
|
+
return (known && !known.has(id)) || age > maxAgeMs;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Sessions started through `dispatch start`, kept after the dispatch settles so the card
|
|
32
|
+
* keeps its marker.
|
|
33
|
+
* @param {object} o
|
|
34
|
+
* @param {() => object|null} o.load returns `{version: 1, sessions: {[id]: {parent, status, at}}}` or null
|
|
35
|
+
* @param {(data: object) => void} o.save
|
|
36
|
+
*/
|
|
37
|
+
function createDispatchedStore({ load, save, now = Date.now }) {
|
|
38
|
+
// Insertion order is oldest first, so the cap drops the first key.
|
|
39
|
+
const entries = new Map();
|
|
40
|
+
const saved = load()?.sessions;
|
|
41
|
+
if (saved && typeof saved === 'object') {
|
|
42
|
+
for (const [id, e] of Object.entries(saved)) {
|
|
43
|
+
if (!e || !Number.isFinite(e.at)) continue;
|
|
44
|
+
// The dispatch registry is in memory: a dispatch that was running when the server
|
|
45
|
+
// stopped can never settle, so it reads as ended.
|
|
46
|
+
const status = OUTCOMES.has(e.status) ? e.status : 'exited';
|
|
47
|
+
entries.set(id, { parent: typeof e.parent === 'string' ? e.parent : null, status, at: e.at });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const persist = () => save({ version: 1, sessions: Object.fromEntries(entries) });
|
|
52
|
+
|
|
53
|
+
function record(id, parent) {
|
|
54
|
+
entries.delete(id);
|
|
55
|
+
entries.set(id, { parent: parent || null, status: 'running', at: now() });
|
|
56
|
+
if (entries.size > MAX_DISPATCHED) entries.delete(entries.keys().next().value);
|
|
57
|
+
persist();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function settle(id, status) {
|
|
61
|
+
const e = entries.get(id);
|
|
62
|
+
if (!e || e.status !== 'running' || !OUTCOMES.has(status)) return;
|
|
63
|
+
e.status = status;
|
|
64
|
+
persist();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Returns how many entries went; writes only when one did.
|
|
68
|
+
function prune({ known, maxAgeMs }) {
|
|
69
|
+
const t = now();
|
|
70
|
+
let removed = 0;
|
|
71
|
+
for (const [id, e] of entries) {
|
|
72
|
+
if (isExpired(id, e.at, { known, maxAgeMs, now: t })) {
|
|
73
|
+
entries.delete(id);
|
|
74
|
+
removed++;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (removed) persist();
|
|
78
|
+
return removed;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { record, settle, prune, get: (id) => entries.get(id) || null };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Names only, no stat or parse: the sweep needs which transcripts exist, not what they hold.
|
|
85
|
+
// Null when none is found, so a missing or unreadable projects dir never reads as every
|
|
86
|
+
// transcript gone.
|
|
87
|
+
async function listTranscriptIds(projectsDir) {
|
|
88
|
+
const ids = new Set();
|
|
89
|
+
let dirs;
|
|
90
|
+
try {
|
|
91
|
+
dirs = await fs.readdir(projectsDir, { withFileTypes: true });
|
|
92
|
+
} catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
for (const d of dirs) {
|
|
96
|
+
if (!d.isDirectory()) continue;
|
|
97
|
+
try {
|
|
98
|
+
for (const f of await fs.readdir(path.join(projectsDir, d.name))) {
|
|
99
|
+
if (f.endsWith('.jsonl')) ids.add(f.slice(0, -'.jsonl'.length));
|
|
100
|
+
}
|
|
101
|
+
} catch {}
|
|
102
|
+
}
|
|
103
|
+
return ids.size ? ids : null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// `<dir>/<session id>/<ts>.md`, one folder per session.
|
|
107
|
+
async function pruneSessionDirs(dir, { known, maxAgeMs, now = Date.now() }) {
|
|
108
|
+
let removed = 0;
|
|
109
|
+
let sessionDirs;
|
|
110
|
+
try {
|
|
111
|
+
sessionDirs = await fs.readdir(dir, { withFileTypes: true });
|
|
112
|
+
} catch {
|
|
113
|
+
return 0;
|
|
114
|
+
}
|
|
115
|
+
for (const d of sessionDirs) {
|
|
116
|
+
if (!d.isDirectory()) continue;
|
|
117
|
+
const sessionDir = path.join(dir, d.name);
|
|
118
|
+
try {
|
|
119
|
+
const files = await fs.readdir(sessionDir);
|
|
120
|
+
let kept = 0;
|
|
121
|
+
for (const f of files) {
|
|
122
|
+
const file = path.join(sessionDir, f);
|
|
123
|
+
const { mtimeMs } = await fs.stat(file);
|
|
124
|
+
if (isExpired(d.name, mtimeMs, { known, maxAgeMs, now })) {
|
|
125
|
+
await fs.rm(file, { force: true });
|
|
126
|
+
removed++;
|
|
127
|
+
} else kept++;
|
|
128
|
+
}
|
|
129
|
+
// An old mtime means no file was added since the read, so a review written right
|
|
130
|
+
// now cannot lose its folder.
|
|
131
|
+
if (!kept && now - (await fs.stat(sessionDir)).mtimeMs >= GRACE_MS) await fs.rmdir(sessionDir);
|
|
132
|
+
} catch {}
|
|
133
|
+
}
|
|
134
|
+
return removed;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = { createDispatchedStore, listTranscriptIds, pruneSessionDirs, retentionMs, GRACE_MS, MAX_DISPATCHED, DAY_MS };
|
package/lib/session-events.js
CHANGED
|
@@ -29,13 +29,33 @@ function sanitizeEventLine(line) {
|
|
|
29
29
|
return line.replace(/[\x00-\x1f\x7f]/g, ' ').trim().slice(0, 1500);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
|
|
32
|
+
// Lines are plain sentences so they still read right once the skill that explains them has
|
|
33
|
+
// left the agent's context. EVENT_PREFIX marks the board as the sender.
|
|
34
|
+
const EVENT_PREFIX = '[kanban board]';
|
|
35
|
+
|
|
36
|
+
// Everything after `Description:` is the description verbatim to end of line, so no amount
|
|
37
|
+
// of board text can pose as a further part of the line. That leaves the subject as the only
|
|
38
|
+
// value that needs delimiting.
|
|
35
39
|
function formatTaskMoved(taskId, prevStatus, task) {
|
|
36
40
|
const subject = String(task.subject || '').replace(/(["\\])/g, '\\$1');
|
|
37
|
-
const
|
|
38
|
-
|
|
41
|
+
const from = prevStatus ? ` from ${prevStatus}` : '';
|
|
42
|
+
const head = `${EVENT_PREFIX} The user moved task ${taskId} "${subject}"${from} to ${task.status}.`;
|
|
43
|
+
return task.description ? `${head} Description: ${task.description}` : head;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// The comments themselves live in the review file; the line only points at it, because a
|
|
47
|
+
// batch of quotes would not survive the one-line cap. The path comes last and runs to end
|
|
48
|
+
// of line, so a space in the config dir cannot split it.
|
|
49
|
+
function formatReviewSubmitted(count, label, reviewFile) {
|
|
50
|
+
const comments = `${count} review comment${count === 1 ? '' : 's'}`;
|
|
51
|
+
return `${EVENT_PREFIX} The user left ${comments} on ${label}. Address them: ${reviewFile}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// A postman between two polls has no waiter for a moment, so this can miss a live one.
|
|
55
|
+
// The caller then falls back to another route, which only costs a duplicate route, never
|
|
56
|
+
// a lost review.
|
|
57
|
+
function hasSessionListener(sessionId) {
|
|
58
|
+
return !!sessionEventBuckets.get(sessionId)?.waiters.size;
|
|
39
59
|
}
|
|
40
60
|
|
|
41
61
|
function enqueueSessionEvent(sessionId, line) {
|
|
@@ -59,8 +79,8 @@ function clampWait(sec) {
|
|
|
59
79
|
return Math.min(Math.max(Number(sec) || 0, 0), MAX_WAIT_SEC);
|
|
60
80
|
}
|
|
61
81
|
|
|
62
|
-
// Each topic rides its own bucket, so the
|
|
63
|
-
// the user did not grant with
|
|
82
|
+
// Each topic rides its own bucket, so the dispatch postman never prints task moves
|
|
83
|
+
// the user did not grant with the follow skill. No topic is the task-move bucket.
|
|
64
84
|
function topicKey(topic, sessionId) {
|
|
65
85
|
return topic ? `${topic}:${sessionId}` : sessionId;
|
|
66
86
|
}
|
|
@@ -109,9 +129,12 @@ function drain(sessionId, bucket) {
|
|
|
109
129
|
}
|
|
110
130
|
|
|
111
131
|
module.exports = {
|
|
132
|
+
EVENT_PREFIX,
|
|
112
133
|
sessionEventBuckets,
|
|
113
134
|
sanitizeEventLine,
|
|
114
135
|
formatTaskMoved,
|
|
136
|
+
formatReviewSubmitted,
|
|
137
|
+
hasSessionListener,
|
|
115
138
|
enqueueSessionEvent,
|
|
116
139
|
handleSessionEvents,
|
|
117
140
|
topicKey,
|
package/lib/terminal.js
CHANGED
|
@@ -356,16 +356,24 @@ function createTerminalService(o) {
|
|
|
356
356
|
// Waits for claude to turn on bracketed paste (its input box is live), then for the
|
|
357
357
|
// screen to settle. A folder-trust question also takes input, and Enter there would
|
|
358
358
|
// answer it, so the prompt waits for as long as that question is on screen.
|
|
359
|
+
function bracketed(text) {
|
|
360
|
+
return `\x1b[200~${text.replace(/\r?\n/g, '\r')}\x1b[201~`;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function onTrustScreen(s) {
|
|
364
|
+
return /\btrust\b/i.test(screenText(s.term));
|
|
365
|
+
}
|
|
366
|
+
|
|
359
367
|
function queuePrompt(s, prompt) {
|
|
360
368
|
let armed = false;
|
|
361
369
|
let timer = null;
|
|
362
|
-
const text =
|
|
370
|
+
const text = bracketed(prompt);
|
|
363
371
|
s.onOutput = (data) => {
|
|
364
372
|
if (!armed && data.includes('\x1b[?2004h')) armed = true;
|
|
365
373
|
if (!armed) return;
|
|
366
374
|
clearTimeout(timer);
|
|
367
375
|
timer = setTimeout(() => {
|
|
368
|
-
if (s.exited ||
|
|
376
|
+
if (s.exited || onTrustScreen(s)) return;
|
|
369
377
|
s.onOutput = null;
|
|
370
378
|
s.pty.write(text);
|
|
371
379
|
setTimeout(() => { if (!s.exited) s.pty.write('\r'); }, 100);
|
|
@@ -654,6 +662,18 @@ function createTerminalService(o) {
|
|
|
654
662
|
return r.error ? r : { id: r.session.id, cwd: r.session.cwd };
|
|
655
663
|
}
|
|
656
664
|
|
|
665
|
+
// Pastes into claude's input box and stops there: Enter would also submit whatever the
|
|
666
|
+
// user had half-typed, and over a permission dialog it would answer it. A fork runs under
|
|
667
|
+
// a new session id, so its terminal id does not name the session the text is for. A
|
|
668
|
+
// pending queued prompt owns the input box until it is sent.
|
|
669
|
+
function paste(id, text) {
|
|
670
|
+
const s = sessions.get(id);
|
|
671
|
+
if (!s || s.ended || s.exited || s.onOutput || s.mode === 'shell' || s.mode === 'fork') return false;
|
|
672
|
+
if (onTrustScreen(s)) return false;
|
|
673
|
+
s.pty.write(bracketed(text));
|
|
674
|
+
return true;
|
|
675
|
+
}
|
|
676
|
+
|
|
657
677
|
function shutdown() {
|
|
658
678
|
shuttingDown = true;
|
|
659
679
|
clearTimeout(saveTimer);
|
|
@@ -663,7 +683,7 @@ function createTerminalService(o) {
|
|
|
663
683
|
sessions.clear();
|
|
664
684
|
}
|
|
665
685
|
|
|
666
|
-
return { token, handleUpgrade, clientConfig, list, isRunning, end, authorized, startNew, restore, shutdown, unavailableReason };
|
|
686
|
+
return { token, handleUpgrade, clientConfig, list, isRunning, end, authorized, startNew, paste, restore, shutdown, unavailableReason };
|
|
667
687
|
}
|
|
668
688
|
|
|
669
689
|
module.exports = { createTerminalService, readTerminalConfig, ptyEnv, shellArgs, resolveShell, claudeArgsFor, parseNewSpec, findPickProcess, tokenMatches };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-kanban",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.17.0",
|
|
4
4
|
"description": "claude-code-kanban dashboard integration: agent activity tracking, context statusline, skills to drive the board from a session and to follow it",
|
|
5
5
|
"experimental": {
|
|
6
6
|
"monitors": "./monitors.json"
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
"hooks": [
|
|
7
7
|
{
|
|
8
8
|
"type": "command",
|
|
9
|
-
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/agent-spy.sh",
|
|
9
|
+
"command": "\"${CLAUDE_PLUGIN_ROOT}/scripts/agent-spy.sh\"",
|
|
10
|
+
"shell": "bash",
|
|
10
11
|
"timeout": 5
|
|
11
12
|
}
|
|
12
13
|
]
|
|
@@ -18,7 +19,8 @@
|
|
|
18
19
|
"hooks": [
|
|
19
20
|
{
|
|
20
21
|
"type": "command",
|
|
21
|
-
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/agent-spy.sh",
|
|
22
|
+
"command": "\"${CLAUDE_PLUGIN_ROOT}/scripts/agent-spy.sh\"",
|
|
23
|
+
"shell": "bash",
|
|
22
24
|
"timeout": 5
|
|
23
25
|
}
|
|
24
26
|
]
|
|
@@ -30,7 +32,8 @@
|
|
|
30
32
|
"hooks": [
|
|
31
33
|
{
|
|
32
34
|
"type": "command",
|
|
33
|
-
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/agent-spy.sh",
|
|
35
|
+
"command": "\"${CLAUDE_PLUGIN_ROOT}/scripts/agent-spy.sh\"",
|
|
36
|
+
"shell": "bash",
|
|
34
37
|
"timeout": 5
|
|
35
38
|
}
|
|
36
39
|
]
|
|
@@ -42,7 +45,8 @@
|
|
|
42
45
|
"hooks": [
|
|
43
46
|
{
|
|
44
47
|
"type": "command",
|
|
45
|
-
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/agent-spy.sh",
|
|
48
|
+
"command": "\"${CLAUDE_PLUGIN_ROOT}/scripts/agent-spy.sh\"",
|
|
49
|
+
"shell": "bash",
|
|
46
50
|
"timeout": 5
|
|
47
51
|
}
|
|
48
52
|
]
|
|
@@ -54,7 +58,8 @@
|
|
|
54
58
|
"hooks": [
|
|
55
59
|
{
|
|
56
60
|
"type": "command",
|
|
57
|
-
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/agent-spy.sh",
|
|
61
|
+
"command": "\"${CLAUDE_PLUGIN_ROOT}/scripts/agent-spy.sh\"",
|
|
62
|
+
"shell": "bash",
|
|
58
63
|
"timeout": 5
|
|
59
64
|
}
|
|
60
65
|
]
|
|
@@ -66,7 +71,8 @@
|
|
|
66
71
|
"hooks": [
|
|
67
72
|
{
|
|
68
73
|
"type": "command",
|
|
69
|
-
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/approval-gate.sh",
|
|
74
|
+
"command": "\"${CLAUDE_PLUGIN_ROOT}/scripts/approval-gate.sh\"",
|
|
75
|
+
"shell": "bash",
|
|
70
76
|
"timeout": 86400
|
|
71
77
|
}
|
|
72
78
|
]
|
|
@@ -78,7 +84,8 @@
|
|
|
78
84
|
"hooks": [
|
|
79
85
|
{
|
|
80
86
|
"type": "command",
|
|
81
|
-
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/agent-spy.sh",
|
|
87
|
+
"command": "\"${CLAUDE_PLUGIN_ROOT}/scripts/agent-spy.sh\"",
|
|
88
|
+
"shell": "bash",
|
|
82
89
|
"timeout": 5
|
|
83
90
|
}
|
|
84
91
|
]
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
[
|
|
2
2
|
{
|
|
3
3
|
"name": "kanban-doorbell",
|
|
4
|
-
"description": "Notifies this session when its tasks are moved on the kanban board.",
|
|
4
|
+
"description": "Notifies this session when its tasks are moved on the kanban board or the user sends it review comments.",
|
|
5
5
|
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/postman.js\"",
|
|
6
|
-
"when": "on-skill-invoke:claude-code-kanban:
|
|
6
|
+
"when": "on-skill-invoke:claude-code-kanban:follow"
|
|
7
7
|
},
|
|
8
8
|
{
|
|
9
9
|
"name": "kanban-dispatch-inbox",
|
|
10
10
|
"description": "Notifies this session when a session it dispatched through cck reports or exits.",
|
|
11
11
|
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/postman.js\" --topic dispatch --keep-backlog",
|
|
12
|
-
"when": "on-skill-invoke:claude-code-kanban:
|
|
12
|
+
"when": "on-skill-invoke:claude-code-kanban:dispatch"
|
|
13
13
|
}
|
|
14
14
|
]
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
//
|
|
12
12
|
// The lines we print carry board text (the card subject and description), which the
|
|
13
13
|
// session is told to read as the user's own brief. That is only safe because we are armed
|
|
14
|
-
// by an explicit `
|
|
14
|
+
// by an explicit `follow` invocation: the user asked to follow the board before anything the
|
|
15
15
|
// board says can reach the model.
|
|
16
16
|
|
|
17
17
|
const fs = require('fs');
|
|
@@ -29,7 +29,7 @@ const RETRY_MS = 15000;
|
|
|
29
29
|
// Windows sometimes fails a loopback connect with ETIMEDOUT while the board is up, so that
|
|
30
30
|
// error gets a few short waits before the normal one.
|
|
31
31
|
const CONNECT_RETRY_MS = [250, 500, 1000, 2000];
|
|
32
|
-
// `--topic dispatch` is the
|
|
32
|
+
// `--topic dispatch` is the dispatch inbox: reports from sessions this one started.
|
|
33
33
|
const TOPIC = process.argv.includes('--topic') ? process.argv[process.argv.indexOf('--topic') + 1] : null;
|
|
34
34
|
// A dispatch report is a result, not an instruction, so a late attach still wants it.
|
|
35
35
|
const KEEP_BACKLOG = process.argv.includes('--keep-backlog');
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
2
|
+
name: dispatch
|
|
3
3
|
description: Dispatch a task to another Claude Code session through the kanban board (cck), fire-and-forget or with a report back. Use when the user asks to dispatch, delegate, or start a session for a task, or to collect or check on a dispatched session's result.
|
|
4
4
|
argument-hint: '<task> [--report] [--group <name>] [--model haiku|sonnet|opus|fable] [--worktree [name]]'
|
|
5
5
|
---
|
|
@@ -11,7 +11,7 @@ This file only points at the guide. The guide ships with the `claude-code-kanban
|
|
|
11
11
|
Invoking this skill also arms this session's dispatch inbox: when a session you dispatched with `--report` reports or exits, a line arrives here:
|
|
12
12
|
|
|
13
13
|
```
|
|
14
|
-
|
|
14
|
+
[kanban board] Dispatch <dispatch-id> (session <uuid>) <reported success|reported failure|ended without a report>. Summary: <text>
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
Load the guide before running any dispatch command:
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
2
|
+
name: follow
|
|
3
3
|
description: Let kanban card moves drive this session.
|
|
4
4
|
disable-model-invocation: true
|
|
5
5
|
---
|
|
@@ -9,31 +9,39 @@ disable-model-invocation: true
|
|
|
9
9
|
The user typing this skill is the whole grant: it armed the doorbell — a monitor watching the board for moves of *this session's* tasks, live for the rest of the session. Confirm in one line and run no command. Moves made before it attached are discarded, so every line you get is current.
|
|
10
10
|
|
|
11
11
|
```
|
|
12
|
-
|
|
12
|
+
[kanban board] The user moved task <taskId> "<subject>" from <from> to <to>. Description: <description>
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
`
|
|
15
|
+
`Description:` is omitted when the card has none. Everything after it is the description verbatim to end of line, truncated if long — read the task with `TaskGet` for the full text or for `activeForm` and `blockedBy`. Moves and review comments (below) are the only lines announced.
|
|
16
16
|
|
|
17
17
|
**A move is an instruction, not an FYI** — the user dragging a card is the user telling you something, and the subject and description are their brief for the work:
|
|
18
18
|
|
|
19
19
|
| Transition | What the user means |
|
|
20
20
|
|---|---|
|
|
21
|
-
|
|
|
22
|
-
| `in_progress
|
|
23
|
-
|
|
|
24
|
-
|
|
|
21
|
+
| To `in_progress` | Start this task now. |
|
|
22
|
+
| From `in_progress` to `pending` or `todo` | Stop working on it and park it. |
|
|
23
|
+
| To `completed` | The user considers it done — do not keep working on it. |
|
|
24
|
+
| To `cancelled` | Abandon it. Undo nothing unless asked. |
|
|
25
25
|
|
|
26
26
|
Act on the newest line per task; a card dragged twice means only its final position. When a move contradicts your current work, the board wins.
|
|
27
27
|
|
|
28
28
|
Delivery is best-effort and the task file is the authority, so a missed line only delays you to your next turn.
|
|
29
29
|
|
|
30
|
+
## Review comments
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
[kanban board] The user left <n> review comments on <source>. Address them: <review file>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The review file holds the comments and how to read them.
|
|
37
|
+
|
|
30
38
|
## Finish the task on the board
|
|
31
39
|
|
|
32
|
-
A move hands you work; the task file is where you hand it back. When the work a
|
|
40
|
+
A move hands you work; the task file is where you hand it back. When the work a move to `in_progress` asked for is done, set that task to `completed` with `TaskUpdate` before you reply — the card moves itself, and the board is where the user is watching. A card left in In Progress reads as work still running.
|
|
33
41
|
|
|
34
42
|
Your own `TaskUpdate` writes the task file rather than going through the board, so it rings no doorbell: there is no echo to guard against.
|
|
35
43
|
|
|
36
44
|
## Troubleshooting
|
|
37
45
|
|
|
38
|
-
- **No lines arrive** → the doorbell arms once per session, on the namespaced dispatch: `/claude-code-kanban:
|
|
46
|
+
- **No lines arrive** → the doorbell arms once per session, on the namespaced dispatch: `/claude-code-kanban:follow`. A session that never got it never hears the board.
|
|
39
47
|
- **"Cannot reach cck server…"** → the error names the port it tried. Ask the user to start the server with `claude-code-kanban`.
|
|
@@ -29,7 +29,7 @@ claude-code-kanban help <command> <subcommand> # flags and examples
|
|
|
29
29
|
| `view` / `peek` | `session view <id>`, `session peek <id>` |
|
|
30
30
|
| tasks, projects | `task list`, `project list` |
|
|
31
31
|
|
|
32
|
-
To be driven *by* the board instead — card moves arriving as instructions — the user types `/claude-code-kanban:
|
|
32
|
+
To be driven *by* the board instead — card moves arriving as instructions — the user types `/claude-code-kanban:follow`.
|
|
33
33
|
|
|
34
34
|
## Troubleshooting
|
|
35
35
|
|