claude-code-session-manager 0.8.5 → 0.10.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/README.md +95 -65
- package/dist/assets/cssMode-DWlBzlpW.js +1 -0
- package/dist/assets/{freemarker2-dhfKZR7u.js → freemarker2-Cgg83m-Z.js} +2 -2
- package/dist/assets/handlebars-C4r4LOI9.js +1 -0
- package/dist/assets/html-DaxRI5sW.js +1 -0
- package/dist/assets/htmlMode-Bu_8jtXo.js +1 -0
- package/dist/assets/index-C_tgFedf.js +3986 -0
- package/dist/assets/{editor-BTnBOi8r.css → index-Dj3Db4OA.css} +32 -1
- package/dist/assets/javascript-D5Ztx-Ej.js +1 -0
- package/dist/assets/{jsonMode-BtjA-2w_.js → jsonMode-tfsgezVc.js} +4 -4
- package/dist/assets/liquid-F2cD9OL0.js +1 -0
- package/dist/assets/lspLanguageFeatures-Bz_Eih8F.js +4 -0
- package/dist/assets/mdx-BPlD1clX.js +1 -0
- package/dist/assets/{ort-wasm-simd-threaded.asyncify-CtKKja6V.wasm → ort-wasm-simd-threaded.asyncify-DMmc6YqF.wasm} +0 -0
- package/dist/assets/python-B4gUOWNI.js +1 -0
- package/dist/assets/razor-B6pMxVp1.js +1 -0
- package/dist/assets/{tsMode-hUkEyjsH.js → tsMode-C9nq6cHi.js} +2 -2
- package/dist/assets/typescript-Do5Vtwxu.js +1 -0
- package/dist/assets/{whisperWorker-QfIS0sPF.js → whisperWorker-CcsPqZUS.js} +19 -19
- package/dist/assets/xml-C0mTbVRp.js +1 -0
- package/dist/assets/yaml-D3sePJfA.js +1 -0
- package/dist/index.html +2 -2
- package/package.json +18 -9
- 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 +85 -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 +291 -250
- 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/cssMode-BCLoTYI0.js +0 -1
- package/dist/assets/editor.main-UoasbVGy.js +0 -908
- package/dist/assets/handlebars-DdpqwFuV.js +0 -1
- package/dist/assets/html-1oTJClkg.js +0 -1
- package/dist/assets/htmlMode-CF1QbIg-.js +0 -1
- package/dist/assets/index-DWDcKbgI.js +0 -3046
- package/dist/assets/index-eqxng9X2.css +0 -32
- package/dist/assets/javascript-BP_Q5MFx.js +0 -1
- package/dist/assets/liquid-DstuL8vm.js +0 -1
- package/dist/assets/lspLanguageFeatures-DvSiaY4f.js +0 -4
- package/dist/assets/mdx-qO-uvsJd.js +0 -1
- package/dist/assets/python-CCPz_1cy.js +0 -1
- package/dist/assets/razor-B7tCzkdh.js +0 -1
- package/dist/assets/typescript-BeXECzAk.js +0 -1
- package/dist/assets/xml-MRJd4GHf.js +0 -1
- package/dist/assets/yaml-CzGliMNL.js +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{l as e}from"./index-C_tgFedf.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};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{l as e}from"./index-C_tgFedf.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-C_tgFedf.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="./assets/index-Dj3Db4OA.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.0",
|
|
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 .",
|
|
@@ -39,7 +39,15 @@
|
|
|
39
39
|
"terminal",
|
|
40
40
|
"electron",
|
|
41
41
|
"session-manager",
|
|
42
|
-
"cli"
|
|
42
|
+
"cli",
|
|
43
|
+
"cockpit",
|
|
44
|
+
"mcp",
|
|
45
|
+
"agents",
|
|
46
|
+
"hooks",
|
|
47
|
+
"scheduler",
|
|
48
|
+
"voice",
|
|
49
|
+
"whisper",
|
|
50
|
+
"prd"
|
|
43
51
|
],
|
|
44
52
|
"author": "bilkobibitkov",
|
|
45
53
|
"license": "MIT",
|
|
@@ -63,7 +71,7 @@
|
|
|
63
71
|
"linux"
|
|
64
72
|
],
|
|
65
73
|
"dependencies": {
|
|
66
|
-
"@electron/rebuild": "
|
|
74
|
+
"@electron/rebuild": "4.0.4",
|
|
67
75
|
"@huggingface/transformers": "^4.1.0",
|
|
68
76
|
"@monaco-editor/react": "^4.6.0",
|
|
69
77
|
"@opentelemetry/api": "^1.9.0",
|
|
@@ -77,9 +85,12 @@
|
|
|
77
85
|
"@xterm/addon-web-links": "^0.11.0",
|
|
78
86
|
"@xterm/xterm": "^5.5.0",
|
|
79
87
|
"chokidar": "^4.0.1",
|
|
80
|
-
"electron": "
|
|
88
|
+
"electron": "42.1.0",
|
|
89
|
+
"framer-motion": "^12.38.0",
|
|
90
|
+
"monaco-editor": "0.55.1",
|
|
81
91
|
"node-pty": "^1.2.0-beta.12",
|
|
82
92
|
"onnxruntime-web": "^1.24.3",
|
|
93
|
+
"recharts": "^2.15.4",
|
|
83
94
|
"zod": "^3.23.8",
|
|
84
95
|
"zustand": "^5.0.0"
|
|
85
96
|
},
|
|
@@ -91,15 +102,13 @@
|
|
|
91
102
|
"@vitejs/plugin-react": "^4.3.3",
|
|
92
103
|
"autoprefixer": "^10.4.20",
|
|
93
104
|
"concurrently": "^9.1.0",
|
|
94
|
-
"framer-motion": "^12.38.0",
|
|
95
105
|
"postcss": "^8.4.49",
|
|
96
106
|
"react": "^18.3.1",
|
|
97
107
|
"react-dom": "^18.3.1",
|
|
98
|
-
"recharts": "^2.15.4",
|
|
99
108
|
"tailwindcss": "^3.4.15",
|
|
100
109
|
"typescript": "^5.6.3",
|
|
101
110
|
"vite": "^6.0.1",
|
|
102
|
-
"vitest": "
|
|
111
|
+
"vitest": "4.1.6",
|
|
103
112
|
"wait-on": "^8.0.1"
|
|
104
113
|
}
|
|
105
114
|
}
|
|
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,9 +17,13 @@ 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');
|
|
23
27
|
|
|
24
28
|
let mainWindow = null;
|
|
25
29
|
let rebooting = false;
|
|
@@ -110,6 +114,7 @@ async function rebootApp() {
|
|
|
110
114
|
});
|
|
111
115
|
scheduler.attachWindow(mainWindow);
|
|
112
116
|
watchers.attachWindow(mainWindow);
|
|
117
|
+
pluginInstall.attachWindow(mainWindow);
|
|
113
118
|
rebooting = false;
|
|
114
119
|
return;
|
|
115
120
|
}
|
|
@@ -190,6 +195,10 @@ ipcMain.handle('app:home-dir', () => os.homedir());
|
|
|
190
195
|
|
|
191
196
|
ipcMain.handle('app:cwd', () => process.cwd());
|
|
192
197
|
|
|
198
|
+
// E2E plumbing: tests set SM_E2E=1 to suppress the voice wizard auto-trigger.
|
|
199
|
+
// The renderer reads this once on mount.
|
|
200
|
+
ipcMain.handle('app:is-e2e', () => process.env.SM_E2E === '1');
|
|
201
|
+
|
|
193
202
|
ipcMain.handle('app:engage-rules-path', () => process.env.SESSION_MANAGER_ENGAGE_RULES || null);
|
|
194
203
|
|
|
195
204
|
ipcMain.handle('app:pick-directory', async () => {
|
|
@@ -211,10 +220,18 @@ ipcMain.on('app:reboot-app', () => rebootApp());
|
|
|
211
220
|
// string. Timeout is enforced via SIGKILL on a timer because spawn's built-in
|
|
212
221
|
// `timeout` option only sends SIGTERM, which a wedged shell may ignore.
|
|
213
222
|
ipcMain.handle('app:test-fire-hook', async (_e, payload) => {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
223
|
+
// Zod-validate up front so a malformed renderer payload can never reach the
|
|
224
|
+
// shell. We `safeParse` (not throw) because the existing return shape is
|
|
225
|
+
// `{ exitCode, stdout, stderr, durationMs }` — callers (Hooks.tsx) don't
|
|
226
|
+
// wrap the call in try/catch.
|
|
227
|
+
const parsed = schemas.appTestFireHook.safeParse(payload);
|
|
228
|
+
if (!parsed.success) {
|
|
229
|
+
return { exitCode: -1, stdout: '', stderr: `invalid payload: ${parsed.error.message}`, durationMs: 0 };
|
|
230
|
+
}
|
|
231
|
+
const command = parsed.data.command;
|
|
232
|
+
const env = parsed.data.env ?? null;
|
|
233
|
+
const stdin = typeof parsed.data.payload === 'string' ? parsed.data.payload : '';
|
|
234
|
+
const requested = parsed.data.timeoutMs;
|
|
218
235
|
const timeoutMs = Number.isFinite(requested) && requested > 0
|
|
219
236
|
? Math.min(requested, 30_000)
|
|
220
237
|
: 5_000;
|
|
@@ -322,8 +339,11 @@ ipcMain.handle('app:test-fire-hook', async (_e, payload) => {
|
|
|
322
339
|
// render `—` without branching on error shape. 1s ceiling keeps a wedged git
|
|
323
340
|
// (network filesystem, hung index lock) from blocking the renderer.
|
|
324
341
|
ipcMain.handle('app:git-branch', async (_e, payload) => {
|
|
325
|
-
|
|
326
|
-
|
|
342
|
+
// safeParse (not throw) so a malformed call resolves to null — matches the
|
|
343
|
+
// existing semantics where StatusBar renders `—` on any failure.
|
|
344
|
+
const parsed = schemas.appGitBranch.safeParse(payload);
|
|
345
|
+
if (!parsed.success) return null;
|
|
346
|
+
const { cwd } = parsed.data;
|
|
327
347
|
return await new Promise((resolve) => {
|
|
328
348
|
execFile('git', ['branch', '--show-current'], { cwd, timeout: 1000, windowsHide: true }, (err, stdout) => {
|
|
329
349
|
if (err) { resolve(null); return; }
|
|
@@ -333,6 +353,44 @@ ipcMain.handle('app:git-branch', async (_e, payload) => {
|
|
|
333
353
|
});
|
|
334
354
|
});
|
|
335
355
|
|
|
356
|
+
// Containment check used by the open-in-{editor,finder,terminal} handlers.
|
|
357
|
+
// Bare `startsWith(home)` matches `/home/bilkoEVIL` when home=`/home/bilko` —
|
|
358
|
+
// the classic prefix-trap. Resolve real paths (follow symlinks) and require
|
|
359
|
+
// either exact equality or a separator boundary. Returns null on success or
|
|
360
|
+
// an error string on failure. ENOENT (cwd doesn't exist) is a soft fail —
|
|
361
|
+
// downstream `spawn`/`shell.openPath` will surface a more precise error.
|
|
362
|
+
function checkInsideHome(cwd) {
|
|
363
|
+
if (typeof cwd !== 'string' || !cwd) return 'cwd outside home';
|
|
364
|
+
const home = os.homedir();
|
|
365
|
+
let realCwd;
|
|
366
|
+
let realHome;
|
|
367
|
+
try {
|
|
368
|
+
realHome = fs.realpathSync(home);
|
|
369
|
+
} catch {
|
|
370
|
+
return 'home directory unresolved';
|
|
371
|
+
}
|
|
372
|
+
try {
|
|
373
|
+
realCwd = fs.realpathSync(cwd);
|
|
374
|
+
} catch (err) {
|
|
375
|
+
if (err && err.code === 'ENOENT') {
|
|
376
|
+
// Fall through to the existing error path: the resolved-but-nonexistent
|
|
377
|
+
// path still has to be home-contained to avoid information disclosure
|
|
378
|
+
// via stat-probes (`/etc/secrets/...` → editor errors that reveal
|
|
379
|
+
// existence).
|
|
380
|
+
const resolved = path.resolve(cwd);
|
|
381
|
+
if (resolved !== realHome && !resolved.startsWith(realHome + path.sep)) {
|
|
382
|
+
return 'cwd outside home';
|
|
383
|
+
}
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
return 'cwd outside home';
|
|
387
|
+
}
|
|
388
|
+
if (realCwd !== realHome && !realCwd.startsWith(realHome + path.sep)) {
|
|
389
|
+
return 'cwd outside home';
|
|
390
|
+
}
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
|
|
336
394
|
// Returns the resolved path of a command, or null if not found on PATH.
|
|
337
395
|
function findCommand(name) {
|
|
338
396
|
try {
|
|
@@ -348,8 +406,8 @@ function findCommand(name) {
|
|
|
348
406
|
|
|
349
407
|
ipcMain.handle('app:open-in-editor', async (_e, payload) => {
|
|
350
408
|
const { cwd, editor } = schemas.openInEditor.parse(payload);
|
|
351
|
-
const
|
|
352
|
-
if (
|
|
409
|
+
const err = checkInsideHome(cwd);
|
|
410
|
+
if (err) throw new Error(err);
|
|
353
411
|
const candidates = (editor && editor !== 'auto')
|
|
354
412
|
? [editor]
|
|
355
413
|
: [process.env.VISUAL, process.env.EDITOR, 'code', 'cursor', 'subl', 'nano'].filter(Boolean);
|
|
@@ -364,16 +422,16 @@ ipcMain.handle('app:open-in-editor', async (_e, payload) => {
|
|
|
364
422
|
|
|
365
423
|
ipcMain.handle('app:open-in-finder', async (_e, payload) => {
|
|
366
424
|
const { cwd } = schemas.openInFinder.parse(payload);
|
|
367
|
-
const
|
|
368
|
-
if (
|
|
425
|
+
const err = checkInsideHome(cwd);
|
|
426
|
+
if (err) throw new Error(err);
|
|
369
427
|
await shell.openPath(cwd);
|
|
370
428
|
return { ok: true };
|
|
371
429
|
});
|
|
372
430
|
|
|
373
431
|
ipcMain.handle('app:open-in-terminal', async (_e, payload) => {
|
|
374
432
|
const { cwd } = schemas.openInTerminal.parse(payload);
|
|
375
|
-
const
|
|
376
|
-
if (
|
|
433
|
+
const err = checkInsideHome(cwd);
|
|
434
|
+
if (err) throw new Error(err);
|
|
377
435
|
if (process.platform === 'linux') {
|
|
378
436
|
const terms = ['gnome-terminal', 'konsole', 'xfce4-terminal', 'xterm'];
|
|
379
437
|
for (const t of terms) {
|
|
@@ -412,7 +470,11 @@ voiceHotkey.registerHotkeyHandlers();
|
|
|
412
470
|
voiceWizard.registerWizardHandlers();
|
|
413
471
|
scheduler.registerScheduleHandlers();
|
|
414
472
|
watchers.registerWatcherHandlers();
|
|
473
|
+
teams.registerTeamsHandlers();
|
|
474
|
+
queueOps.registerQueueOpsHandlers();
|
|
415
475
|
registerHistoryAggregatorHandlers();
|
|
476
|
+
pluginInstall.registerPluginInstallHandlers();
|
|
477
|
+
memoryTool.registerMemoryHandlers();
|
|
416
478
|
|
|
417
479
|
// OTEL telemetry export (opt-in via ~/.config/session-manager/otel.json).
|
|
418
480
|
ipcMain.handle('otel:get-config', async () => otelSettings.load());
|
|
@@ -497,15 +559,23 @@ app.whenReady().then(async () => {
|
|
|
497
559
|
});
|
|
498
560
|
|
|
499
561
|
// Inject Content-Security-Policy for all renderer responses.
|
|
562
|
+
// frame-src / frame-ancestors locked to 'none' — the app is a top-level
|
|
563
|
+
// Electron BrowserWindow; iframes/embedding have no legitimate use.
|
|
500
564
|
const CSP = [
|
|
501
565
|
"default-src 'self'",
|
|
502
566
|
"script-src 'self' 'wasm-unsafe-eval'",
|
|
503
567
|
"style-src 'self' 'unsafe-inline'",
|
|
504
568
|
"img-src 'self' data: blob:",
|
|
505
569
|
"font-src 'self' data:",
|
|
506
|
-
|
|
570
|
+
// schemastore.org is used by Monaco for JSON schema validation
|
|
571
|
+
// (settings.json, keybindings.json — see App.tsx::installMonacoSchemas).
|
|
572
|
+
// The json.schemastore.org URL redirects to www.schemastore.org, so both
|
|
573
|
+
// hosts must be in the allowlist or CSP blocks the redirect.
|
|
574
|
+
"connect-src 'self' https://api.anthropic.com https://registry.npmjs.org https://json.schemastore.org https://www.schemastore.org",
|
|
507
575
|
"media-src 'self' blob:",
|
|
508
576
|
"worker-src 'self' blob:",
|
|
577
|
+
"frame-src 'none'",
|
|
578
|
+
"frame-ancestors 'none'",
|
|
509
579
|
].join('; ') + ';';
|
|
510
580
|
session.defaultSession.webRequest.onHeadersReceived((details, cb) => {
|
|
511
581
|
cb({
|
|
@@ -595,6 +665,7 @@ app.whenReady().then(async () => {
|
|
|
595
665
|
});
|
|
596
666
|
scheduler.attachWindow(mainWindow);
|
|
597
667
|
watchers.attachWindow(mainWindow);
|
|
668
|
+
pluginInstall.attachWindow(mainWindow);
|
|
598
669
|
scheduler.init().catch((e) => {
|
|
599
670
|
logs.writeLine({ scope: 'scheduler', level: 'error', message: 'init failed', meta: { error: e?.message } });
|
|
600
671
|
});
|