@alexismunozdev/claude-session-topics 2.0.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/LICENSE +21 -0
- package/README.md +98 -0
- package/bin/install.js +431 -0
- package/package.json +30 -0
- package/scripts/statusline.sh +85 -0
- package/skills/auto-topic/SKILL.md +72 -0
- package/skills/set-topic/SKILL.md +35 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alexis Muñoz
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# claude-session-topics
|
|
2
|
+
|
|
3
|
+
Session topics for Claude Code. Auto-detect and display a topic in the statusline, change anytime with `/set-topic`.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx @alexismunozdev/claude-session-topics
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## With color
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx @alexismunozdev/claude-session-topics --color cyan
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Supported colors: `red`, `green`, `yellow`, `blue`, `magenta` (default), `cyan`, `white`, `orange`, `grey`/`gray`. Raw ANSI codes are also accepted (e.g., `38;5;208`).
|
|
20
|
+
|
|
21
|
+
## What it does
|
|
22
|
+
|
|
23
|
+
- Auto-detects a session topic from context on the first prompt
|
|
24
|
+
- Shows the topic in the Claude Code statusline (`◆ Topic`)
|
|
25
|
+
- Change the topic anytime with `/set-topic`
|
|
26
|
+
- Composes with existing statusline plugins (doesn't overwrite)
|
|
27
|
+
|
|
28
|
+
## What the installer configures
|
|
29
|
+
|
|
30
|
+
1. Copies the statusline script to `~/.claude/session-topics/`
|
|
31
|
+
2. Configures `statusLine` in `~/.claude/settings.json`
|
|
32
|
+
3. Adds bash permission for the script
|
|
33
|
+
4. Installs `auto-topic` and `set-topic` skills to `~/.claude/skills/`
|
|
34
|
+
5. If you already have a statusline, creates a wrapper that shows both
|
|
35
|
+
|
|
36
|
+
## Requirements
|
|
37
|
+
|
|
38
|
+
- `jq`
|
|
39
|
+
- `bash`
|
|
40
|
+
- POSIX-compatible system (macOS, Linux)
|
|
41
|
+
|
|
42
|
+
## Customization
|
|
43
|
+
|
|
44
|
+
The default topic color is bold magenta. Three ways to change it:
|
|
45
|
+
|
|
46
|
+
- Re-run with `--color <name>`:
|
|
47
|
+
```bash
|
|
48
|
+
npx @alexismunozdev/claude-session-topics --color cyan
|
|
49
|
+
```
|
|
50
|
+
- Edit the config file directly:
|
|
51
|
+
```bash
|
|
52
|
+
echo "cyan" > ~/.claude/session-topics/.color-config
|
|
53
|
+
```
|
|
54
|
+
- Set the `CLAUDE_TOPIC_COLOR` environment variable:
|
|
55
|
+
```bash
|
|
56
|
+
export CLAUDE_TOPIC_COLOR="cyan"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Usage
|
|
60
|
+
|
|
61
|
+
### Auto-topic (automatic)
|
|
62
|
+
|
|
63
|
+
Starts automatically on session start. Claude reads your first message and sets a short topic (2-4 words) summarizing the task.
|
|
64
|
+
|
|
65
|
+
### /set-topic (manual)
|
|
66
|
+
|
|
67
|
+
Change the topic at any time:
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
/set-topic Fix Login Bug
|
|
71
|
+
/set-topic API Redesign
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## How it works
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
Session starts
|
|
78
|
+
|
|
|
79
|
+
auto-topic skill fires on first message
|
|
80
|
+
|
|
|
81
|
+
Claude writes topic to ~/.claude/session-topics/${SESSION_ID}
|
|
82
|
+
|
|
|
83
|
+
Statusline script reads the topic file
|
|
84
|
+
|
|
|
85
|
+
Displays: ◆ Topic
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The statusline script receives the session ID via stdin JSON, reads the corresponding topic file, and renders it with ANSI color codes.
|
|
89
|
+
|
|
90
|
+
## Uninstall
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
npx @alexismunozdev/claude-session-topics --uninstall
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## License
|
|
97
|
+
|
|
98
|
+
MIT
|
package/bin/install.js
ADDED
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// claude-session-topics — npx installer
|
|
4
|
+
// Installs statusline script, skills, and configures settings.json
|
|
5
|
+
// Zero runtime dependency on npm after installation.
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const os = require('os');
|
|
12
|
+
const { execSync } = require('child_process');
|
|
13
|
+
|
|
14
|
+
// ─── ANSI helpers ────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
const GREEN = '\x1b[32m';
|
|
17
|
+
const YELLOW = '\x1b[33m';
|
|
18
|
+
const RED = '\x1b[31m';
|
|
19
|
+
const CYAN = '\x1b[36m';
|
|
20
|
+
const BOLD = '\x1b[1m';
|
|
21
|
+
const DIM = '\x1b[2m';
|
|
22
|
+
const RESET = '\x1b[0m';
|
|
23
|
+
|
|
24
|
+
const ok = (msg) => console.log(` ${GREEN}\u2713${RESET} ${msg}`);
|
|
25
|
+
const warn = (msg) => console.log(` ${YELLOW}\u26A0${RESET} ${msg}`);
|
|
26
|
+
const err = (msg) => console.error(` ${RED}\u2717${RESET} ${msg}`);
|
|
27
|
+
const info = (msg) => console.log(` ${DIM}${msg}${RESET}`);
|
|
28
|
+
const heading = (msg) => console.log(`\n${BOLD}${CYAN}${msg}${RESET}\n`);
|
|
29
|
+
|
|
30
|
+
// ─── Destination paths (fixed — never change) ───────────────────────────────
|
|
31
|
+
|
|
32
|
+
const HOME = os.homedir();
|
|
33
|
+
const TOPICS_DIR = path.join(HOME, '.claude', 'session-topics');
|
|
34
|
+
const DEST_STATUSLINE = path.join(TOPICS_DIR, 'statusline.sh');
|
|
35
|
+
const DEST_WRAPPER = path.join(TOPICS_DIR, 'wrapper-statusline.sh');
|
|
36
|
+
const ORIG_CMD_FILE = path.join(TOPICS_DIR, '.original-statusline-cmd');
|
|
37
|
+
const COLOR_CONFIG = path.join(TOPICS_DIR, '.color-config');
|
|
38
|
+
const SKILLS_DIR = path.join(HOME, '.claude', 'skills');
|
|
39
|
+
const SETTINGS_FILE = path.join(HOME, '.claude', 'settings.json');
|
|
40
|
+
|
|
41
|
+
// ─── Source paths (relative to this script) ──────────────────────────────────
|
|
42
|
+
|
|
43
|
+
const SRC_STATUSLINE = path.join(__dirname, '..', 'scripts', 'statusline.sh');
|
|
44
|
+
const SRC_SKILLS = path.join(__dirname, '..', 'skills');
|
|
45
|
+
|
|
46
|
+
// ─── The statusline command that settings.json will reference ────────────────
|
|
47
|
+
|
|
48
|
+
const STATUSLINE_CMD = `bash "$HOME/.claude/session-topics/statusline.sh"`;
|
|
49
|
+
const WRAPPER_CMD = `bash "$HOME/.claude/session-topics/wrapper-statusline.sh"`;
|
|
50
|
+
|
|
51
|
+
// ─── Permission rule ─────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
const PERMISSION_RULE = 'Bash(*session-topics*)';
|
|
54
|
+
|
|
55
|
+
// ─── Wrapper script content ──────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
const WRAPPER_SCRIPT = `#!/bin/bash
|
|
58
|
+
input=$(cat)
|
|
59
|
+
TOPIC_OUTPUT=$(echo "$input" | bash "$HOME/.claude/session-topics/statusline.sh" 2>/dev/null || echo "")
|
|
60
|
+
ORIG_CMD=$(cat "$HOME/.claude/session-topics/.original-statusline-cmd" 2>/dev/null || echo "")
|
|
61
|
+
ORIG_OUTPUT=""
|
|
62
|
+
[ -n "$ORIG_CMD" ] && ORIG_OUTPUT=$(echo "$input" | eval "$ORIG_CMD" 2>/dev/null || echo "")
|
|
63
|
+
if [ -n "$TOPIC_OUTPUT" ] && [ -n "$ORIG_OUTPUT" ]; then
|
|
64
|
+
echo -e "\${TOPIC_OUTPUT} | \${ORIG_OUTPUT}"
|
|
65
|
+
elif [ -n "$TOPIC_OUTPUT" ]; then
|
|
66
|
+
echo -e "\${TOPIC_OUTPUT}"
|
|
67
|
+
elif [ -n "$ORIG_OUTPUT" ]; then
|
|
68
|
+
echo -e "\${ORIG_OUTPUT}"
|
|
69
|
+
fi
|
|
70
|
+
`;
|
|
71
|
+
|
|
72
|
+
// ─── Utility functions ───────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
function readSettings() {
|
|
75
|
+
try {
|
|
76
|
+
const raw = fs.readFileSync(SETTINGS_FILE, 'utf8');
|
|
77
|
+
return JSON.parse(raw);
|
|
78
|
+
} catch {
|
|
79
|
+
return {};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function writeSettings(obj) {
|
|
84
|
+
const dir = path.dirname(SETTINGS_FILE);
|
|
85
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
86
|
+
fs.writeFileSync(SETTINGS_FILE, JSON.stringify(obj, null, 2) + '\n', 'utf8');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function copyDirRecursive(src, dest) {
|
|
90
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
91
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
92
|
+
const srcPath = path.join(src, entry.name);
|
|
93
|
+
const destPath = path.join(dest, entry.name);
|
|
94
|
+
if (entry.isDirectory()) {
|
|
95
|
+
copyDirRecursive(srcPath, destPath);
|
|
96
|
+
} else {
|
|
97
|
+
fs.copyFileSync(srcPath, destPath);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function hasJq() {
|
|
103
|
+
try {
|
|
104
|
+
execSync('which jq', { stdio: 'pipe' });
|
|
105
|
+
return true;
|
|
106
|
+
} catch {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ─── CLI argument parsing ────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
function parseArgs(argv) {
|
|
114
|
+
const args = argv.slice(2);
|
|
115
|
+
const result = { action: 'install', color: null };
|
|
116
|
+
|
|
117
|
+
for (let i = 0; i < args.length; i++) {
|
|
118
|
+
const arg = args[i];
|
|
119
|
+
if (arg === '--help' || arg === '-h') {
|
|
120
|
+
result.action = 'help';
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
if (arg === '--uninstall') {
|
|
124
|
+
result.action = 'uninstall';
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
if (arg === '--color') {
|
|
128
|
+
if (i + 1 < args.length) {
|
|
129
|
+
result.color = args[i + 1];
|
|
130
|
+
i++;
|
|
131
|
+
} else {
|
|
132
|
+
err('--color requires a value (e.g., --color cyan)');
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ─── Help ────────────────────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
function showHelp() {
|
|
144
|
+
console.log(`
|
|
145
|
+
${BOLD}claude-session-topics${RESET} — session topics for Claude Code
|
|
146
|
+
|
|
147
|
+
${BOLD}Usage:${RESET}
|
|
148
|
+
npx @alexismunozdev/claude-session-topics Install
|
|
149
|
+
npx @alexismunozdev/claude-session-topics --color cyan Install with color
|
|
150
|
+
npx @alexismunozdev/claude-session-topics --uninstall Uninstall
|
|
151
|
+
|
|
152
|
+
${BOLD}Options:${RESET}
|
|
153
|
+
--color <name> Set topic color (red, green, yellow, blue, magenta,
|
|
154
|
+
cyan, white, orange, grey). Default: magenta
|
|
155
|
+
--uninstall Remove scripts, settings, and skills (preserves topic data)
|
|
156
|
+
-h, --help Show this help
|
|
157
|
+
|
|
158
|
+
${BOLD}What it does:${RESET}
|
|
159
|
+
- Copies statusline.sh to ~/.claude/session-topics/
|
|
160
|
+
- Configures statusLine in ~/.claude/settings.json
|
|
161
|
+
- Adds Bash permission for session-topics commands
|
|
162
|
+
- Installs auto-topic and set-topic skills to ~/.claude/skills/
|
|
163
|
+
|
|
164
|
+
${BOLD}After install:${RESET}
|
|
165
|
+
The statusline shows the current topic automatically.
|
|
166
|
+
Use ${CYAN}/set-topic <text>${RESET} to change it manually.
|
|
167
|
+
`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ─── Install ─────────────────────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
function install(color) {
|
|
173
|
+
heading('Installing claude-session-topics');
|
|
174
|
+
|
|
175
|
+
// ── Step 1: Check deps ───────────────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
if (!hasJq()) {
|
|
178
|
+
err('jq is required but not found in PATH.');
|
|
179
|
+
console.log(`\n Install it: ${BOLD}brew install jq${RESET} (macOS)`);
|
|
180
|
+
console.log(` ${BOLD}sudo apt install jq${RESET} (Ubuntu/Debian)\n`);
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
ok('jq found');
|
|
184
|
+
|
|
185
|
+
// ── Step 2: Create dir ───────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
fs.mkdirSync(TOPICS_DIR, { recursive: true });
|
|
188
|
+
ok(`Created ${DIM}~/.claude/session-topics/${RESET}`);
|
|
189
|
+
|
|
190
|
+
// ── Step 3: Copy statusline ──────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
if (!fs.existsSync(SRC_STATUSLINE)) {
|
|
193
|
+
err(`Source statusline not found: ${SRC_STATUSLINE}`);
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
fs.copyFileSync(SRC_STATUSLINE, DEST_STATUSLINE);
|
|
197
|
+
fs.chmodSync(DEST_STATUSLINE, 0o755);
|
|
198
|
+
ok('Copied statusline.sh');
|
|
199
|
+
|
|
200
|
+
// ── Step 4: Configure statusline in settings.json ────────────────────
|
|
201
|
+
|
|
202
|
+
const settings = readSettings();
|
|
203
|
+
const statusLineCase = determineStatusLineCase(settings);
|
|
204
|
+
|
|
205
|
+
switch (statusLineCase) {
|
|
206
|
+
case 'A': {
|
|
207
|
+
// No statusLine — create fresh
|
|
208
|
+
settings.statusLine = {
|
|
209
|
+
type: 'command',
|
|
210
|
+
command: STATUSLINE_CMD,
|
|
211
|
+
};
|
|
212
|
+
writeSettings(settings);
|
|
213
|
+
ok('Configured statusLine in settings.json');
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
case 'B': {
|
|
217
|
+
// Already ours — just update the script (already copied above)
|
|
218
|
+
ok('statusLine already configured for session-topics (updated script)');
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
case 'C': {
|
|
222
|
+
// Another command exists — create wrapper
|
|
223
|
+
const origCmd = settings.statusLine.command;
|
|
224
|
+
|
|
225
|
+
// Backup original command
|
|
226
|
+
fs.writeFileSync(ORIG_CMD_FILE, origCmd, 'utf8');
|
|
227
|
+
info(`Backed up original statusLine command to .original-statusline-cmd`);
|
|
228
|
+
|
|
229
|
+
// Write wrapper
|
|
230
|
+
fs.writeFileSync(DEST_WRAPPER, WRAPPER_SCRIPT, 'utf8');
|
|
231
|
+
fs.chmodSync(DEST_WRAPPER, 0o755);
|
|
232
|
+
info('Created wrapper-statusline.sh');
|
|
233
|
+
|
|
234
|
+
// Update settings to use wrapper
|
|
235
|
+
settings.statusLine.command = WRAPPER_CMD;
|
|
236
|
+
writeSettings(settings);
|
|
237
|
+
ok('Configured statusLine wrapper (preserves your existing statusline)');
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
case 'D': {
|
|
241
|
+
// statusLine exists but no valid command — treat as case A
|
|
242
|
+
settings.statusLine = {
|
|
243
|
+
type: 'command',
|
|
244
|
+
command: STATUSLINE_CMD,
|
|
245
|
+
};
|
|
246
|
+
writeSettings(settings);
|
|
247
|
+
ok('Configured statusLine in settings.json (replaced invalid entry)');
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ── Step 5: Add permission ───────────────────────────────────────────
|
|
253
|
+
|
|
254
|
+
if (!settings.permissions || typeof settings.permissions !== 'object' || Array.isArray(settings.permissions)) {
|
|
255
|
+
settings.permissions = {};
|
|
256
|
+
}
|
|
257
|
+
if (!Array.isArray(settings.permissions.allow)) {
|
|
258
|
+
settings.permissions.allow = [];
|
|
259
|
+
}
|
|
260
|
+
if (!settings.permissions.allow.includes(PERMISSION_RULE)) {
|
|
261
|
+
settings.permissions.allow.push(PERMISSION_RULE);
|
|
262
|
+
writeSettings(settings);
|
|
263
|
+
ok(`Added permission: ${DIM}${PERMISSION_RULE}${RESET}`);
|
|
264
|
+
} else {
|
|
265
|
+
ok('Permission already present');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ── Step 6: Copy skills ──────────────────────────────────────────────
|
|
269
|
+
|
|
270
|
+
const skillsToCopy = ['auto-topic', 'set-topic'];
|
|
271
|
+
for (const skill of skillsToCopy) {
|
|
272
|
+
const srcSkill = path.join(SRC_SKILLS, skill);
|
|
273
|
+
const destSkill = path.join(SKILLS_DIR, skill);
|
|
274
|
+
if (fs.existsSync(srcSkill)) {
|
|
275
|
+
copyDirRecursive(srcSkill, destSkill);
|
|
276
|
+
ok(`Installed skill: ${BOLD}${skill}${RESET}`);
|
|
277
|
+
} else {
|
|
278
|
+
warn(`Skill source not found: ${skill}`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ── Step 7: Configure color ──────────────────────────────────────────
|
|
283
|
+
|
|
284
|
+
if (color) {
|
|
285
|
+
fs.writeFileSync(COLOR_CONFIG, color, 'utf8');
|
|
286
|
+
ok(`Topic color set to: ${BOLD}${color}${RESET}`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ── Step 8: Summary ──────────────────────────────────────────────────
|
|
290
|
+
|
|
291
|
+
console.log('');
|
|
292
|
+
heading('Installation complete');
|
|
293
|
+
console.log(` ${DIM}Statusline:${RESET} ~/.claude/session-topics/statusline.sh`);
|
|
294
|
+
console.log(` ${DIM}Skills:${RESET} ~/.claude/skills/auto-topic/`);
|
|
295
|
+
console.log(` ~/.claude/skills/set-topic/`);
|
|
296
|
+
console.log(` ${DIM}Settings:${RESET} ~/.claude/settings.json`);
|
|
297
|
+
if (color) {
|
|
298
|
+
console.log(` ${DIM}Color:${RESET} ${color}`);
|
|
299
|
+
}
|
|
300
|
+
console.log('');
|
|
301
|
+
console.log(` Topics are set automatically. Use ${CYAN}/set-topic <text>${RESET} to override.`);
|
|
302
|
+
console.log('');
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function determineStatusLineCase(settings) {
|
|
306
|
+
// Case B or C: statusLine exists
|
|
307
|
+
if (settings.statusLine && typeof settings.statusLine === 'object') {
|
|
308
|
+
const cmd = settings.statusLine.command;
|
|
309
|
+
if (typeof cmd === 'string' && cmd.length > 0) {
|
|
310
|
+
// Case B: already ours
|
|
311
|
+
if (cmd.includes('session-topics')) {
|
|
312
|
+
return 'B';
|
|
313
|
+
}
|
|
314
|
+
// Case C: another command
|
|
315
|
+
return 'C';
|
|
316
|
+
}
|
|
317
|
+
// statusLine exists but no valid command
|
|
318
|
+
return 'D';
|
|
319
|
+
}
|
|
320
|
+
// No statusLine at all
|
|
321
|
+
return 'A';
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ─── Uninstall ───────────────────────────────────────────────────────────────
|
|
325
|
+
|
|
326
|
+
function uninstall() {
|
|
327
|
+
heading('Uninstalling claude-session-topics');
|
|
328
|
+
|
|
329
|
+
const settings = readSettings();
|
|
330
|
+
|
|
331
|
+
// ── Step 1: Restore statusline ───────────────────────────────────────
|
|
332
|
+
|
|
333
|
+
if (fs.existsSync(ORIG_CMD_FILE)) {
|
|
334
|
+
// Had a previous command — restore it
|
|
335
|
+
const origCmd = fs.readFileSync(ORIG_CMD_FILE, 'utf8').trim();
|
|
336
|
+
if (origCmd && settings.statusLine) {
|
|
337
|
+
settings.statusLine.command = origCmd;
|
|
338
|
+
writeSettings(settings);
|
|
339
|
+
ok(`Restored original statusLine command`);
|
|
340
|
+
info(` ${origCmd}`);
|
|
341
|
+
}
|
|
342
|
+
} else {
|
|
343
|
+
// No backup — remove statusLine entirely if it's ours
|
|
344
|
+
if (
|
|
345
|
+
settings.statusLine &&
|
|
346
|
+
typeof settings.statusLine.command === 'string' &&
|
|
347
|
+
settings.statusLine.command.includes('session-topics')
|
|
348
|
+
) {
|
|
349
|
+
delete settings.statusLine;
|
|
350
|
+
writeSettings(settings);
|
|
351
|
+
ok('Removed statusLine from settings.json');
|
|
352
|
+
} else if (settings.statusLine) {
|
|
353
|
+
info('statusLine does not reference session-topics — left untouched');
|
|
354
|
+
} else {
|
|
355
|
+
info('No statusLine to remove');
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ── Step 2: Delete scripts ───────────────────────────────────────────
|
|
360
|
+
|
|
361
|
+
const filesToDelete = [DEST_STATUSLINE, DEST_WRAPPER, ORIG_CMD_FILE];
|
|
362
|
+
for (const file of filesToDelete) {
|
|
363
|
+
if (fs.existsSync(file)) {
|
|
364
|
+
fs.unlinkSync(file);
|
|
365
|
+
ok(`Deleted ${path.basename(file)}`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// ── Step 3: Remove permission ────────────────────────────────────────
|
|
370
|
+
|
|
371
|
+
if (
|
|
372
|
+
settings.permissions &&
|
|
373
|
+
typeof settings.permissions === 'object' &&
|
|
374
|
+
Array.isArray(settings.permissions.allow)
|
|
375
|
+
) {
|
|
376
|
+
const before = settings.permissions.allow.length;
|
|
377
|
+
settings.permissions.allow = settings.permissions.allow.filter(
|
|
378
|
+
(rule) => rule !== PERMISSION_RULE
|
|
379
|
+
);
|
|
380
|
+
if (settings.permissions.allow.length < before) {
|
|
381
|
+
writeSettings(settings);
|
|
382
|
+
ok(`Removed permission: ${PERMISSION_RULE}`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// ── Step 4: Delete skills ────────────────────────────────────────────
|
|
387
|
+
|
|
388
|
+
const skillsToDelete = ['auto-topic', 'set-topic'];
|
|
389
|
+
for (const skill of skillsToDelete) {
|
|
390
|
+
const skillDir = path.join(SKILLS_DIR, skill);
|
|
391
|
+
if (fs.existsSync(skillDir)) {
|
|
392
|
+
fs.rmSync(skillDir, { recursive: true, force: true });
|
|
393
|
+
ok(`Removed skill: ${skill}`);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// ── Step 5: Preserve data ────────────────────────────────────────────
|
|
398
|
+
|
|
399
|
+
info('Preserved topic data in ~/.claude/session-topics/ (topic files + color config)');
|
|
400
|
+
|
|
401
|
+
// ── Summary ──────────────────────────────────────────────────────────
|
|
402
|
+
|
|
403
|
+
console.log('');
|
|
404
|
+
heading('Uninstall complete');
|
|
405
|
+
console.log(` Scripts and skills removed. Topic data preserved.`);
|
|
406
|
+
console.log(` To fully remove all data: ${DIM}rm -rf ~/.claude/session-topics/${RESET}`);
|
|
407
|
+
console.log('');
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
411
|
+
|
|
412
|
+
function main() {
|
|
413
|
+
const { action, color } = parseArgs(process.argv);
|
|
414
|
+
|
|
415
|
+
switch (action) {
|
|
416
|
+
case 'help':
|
|
417
|
+
showHelp();
|
|
418
|
+
break;
|
|
419
|
+
case 'install':
|
|
420
|
+
install(color);
|
|
421
|
+
break;
|
|
422
|
+
case 'uninstall':
|
|
423
|
+
uninstall();
|
|
424
|
+
break;
|
|
425
|
+
default:
|
|
426
|
+
showHelp();
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@alexismunozdev/claude-session-topics",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Session topics for Claude Code — auto-set and display a topic in the statusline, change anytime with /set-topic",
|
|
5
|
+
"bin": {
|
|
6
|
+
"claude-session-topics": "bin/install.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin/",
|
|
10
|
+
"scripts/",
|
|
11
|
+
"skills/",
|
|
12
|
+
"LICENSE",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"keywords": [
|
|
16
|
+
"claude",
|
|
17
|
+
"claude-code",
|
|
18
|
+
"statusline",
|
|
19
|
+
"topic",
|
|
20
|
+
"session",
|
|
21
|
+
"productivity"
|
|
22
|
+
],
|
|
23
|
+
"author": "Alexis Muñoz",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/alexismunoz1/claude-session-topics.git"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://github.com/alexismunoz1/claude-session-topics"
|
|
30
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
input=$(cat)
|
|
5
|
+
|
|
6
|
+
# ── Parse JSON
|
|
7
|
+
SESSION_ID=$(echo "$input" | jq -r '.session_id // ""')
|
|
8
|
+
|
|
9
|
+
# ── PID→Session bridge
|
|
10
|
+
if [ -n "$SESSION_ID" ]; then
|
|
11
|
+
echo "$SESSION_ID" > "/tmp/claude-pid-${PPID}"
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
# ── Pick up pending topic from /set-topic
|
|
15
|
+
PENDING="/tmp/claude-pending-topic-${PPID}"
|
|
16
|
+
if [ -n "$SESSION_ID" ] && [ -f "$PENDING" ]; then
|
|
17
|
+
PENDING_TOPIC=$(cat "$PENDING" 2>/dev/null || echo "")
|
|
18
|
+
if [ -n "$PENDING_TOPIC" ]; then
|
|
19
|
+
mkdir -p "$HOME/.claude/session-topics"
|
|
20
|
+
echo "$PENDING_TOPIC" > "$HOME/.claude/session-topics/${SESSION_ID}"
|
|
21
|
+
rm -f "$PENDING"
|
|
22
|
+
fi
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
# ── Topic
|
|
26
|
+
TOPIC=""
|
|
27
|
+
if [ -n "$SESSION_ID" ]; then
|
|
28
|
+
TOPIC_FILE="$HOME/.claude/session-topics/${SESSION_ID}"
|
|
29
|
+
if [ -f "$TOPIC_FILE" ]; then
|
|
30
|
+
TOPIC=$(cat "$TOPIC_FILE" 2>/dev/null || echo "")
|
|
31
|
+
fi
|
|
32
|
+
fi
|
|
33
|
+
if [ -z "$TOPIC" ] && [ -n "${CLAUDE_SESSION_TOPICS_TOPIC:-}" ]; then
|
|
34
|
+
TOPIC="$CLAUDE_SESSION_TOPICS_TOPIC"
|
|
35
|
+
if [ -n "$SESSION_ID" ]; then
|
|
36
|
+
mkdir -p "$HOME/.claude/session-topics"
|
|
37
|
+
echo "$TOPIC" > "$HOME/.claude/session-topics/${SESSION_ID}"
|
|
38
|
+
fi
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
# ── Resolve topic color
|
|
42
|
+
resolve_color() {
|
|
43
|
+
case "$1" in
|
|
44
|
+
red) echo '\033[31m' ;;
|
|
45
|
+
green) echo '\033[32m' ;;
|
|
46
|
+
yellow) echo '\033[33m' ;;
|
|
47
|
+
blue) echo '\033[34m' ;;
|
|
48
|
+
magenta) echo '\033[35m' ;;
|
|
49
|
+
cyan) echo '\033[36m' ;;
|
|
50
|
+
white) echo '\033[37m' ;;
|
|
51
|
+
orange) echo '\033[38;5;208m' ;;
|
|
52
|
+
grey|gray) echo '\033[90m' ;;
|
|
53
|
+
"") echo '\033[35m' ;; # default: magenta
|
|
54
|
+
*)
|
|
55
|
+
if echo "$1" | grep -qE '^[0-9;]+$'; then
|
|
56
|
+
echo "\033[${1}m"
|
|
57
|
+
else
|
|
58
|
+
echo '\033[35m'
|
|
59
|
+
fi
|
|
60
|
+
;;
|
|
61
|
+
esac
|
|
62
|
+
}
|
|
63
|
+
# Color priority: env var > config file > default (magenta)
|
|
64
|
+
_color="${CLAUDE_SESSION_TOPICS_COLOR:-}"
|
|
65
|
+
if [ -z "$_color" ] && [ -f "$HOME/.claude/session-topics/.color-config" ]; then
|
|
66
|
+
_color=$(cat "$HOME/.claude/session-topics/.color-config" 2>/dev/null || echo "")
|
|
67
|
+
fi
|
|
68
|
+
C_TOPIC=$(resolve_color "$_color")
|
|
69
|
+
C_BOLD='\033[1m'
|
|
70
|
+
C_RESET='\033[0m'
|
|
71
|
+
|
|
72
|
+
# ── Cleanup stale files (atomic lock)
|
|
73
|
+
CLEANUP_LOCK="/tmp/.claude-topic-cleanup-lock"
|
|
74
|
+
if mkdir "$CLEANUP_LOCK" 2>/dev/null; then
|
|
75
|
+
trap "rmdir '$CLEANUP_LOCK' 2>/dev/null || true" EXIT
|
|
76
|
+
find "$HOME/.claude/session-topics" -type f -mtime +7 -delete 2>/dev/null || true
|
|
77
|
+
find /tmp -maxdepth 1 -name "claude-pid-*" -mtime +1 -delete 2>/dev/null || true
|
|
78
|
+
find /tmp -maxdepth 1 -name "claude-pending-topic-*" -mtime +1 -delete 2>/dev/null || true
|
|
79
|
+
rmdir "$CLEANUP_LOCK" 2>/dev/null
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
# ── Output
|
|
83
|
+
if [ -n "$TOPIC" ]; then
|
|
84
|
+
echo -e "${C_BOLD}${C_TOPIC}◆ ${TOPIC}${C_RESET}"
|
|
85
|
+
fi
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: auto-topic
|
|
3
|
+
description: Use on EVERY user message to automatically set or update a short session topic (2-4 words) in the statusline. Activates continuously throughout the conversation to keep the topic current as the discussion evolves.
|
|
4
|
+
version: 1.2.0
|
|
5
|
+
allowed-tools: [Bash]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Auto Topic
|
|
9
|
+
|
|
10
|
+
Automatically set and update a short, descriptive topic for the current Claude Code session as the conversation evolves.
|
|
11
|
+
|
|
12
|
+
## When to Activate
|
|
13
|
+
|
|
14
|
+
This skill activates on **every user message**. Run it silently — do not mention to the user that you are setting or updating the topic.
|
|
15
|
+
|
|
16
|
+
## Rules
|
|
17
|
+
|
|
18
|
+
1. After reading each user message, infer a topic of **2-4 words** (max 20 characters) that reflects what the user is currently working on
|
|
19
|
+
2. The topic should summarize the current focus of the conversation
|
|
20
|
+
3. Use the project's domain language (e.g., Spanish is fine if the user writes in Spanish)
|
|
21
|
+
4. Examples: "Auth Refactor", "FAQ Bug", "Search Filters", "DB Migration", "API Tests"
|
|
22
|
+
5. Run the topic check and update **silently** — do not tell the user you set or changed it
|
|
23
|
+
6. If `/set-topic` is used later by the user, that takes priority
|
|
24
|
+
|
|
25
|
+
## How to Check and Update the Topic
|
|
26
|
+
|
|
27
|
+
### Step 1: Read the current topic
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
SESSION_ID=$(cat /tmp/claude-pid-$PPID 2>/dev/null)
|
|
31
|
+
if [ -n "$SESSION_ID" ]; then
|
|
32
|
+
TOPIC_FILE="$HOME/.claude/session-topics/${SESSION_ID}"
|
|
33
|
+
CURRENT_TOPIC=$(cat "$TOPIC_FILE" 2>/dev/null || echo "")
|
|
34
|
+
echo "Current topic: '$CURRENT_TOPIC'"
|
|
35
|
+
fi
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Step 2: Decide whether to update
|
|
39
|
+
|
|
40
|
+
Compare the inferred new topic with the current topic. **Only write a new topic if the conversation has clearly shifted to a different subject.** Do NOT update for:
|
|
41
|
+
- Minor variations of the same topic (e.g., "Auth Refactor" → "Refactor Auth")
|
|
42
|
+
- Subtasks within the same broader topic (e.g., "Auth Refactor" → "Auth Tests" if still working on auth)
|
|
43
|
+
- Rewording without a real subject change
|
|
44
|
+
|
|
45
|
+
DO update when:
|
|
46
|
+
- The user has moved to a genuinely different area of work (e.g., "Auth Refactor" → "DB Migration")
|
|
47
|
+
- The original topic no longer describes what the conversation is about
|
|
48
|
+
- There is no current topic yet (first message)
|
|
49
|
+
|
|
50
|
+
### Step 3: Write the new topic (only if changed)
|
|
51
|
+
|
|
52
|
+
If the topic has meaningfully changed, run:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
SESSION_ID=$(cat /tmp/claude-pid-$PPID 2>/dev/null)
|
|
56
|
+
if [ -n "$SESSION_ID" ]; then
|
|
57
|
+
mkdir -p "$HOME/.claude/session-topics"
|
|
58
|
+
echo "Your New Topic" > "$HOME/.claude/session-topics/${SESSION_ID}"
|
|
59
|
+
fi
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Replace `Your New Topic` with the inferred 2-4 word topic.
|
|
63
|
+
|
|
64
|
+
If the topic has NOT meaningfully changed, **do nothing** — skip the write entirely.
|
|
65
|
+
|
|
66
|
+
## Important
|
|
67
|
+
|
|
68
|
+
- Run this on **every** user message, but only write when the topic has genuinely changed
|
|
69
|
+
- Do NOT mention the topic to the user — ever
|
|
70
|
+
- Keep topics short and descriptive (2-4 words, max 20 characters)
|
|
71
|
+
- If the statusline hasn't run yet (no PID file), skip silently
|
|
72
|
+
- A high bar for "meaningfully changed" prevents unnecessary churn — when in doubt, keep the current topic
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: set-topic
|
|
3
|
+
description: Set or change the session topic displayed in the statusline
|
|
4
|
+
argument-hint: <topic text>
|
|
5
|
+
allowed-tools: [Bash]
|
|
6
|
+
version: 1.1.0
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Set Topic
|
|
10
|
+
|
|
11
|
+
Set or change the topic displayed in the Claude Code statusline.
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
`/set-topic <topic text>`
|
|
16
|
+
|
|
17
|
+
## Instructions
|
|
18
|
+
|
|
19
|
+
1. The topic text is: $ARGUMENTS
|
|
20
|
+
2. If the topic text is empty, inform the user they need to provide a topic (e.g., `/set-topic Auth Refactor`)
|
|
21
|
+
3. Run this bash command to discover the session ID and write the topic file:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
SESSION_ID=$(cat /tmp/claude-pid-$PPID 2>/dev/null)
|
|
25
|
+
if [ -n "$SESSION_ID" ]; then
|
|
26
|
+
mkdir -p "$HOME/.claude/session-topics"
|
|
27
|
+
echo "$ARGUMENTS" > "$HOME/.claude/session-topics/${SESSION_ID}"
|
|
28
|
+
echo "Topic set to: $ARGUMENTS"
|
|
29
|
+
else
|
|
30
|
+
echo "$ARGUMENTS" > "/tmp/claude-pending-topic-$PPID"
|
|
31
|
+
echo "Topic queued: $ARGUMENTS (will appear on next statusline refresh)"
|
|
32
|
+
fi
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
4. Confirm to the user that the topic has been set and will appear in the statusline.
|