clementine-agent 1.18.150 → 1.18.152
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.
|
@@ -17,24 +17,39 @@
|
|
|
17
17
|
* hand-written project settings.
|
|
18
18
|
*
|
|
19
19
|
* Auth: the hook commands include the dashboard token in the X-Dashboard-Token
|
|
20
|
-
* header.
|
|
21
|
-
* dashboard
|
|
22
|
-
*
|
|
23
|
-
*
|
|
20
|
+
* header. As of 1.18.151 the token is read from disk at fire-time via
|
|
21
|
+
* `$(cat ~/.clementine/.dashboard-token)` instead of being baked into the
|
|
22
|
+
* curl command at install time. The token-file path is interpolated at
|
|
23
|
+
* install time, but its CONTENT is read on every hook fire — so dashboard
|
|
24
|
+
* token rotation (which `clementine update restart` does) no longer breaks
|
|
25
|
+
* installed hooks. The dashboard already maintains this file at startup
|
|
26
|
+
* (dashboard.ts:2097-2098); we just point the curl at it.
|
|
27
|
+
*
|
|
28
|
+
* The cost is one tiny `cat` syscall per tool call — negligible compared to
|
|
29
|
+
* the curl + dashboard ingestion already happening. The win is no more
|
|
30
|
+
* silent 401s when the token rotates.
|
|
24
31
|
*/
|
|
32
|
+
/** Bumped on every meaningful template change. Heartbeat reads installed
|
|
33
|
+
* files' `_clementine.installerVersion` and surfaces a re-install nudge
|
|
34
|
+
* when it lags this constant — see heartbeat.ts (1.18.151 stale-template
|
|
35
|
+
* detector). We never overwrite user-facing files silently; the user runs
|
|
36
|
+
* `clementine hooks install` to opt into the new template. */
|
|
37
|
+
export declare const CURRENT_INSTALLER_VERSION = "1.18.151";
|
|
25
38
|
export interface SettingsTemplateOptions {
|
|
26
|
-
/** Dashboard token to include in the X-Dashboard-Token header. Required. */
|
|
27
|
-
token: string;
|
|
28
39
|
/** Localhost port the dashboard is listening on. Defaults to 3030. */
|
|
29
40
|
port?: number;
|
|
30
41
|
/** Mark the file with a comment so users know who wrote it. */
|
|
31
42
|
installerVersion?: string;
|
|
43
|
+
/** Absolute path to the token file the curl will `cat` at fire time.
|
|
44
|
+
* Override is for tests; production always uses the canonical
|
|
45
|
+
* ~/.clementine/.dashboard-token. */
|
|
46
|
+
tokenFilePath?: string;
|
|
32
47
|
}
|
|
33
48
|
/** Build the JSON content of .claude/settings.local.json. The shape matches
|
|
34
49
|
* the SDK's hook config schema: a top-level `hooks` map keyed by event name,
|
|
35
50
|
* each value an array of `{ hooks: [{ type, command }] }` matchers. The
|
|
36
51
|
* empty matcher means "always fire". */
|
|
37
|
-
export declare function buildSettingsTemplate(opts
|
|
52
|
+
export declare function buildSettingsTemplate(opts?: SettingsTemplateOptions): Record<string, unknown>;
|
|
38
53
|
export interface InstallResult {
|
|
39
54
|
ok: boolean;
|
|
40
55
|
filePath: string;
|
|
@@ -47,8 +62,12 @@ export interface InstallResult {
|
|
|
47
62
|
}
|
|
48
63
|
/** Write/update .claude/settings.local.json in `workDir`. If the file
|
|
49
64
|
* already exists and is NOT installer-managed (no _clementine key), we
|
|
50
|
-
* bail out and refuse to overwrite to avoid clobbering user content.
|
|
51
|
-
|
|
65
|
+
* bail out and refuse to overwrite to avoid clobbering user content.
|
|
66
|
+
*
|
|
67
|
+
* As of 1.18.151 no token is required at install time — the curl reads
|
|
68
|
+
* the live token from disk at fire time. Callers can pass tokenFilePath
|
|
69
|
+
* to override the default `~/.clementine/.dashboard-token` (tests). */
|
|
70
|
+
export declare function installPathBHooks(workDir: string, opts?: SettingsTemplateOptions): InstallResult;
|
|
52
71
|
export interface HooksStatus {
|
|
53
72
|
/** Whether a settings.local.json exists in the workDir. */
|
|
54
73
|
installed: boolean;
|
|
@@ -17,18 +17,28 @@
|
|
|
17
17
|
* hand-written project settings.
|
|
18
18
|
*
|
|
19
19
|
* Auth: the hook commands include the dashboard token in the X-Dashboard-Token
|
|
20
|
-
* header.
|
|
21
|
-
* dashboard
|
|
22
|
-
*
|
|
23
|
-
*
|
|
20
|
+
* header. As of 1.18.151 the token is read from disk at fire-time via
|
|
21
|
+
* `$(cat ~/.clementine/.dashboard-token)` instead of being baked into the
|
|
22
|
+
* curl command at install time. The token-file path is interpolated at
|
|
23
|
+
* install time, but its CONTENT is read on every hook fire — so dashboard
|
|
24
|
+
* token rotation (which `clementine update restart` does) no longer breaks
|
|
25
|
+
* installed hooks. The dashboard already maintains this file at startup
|
|
26
|
+
* (dashboard.ts:2097-2098); we just point the curl at it.
|
|
27
|
+
*
|
|
28
|
+
* The cost is one tiny `cat` syscall per tool call — negligible compared to
|
|
29
|
+
* the curl + dashboard ingestion already happening. The win is no more
|
|
30
|
+
* silent 401s when the token rotates.
|
|
24
31
|
*/
|
|
25
32
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
33
|
+
import os from 'node:os';
|
|
26
34
|
import path from 'node:path';
|
|
27
35
|
/** Hooks we install. Picked for the latency dashboard's needs:
|
|
28
36
|
* PreToolUse + PostToolUse give us tool durations (PostToolUse carries
|
|
29
37
|
* duration_ms). SubagentStart/Stop close the gap path C handles via
|
|
30
38
|
* transcript backfill. Stop / Notification add nice-to-have signal but
|
|
31
|
-
* are minimal cost.
|
|
39
|
+
* are minimal cost. PreCompact + PostCompact (added 1.18.151) carry
|
|
40
|
+
* compaction telemetry — pre/post tokens, summary text, trigger source —
|
|
41
|
+
* which the run-detail viewer surfaces as "what got summarized away". */
|
|
32
42
|
const HOOK_EVENTS = [
|
|
33
43
|
'PreToolUse',
|
|
34
44
|
'PostToolUse',
|
|
@@ -39,18 +49,33 @@ const HOOK_EVENTS = [
|
|
|
39
49
|
'UserPromptSubmit',
|
|
40
50
|
'SessionStart',
|
|
41
51
|
'PreCompact',
|
|
52
|
+
'PostCompact',
|
|
42
53
|
];
|
|
54
|
+
/** Bumped on every meaningful template change. Heartbeat reads installed
|
|
55
|
+
* files' `_clementine.installerVersion` and surfaces a re-install nudge
|
|
56
|
+
* when it lags this constant — see heartbeat.ts (1.18.151 stale-template
|
|
57
|
+
* detector). We never overwrite user-facing files silently; the user runs
|
|
58
|
+
* `clementine hooks install` to opt into the new template. */
|
|
59
|
+
export const CURRENT_INSTALLER_VERSION = '1.18.151';
|
|
60
|
+
/** Default location of the dashboard token file. The dashboard writes this
|
|
61
|
+
* on every startup (see dashboard.ts:2097-2098). Hooks read it at fire
|
|
62
|
+
* time via `$(cat …)` so token rotations propagate without re-installing. */
|
|
63
|
+
const DEFAULT_TOKEN_FILE = path.join(os.homedir(), '.clementine', '.dashboard-token');
|
|
43
64
|
/** Build the JSON content of .claude/settings.local.json. The shape matches
|
|
44
65
|
* the SDK's hook config schema: a top-level `hooks` map keyed by event name,
|
|
45
66
|
* each value an array of `{ hooks: [{ type, command }] }` matchers. The
|
|
46
67
|
* empty matcher means "always fire". */
|
|
47
|
-
export function buildSettingsTemplate(opts) {
|
|
68
|
+
export function buildSettingsTemplate(opts = {}) {
|
|
48
69
|
const port = opts.port ?? 3030;
|
|
70
|
+
const tokenFile = opts.tokenFilePath ?? DEFAULT_TOKEN_FILE;
|
|
49
71
|
// Use POSIX `curl` — preinstalled on macOS and most Linuxes; Windows users
|
|
50
72
|
// running WSL or Git Bash also have it. We add `--max-time 2` so a
|
|
51
|
-
// wedged dashboard can't stall the SDK's tool execution.
|
|
73
|
+
// wedged dashboard can't stall the SDK's tool execution. The
|
|
74
|
+
// `$(cat … 2>/dev/null)` substitution reads the live dashboard token at
|
|
75
|
+
// fire time; if the file is briefly missing during startup the curl
|
|
76
|
+
// sends an empty token and the dashboard 401s harmlessly (no SDK break).
|
|
52
77
|
const curlCmd = `curl -s --max-time 2 -X POST `
|
|
53
|
-
+ `-H "X-Dashboard-Token: ${
|
|
78
|
+
+ `-H "X-Dashboard-Token: $(cat ${tokenFile} 2>/dev/null)" `
|
|
54
79
|
+ `-H "Content-Type: application/json" `
|
|
55
80
|
+ `--data-binary @- `
|
|
56
81
|
+ `http://127.0.0.1:${port}/api/hooks/event`;
|
|
@@ -70,7 +95,7 @@ export function buildSettingsTemplate(opts) {
|
|
|
70
95
|
_clementine: {
|
|
71
96
|
managedBy: 'clementine-agent path-b-installer',
|
|
72
97
|
installedAt: new Date().toISOString(),
|
|
73
|
-
installerVersion: opts.installerVersion ??
|
|
98
|
+
installerVersion: opts.installerVersion ?? CURRENT_INSTALLER_VERSION,
|
|
74
99
|
port,
|
|
75
100
|
},
|
|
76
101
|
hooks,
|
|
@@ -78,12 +103,14 @@ export function buildSettingsTemplate(opts) {
|
|
|
78
103
|
}
|
|
79
104
|
/** Write/update .claude/settings.local.json in `workDir`. If the file
|
|
80
105
|
* already exists and is NOT installer-managed (no _clementine key), we
|
|
81
|
-
* bail out and refuse to overwrite to avoid clobbering user content.
|
|
82
|
-
|
|
106
|
+
* bail out and refuse to overwrite to avoid clobbering user content.
|
|
107
|
+
*
|
|
108
|
+
* As of 1.18.151 no token is required at install time — the curl reads
|
|
109
|
+
* the live token from disk at fire time. Callers can pass tokenFilePath
|
|
110
|
+
* to override the default `~/.clementine/.dashboard-token` (tests). */
|
|
111
|
+
export function installPathBHooks(workDir, opts = {}) {
|
|
83
112
|
if (!workDir)
|
|
84
113
|
return { ok: false, filePath: '', wasExisting: false, wasUpdate: false, error: 'workDir required' };
|
|
85
|
-
if (!opts.token)
|
|
86
|
-
return { ok: false, filePath: '', wasExisting: false, wasUpdate: false, error: 'token required' };
|
|
87
114
|
const dir = path.join(workDir, '.claude');
|
|
88
115
|
const file = path.join(dir, 'settings.local.json');
|
|
89
116
|
let wasExisting = false;
|
package/dist/cli/dashboard.js
CHANGED
|
@@ -2047,9 +2047,36 @@ export async function cmdDashboard(opts) {
|
|
|
2047
2047
|
jsonParser(req, res, next);
|
|
2048
2048
|
});
|
|
2049
2049
|
// ── Dashboard authentication ────────────────────────────────────────
|
|
2050
|
-
|
|
2050
|
+
// 1.18.152 — persist the token across restarts. Before this, every
|
|
2051
|
+
// dashboard startup (manual restart, auto-respawn, crash recovery)
|
|
2052
|
+
// generated a fresh random token, which invalidated every browser
|
|
2053
|
+
// bookmark, open tab, and printed URL. Auto-respawn (1.18.146) +
|
|
2054
|
+
// auto-open browser (1.18.147) made this acutely visible — users
|
|
2055
|
+
// would re-open the dashboard and find the URL they had bookmarked
|
|
2056
|
+
// 401'd, then a new browser window would open with a different token,
|
|
2057
|
+
// and any hook commands installed in projects would silently 401 too.
|
|
2058
|
+
//
|
|
2059
|
+
// Fix: read the existing token if `~/.clementine/.dashboard-token`
|
|
2060
|
+
// already exists; only generate-and-persist on first-ever startup.
|
|
2061
|
+
// To rotate tokens deliberately, the user deletes the file and
|
|
2062
|
+
// restarts. Local-machine threat model assumes the home dir is
|
|
2063
|
+
// trusted (see feedback_security_model.md) — a persistent token here
|
|
2064
|
+
// is the same security posture as `~/.ssh/id_rsa`.
|
|
2051
2065
|
const tokenPath = path.join(BASE_DIR, '.dashboard-token');
|
|
2052
|
-
|
|
2066
|
+
let dashboardToken;
|
|
2067
|
+
try {
|
|
2068
|
+
const existing = readFileSync(tokenPath, 'utf-8').trim();
|
|
2069
|
+
// Strict format check — anything that doesn't look like a 48-hex-char
|
|
2070
|
+
// token (the format randomBytes(24).toString('hex') produces) gets
|
|
2071
|
+
// regenerated. Defends against truncated / corrupt files.
|
|
2072
|
+
if (!/^[a-f0-9]{48}$/.test(existing))
|
|
2073
|
+
throw new Error('invalid token format on disk');
|
|
2074
|
+
dashboardToken = existing;
|
|
2075
|
+
}
|
|
2076
|
+
catch {
|
|
2077
|
+
dashboardToken = randomBytes(24).toString('hex');
|
|
2078
|
+
writeFileSync(tokenPath, dashboardToken, { mode: 0o600 });
|
|
2079
|
+
}
|
|
2053
2080
|
// ── Remote access + session management ─────────────────────────────
|
|
2054
2081
|
const remoteConfig = loadRemoteConfig();
|
|
2055
2082
|
const sessions = new Map();
|
|
@@ -5451,7 +5478,10 @@ export async function cmdDashboard(opts) {
|
|
|
5451
5478
|
}
|
|
5452
5479
|
const port = Number(process.env.PORT) || 3030;
|
|
5453
5480
|
const { installPathBHooks } = await import('../agent/path-b-installer.js');
|
|
5454
|
-
|
|
5481
|
+
// 1.18.151 — token no longer baked into hook command (read from
|
|
5482
|
+
// ~/.clementine/.dashboard-token at fire time). Path-b installer
|
|
5483
|
+
// takes only port + version sentinel now.
|
|
5484
|
+
const result = installPathBHooks(job.workDir, { port });
|
|
5455
5485
|
if (!result.ok) {
|
|
5456
5486
|
res.status(409).json({ ...result, ok: false });
|
|
5457
5487
|
return;
|
|
@@ -6693,6 +6723,29 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
6693
6723
|
? body.tool_response.slice(0, 500)
|
|
6694
6724
|
: 'tool returned is_error=true';
|
|
6695
6725
|
}
|
|
6726
|
+
// 1.18.151 — compaction telemetry. PreCompact carries trigger +
|
|
6727
|
+
// optional custom_instructions; PostCompact carries the summary text
|
|
6728
|
+
// and pre/post token counts. We promote these to top-level fields so
|
|
6729
|
+
// the run-detail viewer can render a "summarized N→M tokens" marker
|
|
6730
|
+
// without needing to re-parse the original SDK payload shape.
|
|
6731
|
+
if (hookEventName === 'PreCompact') {
|
|
6732
|
+
ev.kind = 'pre_compact';
|
|
6733
|
+
if (typeof body.trigger === 'string')
|
|
6734
|
+
ev.compactTrigger = body.trigger;
|
|
6735
|
+
if (typeof body.custom_instructions === 'string')
|
|
6736
|
+
ev.compactInstructions = body.custom_instructions;
|
|
6737
|
+
}
|
|
6738
|
+
else if (hookEventName === 'PostCompact') {
|
|
6739
|
+
ev.kind = 'post_compact';
|
|
6740
|
+
if (typeof body.trigger === 'string')
|
|
6741
|
+
ev.compactTrigger = body.trigger;
|
|
6742
|
+
if (typeof body.compact_summary === 'string')
|
|
6743
|
+
ev.compactSummary = body.compact_summary;
|
|
6744
|
+
if (typeof body.pre_tokens === 'number')
|
|
6745
|
+
ev.preTokens = body.pre_tokens;
|
|
6746
|
+
if (typeof body.post_tokens === 'number')
|
|
6747
|
+
ev.postTokens = body.post_tokens;
|
|
6748
|
+
}
|
|
6696
6749
|
entry.eventLog.append(ev);
|
|
6697
6750
|
res.json({ ok: true, runId: entry.runId, seq });
|
|
6698
6751
|
}
|
|
@@ -27293,10 +27346,13 @@ function renderRunDetailWaterfall(events, runId, jobName) {
|
|
|
27293
27346
|
if (k === 'subagent_start' || k === 'subagent_stop') return '#a855f7';
|
|
27294
27347
|
if (k === 'rate_limit') return 'var(--yellow)';
|
|
27295
27348
|
if (k === 'hook') return 'var(--blue)';
|
|
27349
|
+
if (k === 'pre_compact' || k === 'post_compact') return 'var(--orange, #f59e0b)';
|
|
27296
27350
|
if (k === 'error') return 'var(--red)';
|
|
27297
27351
|
return 'var(--text-muted)';
|
|
27298
27352
|
}
|
|
27299
27353
|
function kindLabel(k) {
|
|
27354
|
+
if (k === 'pre_compact') return 'COMPACT START';
|
|
27355
|
+
if (k === 'post_compact') return 'COMPACT DONE';
|
|
27300
27356
|
return (k || 'event').toUpperCase().replace(/_/g, ' ');
|
|
27301
27357
|
}
|
|
27302
27358
|
|
|
@@ -27369,6 +27425,29 @@ function renderRunDetailWaterfall(events, runId, jobName) {
|
|
|
27369
27425
|
preview = ev.sessionId ? 'session ' + String(ev.sessionId).slice(0, 8) + '…' : '';
|
|
27370
27426
|
} else if (ev.kind === 'session_end') {
|
|
27371
27427
|
preview = '$' + (ev.costUsd != null ? ev.costUsd.toFixed(4) : '?') + ' · ' + (ev.stopReason || '?');
|
|
27428
|
+
} else if (ev.kind === 'pre_compact') {
|
|
27429
|
+
// 1.18.151 — PreCompact telemetry. Trigger tells us whether the SDK
|
|
27430
|
+
// auto-compacted to stay under context limits or the user/agent
|
|
27431
|
+
// requested it. custom_instructions, when present, biases what gets
|
|
27432
|
+
// preserved — surface it inline so debugging "why did this run forget
|
|
27433
|
+
// X?" is one click away.
|
|
27434
|
+
preview = '⤓ compaction starting (' + (ev.compactTrigger || '?') + ')';
|
|
27435
|
+
fullContent = ev.compactInstructions
|
|
27436
|
+
? 'Custom instructions for compaction:\\n' + String(ev.compactInstructions)
|
|
27437
|
+
: 'No custom instructions — SDK uses its default summarization.';
|
|
27438
|
+
} else if (ev.kind === 'post_compact') {
|
|
27439
|
+
// 1.18.151 — PostCompact carries the summary text + token deltas.
|
|
27440
|
+
// Showing pre→post tokens makes compaction efficiency visible at a
|
|
27441
|
+
// glance; the summary itself is the canonical "what got remembered"
|
|
27442
|
+
// record for the rest of the run.
|
|
27443
|
+
var tokenDelta = '';
|
|
27444
|
+
if (typeof ev.preTokens === 'number' && typeof ev.postTokens === 'number') {
|
|
27445
|
+
tokenDelta = ' · ' + ev.preTokens.toLocaleString() + '→' + ev.postTokens.toLocaleString() + ' tokens';
|
|
27446
|
+
}
|
|
27447
|
+
preview = '⤴ compaction done' + tokenDelta;
|
|
27448
|
+
fullContent = ev.compactSummary
|
|
27449
|
+
? 'Summary the SDK kept:\\n\\n' + String(ev.compactSummary)
|
|
27450
|
+
: '(no summary text returned)';
|
|
27372
27451
|
}
|
|
27373
27452
|
|
|
27374
27453
|
var rowId = 'run-evt-' + j;
|
|
@@ -159,6 +159,39 @@ export async function runStartupMaintenance(store) {
|
|
|
159
159
|
catch (err) {
|
|
160
160
|
logger.warn({ err }, 'Startup .md.bak sweep failed');
|
|
161
161
|
}
|
|
162
|
+
// Path-B installer-version sweep (1.18.151). For every cron with a workDir
|
|
163
|
+
// that has a clementine-managed .claude/settings.local.json, check whether
|
|
164
|
+
// the installerVersion sentinel matches CURRENT_INSTALLER_VERSION. Any
|
|
165
|
+
// older install is missing the token-at-fire-time fix and will silently
|
|
166
|
+
// 401 against /api/hooks/event after the next dashboard token rotation.
|
|
167
|
+
// We log once per startup with the list of paths so the user knows to
|
|
168
|
+
// re-run `clementine hooks install` (we never silently overwrite — user
|
|
169
|
+
// owns their .claude/).
|
|
170
|
+
try {
|
|
171
|
+
const { parseCronJobs } = await import('../gateway/cron-scheduler.js');
|
|
172
|
+
const { getHooksStatus, CURRENT_INSTALLER_VERSION } = await import('../agent/path-b-installer.js');
|
|
173
|
+
const stale = [];
|
|
174
|
+
const seen = new Set();
|
|
175
|
+
for (const job of parseCronJobs()) {
|
|
176
|
+
if (!job.workDir || seen.has(job.workDir))
|
|
177
|
+
continue;
|
|
178
|
+
seen.add(job.workDir);
|
|
179
|
+
const s = getHooksStatus(job.workDir);
|
|
180
|
+
if (s.managedByUs && s.installerVersion !== CURRENT_INSTALLER_VERSION) {
|
|
181
|
+
stale.push({ workDir: job.workDir, installerVersion: s.installerVersion });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (stale.length > 0) {
|
|
185
|
+
logger.warn({
|
|
186
|
+
stale,
|
|
187
|
+
currentVersion: CURRENT_INSTALLER_VERSION,
|
|
188
|
+
action: 'Re-run `clementine hooks install` per project to pick up the token-at-fire-time fix (1.18.151) — until then those projects 401 silently after each dashboard token rotation.',
|
|
189
|
+
}, 'Path-B hook installs are stale');
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
logger.warn({ err }, 'Path-B stale-version sweep failed');
|
|
194
|
+
}
|
|
162
195
|
// Embedding warm-up — pre-embed the most-cited chunks in the background so
|
|
163
196
|
// the first retrievals after startup don't pay cold-start latency. Fire
|
|
164
197
|
// and forget; never blocks startup.
|