@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 +90 -8
- package/hooks/hooks.json +3 -3
- package/package.json +1 -1
- package/scripts/auto-allow.sh +25 -0
- package/scripts/auto-setup.sh +68 -0
- package/skills/set-topic/SKILL.md +1 -1
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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.
|
|
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" |
|
|
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
|