claude-code-session-manager 0.10.0 → 0.10.2

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.
Files changed (25) hide show
  1. package/dist/assets/{cssMode-DWlBzlpW.js → cssMode-DyodRfD-.js} +1 -1
  2. package/dist/assets/{freemarker2-Cgg83m-Z.js → freemarker2-D1H1ixRK.js} +1 -1
  3. package/dist/assets/{handlebars-C4r4LOI9.js → handlebars-wnlxpTlt.js} +1 -1
  4. package/dist/assets/{html-DaxRI5sW.js → html-Dv_oA_OQ.js} +1 -1
  5. package/dist/assets/{htmlMode-Bu_8jtXo.js → htmlMode-DGXsu2-V.js} +1 -1
  6. package/dist/assets/{index-Dj3Db4OA.css → index-CcRP2nIC.css} +1 -1
  7. package/dist/assets/{index-C_tgFedf.js → index-oiSqLrkZ.js} +602 -602
  8. package/dist/assets/{javascript-D5Ztx-Ej.js → javascript-CxejmYhM.js} +1 -1
  9. package/dist/assets/{jsonMode-tfsgezVc.js → jsonMode-ztPfF7kI.js} +1 -1
  10. package/dist/assets/{liquid-F2cD9OL0.js → liquid-DvtfrYeo.js} +1 -1
  11. package/dist/assets/{lspLanguageFeatures-Bz_Eih8F.js → lspLanguageFeatures-mIBTKOZq.js} +1 -1
  12. package/dist/assets/{mdx-BPlD1clX.js → mdx-DTebMWEJ.js} +1 -1
  13. package/dist/assets/{python-B4gUOWNI.js → python-zea5QgfT.js} +1 -1
  14. package/dist/assets/{razor-B6pMxVp1.js → razor-DODk3om_.js} +1 -1
  15. package/dist/assets/{tsMode-C9nq6cHi.js → tsMode-BQGo_Gc8.js} +1 -1
  16. package/dist/assets/{typescript-Do5Vtwxu.js → typescript-Cfo1NBg6.js} +1 -1
  17. package/dist/assets/{xml-C0mTbVRp.js → xml-D1RKIHcE.js} +1 -1
  18. package/dist/assets/{yaml-D3sePJfA.js → yaml-B8MoJlND.js} +1 -1
  19. package/dist/index.html +2 -2
  20. package/package.json +2 -1
  21. package/src/main/index.cjs +123 -1
  22. package/src/main/scheduler.cjs +5 -5
  23. package/src/main/usage.cjs +2 -4
  24. package/src/preload/api.d.ts +15 -0
  25. package/src/preload/index.cjs +7 -0
@@ -1,4 +1,4 @@
1
- const { app, BrowserWindow, ipcMain, dialog, Menu, session, systemPreferences, globalShortcut, shell } = require('electron');
1
+ const { app, BrowserWindow, ipcMain, dialog, Menu, session, systemPreferences, globalShortcut, shell, clipboard } = require('electron');
2
2
  const { spawn, execFile, execFileSync } = require('node:child_process');
3
3
  const path = require('node:path');
4
4
  const fs = require('node:fs');
@@ -24,10 +24,32 @@ const otel = require('./otel.cjs');
24
24
  const otelSettings = require('./otelSettings.cjs');
25
25
  const { registerHistoryAggregatorHandlers } = require('./historyAggregator.cjs');
26
26
  const memoryTool = require('./memoryTool.cjs');
27
+ const { resolveClaudeBin } = require('./lib/claudeBin.cjs');
28
+ const { assertCwdInsideHome } = require('./lib/insideHome.cjs');
27
29
 
28
30
  let mainWindow = null;
29
31
  let rebooting = false;
30
32
 
33
+ // Boot diagnostics — populated at app.whenReady so the renderer can poll their
34
+ // state via IPC and surface toasts on the failure paths. The first-paint
35
+ // deadline timer reads these into the boot log if ready-to-show never fires.
36
+ let bootClaudeBin = { resolved: 'claude', foundOnDisk: false };
37
+ let bootHomeSelfCheck = { ok: true };
38
+ const bootRecentIpcInvocations = [];
39
+ let firstPaintTimer = null;
40
+
41
+ // Wrap ipcMain.handle once to track which channels the renderer actually
42
+ // invokes — the boot log dumps the last 5 so a hang is attributable to a
43
+ // specific handler.
44
+ const originalIpcHandle = ipcMain.handle.bind(ipcMain);
45
+ ipcMain.handle = function trackedHandle(channel, listener) {
46
+ return originalIpcHandle(channel, (...args) => {
47
+ bootRecentIpcInvocations.push({ channel, at: new Date().toISOString() });
48
+ if (bootRecentIpcInvocations.length > 5) bootRecentIpcInvocations.shift();
49
+ return listener(...args);
50
+ });
51
+ };
52
+
31
53
  const REBOOT_LOG = path.join(os.homedir(), '.claude', 'session-manager-reboot.log');
32
54
 
33
55
  function logReboot(line) {
@@ -39,6 +61,52 @@ function logReboot(line) {
39
61
  } catch { /* best-effort */ }
40
62
  }
41
63
 
64
+ // Writes a diagnostic dump when the renderer fails to fire ready-to-show
65
+ // within the boot deadline. Sync I/O is fine — this is the failure path and
66
+ // the user is already staring at a blank window.
67
+ function writeFirstPaintFailureLog() {
68
+ try {
69
+ const logDir = path.join(os.homedir(), '.claude', 'session-manager', 'logs');
70
+ fs.mkdirSync(logDir, { recursive: true });
71
+
72
+ const ymd = new Date().toISOString().slice(0, 10);
73
+ const logPath = path.join(logDir, `boot-${ymd}.log`);
74
+
75
+ const homeCheck = assertCwdInsideHome(os.homedir());
76
+ const lines = [
77
+ `=== first-paint deadline exceeded @ ${new Date().toISOString()} ===`,
78
+ `process.versions: ${JSON.stringify(process.versions)}`,
79
+ `process.platform: ${process.platform}`,
80
+ `process.arch: ${process.arch}`,
81
+ `os.homedir(): ${os.homedir()}`,
82
+ `claudeBin: ${JSON.stringify(bootClaudeBin)}`,
83
+ `homeSelfCheck: ${JSON.stringify(homeCheck)}`,
84
+ `recentIpcInvocations: ${JSON.stringify(bootRecentIpcInvocations)}`,
85
+ 'RENDERER DID NOT FIRE ready-to-show WITHIN 10s — likely renderer JS error or main-process IPC hang.',
86
+ '',
87
+ ];
88
+ fs.appendFileSync(logPath, lines.join('\n'), { mode: 0o600 });
89
+
90
+ // Keep last 3 boot-*.log files; unlink older ones.
91
+ try {
92
+ const entries = fs.readdirSync(logDir)
93
+ .filter((f) => /^boot-\d{4}-\d{2}-\d{2}\.log$/.test(f))
94
+ .map((f) => {
95
+ const full = path.join(logDir, f);
96
+ let mtimeMs = 0;
97
+ try { mtimeMs = fs.statSync(full).mtimeMs; } catch { /* */ }
98
+ return { full, mtimeMs };
99
+ })
100
+ .sort((a, b) => b.mtimeMs - a.mtimeMs);
101
+ for (const e of entries.slice(3)) {
102
+ try { fs.unlinkSync(e.full); } catch { /* */ }
103
+ }
104
+ } catch { /* */ }
105
+ } catch (err) {
106
+ console.error('[firstPaint] failed to write boot log:', err?.message);
107
+ }
108
+ }
109
+
42
110
  function resolveNpx() {
43
111
  const isWin = process.platform === 'win32';
44
112
  try {
@@ -161,7 +229,14 @@ function createWindow() {
161
229
  },
162
230
  });
163
231
 
232
+ // Boot-time detection 3: if ready-to-show never fires (blank window from
233
+ // renderer JS error or main-process IPC hang), write a diagnostic dump so
234
+ // the user has a postmortem instead of just an empty window.
235
+ if (firstPaintTimer) clearTimeout(firstPaintTimer);
236
+ firstPaintTimer = setTimeout(() => { writeFirstPaintFailureLog(); }, 10_000);
237
+
164
238
  mainWindow.once('ready-to-show', () => {
239
+ if (firstPaintTimer) { clearTimeout(firstPaintTimer); firstPaintTimer = null; }
165
240
  mainWindow.maximize();
166
241
  mainWindow.show();
167
242
  });
@@ -183,6 +258,7 @@ function createWindow() {
183
258
  }
184
259
 
185
260
  mainWindow.on('closed', () => {
261
+ if (firstPaintTimer) { clearTimeout(firstPaintTimer); firstPaintTimer = null; }
186
262
  mainWindow = null;
187
263
  });
188
264
  }
@@ -201,6 +277,11 @@ ipcMain.handle('app:is-e2e', () => process.env.SM_E2E === '1');
201
277
 
202
278
  ipcMain.handle('app:engage-rules-path', () => process.env.SESSION_MANAGER_ENGAGE_RULES || null);
203
279
 
280
+ // Boot diagnostics — renderer polls these to surface toasts when `claude` isn't
281
+ // on disk or the home self-check failed (e.g. macOS /Users symlink mismatch).
282
+ ipcMain.handle('app:claude-bin-status', () => bootClaudeBin);
283
+ ipcMain.handle('app:home-self-check', () => bootHomeSelfCheck);
284
+
204
285
  ipcMain.handle('app:pick-directory', async () => {
205
286
  console.log('[main] pick-directory invoked');
206
287
  const result = await dialog.showOpenDialog(mainWindow, {
@@ -215,6 +296,27 @@ ipcMain.handle('app:pick-directory', async () => {
215
296
 
216
297
  ipcMain.on('app:reboot-app', () => rebootApp());
217
298
 
299
+ // Image paste — Ctrl+V in the Terminal pane. Reads the OS clipboard via
300
+ // Electron's native API (renderer's navigator.clipboard.read() doesn't expose
301
+ // raw image MIME types under contextIsolation), saves the bitmap to a temp
302
+ // PNG, and returns the path. Renderer types the path into the PTY so claude
303
+ // can `@<path>`-reference it.
304
+ ipcMain.handle('clipboard:paste-image', async () => {
305
+ try {
306
+ const img = clipboard.readImage();
307
+ if (!img || img.isEmpty()) return { ok: false, empty: true };
308
+ const buf = img.toPNG();
309
+ if (!buf || buf.length === 0) return { ok: false, empty: true };
310
+ const dir = path.join(os.tmpdir(), 'session-manager-clipboard');
311
+ await fsp.mkdir(dir, { recursive: true });
312
+ const file = path.join(dir, `clipboard-${Date.now()}.png`);
313
+ await fsp.writeFile(file, buf);
314
+ return { ok: true, path: file, bytes: buf.length };
315
+ } catch (e) {
316
+ return { ok: false, error: e && e.message ? e.message : String(e) };
317
+ }
318
+ });
319
+
218
320
  // Hooks tab "Test fire": run a hook command with a fake event payload piped
219
321
  // to stdin. shell:true is intentional — Claude Code's hook field is a shell
220
322
  // string. Timeout is enforced via SIGKILL on a timer because spawn's built-in
@@ -550,6 +652,26 @@ app.whenReady().then(async () => {
550
652
  logs.pruneOld();
551
653
  logs.writeLine({ scope: 'main', level: 'info', message: 'app start', meta: { version: app.getVersion(), platform: process.platform } });
552
654
 
655
+ // Boot-time detection 1: surface `claude` binary resolution so a missing
656
+ // install becomes visible to the renderer instead of failing silently on
657
+ // first spawn attempt.
658
+ const claudeResolved = resolveClaudeBin();
659
+ const claudeFoundOnDisk = claudeResolved !== 'claude';
660
+ bootClaudeBin = { resolved: claudeResolved, foundOnDisk: claudeFoundOnDisk };
661
+ if (claudeFoundOnDisk) {
662
+ console.log(`[claudeBin] resolved=${claudeResolved}`);
663
+ } else {
664
+ console.warn('[claudeBin] FALLBACK no candidate found; spawn will rely on PATH');
665
+ }
666
+
667
+ // Boot-time detection 2: symlinked /Users on macOS can make os.homedir()
668
+ // realpath to a path outside itself, which breaks every cwd containment
669
+ // check downstream. Surface here rather than failing on first session spawn.
670
+ bootHomeSelfCheck = assertCwdInsideHome(os.homedir());
671
+ if (!bootHomeSelfCheck.ok) {
672
+ console.error(`[insideHome] SELF-CHECK FAILED: ${bootHomeSelfCheck.error}; sessions will not be able to spawn`);
673
+ }
674
+
553
675
  process.on('uncaughtException', (err) => {
554
676
  logs.writeLine({ scope: 'main', level: 'error', message: 'uncaughtException', meta: { error: err?.message, stack: err?.stack } });
555
677
  });
@@ -1385,16 +1385,16 @@ function registerScheduleHandlers() {
1385
1385
  return { ok: true };
1386
1386
  });
1387
1387
 
1388
- // Archive all pending+failed PRDs and drop their entries from queue.json.
1389
- // Completed/running entries are kept. PRD files are moved (not deleted) to
1390
- // prds-archived/<ISO>/ so the user can recover them. Path containment is
1391
- // enforced — only files inside PRDS_DIR are moved.
1388
+ // Archive every non-running PRD and drop its entry from queue.json.
1389
+ // Running entries are kept (would orphan an in-flight job). PRD files are
1390
+ // moved (not deleted) to prds-archived/<ISO>/ so the user can recover them.
1391
+ // Path containment is enforced — only files inside PRDS_DIR are moved.
1392
1392
  ipcMain.handle('schedule:clear-queue', async () => {
1393
1393
  ensureDirs();
1394
1394
  const ts = new Date().toISOString().replace(/[:.]/g, '-');
1395
1395
  const archiveDir = path.join(PRDS_ARCHIVE_DIR, ts);
1396
1396
  const state = await readQueue();
1397
- const victims = state.jobs.filter((j) => j.status === 'pending' || j.status === 'failed');
1397
+ const victims = state.jobs.filter((j) => j.status !== 'running');
1398
1398
  if (victims.length === 0) {
1399
1399
  return { ok: true, archived: 0, archivedTo: null };
1400
1400
  }
@@ -21,6 +21,7 @@ const path = require('node:path');
21
21
  const os = require('node:os');
22
22
  const { ipcMain } = require('electron');
23
23
  const { refreshIfNeeded, expiresAtMs } = require('./lib/credentials.cjs');
24
+ const { writeJson } = require('./config.cjs');
24
25
 
25
26
  const USAGE_URL = 'https://api.anthropic.com/api/oauth/usage';
26
27
  const CACHE_PATH = path.join(os.homedir(), '.claude', 'session-manager', 'billing-cache.json');
@@ -62,10 +63,7 @@ async function hydrateCache() {
62
63
  }
63
64
 
64
65
  async function persistCache(c) {
65
- await fsp.mkdir(path.dirname(CACHE_PATH), { recursive: true });
66
- const tmp = `${CACHE_PATH}.${process.pid}.${Date.now()}.tmp`;
67
- await fsp.writeFile(tmp, JSON.stringify(c), { encoding: 'utf8', mode: 0o600 });
68
- await fsp.rename(tmp, CACHE_PATH);
66
+ await writeJson(CACHE_PATH, c);
69
67
  }
70
68
 
71
69
  async function fetchUsage() {
@@ -531,6 +531,12 @@ export interface SessionManagerAPI {
531
531
  testFireHook: (args: TestFireHookArgs) => Promise<TestFireHookResult>;
532
532
  /** F7 — true under SM_E2E=1; renderer uses this to suppress wizard auto-trigger. */
533
533
  isE2E: () => Promise<boolean>;
534
+ /** Boot diagnostic — resolved claude binary path + whether it was found
535
+ * on disk (false means spawn will rely on PATH and may ENOENT). */
536
+ claudeBinStatus: () => Promise<{ resolved: string; foundOnDisk: boolean }>;
537
+ /** Boot diagnostic — assertCwdInsideHome(os.homedir()) result. ok=false
538
+ * on macOS symlinked-/Users mismatch and blocks all session spawns. */
539
+ homeSelfCheck: () => Promise<{ ok: boolean; error?: string; realCwd?: string }>;
534
540
  onNewSession: (handler: () => void) => () => void;
535
541
  onRebootSession: (handler: () => void) => () => void;
536
542
  openInEditor: (cwd: string, editor?: string | null) => Promise<{ ok: boolean; editor?: string; error?: string }>;
@@ -656,6 +662,15 @@ export interface SessionManagerAPI {
656
662
  install: (payload: { slug: string }) => Promise<PluginInstallResult>;
657
663
  onInstallProgress: (handler: (ev: PluginInstallProgressEvent) => void) => () => void;
658
664
  };
665
+ clipboard: {
666
+ /** Ctrl+V image paste — reads OS clipboard, saves any image to a temp
667
+ * PNG, and returns the absolute path so the renderer can type it into
668
+ * the PTY. `ok:false, empty:true` when the clipboard holds no image. */
669
+ pasteImage: () => Promise<
670
+ | { ok: true; path: string; bytes: number }
671
+ | { ok: false; empty?: true; error?: string }
672
+ >;
673
+ };
659
674
  memory: {
660
675
  /** List markdown memory entries for the given workspace (defaults to 'default'). */
661
676
  list: (workspace?: string) => Promise<MemoryListResult>;
@@ -16,6 +16,10 @@ contextBridge.exposeInMainWorld('api', {
16
16
  testFireHook: (args) => ipcRenderer.invoke('app:test-fire-hook', args),
17
17
  // F7: lets the renderer suppress the wizard auto-trigger under SM_E2E=1.
18
18
  isE2E: () => ipcRenderer.invoke('app:is-e2e'),
19
+ // Boot diagnostics (v0.10.1) — renderer polls at mount to surface
20
+ // missing-claude-bin / home-symlink-mismatch as toasts.
21
+ claudeBinStatus: () => ipcRenderer.invoke('app:claude-bin-status'),
22
+ homeSelfCheck: () => ipcRenderer.invoke('app:home-self-check'),
19
23
  onNewSession: (handler) => {
20
24
  const listener = () => handler();
21
25
  ipcRenderer.on('app:new-session', listener);
@@ -182,6 +186,9 @@ contextBridge.exposeInMainWorld('api', {
182
186
  return () => ipcRenderer.removeListener('plugins:install-progress', listener);
183
187
  },
184
188
  },
189
+ clipboard: {
190
+ pasteImage: () => ipcRenderer.invoke('clipboard:paste-image'),
191
+ },
185
192
  memory: {
186
193
  list: (workspace) => ipcRenderer.invoke('memory:list', workspace ? { workspace } : {}),
187
194
  read: (name, workspace) => ipcRenderer.invoke('memory:read', workspace ? { name, workspace } : { name }),