claude-code-autoconfig 1.0.186 → 1.0.191
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/.claude/commands/autoconfig.md +4 -9
- package/.claude/docs/autoconfig.docs.html +2158 -2148
- package/.claude/hooks/.titles/25869694-a68e-449f-8240-f1ff2b80c1d4.txt +1 -0
- package/.claude/hooks/.titles/_debug.log +43 -0
- package/.claude/hooks/terminal-title.directive.md +71 -0
- package/.claude/hooks/terminal-title.js +235 -0
- package/.claude/scripts/sync-docs.js +662 -662
- package/.claude/settings.json +140 -88
- package/CHANGELOG.md +19 -15
- package/README.md +7 -1
- package/bin/cli.js +1013 -779
- package/package.json +49 -48
|
@@ -0,0 +1 @@
|
|
|
1
|
+
terminal-title indicator — Ship hook to npm
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
2026-06-24T21:04:44.135Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Add state-paint logging
|
|
2
|
+
2026-06-24T21:05:20.044Z Notification awaiting ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Add state-paint logging
|
|
3
|
+
2026-06-24T21:06:38.854Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Add state-paint logging
|
|
4
|
+
2026-06-24T21:07:32.793Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Add state-paint logging
|
|
5
|
+
2026-06-24T21:08:11.474Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Add state-paint logging
|
|
6
|
+
2026-06-24T21:08:34.425Z Stop idle ring=0 note=idle sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Add state-paint logging
|
|
7
|
+
2026-06-24T21:09:29.128Z UserPromptSubmit working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Add state-paint logging
|
|
8
|
+
2026-06-24T21:10:28.948Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Audit migration & deploy status
|
|
9
|
+
2026-06-24T21:10:29.615Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Audit migration & deploy status
|
|
10
|
+
2026-06-24T21:10:47.307Z Notification awaiting ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Audit migration & deploy status
|
|
11
|
+
2026-06-24T21:11:11.991Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Audit migration & deploy status
|
|
12
|
+
2026-06-24T21:11:26.335Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Audit migration & deploy status
|
|
13
|
+
2026-06-24T21:12:01.200Z Notification awaiting ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Audit migration & deploy status
|
|
14
|
+
2026-06-24T21:13:50.152Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Audit migration & deploy status
|
|
15
|
+
2026-06-24T21:14:05.826Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Audit migration & deploy status
|
|
16
|
+
2026-06-24T21:14:43.748Z Notification awaiting ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Audit migration & deploy status
|
|
17
|
+
2026-06-24T21:15:28.698Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Audit migration & deploy status
|
|
18
|
+
2026-06-24T21:16:51.777Z Notification awaiting ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Audit migration & deploy status
|
|
19
|
+
2026-06-24T21:16:53.117Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Audit migration & deploy status
|
|
20
|
+
2026-06-24T21:17:35.995Z Notification awaiting ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Audit migration & deploy status
|
|
21
|
+
2026-06-24T21:17:45.796Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Audit migration & deploy status
|
|
22
|
+
2026-06-24T21:18:33.037Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Audit migration & deploy status
|
|
23
|
+
2026-06-24T21:18:59.216Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Audit migration & deploy status
|
|
24
|
+
2026-06-24T21:19:58.103Z Notification awaiting ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Audit migration & deploy status
|
|
25
|
+
2026-06-24T21:20:40.272Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Audit migration & deploy status
|
|
26
|
+
2026-06-24T21:22:39.312Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Audit migration & deploy status
|
|
27
|
+
2026-06-24T21:22:59.865Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Audit migration & deploy status
|
|
28
|
+
2026-06-24T21:23:01.457Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Audit migration & deploy status
|
|
29
|
+
2026-06-24T21:23:24.084Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Audit migration & deploy status
|
|
30
|
+
2026-06-24T21:24:38.813Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Audit migration & deploy status
|
|
31
|
+
2026-06-24T21:25:48.233Z Notification awaiting ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Audit migration & deploy status
|
|
32
|
+
2026-06-25T04:33:43.177Z UserPromptSubmit working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Audit migration & deploy status
|
|
33
|
+
2026-06-25T04:34:48.715Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Ship hook to npm
|
|
34
|
+
2026-06-25T04:34:49.532Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Ship hook to npm
|
|
35
|
+
2026-06-25T04:35:01.572Z Notification awaiting ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Ship hook to npm
|
|
36
|
+
2026-06-25T04:36:15.708Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Ship hook to npm
|
|
37
|
+
2026-06-25T04:36:54.790Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Ship hook to npm
|
|
38
|
+
2026-06-25T04:38:01.871Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Ship hook to npm
|
|
39
|
+
2026-06-25T04:38:38.209Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Ship hook to npm
|
|
40
|
+
2026-06-25T04:39:09.841Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Ship hook to npm
|
|
41
|
+
2026-06-25T04:39:40.870Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Ship hook to npm
|
|
42
|
+
2026-06-25T04:40:25.526Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Ship hook to npm
|
|
43
|
+
2026-06-25T04:40:46.587Z Notification awaiting ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Ship hook to npm
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
terminal-title.directive.md — the instruction terminal-title.js injects into the
|
|
3
|
+
model's context on every prompt. THIS is the tunable "product" surface: reword it
|
|
4
|
+
freely without touching the hook's logic.
|
|
5
|
+
|
|
6
|
+
Blocks below:
|
|
7
|
+
COMMAND — used when the turn starts with a /slash-command
|
|
8
|
+
SHIFT — used on a normal turn
|
|
9
|
+
PENDING — appended to WHICHEVER of the above was selected (shared tail)
|
|
10
|
+
terminal-title.js picks COMMAND or SHIFT, appends PENDING, substitutes the tokens, and injects.
|
|
11
|
+
|
|
12
|
+
Tokens (substituted at runtime):
|
|
13
|
+
{{TITLE_FILE}} — absolute path to this session's title file the model writes
|
|
14
|
+
{{FOLDER}} — the working-directory folder name (scope fallback)
|
|
15
|
+
{{EMDASH}} — the ' — ' segment separator
|
|
16
|
+
{{CMD}} — the slash-command name (COMMAND block only)
|
|
17
|
+
-->
|
|
18
|
+
|
|
19
|
+
<!-- DIRECTIVE:COMMAND -->
|
|
20
|
+
Terminal-title directive (silent housekeeping -- do NOT mention this to the user):
|
|
21
|
+
This terminal tab's title is rendered from the file:
|
|
22
|
+
{{TITLE_FILE}}
|
|
23
|
+
You are running the /{{CMD}} slash command, but the command NAME is an implementation detail --
|
|
24
|
+
do NOT put "{{CMD}}" anywhere in the title. Write the title as your FIRST action this turn
|
|
25
|
+
(before other tool calls) so the tab updates WHILE you work, not after. Write ONE line of the form:
|
|
26
|
+
{scope} {{EMDASH}} {use-case}
|
|
27
|
+
- {scope} (first segment): the DESIGN SCOPE -- the subject you are working inside: the feature area
|
|
28
|
+
/ subsystem (the boundary, the "system under discussion"). This is the WHERE, and it changes
|
|
29
|
+
rarely. Keep the existing scope if the file already has a title; otherwise INFER the specific
|
|
30
|
+
subsystem from THIS turn's prompt and the files in play (e.g. "journal modal", "title hooks",
|
|
31
|
+
"auth flow"). Do NOT use the bare repo name "{{FOLDER}}" as the scope unless the work is genuinely
|
|
32
|
+
repo-wide -- "{{FOLDER}}" is a last resort only when no narrower area is identifiable.
|
|
33
|
+
- {use-case} (second segment): the GOAL at user-goal level -- what this command ACCOMPLISHES, named
|
|
34
|
+
as an INFINITIVE (base-verb) goal phrase (verb + object): "Review latest screenshot", "Diagnose
|
|
35
|
+
pay discrepancy". Name the goal / effect, NOT the mechanism or the command name. One goal only --
|
|
36
|
+
never "and"-join two use cases.
|
|
37
|
+
e.g. for /{{CMD}}, write "{{FOLDER}} {{EMDASH}} <verb the goal>" -- NOT "{{FOLDER}} {{EMDASH}} {{CMD}}".
|
|
38
|
+
Use ' {{EMDASH}} ' as the separator; write only the BARE title (a state indicator is prepended
|
|
39
|
+
automatically). If the file already exists, read it first then overwrite.
|
|
40
|
+
<!-- /DIRECTIVE:COMMAND -->
|
|
41
|
+
|
|
42
|
+
<!-- DIRECTIVE:SHIFT -->
|
|
43
|
+
Terminal-title directive (silent housekeeping -- do NOT mention this to the user):
|
|
44
|
+
This terminal tab's title is rendered from the file:
|
|
45
|
+
{{TITLE_FILE}}
|
|
46
|
+
When the conversation's SCOPE, use-case, or sub-function SHIFTS -- and only then, not every turn --
|
|
47
|
+
write ONE line to that file. When it DOES shift, write it as your FIRST action that turn (before
|
|
48
|
+
other tool calls) so the tab updates WHILE you work, not after. The line's form:
|
|
49
|
+
{scope} {{EMDASH}} {use-case}
|
|
50
|
+
- {scope}: the DESIGN SCOPE -- the subject under discussion: the feature area / subsystem (the
|
|
51
|
+
boundary). This is the WHERE; it changes rarely. INFER the specific subsystem from the prompt and
|
|
52
|
+
the files in play (e.g. "journal modal", "title hooks"); do NOT default to the bare repo name --
|
|
53
|
+
use it only when the work is genuinely repo-wide.
|
|
54
|
+
- {use-case}: the GOAL at user-goal level -- what you are accomplishing, as an INFINITIVE (base-verb)
|
|
55
|
+
goal phrase (verb + object), e.g. "Refine title taxonomy". Name the goal, not the
|
|
56
|
+
mechanism. One goal only.
|
|
57
|
+
Add a third segment ( {{EMDASH}} {sub-function} ) only when the work goes a level deeper -- this is the
|
|
58
|
+
SUBFUNCTION goal level, a step beneath the user goal.
|
|
59
|
+
Rules: use ' {{EMDASH}} ' (space, em-dash, space) as the separator; keep each segment short (a few
|
|
60
|
+
words); write only the BARE title -- a state indicator is prepended automatically; leave the
|
|
61
|
+
file unchanged when the scope has not moved; this is a compass, so change it rarely. If the file
|
|
62
|
+
already exists, read it first then overwrite.
|
|
63
|
+
<!-- /DIRECTIVE:SHIFT -->
|
|
64
|
+
|
|
65
|
+
<!-- DIRECTIVE:PENDING -->
|
|
66
|
+
Pending-question signal: if you END this turn by asking the user a question they must answer
|
|
67
|
+
before you can proceed, phrase it so your FINAL line ends with a question mark ('?') -- the tab
|
|
68
|
+
indicator then flips to the AWAITING half-circle automatically (instead of the idle asterisk).
|
|
69
|
+
Make the closing question self-contained: answerable from the question alone, without re-reading
|
|
70
|
+
the response above it. Only end on a '?' for a genuine blocking question, never a rhetorical one.
|
|
71
|
+
<!-- /DIRECTIVE:PENDING -->
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Terminal Title — distributable plugin hook (installed to <project>/.claude/hooks/terminal-title.js).
|
|
4
|
+
* ONE self-dispatching hook for five events (keyed on hook_event_name):
|
|
5
|
+
* UserPromptSubmit -> ⬤ working + inject the title directive into the model's context
|
|
6
|
+
* PostToolUse -> ⬤ working (refresh, so a mid-turn title flip shows live + clears a stale ◐)
|
|
7
|
+
* Notification -> ◐ awaiting your approval (permission_prompt matcher only)
|
|
8
|
+
* Stop -> ✻ idle / done — OR ◐ awaiting (+ a 2nd BEL = gold tab) when the turn ended
|
|
9
|
+
* on a question (last visible response text ends in '?', or a {sid}.ask flag)
|
|
10
|
+
* SessionStart -> ✻ idle "Claude Code — New session" (or an existing title on resume/compact)
|
|
11
|
+
*
|
|
12
|
+
* The title is set TWO ways on every event: `process.title` (= SetConsoleTitleW on Windows — the ONLY
|
|
13
|
+
* mechanism that flips the tab on UserPromptSubmit, where Claude Code drops `terminalSequence`) AND
|
|
14
|
+
* `terminalSequence` (honored on the other events). node writes UTF-8 natively, so glyphs go out as-is.
|
|
15
|
+
*
|
|
16
|
+
* Title files are PROJECT-SCOPED at <cwd>/.claude/hooks/.titles/<session_id>.txt — the model authors
|
|
17
|
+
* them; the directive injected each prompt tells it the path + format. Optional forensic log (one line
|
|
18
|
+
* per paint, for tracing an out-of-sync tab) is gated behind CLAUDE_TITLE_DEBUG=1 — default OFF,
|
|
19
|
+
* ~512KB-capped, written to .titles/_debug.log — so it never ships a growing log.
|
|
20
|
+
*
|
|
21
|
+
* Requires `env.CLAUDE_CODE_DISABLE_TERMINAL_TITLE = "1"` (set by plugin.json), or CC's own writer races.
|
|
22
|
+
*/
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
const path = require('path');
|
|
25
|
+
|
|
26
|
+
const ESC = '\x1b';
|
|
27
|
+
const BEL = '\x07';
|
|
28
|
+
const EMDASH = String.fromCodePoint(0x2014);
|
|
29
|
+
|
|
30
|
+
// State glyphs, built from code points so an editor can't strip the invisible variation selector.
|
|
31
|
+
// working = U+26AB MEDIUM BLACK CIRCLE + U+FE0E VS15 (text presentation, not emoji)
|
|
32
|
+
// awaiting = U+25D0 half-filled circle — paused, awaiting your approval/answer
|
|
33
|
+
// idle = U+273B teardrop-spoked asterisk (Claude Code brand mark) while idle/done
|
|
34
|
+
const GLYPH = {
|
|
35
|
+
working: String.fromCodePoint(0x26AB, 0xFE0E),
|
|
36
|
+
awaiting: String.fromCodePoint(0x25D0),
|
|
37
|
+
idle: String.fromCodePoint(0x273B),
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Per-invocation context for the optional debug log (populated in handle, read by titleLog).
|
|
41
|
+
let logCtx = null;
|
|
42
|
+
|
|
43
|
+
let input = '';
|
|
44
|
+
process.stdin.setEncoding('utf8');
|
|
45
|
+
process.stdin.on('data', chunk => (input += chunk));
|
|
46
|
+
process.stdin.on('end', () => {
|
|
47
|
+
try {
|
|
48
|
+
handle(JSON.parse(input));
|
|
49
|
+
} catch (err) {
|
|
50
|
+
process.exit(0); // never break the turn on a title error — emit nothing
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
function handle(data) {
|
|
55
|
+
const event = data.hook_event_name || '';
|
|
56
|
+
const sid = data.session_id || '';
|
|
57
|
+
const cwd = data.cwd || process.cwd();
|
|
58
|
+
// PROJECT-SCOPED title dir (cwd-relative) — the plugin installs per-project. (The live ~/.claude
|
|
59
|
+
// variant uses os.homedir() instead; that is the only difference between the two.)
|
|
60
|
+
const dir = path.join(cwd, '.claude', 'hooks', '.titles');
|
|
61
|
+
const file = path.join(dir, `${sid}.txt`);
|
|
62
|
+
logCtx = { event, sid, dir, note: '' };
|
|
63
|
+
|
|
64
|
+
if (event === 'UserPromptSubmit') {
|
|
65
|
+
// Ensure the state dir exists, but NOT the file — the model's Write tool refuses to overwrite a
|
|
66
|
+
// file it hasn't read, so a pre-created empty file would make its first title write fail.
|
|
67
|
+
try { fs.mkdirSync(dir, { recursive: true }); } catch (_) { /* ignore */ }
|
|
68
|
+
const title = normalize(readTitle(file) || folderName(cwd));
|
|
69
|
+
const out = setTitle(GLYPH.working, title);
|
|
70
|
+
out.hookSpecificOutput = {
|
|
71
|
+
hookEventName: 'UserPromptSubmit',
|
|
72
|
+
additionalContext: buildDirective(data, file, cwd),
|
|
73
|
+
};
|
|
74
|
+
emit(out);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (event === 'PostToolUse') {
|
|
79
|
+
// Only refresh once a real title exists; don't stamp the bare folder over what UPS showed.
|
|
80
|
+
const raw = readTitle(file);
|
|
81
|
+
if (!raw) process.exit(0);
|
|
82
|
+
emit(setTitle(GLYPH.working, normalize(raw)));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (event === 'SessionStart') {
|
|
87
|
+
// Fresh-session placeholder; on resume/compact an existing model-authored title is preferred.
|
|
88
|
+
const title = normalize(readTitle(file) || 'Claude Code - New session');
|
|
89
|
+
emit(setTitle(GLYPH.idle, title));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (event === 'Notification') {
|
|
94
|
+
// A permission prompt is open. Single BEL only — CC already rings its own bell here (tab already gold).
|
|
95
|
+
emit(setTitle(GLYPH.awaiting, normalize(readTitle(file) || folderName(cwd))));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Stop: idle, UNLESS the turn ended on a question the user must answer — then awaiting + a 2nd BEL
|
|
100
|
+
// so VS Code paints the (otherwise bell-less) tab gold. "Ended on a question" = last visible
|
|
101
|
+
// assistant text ends in '?' (transcript heuristic) OR an explicit {sid}.ask flag (consumed here).
|
|
102
|
+
const askFile = path.join(dir, `${sid}.ask`);
|
|
103
|
+
let pending = lastResponseEndsWithQuestion(data.transcript_path);
|
|
104
|
+
let note = pending ? 'q-mark' : 'idle';
|
|
105
|
+
if (!pending && fileExists(askFile)) { pending = true; note = 'ask-flag'; }
|
|
106
|
+
if (fileExists(askFile)) { try { fs.unlinkSync(askFile); } catch (_) { /* ignore */ } }
|
|
107
|
+
if (logCtx) logCtx.note = note;
|
|
108
|
+
const glyph = pending ? GLYPH.awaiting : GLYPH.idle;
|
|
109
|
+
emit(setTitle(glyph, normalize(readTitle(file) || folderName(cwd)), pending));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Set the tab title two ways and return the hook payload. `process.title` is the instant flip and the
|
|
113
|
+
// only mechanism CC honors on UserPromptSubmit. `ring` appends a second BEL (Stop-awaiting only) →
|
|
114
|
+
// VS Code renders a gold "needs you" tab.
|
|
115
|
+
function setTitle(glyph, title, ring) {
|
|
116
|
+
const text = `${glyph} ${title}`;
|
|
117
|
+
try { process.title = text; } catch (_) { /* ignore */ }
|
|
118
|
+
let seq = `${ESC}]0;${text}${BEL}`;
|
|
119
|
+
if (ring) seq += BEL;
|
|
120
|
+
titleLog(glyph, title, ring);
|
|
121
|
+
return { terminalSequence: seq };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Optional forensic log (default OFF — gate: CLAUDE_TITLE_DEBUG=1). One line per paint, so a tab that
|
|
125
|
+
// ends up out of sync (e.g. a ◐ that never cleared to ✻) can be traced to the exact event + glyph that
|
|
126
|
+
// last painted it. Bounded to ~512KB with a single rotation; wrapped so it can never break a turn.
|
|
127
|
+
function titleLog(glyph, title, ring) {
|
|
128
|
+
if (process.env.CLAUDE_TITLE_DEBUG !== '1' || !logCtx) return;
|
|
129
|
+
try {
|
|
130
|
+
const name = glyph === GLYPH.working ? 'working'
|
|
131
|
+
: glyph === GLYPH.awaiting ? 'awaiting'
|
|
132
|
+
: glyph === GLYPH.idle ? 'idle' : 'other';
|
|
133
|
+
const line = `${new Date().toISOString()} ${logCtx.event.padEnd(16)} `
|
|
134
|
+
+ `${name.padEnd(8)} ring=${ring ? 1 : 0} note=${(logCtx.note || '-').padEnd(8)} `
|
|
135
|
+
+ `sid=${logCtx.sid} | ${title}\n`;
|
|
136
|
+
const f = path.join(logCtx.dir, '_debug.log');
|
|
137
|
+
try { if (fs.statSync(f).size > 512 * 1024) fs.renameSync(f, `${f}.1`); } catch (_) { /* none yet */ }
|
|
138
|
+
fs.appendFileSync(f, line);
|
|
139
|
+
} catch (_) { /* logging must never throw */ }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function fileExists(file) {
|
|
143
|
+
try { return fs.existsSync(file); } catch (_) { return false; }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Stop heuristic: did the turn end on a question? Read the JSONL transcript, find the most-recent
|
|
147
|
+
// assistant message with VISIBLE text (skip pure tool_use turns so a final title/memory Write doesn't
|
|
148
|
+
// mask the question), test whether it ends in '?' (allowing trailing whitespace / ) * _ "). Any error → false.
|
|
149
|
+
function lastResponseEndsWithQuestion(transcriptPath) {
|
|
150
|
+
if (!transcriptPath) return false;
|
|
151
|
+
let content;
|
|
152
|
+
try {
|
|
153
|
+
content = fs.readFileSync(transcriptPath, 'utf8');
|
|
154
|
+
} catch (_) {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
const lines = content.split('\n');
|
|
158
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
159
|
+
const line = lines[i].trim();
|
|
160
|
+
if (!line) continue;
|
|
161
|
+
let obj;
|
|
162
|
+
try { obj = JSON.parse(line); } catch (_) { continue; }
|
|
163
|
+
if (!obj || obj.type !== 'assistant' || !obj.message) continue;
|
|
164
|
+
const c = obj.message.content;
|
|
165
|
+
let text = '';
|
|
166
|
+
if (typeof c === 'string') {
|
|
167
|
+
text = c;
|
|
168
|
+
} else if (Array.isArray(c)) {
|
|
169
|
+
text = c
|
|
170
|
+
.filter(b => b && b.type === 'text' && typeof b.text === 'string')
|
|
171
|
+
.map(b => b.text)
|
|
172
|
+
.join('\n');
|
|
173
|
+
}
|
|
174
|
+
if (text.trim()) return /\?[\s)*_"]*$/.test(text);
|
|
175
|
+
}
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function readTitle(file) {
|
|
180
|
+
try {
|
|
181
|
+
if (fs.existsSync(file)) return fs.readFileSync(file, 'utf8').trim();
|
|
182
|
+
} catch (_) { /* ignore */ }
|
|
183
|
+
return '';
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function folderName(cwd) {
|
|
187
|
+
const base = cwd ? path.basename(cwd) : '';
|
|
188
|
+
return base || 'Claude Code';
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Normalize ' - ' to ' — ' and capitalize the first letter of each segment.
|
|
192
|
+
function normalize(title) {
|
|
193
|
+
const sep = ` ${EMDASH} `;
|
|
194
|
+
return title
|
|
195
|
+
.split(' - ').join(sep)
|
|
196
|
+
.split(sep)
|
|
197
|
+
.map(s => s.replace(/^(\P{L}*)(\p{L})/u, (_, lead, ch) => lead + ch.toUpperCase()))
|
|
198
|
+
.join(sep);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function emit(obj) {
|
|
202
|
+
process.stdout.write(JSON.stringify(obj));
|
|
203
|
+
process.exit(0);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Build the injected directive from terminal-title.directive.md (COMMAND block for a /slash-command
|
|
207
|
+
// turn, SHIFT otherwise; PENDING tail appended). Wording lives in the .md so it tunes without code edits.
|
|
208
|
+
function buildDirective(data, file, cwd) {
|
|
209
|
+
let tpl = '';
|
|
210
|
+
try {
|
|
211
|
+
tpl = fs.readFileSync(path.join(__dirname, 'terminal-title.directive.md'), 'utf8');
|
|
212
|
+
} catch (_) {
|
|
213
|
+
return '';
|
|
214
|
+
}
|
|
215
|
+
const prompt = typeof data.prompt === 'string' ? data.prompt : '';
|
|
216
|
+
const m = prompt.match(/^\s*\/([A-Za-z0-9][\w:.-]*)/);
|
|
217
|
+
const cmd = m ? m[1] : '';
|
|
218
|
+
const block = extractBlock(tpl, cmd ? 'COMMAND' : 'SHIFT');
|
|
219
|
+
if (!block) return '';
|
|
220
|
+
const pending = extractBlock(tpl, 'PENDING');
|
|
221
|
+
const combined = pending ? `${block}\n\n${pending}` : block;
|
|
222
|
+
return combined
|
|
223
|
+
.split('{{TITLE_FILE}}').join(file)
|
|
224
|
+
.split('{{FOLDER}}').join(folderName(cwd))
|
|
225
|
+
.split('{{EMDASH}}').join(EMDASH)
|
|
226
|
+
.split('{{CMD}}').join(cmd);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function extractBlock(tpl, name) {
|
|
230
|
+
const re = new RegExp(
|
|
231
|
+
`<!-- DIRECTIVE:${name} -->\\s*([\\s\\S]*?)\\s*<!-- /DIRECTIVE:${name} -->`
|
|
232
|
+
);
|
|
233
|
+
const m = tpl.match(re);
|
|
234
|
+
return m ? m[1].trim() : '';
|
|
235
|
+
}
|