claude-code-session-manager 0.104.2 → 0.106.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 (87) hide show
  1. package/README.md +15 -3
  2. package/bin/cli.cjs +0 -5
  3. package/dist/assets/{AgentLibrary-QPw9gC_o.js → AgentLibrary-CKOZzc2W.js} +1 -1
  4. package/dist/assets/{Button-8RAbgh1d.js → Button-yuqFpQsK.js} +1 -1
  5. package/dist/assets/{DataModel-BEkuOZuS.js → DataModel-Bwdc_gaJ.js} +1 -1
  6. package/dist/assets/{History-BLrNSazM.js → History-D1Z0lydo.js} +1 -1
  7. package/dist/assets/{Hooks-C8P6wQq1.js → Hooks-CyD6l4Is.js} +1 -1
  8. package/dist/assets/{Library-CWFvBZw-.js → Library-fnod5cPe.js} +1 -1
  9. package/dist/assets/{ListDetail-B1jVMgzN.js → ListDetail-BKO68SJ4.js} +1 -1
  10. package/dist/assets/{MarkdownEditor-JrnUbWjW.js → MarkdownEditor-DrYMnMBy.js} +1 -1
  11. package/dist/assets/{McpServers-BGqTxDBm.js → McpServers-CgfItWhF.js} +1 -1
  12. package/dist/assets/{Memory-h8bhRNyI.js → Memory-4M3gO3_r.js} +1 -1
  13. package/dist/assets/{Panel-srf0FT0m.js → Panel-CKbXo-LT.js} +1 -1
  14. package/dist/assets/{Permissions-BA4oWydI.js → Permissions-Bc9EpjXL.js} +1 -1
  15. package/dist/assets/{Plugins-DTuTQqUL.js → Plugins-B_-PlEIx.js} +2 -2
  16. package/dist/assets/{ProvenanceBadge-BW18EquH.js → ProvenanceBadge-Cxev7XSA.js} +1 -1
  17. package/dist/assets/{SaveBar-Cc6UukFa.js → SaveBar-DiqscG6q.js} +1 -1
  18. package/dist/assets/{Scheduler-BctPlNL1.js → Scheduler-CCcJzuu8.js} +1 -1
  19. package/dist/assets/{ScopeSwitcher-DV1OnSuc.js → ScopeSwitcher-BsM4-jHQ.js} +1 -1
  20. package/dist/assets/{Settings-Bpyt1RGa.js → Settings-C3-VL-T8.js} +1 -1
  21. package/dist/assets/{SkillReferenceGraph-B28AYo12.js → SkillReferenceGraph-B9YlOuPo.js} +1 -1
  22. package/dist/assets/{Skills-zXLI5OB_.js → Skills-Dtf-z_zN.js} +1 -1
  23. package/dist/assets/{SystemPrompt-DKoN0UAV.js → SystemPrompt-HJDn0E8C.js} +1 -1
  24. package/dist/assets/{TagLibrary-BkDOa8IM.js → TagLibrary-DLGOfauU.js} +1 -1
  25. package/dist/assets/{TiptapBody-CIY1f6mI.js → TiptapBody-Cdmv1CuM.js} +1 -1
  26. package/dist/assets/{Toggle-C139wuAK.js → Toggle-CQFhOo1F.js} +1 -1
  27. package/dist/assets/{ViewTabs-CUEfsMOe.js → ViewTabs-CxjDCHjH.js} +1 -1
  28. package/dist/assets/index-Blo38vfX.js +3077 -0
  29. package/dist/assets/{index-D89EUizB.css → index-DkJv4l6L.css} +1 -1
  30. package/dist/assets/{settingsSchema-CAFckLz6.js → settingsSchema-B0uoL_1R.js} +1 -1
  31. package/dist/assets/useAgentsDirWatch-e3TJUFPP.js +1 -0
  32. package/dist/assets/{useConfigDirWatch-QKxfZx9X.js → useConfigDirWatch-Dxv7JbCU.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 +51 -17
  38. package/src/main/build-info.json +4 -4
  39. package/src/main/chatRunner.cjs +8 -6
  40. package/src/main/crashDiagnostics.cjs +39 -3
  41. package/src/main/historyAggregator.cjs +7 -1
  42. package/src/main/index.cjs +73 -4
  43. package/src/main/ipcSchemas.cjs +27 -0
  44. package/src/main/lib/agentPersonaSchema.cjs +8 -1
  45. package/src/main/lib/appRuntime.cjs +57 -0
  46. package/src/main/lib/childWithLog.cjs +10 -10
  47. package/src/main/lib/claudeBin.cjs +58 -20
  48. package/src/main/lib/cleanEnv.cjs +33 -9
  49. package/src/main/lib/customerFeedbackAdminRoutes.cjs +94 -0
  50. package/src/main/lib/customerFeedbackClient.cjs +261 -0
  51. package/src/main/lib/customerFeedbackInbox.cjs +286 -0
  52. package/src/main/lib/customerFeedbackIpc.cjs +43 -0
  53. package/src/main/lib/defaultShell.cjs +36 -0
  54. package/src/main/lib/definitionOfDone.cjs +6 -2
  55. package/src/main/lib/delegationReadiness.cjs +13 -5
  56. package/src/main/lib/gitWorktree.cjs +6 -3
  57. package/src/main/lib/insideHome.cjs +15 -4
  58. package/src/main/lib/killTree.cjs +80 -0
  59. package/src/main/lib/loadGate.cjs +16 -0
  60. package/src/main/lib/loginShellPath.cjs +82 -0
  61. package/src/main/lib/machineProfile.cjs +4 -2
  62. package/src/main/lib/mcpStdioServer.cjs +127 -0
  63. package/src/main/lib/mcpToolCatalog.cjs +26 -1
  64. package/src/main/lib/openExternalApp.cjs +62 -11
  65. package/src/main/lib/otlpTraceExporter.cjs +147 -0
  66. package/src/main/lib/prereqs.cjs +145 -0
  67. package/src/main/lib/procIdentity.cjs +37 -2
  68. package/src/main/lib/projectFolder.cjs +89 -0
  69. package/src/main/lib/rcaReport.cjs +1 -0
  70. package/src/main/lib/reaperHelpers.cjs +5 -2
  71. package/src/main/lib/shippedPersonaSeeds.cjs +10 -0
  72. package/src/main/lib/timeoutShimScript.cjs +14 -2
  73. package/src/main/lib/updateCheck.cjs +41 -11
  74. package/src/main/lib/watchdogHelpers.cjs +23 -3
  75. package/src/main/lib/winSpawn.cjs +67 -0
  76. package/src/main/lib/winUserData.cjs +22 -0
  77. package/src/main/otel.cjs +18 -62
  78. package/src/main/pty.cjs +32 -11
  79. package/src/main/runVerify.cjs +42 -2
  80. package/src/main/scheduler.cjs +116 -29
  81. package/src/main/seedSchedulerMcp.cjs +11 -2
  82. package/src/preload/api.d.ts +82 -0
  83. package/src/preload/index.cjs +14 -0
  84. package/src/seed/agents/validator.md +2 -2
  85. package/dist/assets/index-DrvxIbmR.js +0 -3073
  86. package/dist/assets/useAgentsDirWatch-XhQEVlaK.js +0 -1
  87. package/scripts/postinstall.cjs +0 -52
@@ -0,0 +1,82 @@
1
+ 'use strict';
2
+
3
+ const { execFile } = require('node:child_process');
4
+ const path = require('node:path');
5
+
6
+ const START = '__SM_PATH_START__';
7
+ const END = '__SM_PATH_END__';
8
+ const DEFAULT_TIMEOUT_MS = 3000;
9
+
10
+ /**
11
+ * A packaged macOS app launched from Finder/Dock inherits only
12
+ * /usr/bin:/bin:/usr/sbin:/sbin, so nvm/asdf/volta/fnm/Homebrew installs of
13
+ * node, git and claude are invisible. Ask the user's login shell for its PATH.
14
+ * Complements cleanEnv.pathWithUserBins (fixed dirs, appended).
15
+ */
16
+
17
+ /** @param {{shell?: string, platform?: string, timeoutMs?: number, env?: NodeJS.ProcessEnv}} [opts] */
18
+ function resolveShell(opts) {
19
+ const platform = opts.platform || process.platform;
20
+ const env = opts.env || process.env;
21
+ return opts.shell || env.SHELL || (platform === 'darwin' ? '/bin/zsh' : '/bin/bash');
22
+ }
23
+
24
+ /**
25
+ * Login-shell PATH, or null on timeout / non-zero exit / missing markers /
26
+ * win32 (fail-open: callers keep their current PATH).
27
+ * @param {{shell?: string, platform?: string, timeoutMs?: number, env?: NodeJS.ProcessEnv}} [opts]
28
+ * @returns {Promise<string|null>}
29
+ */
30
+ function readLoginShellPath(opts = {}) {
31
+ const platform = opts.platform || process.platform;
32
+ if (platform === 'win32') return Promise.resolve(null);
33
+ const shell = resolveShell(opts);
34
+ const cmd = `printf '%s' '${START}'; printf '%s' "$PATH"; printf '%s' '${END}'`;
35
+ return new Promise((resolve) => {
36
+ try {
37
+ execFile(
38
+ shell,
39
+ ['-ilc', cmd],
40
+ { timeout: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS, env: opts.env || process.env, encoding: 'utf8' },
41
+ (err, stdout) => {
42
+ if (err) return resolve(null);
43
+ const out = String(stdout || '');
44
+ const s = out.indexOf(START);
45
+ if (s < 0) return resolve(null);
46
+ const from = s + START.length;
47
+ const e = out.indexOf(END, from);
48
+ if (e < 0) return resolve(null);
49
+ resolve(out.slice(from, e));
50
+ },
51
+ );
52
+ } catch {
53
+ resolve(null);
54
+ }
55
+ });
56
+ }
57
+
58
+ /**
59
+ * Prepend login-shell PATH entries not already present to env.PATH
60
+ * (order preserved, deduped, empties dropped).
61
+ * @param {{shell?: string, platform?: string, timeoutMs?: number, env?: NodeJS.ProcessEnv}} [opts]
62
+ * @returns {Promise<{applied: boolean, added: number}>}
63
+ */
64
+ async function applyLoginShellPath(opts = {}) {
65
+ const platform = opts.platform || process.platform;
66
+ if (platform === 'win32') return { applied: false, added: 0 };
67
+ const loginPath = await readLoginShellPath(opts);
68
+ if (loginPath === null) return { applied: false, added: 0 };
69
+ const env = opts.env || process.env;
70
+ const current = (env.PATH || '').split(path.delimiter).filter(Boolean);
71
+ const seen = new Set(current);
72
+ const fresh = [];
73
+ for (const entry of loginPath.split(path.delimiter)) {
74
+ if (!entry || seen.has(entry)) continue;
75
+ seen.add(entry);
76
+ fresh.push(entry);
77
+ }
78
+ if (fresh.length) env.PATH = [...fresh, ...current].join(path.delimiter);
79
+ return { applied: true, added: fresh.length };
80
+ }
81
+
82
+ module.exports = { readLoginShellPath, applyLoginShellPath };
@@ -16,6 +16,7 @@ const os = require('node:os');
16
16
  const crypto = require('node:crypto');
17
17
  const path = require('node:path');
18
18
  const claudeBin = require('./claudeBin.cjs');
19
+ const { isPackagedApp } = require('./appRuntime.cjs');
19
20
 
20
21
  function resolveElectronApp() {
21
22
  try {
@@ -39,12 +40,13 @@ function resolvePackageVersion() {
39
40
  }
40
41
 
41
42
  /**
42
- * 'npx' when the app path lives inside an npm/npx cache dir, 'dev' when
43
+ * 'installer' for a packaged app build, 'npx' when the app path lives inside an npm/npx cache dir, 'dev' when
43
44
  * SM_DEV is set, else 'unknown'. Injectable so tests never depend on real
44
45
  * process state.
45
46
  */
46
- function resolveInstallChannel({ appPath = null, devFlag = !!process.env.SM_DEV } = {}) {
47
+ function resolveInstallChannel({ appPath = null, devFlag = !!process.env.SM_DEV, packaged } = {}) {
47
48
  if (devFlag) return 'dev';
49
+ if (isPackagedApp(typeof packaged === 'boolean' ? { packaged } : undefined)) return 'installer';
48
50
  if (typeof appPath === 'string' && /[\\/](\.npm|_npx|npm-cache|npx-cache)[\\/]/i.test(appPath)) {
49
51
  return 'npx';
50
52
  }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * mcpStdioServer.cjs — zero-dependency MCP server over newline-delimited
3
+ * JSON-RPC 2.0 on stdio. Implements only what the scheduler MCP server uses:
4
+ * initialize, ping, tools/list, tools/call. Stdout carries protocol messages
5
+ * only; diagnostics go to stderr.
6
+ */
7
+ 'use strict';
8
+
9
+ // Copied verbatim from @modelcontextprotocol/sdk dist/cjs/types.js
10
+ // (LATEST_PROTOCOL_VERSION first).
11
+ const SUPPORTED_PROTOCOL_VERSIONS = ['2025-11-25', '2025-06-18', '2025-03-26', '2024-11-05', '2024-10-07'];
12
+
13
+ const PARSE_ERROR = -32700;
14
+ const METHOD_NOT_FOUND = -32601;
15
+ const INTERNAL_ERROR = -32603;
16
+
17
+ /**
18
+ * @param {{
19
+ * name: string,
20
+ * version: string,
21
+ * listTools: () => any[] | Promise<any[]>,
22
+ * callTool: (params: any) => Promise<any>,
23
+ * input?: NodeJS.ReadableStream,
24
+ * output?: NodeJS.WritableStream,
25
+ * }} opts
26
+ */
27
+ function createStdioServer({ name, version, listTools, callTool, input = process.stdin, output = process.stdout }) {
28
+ let buffer = '';
29
+ let started = false;
30
+ let closed = false;
31
+
32
+ function send(msg) {
33
+ if (closed) return;
34
+ try {
35
+ output.write(JSON.stringify(msg) + '\n');
36
+ } catch (err) {
37
+ process.stderr.write(`[mcpStdioServer] write failed: ${err && err.message}\n`);
38
+ }
39
+ }
40
+
41
+ function sendError(id, code, message) {
42
+ send({ jsonrpc: '2.0', id, error: { code, message } });
43
+ }
44
+
45
+ async function dispatch(msg) {
46
+ const hasId = msg && typeof msg === 'object' && msg.id !== undefined && msg.id !== null;
47
+ if (!hasId) return; // notification (or unusable message): never reply
48
+ const { id, method, params } = msg;
49
+ try {
50
+ let result;
51
+ switch (method) {
52
+ case 'initialize': {
53
+ const requested = params && params.protocolVersion;
54
+ result = {
55
+ protocolVersion: SUPPORTED_PROTOCOL_VERSIONS.includes(requested) ? requested : SUPPORTED_PROTOCOL_VERSIONS[0],
56
+ capabilities: { tools: {} },
57
+ serverInfo: { name, version },
58
+ };
59
+ break;
60
+ }
61
+ case 'ping':
62
+ result = {};
63
+ break;
64
+ case 'tools/list':
65
+ result = { tools: await listTools() };
66
+ break;
67
+ case 'tools/call':
68
+ result = await callTool(params);
69
+ break;
70
+ default:
71
+ sendError(id, METHOD_NOT_FOUND, `Method not found: ${method}`);
72
+ return;
73
+ }
74
+ send({ jsonrpc: '2.0', id, result });
75
+ } catch (err) {
76
+ sendError(id, INTERNAL_ERROR, err && err.message ? err.message : String(err));
77
+ }
78
+ }
79
+
80
+ function handleLine(line) {
81
+ if (line.endsWith('\r')) line = line.slice(0, -1);
82
+ if (!line.trim()) return;
83
+ let msg;
84
+ try {
85
+ msg = JSON.parse(line);
86
+ } catch {
87
+ sendError(null, PARSE_ERROR, 'Parse error');
88
+ return;
89
+ }
90
+ dispatch(msg).catch((err) => {
91
+ process.stderr.write(`[mcpStdioServer] dispatch failed: ${err && err.message}\n`);
92
+ });
93
+ }
94
+
95
+ function onData(chunk) {
96
+ buffer += typeof chunk === 'string' ? chunk : chunk.toString('utf8');
97
+ let idx;
98
+ while ((idx = buffer.indexOf('\n')) !== -1) {
99
+ const line = buffer.slice(0, idx);
100
+ buffer = buffer.slice(idx + 1);
101
+ handleLine(line);
102
+ }
103
+ }
104
+
105
+ function onEnd() {
106
+ close();
107
+ }
108
+
109
+ function start() {
110
+ if (started || closed) return;
111
+ started = true;
112
+ input.on('data', onData);
113
+ input.on('end', onEnd);
114
+ }
115
+
116
+ function close() {
117
+ if (closed) return;
118
+ closed = true;
119
+ input.removeListener('data', onData);
120
+ input.removeListener('end', onEnd);
121
+ buffer = '';
122
+ }
123
+
124
+ return { start, close };
125
+ }
126
+
127
+ module.exports = { createStdioServer, SUPPORTED_PROTOCOL_VERSIONS };
@@ -23,7 +23,7 @@ const { z } = require('zod');
23
23
 
24
24
  const CatalogEntrySchema = z.object({
25
25
  name: z.string().min(1),
26
- group: z.enum(['scheduler', 'chat', 'feedback', 'help', 'project-home', 'macros']),
26
+ group: z.enum(['scheduler', 'chat', 'feedback', 'help', 'project-home', 'macros', 'customer-feedback']),
27
27
  purpose: z.string().min(1),
28
28
  whenToUse: z.string().min(1),
29
29
  whenNotToUse: z.string().min(1),
@@ -293,6 +293,31 @@ const MCP_TOOL_CATALOG = [
293
293
  notes: "cwd is optional — defaults to the calling session's own project root (SM_PROJECT_ROOT or process.cwd()) when omitted. "
294
294
  + 'The document is CSP-stamped after it is written, so the viewer sandbox is the real network fence — these checks are defence in depth.',
295
295
  },
296
+ {
297
+ name: 'customer_feedback_list',
298
+ group: 'customer-feedback',
299
+ purpose: "List the customers' feedback in the owner's local inbox (bug / feature / discussion items submitted "
300
+ + 'from the {F} feedback panel), optionally pulling new items from the feedback service first.',
301
+ whenToUse: 'Use to inspect what customers have submitted and each item\'s status. `pull` defaults to true '
302
+ + '(fetch new items first); pass `includeHidden: true` to include moderated-away items.',
303
+ whenNotToUse: 'WHEN NOT TO USE IT: this only works on the owner\'s machine (it needs the owner token) and is read-only. '
304
+ + 'Turning an item into an Epic is done by the human via Open as Epic in the {F} feedback panel — there is no tool '
305
+ + 'for it. A finding that belongs to another local project uses feedback_open_session instead.',
306
+ exampleArgs: { pull: true, includeHidden: false },
307
+ notes: 'Returns { ok, items, pulled? }. Not-configured machines get { ok:false, error } as an error result.',
308
+ },
309
+ {
310
+ name: 'customer_feedback_set_status',
311
+ group: 'customer-feedback',
312
+ purpose: 'Set the status of one customer feedback item (open | in_progress | resolved | wontfix) with an optional '
313
+ + 'note the customer sees, via the session-manager app\'s admin API.',
314
+ whenToUse: 'Use after inspecting an item with customer_feedback_list, to mark it in progress, resolved, or wontfix.',
315
+ whenNotToUse: 'WHEN NOT TO USE IT: this changes status only. Turning an item into an Epic is done by the human via '
316
+ + 'Open as Epic in the {F} feedback panel — there is no tool for it. Cross-project findings use '
317
+ + 'feedback_open_session. Never mark an item resolved unless the work has actually landed.',
318
+ exampleArgs: { id: 'fb_123', status: 'resolved', note: 'Fixed in 0.97.0' },
319
+ notes: 'The note is shown to the customer; keep it short (the inbox enforces a maximum length).',
320
+ },
296
321
  {
297
322
  name: 'macro_list',
298
323
  group: 'macros',
@@ -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 };