claude-code-session-manager 0.29.0 → 0.31.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-DyiZVN0v.js → TiptapBody-Q7puQu_h.js} +1 -1
- package/dist/assets/{index-qS7GdCsL.js → index-hreMRE8u.js} +4 -4
- package/dist/index.html +1 -1
- package/package.json +3 -2
- package/scripts/postinstall.cjs +43 -0
- package/src/main/kg.cjs +41 -0
- package/src/main/lib/cleanEnv.cjs +32 -1
- package/src/main/lib/credentials.cjs +8 -2
- package/src/main/pluginInstall.cjs +2 -10
- package/src/main/pty.cjs +77 -15
- package/src/main/scheduler.cjs +5 -3
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-
|
|
10
|
+
<script type="module" crossorigin src="./assets/index-hreMRE8u.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.
|
|
3
|
+
"version": "0.31.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",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"bin/",
|
|
12
12
|
".claude-plugin/",
|
|
13
13
|
"plugins/",
|
|
14
|
+
"scripts/postinstall.cjs",
|
|
14
15
|
"src/main/",
|
|
15
16
|
"src/preload/",
|
|
16
17
|
"dist/index.html",
|
|
@@ -26,7 +27,7 @@
|
|
|
26
27
|
"build": "vite build",
|
|
27
28
|
"typecheck": "tsc --noEmit",
|
|
28
29
|
"start": "electron .",
|
|
29
|
-
"postinstall": "
|
|
30
|
+
"postinstall": "node scripts/postinstall.cjs",
|
|
30
31
|
"prepublishOnly": "vite build",
|
|
31
32
|
"test:unit": "vitest run",
|
|
32
33
|
"test:verify": "node src/main/__tests__/runVerify.test.cjs",
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* postinstall — rebuild node-pty against the bundled Electron's ABI.
|
|
6
|
+
*
|
|
7
|
+
* The terminal needs node-pty's native binary to match the Electron we ship.
|
|
8
|
+
* We run electron-rebuild, but a failure here MUST NOT fail `npm i -g` — that
|
|
9
|
+
* would block the entire app (config tabs, scheduler, etc.) over one native
|
|
10
|
+
* module. So we run it best-effort and, on failure, print the exact manual fix
|
|
11
|
+
* and exit 0. pty.cjs surfaces the same guidance in-app if a tab can't start.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const { spawnSync } = require('node:child_process');
|
|
15
|
+
|
|
16
|
+
function run() {
|
|
17
|
+
const r = spawnSync(
|
|
18
|
+
process.platform === 'win32' ? 'npx.cmd' : 'npx',
|
|
19
|
+
['electron-rebuild', '-f', '-w', 'node-pty'],
|
|
20
|
+
{ stdio: 'inherit' },
|
|
21
|
+
);
|
|
22
|
+
return r.status === 0 && !r.error;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (run()) process.exit(0);
|
|
26
|
+
|
|
27
|
+
const mac = process.platform === 'darwin';
|
|
28
|
+
const bar = '─'.repeat(64);
|
|
29
|
+
console.warn(`\n${bar}`);
|
|
30
|
+
console.warn('claude-code-session-manager: node-pty rebuild did not complete.');
|
|
31
|
+
console.warn('The app will still launch, but terminal tabs may not start until');
|
|
32
|
+
console.warn('the native module is rebuilt for this Electron. To fix it:');
|
|
33
|
+
console.warn('');
|
|
34
|
+
console.warn(' cd "' + __dirname.replace(/\/scripts$/, '') + '"');
|
|
35
|
+
console.warn(' npx electron-rebuild -f -w node-pty');
|
|
36
|
+
if (mac) {
|
|
37
|
+
console.warn('');
|
|
38
|
+
console.warn(' # macOS: if the rebuild fails, install the compiler first:');
|
|
39
|
+
console.warn(' xcode-select --install');
|
|
40
|
+
}
|
|
41
|
+
console.warn(`${bar}\n`);
|
|
42
|
+
// Exit 0 — never fail the install over the terminal native module.
|
|
43
|
+
process.exit(0);
|
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
|
-
|
|
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:
|
|
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
|
@@ -1,13 +1,57 @@
|
|
|
1
|
-
|
|
1
|
+
// Load the native node-pty lazily-guarded: if its prebuilt/rebuilt binary
|
|
2
|
+
// doesn't match this Electron's ABI, a bare top-level require would throw and
|
|
3
|
+
// crash the WHOLE app at startup (index.cjs requires this module). Instead we
|
|
4
|
+
// capture the error and surface an actionable message when a tab is opened, so
|
|
5
|
+
// the app still boots and the user sees exactly how to fix the terminal.
|
|
6
|
+
let pty = null;
|
|
7
|
+
let ptyLoadError = null;
|
|
8
|
+
try {
|
|
9
|
+
pty = require('node-pty');
|
|
10
|
+
} catch (e) {
|
|
11
|
+
ptyLoadError = e;
|
|
12
|
+
// eslint-disable-next-line no-console
|
|
13
|
+
console.error('[pty] node-pty failed to load:', e?.message);
|
|
14
|
+
}
|
|
2
15
|
const { ipcMain } = require('electron');
|
|
3
16
|
const path = require('node:path');
|
|
4
17
|
const os = require('node:os');
|
|
5
18
|
const fs = require('node:fs');
|
|
6
19
|
const { addAllowedRoot } = require('./config.cjs');
|
|
7
|
-
const { cleanChildEnv } = require('./lib/cleanEnv.cjs');
|
|
20
|
+
const { cleanChildEnv, pathWithUserBins } = require('./lib/cleanEnv.cjs');
|
|
8
21
|
const { checkInsideHome } = require('./lib/insideHome.cjs');
|
|
9
22
|
const { sendIfAlive } = require('./lib/sendToRenderer.cjs');
|
|
10
23
|
|
|
24
|
+
// Absolute path to the installed package root (src/main/ -> ../../), shown in
|
|
25
|
+
// the remediation message so the user can cd there and rebuild.
|
|
26
|
+
const PKG_DIR = path.join(__dirname, '..', '..');
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* ANSI-formatted terminal text explaining a native-module / immediate-exit
|
|
30
|
+
* failure and exactly how to fix it. Written straight into the tab's output so
|
|
31
|
+
* a dead terminal explains itself instead of just going blank.
|
|
32
|
+
*/
|
|
33
|
+
function nativeModuleHelp(reason) {
|
|
34
|
+
const mac = process.platform === 'darwin';
|
|
35
|
+
const lines = [
|
|
36
|
+
'',
|
|
37
|
+
`\x1b[1;33m[session-manager] Terminal could not start.\x1b[0m`,
|
|
38
|
+
`\x1b[33m${reason}\x1b[0m`,
|
|
39
|
+
'',
|
|
40
|
+
`This is almost always the node-pty native module not matching this`,
|
|
41
|
+
`Electron build. Rebuild it once:`,
|
|
42
|
+
'',
|
|
43
|
+
` \x1b[36mcd ${PKG_DIR}\x1b[0m`,
|
|
44
|
+
` \x1b[36mnpx electron-rebuild -f -w node-pty\x1b[0m`,
|
|
45
|
+
'',
|
|
46
|
+
mac ? `macOS: if the rebuild fails, install the compiler first:` : '',
|
|
47
|
+
mac ? ` \x1b[36mxcode-select --install\x1b[0m` : '',
|
|
48
|
+
mac ? '' : '',
|
|
49
|
+
`Then quit and reopen session-manager.`,
|
|
50
|
+
'',
|
|
51
|
+
].filter((l) => l !== '');
|
|
52
|
+
return lines.join('\r\n') + '\r\n';
|
|
53
|
+
}
|
|
54
|
+
|
|
11
55
|
/**
|
|
12
56
|
* PtyManager — owns every claude PTY process, keyed by tabId (renderer-generated UUID).
|
|
13
57
|
* Streams output to the renderer via IPC channels scoped by tabId.
|
|
@@ -26,6 +70,15 @@ class PtyManager {
|
|
|
26
70
|
spawn({ tabId, cwd, cols = 120, rows = 30 }) {
|
|
27
71
|
console.log('[pty] spawn requested', { tabId });
|
|
28
72
|
|
|
73
|
+
// Native module unavailable — explain in the tab and report a clean exit
|
|
74
|
+
// rather than throwing (which would surface as an opaque IPC error).
|
|
75
|
+
if (!pty || ptyLoadError) {
|
|
76
|
+
const reason = `node-pty failed to load: ${ptyLoadError?.message ?? 'module missing'}`;
|
|
77
|
+
sendIfAlive(this.window, `pty:data:${tabId}`, nativeModuleHelp(reason));
|
|
78
|
+
setImmediate(() => sendIfAlive(this.window, `pty:exit:${tabId}`, { exitCode: 1, signal: undefined }));
|
|
79
|
+
return { pid: null, cwd, reattached: false, error: 'node-pty unavailable' };
|
|
80
|
+
}
|
|
81
|
+
|
|
29
82
|
// Validate that cwd is inside homedir before widening the allowed-root set.
|
|
30
83
|
if (cwd) {
|
|
31
84
|
const r = checkInsideHome(cwd);
|
|
@@ -62,18 +115,11 @@ class PtyManager {
|
|
|
62
115
|
return { pid: existing.proc.pid, cwd: existing.cwd, reattached: true };
|
|
63
116
|
}
|
|
64
117
|
|
|
65
|
-
// Prefer `claude` on PATH; extend PATH with common user bin dirs
|
|
66
|
-
// Electron can launch with a stripped environment
|
|
67
|
-
|
|
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
|
-
|
|
118
|
+
// Prefer `claude` on PATH; extend PATH with common user + Homebrew bin dirs
|
|
119
|
+
// since Electron can launch with a stripped environment (Apple Silicon
|
|
120
|
+
// Homebrew is /opt/homebrew, absent from the default Electron PATH).
|
|
75
121
|
const env = cleanChildEnv({
|
|
76
|
-
PATH:
|
|
122
|
+
PATH: pathWithUserBins(),
|
|
77
123
|
TERM: 'xterm-256color',
|
|
78
124
|
COLORTERM: 'truecolor',
|
|
79
125
|
FORCE_COLOR: '1',
|
|
@@ -95,11 +141,19 @@ class PtyManager {
|
|
|
95
141
|
});
|
|
96
142
|
} catch (err) {
|
|
97
143
|
console.error('[pty] pty.spawn threw:', err);
|
|
98
|
-
|
|
144
|
+
// Surface the cause in the tab + report a clean exit rather than throwing
|
|
145
|
+
// an opaque IPC error (covers a missing/invalid cwd or a native fault).
|
|
146
|
+
sendIfAlive(this.window, `pty:data:${tabId}`, nativeModuleHelp(`pty.spawn failed: ${err?.message ?? String(err)}`));
|
|
147
|
+
setImmediate(() => sendIfAlive(this.window, `pty:exit:${tabId}`, { exitCode: 1, signal: undefined }));
|
|
148
|
+
return { pid: null, cwd, reattached: false, error: String(err?.message || 'spawn-failed') };
|
|
99
149
|
}
|
|
100
150
|
console.log('[pty] spawned pid=', proc.pid, 'for tabId=', tabId);
|
|
101
151
|
|
|
152
|
+
const spawnedAt = Date.now();
|
|
153
|
+
let gotData = false;
|
|
154
|
+
|
|
102
155
|
proc.onData((data) => {
|
|
156
|
+
gotData = true;
|
|
103
157
|
sendIfAlive(this.window, `pty:data:${tabId}`, data);
|
|
104
158
|
});
|
|
105
159
|
|
|
@@ -112,11 +166,19 @@ class PtyManager {
|
|
|
112
166
|
this.sessions.delete(tabId);
|
|
113
167
|
return;
|
|
114
168
|
}
|
|
169
|
+
// Fast-exit detector: a shell that dies in <1.2s with a non-zero status
|
|
170
|
+
// and never printed anything almost certainly couldn't exec (broken
|
|
171
|
+
// node-pty spawn-helper / ABI on macOS). Explain it in the tab instead of
|
|
172
|
+
// leaving a blank, instantly-closed terminal. Guarded by !gotData so a
|
|
173
|
+
// real interactive shell (which prints a prompt immediately) is exempt.
|
|
174
|
+
if (!gotData && exitCode !== 0 && Date.now() - spawnedAt < 1200) {
|
|
175
|
+
sendIfAlive(this.window, `pty:data:${tabId}`, nativeModuleHelp(`The shell exited immediately (code ${exitCode}).`));
|
|
176
|
+
}
|
|
115
177
|
sendIfAlive(this.window, `pty:exit:${tabId}`, { exitCode, signal });
|
|
116
178
|
this.sessions.delete(tabId);
|
|
117
179
|
});
|
|
118
180
|
|
|
119
|
-
this.sessions.set(tabId, { proc, cwd, created:
|
|
181
|
+
this.sessions.set(tabId, { proc, cwd, created: spawnedAt });
|
|
120
182
|
return { pid: proc.pid, cwd, reattached: false };
|
|
121
183
|
}
|
|
122
184
|
|
package/src/main/scheduler.cjs
CHANGED
|
@@ -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
|
-
|
|
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.
|