claude-code-autoconfig 1.0.193 → 1.0.195
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.
|
@@ -69,9 +69,14 @@ proceed, do BOTH of these as near-final actions so the tab flips to the AWAITING
|
|
|
69
69
|
(instead of the idle asterisk):
|
|
70
70
|
1. Write the flag file {{ASK_FILE}} (any short content, e.g. "1"). This is the RELIABLE trigger --
|
|
71
71
|
it is on disk before the turn ends, so it never misses (the transcript-text check in 2 can race
|
|
72
|
-
the turn-end write and silently miss). The flag is one-shot: it
|
|
73
|
-
auto-cleared next turn, so write it ONLY on a turn you are genuinely
|
|
74
|
-
|
|
72
|
+
the turn-end write and silently miss, or miss on phrasing -- see below). The flag is one-shot: it
|
|
73
|
+
is consumed at turn end and auto-cleared next turn, so write it ONLY on a turn you are genuinely
|
|
74
|
+
blocked on an answer. Write it WHENEVER the turn genuinely needs the user's answer to proceed --
|
|
75
|
+
the flag doesn't parse your text, so it is correct even when your closing question is wrapped in
|
|
76
|
+
parens, phrased as a parenthetical aside, or is not the literal final character of the message.
|
|
77
|
+
2. Phrase your FINAL line so it ends with a question mark ('?') -- a backup signal, and good UX. A
|
|
78
|
+
single trailing parenthetical aside after the '?' is fine ("...option A or B? (I lean B.)"), but
|
|
79
|
+
do not rely on phrasing for the signal -- the flag in step 1 is what reliably flips the tab.
|
|
75
80
|
Make the closing question self-contained: answerable from the question alone, without re-reading
|
|
76
81
|
the response above it. Only do this for a genuine blocking question, never a rhetorical one or a recap.
|
|
77
82
|
<!-- /DIRECTIVE:PENDING -->
|
|
@@ -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,27 @@ 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 / ) * _ ", plus one
|
|
181
|
+
// trailing parenthetical aside — see the endsOnQuestion regex below). Returns a
|
|
182
|
+
// diagnostic record { ends, found, tail }: `ends` is the old boolean the caller branches on; `found`
|
|
183
|
+
// and `tail` feed the debug log so a missing half-circle can be told apart — a transcript-flush race
|
|
184
|
+
// shows found=0 (or a stale tail), a genuine regex miss shows a tail that's present but doesn't end
|
|
185
|
+
// in '?'. Any error → a blank record (treated as "no question"), matching the old false return.
|
|
186
|
+
function inspectLastResponse(transcriptPath) {
|
|
187
|
+
const blank = { ends: false, found: false, tail: '', suspectRace: false };
|
|
188
|
+
if (!transcriptPath) return blank;
|
|
156
189
|
let content;
|
|
157
190
|
try {
|
|
158
191
|
content = fs.readFileSync(transcriptPath, 'utf8');
|
|
159
192
|
} catch (_) {
|
|
160
|
-
return
|
|
193
|
+
return blank;
|
|
161
194
|
}
|
|
162
195
|
const lines = content.split('\n');
|
|
196
|
+
// Did we pass a TEXT-LESS assistant block (thinking-only / tool_use-only) on the way back to the last
|
|
197
|
+
// text block? At a fully-flushed Stop the final assistant message HAS text, so we hit it first and this
|
|
198
|
+
// stays false. If it's true, the turn's real final text is most likely still being appended to the
|
|
199
|
+
// JSONL (the flush race) — the caller re-reads after a beat before grading rather than trust this block.
|
|
200
|
+
let sawTextlessAssistant = false;
|
|
163
201
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
164
202
|
const line = lines[i].trim();
|
|
165
203
|
if (!line) continue;
|
|
@@ -176,11 +214,29 @@ function lastResponseEndsWithQuestion(transcriptPath) {
|
|
|
176
214
|
.map(b => b.text)
|
|
177
215
|
.join('\n');
|
|
178
216
|
}
|
|
179
|
-
if (text.trim())
|
|
217
|
+
if (text.trim()) {
|
|
218
|
+
// last ~60 chars, collapsed to one line and quote-stripped so it can't break the log framing
|
|
219
|
+
const tail = text.trim().slice(-60).replace(/\s+/g, ' ').replace(/"/g, "'");
|
|
220
|
+
// Ends on a question? A '?' at the end, tolerating trailing whitespace / ) * _ " — AND an optional
|
|
221
|
+
// SINGLE trailing parenthetical aside after it ("How should we handle it? (I lean option 2.)"), a
|
|
222
|
+
// common shape: ask, then a bracketed recommendation/clarifier. The aside is one level only
|
|
223
|
+
// (`[^()]`, no nesting) and must sit at the very end, so a mid-message rhetorical '?' or a plain
|
|
224
|
+
// statement ending in ')' still won't match — only a genuine closing question does. Belt to the
|
|
225
|
+
// {sid}.ask flag's suspenders: the flag is the primary, parse-free path; this only hardens the
|
|
226
|
+
// fallback for turns that ended on a parenthetical question without writing one.
|
|
227
|
+
const endsOnQuestion = /\?[\s)*_"]*(\([^()]*\)[\s.*_"]*)?$/.test(text);
|
|
228
|
+
return { ends: endsOnQuestion, found: true, tail, suspectRace: sawTextlessAssistant };
|
|
229
|
+
}
|
|
230
|
+
// assistant message with no visible text = a thinking-only or tool_use-only block sitting AFTER the
|
|
231
|
+
// last text we'll grade — a strong hint the final text line hasn't flushed yet.
|
|
232
|
+
sawTextlessAssistant = true;
|
|
180
233
|
}
|
|
181
|
-
return
|
|
234
|
+
return blank;
|
|
182
235
|
}
|
|
183
236
|
|
|
237
|
+
// Event-loop-friendly sleep for the Stop flush-race re-read beat (handle awaits this; no busy-wait).
|
|
238
|
+
function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }
|
|
239
|
+
|
|
184
240
|
function readTitle(file) {
|
|
185
241
|
try {
|
|
186
242
|
if (fs.existsSync(file)) return fs.readFileSync(file, 'utf8').trim();
|
|
@@ -239,3 +295,6 @@ function extractBlock(tpl, name) {
|
|
|
239
295
|
const m = tpl.match(re);
|
|
240
296
|
return m ? m[1].trim() : '';
|
|
241
297
|
}
|
|
298
|
+
|
|
299
|
+
// Exported for tests (require()'d when require.main !== module). The hook itself never reads these.
|
|
300
|
+
module.exports = { inspectLastResponse, normalize, GLYPH };
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v1.0.195
|
|
4
|
+
- fix(terminal-title): flip awaiting ◐ for a closing question with a trailing parenthetical aside
|
|
5
|
+
|
|
6
|
+
## v1.0.194
|
|
7
|
+
- fix(terminal-title): guard the Stop question-grade against the transcript-flush race
|
|
8
|
+
|
|
3
9
|
## v1.0.193
|
|
4
10
|
- fix(installer): always refresh cca-managed title hooks on upgrade
|
|
5
11
|
- fix(terminal-title): arm the {sid}.ask flag so awaiting ◐ is race-free
|
|
@@ -141,9 +147,3 @@
|
|
|
141
147
|
## v1.0.146
|
|
142
148
|
- style: improve inside-Claude block message formatting
|
|
143
149
|
|
|
144
|
-
## v1.0.145
|
|
145
|
-
- fix: block npx install from inside Claude Code session
|
|
146
|
-
|
|
147
|
-
## v1.0.144
|
|
148
|
-
- feat: swagger-style docs, wider layout, better install UX
|
|
149
|
-
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-autoconfig",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.195",
|
|
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",
|