claude-code-kanban 2.3.2 → 3.0.0-rc.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.
- package/install.js +117 -177
- package/lib/parsers.js +9 -2
- package/package.json +2 -3
- package/plugin/.claude-plugin/marketplace.json +13 -0
- package/plugin/plugins/claude-code-kanban/.claude-plugin/plugin.json +6 -0
- package/plugin/plugins/claude-code-kanban/hooks/hooks.json +88 -0
- package/{hooks → plugin/plugins/claude-code-kanban/scripts}/agent-spy.sh +8 -6
- package/{hooks → plugin/plugins/claude-code-kanban/scripts}/context-status.sh +2 -2
- package/public/app.js +3 -0
- package/server.js +37 -28
package/install.js
CHANGED
|
@@ -9,22 +9,9 @@ const { execSync } = require('child_process');
|
|
|
9
9
|
const CLAUDE_DIR = path.join(os.homedir(), '.claude');
|
|
10
10
|
const HOOKS_DIR = path.join(CLAUDE_DIR, 'hooks');
|
|
11
11
|
const SETTINGS_PATH = path.join(CLAUDE_DIR, 'settings.json');
|
|
12
|
-
const
|
|
13
|
-
const
|
|
12
|
+
const PLUGIN_DIR = path.join(__dirname, 'plugin');
|
|
13
|
+
const CTX_SCRIPT_SRC = path.join(PLUGIN_DIR, 'plugins', 'claude-code-kanban', 'scripts', 'context-status.sh');
|
|
14
14
|
const CTX_SCRIPT_DEST = path.join(HOOKS_DIR, 'context-status.sh');
|
|
15
|
-
const CTX_SCRIPT_SRC = path.join(__dirname, 'hooks', 'context-status.sh');
|
|
16
|
-
const AGENT_ACTIVITY_DIR = path.join(CLAUDE_DIR, 'agent-activity');
|
|
17
|
-
|
|
18
|
-
const HOOK_COMMAND = '~/.claude/hooks/agent-spy.sh';
|
|
19
|
-
const HOOK_EVENTS = [
|
|
20
|
-
{ event: 'SessionStart' },
|
|
21
|
-
{ event: 'SubagentStart' },
|
|
22
|
-
{ event: 'SubagentStop' },
|
|
23
|
-
{ event: 'TeammateIdle' },
|
|
24
|
-
{ event: 'PermissionRequest' },
|
|
25
|
-
{ event: 'PreToolUse', matcher: 'AskUserQuestion' },
|
|
26
|
-
{ event: 'PostToolUse' },
|
|
27
|
-
];
|
|
28
15
|
|
|
29
16
|
// ANSI helpers
|
|
30
17
|
const green = s => `\x1b[32m${s}\x1b[0m`;
|
|
@@ -43,136 +30,113 @@ function prompt(question) {
|
|
|
43
30
|
});
|
|
44
31
|
}
|
|
45
32
|
|
|
33
|
+
function runCLI(cmd, okPatterns = []) {
|
|
34
|
+
try {
|
|
35
|
+
const out = execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
36
|
+
return { ok: true, output: out };
|
|
37
|
+
} catch (e) {
|
|
38
|
+
const stderr = e.stderr?.trim() || e.message;
|
|
39
|
+
if (okPatterns.some(p => stderr.includes(p))) return { ok: true, idempotent: true };
|
|
40
|
+
return { ok: false, error: stderr };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function copyScript(src, dest) {
|
|
45
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
46
|
+
fs.copyFileSync(src, dest);
|
|
47
|
+
try { fs.chmodSync(dest, 0o755); } catch {}
|
|
48
|
+
}
|
|
49
|
+
|
|
46
50
|
async function runInstall() {
|
|
47
|
-
console.log(`\n ${bold('claude-code-kanban')} —
|
|
51
|
+
console.log(`\n ${bold('claude-code-kanban')} — Plugin & StatusLine installer\n`);
|
|
48
52
|
|
|
49
|
-
// 1. Check
|
|
50
|
-
process.stdout.write(' Checking
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
console.log(green(`✓ found (${
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
} else {
|
|
59
|
-
const currentShell = shell || process.env.ComSpec || 'unknown';
|
|
60
|
-
console.log(yellow(`⚠ bash not found (current shell: ${currentShell})`));
|
|
61
|
-
console.log(` ${dim('Hook scripts use #!/bin/bash and require a bash environment')}`);
|
|
62
|
-
if (!(await prompt(` Continue anyway? [Y/n] `))) {
|
|
63
|
-
console.log(`\n ${dim('Install cancelled.')}\n`);
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
53
|
+
// 1. Check prerequisites
|
|
54
|
+
process.stdout.write(' Checking claude CLI... ');
|
|
55
|
+
const claude = runCLI('claude --version');
|
|
56
|
+
if (claude.ok) {
|
|
57
|
+
console.log(green(`✓ found (${claude.output})`));
|
|
58
|
+
} else {
|
|
59
|
+
console.log(red('✗ claude CLI not found'));
|
|
60
|
+
console.log(` ${dim('Install Claude Code CLI first: https://docs.anthropic.com/en/docs/claude-code')}`);
|
|
61
|
+
return;
|
|
67
62
|
}
|
|
68
63
|
|
|
69
|
-
// 2. Check jq
|
|
70
64
|
process.stdout.write(' Checking jq... ');
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
console.log(green(`✓ found (${
|
|
74
|
-
}
|
|
75
|
-
console.log(yellow('⚠ not found — hook
|
|
65
|
+
const jq = runCLI('jq --version');
|
|
66
|
+
if (jq.ok) {
|
|
67
|
+
console.log(green(`✓ found (${jq.output})`));
|
|
68
|
+
} else {
|
|
69
|
+
console.log(yellow('⚠ not found — hook scripts require jq for JSON parsing'));
|
|
76
70
|
}
|
|
77
71
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
if (await prompt(` Different version found. Update? [Y/n] `)) {
|
|
88
|
-
fs.mkdirSync(HOOKS_DIR, { recursive: true });
|
|
89
|
-
fs.copyFileSync(src, dest);
|
|
90
|
-
try { fs.chmodSync(dest, 0o755); } catch {}
|
|
91
|
-
console.log(` ${green('✓')} Updated`);
|
|
92
|
-
return true;
|
|
93
|
-
}
|
|
94
|
-
console.log(` ${dim('Skipped')}`);
|
|
95
|
-
return false;
|
|
96
|
-
}
|
|
97
|
-
if (await prompt(` Not found. Install? [Y/n] `)) {
|
|
98
|
-
fs.mkdirSync(HOOKS_DIR, { recursive: true });
|
|
99
|
-
fs.copyFileSync(src, dest);
|
|
100
|
-
try { fs.chmodSync(dest, 0o755); } catch {}
|
|
101
|
-
console.log(` ${green('✓')} Installed and set executable`);
|
|
102
|
-
return true;
|
|
72
|
+
// 2. Register marketplace & install plugin via Claude CLI
|
|
73
|
+
console.log(`\n Plugin: ${dim(PLUGIN_DIR)}`);
|
|
74
|
+
if (await prompt(` Install claude-code-kanban plugin? [Y/n] `)) {
|
|
75
|
+
process.stdout.write(' Registering marketplace... ');
|
|
76
|
+
const mkt = runCLI(`claude plugin marketplace add "${PLUGIN_DIR}"`, ['already', 'exists']);
|
|
77
|
+
if (mkt.ok) {
|
|
78
|
+
console.log(green(mkt.idempotent ? '✓ already registered' : '✓'));
|
|
79
|
+
} else {
|
|
80
|
+
console.log(yellow(`⚠ ${mkt.error}`));
|
|
103
81
|
}
|
|
104
|
-
console.log(` ${dim('Skipped')}`);
|
|
105
|
-
return false;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// 3. Hook scripts
|
|
109
|
-
const hookInstalled = await installScript('Hook script', HOOK_SCRIPT_SRC, HOOK_SCRIPT_DEST);
|
|
110
|
-
const ctxInstalled = await installScript('Context spy', CTX_SCRIPT_SRC, CTX_SCRIPT_DEST);
|
|
111
82
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
try {
|
|
116
|
-
if (fs.existsSync(SETTINGS_PATH)) {
|
|
117
|
-
settings = JSON.parse(fs.readFileSync(SETTINGS_PATH, 'utf8'));
|
|
83
|
+
const inst = runCLI('claude plugin install claude-code-kanban@claude-code-kanban', ['already installed', 'already exists']);
|
|
84
|
+
if (inst.ok) {
|
|
85
|
+
console.log(` ${green('✓')} ${inst.idempotent ? 'Already installed' : 'Plugin installed'}`);
|
|
118
86
|
} else {
|
|
119
|
-
|
|
87
|
+
console.log(` ${red('✗')} Plugin install failed: ${inst.error}`);
|
|
120
88
|
}
|
|
121
|
-
}
|
|
122
|
-
console.log(` ${
|
|
123
|
-
printSummary(hookInstalled, false);
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (!settings.hooks) settings.hooks = {};
|
|
128
|
-
|
|
129
|
-
const needed = [];
|
|
130
|
-
for (const { event, matcher } of HOOK_EVENTS) {
|
|
131
|
-
if (!settings.hooks[event]) settings.hooks[event] = [];
|
|
132
|
-
const matcherStr = matcher || '';
|
|
133
|
-
const exists = settings.hooks[event].some(g =>
|
|
134
|
-
g.matcher === matcherStr && g.hooks?.some(h => h.command === HOOK_COMMAND)
|
|
135
|
-
);
|
|
136
|
-
if (!exists) needed.push({ event, matcher: matcherStr });
|
|
89
|
+
} else {
|
|
90
|
+
console.log(` ${dim('Skipped')}`);
|
|
137
91
|
}
|
|
138
92
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if (
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
fs.mkdirSync(CLAUDE_DIR, { recursive: true });
|
|
153
|
-
fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + '\n');
|
|
154
|
-
console.log(` ${green('✓')} ${needed.length} hook entries added`);
|
|
155
|
-
settingsUpdated = true;
|
|
93
|
+
// 3. StatusLine setup (context-status.sh must be copied globally since statusLine is not plugin-scoped)
|
|
94
|
+
console.log(`\n Context spy: ${dim(CTX_SCRIPT_DEST)}`);
|
|
95
|
+
let ctxInstalled = false;
|
|
96
|
+
if (fs.existsSync(CTX_SCRIPT_DEST)) {
|
|
97
|
+
const existing = fs.readFileSync(CTX_SCRIPT_DEST, 'utf8');
|
|
98
|
+
const bundled = fs.readFileSync(CTX_SCRIPT_SRC, 'utf8');
|
|
99
|
+
if (existing === bundled) {
|
|
100
|
+
console.log(` ${green('✓')} Up to date`);
|
|
101
|
+
ctxInstalled = true;
|
|
102
|
+
} else if (await prompt(` Different version found. Update? [Y/n] `)) {
|
|
103
|
+
copyScript(CTX_SCRIPT_SRC, CTX_SCRIPT_DEST);
|
|
104
|
+
console.log(` ${green('✓')} Updated`);
|
|
105
|
+
ctxInstalled = true;
|
|
156
106
|
} else {
|
|
157
107
|
console.log(` ${dim('Skipped')}`);
|
|
158
108
|
}
|
|
109
|
+
} else if (await prompt(` Not found. Install? [Y/n] `)) {
|
|
110
|
+
copyScript(CTX_SCRIPT_SRC, CTX_SCRIPT_DEST);
|
|
111
|
+
console.log(` ${green('✓')} Installed`);
|
|
112
|
+
ctxInstalled = true;
|
|
113
|
+
} else {
|
|
114
|
+
console.log(` ${dim('Skipped')}`);
|
|
159
115
|
}
|
|
160
116
|
|
|
161
|
-
//
|
|
162
|
-
const CTX_COMMAND = '~/.claude/hooks/context-status.sh';
|
|
163
|
-
let statusLineUpdated = false;
|
|
117
|
+
// 4. StatusLine config in settings.json
|
|
164
118
|
if (ctxInstalled) {
|
|
119
|
+
let settings;
|
|
120
|
+
try {
|
|
121
|
+
settings = fs.existsSync(SETTINGS_PATH)
|
|
122
|
+
? JSON.parse(fs.readFileSync(SETTINGS_PATH, 'utf8'))
|
|
123
|
+
: {};
|
|
124
|
+
} catch {
|
|
125
|
+
console.log(` ${red('✗')} Malformed JSON in settings.json — skipping statusline config`);
|
|
126
|
+
printSummary();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const CTX_COMMAND = '~/.claude/hooks/context-status.sh';
|
|
165
131
|
const hasCtx = settings.statusLine?.command?.includes('context-status.sh');
|
|
166
132
|
if (hasCtx) {
|
|
167
133
|
console.log(`\n StatusLine: ${green('✓')} Already configured`);
|
|
168
|
-
statusLineUpdated = true;
|
|
169
134
|
} else if (!settings.statusLine) {
|
|
170
135
|
console.log(`\n StatusLine: ${dim('not configured')}`);
|
|
171
136
|
if (await prompt(` Set up context tracking statusline? [Y/n] `)) {
|
|
172
137
|
settings.statusLine = { command: CTX_COMMAND };
|
|
173
138
|
fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + '\n');
|
|
174
139
|
console.log(` ${green('✓')} StatusLine configured`);
|
|
175
|
-
statusLineUpdated = true;
|
|
176
140
|
} else {
|
|
177
141
|
console.log(` ${dim('Skipped')}`);
|
|
178
142
|
}
|
|
@@ -183,51 +147,55 @@ async function runInstall() {
|
|
|
183
147
|
settings.statusLine.command = `${CTX_COMMAND} | ${existing}`;
|
|
184
148
|
fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + '\n');
|
|
185
149
|
console.log(` ${green('✓')} StatusLine updated`);
|
|
186
|
-
statusLineUpdated = true;
|
|
187
150
|
} else {
|
|
188
151
|
console.log(` ${dim('Skipped')}`);
|
|
189
152
|
}
|
|
190
153
|
}
|
|
191
154
|
}
|
|
192
155
|
|
|
193
|
-
printSummary(
|
|
156
|
+
printSummary();
|
|
194
157
|
}
|
|
195
158
|
|
|
196
|
-
function printSummary(
|
|
197
|
-
console.log('');
|
|
198
|
-
if (hookOk && settingsOk) {
|
|
199
|
-
console.log(` ${green('Agent Log will appear in the Kanban footer when subagents are active.')}`);
|
|
200
|
-
} else {
|
|
201
|
-
console.log(` ${yellow('Partial install — re-run --install to complete setup.')}`);
|
|
202
|
-
}
|
|
203
|
-
console.log('');
|
|
159
|
+
function printSummary() {
|
|
160
|
+
console.log(`\n ${green('Setup complete. Agent activity will appear in the Kanban dashboard.')}\n`);
|
|
204
161
|
}
|
|
205
162
|
|
|
206
163
|
async function runUninstall() {
|
|
207
|
-
console.log(`\n ${bold('claude-code-kanban')} —
|
|
164
|
+
console.log(`\n ${bold('claude-code-kanban')} — Uninstaller\n`);
|
|
165
|
+
|
|
166
|
+
// 1. Uninstall plugin via Claude CLI
|
|
167
|
+
process.stdout.write(' Removing plugin... ');
|
|
168
|
+
const uninst = runCLI('claude plugin uninstall claude-code-kanban', ['not found', 'not installed']);
|
|
169
|
+
if (uninst.ok) {
|
|
170
|
+
console.log(uninst.idempotent ? dim('Not installed') : green('✓ Removed'));
|
|
171
|
+
} else {
|
|
172
|
+
console.log(yellow(`⚠ ${uninst.error}`));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 2. Remove marketplace
|
|
176
|
+
process.stdout.write(' Removing marketplace... ');
|
|
177
|
+
const rmMkt = runCLI('claude plugin marketplace remove claude-code-kanban', ['not found', 'not configured']);
|
|
178
|
+
if (rmMkt.ok) {
|
|
179
|
+
console.log(rmMkt.idempotent ? dim('Not configured') : green('✓ Removed'));
|
|
180
|
+
} else {
|
|
181
|
+
console.log(yellow(`⚠ ${rmMkt.error}`));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 3. Remove context-status.sh copy
|
|
185
|
+
if (fs.existsSync(CTX_SCRIPT_DEST)) {
|
|
186
|
+
fs.unlinkSync(CTX_SCRIPT_DEST);
|
|
187
|
+
console.log(` Context spy: ${green('✓')} Removed`);
|
|
188
|
+
} else {
|
|
189
|
+
console.log(` Context spy: ${dim('Not found')}`);
|
|
190
|
+
}
|
|
208
191
|
|
|
209
|
-
//
|
|
192
|
+
// 4. Clean up settings.json (statusLine)
|
|
210
193
|
if (fs.existsSync(SETTINGS_PATH)) {
|
|
211
194
|
try {
|
|
212
195
|
const settings = JSON.parse(fs.readFileSync(SETTINGS_PATH, 'utf8'));
|
|
213
|
-
let
|
|
214
|
-
if (settings.hooks) {
|
|
215
|
-
const eventNames = [...new Set(HOOK_EVENTS.map(e => e.event))];
|
|
216
|
-
for (const event of eventNames) {
|
|
217
|
-
if (!Array.isArray(settings.hooks[event])) continue;
|
|
218
|
-
const before = settings.hooks[event].length;
|
|
219
|
-
settings.hooks[event] = settings.hooks[event].map(g => {
|
|
220
|
-
if (!g.hooks?.some(h => h.command === HOOK_COMMAND)) return g;
|
|
221
|
-
const filtered = g.hooks.filter(h => h.command !== HOOK_COMMAND);
|
|
222
|
-
return filtered.length > 0 ? { ...g, hooks: filtered } : null;
|
|
223
|
-
}).filter(Boolean);
|
|
224
|
-
removed += before - settings.hooks[event].length;
|
|
225
|
-
if (settings.hooks[event].length === 0) delete settings.hooks[event];
|
|
226
|
-
}
|
|
227
|
-
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
228
|
-
}
|
|
196
|
+
let changed = false;
|
|
229
197
|
|
|
230
|
-
// Strip context-status.sh from statusLine
|
|
198
|
+
// Strip context-status.sh from statusLine
|
|
231
199
|
if (settings.statusLine?.command?.includes('context-status.sh')) {
|
|
232
200
|
const cmd = settings.statusLine.command;
|
|
233
201
|
const stripped = cmd.replace(/~\/\.claude\/hooks\/context-status\.sh\s*\|\s*/, '').trim();
|
|
@@ -238,43 +206,15 @@ async function runUninstall() {
|
|
|
238
206
|
delete settings.statusLine;
|
|
239
207
|
console.log(` StatusLine: ${green('✓')} Removed`);
|
|
240
208
|
}
|
|
209
|
+
changed = true;
|
|
241
210
|
}
|
|
242
211
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
console.log(` Settings: ${green('✓')} Removed ${removed} hook entries`);
|
|
246
|
-
} else {
|
|
247
|
-
console.log(` Settings: ${dim('No hook entries found')}`);
|
|
212
|
+
if (changed) {
|
|
213
|
+
fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + '\n');
|
|
248
214
|
}
|
|
249
215
|
} catch {
|
|
250
216
|
console.log(` Settings: ${red('✗')} Could not parse settings.json`);
|
|
251
217
|
}
|
|
252
|
-
} else {
|
|
253
|
-
console.log(` Settings: ${dim('No settings.json found')}`);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// 2. Remove hook scripts
|
|
257
|
-
if (fs.existsSync(HOOK_SCRIPT_DEST)) {
|
|
258
|
-
fs.unlinkSync(HOOK_SCRIPT_DEST);
|
|
259
|
-
console.log(` Hook script: ${green('✓')} Removed`);
|
|
260
|
-
} else {
|
|
261
|
-
console.log(` Hook script: ${dim('Not found')}`);
|
|
262
|
-
}
|
|
263
|
-
if (fs.existsSync(CTX_SCRIPT_DEST)) {
|
|
264
|
-
fs.unlinkSync(CTX_SCRIPT_DEST);
|
|
265
|
-
console.log(` Context spy: ${green('✓')} Removed`);
|
|
266
|
-
} else {
|
|
267
|
-
console.log(` Context spy: ${dim('Not found')}`);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// 3. Optionally remove agent-activity data
|
|
271
|
-
if (fs.existsSync(AGENT_ACTIVITY_DIR)) {
|
|
272
|
-
if (await prompt(`\n Remove agent activity data (${AGENT_ACTIVITY_DIR})? [y/N] `)) {
|
|
273
|
-
fs.rmSync(AGENT_ACTIVITY_DIR, { recursive: true, force: true });
|
|
274
|
-
console.log(` ${green('✓')} Agent activity data removed`);
|
|
275
|
-
} else {
|
|
276
|
-
console.log(` ${dim('Kept agent activity data')}`);
|
|
277
|
-
}
|
|
278
218
|
}
|
|
279
219
|
|
|
280
220
|
console.log(`\n ${green('Uninstall complete.')}\n`);
|
package/lib/parsers.js
CHANGED
|
@@ -172,7 +172,7 @@ function readCustomTitle(jsonlPath, existingStat) {
|
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
function readSessionInfoFromJsonl(jsonlPath) {
|
|
175
|
-
const result = { slug: null, projectPath: null, gitBranch: null, customTitle: null };
|
|
175
|
+
const result = { slug: null, projectPath: null, cwd: null, gitBranch: null, customTitle: null };
|
|
176
176
|
let stat;
|
|
177
177
|
try {
|
|
178
178
|
stat = statSync(jsonlPath);
|
|
@@ -182,16 +182,22 @@ function readSessionInfoFromJsonl(jsonlPath) {
|
|
|
182
182
|
|
|
183
183
|
const headBuf = Buffer.alloc(Math.min(HEAD_SIZE, stat.size));
|
|
184
184
|
const hn = fs.readSync(fd, headBuf, 0, headBuf.length, 0);
|
|
185
|
+
let lastCwdFromHead = null;
|
|
185
186
|
for (const line of headBuf.toString('utf8', 0, hn).split('\n')) {
|
|
186
187
|
try {
|
|
187
188
|
const data = JSON.parse(line);
|
|
188
189
|
if (data.slug) result.slug = data.slug;
|
|
189
|
-
if (data.cwd)
|
|
190
|
+
if (data.cwd) {
|
|
191
|
+
if (!result.projectPath) result.projectPath = data.cwd;
|
|
192
|
+
lastCwdFromHead = data.cwd;
|
|
193
|
+
}
|
|
190
194
|
if (data.gitBranch) result.gitBranch = data.gitBranch;
|
|
191
195
|
if (result.slug && result.projectPath && result.gitBranch) break;
|
|
192
196
|
} catch (e) {}
|
|
193
197
|
}
|
|
194
198
|
|
|
199
|
+
result.cwd = lastCwdFromHead;
|
|
200
|
+
|
|
195
201
|
if ((!result.slug || !result.projectPath || !result.gitBranch) && stat.size > HEAD_SIZE) {
|
|
196
202
|
const tailStart = stat.size - TAIL_SIZE;
|
|
197
203
|
const tailBuf = Buffer.alloc(TAIL_SIZE);
|
|
@@ -203,6 +209,7 @@ function readSessionInfoFromJsonl(jsonlPath) {
|
|
|
203
209
|
if (!result.slug && data.slug) result.slug = data.slug;
|
|
204
210
|
if (!result.projectPath && data.cwd) result.projectPath = data.cwd;
|
|
205
211
|
if (!result.gitBranch && data.gitBranch) result.gitBranch = data.gitBranch;
|
|
212
|
+
if (!result.cwd && data.cwd) result.cwd = data.cwd;
|
|
206
213
|
if (result.slug && result.projectPath && result.gitBranch) break;
|
|
207
214
|
} catch (e) {}
|
|
208
215
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-kanban",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0-rc.1",
|
|
4
4
|
"description": "A web-based Kanban board for viewing Claude Code tasks with agent teams support",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"bin": {
|
|
@@ -45,8 +45,7 @@
|
|
|
45
45
|
"files": [
|
|
46
46
|
"server.js",
|
|
47
47
|
"install.js",
|
|
48
|
-
"
|
|
49
|
-
"hooks/context-status.sh",
|
|
48
|
+
"plugin/**/*",
|
|
50
49
|
"lib/**/*",
|
|
51
50
|
"public/**/*"
|
|
52
51
|
],
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "claude-code-kanban",
|
|
3
|
+
"owner": {
|
|
4
|
+
"name": "NikiforovAll"
|
|
5
|
+
},
|
|
6
|
+
"plugins": [
|
|
7
|
+
{
|
|
8
|
+
"name": "claude-code-kanban",
|
|
9
|
+
"source": "./plugins/claude-code-kanban",
|
|
10
|
+
"description": "Agent activity tracking for claude-code-kanban dashboard"
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"SessionStart": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/agent-spy.sh",
|
|
10
|
+
"timeout": 5
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"SubagentStart": [
|
|
16
|
+
{
|
|
17
|
+
"matcher": "",
|
|
18
|
+
"hooks": [
|
|
19
|
+
{
|
|
20
|
+
"type": "command",
|
|
21
|
+
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/agent-spy.sh",
|
|
22
|
+
"timeout": 5
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
],
|
|
27
|
+
"SubagentStop": [
|
|
28
|
+
{
|
|
29
|
+
"matcher": "",
|
|
30
|
+
"hooks": [
|
|
31
|
+
{
|
|
32
|
+
"type": "command",
|
|
33
|
+
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/agent-spy.sh",
|
|
34
|
+
"timeout": 5
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
],
|
|
39
|
+
"TeammateIdle": [
|
|
40
|
+
{
|
|
41
|
+
"matcher": "",
|
|
42
|
+
"hooks": [
|
|
43
|
+
{
|
|
44
|
+
"type": "command",
|
|
45
|
+
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/agent-spy.sh",
|
|
46
|
+
"timeout": 5
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
],
|
|
51
|
+
"PermissionRequest": [
|
|
52
|
+
{
|
|
53
|
+
"matcher": "",
|
|
54
|
+
"hooks": [
|
|
55
|
+
{
|
|
56
|
+
"type": "command",
|
|
57
|
+
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/agent-spy.sh",
|
|
58
|
+
"timeout": 5
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
],
|
|
63
|
+
"PreToolUse": [
|
|
64
|
+
{
|
|
65
|
+
"matcher": "AskUserQuestion",
|
|
66
|
+
"hooks": [
|
|
67
|
+
{
|
|
68
|
+
"type": "command",
|
|
69
|
+
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/agent-spy.sh",
|
|
70
|
+
"timeout": 5
|
|
71
|
+
}
|
|
72
|
+
]
|
|
73
|
+
}
|
|
74
|
+
],
|
|
75
|
+
"PostToolUse": [
|
|
76
|
+
{
|
|
77
|
+
"matcher": "",
|
|
78
|
+
"hooks": [
|
|
79
|
+
{
|
|
80
|
+
"type": "command",
|
|
81
|
+
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/agent-spy.sh",
|
|
82
|
+
"timeout": 5
|
|
83
|
+
}
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# Tracks subagent lifecycle: one JSON file per agent, grouped by session
|
|
3
|
-
# Layout: ~/.claude/agent-activity/{sessionId}/{agentId}.json
|
|
3
|
+
# Layout: ~/.claude/.cck/agent-activity/{sessionId}/{agentId}.json
|
|
4
4
|
|
|
5
5
|
INPUT=$(cat)
|
|
6
6
|
|
|
@@ -16,12 +16,14 @@ eval "$(echo "$INPUT" | jq -r '
|
|
|
16
16
|
|
|
17
17
|
[ -z "$SESSION_ID" ] && exit 0
|
|
18
18
|
|
|
19
|
+
CCK_ACTIVITY="$HOME/.claude/.cck/agent-activity"
|
|
20
|
+
|
|
19
21
|
# Map session to custom task list on session start
|
|
20
22
|
if [ "$EVENT" = "SessionStart" ]; then
|
|
21
23
|
TASK_LIST_ID="${CLAUDE_CODE_TASK_LIST_ID:-}"
|
|
22
24
|
if [ -n "$TASK_LIST_ID" ]; then
|
|
23
25
|
CWD=$(echo "$INPUT" | jq -r '.cwd // ""')
|
|
24
|
-
MAPS_DIR="$
|
|
26
|
+
MAPS_DIR="$CCK_ACTIVITY/_task-maps"
|
|
25
27
|
mkdir -p "$MAPS_DIR"
|
|
26
28
|
MAP_FILE="$MAPS_DIR/$TASK_LIST_ID.json"
|
|
27
29
|
TMP_FILE="$MAP_FILE.$$"
|
|
@@ -36,7 +38,7 @@ fi
|
|
|
36
38
|
|
|
37
39
|
# PostToolUse / non-waiting PreToolUse: clear waiting state
|
|
38
40
|
if [ "$EVENT" = "PostToolUse" ] || { [ "$EVENT" = "PreToolUse" ] && [ "$TOOL_NAME" != "AskUserQuestion" ]; }; then
|
|
39
|
-
WFILE="$
|
|
41
|
+
WFILE="$CCK_ACTIVITY/$SESSION_ID/_waiting.json"
|
|
40
42
|
rm -f "$WFILE"
|
|
41
43
|
[ "$EVENT" = "PostToolUse" ] && exit 0
|
|
42
44
|
fi
|
|
@@ -46,7 +48,7 @@ fi
|
|
|
46
48
|
|
|
47
49
|
# Waiting-for-user events → write _waiting.json marker
|
|
48
50
|
if [ "$EVENT" = "PermissionRequest" ] || { [ "$EVENT" = "PreToolUse" ] && [ "$TOOL_NAME" = "AskUserQuestion" ]; }; then
|
|
49
|
-
DIR="$
|
|
51
|
+
DIR="$CCK_ACTIVITY/$SESSION_ID"
|
|
50
52
|
mkdir -p "$DIR"
|
|
51
53
|
KIND="permission"
|
|
52
54
|
[ "$EVENT" = "PreToolUse" ] && KIND="question"
|
|
@@ -63,7 +65,7 @@ fi
|
|
|
63
65
|
|
|
64
66
|
# TeammateIdle has no agent_id — resolve via name→id mapping file
|
|
65
67
|
if [ "$EVENT" = "TeammateIdle" ] && [ -z "$AGENT_ID" ] && [ -n "$TEAMMATE_NAME" ]; then
|
|
66
|
-
DIR="$
|
|
68
|
+
DIR="$CCK_ACTIVITY/$SESSION_ID"
|
|
67
69
|
MAP_FILE="$DIR/_name-${TEAMMATE_NAME}.id"
|
|
68
70
|
[ ! -f "$MAP_FILE" ] && exit 0
|
|
69
71
|
AGENT_ID=$(cat "$MAP_FILE")
|
|
@@ -83,7 +85,7 @@ fi
|
|
|
83
85
|
|
|
84
86
|
[ -z "$AGENT_ID" ] && exit 0
|
|
85
87
|
|
|
86
|
-
DIR="$
|
|
88
|
+
DIR="$CCK_ACTIVITY/$SESSION_ID"
|
|
87
89
|
FILE="$DIR/$AGENT_ID.json"
|
|
88
90
|
|
|
89
91
|
# On Start: skip if no type (internal agents like AskUserQuestion)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# Statusline spy: writes raw context data for kanban dashboard, passes input through
|
|
3
|
-
# Layout: ~/.claude/context-status/{sessionId}.json
|
|
3
|
+
# Layout: ~/.claude/.cck/context-status/{sessionId}.json
|
|
4
4
|
#
|
|
5
5
|
# Usage: pipe before your statusline command:
|
|
6
6
|
# "command": "~/.claude/hooks/context-status.sh | npx -y ccstatusline@latest"
|
|
@@ -9,7 +9,7 @@ INPUT=$(cat)
|
|
|
9
9
|
|
|
10
10
|
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // ""')
|
|
11
11
|
if [ -n "$SESSION_ID" ]; then
|
|
12
|
-
DIR="$HOME/.claude/context-status"
|
|
12
|
+
DIR="$HOME/.claude/.cck/context-status"
|
|
13
13
|
mkdir -p "$DIR"
|
|
14
14
|
echo "$INPUT" > "$DIR/$SESSION_ID.json"
|
|
15
15
|
fi
|
package/public/app.js
CHANGED
|
@@ -4825,6 +4825,9 @@ function showInfoModal(session, teamConfig, tasks, planContent) {
|
|
|
4825
4825
|
const projectName = session.project.split(/[/\\]/).pop();
|
|
4826
4826
|
infoRows.push(['Project', projectName, { openPath: session.projectDir }]);
|
|
4827
4827
|
infoRows.push(['Path', session.project, { openPath: session.project }]);
|
|
4828
|
+
if (session.cwd && session.cwd !== session.project) {
|
|
4829
|
+
infoRows.push(['CWD', session.cwd, { openPath: session.cwd }]);
|
|
4830
|
+
}
|
|
4828
4831
|
if (session.gitBranch) {
|
|
4829
4832
|
infoRows.push(['Branch', session.gitBranch]);
|
|
4830
4833
|
}
|
package/server.js
CHANGED
|
@@ -64,8 +64,9 @@ const TASKS_DIR = path.join(CLAUDE_DIR, 'tasks');
|
|
|
64
64
|
const PROJECTS_DIR = path.join(CLAUDE_DIR, 'projects');
|
|
65
65
|
const TEAMS_DIR = path.join(CLAUDE_DIR, 'teams');
|
|
66
66
|
const PLANS_DIR = path.join(CLAUDE_DIR, 'plans');
|
|
67
|
-
const
|
|
68
|
-
const
|
|
67
|
+
const CCK_DIR = path.join(CLAUDE_DIR, '.cck');
|
|
68
|
+
const AGENT_ACTIVITY_DIR = path.join(CCK_DIR, 'agent-activity');
|
|
69
|
+
const CONTEXT_STATUS_DIR = path.join(CCK_DIR, 'context-status');
|
|
69
70
|
|
|
70
71
|
const PERMISSION_TTL_MS = 1800000;
|
|
71
72
|
const AGENT_TTL_MS = 3600000;
|
|
@@ -359,6 +360,20 @@ function loadSessionMetadata() {
|
|
|
359
360
|
const files = readdirSync(projectPath).filter(f => f.endsWith('.jsonl'));
|
|
360
361
|
const sessionIds = [];
|
|
361
362
|
|
|
363
|
+
// Read sessions-index.json first for canonical projectPath
|
|
364
|
+
let indexProjectPath = null;
|
|
365
|
+
const indexPath = path.join(projectPath, 'sessions-index.json');
|
|
366
|
+
let indexEntries = [];
|
|
367
|
+
if (existsSync(indexPath)) {
|
|
368
|
+
try {
|
|
369
|
+
const indexData = JSON.parse(readFileSync(indexPath, 'utf8'));
|
|
370
|
+
indexEntries = indexData.entries || [];
|
|
371
|
+
for (const entry of indexEntries) {
|
|
372
|
+
if (entry.projectPath) { indexProjectPath = entry.projectPath; break; }
|
|
373
|
+
}
|
|
374
|
+
} catch (e) {}
|
|
375
|
+
}
|
|
376
|
+
|
|
362
377
|
// First pass: read all JSONL files
|
|
363
378
|
let resolvedProjectPath = null;
|
|
364
379
|
for (const file of files) {
|
|
@@ -372,7 +387,8 @@ function loadSessionMetadata() {
|
|
|
372
387
|
|
|
373
388
|
metadata[sessionId] = {
|
|
374
389
|
slug: sessionInfo.slug,
|
|
375
|
-
project: sessionInfo.projectPath || null,
|
|
390
|
+
project: indexProjectPath || sessionInfo.projectPath || null,
|
|
391
|
+
cwd: sessionInfo.cwd || null,
|
|
376
392
|
gitBranch: sessionInfo.gitBranch || null,
|
|
377
393
|
customTitle: sessionInfo.customTitle || null,
|
|
378
394
|
jsonlPath: jsonlPath
|
|
@@ -381,38 +397,30 @@ function loadSessionMetadata() {
|
|
|
381
397
|
}
|
|
382
398
|
|
|
383
399
|
// Second pass: fill in missing project paths from siblings
|
|
384
|
-
|
|
400
|
+
const canonicalProject = indexProjectPath || resolvedProjectPath;
|
|
401
|
+
if (canonicalProject) {
|
|
385
402
|
for (const sid of sessionIds) {
|
|
386
403
|
if (!metadata[sid].project) {
|
|
387
|
-
metadata[sid].project =
|
|
404
|
+
metadata[sid].project = canonicalProject;
|
|
388
405
|
}
|
|
389
406
|
}
|
|
390
407
|
}
|
|
391
408
|
|
|
392
|
-
//
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
metadata[entry.sessionId] = {
|
|
403
|
-
slug: null,
|
|
404
|
-
project: entry.projectPath || null,
|
|
405
|
-
jsonlPath: null
|
|
406
|
-
};
|
|
407
|
-
}
|
|
408
|
-
metadata[entry.sessionId].description = entry.description || null;
|
|
409
|
-
if (entry.gitBranch) metadata[entry.sessionId].gitBranch = entry.gitBranch;
|
|
410
|
-
if (entry.customTitle) metadata[entry.sessionId].customTitle = entry.customTitle;
|
|
411
|
-
metadata[entry.sessionId].created = entry.created || null;
|
|
412
|
-
}
|
|
409
|
+
// Apply index metadata (descriptions, custom titles, etc.)
|
|
410
|
+
for (const entry of indexEntries) {
|
|
411
|
+
if (entry.sessionId) {
|
|
412
|
+
if (!metadata[entry.sessionId]) {
|
|
413
|
+
metadata[entry.sessionId] = {
|
|
414
|
+
slug: null,
|
|
415
|
+
project: indexProjectPath || entry.projectPath || null,
|
|
416
|
+
cwd: null,
|
|
417
|
+
jsonlPath: null
|
|
418
|
+
};
|
|
413
419
|
}
|
|
414
|
-
|
|
415
|
-
|
|
420
|
+
metadata[entry.sessionId].description = entry.description || null;
|
|
421
|
+
if (entry.gitBranch) metadata[entry.sessionId].gitBranch = entry.gitBranch;
|
|
422
|
+
if (entry.customTitle) metadata[entry.sessionId].customTitle = entry.customTitle;
|
|
423
|
+
metadata[entry.sessionId].created = entry.created || null;
|
|
416
424
|
}
|
|
417
425
|
}
|
|
418
426
|
}
|
|
@@ -480,6 +488,7 @@ function buildSessionObject(id, meta, overrides = {}) {
|
|
|
480
488
|
name: getSessionDisplayName(id, meta),
|
|
481
489
|
slug: meta.slug || null,
|
|
482
490
|
project: meta.project || null,
|
|
491
|
+
cwd: meta.cwd || null,
|
|
483
492
|
description: meta.description || null,
|
|
484
493
|
gitBranch: meta.gitBranch || null,
|
|
485
494
|
customTitle: meta.customTitle || null,
|