claude-code-session-manager 0.29.0 → 0.30.0

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/dist/index.html CHANGED
@@ -7,7 +7,7 @@
7
7
  <link rel="preconnect" href="https://fonts.googleapis.com">
8
8
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
9
  <link href="https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,500;0,6..72,600;0,6..72,700;1,6..72,400&family=Geist:wght@300;400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
10
- <script type="module" crossorigin src="./assets/index-qS7GdCsL.js"></script>
10
+ <script type="module" crossorigin src="./assets/index-BBlKMDCL.js"></script>
11
11
  <link rel="modulepreload" crossorigin href="./assets/monaco-editor-BW5C4Iv1.js">
12
12
  <link rel="stylesheet" crossorigin href="./assets/monaco-editor-BTnBOi8r.css">
13
13
  <link rel="stylesheet" crossorigin href="./assets/index-DMIi9YZH.css">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-session-manager",
3
- "version": "0.29.0",
3
+ "version": "0.30.0",
4
4
  "description": "Local cockpit for the Claude Code CLI — multi-tab terminal, full config surface, scheduler, voice dictation, and live observability.",
5
5
  "type": "module",
6
6
  "main": "src/main/index.cjs",
package/src/main/kg.cjs CHANGED
@@ -51,6 +51,11 @@ const BATCH = 20; // prompts per extraction call (also a per-pro
51
51
  const KNOWN_VOCAB = 200; // top node names pre-seeded for dedup-at-extraction
52
52
  const MAX_TAIL_BYTES = 8 * 1024 * 1024; // bound bytes scanned per ingest run
53
53
  const MAX_EXTRACTIONS_PER_RUN = 30; // bound claude calls per run (cost/time)
54
+ // prompts.jsonl is append-only and otherwise grows forever (observed 325 MB).
55
+ // Once we're caught up past this size, rotate it to prompts.jsonl.1 (one backup
56
+ // kept) and reset the cursor — extraction only ever needs the new tail, and the
57
+ // already-extracted prompts live in the per-project graphs.
58
+ const MAX_LOG_BYTES = 256 * 1024 * 1024;
54
59
  // Coalescing window before an auto-ingest after new prompts land. Units never
55
60
  // mix projects, and a project switch in the log closes the current batch — so
56
61
  // with concurrent sessions a short window yields 1-2-prompt batches and one
@@ -159,6 +164,30 @@ async function saveIngestState(s) {
159
164
  await writeJson(INGEST_STATE_PATH, s);
160
165
  }
161
166
 
167
+ /**
168
+ * Rotate prompts.jsonl once it grows past MAX_LOG_BYTES. Only call when caught
169
+ * up (lastOffset >= size) so no unconsumed prompts are lost. Renames to a single
170
+ * `.1` backup (overwriting any prior one) and resets the cursor to 0; the
171
+ * UserPromptSubmit hook recreates prompts.jsonl on its next append. Best-effort:
172
+ * a failure leaves the log in place and is retried on the next caught-up cycle.
173
+ */
174
+ async function rotateLogIfTooLarge(st, stat) {
175
+ if (!stat || stat.size < MAX_LOG_BYTES) return false;
176
+ const rotated = LOG_PATH + '.1';
177
+ try {
178
+ try { await fsp.unlink(rotated); } catch { /* no prior backup */ }
179
+ await fsp.rename(LOG_PATH, rotated);
180
+ st.lastOffset = 0;
181
+ st.updatedAt = new Date().toISOString();
182
+ await saveIngestState(st);
183
+ logger.writeLine({ scope: 'kg', level: 'info', message: 'rotated prompts.jsonl', meta: { rotatedBytes: stat.size, backup: rotated } });
184
+ return true;
185
+ } catch (e) {
186
+ logger.writeLine({ scope: 'kg', level: 'warn', message: 'log rotation failed', meta: { error: e?.message } });
187
+ return false;
188
+ }
189
+ }
190
+
162
191
  /**
163
192
  * Per-project prompt-count sidecar: { [encodedCwd]: { count: number, cwd: string } }
164
193
  * Returns null when the file does not yet exist (triggers a one-time migration scan).
@@ -356,7 +385,19 @@ async function ingest() {
356
385
  try { stat = await fsp.stat(LOG_PATH); }
357
386
  catch { broadcast('kg:ingest-progress', { phase: 'done', ingesting: false, added: 0 }); return { ok: true, added: 0, note: 'no log yet' }; }
358
387
 
388
+ // Truncation/rotation recovery: if the log shrank below our watermark it was
389
+ // rotated or truncated out from under us — restart from the top of the new
390
+ // file rather than wedging forever at a stale offset.
391
+ if (stat.size < st.lastOffset) {
392
+ logger.writeLine({ scope: 'kg', level: 'info', message: 'log shrank; resetting ingest cursor', meta: { size: stat.size, lastOffset: st.lastOffset } });
393
+ st.lastOffset = 0;
394
+ st.updatedAt = new Date().toISOString();
395
+ await saveIngestState(st);
396
+ }
397
+
359
398
  if (stat.size <= st.lastOffset) {
399
+ // Caught up — a safe moment to rotate the log if it has grown too large.
400
+ await rotateLogIfTooLarge(st, stat);
360
401
  broadcast('kg:ingest-progress', { phase: 'done', ingesting: false, added: 0 });
361
402
  return { ok: true, added: 0, note: 'up to date' };
362
403
  }
@@ -1,5 +1,36 @@
1
+ const os = require('node:os');
2
+ const path = require('node:path');
3
+
1
4
  const SECRET_KEY_RE = /^(?:.*_)?(TOKEN|API_?KEY|SECRET|PASSWORD|AUTHORIZATION|COOKIE|REFRESH[_-]?TOKEN|ACCESS[_-]?TOKEN)$/i;
2
5
 
6
+ /**
7
+ * Common user + Homebrew bin dirs to PREPEND to a spawned child's PATH.
8
+ * Electron can launch from Finder/Dock with a stripped PATH, and Apple Silicon
9
+ * Homebrew lives under /opt/homebrew — without these, `claude`, git, node, etc.
10
+ * are invisible to ptys and `claude -p` jobs on macOS. Single source of truth
11
+ * so pty.cjs and pluginInstall.cjs can't drift (one of them was missing
12
+ * /opt/homebrew and broke plugin install on Apple Silicon).
13
+ */
14
+ function userBinDirs() {
15
+ const home = os.homedir();
16
+ return [
17
+ path.join(home, '.claude', 'local'),
18
+ path.join(home, '.local', 'bin'),
19
+ path.join(home, '.npm-global', 'bin'),
20
+ '/opt/homebrew/bin',
21
+ '/opt/homebrew/sbin',
22
+ '/usr/local/bin',
23
+ '/usr/bin',
24
+ '/bin',
25
+ ];
26
+ }
27
+
28
+ /** Electron's PATH prefixed with the user/Homebrew bin dirs — the value to put
29
+ * in a spawned child's PATH so `claude`, node, git, etc. resolve on macOS. */
30
+ function pathWithUserBins() {
31
+ return `${userBinDirs().join(':')}:${process.env.PATH || ''}`;
32
+ }
33
+
3
34
  function cleanChildEnv(extra = {}) {
4
35
  const env = { ...process.env, ...extra };
5
36
  for (const k of Object.keys(env)) {
@@ -17,4 +48,4 @@ function cleanChildEnv(extra = {}) {
17
48
  return env;
18
49
  }
19
50
 
20
- module.exports = { cleanChildEnv };
51
+ module.exports = { cleanChildEnv, userBinDirs, pathWithUserBins };
@@ -5,7 +5,8 @@ const fs = require('node:fs');
5
5
  const path = require('node:path');
6
6
  const os = require('node:os');
7
7
  const { spawn, execFileSync } = require('node:child_process');
8
- const { cleanChildEnv } = require('./cleanEnv.cjs');
8
+ const { cleanChildEnv, pathWithUserBins } = require('./cleanEnv.cjs');
9
+ const { resolveClaudeBin } = require('./claudeBin.cjs');
9
10
 
10
11
  const CREDS_PATH = path.join(os.homedir(), '.claude', '.credentials.json');
11
12
 
@@ -185,7 +186,12 @@ function tryCliFallback() {
185
186
  const timer = setTimeout(() => settle({ ok: false, reason: 'timeout' }), 15_000);
186
187
  let child;
187
188
  try {
188
- child = spawn('claude', ['--version'], { stdio: 'ignore', env: cleanChildEnv() });
189
+ // Resolve the absolute binary + inject Homebrew/user bins so this works
190
+ // even when Electron launched from Finder/Dock with a stripped PATH (mac).
191
+ child = spawn(resolveClaudeBin(), ['--version'], {
192
+ stdio: 'ignore',
193
+ env: cleanChildEnv({ PATH: pathWithUserBins() }),
194
+ });
189
195
  } catch (e) {
190
196
  clearTimeout(timer);
191
197
  return settle({ ok: false, reason: e?.message });
@@ -21,7 +21,7 @@ const pty = require('node-pty');
21
21
  const path = require('node:path');
22
22
  const os = require('node:os');
23
23
  const fs = require('node:fs');
24
- const { cleanChildEnv } = require('./lib/cleanEnv.cjs');
24
+ const { cleanChildEnv, pathWithUserBins } = require('./lib/cleanEnv.cjs');
25
25
  const { resolveClaudeBin } = require('./lib/claudeBin.cjs');
26
26
  const { sendIfAlive } = require('./lib/sendToRenderer.cjs');
27
27
  const { schemas } = require('./ipcSchemas.cjs');
@@ -58,16 +58,8 @@ function send(channel, payload) {
58
58
  }
59
59
 
60
60
  function childEnv() {
61
- const home = os.homedir();
62
- const extraPath = [
63
- path.join(home, '.local', 'bin'),
64
- path.join(home, '.npm-global', 'bin'),
65
- '/usr/local/bin',
66
- '/usr/bin',
67
- '/bin',
68
- ].join(':');
69
61
  return cleanChildEnv({
70
- PATH: `${extraPath}:${process.env.PATH || ''}`,
62
+ PATH: pathWithUserBins(),
71
63
  TERM: 'xterm-256color',
72
64
  FORCE_COLOR: '0', // strip ANSI so the renderer doesn't have to.
73
65
  });
package/src/main/pty.cjs CHANGED
@@ -4,7 +4,7 @@ const path = require('node:path');
4
4
  const os = require('node:os');
5
5
  const fs = require('node:fs');
6
6
  const { addAllowedRoot } = require('./config.cjs');
7
- const { cleanChildEnv } = require('./lib/cleanEnv.cjs');
7
+ const { cleanChildEnv, pathWithUserBins } = require('./lib/cleanEnv.cjs');
8
8
  const { checkInsideHome } = require('./lib/insideHome.cjs');
9
9
  const { sendIfAlive } = require('./lib/sendToRenderer.cjs');
10
10
 
@@ -62,18 +62,11 @@ class PtyManager {
62
62
  return { pid: existing.proc.pid, cwd: existing.cwd, reattached: true };
63
63
  }
64
64
 
65
- // Prefer `claude` on PATH; extend PATH with common user bin dirs since
66
- // Electron can launch with a stripped environment.
67
- const extraPath = [
68
- path.join(os.homedir(), '.local', 'bin'),
69
- path.join(os.homedir(), '.npm-global', 'bin'),
70
- '/usr/local/bin',
71
- '/usr/bin',
72
- '/bin',
73
- ].join(':');
74
-
65
+ // Prefer `claude` on PATH; extend PATH with common user + Homebrew bin dirs
66
+ // since Electron can launch with a stripped environment (Apple Silicon
67
+ // Homebrew is /opt/homebrew, absent from the default Electron PATH).
75
68
  const env = cleanChildEnv({
76
- PATH: `${extraPath}:${process.env.PATH || ''}`,
69
+ PATH: pathWithUserBins(),
77
70
  TERM: 'xterm-256color',
78
71
  COLORTERM: 'truecolor',
79
72
  FORCE_COLOR: '1',
@@ -48,7 +48,7 @@ const { randomUUID } = require('node:crypto');
48
48
  const { execFile } = require('node:child_process');
49
49
  const { ipcMain } = require('electron');
50
50
  const billing = require('./usage.cjs');
51
- const { cleanChildEnv } = require('./lib/cleanEnv.cjs');
51
+ const { cleanChildEnv, pathWithUserBins } = require('./lib/cleanEnv.cjs');
52
52
  const supervisor = require('./supervisor.cjs');
53
53
  const { resolveClaudeBin } = require('./lib/claudeBin.cjs');
54
54
  const { readTail } = require('./lib/fileTail.cjs');
@@ -921,7 +921,9 @@ async function executeJob(job, runDir, defaultCwd, onPid) {
921
921
  // Strip Claude Code env and secrets that leak in when session-manager is
922
922
  // launched from a `claude` shell. CLAUDE_EFFORT=xhigh forces Opus and
923
923
  // overrides `--model sonnet`, so scheduled jobs burn Opus credits silently.
924
- const childEnv = cleanChildEnv();
924
+ // PATH must include Homebrew/user bins or the job's node/git children ENOENT
925
+ // when Electron was launched from Finder/Dock on macOS (stripped PATH).
926
+ const childEnv = cleanChildEnv({ PATH: pathWithUserBins() });
925
927
 
926
928
  // Track whether the agent has emitted a `result` event in its JSONL stream.
927
929
  // null until seen; then one of "success" | "error_max_turns" | … per the
@@ -1230,7 +1232,7 @@ DO NOT attempt the fix. ONLY write the file. When the file exists, exit immediat
1230
1232
  }
1231
1233
 
1232
1234
  const claudeBin = resolveClaudeBin();
1233
- const childEnv = cleanChildEnv();
1235
+ const childEnv = cleanChildEnv({ PATH: pathWithUserBins() }); // Homebrew/user bins for macOS
1234
1236
 
1235
1237
  // Investigation needs only a deadman watchdog — no idle-tail or result-tail
1236
1238
  // since investigations are short-running Opus probes with a hard ceiling.