clementine-agent 1.18.150 → 1.18.151

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. The token is baked into the curl command at install time. If the
21
- * dashboard restarts and rotates its token, the user has to re-enable hooks
22
- * (a small UX cost we accept; the alternative is having hooks read the
23
- * token from disk at fire-time which adds a syscall to every tool call).
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: SettingsTemplateOptions): Record<string, unknown>;
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
- export declare function installPathBHooks(workDir: string, opts: SettingsTemplateOptions): InstallResult;
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. The token is baked into the curl command at install time. If the
21
- * dashboard restarts and rotates its token, the user has to re-enable hooks
22
- * (a small UX cost we accept; the alternative is having hooks read the
23
- * token from disk at fire-time which adds a syscall to every tool call).
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: ${opts.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 ?? '1.18.102',
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
- export function installPathBHooks(workDir, opts) {
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;
@@ -5451,7 +5451,10 @@ export async function cmdDashboard(opts) {
5451
5451
  }
5452
5452
  const port = Number(process.env.PORT) || 3030;
5453
5453
  const { installPathBHooks } = await import('../agent/path-b-installer.js');
5454
- const result = installPathBHooks(job.workDir, { token: dashboardToken, port });
5454
+ // 1.18.151 token no longer baked into hook command (read from
5455
+ // ~/.clementine/.dashboard-token at fire time). Path-b installer
5456
+ // takes only port + version sentinel now.
5457
+ const result = installPathBHooks(job.workDir, { port });
5455
5458
  if (!result.ok) {
5456
5459
  res.status(409).json({ ...result, ok: false });
5457
5460
  return;
@@ -6693,6 +6696,29 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
6693
6696
  ? body.tool_response.slice(0, 500)
6694
6697
  : 'tool returned is_error=true';
6695
6698
  }
6699
+ // 1.18.151 — compaction telemetry. PreCompact carries trigger +
6700
+ // optional custom_instructions; PostCompact carries the summary text
6701
+ // and pre/post token counts. We promote these to top-level fields so
6702
+ // the run-detail viewer can render a "summarized N→M tokens" marker
6703
+ // without needing to re-parse the original SDK payload shape.
6704
+ if (hookEventName === 'PreCompact') {
6705
+ ev.kind = 'pre_compact';
6706
+ if (typeof body.trigger === 'string')
6707
+ ev.compactTrigger = body.trigger;
6708
+ if (typeof body.custom_instructions === 'string')
6709
+ ev.compactInstructions = body.custom_instructions;
6710
+ }
6711
+ else if (hookEventName === 'PostCompact') {
6712
+ ev.kind = 'post_compact';
6713
+ if (typeof body.trigger === 'string')
6714
+ ev.compactTrigger = body.trigger;
6715
+ if (typeof body.compact_summary === 'string')
6716
+ ev.compactSummary = body.compact_summary;
6717
+ if (typeof body.pre_tokens === 'number')
6718
+ ev.preTokens = body.pre_tokens;
6719
+ if (typeof body.post_tokens === 'number')
6720
+ ev.postTokens = body.post_tokens;
6721
+ }
6696
6722
  entry.eventLog.append(ev);
6697
6723
  res.json({ ok: true, runId: entry.runId, seq });
6698
6724
  }
@@ -27293,10 +27319,13 @@ function renderRunDetailWaterfall(events, runId, jobName) {
27293
27319
  if (k === 'subagent_start' || k === 'subagent_stop') return '#a855f7';
27294
27320
  if (k === 'rate_limit') return 'var(--yellow)';
27295
27321
  if (k === 'hook') return 'var(--blue)';
27322
+ if (k === 'pre_compact' || k === 'post_compact') return 'var(--orange, #f59e0b)';
27296
27323
  if (k === 'error') return 'var(--red)';
27297
27324
  return 'var(--text-muted)';
27298
27325
  }
27299
27326
  function kindLabel(k) {
27327
+ if (k === 'pre_compact') return 'COMPACT START';
27328
+ if (k === 'post_compact') return 'COMPACT DONE';
27300
27329
  return (k || 'event').toUpperCase().replace(/_/g, ' ');
27301
27330
  }
27302
27331
 
@@ -27369,6 +27398,29 @@ function renderRunDetailWaterfall(events, runId, jobName) {
27369
27398
  preview = ev.sessionId ? 'session ' + String(ev.sessionId).slice(0, 8) + '…' : '';
27370
27399
  } else if (ev.kind === 'session_end') {
27371
27400
  preview = '$' + (ev.costUsd != null ? ev.costUsd.toFixed(4) : '?') + ' · ' + (ev.stopReason || '?');
27401
+ } else if (ev.kind === 'pre_compact') {
27402
+ // 1.18.151 — PreCompact telemetry. Trigger tells us whether the SDK
27403
+ // auto-compacted to stay under context limits or the user/agent
27404
+ // requested it. custom_instructions, when present, biases what gets
27405
+ // preserved — surface it inline so debugging "why did this run forget
27406
+ // X?" is one click away.
27407
+ preview = '⤓ compaction starting (' + (ev.compactTrigger || '?') + ')';
27408
+ fullContent = ev.compactInstructions
27409
+ ? 'Custom instructions for compaction:\\n' + String(ev.compactInstructions)
27410
+ : 'No custom instructions — SDK uses its default summarization.';
27411
+ } else if (ev.kind === 'post_compact') {
27412
+ // 1.18.151 — PostCompact carries the summary text + token deltas.
27413
+ // Showing pre→post tokens makes compaction efficiency visible at a
27414
+ // glance; the summary itself is the canonical "what got remembered"
27415
+ // record for the rest of the run.
27416
+ var tokenDelta = '';
27417
+ if (typeof ev.preTokens === 'number' && typeof ev.postTokens === 'number') {
27418
+ tokenDelta = ' · ' + ev.preTokens.toLocaleString() + '→' + ev.postTokens.toLocaleString() + ' tokens';
27419
+ }
27420
+ preview = '⤴ compaction done' + tokenDelta;
27421
+ fullContent = ev.compactSummary
27422
+ ? 'Summary the SDK kept:\\n\\n' + String(ev.compactSummary)
27423
+ : '(no summary text returned)';
27372
27424
  }
27373
27425
 
27374
27426
  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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.18.150",
3
+ "version": "1.18.151",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",