claude-code-session-manager 0.8.6 → 0.10.1
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/README.md +95 -65
- package/dist/assets/{cssMode-DBg6nxUL.js → cssMode-AMMSQWRU.js} +1 -1
- package/dist/assets/{freemarker2-CyjUGY3f.js → freemarker2-B7HmOKy0.js} +1 -1
- package/dist/assets/{handlebars-lhtCWqlB.js → handlebars-DMhLYNJA.js} +1 -1
- package/dist/assets/{html-egptHwbZ.js → html-DIfRfeTv.js} +1 -1
- package/dist/assets/htmlMode-DBqG7xl_.js +1 -0
- package/dist/assets/{index-DjeqNwqn.js → index-BqL_4JKo.js} +1120 -1086
- package/dist/assets/{index-DnLtSCQS.css → index-CxncC9a0.css} +1 -1
- package/dist/assets/{javascript-tZbiID3O.js → javascript-Bsn1K6_V.js} +1 -1
- package/dist/assets/{jsonMode-BGtPN-L-.js → jsonMode-DVLHW2S3.js} +1 -1
- package/dist/assets/{liquid-DvTeXhev.js → liquid-9zGHPTSW.js} +1 -1
- package/dist/assets/{lspLanguageFeatures-D9xoxVlV.js → lspLanguageFeatures-BaqyiCFJ.js} +2 -2
- package/dist/assets/{mdx-BQ3Ja4wM.js → mdx-DhU2NoOP.js} +1 -1
- package/dist/assets/{ort-wasm-simd-threaded.asyncify-CtKKja6V.wasm → ort-wasm-simd-threaded.asyncify-DMmc6YqF.wasm} +0 -0
- package/dist/assets/{python-C71RWXaP.js → python-WMVORwGT.js} +1 -1
- package/dist/assets/{razor-w__Mkyns.js → razor-C13iUMo9.js} +1 -1
- package/dist/assets/{tsMode-DOQLQDB3.js → tsMode-B7lSY5y7.js} +1 -1
- package/dist/assets/{typescript-DEiub2Jt.js → typescript-BbjzsO4g.js} +1 -1
- package/dist/assets/{whisperWorker-QfIS0sPF.js → whisperWorker-CcsPqZUS.js} +19 -19
- package/dist/assets/{xml-RXkLQscS.js → xml-BH6qG4pe.js} +1 -1
- package/dist/assets/{yaml-C8HIpJku.js → yaml-B4r3z4qP.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +19 -10
- package/screenshots/.gitkeep +0 -0
- package/screenshots/README-screenshots.md +13 -0
- package/src/main/config.cjs +47 -9
- package/src/main/historyAggregator.cjs +10 -5
- package/src/main/index.cjs +186 -14
- package/src/main/ipcSchemas.cjs +165 -3
- package/src/main/lib/claudeBin.cjs +39 -0
- package/src/main/lib/encodeCwd.cjs +19 -0
- package/src/main/lib/fileTail.cjs +35 -0
- package/src/main/lib/insideHome.cjs +38 -0
- package/src/main/lib/prdFrontmatter.cjs +51 -0
- package/src/main/lib/sendToRenderer.cjs +21 -0
- package/src/main/memoryTool.cjs +203 -0
- package/src/main/otelSettings.cjs +2 -7
- package/src/main/pluginInstall.cjs +129 -0
- package/src/main/pty.cjs +13 -29
- package/src/main/queueOps.cjs +404 -0
- package/src/main/scheduler/prdParser.cjs +135 -0
- package/src/main/scheduler.cjs +296 -255
- package/src/main/sessionsStore.cjs +2 -6
- package/src/main/supervisor.cjs +3 -35
- package/src/main/teams.cjs +95 -0
- package/src/main/transcripts.cjs +5 -7
- package/src/main/usage.cjs +8 -0
- package/src/main/voiceHotkey.cjs +13 -9
- package/src/main/voiceSettings.cjs +2 -9
- package/src/main/voiceWizard.cjs +4 -11
- package/src/main/watchers.cjs +18 -42
- package/src/preload/api.d.ts +153 -1
- package/src/preload/index.cjs +29 -0
- package/dist/assets/htmlMode-tPDeHGOB.js +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{l as e}from"./index-
|
|
1
|
+
import{l as e}from"./index-BqL_4JKo.js";const n={comments:{blockComment:["<!--","-->"]},brackets:[["<",">"]],autoClosingPairs:[{open:"<",close:">"},{open:"'",close:"'"},{open:'"',close:'"'}],surroundingPairs:[{open:"<",close:">"},{open:"'",close:"'"},{open:'"',close:'"'}],onEnterRules:[{beforeText:new RegExp("<([_:\\w][_:\\w-.\\d]*)([^/>]*(?!/)>)[^<]*$","i"),afterText:/^<\/([_:\w][_:\w-.\d]*)\s*>$/i,action:{indentAction:e.IndentAction.IndentOutdent}},{beforeText:new RegExp("<(\\w[\\w\\d]*)([^/>]*(?!/)>)[^<]*$","i"),action:{indentAction:e.IndentAction.Indent}}]},o={defaultToken:"",tokenPostfix:".xml",ignoreCase:!0,qualifiedName:/(?:[\w\.\-]+:)?[\w\.\-]+/,tokenizer:{root:[[/[^<&]+/,""],{include:"@whitespace"},[/(<)(@qualifiedName)/,[{token:"delimiter"},{token:"tag",next:"@tag"}]],[/(<\/)(@qualifiedName)(\s*)(>)/,[{token:"delimiter"},{token:"tag"},"",{token:"delimiter"}]],[/(<\?)(@qualifiedName)/,[{token:"delimiter"},{token:"metatag",next:"@tag"}]],[/(<\!)(@qualifiedName)/,[{token:"delimiter"},{token:"metatag",next:"@tag"}]],[/<\!\[CDATA\[/,{token:"delimiter.cdata",next:"@cdata"}],[/&\w+;/,"string.escape"]],cdata:[[/[^\]]+/,""],[/\]\]>/,{token:"delimiter.cdata",next:"@pop"}],[/\]/,""]],tag:[[/[ \t\r\n]+/,""],[/(@qualifiedName)(\s*=\s*)("[^"]*"|'[^']*')/,["attribute.name","","attribute.value"]],[/(@qualifiedName)(\s*=\s*)("[^">?\/]*|'[^'>?\/]*)(?=[\?\/]\>)/,["attribute.name","","attribute.value"]],[/(@qualifiedName)(\s*=\s*)("[^">]*|'[^'>]*)/,["attribute.name","","attribute.value"]],[/@qualifiedName/,"attribute.name"],[/\?>/,{token:"delimiter",next:"@pop"}],[/(\/)(>)/,[{token:"tag"},{token:"delimiter",next:"@pop"}]],[/>/,{token:"delimiter",next:"@pop"}]],whitespace:[[/[ \t\r\n]+/,""],[/<!--/,{token:"comment",next:"@comment"}]],comment:[[/[^<\-]+/,"comment.content"],[/-->/,{token:"comment",next:"@pop"}],[/<!--/,"comment.content.invalid"],[/[<\-]/,"comment.content"]]}};export{n as conf,o as language};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{l as e}from"./index-
|
|
1
|
+
import{l as e}from"./index-BqL_4JKo.js";const t={comments:{lineComment:"#"},brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],folding:{offSide:!0},onEnterRules:[{beforeText:/:\s*$/,action:{indentAction:e.IndentAction.Indent}}]},o={tokenPostfix:".yaml",brackets:[{token:"delimiter.bracket",open:"{",close:"}"},{token:"delimiter.square",open:"[",close:"]"}],keywords:["true","True","TRUE","false","False","FALSE","null","Null","Null","~"],numberInteger:/(?:0|[+-]?[0-9]+)/,numberFloat:/(?:0|[+-]?[0-9]+)(?:\.[0-9]+)?(?:e[-+][1-9][0-9]*)?/,numberOctal:/0o[0-7]+/,numberHex:/0x[0-9a-fA-F]+/,numberInfinity:/[+-]?\.(?:inf|Inf|INF)/,numberNaN:/\.(?:nan|Nan|NAN)/,numberDate:/\d{4}-\d\d-\d\d([Tt ]\d\d:\d\d:\d\d(\.\d+)?(( ?[+-]\d\d?(:\d\d)?)|Z)?)?/,escapes:/\\(?:[btnfr\\"']|[0-7][0-7]?|[0-3][0-7]{2})/,tokenizer:{root:[{include:"@whitespace"},{include:"@comment"},[/%[^ ]+.*$/,"meta.directive"],[/---/,"operators.directivesEnd"],[/\.{3}/,"operators.documentEnd"],[/[-?:](?= )/,"operators"],{include:"@anchor"},{include:"@tagHandle"},{include:"@flowCollections"},{include:"@blockStyle"},[/@numberInteger(?![ \t]*\S+)/,"number"],[/@numberFloat(?![ \t]*\S+)/,"number.float"],[/@numberOctal(?![ \t]*\S+)/,"number.octal"],[/@numberHex(?![ \t]*\S+)/,"number.hex"],[/@numberInfinity(?![ \t]*\S+)/,"number.infinity"],[/@numberNaN(?![ \t]*\S+)/,"number.nan"],[/@numberDate(?![ \t]*\S+)/,"number.date"],[/(".*?"|'.*?'|[^#'"]*?)([ \t]*)(:)( |$)/,["type","white","operators","white"]],{include:"@flowScalars"},[/.+?(?=(\s+#|$))/,{cases:{"@keywords":"keyword","@default":"string"}}]],object:[{include:"@whitespace"},{include:"@comment"},[/\}/,"@brackets","@pop"],[/,/,"delimiter.comma"],[/:(?= )/,"operators"],[/(?:".*?"|'.*?'|[^,\{\[]+?)(?=: )/,"type"],{include:"@flowCollections"},{include:"@flowScalars"},{include:"@tagHandle"},{include:"@anchor"},{include:"@flowNumber"},[/[^\},]+/,{cases:{"@keywords":"keyword","@default":"string"}}]],array:[{include:"@whitespace"},{include:"@comment"},[/\]/,"@brackets","@pop"],[/,/,"delimiter.comma"],{include:"@flowCollections"},{include:"@flowScalars"},{include:"@tagHandle"},{include:"@anchor"},{include:"@flowNumber"},[/[^\],]+/,{cases:{"@keywords":"keyword","@default":"string"}}]],multiString:[[/^( +).+$/,"string","@multiStringContinued.$1"]],multiStringContinued:[[/^( *).+$/,{cases:{"$1==$S2":"string","@default":{token:"@rematch",next:"@popall"}}}]],whitespace:[[/[ \t\r\n]+/,"white"]],comment:[[/#.*$/,"comment"]],flowCollections:[[/\[/,"@brackets","@array"],[/\{/,"@brackets","@object"]],flowScalars:[[/"([^"\\]|\\.)*$/,"string.invalid"],[/'([^'\\]|\\.)*$/,"string.invalid"],[/'[^']*'/,"string"],[/"/,"string","@doubleQuotedString"]],doubleQuotedString:[[/[^\\"]+/,"string"],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/"/,"string","@pop"]],blockStyle:[[/[>|][0-9]*[+-]?$/,"operators","@multiString"]],flowNumber:[[/@numberInteger(?=[ \t]*[,\]\}])/,"number"],[/@numberFloat(?=[ \t]*[,\]\}])/,"number.float"],[/@numberOctal(?=[ \t]*[,\]\}])/,"number.octal"],[/@numberHex(?=[ \t]*[,\]\}])/,"number.hex"],[/@numberInfinity(?=[ \t]*[,\]\}])/,"number.infinity"],[/@numberNaN(?=[ \t]*[,\]\}])/,"number.nan"],[/@numberDate(?=[ \t]*[,\]\}])/,"number.date"]],tagHandle:[[/\![^ ]*/,"tag"]],anchor:[[/[&*][^ ]+/,"namespace"]]}};export{t as conf,o as language};
|
package/dist/index.html
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>Claude Session Manager</title>
|
|
7
|
-
<script type="module" crossorigin src="./assets/index-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
7
|
+
<script type="module" crossorigin src="./assets/index-BqL_4JKo.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="./assets/index-CxncC9a0.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body class="bg-bg text-fg font-mono antialiased">
|
|
11
11
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-session-manager",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Local cockpit for Claude Code CLI
|
|
3
|
+
"version": "0.10.1",
|
|
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",
|
|
7
7
|
"bin": {
|
|
@@ -14,13 +14,13 @@
|
|
|
14
14
|
"dist/index.html",
|
|
15
15
|
"dist/assets/",
|
|
16
16
|
"dist/vad/",
|
|
17
|
+
"screenshots/",
|
|
17
18
|
"README.md"
|
|
18
19
|
],
|
|
19
20
|
"scripts": {
|
|
20
21
|
"dev": "concurrently -k -n vite,electron -c magenta,cyan \"npm:dev:renderer\" \"npm:dev:electron\"",
|
|
21
22
|
"dev:renderer": "vite",
|
|
22
23
|
"dev:electron": "wait-on http://localhost:5173 && SM_DEV=1 electron .",
|
|
23
|
-
"build:renderer": "vite build",
|
|
24
24
|
"build": "vite build",
|
|
25
25
|
"typecheck": "tsc --noEmit",
|
|
26
26
|
"start": "electron .",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"prepublishOnly": "vite build",
|
|
29
29
|
"test:unit": "vitest run",
|
|
30
30
|
"test:e2e": "xvfb-run -a playwright test",
|
|
31
|
+
"smoke:darwin": "playwright test tests/smoke/darwin-boot.spec.ts",
|
|
31
32
|
"test:e2e:mic": "xvfb-run -a playwright test e2e/mic.spec.mjs",
|
|
32
33
|
"test:e2e:gen-fixture": "espeak-ng -w /tmp/sm-raw.wav -s 140 'testing one two three four five' && ffmpeg -y -i /tmp/sm-raw.wav -ar 48000 -ac 2 -sample_fmt s16 e2e/fixtures/speech.wav",
|
|
33
34
|
"refresh:vad-assets": "cp node_modules/@ricky0123/vad-web/dist/silero_vad_v5.onnx node_modules/@ricky0123/vad-web/dist/vad.worklet.bundle.min.js src/renderer/public/vad/ && cp node_modules/onnxruntime-web/dist/ort-wasm-simd-threaded.wasm node_modules/onnxruntime-web/dist/ort-wasm-simd-threaded.mjs node_modules/onnxruntime-web/dist/ort-wasm-simd-threaded.jsep.wasm node_modules/onnxruntime-web/dist/ort-wasm-simd-threaded.jsep.mjs src/renderer/public/vad/"
|
|
@@ -39,7 +40,15 @@
|
|
|
39
40
|
"terminal",
|
|
40
41
|
"electron",
|
|
41
42
|
"session-manager",
|
|
42
|
-
"cli"
|
|
43
|
+
"cli",
|
|
44
|
+
"cockpit",
|
|
45
|
+
"mcp",
|
|
46
|
+
"agents",
|
|
47
|
+
"hooks",
|
|
48
|
+
"scheduler",
|
|
49
|
+
"voice",
|
|
50
|
+
"whisper",
|
|
51
|
+
"prd"
|
|
43
52
|
],
|
|
44
53
|
"author": "bilkobibitkov",
|
|
45
54
|
"license": "MIT",
|
|
@@ -63,10 +72,9 @@
|
|
|
63
72
|
"linux"
|
|
64
73
|
],
|
|
65
74
|
"dependencies": {
|
|
66
|
-
"@electron/rebuild": "
|
|
75
|
+
"@electron/rebuild": "4.0.4",
|
|
67
76
|
"@huggingface/transformers": "^4.1.0",
|
|
68
77
|
"@monaco-editor/react": "^4.6.0",
|
|
69
|
-
"monaco-editor": "^0.55.0",
|
|
70
78
|
"@opentelemetry/api": "^1.9.0",
|
|
71
79
|
"@opentelemetry/exporter-trace-otlp-http": "^0.57.0",
|
|
72
80
|
"@opentelemetry/resources": "^1.30.0",
|
|
@@ -78,9 +86,12 @@
|
|
|
78
86
|
"@xterm/addon-web-links": "^0.11.0",
|
|
79
87
|
"@xterm/xterm": "^5.5.0",
|
|
80
88
|
"chokidar": "^4.0.1",
|
|
81
|
-
"electron": "
|
|
89
|
+
"electron": "42.1.0",
|
|
90
|
+
"framer-motion": "^12.38.0",
|
|
91
|
+
"monaco-editor": "0.55.1",
|
|
82
92
|
"node-pty": "^1.2.0-beta.12",
|
|
83
93
|
"onnxruntime-web": "^1.24.3",
|
|
94
|
+
"recharts": "^2.15.4",
|
|
84
95
|
"zod": "^3.23.8",
|
|
85
96
|
"zustand": "^5.0.0"
|
|
86
97
|
},
|
|
@@ -92,15 +103,13 @@
|
|
|
92
103
|
"@vitejs/plugin-react": "^4.3.3",
|
|
93
104
|
"autoprefixer": "^10.4.20",
|
|
94
105
|
"concurrently": "^9.1.0",
|
|
95
|
-
"framer-motion": "^12.38.0",
|
|
96
106
|
"postcss": "^8.4.49",
|
|
97
107
|
"react": "^18.3.1",
|
|
98
108
|
"react-dom": "^18.3.1",
|
|
99
|
-
"recharts": "^2.15.4",
|
|
100
109
|
"tailwindcss": "^3.4.15",
|
|
101
110
|
"typescript": "^5.6.3",
|
|
102
111
|
"vite": "^6.0.1",
|
|
103
|
-
"vitest": "
|
|
112
|
+
"vitest": "4.1.6",
|
|
104
113
|
"wait-on": "^8.0.1"
|
|
105
114
|
}
|
|
106
115
|
}
|
|
File without changes
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Screenshots needed
|
|
2
|
+
|
|
3
|
+
The repo README references these six screenshots. Each one is a TODO — the
|
|
4
|
+
images do not exist yet. PNG, target 1500×950, under 200 KB each (pngquant).
|
|
5
|
+
|
|
6
|
+
1. **`overview-cockpit.png`** — TODO: capture this view. Overview tab full window. Captures AppStatusBar pills at the top, CockpitStrip, the two-by-five instrument grid, TeamsCard (if enabled), system row, and the quick-actions footer.
|
|
7
|
+
2. **`agent-view.png`** — TODO: capture this view. Agent-View tab with the animated workshop scene visible: subagent dock, plan whiteboard, todo board, and the SchedulerDock with at least one mini-bot active.
|
|
8
|
+
3. **`command-palette.png`** — TODO: capture this view. CommandPalette open over any tab, ideally with the scheduler section expanded to show several scheduler commands (Run now / Force tick / Lint queue).
|
|
9
|
+
4. **`scheduler.png`** — TODO: capture this view. Plans tab → PRDs sub-view. PRD list with multi-select checkboxes on the left, structured editor in the centre, queue-health findings panel visible.
|
|
10
|
+
5. **`voice.png`** — TODO: capture this view. Voice subsystem — either the MicWizard first-run dialog with device picker and sample utterance UI, or the in-tab voice settings panel with a live VAD trace.
|
|
11
|
+
6. **`hooks-events.png`** — TODO: capture this view. Hooks tab with the sidebar listing all 29 events and one event hovered to show the inline description tooltip from `hookEventDocs.ts`.
|
|
12
|
+
|
|
13
|
+
Total image budget across all six: roughly 1 MB so the npm tarball does not bloat.
|
package/src/main/config.cjs
CHANGED
|
@@ -20,6 +20,8 @@ const fsp = require('node:fs/promises');
|
|
|
20
20
|
const path = require('node:path');
|
|
21
21
|
const os = require('node:os');
|
|
22
22
|
const chokidar = require('chokidar');
|
|
23
|
+
const logs = require('./logs.cjs');
|
|
24
|
+
const { sendIfAlive } = require('./lib/sendToRenderer.cjs');
|
|
23
25
|
|
|
24
26
|
/** Map<absPath, {watcher, refCount}> — one chokidar watcher per path. */
|
|
25
27
|
const watchers = new Map();
|
|
@@ -157,16 +159,29 @@ async function readText(abs) {
|
|
|
157
159
|
|
|
158
160
|
/**
|
|
159
161
|
* Atomic write — write to <path>.tmp-<pid>-<ts> then rename. Creates parent
|
|
160
|
-
* directories if missing.
|
|
162
|
+
* directories if missing. Cleans up the tmp file on rename failure.
|
|
163
|
+
*
|
|
164
|
+
* opts.mode: optional POSIX permission bits, applied to the tmp file before
|
|
165
|
+
* rename. Used by voiceSettings/otelSettings for 0o600 credential storage.
|
|
161
166
|
*/
|
|
162
|
-
async function writeTextAtomic(abs, text) {
|
|
167
|
+
async function writeTextAtomic(abs, text, opts = {}) {
|
|
163
168
|
const real = validatePath(expandHome(abs));
|
|
164
169
|
validateWrite(real);
|
|
165
170
|
const dir = path.dirname(real);
|
|
166
171
|
await fsp.mkdir(dir, { recursive: true });
|
|
167
172
|
const tmp = `${real}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
168
|
-
|
|
169
|
-
|
|
173
|
+
try {
|
|
174
|
+
await fsp.writeFile(tmp, text, opts.mode ? { encoding: 'utf8', mode: opts.mode } : 'utf8');
|
|
175
|
+
if (opts.mode) {
|
|
176
|
+
// chmod explicitly because some platforms ignore the mode arg on
|
|
177
|
+
// writeFile when the file pre-exists.
|
|
178
|
+
try { await fsp.chmod(tmp, opts.mode); } catch { /* */ }
|
|
179
|
+
}
|
|
180
|
+
await fsp.rename(tmp, real);
|
|
181
|
+
} catch (e) {
|
|
182
|
+
try { await fsp.unlink(tmp); } catch { /* tmp never created or already gone */ }
|
|
183
|
+
throw e;
|
|
184
|
+
}
|
|
170
185
|
const stat = await fsp.stat(real);
|
|
171
186
|
return { ok: true, mtimeMs: stat.mtimeMs };
|
|
172
187
|
}
|
|
@@ -176,6 +191,28 @@ async function writeJson(abs, data) {
|
|
|
176
191
|
return writeTextAtomic(abs, pretty);
|
|
177
192
|
}
|
|
178
193
|
|
|
194
|
+
/**
|
|
195
|
+
* Sync variant — only for child-exit handlers and similar callbacks where
|
|
196
|
+
* an event-loop yield would deadlock the caller. Use writeJson/writeTextAtomic
|
|
197
|
+
* for everything else.
|
|
198
|
+
*/
|
|
199
|
+
function writeJsonSync(abs, data) {
|
|
200
|
+
const real = validatePath(expandHome(abs));
|
|
201
|
+
validateWrite(real);
|
|
202
|
+
const dir = path.dirname(real);
|
|
203
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
204
|
+
const tmp = `${real}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
205
|
+
try {
|
|
206
|
+
fs.writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n', 'utf8');
|
|
207
|
+
fs.renameSync(tmp, real);
|
|
208
|
+
} catch (e) {
|
|
209
|
+
try { fs.unlinkSync(tmp); } catch { /* tmp never created or already gone */ }
|
|
210
|
+
throw e;
|
|
211
|
+
}
|
|
212
|
+
const stat = fs.statSync(real);
|
|
213
|
+
return { ok: true, mtimeMs: stat.mtimeMs };
|
|
214
|
+
}
|
|
215
|
+
|
|
179
216
|
async function listDir(abs, { filesOnly = false, dirsOnly = false, includeHidden = false } = {}) {
|
|
180
217
|
abs = validatePath(expandHome(abs));
|
|
181
218
|
try {
|
|
@@ -257,9 +294,7 @@ function watch(paths) {
|
|
|
257
294
|
} catch {
|
|
258
295
|
/* file may have been deleted */
|
|
259
296
|
}
|
|
260
|
-
|
|
261
|
-
window.webContents.send('config:changed', { path: key, mtimeMs, kind });
|
|
262
|
-
}
|
|
297
|
+
sendIfAlive(window, 'config:changed', { path: key, mtimeMs, kind });
|
|
263
298
|
};
|
|
264
299
|
w.on('add', emit('add'));
|
|
265
300
|
w.on('change', emit('change'));
|
|
@@ -299,8 +334,8 @@ function registerConfigHandlers() {
|
|
|
299
334
|
ipcMain.handle('config:write-text', v(s.configWriteText, ({ path: p, text }) => writeTextAtomic(p, text)));
|
|
300
335
|
ipcMain.handle('config:list-dir', v(s.configListDir, ({ path: p, opts }) => listDir(p, opts || {})));
|
|
301
336
|
ipcMain.handle('config:exists', v(s.configPath, ({ path: p }) => exists(p)));
|
|
302
|
-
ipcMain.on('config:watch', (_e, { paths }) => { try { s.configWatch.parse(paths); watch(paths); } catch {
|
|
303
|
-
ipcMain.on('config:unwatch', (_e, { paths }) => { try { s.configWatch.parse(paths); unwatch(paths); } catch {
|
|
337
|
+
ipcMain.on('config:watch', (_e, { paths }) => { try { s.configWatch.parse(paths); watch(paths); } catch (e) { logs.writeLine({ level: 'warn', scope: 'config', message: 'config:watch schema reject', meta: { error: e?.message } }); } });
|
|
338
|
+
ipcMain.on('config:unwatch', (_e, { paths }) => { try { s.configWatch.parse(paths); unwatch(paths); } catch (e) { logs.writeLine({ level: 'warn', scope: 'config', message: 'config:unwatch schema reject', meta: { error: e?.message } }); } });
|
|
304
339
|
}
|
|
305
340
|
|
|
306
341
|
module.exports = {
|
|
@@ -311,8 +346,11 @@ module.exports = {
|
|
|
311
346
|
readJson,
|
|
312
347
|
readText,
|
|
313
348
|
writeJson,
|
|
349
|
+
writeJsonSync,
|
|
314
350
|
writeTextAtomic,
|
|
315
351
|
listDir,
|
|
316
352
|
exists,
|
|
317
353
|
addAllowedRoot,
|
|
354
|
+
validatePath,
|
|
355
|
+
validateWrite,
|
|
318
356
|
};
|
|
@@ -4,6 +4,7 @@ const { ipcMain } = require('electron');
|
|
|
4
4
|
const fsp = require('node:fs/promises');
|
|
5
5
|
const path = require('node:path');
|
|
6
6
|
const os = require('node:os');
|
|
7
|
+
const { schemas } = require('./ipcSchemas.cjs');
|
|
7
8
|
|
|
8
9
|
const PROJECTS_DIR = path.join(os.homedir(), '.claude', 'projects');
|
|
9
10
|
const SLOW_THRESHOLD_MS = 2_000;
|
|
@@ -105,14 +106,18 @@ async function parseJSONL(filePath, stat) {
|
|
|
105
106
|
}
|
|
106
107
|
|
|
107
108
|
function registerHistoryAggregatorHandlers() {
|
|
108
|
-
ipcMain.handle('history:aggregate', async (_e,
|
|
109
|
+
ipcMain.handle('history:aggregate', async (_e, rawReq) => {
|
|
110
|
+
// Wire the historyAggregate schema (previously defined but never used).
|
|
111
|
+
// safeParse so a malformed payload still falls through to defaults
|
|
112
|
+
// (today − 30d) rather than throwing — matches the current "best-effort"
|
|
113
|
+
// semantics expected by the History tab.
|
|
114
|
+
const parsed = schemas.historyAggregate.safeParse(rawReq);
|
|
115
|
+
const req = parsed.success ? (parsed.data ?? {}) : {};
|
|
109
116
|
const t0 = Date.now();
|
|
110
117
|
const today = new Date().toISOString().slice(0, 10);
|
|
111
|
-
let effectiveTo =
|
|
118
|
+
let effectiveTo = req?.toDate ? req.toDate : today;
|
|
112
119
|
if (effectiveTo > today) effectiveTo = today;
|
|
113
|
-
const effectiveFrom =
|
|
114
|
-
? req.fromDate
|
|
115
|
-
: subtractDays(today, 30);
|
|
120
|
+
const effectiveFrom = req?.fromDate ? req.fromDate : subtractDays(today, 30);
|
|
116
121
|
|
|
117
122
|
const buckets = new Map();
|
|
118
123
|
let partial = false;
|
package/src/main/index.cjs
CHANGED
|
@@ -4,7 +4,7 @@ const path = require('node:path');
|
|
|
4
4
|
const fs = require('node:fs');
|
|
5
5
|
const fsp = require('node:fs/promises');
|
|
6
6
|
const os = require('node:os');
|
|
7
|
-
const { schemas } = require('./ipcSchemas.cjs');
|
|
7
|
+
const { schemas, validated } = require('./ipcSchemas.cjs');
|
|
8
8
|
const { cleanChildEnv } = require('./lib/cleanEnv.cjs');
|
|
9
9
|
const { manager: ptyManager, registerPtyHandlers } = require('./pty.cjs');
|
|
10
10
|
const configMgr = require('./config.cjs');
|
|
@@ -17,13 +17,39 @@ const voiceWizard = require('./voiceWizard.cjs');
|
|
|
17
17
|
const scheduler = require('./scheduler.cjs');
|
|
18
18
|
const supervisor = require('./supervisor.cjs');
|
|
19
19
|
const watchers = require('./watchers.cjs');
|
|
20
|
+
const teams = require('./teams.cjs');
|
|
21
|
+
const queueOps = require('./queueOps.cjs');
|
|
22
|
+
const pluginInstall = require('./pluginInstall.cjs');
|
|
20
23
|
const otel = require('./otel.cjs');
|
|
21
24
|
const otelSettings = require('./otelSettings.cjs');
|
|
22
25
|
const { registerHistoryAggregatorHandlers } = require('./historyAggregator.cjs');
|
|
26
|
+
const memoryTool = require('./memoryTool.cjs');
|
|
27
|
+
const { resolveClaudeBin } = require('./lib/claudeBin.cjs');
|
|
28
|
+
const { assertCwdInsideHome } = require('./lib/insideHome.cjs');
|
|
23
29
|
|
|
24
30
|
let mainWindow = null;
|
|
25
31
|
let rebooting = false;
|
|
26
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
|
+
|
|
27
53
|
const REBOOT_LOG = path.join(os.homedir(), '.claude', 'session-manager-reboot.log');
|
|
28
54
|
|
|
29
55
|
function logReboot(line) {
|
|
@@ -35,6 +61,52 @@ function logReboot(line) {
|
|
|
35
61
|
} catch { /* best-effort */ }
|
|
36
62
|
}
|
|
37
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
|
+
|
|
38
110
|
function resolveNpx() {
|
|
39
111
|
const isWin = process.platform === 'win32';
|
|
40
112
|
try {
|
|
@@ -110,6 +182,7 @@ async function rebootApp() {
|
|
|
110
182
|
});
|
|
111
183
|
scheduler.attachWindow(mainWindow);
|
|
112
184
|
watchers.attachWindow(mainWindow);
|
|
185
|
+
pluginInstall.attachWindow(mainWindow);
|
|
113
186
|
rebooting = false;
|
|
114
187
|
return;
|
|
115
188
|
}
|
|
@@ -156,7 +229,14 @@ function createWindow() {
|
|
|
156
229
|
},
|
|
157
230
|
});
|
|
158
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
|
+
|
|
159
238
|
mainWindow.once('ready-to-show', () => {
|
|
239
|
+
if (firstPaintTimer) { clearTimeout(firstPaintTimer); firstPaintTimer = null; }
|
|
160
240
|
mainWindow.maximize();
|
|
161
241
|
mainWindow.show();
|
|
162
242
|
});
|
|
@@ -178,6 +258,7 @@ function createWindow() {
|
|
|
178
258
|
}
|
|
179
259
|
|
|
180
260
|
mainWindow.on('closed', () => {
|
|
261
|
+
if (firstPaintTimer) { clearTimeout(firstPaintTimer); firstPaintTimer = null; }
|
|
181
262
|
mainWindow = null;
|
|
182
263
|
});
|
|
183
264
|
}
|
|
@@ -190,8 +271,17 @@ ipcMain.handle('app:home-dir', () => os.homedir());
|
|
|
190
271
|
|
|
191
272
|
ipcMain.handle('app:cwd', () => process.cwd());
|
|
192
273
|
|
|
274
|
+
// E2E plumbing: tests set SM_E2E=1 to suppress the voice wizard auto-trigger.
|
|
275
|
+
// The renderer reads this once on mount.
|
|
276
|
+
ipcMain.handle('app:is-e2e', () => process.env.SM_E2E === '1');
|
|
277
|
+
|
|
193
278
|
ipcMain.handle('app:engage-rules-path', () => process.env.SESSION_MANAGER_ENGAGE_RULES || null);
|
|
194
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
|
+
|
|
195
285
|
ipcMain.handle('app:pick-directory', async () => {
|
|
196
286
|
console.log('[main] pick-directory invoked');
|
|
197
287
|
const result = await dialog.showOpenDialog(mainWindow, {
|
|
@@ -211,10 +301,18 @@ ipcMain.on('app:reboot-app', () => rebootApp());
|
|
|
211
301
|
// string. Timeout is enforced via SIGKILL on a timer because spawn's built-in
|
|
212
302
|
// `timeout` option only sends SIGTERM, which a wedged shell may ignore.
|
|
213
303
|
ipcMain.handle('app:test-fire-hook', async (_e, payload) => {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
304
|
+
// Zod-validate up front so a malformed renderer payload can never reach the
|
|
305
|
+
// shell. We `safeParse` (not throw) because the existing return shape is
|
|
306
|
+
// `{ exitCode, stdout, stderr, durationMs }` — callers (Hooks.tsx) don't
|
|
307
|
+
// wrap the call in try/catch.
|
|
308
|
+
const parsed = schemas.appTestFireHook.safeParse(payload);
|
|
309
|
+
if (!parsed.success) {
|
|
310
|
+
return { exitCode: -1, stdout: '', stderr: `invalid payload: ${parsed.error.message}`, durationMs: 0 };
|
|
311
|
+
}
|
|
312
|
+
const command = parsed.data.command;
|
|
313
|
+
const env = parsed.data.env ?? null;
|
|
314
|
+
const stdin = typeof parsed.data.payload === 'string' ? parsed.data.payload : '';
|
|
315
|
+
const requested = parsed.data.timeoutMs;
|
|
218
316
|
const timeoutMs = Number.isFinite(requested) && requested > 0
|
|
219
317
|
? Math.min(requested, 30_000)
|
|
220
318
|
: 5_000;
|
|
@@ -322,8 +420,11 @@ ipcMain.handle('app:test-fire-hook', async (_e, payload) => {
|
|
|
322
420
|
// render `—` without branching on error shape. 1s ceiling keeps a wedged git
|
|
323
421
|
// (network filesystem, hung index lock) from blocking the renderer.
|
|
324
422
|
ipcMain.handle('app:git-branch', async (_e, payload) => {
|
|
325
|
-
|
|
326
|
-
|
|
423
|
+
// safeParse (not throw) so a malformed call resolves to null — matches the
|
|
424
|
+
// existing semantics where StatusBar renders `—` on any failure.
|
|
425
|
+
const parsed = schemas.appGitBranch.safeParse(payload);
|
|
426
|
+
if (!parsed.success) return null;
|
|
427
|
+
const { cwd } = parsed.data;
|
|
327
428
|
return await new Promise((resolve) => {
|
|
328
429
|
execFile('git', ['branch', '--show-current'], { cwd, timeout: 1000, windowsHide: true }, (err, stdout) => {
|
|
329
430
|
if (err) { resolve(null); return; }
|
|
@@ -333,6 +434,44 @@ ipcMain.handle('app:git-branch', async (_e, payload) => {
|
|
|
333
434
|
});
|
|
334
435
|
});
|
|
335
436
|
|
|
437
|
+
// Containment check used by the open-in-{editor,finder,terminal} handlers.
|
|
438
|
+
// Bare `startsWith(home)` matches `/home/bilkoEVIL` when home=`/home/bilko` —
|
|
439
|
+
// the classic prefix-trap. Resolve real paths (follow symlinks) and require
|
|
440
|
+
// either exact equality or a separator boundary. Returns null on success or
|
|
441
|
+
// an error string on failure. ENOENT (cwd doesn't exist) is a soft fail —
|
|
442
|
+
// downstream `spawn`/`shell.openPath` will surface a more precise error.
|
|
443
|
+
function checkInsideHome(cwd) {
|
|
444
|
+
if (typeof cwd !== 'string' || !cwd) return 'cwd outside home';
|
|
445
|
+
const home = os.homedir();
|
|
446
|
+
let realCwd;
|
|
447
|
+
let realHome;
|
|
448
|
+
try {
|
|
449
|
+
realHome = fs.realpathSync(home);
|
|
450
|
+
} catch {
|
|
451
|
+
return 'home directory unresolved';
|
|
452
|
+
}
|
|
453
|
+
try {
|
|
454
|
+
realCwd = fs.realpathSync(cwd);
|
|
455
|
+
} catch (err) {
|
|
456
|
+
if (err && err.code === 'ENOENT') {
|
|
457
|
+
// Fall through to the existing error path: the resolved-but-nonexistent
|
|
458
|
+
// path still has to be home-contained to avoid information disclosure
|
|
459
|
+
// via stat-probes (`/etc/secrets/...` → editor errors that reveal
|
|
460
|
+
// existence).
|
|
461
|
+
const resolved = path.resolve(cwd);
|
|
462
|
+
if (resolved !== realHome && !resolved.startsWith(realHome + path.sep)) {
|
|
463
|
+
return 'cwd outside home';
|
|
464
|
+
}
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
return 'cwd outside home';
|
|
468
|
+
}
|
|
469
|
+
if (realCwd !== realHome && !realCwd.startsWith(realHome + path.sep)) {
|
|
470
|
+
return 'cwd outside home';
|
|
471
|
+
}
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
|
|
336
475
|
// Returns the resolved path of a command, or null if not found on PATH.
|
|
337
476
|
function findCommand(name) {
|
|
338
477
|
try {
|
|
@@ -348,8 +487,8 @@ function findCommand(name) {
|
|
|
348
487
|
|
|
349
488
|
ipcMain.handle('app:open-in-editor', async (_e, payload) => {
|
|
350
489
|
const { cwd, editor } = schemas.openInEditor.parse(payload);
|
|
351
|
-
const
|
|
352
|
-
if (
|
|
490
|
+
const err = checkInsideHome(cwd);
|
|
491
|
+
if (err) throw new Error(err);
|
|
353
492
|
const candidates = (editor && editor !== 'auto')
|
|
354
493
|
? [editor]
|
|
355
494
|
: [process.env.VISUAL, process.env.EDITOR, 'code', 'cursor', 'subl', 'nano'].filter(Boolean);
|
|
@@ -364,16 +503,16 @@ ipcMain.handle('app:open-in-editor', async (_e, payload) => {
|
|
|
364
503
|
|
|
365
504
|
ipcMain.handle('app:open-in-finder', async (_e, payload) => {
|
|
366
505
|
const { cwd } = schemas.openInFinder.parse(payload);
|
|
367
|
-
const
|
|
368
|
-
if (
|
|
506
|
+
const err = checkInsideHome(cwd);
|
|
507
|
+
if (err) throw new Error(err);
|
|
369
508
|
await shell.openPath(cwd);
|
|
370
509
|
return { ok: true };
|
|
371
510
|
});
|
|
372
511
|
|
|
373
512
|
ipcMain.handle('app:open-in-terminal', async (_e, payload) => {
|
|
374
513
|
const { cwd } = schemas.openInTerminal.parse(payload);
|
|
375
|
-
const
|
|
376
|
-
if (
|
|
514
|
+
const err = checkInsideHome(cwd);
|
|
515
|
+
if (err) throw new Error(err);
|
|
377
516
|
if (process.platform === 'linux') {
|
|
378
517
|
const terms = ['gnome-terminal', 'konsole', 'xfce4-terminal', 'xterm'];
|
|
379
518
|
for (const t of terms) {
|
|
@@ -412,7 +551,11 @@ voiceHotkey.registerHotkeyHandlers();
|
|
|
412
551
|
voiceWizard.registerWizardHandlers();
|
|
413
552
|
scheduler.registerScheduleHandlers();
|
|
414
553
|
watchers.registerWatcherHandlers();
|
|
554
|
+
teams.registerTeamsHandlers();
|
|
555
|
+
queueOps.registerQueueOpsHandlers();
|
|
415
556
|
registerHistoryAggregatorHandlers();
|
|
557
|
+
pluginInstall.registerPluginInstallHandlers();
|
|
558
|
+
memoryTool.registerMemoryHandlers();
|
|
416
559
|
|
|
417
560
|
// OTEL telemetry export (opt-in via ~/.config/session-manager/otel.json).
|
|
418
561
|
ipcMain.handle('otel:get-config', async () => otelSettings.load());
|
|
@@ -488,6 +631,26 @@ app.whenReady().then(async () => {
|
|
|
488
631
|
logs.pruneOld();
|
|
489
632
|
logs.writeLine({ scope: 'main', level: 'info', message: 'app start', meta: { version: app.getVersion(), platform: process.platform } });
|
|
490
633
|
|
|
634
|
+
// Boot-time detection 1: surface `claude` binary resolution so a missing
|
|
635
|
+
// install becomes visible to the renderer instead of failing silently on
|
|
636
|
+
// first spawn attempt.
|
|
637
|
+
const claudeResolved = resolveClaudeBin();
|
|
638
|
+
const claudeFoundOnDisk = claudeResolved !== 'claude';
|
|
639
|
+
bootClaudeBin = { resolved: claudeResolved, foundOnDisk: claudeFoundOnDisk };
|
|
640
|
+
if (claudeFoundOnDisk) {
|
|
641
|
+
console.log(`[claudeBin] resolved=${claudeResolved}`);
|
|
642
|
+
} else {
|
|
643
|
+
console.warn('[claudeBin] FALLBACK no candidate found; spawn will rely on PATH');
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Boot-time detection 2: symlinked /Users on macOS can make os.homedir()
|
|
647
|
+
// realpath to a path outside itself, which breaks every cwd containment
|
|
648
|
+
// check downstream. Surface here rather than failing on first session spawn.
|
|
649
|
+
bootHomeSelfCheck = assertCwdInsideHome(os.homedir());
|
|
650
|
+
if (!bootHomeSelfCheck.ok) {
|
|
651
|
+
console.error(`[insideHome] SELF-CHECK FAILED: ${bootHomeSelfCheck.error}; sessions will not be able to spawn`);
|
|
652
|
+
}
|
|
653
|
+
|
|
491
654
|
process.on('uncaughtException', (err) => {
|
|
492
655
|
logs.writeLine({ scope: 'main', level: 'error', message: 'uncaughtException', meta: { error: err?.message, stack: err?.stack } });
|
|
493
656
|
});
|
|
@@ -497,15 +660,23 @@ app.whenReady().then(async () => {
|
|
|
497
660
|
});
|
|
498
661
|
|
|
499
662
|
// Inject Content-Security-Policy for all renderer responses.
|
|
663
|
+
// frame-src / frame-ancestors locked to 'none' — the app is a top-level
|
|
664
|
+
// Electron BrowserWindow; iframes/embedding have no legitimate use.
|
|
500
665
|
const CSP = [
|
|
501
666
|
"default-src 'self'",
|
|
502
667
|
"script-src 'self' 'wasm-unsafe-eval'",
|
|
503
668
|
"style-src 'self' 'unsafe-inline'",
|
|
504
669
|
"img-src 'self' data: blob:",
|
|
505
670
|
"font-src 'self' data:",
|
|
506
|
-
|
|
671
|
+
// schemastore.org is used by Monaco for JSON schema validation
|
|
672
|
+
// (settings.json, keybindings.json — see App.tsx::installMonacoSchemas).
|
|
673
|
+
// The json.schemastore.org URL redirects to www.schemastore.org, so both
|
|
674
|
+
// hosts must be in the allowlist or CSP blocks the redirect.
|
|
675
|
+
"connect-src 'self' https://api.anthropic.com https://registry.npmjs.org https://json.schemastore.org https://www.schemastore.org",
|
|
507
676
|
"media-src 'self' blob:",
|
|
508
677
|
"worker-src 'self' blob:",
|
|
678
|
+
"frame-src 'none'",
|
|
679
|
+
"frame-ancestors 'none'",
|
|
509
680
|
].join('; ') + ';';
|
|
510
681
|
session.defaultSession.webRequest.onHeadersReceived((details, cb) => {
|
|
511
682
|
cb({
|
|
@@ -595,6 +766,7 @@ app.whenReady().then(async () => {
|
|
|
595
766
|
});
|
|
596
767
|
scheduler.attachWindow(mainWindow);
|
|
597
768
|
watchers.attachWindow(mainWindow);
|
|
769
|
+
pluginInstall.attachWindow(mainWindow);
|
|
598
770
|
scheduler.init().catch((e) => {
|
|
599
771
|
logs.writeLine({ scope: 'scheduler', level: 'error', message: 'init failed', meta: { error: e?.message } });
|
|
600
772
|
});
|