claude-code-session-manager 0.104.2 → 0.105.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 (77) hide show
  1. package/README.md +15 -3
  2. package/bin/cli.cjs +0 -5
  3. package/dist/assets/{AgentLibrary-QPw9gC_o.js → AgentLibrary-BDtr4oI-.js} +1 -1
  4. package/dist/assets/{Button-8RAbgh1d.js → Button-CuXBCQDp.js} +1 -1
  5. package/dist/assets/{DataModel-BEkuOZuS.js → DataModel-DmvRipT5.js} +1 -1
  6. package/dist/assets/{History-BLrNSazM.js → History-B-v9pwwr.js} +1 -1
  7. package/dist/assets/{Hooks-C8P6wQq1.js → Hooks-S_0oHHlr.js} +1 -1
  8. package/dist/assets/{Library-CWFvBZw-.js → Library-BcbGJxQ3.js} +1 -1
  9. package/dist/assets/{ListDetail-B1jVMgzN.js → ListDetail-DtqHqJHt.js} +1 -1
  10. package/dist/assets/{MarkdownEditor-JrnUbWjW.js → MarkdownEditor-B56sPp_l.js} +1 -1
  11. package/dist/assets/{McpServers-BGqTxDBm.js → McpServers-D5VnmxAn.js} +1 -1
  12. package/dist/assets/{Memory-h8bhRNyI.js → Memory-0yGZLyRP.js} +1 -1
  13. package/dist/assets/{Panel-srf0FT0m.js → Panel-NoAADJwx.js} +1 -1
  14. package/dist/assets/{Permissions-BA4oWydI.js → Permissions-D2Dh-ee8.js} +1 -1
  15. package/dist/assets/{Plugins-DTuTQqUL.js → Plugins-kGoUgS9b.js} +2 -2
  16. package/dist/assets/{ProvenanceBadge-BW18EquH.js → ProvenanceBadge-CHwDFTX7.js} +1 -1
  17. package/dist/assets/{SaveBar-Cc6UukFa.js → SaveBar-BtKNMIYK.js} +1 -1
  18. package/dist/assets/{Scheduler-BctPlNL1.js → Scheduler-B0_kMMDs.js} +1 -1
  19. package/dist/assets/{ScopeSwitcher-DV1OnSuc.js → ScopeSwitcher-BusohWZ5.js} +1 -1
  20. package/dist/assets/{Settings-Bpyt1RGa.js → Settings-DuRJntaE.js} +1 -1
  21. package/dist/assets/{SkillReferenceGraph-B28AYo12.js → SkillReferenceGraph-BHOdIpsy.js} +1 -1
  22. package/dist/assets/{Skills-zXLI5OB_.js → Skills-DSTLZimI.js} +1 -1
  23. package/dist/assets/{SystemPrompt-DKoN0UAV.js → SystemPrompt-BG7wFWdb.js} +1 -1
  24. package/dist/assets/{TagLibrary-BkDOa8IM.js → TagLibrary-Bx27aE3_.js} +1 -1
  25. package/dist/assets/{TiptapBody-CIY1f6mI.js → TiptapBody-j0O8J7YI.js} +1 -1
  26. package/dist/assets/{Toggle-C139wuAK.js → Toggle-DzVZ6rC7.js} +1 -1
  27. package/dist/assets/{ViewTabs-CUEfsMOe.js → ViewTabs-BizjuynT.js} +1 -1
  28. package/dist/assets/{index-D89EUizB.css → index-BLVzrc4k.css} +1 -1
  29. package/dist/assets/{index-DrvxIbmR.js → index-CLIaYljt.js} +416 -416
  30. package/dist/assets/{settingsSchema-CAFckLz6.js → settingsSchema-7MRAxaJk.js} +1 -1
  31. package/dist/assets/useAgentsDirWatch-BUwfU2Kq.js +1 -0
  32. package/dist/assets/{useConfigDirWatch-QKxfZx9X.js → useConfigDirWatch-ot0VYwQC.js} +1 -1
  33. package/dist/index.html +2 -2
  34. package/package.json +10 -12
  35. package/plugins/session-manager-dev/skills/builder/3-publish/SKILL.md +12 -0
  36. package/plugins/session-manager-dev/skills/develop/SKILL.md +4 -2
  37. package/scripts/scheduler-mcp-server.cjs +7 -17
  38. package/src/main/build-info.json +4 -4
  39. package/src/main/chatRunner.cjs +4 -4
  40. package/src/main/historyAggregator.cjs +7 -1
  41. package/src/main/index.cjs +34 -1
  42. package/src/main/lib/agentPersonaSchema.cjs +8 -1
  43. package/src/main/lib/appRuntime.cjs +57 -0
  44. package/src/main/lib/childWithLog.cjs +10 -10
  45. package/src/main/lib/claudeBin.cjs +40 -16
  46. package/src/main/lib/cleanEnv.cjs +29 -6
  47. package/src/main/lib/defaultShell.cjs +36 -0
  48. package/src/main/lib/definitionOfDone.cjs +6 -2
  49. package/src/main/lib/delegationReadiness.cjs +13 -5
  50. package/src/main/lib/gitWorktree.cjs +6 -3
  51. package/src/main/lib/insideHome.cjs +15 -4
  52. package/src/main/lib/killTree.cjs +80 -0
  53. package/src/main/lib/loadGate.cjs +16 -0
  54. package/src/main/lib/loginShellPath.cjs +82 -0
  55. package/src/main/lib/machineProfile.cjs +4 -2
  56. package/src/main/lib/mcpStdioServer.cjs +127 -0
  57. package/src/main/lib/openExternalApp.cjs +62 -11
  58. package/src/main/lib/otlpTraceExporter.cjs +147 -0
  59. package/src/main/lib/prereqs.cjs +145 -0
  60. package/src/main/lib/procIdentity.cjs +37 -2
  61. package/src/main/lib/rcaReport.cjs +1 -0
  62. package/src/main/lib/reaperHelpers.cjs +5 -2
  63. package/src/main/lib/shippedPersonaSeeds.cjs +10 -0
  64. package/src/main/lib/timeoutShimScript.cjs +14 -2
  65. package/src/main/lib/updateCheck.cjs +41 -11
  66. package/src/main/lib/watchdogHelpers.cjs +23 -3
  67. package/src/main/lib/winSpawn.cjs +67 -0
  68. package/src/main/otel.cjs +18 -62
  69. package/src/main/pty.cjs +32 -11
  70. package/src/main/runVerify.cjs +42 -2
  71. package/src/main/scheduler.cjs +105 -20
  72. package/src/main/seedSchedulerMcp.cjs +11 -2
  73. package/src/preload/api.d.ts +14 -0
  74. package/src/preload/index.cjs +2 -0
  75. package/src/seed/agents/validator.md +2 -2
  76. package/dist/assets/useAgentsDirWatch-XhQEVlaK.js +0 -1
  77. package/scripts/postinstall.cjs +0 -52
@@ -49,11 +49,11 @@ function findCommand(name) {
49
49
  *
50
50
  * @returns {Promise<{ ok: true, opener: string } | { ok: false, error: string }>}
51
51
  */
52
- function spawnDetached(cmd, args, opener = cmd) {
52
+ function spawnDetached(cmd, args, opener = cmd, spawnFn = spawn, extraOpts = {}) {
53
53
  return new Promise((resolve) => {
54
54
  let child;
55
55
  try {
56
- child = spawn(cmd, args, { detached: true, stdio: 'ignore', env: cleanChildEnv() });
56
+ child = spawnFn(cmd, args, { detached: true, stdio: 'ignore', env: cleanChildEnv(), ...extraOpts });
57
57
  } catch (e) {
58
58
  resolve({ ok: false, error: `failed to launch ${opener}: ${e?.message ?? e}` });
59
59
  return;
@@ -116,12 +116,16 @@ async function openFileInEditor({ path: abs, line, col, editor }) {
116
116
  }
117
117
 
118
118
  /**
119
- * Open a directory in the OS file manager (Finder on macOS, Nautilus etc on Linux).
119
+ * Open a directory in the OS file manager (Finder on macOS, Nautilus etc on Linux,
120
+ * explorer.exe on Windows).
120
121
  *
121
122
  * @param {{ cwd: string }} opts
123
+ * @param {{ platform?: string, spawn?: Function }} [deps]
122
124
  * @returns {Promise<{ ok: true, opener: string } | { ok: false, error: string }>}
123
125
  */
124
- async function openInFinder({ cwd }) {
126
+ async function openInFinder({ cwd }, deps = {}) {
127
+ const { platform = process.platform, spawn: spawnFn = spawn } = deps;
128
+ if (platform === 'win32') return spawnDetached('explorer.exe', [cwd], 'explorer', spawnFn);
125
129
  const errStr = await shell.openPath(cwd);
126
130
  if (errStr) return { ok: false, error: errStr };
127
131
  return { ok: true, opener: 'shell' };
@@ -132,28 +136,75 @@ async function openInFinder({ cwd }) {
132
136
  *
133
137
  * Linux: tries gnome-terminal → konsole → xfce4-terminal → xterm.
134
138
  * macOS: delegates to Terminal.app via `open -a`.
139
+ * Windows: `wt.exe -d <cwd>` when on PATH, else powershell.exe -NoExit in a new
140
+ * console via `cmd.exe /d /c start`.
135
141
  *
136
142
  * Arg shapes per terminal are preserved verbatim from index.cjs (AC impl note):
137
143
  * gnome-terminal → ['--working-directory=<cwd>']
138
144
  * others → ['-e', "bash -c \"cd '<cwd>' && exec bash\""]
139
145
  *
140
146
  * @param {{ cwd: string }} opts
147
+ * @param {{ platform?: string, spawn?: Function, findCommand?: (n: string) => string | null }} [deps]
141
148
  * @returns {Promise<{ ok: true, opener: string } | { ok: false, error: string }>}
142
149
  */
143
- async function openInTerminal({ cwd }) {
144
- if (process.platform === 'linux') {
150
+ async function openInTerminal({ cwd }, deps = {}) {
151
+ const { platform = process.platform, spawn: spawnFn = spawn, findCommand: find = findCommand } = deps;
152
+ if (platform === 'linux') {
145
153
  const terms = ['gnome-terminal', 'konsole', 'xfce4-terminal', 'xterm'];
146
154
  for (const t of terms) {
147
- if (!findCommand(t)) continue;
155
+ if (!find(t)) continue;
148
156
  const args = t === 'gnome-terminal'
149
157
  ? ['--working-directory=' + cwd]
150
158
  : ['-e', `bash -c "cd '${cwd.replace(/'/g, "'\\''")}' && exec bash"`];
151
- return spawnDetached(t, args);
159
+ return spawnDetached(t, args, t, spawnFn);
152
160
  }
153
- } else if (process.platform === 'darwin') {
154
- return spawnDetached('open', ['-a', 'Terminal', cwd], 'Terminal.app');
161
+ } else if (platform === 'darwin') {
162
+ return spawnDetached('open', ['-a', 'Terminal', cwd], 'Terminal.app', spawnFn);
163
+ } else if (platform === 'win32') {
164
+ if (find('wt.exe')) return spawnDetached('wt.exe', ['-d', cwd], 'wt', spawnFn);
165
+ const setLoc = `Set-Location -LiteralPath '${cwd.replace(/'/g, "''")}'`;
166
+ return spawnDetached('cmd.exe',
167
+ ['/d', '/c', 'start', '', 'powershell.exe', '-NoExit', '-Command', setLoc],
168
+ 'powershell', spawnFn, { windowsHide: true });
155
169
  }
156
170
  return { ok: false, error: 'no terminal found' };
157
171
  }
158
172
 
159
- module.exports = { spawnDetached, findCommand, openInEditor, openFileInEditor, openInFinder, openInTerminal };
173
+ /** Escape a string for use inside an AppleScript double-quoted literal. */
174
+ const appleScriptQuote = (s) => `"${String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
175
+
176
+ /**
177
+ * Run a command in a visible OS terminal so the user sees and controls it
178
+ * (password prompts, installer dialogs). `command` MUST come from main-side
179
+ * code, never from the renderer. argv arrays only — never shell:true.
180
+ *
181
+ * @param {{ command: string, shell?: 'sh' | 'powershell' }} opts
182
+ * @param {{ platform?: string, spawn?: Function, find?: (n: string) => string | null }} [deps]
183
+ * @returns {Promise<{ ok: true, opener: string } | { ok: false, error: string }>}
184
+ */
185
+ async function runInTerminal({ command, shell: shellKind }, deps = {}) {
186
+ const { platform = process.platform, spawn: spawnFn = spawn, find = findCommand } = deps;
187
+ if (typeof command !== 'string' || !command.trim()) return { ok: false, error: 'empty command' };
188
+ if (platform === 'darwin') {
189
+ const script = `tell application "Terminal"\nactivate\ndo script ${appleScriptQuote(command)}\nend tell`;
190
+ return spawnDetached('osascript', ['-e', script], 'Terminal.app', spawnFn);
191
+ }
192
+ if (platform === 'win32') {
193
+ return spawnDetached('cmd.exe',
194
+ ['/d', '/c', 'start', '', 'powershell.exe', '-NoExit', '-Command', command],
195
+ 'powershell', spawnFn);
196
+ }
197
+ if (platform === 'linux') {
198
+ const inner = `${command}; exec bash`;
199
+ for (const t of ['gnome-terminal', 'konsole', 'xfce4-terminal', 'xterm']) {
200
+ if (!find(t)) continue;
201
+ const args = t === 'gnome-terminal'
202
+ ? ['--', 'bash', '-lc', inner]
203
+ : ['-e', 'bash', '-lc', inner];
204
+ return spawnDetached(t, args, t, spawnFn);
205
+ }
206
+ }
207
+ return { ok: false, error: 'no terminal found' };
208
+ }
209
+
210
+ module.exports = { spawnDetached, findCommand, openInEditor, openFileInEditor, openInFinder, openInTerminal, runInTerminal };
@@ -0,0 +1,147 @@
1
+ /**
2
+ * otlpTraceExporter — zero-dependency batching exporter that POSTs 0-duration
3
+ * spans to an OTLP/HTTP endpoint as JSON (ExportTraceServiceRequest).
4
+ *
5
+ * Replaces the @opentelemetry/* SDK for the one thing otel.cjs does: emit
6
+ * "event" spans. recordSpan() only queues (no I/O, never throws); a timer
7
+ * unref()'d so it can't hold the process open drains the queue in batches.
8
+ * A failed POST drops that batch and is surfaced via stats() — the app must
9
+ * keep working without telemetry. Field names mirror otlp-transformer's JSON.
10
+ */
11
+
12
+ const crypto = require('node:crypto');
13
+
14
+ function toAnyValue(v) {
15
+ switch (typeof v) {
16
+ case 'string': return { stringValue: v };
17
+ case 'boolean': return { boolValue: v };
18
+ case 'bigint': return { intValue: v.toString() };
19
+ case 'number':
20
+ if (!Number.isFinite(v)) return { stringValue: String(v) };
21
+ return Number.isInteger(v) ? { intValue: String(v) } : { doubleValue: v };
22
+ default:
23
+ try { return { stringValue: JSON.stringify(v) ?? String(v) }; } catch { return { stringValue: String(v) }; }
24
+ }
25
+ }
26
+
27
+ function toAttributes(obj) {
28
+ const out = [];
29
+ if (!obj || typeof obj !== 'object') return out;
30
+ for (const [key, v] of Object.entries(obj)) {
31
+ if (v === undefined || v === null) continue;
32
+ out.push({ key, value: toAnyValue(v) });
33
+ }
34
+ return out;
35
+ }
36
+
37
+ function createOtlpTraceExporter({
38
+ url,
39
+ headers,
40
+ serviceName,
41
+ scopeName,
42
+ maxQueue = 2048,
43
+ maxBatch = 512,
44
+ flushIntervalMs = 5000,
45
+ timeoutMs = 10000,
46
+ } = {}) {
47
+ let queue = [];
48
+ let timer = null;
49
+ let closed = false;
50
+ let inFlight = Promise.resolve();
51
+ const counts = { exported: 0, dropped: 0, failed: 0, lastError: null };
52
+
53
+ function ensureTimer() {
54
+ if (timer || closed) return;
55
+ timer = setInterval(() => { flush(); }, flushIntervalMs);
56
+ if (typeof timer.unref === 'function') timer.unref();
57
+ }
58
+
59
+ function recordSpan({ name, timeMs, attributes } = {}) {
60
+ try {
61
+ if (closed) return;
62
+ const ms = typeof timeMs === 'number' && Number.isFinite(timeMs) ? timeMs : Date.now();
63
+ const nanos = (BigInt(Math.round(ms)) * 1000000n).toString();
64
+ queue.push({
65
+ traceId: crypto.randomBytes(16).toString('hex'),
66
+ spanId: crypto.randomBytes(8).toString('hex'),
67
+ name: String(name),
68
+ kind: 1,
69
+ startTimeUnixNano: nanos,
70
+ endTimeUnixNano: nanos,
71
+ attributes: toAttributes(attributes),
72
+ });
73
+ if (queue.length > maxQueue) {
74
+ const over = queue.length - maxQueue;
75
+ queue.splice(0, over);
76
+ counts.dropped += over;
77
+ }
78
+ ensureTimer();
79
+ } catch (err) {
80
+ counts.lastError = err?.message || String(err);
81
+ }
82
+ }
83
+
84
+ async function post(spans) {
85
+ const body = JSON.stringify({
86
+ resourceSpans: [{
87
+ resource: { attributes: [{ key: 'service.name', value: { stringValue: String(serviceName) } }] },
88
+ scopeSpans: [{ scope: { name: String(scopeName) }, spans }],
89
+ }],
90
+ });
91
+ const res = await fetch(url, {
92
+ method: 'POST',
93
+ headers: { ...(headers || {}), 'content-type': 'application/json' },
94
+ body,
95
+ signal: AbortSignal.timeout(timeoutMs),
96
+ });
97
+ try { await res.arrayBuffer(); } catch { /* body drain is best-effort */ }
98
+ if (!res.ok) throw new Error(`OTLP endpoint responded ${res.status}`);
99
+ }
100
+
101
+ async function drain() {
102
+ while (queue.length > 0) {
103
+ const batch = queue.splice(0, maxBatch);
104
+ try {
105
+ await post(batch);
106
+ counts.exported += batch.length;
107
+ counts.lastError = null;
108
+ } catch (err) {
109
+ counts.failed += 1;
110
+ counts.lastError = err?.message || String(err);
111
+ return; // drop this batch; the next tick retries whatever is left
112
+ }
113
+ }
114
+ }
115
+
116
+ function flush() {
117
+ const next = inFlight.then(drain, drain);
118
+ inFlight = next.catch(() => {});
119
+ return next.catch(() => {});
120
+ }
121
+
122
+ async function shutdown() {
123
+ closed = true;
124
+ if (timer) { clearInterval(timer); timer = null; }
125
+ let guard;
126
+ await Promise.race([
127
+ flush(),
128
+ new Promise((resolve) => { guard = setTimeout(resolve, timeoutMs); }),
129
+ ]);
130
+ clearTimeout(guard);
131
+ queue = [];
132
+ }
133
+
134
+ function stats() {
135
+ return {
136
+ queued: queue.length,
137
+ exported: counts.exported,
138
+ dropped: counts.dropped,
139
+ failed: counts.failed,
140
+ lastError: counts.lastError,
141
+ };
142
+ }
143
+
144
+ return { recordSpan, flush, shutdown, stats };
145
+ }
146
+
147
+ module.exports = { createOtlpTraceExporter };
@@ -0,0 +1,145 @@
1
+ /**
2
+ * prereqs.cjs — first-run prerequisite checklist (git, Claude Code CLI,
3
+ * Claude sign-in, and Git Bash on Windows). Each missing item carries the
4
+ * OFFICIAL one-line install command so a UI can offer one-click installs.
5
+ * Detection only — never runs an installer. Probes are injectable
6
+ * (opts.execFile / hasCredentials / exists) so tests spawn nothing.
7
+ */
8
+ 'use strict';
9
+
10
+ const fs = require('node:fs');
11
+ const path = require('node:path');
12
+ const { promisify } = require('node:util');
13
+ const childProcess = require('node:child_process');
14
+ const { pathWithUserBins } = require('./cleanEnv.cjs');
15
+ const { resolveClaudeBin } = require('./claudeBin.cjs');
16
+ const { readCredentials } = require('./credentials.cjs');
17
+
18
+ const PROBE_TIMEOUT_MS = 5000;
19
+ const GIT_URL = 'https://git-scm.com/downloads';
20
+ const CLAUDE_SETUP_URL = 'https://docs.claude.com/en/docs/claude-code/setup';
21
+ const WINGET_GIT = 'winget install --id Git.Git -e --source winget';
22
+
23
+ const execFileP = promisify(childProcess.execFile);
24
+
25
+ /** Default probe: no shell, 5 s timeout, PATH widened with the user bin dirs. */
26
+ function defaultExecFile(cmd, args) {
27
+ return execFileP(cmd, args, {
28
+ env: { ...process.env, PATH: pathWithUserBins() },
29
+ timeout: PROBE_TIMEOUT_MS,
30
+ windowsHide: true,
31
+ });
32
+ }
33
+
34
+ async function defaultHasCredentials() {
35
+ const r = await readCredentials();
36
+ return r.kind === 'ok';
37
+ }
38
+
39
+ function errText(e) {
40
+ const msg = (e && (e.message || e.code)) || String(e);
41
+ return String(msg).split('\n')[0].slice(0, 200);
42
+ }
43
+
44
+ /** Run a probe; resolves {ok, out} and never rejects (timeout/ENOENT → ok:false). */
45
+ async function probe(execFile, cmd, args) {
46
+ try {
47
+ const r = await execFile(cmd, args);
48
+ const out = typeof r === 'string' ? r : (r && r.stdout) || '';
49
+ return { ok: true, out: String(out).trim(), error: null };
50
+ } catch (e) {
51
+ return { ok: false, out: '', error: errText(e) };
52
+ }
53
+ }
54
+
55
+ function item(id, label, ok, version, detail, fix) {
56
+ return { id, label, ok, version: ok ? version : null, detail: ok ? null : detail, fix: ok ? null : fix };
57
+ }
58
+
59
+ function gitFix(platform) {
60
+ if (platform === 'darwin') return { command: 'xcode-select --install', shell: 'sh', url: GIT_URL };
61
+ if (platform === 'win32') return { command: WINGET_GIT, shell: 'powershell', url: GIT_URL };
62
+ // linux: distro-specific — point at the package-manager docs instead of a command.
63
+ return { command: '', shell: 'sh', url: GIT_URL };
64
+ }
65
+
66
+ async function checkGit(platform, execFile) {
67
+ const ver = await probe(execFile, 'git', ['--version']);
68
+ let ok = ver.ok;
69
+ let detail = ver.error;
70
+ if (ok && platform === 'darwin') {
71
+ // /usr/bin/git on macOS is a stub that prompts for the Command Line Tools.
72
+ const xc = await probe(execFile, 'xcode-select', ['-p']);
73
+ if (!xc.ok) {
74
+ const which = await probe(execFile, 'which', ['git']);
75
+ if (!which.ok || which.out === '/usr/bin/git' || !which.out) {
76
+ ok = false;
77
+ detail = 'Xcode Command Line Tools are not installed';
78
+ }
79
+ }
80
+ }
81
+ const version = ver.out.replace(/^git version\s+/, '').split(/\s/)[0] || null;
82
+ const fix = gitFix(platform);
83
+ const missing = platform === 'linux'
84
+ ? `${detail || 'git not found'} — install it with your distro's package manager`
85
+ : (detail || 'git not found');
86
+ return item('git', 'Git', ok, version, missing, fix);
87
+ }
88
+
89
+ async function checkClaudeCli(platform, execFile, claudeBin) {
90
+ const bin = claudeBin || resolveClaudeBin();
91
+ const r = await probe(execFile, bin, ['--version']);
92
+ const win = platform === 'win32';
93
+ return item('claude-cli', 'Claude Code CLI', r.ok, r.out.split('\n')[0] || null,
94
+ r.error || 'claude not found', {
95
+ command: win ? 'irm https://claude.ai/install.ps1 | iex' : 'curl -fsSL https://claude.ai/install.sh | bash',
96
+ shell: win ? 'powershell' : 'sh',
97
+ url: CLAUDE_SETUP_URL,
98
+ });
99
+ }
100
+
101
+ async function checkClaudeAuth(platform, hasCredentials) {
102
+ let ok = false;
103
+ try { ok = !!(await hasCredentials()); } catch { ok = false; }
104
+ return item('claude-auth', 'Claude account', ok, null,
105
+ 'Not signed in — run `claude` and sign in to your Claude account', {
106
+ command: 'claude',
107
+ shell: platform === 'win32' ? 'powershell' : 'sh',
108
+ url: CLAUDE_SETUP_URL,
109
+ });
110
+ }
111
+
112
+ function checkGitBash(exists) {
113
+ const candidates = [];
114
+ if (process.env.CLAUDE_CODE_GIT_BASH_PATH) candidates.push(process.env.CLAUDE_CODE_GIT_BASH_PATH);
115
+ candidates.push(path.win32.join(process.env.ProgramFiles || 'C:\\Program Files', 'Git', 'bin', 'bash.exe'));
116
+ let found = null;
117
+ for (const c of candidates) {
118
+ try { if (exists(c)) { found = c; break; } } catch { /* treat as absent */ }
119
+ }
120
+ return item('git-bash', 'Git Bash', !!found, found,
121
+ 'Claude Code on Windows needs Git for Windows (bash.exe)',
122
+ { command: WINGET_GIT, shell: 'powershell', url: GIT_URL });
123
+ }
124
+
125
+ /**
126
+ * @param {{platform?: string, execFile?: Function, hasCredentials?: Function,
127
+ * claudeBin?: string, exists?: (p: string) => boolean}} [opts]
128
+ * @returns {Promise<Array<{id:string,label:string,ok:boolean,version:string|null,detail:string|null,
129
+ * fix:{command:string,shell:'sh'|'powershell',url?:string}|null}>>}
130
+ */
131
+ async function checkPrereqs(opts = {}) {
132
+ const {
133
+ platform = process.platform,
134
+ execFile = defaultExecFile,
135
+ hasCredentials = defaultHasCredentials,
136
+ claudeBin,
137
+ exists = fs.existsSync,
138
+ } = opts;
139
+ const jobs = [checkGit(platform, execFile)];
140
+ if (platform === 'win32') jobs.push(Promise.resolve(checkGitBash(exists)));
141
+ jobs.push(checkClaudeCli(platform, execFile, claudeBin), checkClaudeAuth(platform, hasCredentials));
142
+ return Promise.all(jobs);
143
+ }
144
+
145
+ module.exports = { checkPrereqs };
@@ -97,12 +97,47 @@ function identityFromPs(pid) {
97
97
  return { pid, startTicks, cmdline, complete: startTicks !== null && cmdline !== null };
98
98
  }
99
99
 
100
- /** identity(pid) → {pid, startTicks, cmdline, complete}. Never throws. */
101
- function identity(pid) {
100
+ /** PowerShell serialises DateTime as "\/Date(ms)\/" (5.1) or ISO-8601 (7+). */
101
+ function parseWinStartTime(v) {
102
+ if (typeof v === 'string') {
103
+ const m = /^\/Date\((-?\d+)\)\/$/.exec(v.trim());
104
+ if (m) return Number(m[1]);
105
+ const ms = Date.parse(v);
106
+ return Number.isFinite(ms) ? ms : null;
107
+ }
108
+ if (v && typeof v === 'object' && typeof v.value === 'string') return parseWinStartTime(v.value);
109
+ return null;
110
+ }
111
+
112
+ function identityFromPowerShell(pid, execFile) {
113
+ const unknown = { pid, startTicks: null, cmdline: null, complete: false };
114
+ let out;
115
+ try {
116
+ out = execFile(
117
+ 'powershell.exe',
118
+ ['-NoProfile', '-Command', `Get-Process -Id ${pid} | Select-Object StartTime,ProcessName | ConvertTo-Json`],
119
+ { encoding: 'utf8', windowsHide: true, timeout: 5000 },
120
+ );
121
+ const parsed = JSON.parse(String(out));
122
+ const startTicks = parseWinStartTime(parsed && parsed.StartTime);
123
+ const name = parsed && typeof parsed.ProcessName === 'string' ? parsed.ProcessName.trim() : '';
124
+ const cmdline = name || null;
125
+ return { pid, startTicks, cmdline, complete: startTicks !== null && cmdline !== null };
126
+ } catch {
127
+ return unknown; // missing process, timeout, or unparseable output
128
+ }
129
+ }
130
+
131
+ /**
132
+ * identity(pid, opts?) → {pid, startTicks, cmdline, complete}. Never throws.
133
+ * opts.platform / opts.execFile (sync, execFileSync-shaped) are injectable for tests.
134
+ */
135
+ function identity(pid, { platform = process.platform, execFile = execFileSync } = {}) {
102
136
  if (!Number.isInteger(pid) || pid <= 0) {
103
137
  return { pid, startTicks: null, cmdline: null, complete: false };
104
138
  }
105
139
  try {
140
+ if (platform === 'win32') return identityFromPowerShell(pid, execFile);
106
141
  return hasProc() ? identityFromProc(pid) : identityFromPs(pid);
107
142
  } catch {
108
143
  return { pid, startTicks: null, cmdline: null, complete: false };
@@ -50,6 +50,7 @@ const VERDICT_LABELS = {
50
50
  no_verdict_sentinel: 'no commit or verdict sentinel',
51
51
  abandoned_background_task: 'abandoned auto-backgrounded task',
52
52
  pass_no_commit: 'PASS sentinel but no commit landed',
53
+ sentinel_fail: 'executor reported SCHEDULER_VERDICT: FAIL',
53
54
  pass_no_commit_already_shipped: 'PASS with no commit — deliverables already shipped',
54
55
  pass_no_commit_prior_run_verified: 'PASS with no commit — prior run of this slug already landed the work',
55
56
  silent_no_op: 'no commit, clean tree — no evidence of work',
@@ -30,9 +30,11 @@ const { pidAlive } = require('./pidAlive.cjs');
30
30
  * Conservative by design: a false negative (live process treated as dead) is
31
31
  * far worse than a late reap.
32
32
  */
33
- function claudePidAlive(pid) {
33
+ function claudePidAlive(pid, { platform = process.platform } = {}) {
34
34
  if (!pid || typeof pid !== 'number' || pid <= 1) return false;
35
35
  if (!pidAlive(pid)) return false;
36
+ // Windows has no /proc — cannot verify identity, so never call it dead.
37
+ if (platform === 'win32') return true;
36
38
  try {
37
39
  const cmd = fs.readFileSync(`/proc/${pid}/cmdline`, 'utf8').replace(/\0/g, ' ');
38
40
  return /\bclaude\b/.test(cmd);
@@ -73,7 +75,8 @@ function claudePidAlive(pid) {
73
75
  * — i.e. exactly today's behaviour (fail toward terminalizing), never a hang
74
76
  * or a thrown error propagating to the caller.
75
77
  */
76
- function findLiveProcessForJob(job, { worktreeDir, runCwd } = {}) {
78
+ function findLiveProcessForJob(job, { worktreeDir, runCwd, platform = process.platform } = {}) {
79
+ if (platform === 'win32') return null; // no /proc — skip every read
77
80
  const slug = job?.slug;
78
81
  let entries;
79
82
  try {
@@ -207,6 +207,16 @@ const SHIPPED_PERSONA_SEEDS = deepFreeze({
207
207
  tools: "Read, Grep, Glob, Bash",
208
208
  },
209
209
  },
210
+ {
211
+ commit: "<fill in>",
212
+ bodySha256: "ca159cd47ac4b618af842c6ccd59107fac4a34b0efcfa5dfe7d06875736156ed",
213
+ fm: {
214
+ description: "Validates a finished PLAN (the PRDs its validate PRD lists) once, after its last PRD lands — re-runs each PRD's gate, checks every acceptance criterion against the real tree, reviews the plan's combined diff, and reports one VERIFIED/REFUTED verdict per PRD via sentinel lines. Runs headless as a scheduled PRD (agentType: validator); never edits product code and never queues work.",
215
+ model: "sonnet",
216
+ title: "Engineering — Plan Validator",
217
+ tools: "Read, Grep, Glob, Bash",
218
+ },
219
+ },
210
220
  ],
211
221
  "demo-video-builder": [
212
222
  {
@@ -222,16 +222,28 @@ function main() {
222
222
  // spawned, without detaching further, dies too); fall back to the direct
223
223
  // pid only when the group signal itself fails (e.g. --foreground, where
224
224
  // the child shares OUR group rather than leading its own).
225
+ // Inlined (not require('./killTree.cjs')): this script is copied to ~ and
226
+ // runs standalone, so it cannot resolve app-relative modules. win32 has no
227
+ // process groups, so taskkill /T /F stands in for the group signal.
225
228
  function killGroup(sig) {
229
+ if (process.platform === 'win32') {
230
+ try {
231
+ require('node:child_process').spawn('taskkill', ['/PID', String(child.pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true });
232
+ } catch {
233
+ try { process.kill(child.pid, sig); } catch { /* already gone */ }
234
+ }
235
+ return;
236
+ }
226
237
  try {
227
- process.kill(-child.pid, sig);
238
+ const groupPid = -child.pid; // negative pid = whole POSIX process group
239
+ process.kill(groupPid, sig);
228
240
  } catch {
229
241
  try { process.kill(child.pid, sig); } catch { /* already gone */ }
230
242
  }
231
243
  }
232
244
 
233
245
  try {
234
- child = spawn(command, commandArgs, { stdio: 'inherit', detached: !opts.foreground });
246
+ child = spawn(command, commandArgs, { stdio: 'inherit', detached: !opts.foreground, windowsHide: true });
235
247
  } catch (err) {
236
248
  finish(err && err.code === 'EACCES' ? 126 : 127);
237
249
  return;
@@ -3,9 +3,10 @@
3
3
  /**
4
4
  * updateCheck.cjs — bounded, offline-safe, opt-out "is a newer build published?" probe.
5
5
  *
6
- * getUpdateStatus() → { current, latest, behind }. Never throws/rejects; any failure
7
- * (offline, DNS, non-200, bad JSON, timeout) → { current, latest: null, behind: false }.
8
- * The request is a bare GET (no headers/cookies/query/body) so it carries nothing that
6
+ * getUpdateStatus() → { current, latest, behind, channel, downloadUrl? } (installer builds compare
7
+ * against the latest GitHub Release; npx/dev builds against the npm registry). Never throws/rejects; any failure
8
+ * (offline, DNS, non-200, bad JSON, timeout) → { current, latest: null, behind: false, channel }.
9
+ * The request is a bare GET (only a static User-Agent; no cookies/query/body) so it carries nothing that
9
10
  * identifies the user or machine. Disabled by SM_UPDATE_CHECK=0 or E2E mode (SM_E2E=1).
10
11
  */
11
12
 
@@ -16,11 +17,14 @@ const path = require('node:path');
16
17
  const CACHE_TTL_MS = 6 * 60 * 60 * 1000;
17
18
  const REQUEST_TIMEOUT_MS = 3000;
18
19
  const CACHE_FILE = 'update-check.json';
20
+ const GITHUB_CACHE_FILE = 'update-check-github.json';
21
+ const GITHUB_RELEASE_URL = 'https://api.github.com/repos/StanislavBG/claude-code-session-manager/releases/latest';
19
22
 
20
23
  /** Plain GET → parsed JSON. Rejects on non-200, bad JSON, or timeout. */
21
24
  function httpGetJson(url, timeoutMs) {
22
25
  return new Promise((resolve, reject) => {
23
- const req = https.get(url, { timeout: timeoutMs }, (res) => {
26
+ // GitHub's API rejects requests without a User-Agent; it names the app only, not the user.
27
+ const req = https.get(url, { timeout: timeoutMs, headers: { 'User-Agent': 'claude-code-session-manager' } }, (res) => {
24
28
  if (res.statusCode !== 200) {
25
29
  res.resume();
26
30
  reject(new Error(`HTTP ${res.statusCode}`));
@@ -58,15 +62,20 @@ function isBehind(current, latest) {
58
62
  return false;
59
63
  }
60
64
 
61
- function readCache(file, now) {
65
+ function readCacheEntry(file, now) {
62
66
  try {
63
67
  const c = JSON.parse(fs.readFileSync(file, 'utf8'));
64
68
  if (c && typeof c.latest === 'string' && typeof c.checkedAt === 'number'
65
- && now - c.checkedAt >= 0 && now - c.checkedAt < CACHE_TTL_MS) return c.latest;
69
+ && now - c.checkedAt >= 0 && now - c.checkedAt < CACHE_TTL_MS) return c;
66
70
  } catch { /* missing/corrupt cache = miss */ }
67
71
  return null;
68
72
  }
69
73
 
74
+ function readCache(file, now) {
75
+ const c = readCacheEntry(file, now);
76
+ return c ? c.latest : null;
77
+ }
78
+
70
79
  function defaultPackageName() {
71
80
  return require(path.join(__dirname, '..', '..', '..', 'package.json')).name;
72
81
  }
@@ -74,13 +83,34 @@ function defaultPackageName() {
74
83
  async function getUpdateStatus({ deps } = {}) {
75
84
  const d = deps || {};
76
85
  let current = '';
86
+ let channel = 'npm';
77
87
  try {
78
88
  const app = d.app || require('electron').app;
79
89
  current = app.getVersion();
90
+ const packaged = (d.isPackaged || require('./appRuntime.cjs').isPackagedApp)();
91
+ channel = packaged ? 'installer' : 'npm';
80
92
  const env = d.env || process.env;
81
- if (env.SM_UPDATE_CHECK === '0' || env.SM_E2E === '1') return { current, latest: null, behind: false };
93
+ if (env.SM_UPDATE_CHECK === '0' || env.SM_E2E === '1') return { current, latest: null, behind: false, channel };
82
94
 
83
95
  const now = (d.now || Date.now)();
96
+ const write = d.writeJson || require('../config.cjs').writeJson;
97
+ if (packaged) {
98
+ const file = path.join(app.getPath('userData'), GITHUB_CACHE_FILE);
99
+ let entry = readCacheEntry(file, now);
100
+ if (entry == null) {
101
+ const rel = await (d.fetchJson || httpGetJson)(GITHUB_RELEASE_URL, REQUEST_TIMEOUT_MS);
102
+ const tag = rel && typeof rel.tag_name === 'string' ? rel.tag_name.replace(/^v/, '') : '';
103
+ if (!tag) return { current, latest: null, behind: false, channel };
104
+ const downloadUrl = typeof rel.html_url === 'string' ? rel.html_url : null;
105
+ entry = { checkedAt: now, latest: tag, downloadUrl };
106
+ try { await write(file, entry); } catch { /* cache is best-effort */ }
107
+ }
108
+ return {
109
+ current, latest: entry.latest, behind: isBehind(current, entry.latest), channel,
110
+ downloadUrl: typeof entry.downloadUrl === 'string' ? entry.downloadUrl : null,
111
+ };
112
+ }
113
+
84
114
  const file = path.join(app.getPath('userData'), CACHE_FILE);
85
115
  let latest = readCache(file, now);
86
116
  if (latest == null) {
@@ -88,14 +118,14 @@ async function getUpdateStatus({ deps } = {}) {
88
118
  const url = `https://registry.npmjs.org/-/package/${name}/dist-tags`;
89
119
  const tags = await (d.fetchJson || httpGetJson)(url, REQUEST_TIMEOUT_MS);
90
120
  latest = tags && typeof tags.latest === 'string' ? tags.latest : null;
91
- if (latest == null) return { current, latest: null, behind: false };
121
+ if (latest == null) return { current, latest: null, behind: false, channel };
92
122
  try {
93
- await (d.writeJson || require('../config.cjs').writeJson)(file, { checkedAt: now, latest });
123
+ await write(file, { checkedAt: now, latest });
94
124
  } catch { /* cache is best-effort */ }
95
125
  }
96
- return { current, latest, behind: isBehind(current, latest) };
126
+ return { current, latest, behind: isBehind(current, latest), channel };
97
127
  } catch {
98
- return { current, latest: null, behind: false };
128
+ return { current, latest: null, behind: false, channel };
99
129
  }
100
130
  }
101
131