claude-code-session-manager 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/README.md +44 -0
  2. package/bin/cli.cjs +26 -0
  3. package/dist/assets/abap-DLDM7-KI.js +1 -0
  4. package/dist/assets/apex-DNDY2TF8.js +1 -0
  5. package/dist/assets/azcli-Y6nb8tq_.js +1 -0
  6. package/dist/assets/bat-BwHxbl9M.js +1 -0
  7. package/dist/assets/bicep-CFznDFnq.js +2 -0
  8. package/dist/assets/cameligo-Bf6VGUru.js +1 -0
  9. package/dist/assets/clojure-Dnu-v4kV.js +1 -0
  10. package/dist/assets/codicon-ngg6Pgfi.ttf +0 -0
  11. package/dist/assets/coffee-Bd8akH9Z.js +1 -0
  12. package/dist/assets/cpp-BbWJElDN.js +1 -0
  13. package/dist/assets/csharp-Co3qMtFm.js +1 -0
  14. package/dist/assets/csp-D-4FJmMZ.js +1 -0
  15. package/dist/assets/css-DdJfP1eB.js +3 -0
  16. package/dist/assets/css.worker-B4z49cGk.js +93 -0
  17. package/dist/assets/cssMode-CnHaSLkc.js +1 -0
  18. package/dist/assets/cypher-cTPe9QuQ.js +1 -0
  19. package/dist/assets/dart-BOtBlQCF.js +1 -0
  20. package/dist/assets/dockerfile-BG73LgW2.js +1 -0
  21. package/dist/assets/ecl-BEgZUVRK.js +1 -0
  22. package/dist/assets/editor-BTnBOi8r.css +1 -0
  23. package/dist/assets/editor.main-HqS8FFud.js +908 -0
  24. package/dist/assets/elixir-BkW5O-1t.js +1 -0
  25. package/dist/assets/flow9-BeJ5waoc.js +1 -0
  26. package/dist/assets/freemarker2-Dk6tNdYu.js +3 -0
  27. package/dist/assets/fsharp-PahG7c26.js +1 -0
  28. package/dist/assets/go-acbASCJo.js +1 -0
  29. package/dist/assets/graphql-BxJiqAUM.js +1 -0
  30. package/dist/assets/handlebars-CAvgYvfp.js +1 -0
  31. package/dist/assets/hcl-DtV1sZF8.js +1 -0
  32. package/dist/assets/html-BA1RF9wZ.js +1 -0
  33. package/dist/assets/html.worker-DtiGdgqp.js +470 -0
  34. package/dist/assets/htmlMode-DFW80Ka7.js +1 -0
  35. package/dist/assets/index-D1uJQ7wg.js +121 -0
  36. package/dist/assets/index-D5g_3PBl.css +32 -0
  37. package/dist/assets/ini-Kd9XrMLS.js +1 -0
  38. package/dist/assets/java-CXBNlu9o.js +1 -0
  39. package/dist/assets/javascript-DyjJvfl0.js +1 -0
  40. package/dist/assets/json.worker-leyajbqV.js +58 -0
  41. package/dist/assets/jsonMode-YyKPYrIT.js +7 -0
  42. package/dist/assets/julia-cl7-CwDS.js +1 -0
  43. package/dist/assets/kotlin-s7OhZKlX.js +1 -0
  44. package/dist/assets/less-9HpZscsL.js +2 -0
  45. package/dist/assets/lexon-OrD6JF1K.js +1 -0
  46. package/dist/assets/liquid-FOT2GA4k.js +1 -0
  47. package/dist/assets/lspLanguageFeatures-Bpjj6MSM.js +4 -0
  48. package/dist/assets/lua-Cyyb5UIc.js +1 -0
  49. package/dist/assets/m3-B8OfTtLu.js +1 -0
  50. package/dist/assets/markdown-BFxVWTOG.js +1 -0
  51. package/dist/assets/mdx-BkJBjkOz.js +1 -0
  52. package/dist/assets/mips-CiqrrVzr.js +1 -0
  53. package/dist/assets/msdax-DmeGPVcC.js +1 -0
  54. package/dist/assets/mysql-C_tMU-Nz.js +1 -0
  55. package/dist/assets/objective-c-BDtDVThU.js +1 -0
  56. package/dist/assets/pascal-vHIfCaH5.js +1 -0
  57. package/dist/assets/pascaligo-DtZ0uQbO.js +1 -0
  58. package/dist/assets/perl-Ub6l9XKa.js +1 -0
  59. package/dist/assets/pgsql-BlNEE0v7.js +1 -0
  60. package/dist/assets/php-BBUBE1dy.js +1 -0
  61. package/dist/assets/pla-DSh2-awV.js +1 -0
  62. package/dist/assets/postiats-CocnycG-.js +1 -0
  63. package/dist/assets/powerquery-tScXyioY.js +1 -0
  64. package/dist/assets/powershell-COWaemsV.js +1 -0
  65. package/dist/assets/protobuf-Brw8urJB.js +2 -0
  66. package/dist/assets/pug-8SOpv6rk.js +1 -0
  67. package/dist/assets/python-BCzercLQ.js +1 -0
  68. package/dist/assets/qsharp-Bw9ernYp.js +1 -0
  69. package/dist/assets/r-j7ic8hl3.js +1 -0
  70. package/dist/assets/razor-C06x2uSS.js +1 -0
  71. package/dist/assets/redis-Bu5POkcn.js +1 -0
  72. package/dist/assets/redshift-Bs9aos_-.js +1 -0
  73. package/dist/assets/restructuredtext-CqXO7rUv.js +1 -0
  74. package/dist/assets/ruby-zBfavPgS.js +1 -0
  75. package/dist/assets/rust-BzKRNQWT.js +1 -0
  76. package/dist/assets/sb-BBc9UKZt.js +1 -0
  77. package/dist/assets/scala-D9hQfWCl.js +1 -0
  78. package/dist/assets/scheme-BPhDTwHR.js +1 -0
  79. package/dist/assets/scss-CBJaRo0y.js +3 -0
  80. package/dist/assets/shell-DiJ1NA_G.js +1 -0
  81. package/dist/assets/solidity-Db0IVjzk.js +1 -0
  82. package/dist/assets/sophia-CnS9iZB_.js +1 -0
  83. package/dist/assets/sparql-CJmd_6j2.js +1 -0
  84. package/dist/assets/sql-ClhHkBeG.js +1 -0
  85. package/dist/assets/st-CHwy0fLd.js +1 -0
  86. package/dist/assets/swift-Bqt4WxQ4.js +3 -0
  87. package/dist/assets/systemverilog-Bs9z6M-B.js +1 -0
  88. package/dist/assets/tcl-Dm6ycUr_.js +1 -0
  89. package/dist/assets/ts.worker-59MjiAqk.js +67731 -0
  90. package/dist/assets/tsMode-C8HsFkIE.js +11 -0
  91. package/dist/assets/twig-Csy3S7wG.js +1 -0
  92. package/dist/assets/typescript-COsI_g0X.js +1 -0
  93. package/dist/assets/typespec-Btyra-wh.js +1 -0
  94. package/dist/assets/vb-Db0cS2oM.js +1 -0
  95. package/dist/assets/wgsl-BTesnYfV.js +298 -0
  96. package/dist/assets/xml-DsbXKT_u.js +1 -0
  97. package/dist/assets/yaml-C_KvuH83.js +1 -0
  98. package/dist/index.html +13 -0
  99. package/package.json +75 -0
  100. package/src/main/config.cjs +244 -0
  101. package/src/main/index.cjs +181 -0
  102. package/src/main/ipcSchemas.cjs +110 -0
  103. package/src/main/pty.cjs +153 -0
  104. package/src/main/sessionsStore.cjs +70 -0
  105. package/src/main/transcripts.cjs +212 -0
  106. package/src/preload/api.d.ts +138 -0
  107. package/src/preload/index.cjs +72 -0
@@ -0,0 +1,70 @@
1
+ /**
2
+ * SessionsStore — persists the renderer's tab list to disk so the app can
3
+ * restore tabs + resume claude sessions after an electron restart.
4
+ *
5
+ * Storage: ~/.config/session-manager/tabs.json
6
+ * Shape: { tabs: PersistedTab[], activeTabId: string | null, savedAt: number }
7
+ *
8
+ * Only serializable, durable fields are persisted: id, claudeSessionId, cwd,
9
+ * label, presetId. Runtime-only fields (pid, status, startupCommand,
10
+ * exitCode) are recomputed on boot.
11
+ */
12
+
13
+ const fs = require('node:fs');
14
+ const fsp = require('node:fs/promises');
15
+ const path = require('node:path');
16
+ const os = require('node:os');
17
+ const { ipcMain } = require('electron');
18
+
19
+ function storePath() {
20
+ return path.join(os.homedir(), '.config', 'session-manager', 'tabs.json');
21
+ }
22
+
23
+ async function load() {
24
+ const p = storePath();
25
+ try {
26
+ const raw = await fsp.readFile(p, 'utf8');
27
+ const data = JSON.parse(raw);
28
+ if (!data || !Array.isArray(data.tabs)) return { tabs: [], activeTabId: null, freshStart: false };
29
+ return {
30
+ tabs: data.tabs,
31
+ activeTabId: typeof data.activeTabId === 'string' ? data.activeTabId : null,
32
+ freshStart: !!data.freshStart,
33
+ };
34
+ } catch (e) {
35
+ if (e.code !== 'ENOENT') console.warn('[sessionsStore] load failed:', e.message);
36
+ return { tabs: [], activeTabId: null, freshStart: false };
37
+ }
38
+ }
39
+
40
+ async function save({ tabs, activeTabId, freshStart }) {
41
+ const p = storePath();
42
+ await fsp.mkdir(path.dirname(p), { recursive: true }).catch(() => {});
43
+ const payload = { tabs, activeTabId, savedAt: Date.now() };
44
+ if (freshStart) payload.freshStart = true;
45
+ const body = JSON.stringify(payload, null, 2) + '\n';
46
+ const tmp = `${p}.tmp-${process.pid}-${Date.now()}`;
47
+ await fsp.writeFile(tmp, body, 'utf8');
48
+ await fsp.rename(tmp, p);
49
+ return { ok: true };
50
+ }
51
+
52
+ function registerSessionsHandlers() {
53
+ const { schemas: s, validated: v } = require('./ipcSchemas.cjs');
54
+ ipcMain.handle('sessions:load', () => load());
55
+ ipcMain.handle('sessions:save', v(s.sessionsPayload, (payload) => save(payload)));
56
+ }
57
+
58
+ /** Rewrite persisted tabs with fresh session IDs so next boot starts new sessions. */
59
+ async function markFreshRestart() {
60
+ const { tabs, activeTabId } = await load();
61
+ if (tabs.length === 0) return;
62
+ const crypto = require('node:crypto');
63
+ const freshTabs = tabs.map((t) => ({
64
+ ...t,
65
+ claudeSessionId: crypto.randomUUID(),
66
+ }));
67
+ await save({ tabs: freshTabs, activeTabId, freshStart: true });
68
+ }
69
+
70
+ module.exports = { registerSessionsHandlers, load, save, storePath, markFreshRestart };
@@ -0,0 +1,212 @@
1
+ /**
2
+ * TranscriptsManager — tails Claude Code session JSONL files and emits
3
+ * structured events to the renderer.
4
+ *
5
+ * Transcript path convention (Apr 2026):
6
+ * ~/.claude/projects/<encoded-cwd>/<session-uuid>.jsonl
7
+ *
8
+ * cwd encoding: every non-alphanumeric char → '-' (literal regex
9
+ * /[^a-zA-Z0-9]/g → '-').
10
+ *
11
+ * Deterministic mapping: renderer passes `--session-id <tab-uuid>` when
12
+ * launching `claude`. This manager expects that same UUID as the file's
13
+ * basename. Consumers subscribe per (tabId, cwd, sessionUuid) triple.
14
+ *
15
+ * Each subscription opens a single chokidar watcher on the directory and
16
+ * streams new bytes from the matching file, splits on newlines, zod-passes
17
+ * each line through a lax schema, then forwards typed events.
18
+ */
19
+
20
+ const { ipcMain } = require('electron');
21
+ const fs = require('node:fs');
22
+ const fsp = require('node:fs/promises');
23
+ const path = require('node:path');
24
+ const os = require('node:os');
25
+ const chokidar = require('chokidar');
26
+
27
+ let window = null;
28
+
29
+ /** Map<tabId, Subscription> */
30
+ const subs = new Map();
31
+
32
+ function attachWindow(w) {
33
+ window = w;
34
+ }
35
+
36
+ function encodeCwd(cwd) {
37
+ return cwd.replace(/[^a-zA-Z0-9]/g, '-');
38
+ }
39
+
40
+ function transcriptPath(cwd, sessionUuid) {
41
+ return path.join(os.homedir(), '.claude', 'projects', encodeCwd(cwd), `${sessionUuid}.jsonl`);
42
+ }
43
+
44
+ /**
45
+ * Parse one JSONL line defensively. Real schema drifts, so we pass through
46
+ * anything that parses and tag a coarse `kind`.
47
+ */
48
+ function classifyLine(obj) {
49
+ if (!obj || typeof obj !== 'object') return null;
50
+ // Many shapes exist — try several common fields.
51
+ const type = obj.type || obj.event || obj.role;
52
+ const msg = obj.message || obj;
53
+ const content = msg?.content;
54
+
55
+ // Usage rollups arrive as summary events.
56
+ if (obj.usage || msg?.usage) {
57
+ return { kind: 'usage', data: obj.usage || msg.usage, raw: obj };
58
+ }
59
+
60
+ // Tool uses: scan content array for tool_use blocks.
61
+ if (Array.isArray(content)) {
62
+ for (const block of content) {
63
+ if (block?.type === 'tool_use') {
64
+ if (block.name === 'TodoWrite') {
65
+ return { kind: 'todo_write', data: block.input?.todos || block.input || [], raw: obj };
66
+ }
67
+ if (block.name === 'ExitPlanMode' || block.name === 'EnterPlanMode') {
68
+ return { kind: 'plan', data: block.input, raw: obj };
69
+ }
70
+ if (block.name === 'Agent' || block.name === 'Task') {
71
+ return { kind: 'agent_spawn', data: block.input, raw: obj };
72
+ }
73
+ return {
74
+ kind: 'tool_use',
75
+ data: { name: block.name, input: block.input, id: block.id },
76
+ raw: obj,
77
+ };
78
+ }
79
+ }
80
+ }
81
+
82
+ return { kind: type || 'message', data: obj, raw: obj };
83
+ }
84
+
85
+ /**
86
+ * Read new bytes from the file starting at `offset`, returning {lines, newOffset}.
87
+ * Tolerates partial last lines (keeps them as pending for the next read).
88
+ */
89
+ async function readDelta(filePath, offset, pending) {
90
+ let stat;
91
+ try {
92
+ stat = await fsp.stat(filePath);
93
+ } catch {
94
+ return { lines: [], newOffset: offset, pending };
95
+ }
96
+ if (stat.size < offset) {
97
+ // File was truncated/rotated — start over.
98
+ offset = 0;
99
+ pending = '';
100
+ }
101
+ if (stat.size === offset) return { lines: [], newOffset: offset, pending };
102
+ const fd = await fsp.open(filePath, 'r');
103
+ try {
104
+ const length = stat.size - offset;
105
+ const buf = Buffer.alloc(length);
106
+ await fd.read(buf, 0, length, offset);
107
+ const text = pending + buf.toString('utf8');
108
+ const parts = text.split('\n');
109
+ const newPending = parts.pop() ?? '';
110
+ return { lines: parts.filter(Boolean), newOffset: stat.size, pending: newPending };
111
+ } finally {
112
+ await fd.close();
113
+ }
114
+ }
115
+
116
+ async function flush(sub, { emit = true } = {}) {
117
+ const { lines, newOffset, pending } = await readDelta(sub.filePath, sub.offset, sub.pending);
118
+ sub.offset = newOffset;
119
+ sub.pending = pending;
120
+ for (const line of lines) {
121
+ let obj;
122
+ try {
123
+ obj = JSON.parse(line);
124
+ } catch {
125
+ continue;
126
+ }
127
+ const ev = classifyLine(obj);
128
+ if (!ev) continue;
129
+ // Ring buffer (cap at 500 entries to bound memory).
130
+ sub.buffer.push(ev);
131
+ if (sub.buffer.length > 500) sub.buffer.shift();
132
+ if (emit && window && !window.isDestroyed()) {
133
+ window.webContents.send(`transcript:event:${sub.tabId}`, ev);
134
+ }
135
+ }
136
+ }
137
+
138
+ const MAX_TRANSCRIPT_SUBS = 20;
139
+
140
+ async function subscribe({ tabId, cwd, sessionUuid }) {
141
+ if (subs.has(tabId)) return { ok: true, path: subs.get(tabId).filePath };
142
+ if (subs.size >= MAX_TRANSCRIPT_SUBS) {
143
+ return { ok: false, path: null, error: 'too many active subscriptions' };
144
+ }
145
+ const filePath = transcriptPath(cwd, sessionUuid);
146
+ const dir = path.dirname(filePath);
147
+ await fsp.mkdir(dir, { recursive: true }).catch(() => {});
148
+ const sub = {
149
+ tabId,
150
+ cwd,
151
+ sessionUuid,
152
+ filePath,
153
+ offset: 0,
154
+ pending: '',
155
+ buffer: [],
156
+ watcher: null,
157
+ };
158
+ // If the file already exists, read current content as replay. Do not emit
159
+ // during this initial drain — the renderer drains sub.buffer via
160
+ // `transcript:buffer` after `transcript:subscribe` resolves. Emitting here
161
+ // would race the renderer's onEvent listener registration and drop events.
162
+ if (fs.existsSync(filePath)) {
163
+ await flush(sub, { emit: false });
164
+ }
165
+ const watcher = chokidar.watch(filePath, {
166
+ ignoreInitial: false,
167
+ persistent: true,
168
+ awaitWriteFinish: { stabilityThreshold: 30, pollInterval: 20 },
169
+ });
170
+ watcher.on('add', () => flush(sub).catch(() => {}));
171
+ watcher.on('change', () => flush(sub).catch(() => {}));
172
+ watcher.on('error', (err) => console.warn('[transcripts] watcher error:', err.message));
173
+ sub.watcher = watcher;
174
+ subs.set(tabId, sub);
175
+ return { ok: true, path: filePath };
176
+ }
177
+
178
+ function unsubscribe(tabId) {
179
+ const sub = subs.get(tabId);
180
+ if (!sub) return;
181
+ sub.watcher?.close().catch(() => {});
182
+ subs.delete(tabId);
183
+ }
184
+
185
+ function getBuffer(tabId) {
186
+ const sub = subs.get(tabId);
187
+ return sub ? sub.buffer.slice() : [];
188
+ }
189
+
190
+ function closeAll() {
191
+ for (const sub of subs.values()) sub.watcher?.close().catch(() => {});
192
+ subs.clear();
193
+ }
194
+
195
+ function registerTranscriptHandlers() {
196
+ const { schemas: s, validated: v } = require('./ipcSchemas.cjs');
197
+ ipcMain.handle('transcript:subscribe', v(s.transcriptSubscribe, (payload) => subscribe(payload)));
198
+ ipcMain.handle('transcript:unsubscribe', v(s.transcriptTabId, ({ tabId }) => {
199
+ unsubscribe(tabId);
200
+ return { ok: true };
201
+ }));
202
+ ipcMain.handle('transcript:buffer', v(s.transcriptTabId, ({ tabId }) => getBuffer(tabId)));
203
+ ipcMain.handle('transcript:path', v(s.transcriptPath, ({ cwd, sessionUuid }) => transcriptPath(cwd, sessionUuid)));
204
+ }
205
+
206
+ module.exports = {
207
+ attachWindow,
208
+ registerTranscriptHandlers,
209
+ closeAll,
210
+ encodeCwd,
211
+ transcriptPath,
212
+ };
@@ -0,0 +1,138 @@
1
+ export interface SpawnResult {
2
+ pid: number;
3
+ cwd: string;
4
+ /** True when spawn() found an existing PTY for this tabId and reattached. */
5
+ reattached: boolean;
6
+ }
7
+
8
+ export interface PtyExit {
9
+ exitCode: number;
10
+ signal?: number;
11
+ }
12
+
13
+ export interface ReadJsonResult {
14
+ exists: boolean;
15
+ raw: string;
16
+ data: unknown;
17
+ parseError: string | null;
18
+ mtimeMs: number;
19
+ error: string | null;
20
+ }
21
+
22
+ export interface ReadTextResult {
23
+ exists: boolean;
24
+ text: string;
25
+ mtimeMs: number;
26
+ error: string | null;
27
+ }
28
+
29
+ export interface WriteResult {
30
+ ok: boolean;
31
+ mtimeMs: number;
32
+ error?: string;
33
+ }
34
+
35
+ export interface DirEntry {
36
+ name: string;
37
+ path: string;
38
+ isDirectory: boolean;
39
+ isFile: boolean;
40
+ mtimeMs: number;
41
+ size: number;
42
+ }
43
+
44
+ export interface ListDirResult {
45
+ ok: boolean;
46
+ entries: DirEntry[];
47
+ error: string | null;
48
+ }
49
+
50
+ export interface ConfigChangedEvent {
51
+ path: string;
52
+ mtimeMs: number;
53
+ kind: 'add' | 'change' | 'unlink';
54
+ }
55
+
56
+ export type TranscriptEventKind =
57
+ | 'tool_use'
58
+ | 'todo_write'
59
+ | 'plan'
60
+ | 'usage'
61
+ | 'agent_spawn'
62
+ | 'message'
63
+ | string;
64
+
65
+ export interface TranscriptEvent {
66
+ kind: TranscriptEventKind;
67
+ data: unknown;
68
+ raw: unknown;
69
+ }
70
+
71
+ export interface SubscribeResult {
72
+ ok: boolean;
73
+ path: string;
74
+ }
75
+
76
+ export interface PersistedTab {
77
+ id: string;
78
+ claudeSessionId: string;
79
+ cwd: string;
80
+ label: string;
81
+ presetId: string | null;
82
+ }
83
+
84
+ export interface LoadedSessions {
85
+ tabs: PersistedTab[];
86
+ activeTabId: string | null;
87
+ }
88
+
89
+ export interface SessionManagerAPI {
90
+ app: {
91
+ version: () => Promise<string>;
92
+ homeDir: () => Promise<string>;
93
+ cwd: () => Promise<string>;
94
+ engageRulesPath: () => Promise<string | null>;
95
+ pickDirectory: () => Promise<string | null>;
96
+ rebootApp: () => void;
97
+ onNewSession: (handler: () => void) => () => void;
98
+ onRebootSession: (handler: () => void) => () => void;
99
+ };
100
+ pty: {
101
+ spawn: (payload: { tabId: string; cwd: string; cols?: number; rows?: number }) => Promise<SpawnResult>;
102
+ write: (payload: { tabId: string; data: string }) => void;
103
+ resize: (payload: { tabId: string; cols: number; rows: number }) => void;
104
+ kill: (tabId: string) => void;
105
+ onData: (tabId: string, handler: (data: string) => void) => () => void;
106
+ onExit: (tabId: string, handler: (info: PtyExit) => void) => () => void;
107
+ };
108
+ transcripts: {
109
+ subscribe: (payload: { tabId: string; cwd: string; sessionUuid: string }) => Promise<SubscribeResult>;
110
+ unsubscribe: (tabId: string) => Promise<{ ok: boolean }>;
111
+ buffer: (tabId: string) => Promise<TranscriptEvent[]>;
112
+ pathFor: (cwd: string, sessionUuid: string) => Promise<string>;
113
+ onEvent: (tabId: string, handler: (ev: TranscriptEvent) => void) => () => void;
114
+ };
115
+ sessions: {
116
+ load: () => Promise<LoadedSessions>;
117
+ save: (payload: LoadedSessions) => Promise<{ ok: boolean }>;
118
+ };
119
+ config: {
120
+ readJson: (path: string) => Promise<ReadJsonResult>;
121
+ readText: (path: string) => Promise<ReadTextResult>;
122
+ writeJson: (path: string, data: unknown) => Promise<WriteResult>;
123
+ writeText: (path: string, text: string) => Promise<WriteResult>;
124
+ listDir: (path: string, opts?: { filesOnly?: boolean; dirsOnly?: boolean; includeHidden?: boolean }) => Promise<ListDirResult>;
125
+ exists: (path: string) => Promise<boolean>;
126
+ watch: (paths: string[]) => void;
127
+ unwatch: (paths: string[]) => void;
128
+ onChanged: (handler: (info: ConfigChangedEvent) => void) => () => void;
129
+ };
130
+ }
131
+
132
+ declare global {
133
+ interface Window {
134
+ api: SessionManagerAPI;
135
+ }
136
+ }
137
+
138
+ export {};
@@ -0,0 +1,72 @@
1
+ const { contextBridge, ipcRenderer } = require('electron');
2
+
3
+ contextBridge.exposeInMainWorld('api', {
4
+ app: {
5
+ version: () => ipcRenderer.invoke('app:version'),
6
+ homeDir: () => ipcRenderer.invoke('app:home-dir'),
7
+ cwd: () => ipcRenderer.invoke('app:cwd'),
8
+ engageRulesPath: () => ipcRenderer.invoke('app:engage-rules-path'),
9
+ pickDirectory: () => ipcRenderer.invoke('app:pick-directory'),
10
+ rebootApp: () => ipcRenderer.send('app:reboot-app'),
11
+ onNewSession: (handler) => {
12
+ const listener = () => handler();
13
+ ipcRenderer.on('app:new-session', listener);
14
+ return () => ipcRenderer.removeListener('app:new-session', listener);
15
+ },
16
+ onRebootSession: (handler) => {
17
+ const listener = () => handler();
18
+ ipcRenderer.on('app:reboot-session', listener);
19
+ return () => ipcRenderer.removeListener('app:reboot-session', listener);
20
+ },
21
+ },
22
+ pty: {
23
+ spawn: (payload) => ipcRenderer.invoke('pty:spawn', payload),
24
+ write: (payload) => ipcRenderer.send('pty:write', payload),
25
+ resize: (payload) => ipcRenderer.send('pty:resize', payload),
26
+ kill: (tabId) => ipcRenderer.send('pty:kill', tabId),
27
+ onData: (tabId, handler) => {
28
+ const channel = `pty:data:${tabId}`;
29
+ const listener = (_e, data) => handler(data);
30
+ ipcRenderer.on(channel, listener);
31
+ return () => ipcRenderer.removeListener(channel, listener);
32
+ },
33
+ onExit: (tabId, handler) => {
34
+ const channel = `pty:exit:${tabId}`;
35
+ const listener = (_e, info) => handler(info);
36
+ ipcRenderer.on(channel, listener);
37
+ return () => ipcRenderer.removeListener(channel, listener);
38
+ },
39
+ },
40
+ transcripts: {
41
+ subscribe: (payload) => ipcRenderer.invoke('transcript:subscribe', payload),
42
+ unsubscribe: (tabId) => ipcRenderer.invoke('transcript:unsubscribe', { tabId }),
43
+ buffer: (tabId) => ipcRenderer.invoke('transcript:buffer', { tabId }),
44
+ pathFor: (cwd, sessionUuid) =>
45
+ ipcRenderer.invoke('transcript:path', { cwd, sessionUuid }),
46
+ onEvent: (tabId, handler) => {
47
+ const channel = `transcript:event:${tabId}`;
48
+ const listener = (_e, ev) => handler(ev);
49
+ ipcRenderer.on(channel, listener);
50
+ return () => ipcRenderer.removeListener(channel, listener);
51
+ },
52
+ },
53
+ sessions: {
54
+ load: () => ipcRenderer.invoke('sessions:load'),
55
+ save: (payload) => ipcRenderer.invoke('sessions:save', payload),
56
+ },
57
+ config: {
58
+ readJson: (path) => ipcRenderer.invoke('config:read-json', { path }),
59
+ readText: (path) => ipcRenderer.invoke('config:read-text', { path }),
60
+ writeJson: (path, data) => ipcRenderer.invoke('config:write-json', { path, data }),
61
+ writeText: (path, text) => ipcRenderer.invoke('config:write-text', { path, text }),
62
+ listDir: (path, opts) => ipcRenderer.invoke('config:list-dir', { path, opts }),
63
+ exists: (path) => ipcRenderer.invoke('config:exists', { path }),
64
+ watch: (paths) => ipcRenderer.send('config:watch', { paths }),
65
+ unwatch: (paths) => ipcRenderer.send('config:unwatch', { paths }),
66
+ onChanged: (handler) => {
67
+ const listener = (_e, info) => handler(info);
68
+ ipcRenderer.on('config:changed', listener);
69
+ return () => ipcRenderer.removeListener('config:changed', listener);
70
+ },
71
+ },
72
+ });