claude-code-session-manager 0.8.3 → 0.8.5

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 (34) hide show
  1. package/LICENSE +21 -0
  2. package/dist/assets/{cssMode-DyaNC2Cs.js → cssMode-BCLoTYI0.js} +1 -1
  3. package/dist/assets/{editor.main-BhSGi_Jw.js → editor.main-UoasbVGy.js} +3 -3
  4. package/dist/assets/{freemarker2-DZH3si5v.js → freemarker2-dhfKZR7u.js} +1 -1
  5. package/dist/assets/{handlebars-DvzTd6uL.js → handlebars-DdpqwFuV.js} +1 -1
  6. package/dist/assets/{html-C5GmopAN.js → html-1oTJClkg.js} +1 -1
  7. package/dist/assets/{htmlMode-DwnrHwx1.js → htmlMode-CF1QbIg-.js} +1 -1
  8. package/dist/assets/index-DWDcKbgI.js +3046 -0
  9. package/dist/assets/index-eqxng9X2.css +32 -0
  10. package/dist/assets/{javascript-JqHrxiCa.js → javascript-BP_Q5MFx.js} +1 -1
  11. package/dist/assets/{jsonMode-8rZcy09i.js → jsonMode-BtjA-2w_.js} +1 -1
  12. package/dist/assets/{liquid-ClpD_v7G.js → liquid-DstuL8vm.js} +1 -1
  13. package/dist/assets/{lspLanguageFeatures-u0WgQBQz.js → lspLanguageFeatures-DvSiaY4f.js} +1 -1
  14. package/dist/assets/{mdx-DtViUgdm.js → mdx-qO-uvsJd.js} +1 -1
  15. package/dist/assets/{python-CaAvhRGm.js → python-CCPz_1cy.js} +1 -1
  16. package/dist/assets/{razor-saGNVU7l.js → razor-B7tCzkdh.js} +1 -1
  17. package/dist/assets/{tsMode-HZwWTCj8.js → tsMode-hUkEyjsH.js} +1 -1
  18. package/dist/assets/{typescript-BInV4PNE.js → typescript-BeXECzAk.js} +1 -1
  19. package/dist/assets/{whisperWorker-ivwFFLMj.js → whisperWorker-QfIS0sPF.js} +5 -5
  20. package/dist/assets/{xml-tgO806YR.js → xml-MRJd4GHf.js} +1 -1
  21. package/dist/assets/{yaml-CHApZArv.js → yaml-CzGliMNL.js} +1 -1
  22. package/dist/index.html +2 -2
  23. package/package.json +16 -1
  24. package/src/main/historyAggregator.cjs +208 -0
  25. package/src/main/index.cjs +4 -0
  26. package/src/main/ipcSchemas.cjs +15 -0
  27. package/src/main/lib/schedulerConfig.cjs +2 -0
  28. package/src/main/scheduler.cjs +604 -120
  29. package/src/main/supervisor.cjs +512 -0
  30. package/src/main/usage.cjs +44 -2
  31. package/src/preload/api.d.ts +64 -2
  32. package/src/preload/index.cjs +10 -0
  33. package/dist/assets/index-BGshD4Pw.js +0 -2976
  34. package/dist/assets/index-DCK87t79.css +0 -32
@@ -1 +1 @@
1
- import{l as e}from"./editor.main-BhSGi_Jw.js";import"./index-BGshD4Pw.js";const o={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}}]},i={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{o as conf,i as language};
1
+ import{l as e}from"./editor.main-UoasbVGy.js";import"./index-DWDcKbgI.js";const o={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}}]},i={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{o as conf,i as language};
@@ -1 +1 @@
1
- import{l as e}from"./editor.main-BhSGi_Jw.js";import"./index-BGshD4Pw.js";const o={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}}]},r={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{o as conf,r as language};
1
+ import{l as e}from"./editor.main-UoasbVGy.js";import"./index-DWDcKbgI.js";const o={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}}]},r={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{o as conf,r 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-BGshD4Pw.js"></script>
8
- <link rel="stylesheet" crossorigin href="./assets/index-DCK87t79.css">
7
+ <script type="module" crossorigin src="./assets/index-DWDcKbgI.js"></script>
8
+ <link rel="stylesheet" crossorigin href="./assets/index-eqxng9X2.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,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-session-manager",
3
- "version": "0.8.3",
3
+ "version": "0.8.5",
4
4
  "description": "Local cockpit for Claude Code CLI sessions — terminal + full config surface.",
5
5
  "type": "module",
6
6
  "main": "src/main/index.cjs",
@@ -26,6 +26,7 @@
26
26
  "start": "electron .",
27
27
  "postinstall": "electron-rebuild -f -w node-pty",
28
28
  "prepublishOnly": "vite build",
29
+ "test:unit": "vitest run",
29
30
  "test:e2e": "xvfb-run -a playwright test",
30
31
  "test:e2e:mic": "xvfb-run -a playwright test e2e/mic.spec.mjs",
31
32
  "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",
@@ -42,6 +43,18 @@
42
43
  ],
43
44
  "author": "bilkobibitkov",
44
45
  "license": "MIT",
46
+ "homepage": "https://github.com/StanislavBG/claude-code-session-manager#readme",
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "https://github.com/StanislavBG/claude-code-session-manager.git"
50
+ },
51
+ "bugs": {
52
+ "url": "https://github.com/StanislavBG/claude-code-session-manager/issues"
53
+ },
54
+ "publishConfig": {
55
+ "access": "public",
56
+ "provenance": true
57
+ },
45
58
  "engines": {
46
59
  "node": ">=18"
47
60
  },
@@ -82,9 +95,11 @@
82
95
  "postcss": "^8.4.49",
83
96
  "react": "^18.3.1",
84
97
  "react-dom": "^18.3.1",
98
+ "recharts": "^2.15.4",
85
99
  "tailwindcss": "^3.4.15",
86
100
  "typescript": "^5.6.3",
87
101
  "vite": "^6.0.1",
102
+ "vitest": "^2.1.9",
88
103
  "wait-on": "^8.0.1"
89
104
  }
90
105
  }
@@ -0,0 +1,208 @@
1
+ 'use strict';
2
+
3
+ const { ipcMain } = require('electron');
4
+ const fsp = require('node:fs/promises');
5
+ const path = require('node:path');
6
+ const os = require('node:os');
7
+
8
+ const PROJECTS_DIR = path.join(os.homedir(), '.claude', 'projects');
9
+ const SLOW_THRESHOLD_MS = 2_000;
10
+ const MAX_FILE_BYTES = 20 * 1024 * 1024;
11
+
12
+ function decodeCwd(encoded) {
13
+ return '/' + encoded.replace(/-+/g, '/');
14
+ }
15
+
16
+ function subtractDays(dateStr, days) {
17
+ const d = new Date(dateStr + 'T00:00:00Z');
18
+ d.setUTCDate(d.getUTCDate() - days);
19
+ return d.toISOString().slice(0, 10);
20
+ }
21
+
22
+ async function parseJSONL(filePath, stat) {
23
+ const acc = {
24
+ promptCount: 0,
25
+ inputTokens: 0,
26
+ outputTokens: 0,
27
+ cacheReadTokens: 0,
28
+ cacheCreationTokens: 0,
29
+ toolCallCount: 0,
30
+ toolBreakdown: {},
31
+ errorCount: 0,
32
+ sessionDate: null,
33
+ skipped: false,
34
+ };
35
+
36
+ if (stat.size > MAX_FILE_BYTES) {
37
+ acc.skipped = true;
38
+ return acc;
39
+ }
40
+
41
+ let text;
42
+ try {
43
+ text = await fsp.readFile(filePath, 'utf8');
44
+ } catch {
45
+ return acc;
46
+ }
47
+
48
+ const lines = text.split('\n');
49
+ let firstTs = null;
50
+
51
+ for (const raw of lines) {
52
+ const line = raw.trim();
53
+ if (!line) continue;
54
+ let obj;
55
+ try {
56
+ obj = JSON.parse(line);
57
+ } catch {
58
+ continue;
59
+ }
60
+
61
+ if (firstTs === null) {
62
+ const ts = obj.ts ?? obj.timestamp;
63
+ if (ts) firstTs = ts;
64
+ }
65
+
66
+ const role = obj.role ?? obj.message?.role;
67
+ if (role === 'user') acc.promptCount++;
68
+
69
+ const usage = obj.usage ?? obj.message?.usage;
70
+ if (usage && typeof usage === 'object') {
71
+ if (typeof usage.inputTokens === 'number') acc.inputTokens += usage.inputTokens;
72
+ if (typeof usage.outputTokens === 'number') acc.outputTokens += usage.outputTokens;
73
+ if (typeof usage.cacheReadInputTokens === 'number') acc.cacheReadTokens += usage.cacheReadInputTokens;
74
+ if (typeof usage.cacheCreationInputTokens === 'number') acc.cacheCreationTokens += usage.cacheCreationInputTokens;
75
+ }
76
+
77
+ const content = obj.message?.content ?? obj.content;
78
+ if (Array.isArray(content)) {
79
+ for (const block of content) {
80
+ if (block?.type === 'tool_use' && typeof block.name === 'string') {
81
+ acc.toolCallCount++;
82
+ acc.toolBreakdown[block.name] = (acc.toolBreakdown[block.name] ?? 0) + 1;
83
+ }
84
+ }
85
+ }
86
+
87
+ if (
88
+ (obj.type === 'tool_result' && obj.is_error === true) ||
89
+ obj.message?.stop_reason === 'error' ||
90
+ obj.stop_reason === 'error'
91
+ ) {
92
+ acc.errorCount++;
93
+ }
94
+ }
95
+
96
+ try {
97
+ acc.sessionDate = firstTs
98
+ ? new Date(firstTs).toISOString().slice(0, 10)
99
+ : new Date(stat.mtimeMs).toISOString().slice(0, 10);
100
+ } catch {
101
+ acc.sessionDate = new Date(stat.mtimeMs).toISOString().slice(0, 10);
102
+ }
103
+
104
+ return acc;
105
+ }
106
+
107
+ function registerHistoryAggregatorHandlers() {
108
+ ipcMain.handle('history:aggregate', async (_e, req) => {
109
+ const t0 = Date.now();
110
+ 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;
112
+ 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);
116
+
117
+ const buckets = new Map();
118
+ let partial = false;
119
+ let skippedLargeFiles = 0;
120
+
121
+ let projectDirs;
122
+ try {
123
+ projectDirs = await fsp.readdir(PROJECTS_DIR, { withFileTypes: true });
124
+ } catch {
125
+ return { rows: [], partial: false, scannedMs: Date.now() - t0 };
126
+ }
127
+
128
+ for (const projEntry of projectDirs) {
129
+ if (!projEntry.isDirectory()) continue;
130
+ const encodedCwd = projEntry.name;
131
+ const projectDir = path.join(PROJECTS_DIR, encodedCwd);
132
+
133
+ let files;
134
+ try {
135
+ files = await fsp.readdir(projectDir, { withFileTypes: true });
136
+ } catch {
137
+ continue;
138
+ }
139
+
140
+ for (const fileEntry of files) {
141
+ if (!fileEntry.name.endsWith('.jsonl')) continue;
142
+ const filePath = path.join(projectDir, fileEntry.name);
143
+
144
+ let stat;
145
+ try {
146
+ stat = await fsp.stat(filePath);
147
+ } catch {
148
+ continue;
149
+ }
150
+
151
+ const parsed = await parseJSONL(filePath, stat);
152
+ if (parsed.skipped) { skippedLargeFiles++; continue; }
153
+
154
+ const { sessionDate } = parsed;
155
+ if (!sessionDate || sessionDate < effectiveFrom || sessionDate >= effectiveTo) continue;
156
+
157
+ const key = `${sessionDate}|${encodedCwd}`;
158
+ if (!buckets.has(key)) {
159
+ buckets.set(key, {
160
+ date: sessionDate,
161
+ projectCwd: decodeCwd(encodedCwd),
162
+ encodedCwd,
163
+ promptCount: 0,
164
+ inputTokens: 0,
165
+ outputTokens: 0,
166
+ cacheReadTokens: 0,
167
+ cacheCreationTokens: 0,
168
+ toolCallCount: 0,
169
+ toolBreakdown: {},
170
+ sessionCount: 0,
171
+ errorCount: 0,
172
+ });
173
+ }
174
+
175
+ const b = buckets.get(key);
176
+ b.promptCount += parsed.promptCount;
177
+ b.inputTokens += parsed.inputTokens;
178
+ b.outputTokens += parsed.outputTokens;
179
+ b.cacheReadTokens += parsed.cacheReadTokens;
180
+ b.cacheCreationTokens += parsed.cacheCreationTokens;
181
+ b.toolCallCount += parsed.toolCallCount;
182
+ for (const [tool, cnt] of Object.entries(parsed.toolBreakdown)) {
183
+ b.toolBreakdown[tool] = (b.toolBreakdown[tool] ?? 0) + cnt;
184
+ }
185
+ b.sessionCount++;
186
+ b.errorCount += parsed.errorCount;
187
+ }
188
+
189
+ if (Date.now() - t0 > SLOW_THRESHOLD_MS) {
190
+ console.warn(`[historyAggregator] slow scan: ${Date.now() - t0}ms`);
191
+ partial = true;
192
+ break;
193
+ }
194
+ }
195
+
196
+ const rows = Array.from(buckets.values()).map((b) => ({
197
+ ...b,
198
+ estimatedCostUsd: (b.inputTokens * 3 + b.outputTokens * 15) / 1_000_000,
199
+ }));
200
+
201
+ rows.sort((a, b) => a.date.localeCompare(b.date) || a.projectCwd.localeCompare(b.projectCwd));
202
+
203
+ const scannedMs = Date.now() - t0;
204
+ return { rows, partial, scannedMs, skippedLargeFiles };
205
+ });
206
+ }
207
+
208
+ module.exports = { registerHistoryAggregatorHandlers };
@@ -15,9 +15,11 @@ const logs = require('./logs.cjs');
15
15
  const voiceHotkey = require('./voiceHotkey.cjs');
16
16
  const voiceWizard = require('./voiceWizard.cjs');
17
17
  const scheduler = require('./scheduler.cjs');
18
+ const supervisor = require('./supervisor.cjs');
18
19
  const watchers = require('./watchers.cjs');
19
20
  const otel = require('./otel.cjs');
20
21
  const otelSettings = require('./otelSettings.cjs');
22
+ const { registerHistoryAggregatorHandlers } = require('./historyAggregator.cjs');
21
23
 
22
24
  let mainWindow = null;
23
25
  let rebooting = false;
@@ -410,6 +412,7 @@ voiceHotkey.registerHotkeyHandlers();
410
412
  voiceWizard.registerWizardHandlers();
411
413
  scheduler.registerScheduleHandlers();
412
414
  watchers.registerWatcherHandlers();
415
+ registerHistoryAggregatorHandlers();
413
416
 
414
417
  // OTEL telemetry export (opt-in via ~/.config/session-manager/otel.json).
415
418
  ipcMain.handle('otel:get-config', async () => otelSettings.load());
@@ -630,6 +633,7 @@ app.on('activate', () => {
630
633
  });
631
634
 
632
635
  app.on('before-quit', () => {
636
+ supervisor.stopSupervisor();
633
637
  ptyManager.killAll();
634
638
  configMgr.closeAllWatchers();
635
639
  transcripts.closeAll();
@@ -127,8 +127,22 @@ const setConfigSchema = z.object({
127
127
  ).optional(),
128
128
  firePolicy: z.enum(['when-available', 'on-reset', 'manual']).optional(),
129
129
  utilizationThreshold: z.number().min(0).max(200).optional(),
130
+ supervisor: z.object({
131
+ enabled: z.boolean().optional(),
132
+ intervalMinutes: z.number().int().min(5).max(60).optional(),
133
+ maxConcurrentProbes: z.number().int().min(1).max(5).optional(),
134
+ probeStaleThresholdMinutes: z.number().int().min(5).max(30).optional(),
135
+ }).optional(),
130
136
  }).strict();
131
137
 
138
+ // ──────────────────────────────────────────── History
139
+ const DATE_YYYY_MM_DD = /^\d{4}-\d{2}-\d{2}$/;
140
+
141
+ const historyAggregate = z.object({
142
+ fromDate: z.string().regex(DATE_YYYY_MM_DD).optional(),
143
+ toDate: z.string().regex(DATE_YYYY_MM_DD).optional(),
144
+ }).optional().nullable();
145
+
132
146
  /**
133
147
  * Wrap an IPC handler with schema validation. Returns a new handler that
134
148
  * parses the payload before calling the original.
@@ -162,6 +176,7 @@ module.exports = {
162
176
  openInFinder,
163
177
  openInTerminal,
164
178
  archiveProject,
179
+ historyAggregate,
165
180
  },
166
181
  validated,
167
182
  };
@@ -7,4 +7,6 @@ module.exports = {
7
7
  OFFSET_MINUTES_MAX: 180,
8
8
  CONCURRENCY_CAP_MAX: 20,
9
9
  MAX_JOB_DURATION_MS: 4 * 60 * 60_000,
10
+ SUPERVISOR_INTERVAL_MS: 15 * 60_000,
11
+ SUPERVISOR_PROBE_STALE_MS: 10 * 60_000,
10
12
  };