claude-code-session-manager 0.19.0 → 0.20.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 (35) hide show
  1. package/dist/assets/{TiptapBody-CO4q65kH.js → TiptapBody-Db7_uXrI.js} +1 -1
  2. package/dist/assets/{cssMode-0tbceX4i.js → cssMode-DFKJhhi6.js} +1 -1
  3. package/dist/assets/{freemarker2-Dv8wl_HH.js → freemarker2-DUat8x8o.js} +1 -1
  4. package/dist/assets/{handlebars-MzrjkW3b.js → handlebars-B2C1qhAI.js} +1 -1
  5. package/dist/assets/{html-C0YEYUHk.js → html-khtg0DVs.js} +1 -1
  6. package/dist/assets/{htmlMode-Bf9ccIo3.js → htmlMode-Jmhs-vfl.js} +1 -1
  7. package/dist/assets/{index-BsSklu93.css → index-BkkBX1z7.css} +1 -1
  8. package/dist/assets/{index-BXeFi7dA.js → index-pqnuXM14.js} +634 -624
  9. package/dist/assets/{javascript-BZhQgLYg.js → javascript-i1CXbgg4.js} +1 -1
  10. package/dist/assets/{jsonMode-XkKuSIs5.js → jsonMode-DXZaj-kR.js} +1 -1
  11. package/dist/assets/{liquid-B6fnroVU.js → liquid-Ds7jUF53.js} +1 -1
  12. package/dist/assets/{lspLanguageFeatures-BAIq7N4N.js → lspLanguageFeatures-B_15vO6X.js} +1 -1
  13. package/dist/assets/{mdx-DzH38OXA.js → mdx-DgrrLgTE.js} +1 -1
  14. package/dist/assets/{python-ak0De5ar.js → python-Cff3tPw3.js} +1 -1
  15. package/dist/assets/{razor-DC-IpQpX.js → razor-DlyG7FmM.js} +1 -1
  16. package/dist/assets/{tsMode-DaZCqNuS.js → tsMode-DRmmmttS.js} +1 -1
  17. package/dist/assets/{typescript-D5YkmMgh.js → typescript-DQFL2T1p.js} +1 -1
  18. package/dist/assets/{whisperWorker-CcsPqZUS.js → whisperWorker-Dbia1OpC.js} +15 -15
  19. package/dist/assets/{xml-8idHpw2C.js → xml-CwsJEzdU.js} +1 -1
  20. package/dist/assets/{yaml-Dm8NKlcv.js → yaml-BDsDjf-y.js} +1 -1
  21. package/dist/index.html +2 -2
  22. package/package.json +5 -2
  23. package/src/main/health.cjs +216 -0
  24. package/src/main/historyAggregator.cjs +15 -9
  25. package/src/main/index.cjs +7 -2
  26. package/src/main/ipcSchemas.cjs +43 -0
  27. package/src/main/kg.cjs +0 -0
  28. package/src/main/lib/reaperHelpers.cjs +67 -0
  29. package/src/main/lib/schedulerBatch.cjs +212 -0
  30. package/src/main/lib/schedulerConfig.cjs +9 -1
  31. package/src/main/scheduler.cjs +274 -125
  32. package/src/main/webRemote.cjs +916 -0
  33. package/src/preload/api.d.ts +78 -15
  34. package/src/preload/index.cjs +41 -8
  35. package/src/main/projectSkills.cjs +0 -124
@@ -1 +1 @@
1
- import{l as e}from"./index-BXeFi7dA.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-pqnuXM14.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-BXeFi7dA.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-pqnuXM14.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
@@ -7,8 +7,8 @@
7
7
  <link rel="preconnect" href="https://fonts.googleapis.com">
8
8
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
9
  <link href="https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,500;0,6..72,600;0,6..72,700;1,6..72,400&family=Geist:wght@300;400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
10
- <script type="module" crossorigin src="./assets/index-BXeFi7dA.js"></script>
11
- <link rel="stylesheet" crossorigin href="./assets/index-BsSklu93.css">
10
+ <script type="module" crossorigin src="./assets/index-pqnuXM14.js"></script>
11
+ <link rel="stylesheet" crossorigin href="./assets/index-BkkBX1z7.css">
12
12
  </head>
13
13
  <body class="bg-bg text-fg font-sans antialiased">
14
14
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-session-manager",
3
- "version": "0.19.0",
3
+ "version": "0.20.1",
4
4
  "description": "Local cockpit for the Claude Code CLI — multi-tab terminal, full config surface, scheduler, voice dictation, and live observability.",
5
5
  "type": "module",
6
6
  "main": "src/main/index.cjs",
@@ -32,7 +32,8 @@
32
32
  "smoke:darwin": "playwright test tests/smoke/darwin-boot.spec.ts",
33
33
  "test:e2e:mic": "xvfb-run -a playwright test e2e/mic.spec.mjs",
34
34
  "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",
35
- "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/"
35
+ "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/",
36
+ "health": "node src/main/health.cjs"
36
37
  },
37
38
  "keywords": [
38
39
  "claude",
@@ -103,12 +104,14 @@
103
104
  "react-force-graph-2d": "^1.29.1",
104
105
  "recharts": "^2.15.4",
105
106
  "tiptap-markdown": "^0.9.0",
107
+ "ws": "^8.18.0",
106
108
  "zod": "^3.23.8",
107
109
  "zustand": "^5.0.0"
108
110
  },
109
111
  "devDependencies": {
110
112
  "@playwright/test": "^1.59.1",
111
113
  "@types/node": "^22.9.0",
114
+ "@types/ws": "^8.5.13",
112
115
  "@types/react": "^18.3.12",
113
116
  "@types/react-dom": "^18.3.1",
114
117
  "@vitejs/plugin-react": "^4.3.3",
@@ -0,0 +1,216 @@
1
+ /**
2
+ * health.cjs — health check for session-manager Electron app.
3
+ * Verifies: app startup, IPC responsiveness, scheduler health, watchers active.
4
+ * Exported as check() for /local-project-health skill.
5
+ */
6
+
7
+ const fs = require('node:fs');
8
+ const fsp = require('node:fs/promises');
9
+ const path = require('node:path');
10
+ const os = require('node:os');
11
+ const { execFileSync } = require('node:child_process');
12
+
13
+ const MAX_LOG_AGE_MS = 5 * 60_000; // 5 min — warn if no logs this old
14
+ const PROJECT_ROOT = path.resolve(__dirname, '../..');
15
+
16
+ function runCheck(cmd, cwd = PROJECT_ROOT) {
17
+ try {
18
+ execFileSync('bash', ['-c', cmd], {
19
+ cwd,
20
+ encoding: 'utf8',
21
+ stdio: ['pipe', 'pipe', 'pipe'],
22
+ });
23
+ return true;
24
+ } catch {
25
+ return false;
26
+ }
27
+ }
28
+
29
+ async function check() {
30
+ const start = Date.now();
31
+ const status = {
32
+ ok: true,
33
+ timestamp: new Date().toISOString(),
34
+ components: {},
35
+ issues: [],
36
+ };
37
+
38
+ // 1. Check Node.js and key dependencies exist.
39
+ try {
40
+ const nodeVer = execFileSync('node', ['--version'], { encoding: 'utf8' }).trim();
41
+ status.components.nodejs = { ok: true, version: nodeVer };
42
+ } catch (e) {
43
+ status.components.nodejs = { ok: false, error: e.message };
44
+ status.issues.push('Node.js not available');
45
+ status.ok = false;
46
+ }
47
+
48
+ // 1.5. Check TypeScript compilation (no errors).
49
+ const typesOk = runCheck('npm run typecheck 2>&1 | grep -q "error" && exit 1 || exit 0');
50
+ status.components.typescript = { ok: typesOk };
51
+ if (!typesOk) {
52
+ status.issues.push('TypeScript compilation has errors');
53
+ status.ok = false;
54
+ }
55
+
56
+ // 1.6. Check build artifact exists.
57
+ const distExists = fs.existsSync(path.join(PROJECT_ROOT, 'dist/index.html'));
58
+ status.components.build_artifact = { ok: distExists, path: 'dist/index.html' };
59
+ if (!distExists) {
60
+ status.issues.push('Build artifact missing (run: npm run build)');
61
+ status.ok = false;
62
+ }
63
+
64
+ // 1.7. Check test infrastructure exists.
65
+ const hasPlaywright = fs.existsSync(path.join(PROJECT_ROOT, 'playwright.config.ts'));
66
+ const hasE2E = fs.existsSync(path.join(PROJECT_ROOT, 'e2e'));
67
+ status.components.test_infrastructure = {
68
+ ok: hasPlaywright && hasE2E,
69
+ playwright: hasPlaywright,
70
+ e2e_dir: hasE2E,
71
+ };
72
+
73
+ // 2. Check config directory exists and is writable.
74
+ const configDir = path.join(os.homedir(), '.claude');
75
+ try {
76
+ await fsp.access(configDir, fs.constants.R_OK | fs.constants.W_OK);
77
+ const stat = await fsp.stat(configDir);
78
+ status.components.config_dir = {
79
+ ok: true,
80
+ path: configDir,
81
+ writable: true,
82
+ };
83
+ } catch (e) {
84
+ status.components.config_dir = {
85
+ ok: false,
86
+ error: e.message,
87
+ path: configDir,
88
+ };
89
+ status.issues.push(`Config dir not accessible: ${e.message}`);
90
+ status.ok = false;
91
+ }
92
+
93
+ // 3. Check scheduler PRD directory and queue.json.
94
+ const schedulerBaseDir = path.join(
95
+ os.homedir(),
96
+ '.claude/session-manager/scheduled-plans'
97
+ );
98
+ const queuePath = path.join(schedulerBaseDir, 'queue.json');
99
+ let queueState = null;
100
+ try {
101
+ const queueText = await fsp.readFile(queuePath, 'utf8');
102
+ queueState = JSON.parse(queueText);
103
+ const runningCount = Object.values(queueState.jobs || {}).filter(
104
+ (j) => j.status === 'running'
105
+ ).length;
106
+ const failedCount = Object.values(queueState.jobs || {}).filter(
107
+ (j) => j.status === 'failed'
108
+ ).length;
109
+ status.components.scheduler_queue = {
110
+ ok: true,
111
+ path: queuePath,
112
+ jobs: Object.keys(queueState.jobs || {}).length,
113
+ running: runningCount,
114
+ failed: failedCount,
115
+ };
116
+ } catch (e) {
117
+ if (e.code !== 'ENOENT') {
118
+ status.issues.push(`Scheduler queue unreadable: ${e.message}`);
119
+ }
120
+ status.components.scheduler_queue = {
121
+ ok: e.code === 'ENOENT', // ok if queue doesn't exist yet
122
+ path: queuePath,
123
+ exists: false,
124
+ error: e.code === 'ENOENT' ? 'not yet created' : e.message,
125
+ };
126
+ }
127
+
128
+ // 4. Check PRDs directory.
129
+ const prdsDir = path.join(schedulerBaseDir, 'prds');
130
+ try {
131
+ await fsp.access(prdsDir, fs.constants.R_OK);
132
+ const files = await fsp.readdir(prdsDir);
133
+ const prdFiles = files.filter((f) => f.endsWith('.md'));
134
+ status.components.scheduler_prds = {
135
+ ok: true,
136
+ path: prdsDir,
137
+ count: prdFiles.length,
138
+ };
139
+ } catch (e) {
140
+ if (e.code !== 'ENOENT') {
141
+ status.issues.push(`PRDs directory not accessible: ${e.message}`);
142
+ }
143
+ status.components.scheduler_prds = {
144
+ ok: e.code === 'ENOENT', // ok if not yet created
145
+ path: prdsDir,
146
+ exists: false,
147
+ error: e.code === 'ENOENT' ? 'not yet created' : e.message,
148
+ };
149
+ }
150
+
151
+ // 5. Check transcripts directory (where live session logs are tailed).
152
+ const projectsDir = path.join(os.homedir(), '.claude/projects');
153
+ try {
154
+ await fsp.access(projectsDir, fs.constants.R_OK);
155
+ status.components.transcripts_dir = {
156
+ ok: true,
157
+ path: projectsDir,
158
+ };
159
+ } catch (e) {
160
+ // Not fatal — transcripts dir may not exist until first session.
161
+ status.components.transcripts_dir = {
162
+ ok: true,
163
+ path: projectsDir,
164
+ note: 'not yet created (normal for fresh install)',
165
+ };
166
+ }
167
+
168
+ // 6. Check session-manager's own logs (informational, not blocking).
169
+ const smLogsDir = path.join(
170
+ os.homedir(),
171
+ '.claude/session-manager/logs'
172
+ );
173
+ let logAge = null;
174
+ try {
175
+ const files = await fsp.readdir(smLogsDir);
176
+ if (files.length > 0) {
177
+ const latestLog = files.sort().pop();
178
+ const logPath = path.join(smLogsDir, latestLog);
179
+ const stat = await fsp.stat(logPath);
180
+ logAge = Date.now() - stat.mtimeMs;
181
+ }
182
+ status.components.app_logs = {
183
+ ok: true,
184
+ path: smLogsDir,
185
+ latestLogAgeMs: logAge,
186
+ note: logAge ? `Last log ${Math.round(logAge / 60_000)}m ago` : 'app not yet run',
187
+ };
188
+ } catch (e) {
189
+ status.components.app_logs = {
190
+ ok: true,
191
+ path: smLogsDir,
192
+ note: 'logs directory not yet created (normal for fresh installs)',
193
+ };
194
+ }
195
+
196
+ // 7. Summary scoring: ok if all critical components pass.
197
+ // Critical: nodejs, config dir, typescript, build artifact, test infrastructure.
198
+ // Non-fatal: scheduler/transcripts dirs may not exist on fresh install.
199
+ // Informational: app log age (shows if app is running, but not blocking).
200
+ const criticalComponents = ['nodejs', 'config_dir', 'typescript', 'build_artifact', 'test_infrastructure'];
201
+ status.ok = criticalComponents.every((c) => status.components[c]?.ok !== false);
202
+
203
+ status.elapsedMs = Date.now() - start;
204
+ return status;
205
+ }
206
+
207
+ // CLI entry point: `node src/main/health.cjs`
208
+ if (require.main === module) {
209
+ (async () => {
210
+ const result = await check();
211
+ console.log(JSON.stringify(result, null, 2));
212
+ process.exit(result.ok ? 0 : 1);
213
+ })();
214
+ }
215
+
216
+ module.exports = { check };
@@ -153,14 +153,7 @@ async function parseConversationMeta(filePath, stat) {
153
153
  return meta;
154
154
  }
155
155
 
156
- function registerHistoryAggregatorHandlers() {
157
- ipcMain.handle('history:aggregate', async (_e, rawReq) => {
158
- // Wire the historyAggregate schema (previously defined but never used).
159
- // safeParse so a malformed payload still falls through to defaults
160
- // (today − 30d) rather than throwing — matches the current "best-effort"
161
- // semantics expected by the History tab.
162
- const parsed = schemas.historyAggregate.safeParse(rawReq);
163
- const req = parsed.success ? (parsed.data ?? {}) : {};
156
+ async function aggregate(req) {
164
157
  const t0 = Date.now();
165
158
  const today = localDate(new Date());
166
159
  let effectiveTo = req?.toDate ? req.toDate : today;
@@ -258,6 +251,17 @@ function registerHistoryAggregatorHandlers() {
258
251
 
259
252
  const scannedMs = Date.now() - t0;
260
253
  return { rows, partial, scannedMs, skippedLargeFiles };
254
+ }
255
+
256
+ function registerHistoryAggregatorHandlers() {
257
+ ipcMain.handle('history:aggregate', async (_e, rawReq) => {
258
+ // Wire the historyAggregate schema (previously defined but never used).
259
+ // safeParse so a malformed payload still falls through to defaults
260
+ // (today − 30d) rather than throwing — matches the current "best-effort"
261
+ // semantics expected by the History tab.
262
+ const parsed = schemas.historyAggregate.safeParse(rawReq);
263
+ const req = parsed.success ? (parsed.data ?? {}) : {};
264
+ return aggregate(req);
261
265
  });
262
266
 
263
267
  /** Per-conversation metadata: one row per JSONL with derived duration +
@@ -303,4 +307,6 @@ function registerHistoryAggregatorHandlers() {
303
307
  });
304
308
  }
305
309
 
306
- module.exports = { registerHistoryAggregatorHandlers };
310
+ const remote = { aggregate };
311
+
312
+ module.exports = { registerHistoryAggregatorHandlers, remote };
@@ -35,11 +35,11 @@ const { registerDocEditorHandlers } = require('./docEditor.cjs');
35
35
  const git = require('./git.cjs');
36
36
  const superagent = require('./superagent.cjs');
37
37
  const kg = require('./kg.cjs');
38
- const { registerProjectSkillsHandlers } = require('./projectSkills.cjs');
39
38
  const filesIpc = require('./files.cjs');
40
39
  const searchIpc = require('./search.cjs');
41
40
  const repoAnalyzer = require('./repoAnalyzer.cjs');
42
41
  const hivesIpc = require('./hives.cjs');
42
+ const webRemote = require('./webRemote.cjs');
43
43
  const { resolveClaudeBin } = require('./lib/claudeBin.cjs');
44
44
  const { checkInsideHome, assertInsideHome } = require('./lib/insideHome.cjs');
45
45
  const { openInEditor, openFileInEditor, openInFinder, openInTerminal } = require('./lib/openExternalApp.cjs');
@@ -626,11 +626,11 @@ agentMemory.registerAgentMemoryHandlers();
626
626
  registerDocEditorHandlers();
627
627
  git.register(ipcMain);
628
628
  superagent.registerSuperAgentHandlers();
629
- registerProjectSkillsHandlers();
630
629
  filesIpc.registerFilesHandlers();
631
630
  searchIpc.registerSearchHandlers();
632
631
  repoAnalyzer.register(ipcMain);
633
632
  hivesIpc.registerHiveHandlers();
633
+ webRemote.registerRemoteHandlers();
634
634
 
635
635
  // OTEL telemetry export (opt-in via ~/.config/session-manager/otel.json).
636
636
  ipcMain.handle('otel:get-config', async () => otelSettings.load());
@@ -918,9 +918,13 @@ app.whenReady().then(async () => {
918
918
  pluginInstall.attachWindow(mainWindow);
919
919
  superagent.attachWindow(mainWindow);
920
920
  kg.attachWindow(mainWindow);
921
+ webRemote.attachWindow(mainWindow);
921
922
  scheduler.init().catch((e) => {
922
923
  logs.writeLine({ scope: 'scheduler', level: 'error', message: 'init failed', meta: { error: e?.message } });
923
924
  });
925
+ webRemote.init().catch((e) => {
926
+ logs.writeLine({ scope: 'webRemote', level: 'error', message: 'init failed', meta: { error: e?.message } });
927
+ });
924
928
  // Knowledge Graph: watch the prompt log + register kg:* IPC. Best-effort.
925
929
  try { kg.init({ logger: logs }); } catch (e) {
926
930
  logs.writeLine({ scope: 'kg', level: 'error', message: 'init failed', meta: { error: e?.message } });
@@ -964,6 +968,7 @@ let teardownDone = false;
964
968
  function runShutdownCleanup() {
965
969
  if (teardownDone) return; // idempotent — will-quit may still fire after an app.exit path
966
970
  teardownDone = true;
971
+ webRemote.destroy();
967
972
  // Mark a clean exit so the next boot can distinguish a graceful quit from an
968
973
  // OOM-kill / native crash (which leaves the sentinel `open`).
969
974
  crashDiagnostics.markCleanShutdown();
@@ -242,6 +242,23 @@ const agentMemoryDelete = z.object({
242
242
  entryId: z.string().regex(AGENT_MEMORY_ID_RE),
243
243
  }).strict();
244
244
 
245
+ // ──────────────────────────────────────────── Web Remote
246
+ // OTP is 8 uppercase alphanumeric chars (case-insensitive entry, normalised to upper in handler).
247
+ const WEB_REMOTE_OTP_RE = /^[A-Z0-9]{8}$/i;
248
+ const DEVICE_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
249
+
250
+ const webRemotePair = z.object({
251
+ otp: z.string().regex(WEB_REMOTE_OTP_RE),
252
+ }).strict();
253
+
254
+ const webRemoteRevokeDevice = z.object({
255
+ deviceId: z.string().regex(DEVICE_ID_RE),
256
+ }).strict();
257
+
258
+ const webRemoteAuditTail = z.object({
259
+ lines: z.number().int().min(1).max(500).optional(),
260
+ }).strict();
261
+
245
262
  // ──────────────────────────────────────────── History
246
263
  const DATE_YYYY_MM_DD = /^\d{4}-\d{2}-\d{2}$/;
247
264
 
@@ -365,12 +382,38 @@ function validated(schema, handler) {
365
382
  };
366
383
  }
367
384
 
385
+ // ──────────────────────────────────────────── Web Remote command allowlist
386
+ // Single source of truth — imported by webRemote.cjs and by the unit test.
387
+ // Only these type strings will ever reach a handler; all others are silently
388
+ // dropped without leaking error details back to the relay (ADR §6.2).
389
+ const ALLOWED_COMMANDS = new Set([
390
+ 'cmd:sessions:load',
391
+ 'cmd:sessions:save',
392
+ 'cmd:pty:spawn',
393
+ 'cmd:pty:write',
394
+ 'cmd:pty:resize',
395
+ 'cmd:pty:kill',
396
+ 'cmd:schedule:state',
397
+ 'cmd:schedule:read-prd',
398
+ 'cmd:schedule:read-log',
399
+ 'cmd:schedule:write-prd',
400
+ 'cmd:schedule:reset-job',
401
+ 'cmd:schedule:run-now',
402
+ 'cmd:schedule:set-config',
403
+ 'cmd:history:aggregate',
404
+ 'cmd:app:version',
405
+ ]);
406
+
368
407
  module.exports = {
369
408
  // Centralized slug regex — used by scheduler.cjs and queueOps.cjs for
370
409
  // direct test()/match() containment checks alongside the zod parses.
371
410
  SCHEDULE_SLUG_RE,
372
411
  SCHEDULE_RUN_ID_RE,
412
+ ALLOWED_COMMANDS,
373
413
  schemas: {
414
+ webRemotePair,
415
+ webRemoteRevokeDevice,
416
+ webRemoteAuditTail,
374
417
  ptySpawn,
375
418
  ptyTabId,
376
419
  ptyWrite,
package/src/main/kg.cjs CHANGED
Binary file
@@ -0,0 +1,67 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * reaperHelpers.cjs — pure helpers for the dead-process reaper in scheduler.cjs.
5
+ *
6
+ * Kept in a separate lib file so they can be unit-tested without importing
7
+ * scheduler.cjs (which requires electron/ipcMain).
8
+ */
9
+
10
+ const fs = require('node:fs');
11
+ const { readTail } = require('./fileTail.cjs');
12
+
13
+ /**
14
+ * Return true if pid is alive AND its cmdline looks like a claude process.
15
+ *
16
+ * Guards against PID recycling: on Linux we read /proc/<pid>/cmdline and
17
+ * require /\bclaude\b/ in the command. On macOS (no /proc) we can't read
18
+ * cmdline, so we conservatively return true — never false-reap a live PID
19
+ * just because we can't verify its identity.
20
+ *
21
+ * Conservative by design: a false negative (live process treated as dead) is
22
+ * far worse than a late reap.
23
+ */
24
+ function claudePidAlive(pid) {
25
+ if (!pid || typeof pid !== 'number' || pid <= 1) return false;
26
+ try { process.kill(pid, 0); } catch { return false; }
27
+ try {
28
+ const cmd = fs.readFileSync(`/proc/${pid}/cmdline`, 'utf8').replace(/\0/g, ' ');
29
+ return /\bclaude\b/.test(cmd);
30
+ } catch {
31
+ // Can't read cmdline (macOS, permission denied) → assume alive.
32
+ return true;
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Classify the terminal outcome of a completed run by reading the last 64 KB
38
+ * of its log file and scanning for the LAST `{"type":"result"}` JSONL event.
39
+ *
40
+ * Returns:
41
+ * 'success' — last result event has subtype=success and is_error !== true
42
+ * 'failed' — last result event exists but indicates an error
43
+ * 'no_result' — no result event found in the tail (process may have been killed
44
+ * before emitting one, or the log is absent/empty)
45
+ * 'unknown' — unexpected error reading/parsing (outer catch)
46
+ */
47
+ function classifyRunOutcome(logPath) {
48
+ try {
49
+ const text = readTail(logPath, 65536);
50
+ let lastResult = null;
51
+ for (const line of text.split('\n')) {
52
+ const t = line.trim();
53
+ if (!t.startsWith('{')) continue;
54
+ try {
55
+ const obj = JSON.parse(t);
56
+ if (obj && obj.type === 'result') lastResult = obj;
57
+ } catch { /* partial line at tail boundary or non-JSON scheduler log line */ }
58
+ }
59
+ if (!lastResult) return 'no_result';
60
+ if (lastResult.subtype === 'success' && lastResult.is_error !== true) return 'success';
61
+ return 'failed';
62
+ } catch {
63
+ return 'unknown';
64
+ }
65
+ }
66
+
67
+ module.exports = { claudePidAlive, classifyRunOutcome };