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.
- package/README.md +15 -3
- package/bin/cli.cjs +0 -5
- package/dist/assets/{AgentLibrary-QPw9gC_o.js → AgentLibrary-BDtr4oI-.js} +1 -1
- package/dist/assets/{Button-8RAbgh1d.js → Button-CuXBCQDp.js} +1 -1
- package/dist/assets/{DataModel-BEkuOZuS.js → DataModel-DmvRipT5.js} +1 -1
- package/dist/assets/{History-BLrNSazM.js → History-B-v9pwwr.js} +1 -1
- package/dist/assets/{Hooks-C8P6wQq1.js → Hooks-S_0oHHlr.js} +1 -1
- package/dist/assets/{Library-CWFvBZw-.js → Library-BcbGJxQ3.js} +1 -1
- package/dist/assets/{ListDetail-B1jVMgzN.js → ListDetail-DtqHqJHt.js} +1 -1
- package/dist/assets/{MarkdownEditor-JrnUbWjW.js → MarkdownEditor-B56sPp_l.js} +1 -1
- package/dist/assets/{McpServers-BGqTxDBm.js → McpServers-D5VnmxAn.js} +1 -1
- package/dist/assets/{Memory-h8bhRNyI.js → Memory-0yGZLyRP.js} +1 -1
- package/dist/assets/{Panel-srf0FT0m.js → Panel-NoAADJwx.js} +1 -1
- package/dist/assets/{Permissions-BA4oWydI.js → Permissions-D2Dh-ee8.js} +1 -1
- package/dist/assets/{Plugins-DTuTQqUL.js → Plugins-kGoUgS9b.js} +2 -2
- package/dist/assets/{ProvenanceBadge-BW18EquH.js → ProvenanceBadge-CHwDFTX7.js} +1 -1
- package/dist/assets/{SaveBar-Cc6UukFa.js → SaveBar-BtKNMIYK.js} +1 -1
- package/dist/assets/{Scheduler-BctPlNL1.js → Scheduler-B0_kMMDs.js} +1 -1
- package/dist/assets/{ScopeSwitcher-DV1OnSuc.js → ScopeSwitcher-BusohWZ5.js} +1 -1
- package/dist/assets/{Settings-Bpyt1RGa.js → Settings-DuRJntaE.js} +1 -1
- package/dist/assets/{SkillReferenceGraph-B28AYo12.js → SkillReferenceGraph-BHOdIpsy.js} +1 -1
- package/dist/assets/{Skills-zXLI5OB_.js → Skills-DSTLZimI.js} +1 -1
- package/dist/assets/{SystemPrompt-DKoN0UAV.js → SystemPrompt-BG7wFWdb.js} +1 -1
- package/dist/assets/{TagLibrary-BkDOa8IM.js → TagLibrary-Bx27aE3_.js} +1 -1
- package/dist/assets/{TiptapBody-CIY1f6mI.js → TiptapBody-j0O8J7YI.js} +1 -1
- package/dist/assets/{Toggle-C139wuAK.js → Toggle-DzVZ6rC7.js} +1 -1
- package/dist/assets/{ViewTabs-CUEfsMOe.js → ViewTabs-BizjuynT.js} +1 -1
- package/dist/assets/{index-D89EUizB.css → index-BLVzrc4k.css} +1 -1
- package/dist/assets/{index-DrvxIbmR.js → index-CLIaYljt.js} +416 -416
- package/dist/assets/{settingsSchema-CAFckLz6.js → settingsSchema-7MRAxaJk.js} +1 -1
- package/dist/assets/useAgentsDirWatch-BUwfU2Kq.js +1 -0
- package/dist/assets/{useConfigDirWatch-QKxfZx9X.js → useConfigDirWatch-ot0VYwQC.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +10 -12
- package/plugins/session-manager-dev/skills/builder/3-publish/SKILL.md +12 -0
- package/plugins/session-manager-dev/skills/develop/SKILL.md +4 -2
- package/scripts/scheduler-mcp-server.cjs +7 -17
- package/src/main/build-info.json +4 -4
- package/src/main/chatRunner.cjs +4 -4
- package/src/main/historyAggregator.cjs +7 -1
- package/src/main/index.cjs +34 -1
- package/src/main/lib/agentPersonaSchema.cjs +8 -1
- package/src/main/lib/appRuntime.cjs +57 -0
- package/src/main/lib/childWithLog.cjs +10 -10
- package/src/main/lib/claudeBin.cjs +40 -16
- package/src/main/lib/cleanEnv.cjs +29 -6
- package/src/main/lib/defaultShell.cjs +36 -0
- package/src/main/lib/definitionOfDone.cjs +6 -2
- package/src/main/lib/delegationReadiness.cjs +13 -5
- package/src/main/lib/gitWorktree.cjs +6 -3
- package/src/main/lib/insideHome.cjs +15 -4
- package/src/main/lib/killTree.cjs +80 -0
- package/src/main/lib/loadGate.cjs +16 -0
- package/src/main/lib/loginShellPath.cjs +82 -0
- package/src/main/lib/machineProfile.cjs +4 -2
- package/src/main/lib/mcpStdioServer.cjs +127 -0
- package/src/main/lib/openExternalApp.cjs +62 -11
- package/src/main/lib/otlpTraceExporter.cjs +147 -0
- package/src/main/lib/prereqs.cjs +145 -0
- package/src/main/lib/procIdentity.cjs +37 -2
- package/src/main/lib/rcaReport.cjs +1 -0
- package/src/main/lib/reaperHelpers.cjs +5 -2
- package/src/main/lib/shippedPersonaSeeds.cjs +10 -0
- package/src/main/lib/timeoutShimScript.cjs +14 -2
- package/src/main/lib/updateCheck.cjs +41 -11
- package/src/main/lib/watchdogHelpers.cjs +23 -3
- package/src/main/lib/winSpawn.cjs +67 -0
- package/src/main/otel.cjs +18 -62
- package/src/main/pty.cjs +32 -11
- package/src/main/runVerify.cjs +42 -2
- package/src/main/scheduler.cjs +105 -20
- package/src/main/seedSchedulerMcp.cjs +11 -2
- package/src/preload/api.d.ts +14 -0
- package/src/preload/index.cjs +2 -0
- package/src/seed/agents/validator.md +2 -2
- package/dist/assets/useAgentsDirWatch-XhQEVlaK.js +0 -1
- 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 =
|
|
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
|
-
|
|
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 (!
|
|
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 (
|
|
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
|
-
|
|
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
|
-
/**
|
|
101
|
-
function
|
|
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
|
-
|
|
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 }
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|