@alexismunozdev/claude-session-topics 2.1.1 → 2.3.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/bin/install.js CHANGED
@@ -33,6 +33,7 @@ const HOME = os.homedir();
33
33
  const TOPICS_DIR = path.join(HOME, '.claude', 'session-topics');
34
34
  const DEST_STATUSLINE = path.join(TOPICS_DIR, 'statusline.sh');
35
35
  const DEST_WRAPPER = path.join(TOPICS_DIR, 'wrapper-statusline.sh');
36
+ const DEST_HOOK_SCRIPT = path.join(TOPICS_DIR, 'auto-topic-hook.sh');
36
37
  const ORIG_CMD_FILE = path.join(TOPICS_DIR, '.original-statusline-cmd');
37
38
  const COLOR_CONFIG = path.join(TOPICS_DIR, '.color-config');
38
39
  const SKILLS_DIR = path.join(HOME, '.claude', 'skills');
@@ -41,12 +42,14 @@ const SETTINGS_FILE = path.join(HOME, '.claude', 'settings.json');
41
42
  // ─── Source paths (relative to this script) ──────────────────────────────────
42
43
 
43
44
  const SRC_STATUSLINE = path.join(__dirname, '..', 'scripts', 'statusline.sh');
45
+ const SRC_HOOK_SCRIPT = path.join(__dirname, '..', 'scripts', 'auto-topic-hook.sh');
44
46
  const SRC_SKILLS = path.join(__dirname, '..', 'skills');
45
47
 
46
48
  // ─── The statusline command that settings.json will reference ────────────────
47
49
 
48
50
  const STATUSLINE_CMD = `bash "$HOME/.claude/session-topics/statusline.sh"`;
49
51
  const WRAPPER_CMD = `bash "$HOME/.claude/session-topics/wrapper-statusline.sh"`;
52
+ const STOP_HOOK_CMD = `bash "$HOME/.claude/session-topics/auto-topic-hook.sh" || true`;
50
53
 
51
54
  // ─── Permission rule ─────────────────────────────────────────────────────────
52
55
 
@@ -196,6 +199,7 @@ ${BOLD}What it does:${RESET}
196
199
  - Copies statusline.sh to ~/.claude/session-topics/
197
200
  - Configures statusLine in ~/.claude/settings.json
198
201
  - Adds Bash permission for session-topics commands
202
+ - Registers Stop hook for automatic topic detection
199
203
  - Installs auto-topic and set-topic skills to ~/.claude/skills/
200
204
 
201
205
  ${BOLD}After install:${RESET}
@@ -234,7 +238,17 @@ function install(color) {
234
238
  fs.chmodSync(DEST_STATUSLINE, 0o755);
235
239
  ok('Copied statusline.sh');
236
240
 
237
- // ── Step 4: Configure statusline in settings.json ────────────────────
241
+ // ── Step 4: Copy auto-topic hook script ─────────────────────────────
242
+
243
+ if (!fs.existsSync(SRC_HOOK_SCRIPT)) {
244
+ err(`Source hook script not found: ${SRC_HOOK_SCRIPT}`);
245
+ process.exit(1);
246
+ }
247
+ fs.copyFileSync(SRC_HOOK_SCRIPT, DEST_HOOK_SCRIPT);
248
+ fs.chmodSync(DEST_HOOK_SCRIPT, 0o755);
249
+ ok('Copied auto-topic-hook.sh');
250
+
251
+ // ── Step 5: Configure statusline in settings.json ────────────────────
238
252
 
239
253
  const settings = readSettings();
240
254
  const statusLineCase = determineStatusLineCase(settings);
@@ -286,7 +300,7 @@ function install(color) {
286
300
  }
287
301
  }
288
302
 
289
- // ── Step 5: Add permission ───────────────────────────────────────────
303
+ // ── Step 6: Add permission ───────────────────────────────────────────
290
304
 
291
305
  if (!settings.permissions || typeof settings.permissions !== 'object' || Array.isArray(settings.permissions)) {
292
306
  settings.permissions = {};
@@ -302,7 +316,46 @@ function install(color) {
302
316
  ok('Permission already present');
303
317
  }
304
318
 
305
- // ── Step 6: Copy skills ──────────────────────────────────────────────
319
+ // ── Step 7: Register Stop hook ──────────────────────────────────────
320
+
321
+ if (!settings.hooks || typeof settings.hooks !== 'object' || Array.isArray(settings.hooks)) {
322
+ settings.hooks = {};
323
+ }
324
+ if (!Array.isArray(settings.hooks.Stop)) {
325
+ settings.hooks.Stop = [];
326
+ }
327
+
328
+ // Find existing session-topics hook entry
329
+ let hookFound = false;
330
+ for (const entry of settings.hooks.Stop) {
331
+ if (entry && Array.isArray(entry.hooks)) {
332
+ for (const h of entry.hooks) {
333
+ if (h && typeof h.command === 'string' && h.command.includes('session-topics')) {
334
+ h.command = STOP_HOOK_CMD;
335
+ hookFound = true;
336
+ }
337
+ }
338
+ }
339
+ }
340
+
341
+ if (!hookFound) {
342
+ settings.hooks.Stop.push({
343
+ hooks: [
344
+ {
345
+ type: 'command',
346
+ command: STOP_HOOK_CMD,
347
+ },
348
+ ],
349
+ });
350
+ }
351
+ writeSettings(settings);
352
+ if (hookFound) {
353
+ ok('Updated Stop hook for auto-topic detection');
354
+ } else {
355
+ ok('Registered Stop hook for auto-topic detection');
356
+ }
357
+
358
+ // ── Step 8: Copy skills ──────────────────────────────────────────────
306
359
 
307
360
  const skillsToCopy = ['auto-topic', 'set-topic'];
308
361
  for (const skill of skillsToCopy) {
@@ -316,20 +369,21 @@ function install(color) {
316
369
  }
317
370
  }
318
371
 
319
- // ── Step 7: Configure color ──────────────────────────────────────────
372
+ // ── Step 9: Configure color ──────────────────────────────────────────
320
373
 
321
374
  if (color) {
322
375
  fs.writeFileSync(COLOR_CONFIG, color, { encoding: 'utf8', mode: 0o600 });
323
376
  ok(`Topic color set to: ${BOLD}${color}${RESET}`);
324
377
  }
325
378
 
326
- // ── Step 8: Summary ──────────────────────────────────────────────────
379
+ // ── Step 10: Summary ─────────────────────────────────────────────────
327
380
 
328
381
  console.log('');
329
382
  heading('Installation complete');
330
383
  console.log(` ${DIM}Statusline:${RESET} ~/.claude/session-topics/statusline.sh`);
331
384
  console.log(` ${DIM}Skills:${RESET} ~/.claude/skills/auto-topic/`);
332
385
  console.log(` ~/.claude/skills/set-topic/`);
386
+ console.log(` ${DIM}Hook:${RESET} Stop → auto-topic-hook.sh`);
333
387
  console.log(` ${DIM}Settings:${RESET} ~/.claude/settings.json`);
334
388
  if (color) {
335
389
  console.log(` ${DIM}Color:${RESET} ${color}`);
@@ -395,7 +449,7 @@ function uninstall() {
395
449
 
396
450
  // ── Step 2: Delete scripts ───────────────────────────────────────────
397
451
 
398
- const filesToDelete = [DEST_STATUSLINE, DEST_WRAPPER, ORIG_CMD_FILE];
452
+ const filesToDelete = [DEST_STATUSLINE, DEST_WRAPPER, DEST_HOOK_SCRIPT, ORIG_CMD_FILE];
399
453
  for (const file of filesToDelete) {
400
454
  if (fs.existsSync(file)) {
401
455
  fs.unlinkSync(file);
@@ -421,7 +475,35 @@ function uninstall() {
421
475
  }
422
476
  }
423
477
 
424
- // ── Step 4: Delete skills ────────────────────────────────────────────
478
+ // ── Step 4: Remove Stop hook ───────────────────────────────────────
479
+
480
+ if (
481
+ settings.hooks &&
482
+ typeof settings.hooks === 'object' &&
483
+ Array.isArray(settings.hooks.Stop)
484
+ ) {
485
+ const beforeLen = settings.hooks.Stop.length;
486
+ settings.hooks.Stop = settings.hooks.Stop.filter((entry) => {
487
+ if (entry && Array.isArray(entry.hooks)) {
488
+ return !entry.hooks.some(
489
+ (h) => h && typeof h.command === 'string' && h.command.includes('session-topics')
490
+ );
491
+ }
492
+ return true;
493
+ });
494
+ if (settings.hooks.Stop.length < beforeLen) {
495
+ if (settings.hooks.Stop.length === 0) {
496
+ delete settings.hooks.Stop;
497
+ }
498
+ if (Object.keys(settings.hooks).length === 0) {
499
+ delete settings.hooks;
500
+ }
501
+ writeSettings(settings);
502
+ ok('Removed Stop hook');
503
+ }
504
+ }
505
+
506
+ // ── Step 5: Delete skills ────────────────────────────────────────────
425
507
 
426
508
  const skillsToDelete = ['auto-topic', 'set-topic'];
427
509
  for (const skill of skillsToDelete) {
@@ -432,7 +514,7 @@ function uninstall() {
432
514
  }
433
515
  }
434
516
 
435
- // ── Step 5: Preserve data ────────────────────────────────────────────
517
+ // ── Step 6: Preserve data ────────────────────────────────────────────
436
518
 
437
519
  info('Preserved topic data in ~/.claude/session-topics/ (topic files + color config)');
438
520
 
package/hooks/hooks.json CHANGED
@@ -9,7 +9,7 @@
9
9
  },
10
10
  {
11
11
  "type": "command",
12
- "command": "S=$(ls \"$HOME/.claude/plugins/cache/claude-session-topics/claude-session-topics\"/*/scripts/auto-setup.sh 2>/dev/null | tail -1); [ -n \"$S\" ] && bash \"$S\" || true"
12
+ "command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/auto-setup.sh\" || true"
13
13
  }
14
14
  ]
15
15
  }
@@ -20,7 +20,7 @@
20
20
  "hooks": [
21
21
  {
22
22
  "type": "command",
23
- "command": "S=$(ls \"$HOME/.claude/plugins/cache/claude-session-topics/claude-session-topics\"/*/scripts/auto-allow.sh 2>/dev/null | tail -1); [ -n \"$S\" ] && bash \"$S\" || true"
23
+ "command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/auto-allow.sh\" || true"
24
24
  }
25
25
  ]
26
26
  }
@@ -30,7 +30,7 @@
30
30
  "hooks": [
31
31
  {
32
32
  "type": "command",
33
- "command": "S=$(ls \"$HOME/.claude/plugins/cache/claude-session-topics/claude-session-topics\"/*/scripts/auto-topic-hook.sh 2>/dev/null | tail -1); [ -n \"$S\" ] && bash \"$S\" || true"
33
+ "command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/auto-topic-hook.sh\" || true"
34
34
  }
35
35
  ]
36
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alexismunozdev/claude-session-topics",
3
- "version": "2.1.1",
3
+ "version": "2.3.0",
4
4
  "description": "Session topics for Claude Code — auto-set and display a topic in the statusline, change anytime with /set-topic",
5
5
  "bin": {
6
6
  "claude-session-topics": "bin/install.js"
@@ -0,0 +1,25 @@
1
+ #!/bin/bash
2
+ # Auto-approve Bash commands containing "session-topics" (PermissionRequest hook)
3
+ # Writes the allow rule to userSettings so future requests skip the prompt
4
+
5
+ input=$(cat)
6
+ CMD=$(echo "$input" | jq -r '.tool_input.command // ""')
7
+
8
+ if echo "$CMD" | grep -q 'session-topics'; then
9
+ cat <<'EOF'
10
+ {
11
+ "hookSpecificOutput": {
12
+ "hookEventName": "PermissionRequest",
13
+ "permissionDecision": "allow",
14
+ "updatedPermissions": [
15
+ {
16
+ "type": "addRules",
17
+ "rules": [{"toolName": "Bash", "ruleContent": "*session-topics*"}],
18
+ "behavior": "allow",
19
+ "destination": "userSettings"
20
+ }
21
+ ]
22
+ }
23
+ }
24
+ EOF
25
+ fi
@@ -0,0 +1,68 @@
1
+ #!/bin/bash
2
+ # Auto-configure statusline on first run (SessionStart hook)
3
+ # Handles two cases:
4
+ # 1. No statusline configured -> set plugin's statusline.sh directly
5
+ # 2. Existing custom statusline -> generate a wrapper that prepends topic to original output
6
+
7
+ SETTINGS="$HOME/.claude/settings.json"
8
+ TOPIC_DIR="$HOME/.claude/session-topics"
9
+ WRAPPER="$TOPIC_DIR/wrapper-statusline.sh"
10
+ STABLE_SL="$TOPIC_DIR/plugin-statusline.sh"
11
+ ORIG_CMD_FILE="$TOPIC_DIR/.original-statusline-cmd"
12
+
13
+ [ ! -f "$SETTINGS" ] && exit 0
14
+
15
+ # Find the plugin's statusline script via CLAUDE_PLUGIN_ROOT (set by hooks system)
16
+ PLUGIN_SL="${CLAUDE_PLUGIN_ROOT}/scripts/statusline.sh"
17
+ [ ! -f "$PLUGIN_SL" ] && exit 0
18
+
19
+ mkdir -p "$TOPIC_DIR"
20
+
21
+ # Always refresh the stable copy (keeps it up-to-date across plugin updates)
22
+ cp "$PLUGIN_SL" "$STABLE_SL"
23
+ chmod +x "$STABLE_SL"
24
+
25
+ CURRENT_CMD=$(jq -r '.statusLine.command // ""' "$SETTINGS" 2>/dev/null)
26
+
27
+ # Already integrated — skip (but the copy above still refreshes)
28
+ echo "$CURRENT_CMD" | grep -q 'session-topics' && exit 0
29
+
30
+ HAS_STATUSLINE=$(jq 'has("statusLine")' "$SETTINGS" 2>/dev/null)
31
+
32
+ if [ "$HAS_STATUSLINE" = "true" ] && [ -n "$CURRENT_CMD" ]; then
33
+ # Case 2: User has a custom statusline — generate wrapper
34
+ echo "$CURRENT_CMD" > "$ORIG_CMD_FILE"
35
+
36
+ cat > "$WRAPPER" << 'WRAPPER_EOF'
37
+ #!/bin/bash
38
+ input=$(cat)
39
+
40
+ # Run the plugin's topic statusline (stable copy refreshed each session)
41
+ TOPIC_OUTPUT=""
42
+ if [ -f "$HOME/.claude/session-topics/plugin-statusline.sh" ]; then
43
+ TOPIC_OUTPUT=$(echo "$input" | bash "$HOME/.claude/session-topics/plugin-statusline.sh" 2>/dev/null || echo "")
44
+ fi
45
+
46
+ # Run the user's original statusline command
47
+ ORIG_CMD=$(cat "$HOME/.claude/session-topics/.original-statusline-cmd" 2>/dev/null || echo "")
48
+ ORIG_OUTPUT=""
49
+ if [ -n "$ORIG_CMD" ]; then
50
+ ORIG_OUTPUT=$(echo "$input" | bash -c "$ORIG_CMD" 2>/dev/null || echo "")
51
+ fi
52
+
53
+ # Combine: topic | original
54
+ if [ -n "$TOPIC_OUTPUT" ] && [ -n "$ORIG_OUTPUT" ]; then
55
+ echo -e "${TOPIC_OUTPUT} | ${ORIG_OUTPUT}"
56
+ elif [ -n "$TOPIC_OUTPUT" ]; then
57
+ echo -e "${TOPIC_OUTPUT}"
58
+ elif [ -n "$ORIG_OUTPUT" ]; then
59
+ echo -e "${ORIG_OUTPUT}"
60
+ fi
61
+ WRAPPER_EOF
62
+ chmod +x "$WRAPPER"
63
+
64
+ jq --arg cmd "bash \"$WRAPPER\"" '.statusLine.command = $cmd' "$SETTINGS" > "${SETTINGS}.tmp" && mv "${SETTINGS}.tmp" "$SETTINGS"
65
+ else
66
+ # Case 1: No statusline at all — use stable copy directly
67
+ jq --arg cmd "bash \"$STABLE_SL\"" '.statusLine = {"type": "command", "command": $cmd}' "$SETTINGS" > "${SETTINGS}.tmp" && mv "${SETTINGS}.tmp" "$SETTINGS"
68
+ fi
@@ -28,7 +28,7 @@ if [ -z "$SESSION_ID" ]; then
28
28
  echo "Error: No active session found. The statusline must run at least once before setting a topic."
29
29
  exit 1
30
30
  fi
31
- TOPIC=$(printf '%s' "$ARGUMENTS" | tr -cd 'a-zA-Z0-9 .,:!?'"'"'-' | cut -c1-100)
31
+ TOPIC=$(printf '%s' "$ARGUMENTS" | sed "s/[^a-zA-Z0-9àáâãäåèéêëìíîïòóôõöùúûüýÿñçÀÁÂÃÄÅÈÉÊËÌÍÎÏÒÓÔÕÖÙÚÛÜÝÑÇ .,:!?'-]//g" | cut -c1-100)
32
32
  if [ -z "$TOPIC" ]; then
33
33
  echo "Error: Topic text is empty after sanitization."
34
34
  exit 1