claude-code-session-manager 0.8.6 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -65
- package/dist/assets/{cssMode-DBg6nxUL.js → cssMode-DWlBzlpW.js} +1 -1
- package/dist/assets/{freemarker2-CyjUGY3f.js → freemarker2-Cgg83m-Z.js} +1 -1
- package/dist/assets/{handlebars-lhtCWqlB.js → handlebars-C4r4LOI9.js} +1 -1
- package/dist/assets/{html-egptHwbZ.js → html-DaxRI5sW.js} +1 -1
- package/dist/assets/htmlMode-Bu_8jtXo.js +1 -0
- package/dist/assets/{index-DjeqNwqn.js → index-C_tgFedf.js} +1115 -1081
- package/dist/assets/{index-DnLtSCQS.css → index-Dj3Db4OA.css} +1 -1
- package/dist/assets/{javascript-tZbiID3O.js → javascript-D5Ztx-Ej.js} +1 -1
- package/dist/assets/{jsonMode-BGtPN-L-.js → jsonMode-tfsgezVc.js} +1 -1
- package/dist/assets/{liquid-DvTeXhev.js → liquid-F2cD9OL0.js} +1 -1
- package/dist/assets/{lspLanguageFeatures-D9xoxVlV.js → lspLanguageFeatures-Bz_Eih8F.js} +2 -2
- package/dist/assets/{mdx-BQ3Ja4wM.js → mdx-BPlD1clX.js} +1 -1
- package/dist/assets/{ort-wasm-simd-threaded.asyncify-CtKKja6V.wasm → ort-wasm-simd-threaded.asyncify-DMmc6YqF.wasm} +0 -0
- package/dist/assets/{python-C71RWXaP.js → python-B4gUOWNI.js} +1 -1
- package/dist/assets/{razor-w__Mkyns.js → razor-B6pMxVp1.js} +1 -1
- package/dist/assets/{tsMode-DOQLQDB3.js → tsMode-C9nq6cHi.js} +1 -1
- package/dist/assets/{typescript-DEiub2Jt.js → typescript-Do5Vtwxu.js} +1 -1
- package/dist/assets/{whisperWorker-QfIS0sPF.js → whisperWorker-CcsPqZUS.js} +19 -19
- package/dist/assets/{xml-RXkLQscS.js → xml-C0mTbVRp.js} +1 -1
- package/dist/assets/{yaml-C8HIpJku.js → yaml-D3sePJfA.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +18 -10
- package/screenshots/.gitkeep +0 -0
- package/screenshots/README-screenshots.md +13 -0
- package/src/main/config.cjs +47 -9
- package/src/main/historyAggregator.cjs +10 -5
- package/src/main/index.cjs +85 -14
- package/src/main/ipcSchemas.cjs +165 -3
- package/src/main/lib/claudeBin.cjs +39 -0
- package/src/main/lib/encodeCwd.cjs +19 -0
- package/src/main/lib/fileTail.cjs +35 -0
- package/src/main/lib/insideHome.cjs +38 -0
- package/src/main/lib/prdFrontmatter.cjs +51 -0
- package/src/main/lib/sendToRenderer.cjs +21 -0
- package/src/main/memoryTool.cjs +203 -0
- package/src/main/otelSettings.cjs +2 -7
- package/src/main/pluginInstall.cjs +129 -0
- package/src/main/pty.cjs +13 -29
- package/src/main/queueOps.cjs +404 -0
- package/src/main/scheduler/prdParser.cjs +135 -0
- package/src/main/scheduler.cjs +291 -250
- package/src/main/sessionsStore.cjs +2 -6
- package/src/main/supervisor.cjs +3 -35
- package/src/main/teams.cjs +95 -0
- package/src/main/transcripts.cjs +5 -7
- package/src/main/usage.cjs +8 -0
- package/src/main/voiceHotkey.cjs +13 -9
- package/src/main/voiceSettings.cjs +2 -9
- package/src/main/voiceWizard.cjs +4 -11
- package/src/main/watchers.cjs +18 -42
- package/src/preload/api.d.ts +153 -1
- package/src/preload/index.cjs +29 -0
- package/dist/assets/htmlMode-tPDeHGOB.js +0 -1
|
@@ -15,6 +15,7 @@ const fsp = require('node:fs/promises');
|
|
|
15
15
|
const path = require('node:path');
|
|
16
16
|
const os = require('node:os');
|
|
17
17
|
const { ipcMain } = require('electron');
|
|
18
|
+
const config = require('./config.cjs');
|
|
18
19
|
|
|
19
20
|
function storePath() {
|
|
20
21
|
return path.join(os.homedir(), '.config', 'session-manager', 'tabs.json');
|
|
@@ -38,14 +39,9 @@ async function load() {
|
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
async function save({ tabs, activeTabId, freshStart }) {
|
|
41
|
-
const p = storePath();
|
|
42
|
-
await fsp.mkdir(path.dirname(p), { recursive: true }).catch(() => {});
|
|
43
42
|
const payload = { tabs, activeTabId, savedAt: Date.now() };
|
|
44
43
|
if (freshStart) payload.freshStart = true;
|
|
45
|
-
|
|
46
|
-
const tmp = `${p}.tmp-${process.pid}-${Date.now()}`;
|
|
47
|
-
await fsp.writeFile(tmp, body, 'utf8');
|
|
48
|
-
await fsp.rename(tmp, p);
|
|
44
|
+
await config.writeJson(storePath(), payload);
|
|
49
45
|
return { ok: true };
|
|
50
46
|
}
|
|
51
47
|
|
package/src/main/supervisor.cjs
CHANGED
|
@@ -16,6 +16,7 @@ const path = require('node:path');
|
|
|
16
16
|
const os = require('node:os');
|
|
17
17
|
const { spawn, execFileSync } = require('node:child_process');
|
|
18
18
|
const { ipcMain } = require('electron');
|
|
19
|
+
const { resolveClaudeBin } = require('./lib/claudeBin.cjs');
|
|
19
20
|
|
|
20
21
|
const HOME = os.homedir();
|
|
21
22
|
const SUPERVISOR_LOG_PATH = path.join(HOME, '.claude', 'session-manager', 'supervisor.log');
|
|
@@ -27,7 +28,6 @@ const inFlightProbes = new Set();
|
|
|
27
28
|
|
|
28
29
|
let supervisorInterval = null;
|
|
29
30
|
let _readQueue = null;
|
|
30
|
-
let _mutate = null;
|
|
31
31
|
|
|
32
32
|
// ─── /proc helpers (Linux-only) ────────────────────────────────────────────
|
|
33
33
|
|
|
@@ -101,20 +101,7 @@ function getChildBashCmdlines(jobPid) {
|
|
|
101
101
|
|
|
102
102
|
// ─── Log tail helpers ───────────────────────────────────────────────────────
|
|
103
103
|
|
|
104
|
-
|
|
105
|
-
try {
|
|
106
|
-
const stat = fs.statSync(filePath);
|
|
107
|
-
const n = Math.min(stat.size, bytes);
|
|
108
|
-
if (n <= 0) return '';
|
|
109
|
-
const fd = fs.openSync(filePath, 'r');
|
|
110
|
-
const buf = Buffer.alloc(n);
|
|
111
|
-
fs.readSync(fd, buf, 0, n, stat.size - n);
|
|
112
|
-
fs.closeSync(fd);
|
|
113
|
-
return buf.toString('utf8');
|
|
114
|
-
} catch {
|
|
115
|
-
return '';
|
|
116
|
-
}
|
|
117
|
-
}
|
|
104
|
+
const { readTail: readTailBytes } = require('./lib/fileTail.cjs');
|
|
118
105
|
|
|
119
106
|
/**
|
|
120
107
|
* Skim the last 16 KB of a run log for the most recent assistant/user/result
|
|
@@ -178,24 +165,6 @@ function readSupervisorLog(n) {
|
|
|
178
165
|
}
|
|
179
166
|
}
|
|
180
167
|
|
|
181
|
-
// ─── Claude binary resolution ────────────────────────────────────────────────
|
|
182
|
-
|
|
183
|
-
let claudeBinCached = null;
|
|
184
|
-
function resolveClaudeBin() {
|
|
185
|
-
if (claudeBinCached) return claudeBinCached;
|
|
186
|
-
const candidates = [
|
|
187
|
-
path.join(HOME, '.claude', 'local', 'claude'),
|
|
188
|
-
'/usr/local/bin/claude',
|
|
189
|
-
'/opt/homebrew/bin/claude',
|
|
190
|
-
'/usr/bin/claude',
|
|
191
|
-
];
|
|
192
|
-
for (const c of candidates) {
|
|
193
|
-
try { fs.accessSync(c, fs.constants.X_OK); claudeBinCached = c; return c; } catch { /* */ }
|
|
194
|
-
}
|
|
195
|
-
claudeBinCached = 'claude';
|
|
196
|
-
return claudeBinCached;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
168
|
// ─── Probe ──────────────────────────────────────────────────────────────────
|
|
200
169
|
|
|
201
170
|
function buildProbePrompt({ slug, cwd, startedAt, ageMinutes, lastActivityAge, jobPid, pstreeOutput, childBashCmdlines, logTail }) {
|
|
@@ -454,7 +423,7 @@ async function supervisorTick() {
|
|
|
454
423
|
|
|
455
424
|
// ─── Lifecycle ───────────────────────────────────────────────────────────────
|
|
456
425
|
|
|
457
|
-
function startSupervisor({ readQueue
|
|
426
|
+
function startSupervisor({ readQueue }) {
|
|
458
427
|
if (process.platform !== 'linux') {
|
|
459
428
|
console.log('[supervisor] non-Linux platform detected; supervisor is a no-op for v1');
|
|
460
429
|
return;
|
|
@@ -465,7 +434,6 @@ function startSupervisor({ readQueue, mutate }) {
|
|
|
465
434
|
}
|
|
466
435
|
|
|
467
436
|
_readQueue = readQueue;
|
|
468
|
-
_mutate = mutate;
|
|
469
437
|
|
|
470
438
|
stopSupervisor(); // idempotent: clear any existing interval
|
|
471
439
|
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Teams enumerator — surfaces ~/.claude/teams/<name>/ to the renderer.
|
|
3
|
+
*
|
|
4
|
+
* Each team is a directory containing:
|
|
5
|
+
* - config.json: { name, description?, members: [{ name, agentType, model, cwd?, ... }], ... }
|
|
6
|
+
* - inboxes/<thread>.json: one file per pending message thread (depth = file count)
|
|
7
|
+
*
|
|
8
|
+
* We treat teams as feature-flagged: enabled when env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS === "1"
|
|
9
|
+
* is set in merged Claude settings. The renderer does its own check; this
|
|
10
|
+
* handler always returns the on-disk roster so the UI can keep recovering
|
|
11
|
+
* gracefully even if settings are unparseable.
|
|
12
|
+
*
|
|
13
|
+
* All reads route through config.cjs (validatePath + atomic semantics).
|
|
14
|
+
* Complexity: O(T·M) where T = teams and M = members; both are small (< 20).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const { ipcMain } = require('electron');
|
|
18
|
+
const os = require('node:os');
|
|
19
|
+
const path = require('node:path');
|
|
20
|
+
const configMgr = require('./config.cjs');
|
|
21
|
+
const logs = require('./logs.cjs');
|
|
22
|
+
|
|
23
|
+
const TEAMS_ROOT = path.join(os.homedir(), '.claude', 'teams');
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Enumerate teams. Returns { teams: TeamInfo[] }.
|
|
27
|
+
* Failures per-team (unreadable config.json, missing inbox dir) are swallowed —
|
|
28
|
+
* the team is included with whatever data we could recover. The renderer
|
|
29
|
+
* decides how to render incomplete entries.
|
|
30
|
+
*/
|
|
31
|
+
async function listTeams() {
|
|
32
|
+
let dir;
|
|
33
|
+
try {
|
|
34
|
+
dir = await configMgr.listDir(TEAMS_ROOT, { dirsOnly: true });
|
|
35
|
+
} catch {
|
|
36
|
+
// listDir throws when path is outside allowed boundaries — should not
|
|
37
|
+
// happen for ~/.claude/teams, but guard anyway.
|
|
38
|
+
return { teams: [] };
|
|
39
|
+
}
|
|
40
|
+
if (!dir.ok) {
|
|
41
|
+
if (dir.error) logs.writeLine({ level: 'warn', scope: 'teams', message: 'listDir failed', meta: { error: dir.error } });
|
|
42
|
+
return { teams: [] };
|
|
43
|
+
}
|
|
44
|
+
if (dir.entries.length === 0) return { teams: [] };
|
|
45
|
+
|
|
46
|
+
const teams = [];
|
|
47
|
+
for (const entry of dir.entries) {
|
|
48
|
+
const teamName = entry.name;
|
|
49
|
+
const configPath = path.join(entry.path, 'config.json');
|
|
50
|
+
const inboxDir = path.join(entry.path, 'inboxes');
|
|
51
|
+
|
|
52
|
+
let config = null;
|
|
53
|
+
try {
|
|
54
|
+
const r = await configMgr.readJson(configPath);
|
|
55
|
+
if (r.exists && !r.parseError) config = r.data;
|
|
56
|
+
} catch { /* unreadable — leave config null */ }
|
|
57
|
+
|
|
58
|
+
let inboxDepth = 0;
|
|
59
|
+
try {
|
|
60
|
+
const inbox = await configMgr.listDir(inboxDir, { filesOnly: true });
|
|
61
|
+
if (inbox.ok) {
|
|
62
|
+
inboxDepth = inbox.entries.filter((f) => f.name.endsWith('.json')).length;
|
|
63
|
+
}
|
|
64
|
+
} catch { /* no inbox dir is fine */ }
|
|
65
|
+
|
|
66
|
+
const members = Array.isArray(config?.members)
|
|
67
|
+
? config.members.map((m) => ({
|
|
68
|
+
name: typeof m?.name === 'string' ? m.name : 'unnamed',
|
|
69
|
+
agentType: typeof m?.agentType === 'string' ? m.agentType : null,
|
|
70
|
+
model: typeof m?.model === 'string' ? m.model : null,
|
|
71
|
+
}))
|
|
72
|
+
: [];
|
|
73
|
+
|
|
74
|
+
teams.push({
|
|
75
|
+
name: teamName,
|
|
76
|
+
configPath,
|
|
77
|
+
description: typeof config?.description === 'string' ? config.description : null,
|
|
78
|
+
leadAgentId: typeof config?.leadAgentId === 'string' ? config.leadAgentId : null,
|
|
79
|
+
members,
|
|
80
|
+
memberCount: members.length,
|
|
81
|
+
inboxDepth,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return { teams };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function registerTeamsHandlers() {
|
|
88
|
+
ipcMain.handle('teams:list', () => listTeams());
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = {
|
|
92
|
+
registerTeamsHandlers,
|
|
93
|
+
// exported for direct use / tests
|
|
94
|
+
listTeams,
|
|
95
|
+
};
|
package/src/main/transcripts.cjs
CHANGED
|
@@ -24,6 +24,8 @@ const path = require('node:path');
|
|
|
24
24
|
const os = require('node:os');
|
|
25
25
|
const chokidar = require('chokidar');
|
|
26
26
|
const otel = require('./otel.cjs');
|
|
27
|
+
const logs = require('./logs.cjs');
|
|
28
|
+
const { sendIfAlive } = require('./lib/sendToRenderer.cjs');
|
|
27
29
|
|
|
28
30
|
let window = null;
|
|
29
31
|
|
|
@@ -34,9 +36,7 @@ function attachWindow(w) {
|
|
|
34
36
|
window = w;
|
|
35
37
|
}
|
|
36
38
|
|
|
37
|
-
|
|
38
|
-
return cwd.replace(/[^a-zA-Z0-9]/g, '-');
|
|
39
|
-
}
|
|
39
|
+
const { encodeCwd } = require('./lib/encodeCwd.cjs');
|
|
40
40
|
|
|
41
41
|
function transcriptPath(cwd, sessionUuid) {
|
|
42
42
|
return path.join(os.homedir(), '.claude', 'projects', encodeCwd(cwd), `${sessionUuid}.jsonl`);
|
|
@@ -135,9 +135,7 @@ async function flush(sub, { emit = true } = {}) {
|
|
|
135
135
|
// Ring buffer (cap at 500 entries to bound memory).
|
|
136
136
|
sub.buffer.push(ev);
|
|
137
137
|
if (sub.buffer.length > 500) sub.buffer.shift();
|
|
138
|
-
if (emit
|
|
139
|
-
window.webContents.send(`transcript:event:${sub.tabId}`, ev);
|
|
140
|
-
}
|
|
138
|
+
if (emit) sendIfAlive(window, `transcript:event:${sub.tabId}`, ev);
|
|
141
139
|
// Mirror to OTEL — no-op when disabled. We emit on the initial drain too
|
|
142
140
|
// so backfilled transcripts show up in the trace store.
|
|
143
141
|
otel.recordTranscriptEvent({
|
|
@@ -184,7 +182,7 @@ async function subscribe({ tabId, cwd, sessionUuid }) {
|
|
|
184
182
|
});
|
|
185
183
|
watcher.on('add', () => flush(sub).catch(() => {}));
|
|
186
184
|
watcher.on('change', () => flush(sub).catch(() => {}));
|
|
187
|
-
watcher.on('error', (err) =>
|
|
185
|
+
watcher.on('error', (err) => logs.writeLine({ level: 'warn', scope: 'transcripts', message: 'chokidar watcher error', meta: { error: err?.message } }));
|
|
188
186
|
sub.watcher = watcher;
|
|
189
187
|
subs.set(tabId, sub);
|
|
190
188
|
return { ok: true, path: filePath };
|
package/src/main/usage.cjs
CHANGED
|
@@ -24,6 +24,10 @@ const { refreshIfNeeded, expiresAtMs } = require('./lib/credentials.cjs');
|
|
|
24
24
|
|
|
25
25
|
const USAGE_URL = 'https://api.anthropic.com/api/oauth/usage';
|
|
26
26
|
const CACHE_PATH = path.join(os.homedir(), '.claude', 'session-manager', 'billing-cache.json');
|
|
27
|
+
// Coalesce the 4 renderer pollers (Overview/AppStatusBar/StatusBar/Usage). A
|
|
28
|
+
// fresh ok-cache is served directly without touching the network. Auth/
|
|
29
|
+
// transient/config skip this TTL so they retry promptly on next poll.
|
|
30
|
+
const OK_CACHE_TTL_MS = 30_000;
|
|
27
31
|
|
|
28
32
|
/**
|
|
29
33
|
* Pure: classify a raw HTTP response status + body from the usage endpoint into a
|
|
@@ -140,6 +144,10 @@ function registerBillingHandlers() {
|
|
|
140
144
|
ipcMain.handle('billing:fetch', async () => {
|
|
141
145
|
if (hydrationPromise) { await hydrationPromise; hydrationPromise = null; }
|
|
142
146
|
|
|
147
|
+
if (cache && cache.fetchedAt && Date.now() - cache.fetchedAt < OK_CACHE_TTL_MS) {
|
|
148
|
+
return { kind: 'ok', data: cache.data };
|
|
149
|
+
}
|
|
150
|
+
|
|
143
151
|
const r = await fetchUsage();
|
|
144
152
|
if (r.kind === 'ok') {
|
|
145
153
|
cache = { data: r.data, fetchedAt: Date.now(), sourceCredsExpiresAt: r.data.credentialsExpiresAt };
|
package/src/main/voiceHotkey.cjs
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
const { app, globalShortcut, ipcMain } = require('electron');
|
|
19
19
|
const voiceSettings = require('./voiceSettings.cjs');
|
|
20
20
|
const { voiceHotkeyLog } = require('./lib/voice-hotkey-log.cjs');
|
|
21
|
+
const { schemas, validated } = require('./ipcSchemas.cjs');
|
|
21
22
|
|
|
22
23
|
let mainWindow = null;
|
|
23
24
|
let currentConfig = null;
|
|
@@ -297,7 +298,9 @@ function registerHotkeyHandlers() {
|
|
|
297
298
|
return currentConfig;
|
|
298
299
|
});
|
|
299
300
|
|
|
300
|
-
ipcMain.handle('voice:set-hotkey', async (
|
|
301
|
+
ipcMain.handle('voice:set-hotkey', validated(schemas.voiceSetHotkey, async (cfg) => {
|
|
302
|
+
// voiceSettings.isValidConfig adds one cross-field check zod can't express
|
|
303
|
+
// tersely (global=true + mode=hold is rejected, see PRD F1 v2). Keep it.
|
|
301
304
|
if (!voiceSettings.isValidConfig(cfg)) {
|
|
302
305
|
throw new Error('Invalid voice hotkey config');
|
|
303
306
|
}
|
|
@@ -308,7 +311,7 @@ function registerHotkeyHandlers() {
|
|
|
308
311
|
mainWindow.webContents.send('voice:hotkey-changed', currentConfig);
|
|
309
312
|
}
|
|
310
313
|
return { ok: true, config: currentConfig };
|
|
311
|
-
});
|
|
314
|
+
}));
|
|
312
315
|
|
|
313
316
|
ipcMain.handle('voice:get-hotkey-config-path', () => voiceSettings.storePath());
|
|
314
317
|
|
|
@@ -317,20 +320,21 @@ function registerHotkeyHandlers() {
|
|
|
317
320
|
return await voiceSettings.loadDevice();
|
|
318
321
|
});
|
|
319
322
|
|
|
320
|
-
ipcMain.handle('voice:set-device-pref', async (
|
|
321
|
-
if (!voiceSettings.isValidDevicePref(pref)) {
|
|
322
|
-
throw new Error('Invalid device pref payload');
|
|
323
|
-
}
|
|
323
|
+
ipcMain.handle('voice:set-device-pref', validated(schemas.voiceSetDevicePref, async (pref) => {
|
|
324
324
|
await voiceSettings.saveDevice(pref);
|
|
325
325
|
return { ok: true };
|
|
326
|
-
});
|
|
326
|
+
}));
|
|
327
327
|
|
|
328
328
|
// Renderer pings this when isRecording flips so we can prefix the window
|
|
329
|
-
// title with `● REC — ` (PRD F1 v2 §Security: privacy invariant).
|
|
329
|
+
// title with `● REC — ` (PRD F1 v2 §Security: privacy invariant). This is
|
|
330
|
+
// `ipcMain.on` (not handle), so we can't use the validated() helper —
|
|
331
|
+
// safeParse inline and silently drop malformed payloads.
|
|
330
332
|
ipcMain.on('voice:set-recording', (_e, recording) => {
|
|
333
|
+
const parsed = schemas.voiceSetRecording.safeParse(recording);
|
|
334
|
+
if (!parsed.success) return;
|
|
331
335
|
if (!mainWindow || mainWindow.isDestroyed()) return;
|
|
332
336
|
const baseTitle = 'Claude Session Manager';
|
|
333
|
-
const next =
|
|
337
|
+
const next = parsed.data ? `● REC — ${baseTitle}` : baseTitle;
|
|
334
338
|
try { mainWindow.setTitle(next); } catch { /* */ }
|
|
335
339
|
});
|
|
336
340
|
}
|
|
@@ -24,6 +24,7 @@ const fs = require('node:fs');
|
|
|
24
24
|
const fsp = require('node:fs/promises');
|
|
25
25
|
const path = require('node:path');
|
|
26
26
|
const os = require('node:os');
|
|
27
|
+
const config = require('./config.cjs');
|
|
27
28
|
|
|
28
29
|
const SCHEMA_VERSION = 1;
|
|
29
30
|
const DEVICE_SCHEMA_VERSION = 1;
|
|
@@ -103,17 +104,9 @@ async function readRaw() {
|
|
|
103
104
|
let writeQueue = Promise.resolve();
|
|
104
105
|
async function writeMerged(patch) {
|
|
105
106
|
const run = async () => {
|
|
106
|
-
const p = storePath();
|
|
107
|
-
await fsp.mkdir(path.dirname(p), { recursive: true }).catch(() => {});
|
|
108
107
|
const existing = (await readRaw()) || {};
|
|
109
108
|
const next = { ...existing, ...patch };
|
|
110
|
-
|
|
111
|
-
const tmp = `${p}.tmp-${process.pid}-${Date.now()}`;
|
|
112
|
-
await fsp.writeFile(tmp, body, { encoding: 'utf8', mode: 0o600 });
|
|
113
|
-
// chmod tmp explicitly because some platforms ignore the mode arg on
|
|
114
|
-
// writeFile when the file pre-exists. Then rename for atomicity.
|
|
115
|
-
try { await fsp.chmod(tmp, 0o600); } catch { /* */ }
|
|
116
|
-
await fsp.rename(tmp, p);
|
|
109
|
+
await config.writeTextAtomic(storePath(), JSON.stringify(next, null, 2) + '\n', { mode: 0o600 });
|
|
117
110
|
return { ok: true };
|
|
118
111
|
};
|
|
119
112
|
// Tail-promise pattern: each call awaits the previous, so writes are
|
package/src/main/voiceWizard.cjs
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* voiceWizard — F7 first-run mic-check wizard, main-process side.
|
|
3
3
|
*
|
|
4
|
-
* Owns
|
|
4
|
+
* Owns:
|
|
5
5
|
* - voice:wizard-state → returns persisted state + current schema constant
|
|
6
6
|
* - voice:wizard-complete → stamps completedAt + completedSchema to voice.json
|
|
7
|
-
* - app:is-e2e → exposes process.env.SM_E2E === '1' to the renderer
|
|
8
7
|
*
|
|
9
8
|
* No window state held here; persistence is delegated to voiceSettings.cjs
|
|
10
9
|
* (additive `wizard` subtree on the same file as F1/F5).
|
|
@@ -12,6 +11,7 @@
|
|
|
12
11
|
|
|
13
12
|
const { ipcMain } = require('electron');
|
|
14
13
|
const voiceSettings = require('./voiceSettings.cjs');
|
|
14
|
+
const { schemas, validated } = require('./ipcSchemas.cjs');
|
|
15
15
|
|
|
16
16
|
function registerWizardHandlers() {
|
|
17
17
|
ipcMain.handle('voice:wizard-state', async () => {
|
|
@@ -32,23 +32,16 @@ function registerWizardHandlers() {
|
|
|
32
32
|
return { ok: true, ...next, currentSchema: voiceSettings.WIZARD_SCHEMA };
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
-
// E2E plumbing: tests set SM_E2E=1 to suppress the wizard auto-trigger.
|
|
36
|
-
// The renderer reads this once on mount.
|
|
37
|
-
ipcMain.handle('app:is-e2e', () => process.env.SM_E2E === '1');
|
|
38
|
-
|
|
39
35
|
// F8 — turn-detector settings (additive subtree on voice.json).
|
|
40
36
|
// MVP: persistence only; no model loaded in v1 (see PRD §Loading & inference).
|
|
41
37
|
ipcMain.handle('voice:get-turn-detector', async () => {
|
|
42
38
|
return await voiceSettings.loadTurnDetector();
|
|
43
39
|
});
|
|
44
40
|
|
|
45
|
-
ipcMain.handle('voice:set-turn-detector', async (
|
|
46
|
-
if (!voiceSettings.isValidTurnDetectorState(state)) {
|
|
47
|
-
throw new Error('Invalid turn-detector state payload');
|
|
48
|
-
}
|
|
41
|
+
ipcMain.handle('voice:set-turn-detector', validated(schemas.voiceSetTurnDetector, async (state) => {
|
|
49
42
|
await voiceSettings.saveTurnDetector(state);
|
|
50
43
|
return { ok: true, state: await voiceSettings.loadTurnDetector() };
|
|
51
|
-
});
|
|
44
|
+
}));
|
|
52
45
|
}
|
|
53
46
|
|
|
54
47
|
module.exports = { registerWizardHandlers };
|
package/src/main/watchers.cjs
CHANGED
|
@@ -15,6 +15,8 @@ const path = require('node:path');
|
|
|
15
15
|
const os = require('node:os');
|
|
16
16
|
const fs = require('node:fs');
|
|
17
17
|
const { cleanChildEnv } = require('./lib/cleanEnv.cjs');
|
|
18
|
+
const { assertCwdInsideHome } = require('./lib/insideHome.cjs');
|
|
19
|
+
const { sendIfAlive } = require('./lib/sendToRenderer.cjs');
|
|
18
20
|
|
|
19
21
|
// Splits a readable stream into lines capped at maxLineBytes. Prevents OOM
|
|
20
22
|
// from commands that produce no newlines (e.g. cat /dev/urandom | base64).
|
|
@@ -59,17 +61,8 @@ class WatcherManager {
|
|
|
59
61
|
add({ tabId, label, command, cwd }) {
|
|
60
62
|
const resolvedCwd = cwd || process.cwd();
|
|
61
63
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
let realCwd;
|
|
65
|
-
try {
|
|
66
|
-
realCwd = fs.realpathSync(resolvedCwd);
|
|
67
|
-
} catch {
|
|
68
|
-
realCwd = path.resolve(resolvedCwd);
|
|
69
|
-
}
|
|
70
|
-
if (realCwd !== home && !realCwd.startsWith(home + path.sep)) {
|
|
71
|
-
throw new Error(`watcher cwd outside home directory: ${realCwd}`);
|
|
72
|
-
}
|
|
64
|
+
const r = assertCwdInsideHome(resolvedCwd);
|
|
65
|
+
if (!r.ok) throw new Error(`watcher ${r.error}`);
|
|
73
66
|
|
|
74
67
|
const watcherId = crypto.randomUUID();
|
|
75
68
|
const trimmedLabel = (label && label.trim()) || command.slice(0, 40);
|
|
@@ -95,14 +88,12 @@ class WatcherManager {
|
|
|
95
88
|
|
|
96
89
|
const emitLine = (line) => {
|
|
97
90
|
entry.lineCount++;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
});
|
|
105
|
-
}
|
|
91
|
+
sendIfAlive(this.window, 'watcher:line', {
|
|
92
|
+
tabId,
|
|
93
|
+
watcherId,
|
|
94
|
+
line,
|
|
95
|
+
ts: Date.now(),
|
|
96
|
+
});
|
|
106
97
|
};
|
|
107
98
|
|
|
108
99
|
lineSplit(child.stdout, emitLine);
|
|
@@ -119,9 +110,7 @@ class WatcherManager {
|
|
|
119
110
|
if (this.watchers.has(watcherId)) {
|
|
120
111
|
emitLine(`[exited code=${code ?? 'null'}${signal ? ` signal=${signal}` : ''}]`);
|
|
121
112
|
this.watchers.delete(watcherId);
|
|
122
|
-
|
|
123
|
-
this.window.webContents.send('watcher:closed', { tabId, watcherId });
|
|
124
|
-
}
|
|
113
|
+
sendIfAlive(this.window, 'watcher:closed', { tabId, watcherId });
|
|
125
114
|
}
|
|
126
115
|
});
|
|
127
116
|
|
|
@@ -164,9 +153,7 @@ class WatcherManager {
|
|
|
164
153
|
setTimeout(() => {
|
|
165
154
|
try { if (w.child.exitCode === null && w.child.signalCode === null) w.child.kill('SIGKILL'); } catch { /* */ }
|
|
166
155
|
}, 2000).unref?.();
|
|
167
|
-
|
|
168
|
-
this.window.webContents.send('watcher:closed', { tabId: w.tabId, watcherId });
|
|
169
|
-
}
|
|
156
|
+
sendIfAlive(this.window, 'watcher:closed', { tabId: w.tabId, watcherId });
|
|
170
157
|
return { ok: true };
|
|
171
158
|
}
|
|
172
159
|
|
|
@@ -186,27 +173,16 @@ function attachWindow(window) {
|
|
|
186
173
|
}
|
|
187
174
|
|
|
188
175
|
function registerWatcherHandlers() {
|
|
189
|
-
const {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
cwd: z.string().max(4096).optional().nullable(),
|
|
195
|
-
});
|
|
196
|
-
const listSchema = z.object({ tabId: z.string().min(1).max(128) });
|
|
197
|
-
const removeSchema = z.object({ watcherId: z.string().min(1).max(128) });
|
|
198
|
-
const killTabSchema = z.object({ tabId: z.string().min(1).max(128) });
|
|
199
|
-
|
|
200
|
-
ipcMain.handle('watchers:add', (_e, payload) => manager.add(addSchema.parse(payload)));
|
|
201
|
-
ipcMain.handle('watchers:list', (_e, payload) => manager.list(listSchema.parse(payload)));
|
|
202
|
-
ipcMain.handle('watchers:remove', (_e, payload) => manager.remove(removeSchema.parse(payload)));
|
|
203
|
-
ipcMain.handle('watchers:kill-tab', (_e, payload) => {
|
|
204
|
-
const { tabId } = killTabSchema.parse(payload);
|
|
176
|
+
const { schemas: s, validated: v } = require('./ipcSchemas.cjs');
|
|
177
|
+
ipcMain.handle('watchers:add', v(s.watchersAdd, (payload) => manager.add(payload)));
|
|
178
|
+
ipcMain.handle('watchers:list', v(s.watchersList, (payload) => manager.list(payload)));
|
|
179
|
+
ipcMain.handle('watchers:remove', v(s.watchersRemove, (payload) => manager.remove(payload)));
|
|
180
|
+
ipcMain.handle('watchers:kill-tab', v(s.watchersKillTab, ({ tabId }) => {
|
|
205
181
|
for (const [id, w] of manager.watchers) {
|
|
206
182
|
if (w.tabId === tabId) manager.remove({ watcherId: id });
|
|
207
183
|
}
|
|
208
184
|
return { ok: true };
|
|
209
|
-
});
|
|
185
|
+
}));
|
|
210
186
|
}
|
|
211
187
|
|
|
212
188
|
module.exports = { manager, attachWindow, registerWatcherHandlers };
|