claude-code-autoconfig 1.0.192 → 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.directive.md +10 -4
- package/.claude/hooks/terminal-title.js +79 -23
- package/CHANGELOG.md +8 -3
- package/CLAUDE.md +12 -0
- package/bin/cli.js +12 -1
- package/package.json +1 -1
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
Tokens (substituted at runtime):
|
|
13
13
|
{{TITLE_FILE}} — absolute path to this session's title file the model writes
|
|
14
|
+
{{ASK_FILE}} — absolute path to this session's one-shot {sid}.ask flag (PENDING block)
|
|
14
15
|
{{FOLDER}} — the working-directory folder name (scope fallback)
|
|
15
16
|
{{EMDASH}} — the ' — ' segment separator
|
|
16
17
|
{{CMD}} — the slash-command name (COMMAND block only)
|
|
@@ -63,9 +64,14 @@ already exists, read it first then overwrite.
|
|
|
63
64
|
<!-- /DIRECTIVE:SHIFT -->
|
|
64
65
|
|
|
65
66
|
<!-- DIRECTIVE:PENDING -->
|
|
66
|
-
Pending-question signal:
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
Pending-question signal: when you END this turn on a question the user must answer before you can
|
|
68
|
+
proceed, do BOTH of these as near-final actions so the tab flips to the AWAITING half-circle
|
|
69
|
+
(instead of the idle asterisk):
|
|
70
|
+
1. Write the flag file {{ASK_FILE}} (any short content, e.g. "1"). This is the RELIABLE trigger --
|
|
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 is consumed at turn end and
|
|
73
|
+
auto-cleared next turn, so write it ONLY on a turn you are genuinely blocked on an answer.
|
|
74
|
+
2. Phrase your FINAL line so it ends with a question mark ('?') -- a backup signal, and good UX.
|
|
69
75
|
Make the closing question self-contained: answerable from the question alone, without re-reading
|
|
70
|
-
the response above it. Only
|
|
76
|
+
the response above it. Only do this for a genuine blocking question, never a rhetorical one or a recap.
|
|
71
77
|
<!-- /DIRECTIVE:PENDING -->
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Terminal Title — distributable plugin hook (installed to <project>/.claude/hooks/terminal-title.js).
|
|
4
4
|
* ONE self-dispatching hook for five events (keyed on hook_event_name):
|
|
5
|
-
* UserPromptSubmit -> ⬤ working + inject the title directive
|
|
5
|
+
* UserPromptSubmit -> ⬤ working + inject the title directive (incl. the {sid}.ask path) +
|
|
6
|
+
* clear any stale {sid}.ask flag so it reflects only the turn about to run
|
|
6
7
|
* PostToolUse -> ⬤ working (refresh, so a mid-turn title flip shows live + clears a stale ◐)
|
|
7
8
|
* Notification -> ◐ awaiting your approval (permission_prompt matcher only)
|
|
8
9
|
* Stop -> ✻ idle / done — OR ◐ awaiting (+ a 2nd BEL = gold tab) when the turn ended
|
|
@@ -40,18 +41,23 @@ const GLYPH = {
|
|
|
40
41
|
// Per-invocation context for the optional debug log (populated in handle, read by titleLog).
|
|
41
42
|
let logCtx = null;
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
+
}
|
|
53
59
|
|
|
54
|
-
function handle(data) {
|
|
60
|
+
async function handle(data) {
|
|
55
61
|
const event = data.hook_event_name || '';
|
|
56
62
|
const sid = data.session_id || '';
|
|
57
63
|
const cwd = data.cwd || process.cwd();
|
|
@@ -65,6 +71,10 @@ function handle(data) {
|
|
|
65
71
|
// Ensure the state dir exists, but NOT the file — the model's Write tool refuses to overwrite a
|
|
66
72
|
// file it hasn't read, so a pre-created empty file would make its first title write fail.
|
|
67
73
|
try { fs.mkdirSync(dir, { recursive: true }); } catch (_) { /* ignore */ }
|
|
74
|
+
// Clear any stale {sid}.ask left by an interrupted prior turn (Stop never ran to consume it), so a
|
|
75
|
+
// leftover flag can't paint a false ◐ on this turn's end. The flag must reflect ONLY this turn.
|
|
76
|
+
const askFile = path.join(dir, `${sid}.ask`);
|
|
77
|
+
if (fileExists(askFile)) { try { fs.unlinkSync(askFile); } catch (_) { /* ignore */ } }
|
|
68
78
|
const title = normalize(readTitle(file) || folderName(cwd));
|
|
69
79
|
const out = setTitle(GLYPH.working, title);
|
|
70
80
|
out.hookSpecificOutput = {
|
|
@@ -100,11 +110,32 @@ function handle(data) {
|
|
|
100
110
|
// so VS Code paints the (otherwise bell-less) tab gold. "Ended on a question" = last visible
|
|
101
111
|
// assistant text ends in '?' (transcript heuristic) OR an explicit {sid}.ask flag (consumed here).
|
|
102
112
|
const askFile = path.join(dir, `${sid}.ask`);
|
|
103
|
-
|
|
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;
|
|
104
132
|
let note = pending ? 'q-mark' : 'idle';
|
|
105
|
-
if (!pending &&
|
|
106
|
-
if (
|
|
107
|
-
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
|
+
}
|
|
108
139
|
const glyph = pending ? GLYPH.awaiting : GLYPH.idle;
|
|
109
140
|
emit(setTitle(glyph, normalize(readTitle(file) || folderName(cwd)), pending));
|
|
110
141
|
}
|
|
@@ -130,9 +161,10 @@ function titleLog(glyph, title, ring) {
|
|
|
130
161
|
const name = glyph === GLYPH.working ? 'working'
|
|
131
162
|
: glyph === GLYPH.awaiting ? 'awaiting'
|
|
132
163
|
: glyph === GLYPH.idle ? 'idle' : 'other';
|
|
164
|
+
const diag = logCtx.diag ? ` ${logCtx.diag}` : '';
|
|
133
165
|
const line = `${new Date().toISOString()} ${logCtx.event.padEnd(16)} `
|
|
134
166
|
+ `${name.padEnd(8)} ring=${ring ? 1 : 0} note=${(logCtx.note || '-').padEnd(8)} `
|
|
135
|
-
+ `sid=${logCtx.sid} | ${title}\n`;
|
|
167
|
+
+ `sid=${logCtx.sid} | ${title}${diag}\n`;
|
|
136
168
|
const f = path.join(logCtx.dir, '_debug.log');
|
|
137
169
|
try { if (fs.statSync(f).size > 512 * 1024) fs.renameSync(f, `${f}.1`); } catch (_) { /* none yet */ }
|
|
138
170
|
fs.appendFileSync(f, line);
|
|
@@ -145,16 +177,26 @@ function fileExists(file) {
|
|
|
145
177
|
|
|
146
178
|
// Stop heuristic: did the turn end on a question? Read the JSONL transcript, find the most-recent
|
|
147
179
|
// 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 / ) * _ ").
|
|
149
|
-
|
|
150
|
-
|
|
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;
|
|
151
188
|
let content;
|
|
152
189
|
try {
|
|
153
190
|
content = fs.readFileSync(transcriptPath, 'utf8');
|
|
154
191
|
} catch (_) {
|
|
155
|
-
return
|
|
192
|
+
return blank;
|
|
156
193
|
}
|
|
157
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;
|
|
158
200
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
159
201
|
const line = lines[i].trim();
|
|
160
202
|
if (!line) continue;
|
|
@@ -171,11 +213,21 @@ function lastResponseEndsWithQuestion(transcriptPath) {
|
|
|
171
213
|
.map(b => b.text)
|
|
172
214
|
.join('\n');
|
|
173
215
|
}
|
|
174
|
-
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;
|
|
175
224
|
}
|
|
176
|
-
return
|
|
225
|
+
return blank;
|
|
177
226
|
}
|
|
178
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
|
+
|
|
179
231
|
function readTitle(file) {
|
|
180
232
|
try {
|
|
181
233
|
if (fs.existsSync(file)) return fs.readFileSync(file, 'utf8').trim();
|
|
@@ -221,6 +273,7 @@ function buildDirective(data, file, cwd) {
|
|
|
221
273
|
const combined = pending ? `${block}\n\n${pending}` : block;
|
|
222
274
|
return combined
|
|
223
275
|
.split('{{TITLE_FILE}}').join(file)
|
|
276
|
+
.split('{{ASK_FILE}}').join(file.replace(/\.txt$/, '.ask'))
|
|
224
277
|
.split('{{FOLDER}}').join(folderName(cwd))
|
|
225
278
|
.split('{{EMDASH}}').join(EMDASH)
|
|
226
279
|
.split('{{CMD}}').join(cmd);
|
|
@@ -233,3 +286,6 @@ function extractBlock(tpl, name) {
|
|
|
233
286
|
const m = tpl.match(re);
|
|
234
287
|
return m ? m[1].trim() : '';
|
|
235
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,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v1.0.194
|
|
4
|
+
- fix(terminal-title): guard the Stop question-grade against the transcript-flush race
|
|
5
|
+
|
|
6
|
+
## v1.0.193
|
|
7
|
+
- fix(installer): always refresh cca-managed title hooks on upgrade
|
|
8
|
+
- fix(terminal-title): arm the {sid}.ask flag so awaiting ◐ is race-free
|
|
9
|
+
- docs(publish): document web-auth/passkey publish flow in Discoveries
|
|
10
|
+
|
|
3
11
|
## v1.0.192
|
|
4
12
|
- fix(pkg): exclude runtime .titles/ from published tarball
|
|
5
13
|
|
|
@@ -142,6 +150,3 @@
|
|
|
142
150
|
## v1.0.144
|
|
143
151
|
- feat: swagger-style docs, wider layout, better install UX
|
|
144
152
|
|
|
145
|
-
## v1.0.143
|
|
146
|
-
- feat: add /validate-cca-install command
|
|
147
|
-
|
package/CLAUDE.md
CHANGED
|
@@ -42,6 +42,18 @@ See `.claude/feedback/` for corrections and guidance from the team.
|
|
|
42
42
|
## Discoveries
|
|
43
43
|
<!-- Claude: append project-specific learnings, gotchas, and context below. This section persists across /autoconfig runs. -->
|
|
44
44
|
|
|
45
|
+
### Publishing to npm — web-auth (passkey) flow, run by the human
|
|
46
|
+
|
|
47
|
+
`npm publish` for this package is a **two-actor** flow; a bare in-agent `npm publish` will FAIL. (Supersedes the auto-generated "## Publishing" one-liner above.)
|
|
48
|
+
|
|
49
|
+
- The npm account has **2FA**. A sandboxed/agent shell can't open a browser, so `npm publish` there dies with `npm error code EOTP`. Don't retry it in-agent; don't default to asking for a typed OTP.
|
|
50
|
+
- **Working flow:** the agent runs `npm test` + `npm version patch` (commit + tag), then the **human runs the publish in their own terminal** (needs a browser):
|
|
51
|
+
```
|
|
52
|
+
cd C:\CODE\claude-code-autoconfig && npm login --auth-type=web && npm publish
|
|
53
|
+
```
|
|
54
|
+
→ browser opens → pick the Google passkey → close it → `+ claude-code-autoconfig@<version>`.
|
|
55
|
+
- **After** the success line, the agent pushes: `git push origin main --follow-tags`. Never double-bump if the version is already bumped. Full procedure: `.claude/commands/publish.md`.
|
|
56
|
+
|
|
45
57
|
## Debugging Methodology — Evidence Before Solutions
|
|
46
58
|
|
|
47
59
|
**NEVER jump to a fix based on assumptions.** When investigating a bug:
|
package/bin/cli.js
CHANGED
|
@@ -767,10 +767,21 @@ if (fs.existsSync(feedbackSrc)) {
|
|
|
767
767
|
copyFn(feedbackSrc, path.join(claudeDest, 'feedback'));
|
|
768
768
|
}
|
|
769
769
|
|
|
770
|
-
// Copy hooks directory
|
|
770
|
+
// Copy hooks directory. Genuinely user-authorable hooks are preserved on upgrade
|
|
771
|
+
// (copyDirIfMissing), BUT the cca-managed title-hook files are ALWAYS refreshed so bug-fixes
|
|
772
|
+
// reach existing installs — without this, copyDirIfMissing leaves stale hooks in place forever
|
|
773
|
+
// (same always-overwrite rationale as scripts/ below). --force already overwrites everything.
|
|
774
|
+
const MANAGED_HOOKS = ['terminal-title.js', 'terminal-title.directive.md'];
|
|
771
775
|
if (fs.existsSync(hooksSrc)) {
|
|
772
776
|
const copyFn = forceMode ? copyDir : copyDirIfMissing;
|
|
773
777
|
copyFn(hooksSrc, path.join(claudeDest, 'hooks'));
|
|
778
|
+
if (!forceMode) {
|
|
779
|
+
const hooksDestDir = path.join(claudeDest, 'hooks');
|
|
780
|
+
for (const name of MANAGED_HOOKS) {
|
|
781
|
+
const src = path.join(hooksSrc, name);
|
|
782
|
+
if (fs.existsSync(src)) fs.copyFileSync(src, path.join(hooksDestDir, name));
|
|
783
|
+
}
|
|
784
|
+
}
|
|
774
785
|
}
|
|
775
786
|
|
|
776
787
|
// Copy scripts directory (always overwrite — these are utility scripts, not user-customizable)
|
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",
|