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.
Files changed (54) hide show
  1. package/README.md +95 -65
  2. package/dist/assets/{cssMode-DBg6nxUL.js → cssMode-AMMSQWRU.js} +1 -1
  3. package/dist/assets/{freemarker2-CyjUGY3f.js → freemarker2-B7HmOKy0.js} +1 -1
  4. package/dist/assets/{handlebars-lhtCWqlB.js → handlebars-DMhLYNJA.js} +1 -1
  5. package/dist/assets/{html-egptHwbZ.js → html-DIfRfeTv.js} +1 -1
  6. package/dist/assets/htmlMode-DBqG7xl_.js +1 -0
  7. package/dist/assets/{index-DjeqNwqn.js → index-BqL_4JKo.js} +1120 -1086
  8. package/dist/assets/{index-DnLtSCQS.css → index-CxncC9a0.css} +1 -1
  9. package/dist/assets/{javascript-tZbiID3O.js → javascript-Bsn1K6_V.js} +1 -1
  10. package/dist/assets/{jsonMode-BGtPN-L-.js → jsonMode-DVLHW2S3.js} +1 -1
  11. package/dist/assets/{liquid-DvTeXhev.js → liquid-9zGHPTSW.js} +1 -1
  12. package/dist/assets/{lspLanguageFeatures-D9xoxVlV.js → lspLanguageFeatures-BaqyiCFJ.js} +2 -2
  13. package/dist/assets/{mdx-BQ3Ja4wM.js → mdx-DhU2NoOP.js} +1 -1
  14. package/dist/assets/{ort-wasm-simd-threaded.asyncify-CtKKja6V.wasm → ort-wasm-simd-threaded.asyncify-DMmc6YqF.wasm} +0 -0
  15. package/dist/assets/{python-C71RWXaP.js → python-WMVORwGT.js} +1 -1
  16. package/dist/assets/{razor-w__Mkyns.js → razor-C13iUMo9.js} +1 -1
  17. package/dist/assets/{tsMode-DOQLQDB3.js → tsMode-B7lSY5y7.js} +1 -1
  18. package/dist/assets/{typescript-DEiub2Jt.js → typescript-BbjzsO4g.js} +1 -1
  19. package/dist/assets/{whisperWorker-QfIS0sPF.js → whisperWorker-CcsPqZUS.js} +19 -19
  20. package/dist/assets/{xml-RXkLQscS.js → xml-BH6qG4pe.js} +1 -1
  21. package/dist/assets/{yaml-C8HIpJku.js → yaml-B4r3z4qP.js} +1 -1
  22. package/dist/index.html +2 -2
  23. package/package.json +19 -10
  24. package/screenshots/.gitkeep +0 -0
  25. package/screenshots/README-screenshots.md +13 -0
  26. package/src/main/config.cjs +47 -9
  27. package/src/main/historyAggregator.cjs +10 -5
  28. package/src/main/index.cjs +186 -14
  29. package/src/main/ipcSchemas.cjs +165 -3
  30. package/src/main/lib/claudeBin.cjs +39 -0
  31. package/src/main/lib/encodeCwd.cjs +19 -0
  32. package/src/main/lib/fileTail.cjs +35 -0
  33. package/src/main/lib/insideHome.cjs +38 -0
  34. package/src/main/lib/prdFrontmatter.cjs +51 -0
  35. package/src/main/lib/sendToRenderer.cjs +21 -0
  36. package/src/main/memoryTool.cjs +203 -0
  37. package/src/main/otelSettings.cjs +2 -7
  38. package/src/main/pluginInstall.cjs +129 -0
  39. package/src/main/pty.cjs +13 -29
  40. package/src/main/queueOps.cjs +404 -0
  41. package/src/main/scheduler/prdParser.cjs +135 -0
  42. package/src/main/scheduler.cjs +296 -255
  43. package/src/main/sessionsStore.cjs +2 -6
  44. package/src/main/supervisor.cjs +3 -35
  45. package/src/main/teams.cjs +95 -0
  46. package/src/main/transcripts.cjs +5 -7
  47. package/src/main/usage.cjs +8 -0
  48. package/src/main/voiceHotkey.cjs +13 -9
  49. package/src/main/voiceSettings.cjs +2 -9
  50. package/src/main/voiceWizard.cjs +4 -11
  51. package/src/main/watchers.cjs +18 -42
  52. package/src/preload/api.d.ts +153 -1
  53. package/src/preload/index.cjs +29 -0
  54. package/dist/assets/htmlMode-tPDeHGOB.js +0 -1
@@ -1 +1 @@
1
- import{l as e}from"./index-DjeqNwqn.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
+ 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-DjeqNwqn.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};
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-DjeqNwqn.js"></script>
8
- <link rel="stylesheet" crossorigin href="./assets/index-DnLtSCQS.css">
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.8.6",
4
- "description": "Local cockpit for Claude Code CLI sessions — terminal + full config surface.",
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": "^3.7.0",
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": "^33.2.0",
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": "^2.1.9",
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.
@@ -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
- await fsp.writeFile(tmp, text, 'utf8');
169
- await fsp.rename(tmp, real);
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
- if (window && !window.isDestroyed()) {
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 { /* ignore */ } });
303
- ipcMain.on('config:unwatch', (_e, { paths }) => { try { s.configWatch.parse(paths); unwatch(paths); } catch { /* ignore */ } });
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, req) => {
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 = (req?.toDate && /^\d{4}-\d{2}-\d{2}$/.test(req.toDate)) ? req.toDate : today;
118
+ let effectiveTo = req?.toDate ? req.toDate : today;
112
119
  if (effectiveTo > today) effectiveTo = today;
113
- const effectiveFrom = (req?.fromDate && /^\d{4}-\d{2}-\d{2}$/.test(req.fromDate))
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;
@@ -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
- const command = typeof payload?.command === 'string' ? payload.command : '';
215
- const env = payload && typeof payload.env === 'object' && payload.env !== null ? payload.env : null;
216
- const stdin = typeof payload?.payload === 'string' ? payload.payload : '';
217
- const requested = Number(payload?.timeoutMs);
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
- const cwd = payload && typeof payload.cwd === 'string' ? payload.cwd : null;
326
- if (!cwd) return null;
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 home = os.homedir();
352
- if (!path.resolve(cwd).startsWith(home)) throw new Error('cwd outside home');
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 home = os.homedir();
368
- if (!path.resolve(cwd).startsWith(home)) throw new Error('cwd outside home');
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 home = os.homedir();
376
- if (!path.resolve(cwd).startsWith(home)) throw new Error('cwd outside home');
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
- "connect-src 'self' https://api.anthropic.com",
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
  });