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.
- package/dist/assets/{cssMode-DWlBzlpW.js → cssMode-DyodRfD-.js} +1 -1
- package/dist/assets/{freemarker2-Cgg83m-Z.js → freemarker2-D1H1ixRK.js} +1 -1
- package/dist/assets/{handlebars-C4r4LOI9.js → handlebars-wnlxpTlt.js} +1 -1
- package/dist/assets/{html-DaxRI5sW.js → html-Dv_oA_OQ.js} +1 -1
- package/dist/assets/{htmlMode-Bu_8jtXo.js → htmlMode-DGXsu2-V.js} +1 -1
- package/dist/assets/{index-Dj3Db4OA.css → index-CcRP2nIC.css} +1 -1
- package/dist/assets/{index-C_tgFedf.js → index-oiSqLrkZ.js} +602 -602
- package/dist/assets/{javascript-D5Ztx-Ej.js → javascript-CxejmYhM.js} +1 -1
- package/dist/assets/{jsonMode-tfsgezVc.js → jsonMode-ztPfF7kI.js} +1 -1
- package/dist/assets/{liquid-F2cD9OL0.js → liquid-DvtfrYeo.js} +1 -1
- package/dist/assets/{lspLanguageFeatures-Bz_Eih8F.js → lspLanguageFeatures-mIBTKOZq.js} +1 -1
- package/dist/assets/{mdx-BPlD1clX.js → mdx-DTebMWEJ.js} +1 -1
- package/dist/assets/{python-B4gUOWNI.js → python-zea5QgfT.js} +1 -1
- package/dist/assets/{razor-B6pMxVp1.js → razor-DODk3om_.js} +1 -1
- package/dist/assets/{tsMode-C9nq6cHi.js → tsMode-BQGo_Gc8.js} +1 -1
- package/dist/assets/{typescript-Do5Vtwxu.js → typescript-Cfo1NBg6.js} +1 -1
- package/dist/assets/{xml-C0mTbVRp.js → xml-D1RKIHcE.js} +1 -1
- package/dist/assets/{yaml-D3sePJfA.js → yaml-B8MoJlND.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +2 -1
- package/src/main/index.cjs +123 -1
- package/src/main/scheduler.cjs +5 -5
- package/src/main/usage.cjs +2 -4
- package/src/preload/api.d.ts +15 -0
- package/src/preload/index.cjs +7 -0
package/src/main/index.cjs
CHANGED
|
@@ -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
|
});
|
package/src/main/scheduler.cjs
CHANGED
|
@@ -1385,16 +1385,16 @@ function registerScheduleHandlers() {
|
|
|
1385
1385
|
return { ok: true };
|
|
1386
1386
|
});
|
|
1387
1387
|
|
|
1388
|
-
// Archive
|
|
1389
|
-
//
|
|
1390
|
-
// prds-archived/<ISO>/ so the user can recover them.
|
|
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
|
|
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
|
}
|
package/src/main/usage.cjs
CHANGED
|
@@ -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
|
|
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() {
|
package/src/preload/api.d.ts
CHANGED
|
@@ -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>;
|
package/src/preload/index.cjs
CHANGED
|
@@ -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 }),
|