claude-code-session-manager 0.13.1 → 0.15.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/assets/{TiptapBody-Btu-_mZq.js → TiptapBody-D0tfDVZb.js} +1 -1
- package/dist/assets/{cssMode-EkBJI3CN.js → cssMode-C3tZkaJ9.js} +1 -1
- package/dist/assets/{freemarker2-DFbgmGC_.js → freemarker2-DEh8tC5X.js} +1 -1
- package/dist/assets/{handlebars-CUy9oTnL.js → handlebars-D_6KmsOK.js} +1 -1
- package/dist/assets/{html-CUFKxIqK.js → html-DF1KVjzv.js} +1 -1
- package/dist/assets/{htmlMode-hBSk7Fab.js → htmlMode-DEakPokt.js} +1 -1
- package/dist/assets/{index-D4dlTF2R.css → index-D-kX3T0V.css} +1 -1
- package/dist/assets/{index-BGIAQ9_i.js → index-Zg61GP50.js} +1416 -1115
- package/dist/assets/{javascript-CAH2ooMO.js → javascript-Du7a359D.js} +1 -1
- package/dist/assets/{jsonMode-BSMCRMaw.js → jsonMode-W3BJwbUD.js} +1 -1
- package/dist/assets/{liquid-CSMs05cs.js → liquid-DDj1fqca.js} +1 -1
- package/dist/assets/{lspLanguageFeatures-DEyYbu0m.js → lspLanguageFeatures-uyEbiR-d.js} +1 -1
- package/dist/assets/{mdx-BocaqTLI.js → mdx-DUqSETvC.js} +1 -1
- package/dist/assets/{python-Dj6u2CWq.js → python-D7S2lUAn.js} +1 -1
- package/dist/assets/{razor-D5OWmIwh.js → razor-19nfZNaZ.js} +1 -1
- package/dist/assets/{tsMode-Cy6xJd5e.js → tsMode-CQke5zpL.js} +1 -1
- package/dist/assets/{typescript-CsWWOqVn.js → typescript-D4ge0PUF.js} +1 -1
- package/dist/assets/{xml-DiTZK7ii.js → xml-D42QDS7q.js} +1 -1
- package/dist/assets/{yaml-pdLhZQr9.js → yaml-CEiz9NyU.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +2 -1
- package/src/main/__tests__/runVerify.test.cjs +388 -0
- package/src/main/config.cjs +1 -7
- package/src/main/files.cjs +4 -37
- package/src/main/historyAggregator.cjs +37 -14
- package/src/main/index.cjs +57 -134
- package/src/main/ipcSchemas.cjs +4 -0
- package/src/main/lib/childWithLog.cjs +218 -0
- package/src/main/lib/expandHome.cjs +21 -0
- package/src/main/lib/insideHome.cjs +74 -21
- package/src/main/lib/openExternalApp.cjs +141 -0
- package/src/main/pluginInstall.cjs +39 -1
- package/src/main/pty.cjs +11 -4
- package/src/main/queueOps.cjs +2 -2
- package/src/main/runVerify.cjs +527 -0
- package/src/main/scheduler.cjs +335 -286
- package/src/main/search.cjs +1 -6
- package/src/main/superagent.cjs +10 -0
- package/src/main/supervisor.cjs +16 -8
- package/src/main/transcripts.cjs +18 -0
- package/src/main/usageMatrix.cjs +336 -0
- package/src/main/watchers.cjs +2 -2
- package/src/preload/api.d.ts +68 -7
- package/src/preload/index.cjs +9 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* openExternalApp.cjs — helpers for launching editor / file manager / terminal.
|
|
3
|
+
*
|
|
4
|
+
* Security invariant: callers (IPC handlers in index.cjs) MUST run containment
|
|
5
|
+
* checks before delegating here. These functions receive already-validated paths.
|
|
6
|
+
*
|
|
7
|
+
* All four functions return:
|
|
8
|
+
* { ok: true; opener: string } — name of the program that was launched
|
|
9
|
+
* { ok: false; error: string } — human-readable failure reason
|
|
10
|
+
*/
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const { execFileSync, spawn } = require('node:child_process');
|
|
14
|
+
const path = require('node:path');
|
|
15
|
+
const fsp = require('node:fs/promises');
|
|
16
|
+
const { shell } = require('electron');
|
|
17
|
+
const { cleanChildEnv } = require('./cleanEnv.cjs');
|
|
18
|
+
|
|
19
|
+
// IMAGE_RE — extensions opened in the OS default viewer, not a code editor.
|
|
20
|
+
// Keep in sync with the comment in index.cjs open-file-in-editor handler.
|
|
21
|
+
const IMAGE_RE = /\.(png|jpe?g|gif|webp|bmp|svg|tiff?|avif|heic|ico)$/i;
|
|
22
|
+
|
|
23
|
+
// EDITOR_CANDIDATES — single source for the default editor resolution order (AC #5).
|
|
24
|
+
// process.env.VISUAL / EDITOR are read lazily so they pick up any runtime overrides.
|
|
25
|
+
const editorCandidates = () =>
|
|
26
|
+
[process.env.VISUAL, process.env.EDITOR, 'code', 'cursor', 'subl', 'nano'].filter(Boolean);
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Returns the resolved path of a command, or null if not found on $PATH.
|
|
30
|
+
* Moved here from index.cjs (was the only caller of the local helper there).
|
|
31
|
+
*/
|
|
32
|
+
function findCommand(name) {
|
|
33
|
+
try {
|
|
34
|
+
const out = execFileSync(
|
|
35
|
+
process.platform === 'win32' ? 'where' : 'which',
|
|
36
|
+
[name],
|
|
37
|
+
{ encoding: 'utf8', env: process.env, timeout: 500 },
|
|
38
|
+
).trim().split(/\r?\n/)[0];
|
|
39
|
+
if (out) return out;
|
|
40
|
+
} catch { /* not found */ }
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Open a project root directory in the user's editor.
|
|
46
|
+
*
|
|
47
|
+
* @param {{ cwd: string, editor?: string | null }} opts
|
|
48
|
+
* @returns {Promise<{ ok: true, opener: string } | { ok: false, error: string }>}
|
|
49
|
+
*/
|
|
50
|
+
async function openInEditor({ cwd, editor }) {
|
|
51
|
+
const candidates = (editor && editor !== 'auto') ? [editor] : editorCandidates();
|
|
52
|
+
for (const cmd of candidates) {
|
|
53
|
+
if (!findCommand(cmd)) continue;
|
|
54
|
+
spawn(cmd, [cwd], { detached: true, stdio: 'ignore', env: cleanChildEnv() }).unref();
|
|
55
|
+
return { ok: true, opener: cmd };
|
|
56
|
+
}
|
|
57
|
+
return { ok: false, error: 'no editor found' };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Open a specific file (with optional line:col) in the user's editor.
|
|
62
|
+
* Image files are routed to the OS default viewer via shell.openPath.
|
|
63
|
+
*
|
|
64
|
+
* The path MUST already have passed containment checks at the IPC boundary.
|
|
65
|
+
* `abs` must be an absolute path.
|
|
66
|
+
*
|
|
67
|
+
* Goto-line flag behavior (AC #6):
|
|
68
|
+
* code | cursor | subl → spawn with ['-g', 'file:line:col']
|
|
69
|
+
* everything else → spawn with ['file'] (bare path, no goto support)
|
|
70
|
+
*
|
|
71
|
+
* @param {{ path: string, line?: number, col?: number, editor?: string | null }} opts
|
|
72
|
+
* @returns {Promise<{ ok: true, opener: string } | { ok: false, error: string }>}
|
|
73
|
+
*/
|
|
74
|
+
async function openFileInEditor({ path: abs, line, col, editor }) {
|
|
75
|
+
try { await fsp.access(abs); } catch { return { ok: false, error: `file not found: ${abs}` }; }
|
|
76
|
+
|
|
77
|
+
// Image fast path — use the OS default viewer instead of a code editor.
|
|
78
|
+
// path.basename avoids .tar.gz-style extension ambiguities (AC implementation note).
|
|
79
|
+
if (IMAGE_RE.test(path.basename(abs))) {
|
|
80
|
+
const errStr = await shell.openPath(abs);
|
|
81
|
+
if (errStr) return { ok: false, error: errStr };
|
|
82
|
+
return { ok: true, opener: 'shell' };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const candidates = (editor && editor !== 'auto') ? [editor] : editorCandidates();
|
|
86
|
+
for (const cmd of candidates) {
|
|
87
|
+
if (!findCommand(cmd)) continue;
|
|
88
|
+
// Only code/cursor/subl understand the -g goto-line flag (AC #6).
|
|
89
|
+
const supportsGoto = /^(code|cursor|subl)$/.test(cmd);
|
|
90
|
+
const target = (supportsGoto && line) ? `${abs}:${line}${col ? `:${col}` : ''}` : abs;
|
|
91
|
+
const args = supportsGoto ? ['-g', target] : [abs];
|
|
92
|
+
spawn(cmd, args, { detached: true, stdio: 'ignore', env: cleanChildEnv() }).unref();
|
|
93
|
+
return { ok: true, opener: cmd };
|
|
94
|
+
}
|
|
95
|
+
return { ok: false, error: 'no editor found' };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Open a directory in the OS file manager (Finder on macOS, Nautilus etc on Linux).
|
|
100
|
+
*
|
|
101
|
+
* @param {{ cwd: string }} opts
|
|
102
|
+
* @returns {Promise<{ ok: true, opener: string } | { ok: false, error: string }>}
|
|
103
|
+
*/
|
|
104
|
+
async function openInFinder({ cwd }) {
|
|
105
|
+
const errStr = await shell.openPath(cwd);
|
|
106
|
+
if (errStr) return { ok: false, error: errStr };
|
|
107
|
+
return { ok: true, opener: 'shell' };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Open a terminal emulator in the given directory.
|
|
112
|
+
*
|
|
113
|
+
* Linux: tries gnome-terminal → konsole → xfce4-terminal → xterm.
|
|
114
|
+
* macOS: delegates to Terminal.app via `open -a`.
|
|
115
|
+
*
|
|
116
|
+
* Arg shapes per terminal are preserved verbatim from index.cjs (AC impl note):
|
|
117
|
+
* gnome-terminal → ['--working-directory=<cwd>']
|
|
118
|
+
* others → ['-e', "bash -c \"cd '<cwd>' && exec bash\""]
|
|
119
|
+
*
|
|
120
|
+
* @param {{ cwd: string }} opts
|
|
121
|
+
* @returns {Promise<{ ok: true, opener: string } | { ok: false, error: string }>}
|
|
122
|
+
*/
|
|
123
|
+
async function openInTerminal({ cwd }) {
|
|
124
|
+
if (process.platform === 'linux') {
|
|
125
|
+
const terms = ['gnome-terminal', 'konsole', 'xfce4-terminal', 'xterm'];
|
|
126
|
+
for (const t of terms) {
|
|
127
|
+
if (!findCommand(t)) continue;
|
|
128
|
+
const args = t === 'gnome-terminal'
|
|
129
|
+
? ['--working-directory=' + cwd]
|
|
130
|
+
: ['-e', `bash -c "cd '${cwd.replace(/'/g, "'\\''")}' && exec bash"`];
|
|
131
|
+
spawn(t, args, { detached: true, stdio: 'ignore', env: cleanChildEnv() }).unref();
|
|
132
|
+
return { ok: true, opener: t };
|
|
133
|
+
}
|
|
134
|
+
} else if (process.platform === 'darwin') {
|
|
135
|
+
spawn('open', ['-a', 'Terminal', cwd], { detached: true, stdio: 'ignore', env: cleanChildEnv() }).unref();
|
|
136
|
+
return { ok: true, opener: 'Terminal.app' };
|
|
137
|
+
}
|
|
138
|
+
return { ok: false, error: 'no terminal found' };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
module.exports = { findCommand, openInEditor, openFileInEditor, openInFinder, openInTerminal };
|
|
@@ -28,6 +28,7 @@ const { schemas } = require('./ipcSchemas.cjs');
|
|
|
28
28
|
const SLUG_RE = /^[a-z0-9\-/]+$/;
|
|
29
29
|
const MAX_LINE_BYTES = 16 * 1024;
|
|
30
30
|
const KILL_AFTER_MS = 5 * 60 * 1000; // 5 min hard ceiling per install
|
|
31
|
+
const KILL_GRACE_MS = 5_000; // SIGTERM → SIGKILL escalation window
|
|
31
32
|
|
|
32
33
|
let mainWindow = null;
|
|
33
34
|
const inFlight = new Map(); // slug -> proc
|
|
@@ -81,9 +82,28 @@ function install({ slug }) {
|
|
|
81
82
|
inFlight.set(slug, proc);
|
|
82
83
|
|
|
83
84
|
let lineBuf = '';
|
|
85
|
+
let settled = false;
|
|
86
|
+
|
|
84
87
|
const killTimer = setTimeout(() => {
|
|
85
|
-
try { proc.kill(); } catch { /* */ }
|
|
88
|
+
try { proc.kill('SIGTERM'); } catch { /* */ }
|
|
89
|
+
// Escalate to SIGKILL after KILL_GRACE_MS if the pty hasn't exited.
|
|
90
|
+
const escalate = setTimeout(() => {
|
|
91
|
+
try { proc.kill('SIGKILL'); } catch { /* already dead */ }
|
|
92
|
+
}, KILL_GRACE_MS);
|
|
93
|
+
if (escalate.unref) escalate.unref();
|
|
86
94
|
}, KILL_AFTER_MS);
|
|
95
|
+
if (killTimer.unref) killTimer.unref();
|
|
96
|
+
|
|
97
|
+
// Belt-and-suspenders: if onExit never fires (broken pty event path after
|
|
98
|
+
// SIGKILL — analogous to anthropics/claude-code #61735's unreachable pts),
|
|
99
|
+
// force-release the inFlight lock so the slug isn't permanently stuck.
|
|
100
|
+
const deadman = setTimeout(() => {
|
|
101
|
+
if (settled) return;
|
|
102
|
+
settled = true;
|
|
103
|
+
inFlight.delete(slug);
|
|
104
|
+
resolve({ ok: false, exitCode: -1, error: 'install hung — pty onExit never fired' });
|
|
105
|
+
}, KILL_AFTER_MS + KILL_GRACE_MS + 30_000);
|
|
106
|
+
if (deadman.unref) deadman.unref();
|
|
87
107
|
|
|
88
108
|
proc.onData((data) => {
|
|
89
109
|
lineBuf += data;
|
|
@@ -101,7 +121,10 @@ function install({ slug }) {
|
|
|
101
121
|
});
|
|
102
122
|
|
|
103
123
|
proc.onExit(({ exitCode }) => {
|
|
124
|
+
if (settled) return;
|
|
125
|
+
settled = true;
|
|
104
126
|
clearTimeout(killTimer);
|
|
127
|
+
clearTimeout(deadman);
|
|
105
128
|
if (lineBuf.length > 0) {
|
|
106
129
|
send('plugins:install-progress', { slug, line: lineBuf });
|
|
107
130
|
lineBuf = '';
|
|
@@ -124,6 +147,21 @@ function registerPluginInstallHandlers() {
|
|
|
124
147
|
}
|
|
125
148
|
return install({ slug: parsed.data.slug });
|
|
126
149
|
});
|
|
150
|
+
|
|
151
|
+
// plugins:abort — send SIGKILL to a stuck install and release the inFlight
|
|
152
|
+
// lock immediately, analogous to pty:kill. UI wiring is a follow-up PRD.
|
|
153
|
+
ipcMain.handle('plugins:abort', async (_e, payload) => {
|
|
154
|
+
const parsed = schemas.pluginsAbort.safeParse(payload);
|
|
155
|
+
if (!parsed.success) {
|
|
156
|
+
return { ok: false, error: 'invalid slug' };
|
|
157
|
+
}
|
|
158
|
+
const { slug } = parsed.data;
|
|
159
|
+
const proc = inFlight.get(slug);
|
|
160
|
+
if (!proc) return { ok: false, error: 'no install in progress for slug' };
|
|
161
|
+
try { proc.kill('SIGKILL'); } catch { /* */ }
|
|
162
|
+
inFlight.delete(slug);
|
|
163
|
+
return { ok: true };
|
|
164
|
+
});
|
|
127
165
|
}
|
|
128
166
|
|
|
129
167
|
module.exports = { registerPluginInstallHandlers, attachWindow };
|
package/src/main/pty.cjs
CHANGED
|
@@ -5,7 +5,7 @@ const os = require('node:os');
|
|
|
5
5
|
const fs = require('node:fs');
|
|
6
6
|
const { addAllowedRoot } = require('./config.cjs');
|
|
7
7
|
const { cleanChildEnv } = require('./lib/cleanEnv.cjs');
|
|
8
|
-
const {
|
|
8
|
+
const { checkInsideHome } = require('./lib/insideHome.cjs');
|
|
9
9
|
const { sendIfAlive } = require('./lib/sendToRenderer.cjs');
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -28,9 +28,9 @@ class PtyManager {
|
|
|
28
28
|
|
|
29
29
|
// Validate that cwd is inside homedir before widening the allowed-root set.
|
|
30
30
|
if (cwd) {
|
|
31
|
-
const r =
|
|
31
|
+
const r = checkInsideHome(cwd);
|
|
32
32
|
if (!r.ok) throw new Error(`pty ${r.error}`);
|
|
33
|
-
addAllowedRoot(r.
|
|
33
|
+
addAllowedRoot(r.realPath);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
// Idempotent reattach: renderer reloads (HMR/Ctrl+R) re-run App.tsx's
|
|
@@ -164,6 +164,9 @@ class PtyManager {
|
|
|
164
164
|
}
|
|
165
165
|
this.sessions.delete(tabId);
|
|
166
166
|
}
|
|
167
|
+
// Always drop superagent run state — a run can be started before the pty
|
|
168
|
+
// finishes spawning, so clean up regardless of whether a session existed.
|
|
169
|
+
try { require('./superagent.cjs').dropTab(tabId); } catch { /* superagent not loaded (e2e) */ }
|
|
167
170
|
}
|
|
168
171
|
|
|
169
172
|
killAll() {
|
|
@@ -178,7 +181,11 @@ function registerPtyHandlers() {
|
|
|
178
181
|
ipcMain.handle('pty:spawn', v(s.ptySpawn, (payload) => manager.spawn(payload)));
|
|
179
182
|
ipcMain.on('pty:write', (_e, payload) => { try { manager.write(s.ptyWrite.parse(payload)); } catch { /* ignore */ } });
|
|
180
183
|
ipcMain.on('pty:resize', (_e, payload) => { try { manager.resize(s.ptyResize.parse(payload)); } catch { /* ignore */ } });
|
|
181
|
-
ipcMain.on('pty:kill', (_e, tabId) => {
|
|
184
|
+
ipcMain.on('pty:kill', (_e, tabId) => {
|
|
185
|
+
if (typeof tabId !== 'string') return;
|
|
186
|
+
manager.kill(tabId);
|
|
187
|
+
try { require('./superagent.cjs').dropTab(tabId); } catch { /* superagent module not initialized (e2e) */ }
|
|
188
|
+
});
|
|
182
189
|
}
|
|
183
190
|
|
|
184
191
|
module.exports = { manager, registerPtyHandlers };
|
package/src/main/queueOps.cjs
CHANGED
|
@@ -37,6 +37,7 @@ const { ipcMain } = require('electron');
|
|
|
37
37
|
const { SCHEDULE_SLUG_RE: SLUG_RE, schemas } = require('./ipcSchemas.cjs');
|
|
38
38
|
const logs = require('./logs.cjs');
|
|
39
39
|
const config = require('./config.cjs');
|
|
40
|
+
const { expandHome } = require('./lib/expandHome.cjs');
|
|
40
41
|
|
|
41
42
|
const ROOT = path.join(os.homedir(), '.claude', 'session-manager', 'scheduled-plans');
|
|
42
43
|
const PRDS_DIR = path.join(ROOT, 'prds');
|
|
@@ -114,8 +115,7 @@ function lintParsed(slug, raw) {
|
|
|
114
115
|
findings.push({ rule: 'missing-cwd', line: 1, snippet: 'frontmatter "cwd" is required', severity: 'error' });
|
|
115
116
|
} else {
|
|
116
117
|
// cwd existence — only if it looks like an absolute path. ~ -> homedir.
|
|
117
|
-
|
|
118
|
-
if (candidate.startsWith('~/')) candidate = path.join(os.homedir(), candidate.slice(2));
|
|
118
|
+
const candidate = expandHome(fm.cwd);
|
|
119
119
|
if (path.isAbsolute(candidate)) {
|
|
120
120
|
try {
|
|
121
121
|
fs.accessSync(candidate, fs.constants.F_OK);
|