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 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 HOOK_SCRIPT_DEST = path.join(HOOKS_DIR, 'agent-spy.sh');
13
- const HOOK_SCRIPT_SRC = path.join(__dirname, 'hooks', 'agent-spy.sh');
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')} — Agent Log hook installer\n`);
51
+ console.log(`\n ${bold('claude-code-kanban')} — Plugin & StatusLine installer\n`);
48
52
 
49
- // 1. Check bash
50
- process.stdout.write(' Checking bash... ');
51
- try {
52
- const bashPath = execSync('which bash', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
53
- console.log(green(`✓ found (${bashPath})`));
54
- } catch {
55
- const shell = process.env.SHELL || process.env.BASH || '';
56
- if (shell.includes('bash')) {
57
- console.log(green(`✓ found via $SHELL (${shell})`));
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
- try {
72
- const ver = execSync('jq --version', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
73
- console.log(green(`✓ found (${ver})`));
74
- } catch {
75
- console.log(yellow('⚠ not found — hook script requires jq for JSON parsing'));
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
- async function installScript(label, src, dest) {
79
- console.log(`\n ${label}: ${dim(dest)}`);
80
- if (fs.existsSync(dest)) {
81
- const existing = fs.readFileSync(dest, 'utf8');
82
- const bundled = fs.readFileSync(src, 'utf8');
83
- if (existing === bundled) {
84
- console.log(` ${green('✓')} Up to date`);
85
- return true;
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
- // 4. Settings.json
113
- console.log(`\n Settings: ${dim(SETTINGS_PATH)}`);
114
- let settings;
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
- settings = {};
87
+ console.log(` ${red('✗')} Plugin install failed: ${inst.error}`);
120
88
  }
121
- } catch (e) {
122
- console.log(` ${red('')} Malformed JSON in settings.json — aborting settings update`);
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
- let settingsUpdated = false;
140
- if (needed.length === 0) {
141
- console.log(` ${green('✓')} Already configured`);
142
- settingsUpdated = true;
143
- } else {
144
- console.log(` Adding hooks for: ${needed.map(n => n.matcher ? `${n.event}:${n.matcher}` : n.event).join(', ')}`);
145
- if (await prompt(` Update settings? [Y/n] `)) {
146
- for (const { event, matcher } of needed) {
147
- settings.hooks[event].push({
148
- matcher,
149
- hooks: [{ type: 'command', command: HOOK_COMMAND, timeout: 5 }]
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
- // 5. StatusLine setup (separate approval)
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(hookInstalled, settingsUpdated);
156
+ printSummary();
194
157
  }
195
158
 
196
- function printSummary(hookOk, settingsOk) {
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')} — Agent Log hook uninstaller\n`);
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
- // 1. Remove hook entries from settings.json
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 removed = 0;
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, restore downstream command if any
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
- fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + '\n');
244
- if (removed > 0) {
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) result.projectPath = 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": "2.3.2",
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
- "hooks/agent-spy.sh",
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,6 @@
1
+ {
2
+ "name": "claude-code-kanban",
3
+ "version": "1.0.0",
4
+ "description": "Agent activity tracking for claude-code-kanban dashboard",
5
+ "hooks": "./hooks/hooks.json"
6
+ }
@@ -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="$HOME/.claude/agent-activity/_task-maps"
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="$HOME/.claude/agent-activity/$SESSION_ID/_waiting.json"
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="$HOME/.claude/agent-activity/$SESSION_ID"
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="$HOME/.claude/agent-activity/$SESSION_ID"
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="$HOME/.claude/agent-activity/$SESSION_ID"
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 AGENT_ACTIVITY_DIR = path.join(CLAUDE_DIR, 'agent-activity');
68
- const CONTEXT_STATUS_DIR = path.join(CLAUDE_DIR, 'context-status');
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
- if (resolvedProjectPath) {
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 = resolvedProjectPath;
404
+ metadata[sid].project = canonicalProject;
388
405
  }
389
406
  }
390
407
  }
391
408
 
392
- // Also check sessions-index.json for custom names (if /rename was used)
393
- const indexPath = path.join(projectPath, 'sessions-index.json');
394
- if (existsSync(indexPath)) {
395
- try {
396
- const indexData = JSON.parse(readFileSync(indexPath, 'utf8'));
397
- const entries = indexData.entries || [];
398
-
399
- for (const entry of entries) {
400
- if (entry.sessionId) {
401
- if (!metadata[entry.sessionId]) {
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
- } catch (e) {
415
- // Skip invalid index files
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,