claude-code-autoconfig 1.0.193 → 1.0.194
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/hooks/terminal-title.js +72 -22
- package/CHANGELOG.md +3 -0
- package/package.json +1 -1
|
@@ -41,18 +41,23 @@ const GLYPH = {
|
|
|
41
41
|
// Per-invocation context for the optional debug log (populated in handle, read by titleLog).
|
|
42
42
|
let logCtx = null;
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
44
|
+
// Only drive from stdin when run AS the hook (`node terminal-title.js`). When require()'d by a test the
|
|
45
|
+
// functions below are exported instead, so the stdin read doesn't hang the runner. async because the
|
|
46
|
+
// Stop branch may await a short re-read beat (see the flush-race guard in handle).
|
|
47
|
+
if (require.main === module) {
|
|
48
|
+
let input = '';
|
|
49
|
+
process.stdin.setEncoding('utf8');
|
|
50
|
+
process.stdin.on('data', chunk => (input += chunk));
|
|
51
|
+
process.stdin.on('end', async () => {
|
|
52
|
+
try {
|
|
53
|
+
await handle(JSON.parse(input));
|
|
54
|
+
} catch (err) {
|
|
55
|
+
process.exit(0); // never break the turn on a title error — emit nothing
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
54
59
|
|
|
55
|
-
function handle(data) {
|
|
60
|
+
async function handle(data) {
|
|
56
61
|
const event = data.hook_event_name || '';
|
|
57
62
|
const sid = data.session_id || '';
|
|
58
63
|
const cwd = data.cwd || process.cwd();
|
|
@@ -105,11 +110,32 @@ function handle(data) {
|
|
|
105
110
|
// so VS Code paints the (otherwise bell-less) tab gold. "Ended on a question" = last visible
|
|
106
111
|
// assistant text ends in '?' (transcript heuristic) OR an explicit {sid}.ask flag (consumed here).
|
|
107
112
|
const askFile = path.join(dir, `${sid}.ask`);
|
|
108
|
-
|
|
113
|
+
const askPresent = fileExists(askFile);
|
|
114
|
+
|
|
115
|
+
// Grade the transcript, guarding the flush race: the final assistant text can land in the JSONL a beat
|
|
116
|
+
// AFTER Stop fires (the screen renders from the live stream; the file append lags by ~200ms). The tell
|
|
117
|
+
// is `suspectRace`: the freshest on-disk assistant block is text-less (thinking/tool_use only), or no
|
|
118
|
+
// text was found at all — both mean the real final message is still flushing. When that's the case,
|
|
119
|
+
// re-read a few times (~120ms apart) before grading, so a turn that actually ended on '?' isn't painted
|
|
120
|
+
// ✻ idle off a stale earlier block. A fully-flushed turn hits the text block first → suspectRace=false →
|
|
121
|
+
// zero delay. The {sid}.ask flag remains the race-proof backstop.
|
|
122
|
+
let q = inspectLastResponse(data.transcript_path);
|
|
123
|
+
let reread = 0;
|
|
124
|
+
while (!q.ends && (q.suspectRace || !q.found) && reread < 7) {
|
|
125
|
+
await delay(120);
|
|
126
|
+
reread++;
|
|
127
|
+
q = inspectLastResponse(data.transcript_path);
|
|
128
|
+
if (q.ends || (q.found && !q.suspectRace)) break;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let pending = q.ends;
|
|
109
132
|
let note = pending ? 'q-mark' : 'idle';
|
|
110
|
-
if (!pending &&
|
|
111
|
-
if (
|
|
112
|
-
if (logCtx)
|
|
133
|
+
if (!pending && askPresent) { pending = true; note = 'ask-flag'; }
|
|
134
|
+
if (askPresent) { try { fs.unlinkSync(askFile); } catch (_) { /* ignore */ } }
|
|
135
|
+
if (logCtx) {
|
|
136
|
+
logCtx.note = note;
|
|
137
|
+
logCtx.diag = `ask=${askPresent ? 1 : 0} qmark=${q.ends ? 1 : 0} found=${q.found ? 1 : 0} reread=${reread} tail="${q.tail}"`;
|
|
138
|
+
}
|
|
113
139
|
const glyph = pending ? GLYPH.awaiting : GLYPH.idle;
|
|
114
140
|
emit(setTitle(glyph, normalize(readTitle(file) || folderName(cwd)), pending));
|
|
115
141
|
}
|
|
@@ -135,9 +161,10 @@ function titleLog(glyph, title, ring) {
|
|
|
135
161
|
const name = glyph === GLYPH.working ? 'working'
|
|
136
162
|
: glyph === GLYPH.awaiting ? 'awaiting'
|
|
137
163
|
: glyph === GLYPH.idle ? 'idle' : 'other';
|
|
164
|
+
const diag = logCtx.diag ? ` ${logCtx.diag}` : '';
|
|
138
165
|
const line = `${new Date().toISOString()} ${logCtx.event.padEnd(16)} `
|
|
139
166
|
+ `${name.padEnd(8)} ring=${ring ? 1 : 0} note=${(logCtx.note || '-').padEnd(8)} `
|
|
140
|
-
+ `sid=${logCtx.sid} | ${title}\n`;
|
|
167
|
+
+ `sid=${logCtx.sid} | ${title}${diag}\n`;
|
|
141
168
|
const f = path.join(logCtx.dir, '_debug.log');
|
|
142
169
|
try { if (fs.statSync(f).size > 512 * 1024) fs.renameSync(f, `${f}.1`); } catch (_) { /* none yet */ }
|
|
143
170
|
fs.appendFileSync(f, line);
|
|
@@ -150,16 +177,26 @@ function fileExists(file) {
|
|
|
150
177
|
|
|
151
178
|
// Stop heuristic: did the turn end on a question? Read the JSONL transcript, find the most-recent
|
|
152
179
|
// assistant message with VISIBLE text (skip pure tool_use turns so a final title/memory Write doesn't
|
|
153
|
-
// mask the question), test whether it ends in '?' (allowing trailing whitespace / ) * _ ").
|
|
154
|
-
|
|
155
|
-
|
|
180
|
+
// mask the question), test whether it ends in '?' (allowing trailing whitespace / ) * _ "). Returns a
|
|
181
|
+
// diagnostic record { ends, found, tail }: `ends` is the old boolean the caller branches on; `found`
|
|
182
|
+
// and `tail` feed the debug log so a missing half-circle can be told apart — a transcript-flush race
|
|
183
|
+
// shows found=0 (or a stale tail), a genuine regex miss shows a tail that's present but doesn't end
|
|
184
|
+
// in '?'. Any error → a blank record (treated as "no question"), matching the old false return.
|
|
185
|
+
function inspectLastResponse(transcriptPath) {
|
|
186
|
+
const blank = { ends: false, found: false, tail: '', suspectRace: false };
|
|
187
|
+
if (!transcriptPath) return blank;
|
|
156
188
|
let content;
|
|
157
189
|
try {
|
|
158
190
|
content = fs.readFileSync(transcriptPath, 'utf8');
|
|
159
191
|
} catch (_) {
|
|
160
|
-
return
|
|
192
|
+
return blank;
|
|
161
193
|
}
|
|
162
194
|
const lines = content.split('\n');
|
|
195
|
+
// Did we pass a TEXT-LESS assistant block (thinking-only / tool_use-only) on the way back to the last
|
|
196
|
+
// text block? At a fully-flushed Stop the final assistant message HAS text, so we hit it first and this
|
|
197
|
+
// stays false. If it's true, the turn's real final text is most likely still being appended to the
|
|
198
|
+
// JSONL (the flush race) — the caller re-reads after a beat before grading rather than trust this block.
|
|
199
|
+
let sawTextlessAssistant = false;
|
|
163
200
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
164
201
|
const line = lines[i].trim();
|
|
165
202
|
if (!line) continue;
|
|
@@ -176,11 +213,21 @@ function lastResponseEndsWithQuestion(transcriptPath) {
|
|
|
176
213
|
.map(b => b.text)
|
|
177
214
|
.join('\n');
|
|
178
215
|
}
|
|
179
|
-
if (text.trim())
|
|
216
|
+
if (text.trim()) {
|
|
217
|
+
// last ~60 chars, collapsed to one line and quote-stripped so it can't break the log framing
|
|
218
|
+
const tail = text.trim().slice(-60).replace(/\s+/g, ' ').replace(/"/g, "'");
|
|
219
|
+
return { ends: /\?[\s)*_"]*$/.test(text), found: true, tail, suspectRace: sawTextlessAssistant };
|
|
220
|
+
}
|
|
221
|
+
// assistant message with no visible text = a thinking-only or tool_use-only block sitting AFTER the
|
|
222
|
+
// last text we'll grade — a strong hint the final text line hasn't flushed yet.
|
|
223
|
+
sawTextlessAssistant = true;
|
|
180
224
|
}
|
|
181
|
-
return
|
|
225
|
+
return blank;
|
|
182
226
|
}
|
|
183
227
|
|
|
228
|
+
// Event-loop-friendly sleep for the Stop flush-race re-read beat (handle awaits this; no busy-wait).
|
|
229
|
+
function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }
|
|
230
|
+
|
|
184
231
|
function readTitle(file) {
|
|
185
232
|
try {
|
|
186
233
|
if (fs.existsSync(file)) return fs.readFileSync(file, 'utf8').trim();
|
|
@@ -239,3 +286,6 @@ function extractBlock(tpl, name) {
|
|
|
239
286
|
const m = tpl.match(re);
|
|
240
287
|
return m ? m[1].trim() : '';
|
|
241
288
|
}
|
|
289
|
+
|
|
290
|
+
// Exported for tests (require()'d when require.main !== module). The hook itself never reads these.
|
|
291
|
+
module.exports = { inspectLastResponse, normalize, GLYPH };
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v1.0.194
|
|
4
|
+
- fix(terminal-title): guard the Stop question-grade against the transcript-flush race
|
|
5
|
+
|
|
3
6
|
## v1.0.193
|
|
4
7
|
- fix(installer): always refresh cca-managed title hooks on upgrade
|
|
5
8
|
- fix(terminal-title): arm the {sid}.ask flag so awaiting ◐ is race-free
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-autoconfig",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.194",
|
|
4
4
|
"description": "Intelligent, self-configuring setup for Claude Code. One command analyzes your project, configures Claude, and shows you what it did.",
|
|
5
5
|
"author": "ADAC 1001 <info@adac1001.com>",
|
|
6
6
|
"license": "MIT",
|