claude-code-session-manager 0.105.0 → 0.107.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 (59) hide show
  1. package/dist/assets/{AgentLibrary-BDtr4oI-.js → AgentLibrary-Bjeq2-kp.js} +1 -1
  2. package/dist/assets/{Button-CuXBCQDp.js → Button-CRLhoI2K.js} +1 -1
  3. package/dist/assets/{DataModel-DmvRipT5.js → DataModel-iPJw5jcG.js} +1 -1
  4. package/dist/assets/{History-B-v9pwwr.js → History-DfPR3_6N.js} +1 -1
  5. package/dist/assets/{Hooks-S_0oHHlr.js → Hooks-Bw7H_DmC.js} +1 -1
  6. package/dist/assets/{Library-BcbGJxQ3.js → Library-rqaJJJ1b.js} +1 -1
  7. package/dist/assets/{ListDetail-DtqHqJHt.js → ListDetail-qI27OYfy.js} +1 -1
  8. package/dist/assets/{MarkdownEditor-B56sPp_l.js → MarkdownEditor-BEFuHVNz.js} +1 -1
  9. package/dist/assets/{McpServers-D5VnmxAn.js → McpServers-Bz9O79rz.js} +1 -1
  10. package/dist/assets/{Memory-0yGZLyRP.js → Memory-DobxIoZ-.js} +1 -1
  11. package/dist/assets/{Panel-NoAADJwx.js → Panel-Cyt1wpUf.js} +1 -1
  12. package/dist/assets/{Permissions-D2Dh-ee8.js → Permissions-ChSNRUJJ.js} +1 -1
  13. package/dist/assets/{Plugins-kGoUgS9b.js → Plugins-CII221R2.js} +2 -2
  14. package/dist/assets/{ProvenanceBadge-CHwDFTX7.js → ProvenanceBadge-C_sk5-v3.js} +1 -1
  15. package/dist/assets/{SaveBar-BtKNMIYK.js → SaveBar-BuYrg5E9.js} +1 -1
  16. package/dist/assets/Scheduler-eixbMGEk.js +16 -0
  17. package/dist/assets/{ScopeSwitcher-BusohWZ5.js → ScopeSwitcher-BM0gLPhd.js} +1 -1
  18. package/dist/assets/Settings-qD-0W-Ut.js +4 -0
  19. package/dist/assets/{SkillReferenceGraph-BHOdIpsy.js → SkillReferenceGraph-D0Hgdfxq.js} +1 -1
  20. package/dist/assets/{Skills-DSTLZimI.js → Skills-C3T_2_2o.js} +1 -1
  21. package/dist/assets/{SystemPrompt-BG7wFWdb.js → SystemPrompt-B-xTy9Qn.js} +1 -1
  22. package/dist/assets/{TagLibrary-Bx27aE3_.js → TagLibrary-BH67tR5z.js} +1 -1
  23. package/dist/assets/{TiptapBody-j0O8J7YI.js → TiptapBody-CP_F_mwb.js} +1 -1
  24. package/dist/assets/{Toggle-DzVZ6rC7.js → Toggle-haRPHS5N.js} +1 -1
  25. package/dist/assets/{ViewTabs-BizjuynT.js → ViewTabs-BHV3MA5u.js} +1 -1
  26. package/dist/assets/{index-BLVzrc4k.css → index-tr1w3hHB.css} +1 -1
  27. package/dist/assets/index-v_3LvhMx.js +3077 -0
  28. package/dist/assets/{settingsSchema-7MRAxaJk.js → settingsSchema-CE46TA3V.js} +1 -1
  29. package/dist/assets/useAgentsDirWatch-BZTi2K2e.js +1 -0
  30. package/dist/assets/{useConfigDirWatch-ot0VYwQC.js → useConfigDirWatch-CjcswsWR.js} +1 -1
  31. package/dist/index.html +2 -2
  32. package/package.json +2 -1
  33. package/scripts/scheduler-mcp-server.cjs +58 -2
  34. package/src/main/build-info.json +4 -4
  35. package/src/main/chatRunner.cjs +4 -2
  36. package/src/main/crashDiagnostics.cjs +39 -3
  37. package/src/main/index.cjs +47 -3
  38. package/src/main/ipcSchemas.cjs +120 -0
  39. package/src/main/lib/claudeBin.cjs +22 -8
  40. package/src/main/lib/cleanEnv.cjs +6 -5
  41. package/src/main/lib/customerFeedbackAdminRoutes.cjs +94 -0
  42. package/src/main/lib/customerFeedbackClient.cjs +261 -0
  43. package/src/main/lib/customerFeedbackInbox.cjs +286 -0
  44. package/src/main/lib/customerFeedbackIpc.cjs +43 -0
  45. package/src/main/lib/e2eStateMachine.cjs +39 -0
  46. package/src/main/lib/insideHome.cjs +1 -1
  47. package/src/main/lib/mcpToolCatalog.cjs +31 -6
  48. package/src/main/lib/projectFolder.cjs +89 -0
  49. package/src/main/lib/usageCircuit.cjs +15 -0
  50. package/src/main/lib/winUserData.cjs +22 -0
  51. package/src/main/scheduler.cjs +68 -20
  52. package/src/main/usage.cjs +10 -2
  53. package/src/main/webRemote.cjs +1364 -0
  54. package/src/preload/api.d.ts +132 -1
  55. package/src/preload/index.cjs +51 -0
  56. package/dist/assets/Scheduler-B0_kMMDs.js +0 -16
  57. package/dist/assets/Settings-DuRJntaE.js +0 -3
  58. package/dist/assets/index-CLIaYljt.js +0 -3073
  59. package/dist/assets/useAgentsDirWatch-BUwfU2Kq.js +0 -1
@@ -0,0 +1,286 @@
1
+ // @ts-check
2
+ /**
3
+ * customerFeedbackInbox — OWNER side of customer feedback.
4
+ *
5
+ * With an owner token configured (env SM_FEEDBACK_OWNER_TOKEN or
6
+ * ~/.claude/session-manager/customer-feedback-owner.json) this machine incrementally pulls every
7
+ * submission for the session-manager slug from bilko.run into a local mirror, sets item status on
8
+ * the host, and records which local Epic an item became. No token = customer machine (ownerMode
9
+ * false). The token is only ever sent as a bearer header and never appears in errors or the mirror.
10
+ */
11
+ 'use strict';
12
+
13
+ const path = require('node:path');
14
+ const fsp = require('node:fs/promises');
15
+ const atomicFs = require('./atomicFs.cjs');
16
+ const schedulerPaths = require('./schedulerPaths.cjs');
17
+ const { resolveFeedbackBase, fromWireType, FEEDBACK_SLUG } = require('./customerFeedbackClient.cjs');
18
+
19
+ const STATUSES = /** @type {const} */ (['open', 'in_progress', 'resolved', 'wontfix']);
20
+ const MAX_NOTE = 500;
21
+ const MAX_ITEMS = 2000;
22
+ const PAGE_LIMIT = 500;
23
+ const MAX_PAGES = 10;
24
+ const TIMEOUT_MS = 15000;
25
+ const SCHEMA_VERSION = 1;
26
+ const NO_TOKEN = 'Feedback owner token not configured';
27
+ const NO_STATUS_SUPPORT = 'Host does not support feedback status yet (or unknown id)';
28
+ const HIDDEN = new Set(['archived', 'deleted']);
29
+
30
+ /**
31
+ * @typedef {{ id: string, receivedAt: string|number|null, tag: string, title: string, body: string,
32
+ * clientVersion: string|null, clientPlatform: string|null, moderation: string|null, status: string,
33
+ * statusNote: string|null, statusAt: string|number|null, epicId: string|null }} InboxItem
34
+ * @typedef {{ items: InboxItem[], nextSince: string|null, nextModeratedSince: string|null }} Store
35
+ * @typedef {{ fetchImpl?: typeof fetch, storeFile?: string, ownerFile?: string,
36
+ * env?: Record<string, string|undefined>, now?: () => number }} Deps
37
+ */
38
+
39
+ /** @param {Deps} [deps] */
40
+ function ctx(deps) {
41
+ const d = deps || {};
42
+ return {
43
+ fetchImpl: d.fetchImpl || globalThis.fetch,
44
+ storeFile: d.storeFile || path.join(schedulerPaths.schedulerHome(), 'customer-feedback-inbox.json'),
45
+ ownerFile: d.ownerFile || path.join(schedulerPaths.schedulerHome(), 'customer-feedback-owner.json'),
46
+ env: d.env || process.env,
47
+ now: d.now || Date.now,
48
+ };
49
+ }
50
+
51
+ /** @param {Deps} [deps] @returns {{ token: string, projectCwd: string|null }|null} */
52
+ function loadOwnerConfig(deps) {
53
+ const c = ctx(deps);
54
+ const envToken = (c.env.SM_FEEDBACK_OWNER_TOKEN || '').trim();
55
+ if (envToken) return { token: envToken, projectCwd: c.env.SM_FEEDBACK_OWNER_PROJECT_CWD || null };
56
+ try {
57
+ const j = JSON.parse(require('node:fs').readFileSync(c.ownerFile, 'utf8'));
58
+ const token = j && typeof j.token === 'string' ? j.token.trim() : '';
59
+ if (token) return { token, projectCwd: typeof j.projectCwd === 'string' && j.projectCwd ? j.projectCwd : null };
60
+ } catch { /* missing or corrupt = not an owner */ }
61
+ return null;
62
+ }
63
+
64
+ /** Never includes the token. @param {Deps} [deps] */
65
+ function getOwnerInfo(deps) {
66
+ const cfg = loadOwnerConfig(deps);
67
+ return { ownerMode: !!cfg, projectCwd: cfg ? cfg.projectCwd : null };
68
+ }
69
+
70
+ /** @param {string} file @returns {Promise<Store>} */
71
+ async function readStore(file) {
72
+ try {
73
+ const j = JSON.parse(await fsp.readFile(file, 'utf8'));
74
+ if (j && Array.isArray(j.items)) {
75
+ return {
76
+ items: j.items.filter((/** @type {unknown} */ i) => i && typeof i === 'object'),
77
+ nextSince: typeof j.nextSince === 'string' ? j.nextSince : null,
78
+ nextModeratedSince: typeof j.nextModeratedSince === 'string' ? j.nextModeratedSince : null,
79
+ };
80
+ }
81
+ } catch { /* missing or corrupt reads as empty */ }
82
+ return { items: [], nextSince: null, nextModeratedSince: null };
83
+ }
84
+
85
+ let writeQueue = Promise.resolve();
86
+ /**
87
+ * Serialized read-modify-write over the mirror.
88
+ * @template T
89
+ * @param {string} file
90
+ * @param {(s: Store) => T} mutate mutates the store in place, returns a result
91
+ * @returns {Promise<T>}
92
+ */
93
+ function mutateStore(file, mutate) {
94
+ const run = async () => {
95
+ const store = await readStore(file);
96
+ const result = mutate(store);
97
+ store.items = sortItems(store.items).slice(0, MAX_ITEMS);
98
+ await atomicFs.writeJsonAtomic(file, { schemaVersion: SCHEMA_VERSION, ...store }, { mode: 0o600 });
99
+ return result;
100
+ };
101
+ const tail = writeQueue.then(run, run);
102
+ writeQueue = tail.then(() => {}, () => {});
103
+ return tail;
104
+ }
105
+
106
+ /** @param {InboxItem[]} items */
107
+ function sortItems(items) {
108
+ const t = (/** @type {InboxItem} */ i) => (i.receivedAt == null ? 0 : (typeof i.receivedAt === 'number' ? i.receivedAt : Date.parse(i.receivedAt) || 0));
109
+ return items.slice().sort((a, b) => t(b) - t(a));
110
+ }
111
+
112
+ /** @param {string} s @param {string} token */
113
+ function redact(s, token) {
114
+ return token ? s.split(token).join('[redacted]') : s;
115
+ }
116
+
117
+ /** @param {unknown} e @param {string} token */
118
+ function netError(e, token) {
119
+ return `Could not reach the feedback service: ${redact(e instanceof Error ? e.message : String(e), token)}`;
120
+ }
121
+
122
+ /** @param {Response} res @param {string} fallback */
123
+ async function errorText(res, fallback) {
124
+ try {
125
+ /** @type {any} */
126
+ const j = await res.json();
127
+ if (j && typeof j.error === 'string' && j.error) return j.error;
128
+ } catch { /* no body */ }
129
+ return fallback;
130
+ }
131
+
132
+ /** @param {number} status */
133
+ function httpError(status) {
134
+ if (status === 401) return 'Feedback owner token was rejected (401).';
135
+ if (status === 503) return 'Host has no feedback owner token configured (503).';
136
+ return `Feedback service returned ${status}.`;
137
+ }
138
+
139
+ /** @param {any} w @param {InboxItem|undefined} prev @returns {InboxItem} */
140
+ function toItem(w, prev) {
141
+ const client = w.client && typeof w.client === 'object' ? w.client : {};
142
+ const st = w.status && typeof w.status === 'object' ? w.status : null;
143
+ return {
144
+ id: w.id,
145
+ receivedAt: w.receivedAt ?? null,
146
+ tag: fromWireType(String(w.type)),
147
+ title: String(w.title ?? ''),
148
+ body: String(w.description ?? ''),
149
+ clientVersion: typeof client.version === 'string' ? client.version : null,
150
+ clientPlatform: typeof client.platform === 'string' ? client.platform : null,
151
+ moderation: w.moderation && typeof w.moderation.action === 'string' ? w.moderation.action : null,
152
+ status: st && typeof st.value === 'string' ? st.value : 'open',
153
+ statusNote: st && typeof st.note === 'string' ? st.note : null,
154
+ statusAt: st ? (st.at ?? null) : null,
155
+ epicId: prev && prev.epicId ? prev.epicId : null,
156
+ };
157
+ }
158
+
159
+ /**
160
+ * @param {Deps} [deps]
161
+ * @returns {Promise<{ ok: true, fetched: number, pages: number } | { ok: false, error: string }>}
162
+ */
163
+ async function pull(deps) {
164
+ const cfg = loadOwnerConfig(deps);
165
+ if (!cfg) return { ok: false, error: NO_TOKEN };
166
+ const c = ctx(deps);
167
+ const base = `${resolveFeedbackBase()}/api/projects/${FEEDBACK_SLUG}/feedback`;
168
+ const prior = await readStore(c.storeFile);
169
+ let since = prior.nextSince;
170
+ let moderatedSince = prior.nextModeratedSince;
171
+ /** @type {any[]} */
172
+ const fetched = [];
173
+ let pages = 0;
174
+ try {
175
+ for (; pages < MAX_PAGES;) {
176
+ const qs = new URLSearchParams();
177
+ if (since) qs.set('since', since);
178
+ if (moderatedSince) qs.set('moderatedSince', moderatedSince);
179
+ qs.set('images', 'none');
180
+ qs.set('limit', String(PAGE_LIMIT));
181
+ const res = await c.fetchImpl(`${base}?${qs.toString()}`, {
182
+ headers: { authorization: `Bearer ${cfg.token}` },
183
+ signal: AbortSignal.timeout(TIMEOUT_MS),
184
+ });
185
+ if (!res.ok) return { ok: false, error: await errorText(res, httpError(res.status)) };
186
+ /** @type {any} */
187
+ const j = await res.json().catch(() => null);
188
+ if (!j || !Array.isArray(j.items)) return { ok: false, error: 'Feedback service returned an unexpected response.' };
189
+ pages++;
190
+ fetched.push(...j.items.filter((/** @type {any} */ i) => i && typeof i.id === 'string' && i.id));
191
+ if (typeof j.nextSince === 'string') since = j.nextSince;
192
+ if (typeof j.nextModeratedSince === 'string') moderatedSince = j.nextModeratedSince;
193
+ if (j.items.length < PAGE_LIMIT) break;
194
+ }
195
+ } catch (e) {
196
+ return { ok: false, error: netError(e, cfg.token) };
197
+ }
198
+ const finalSince = since;
199
+ const finalModerated = moderatedSince;
200
+ await mutateStore(c.storeFile, (store) => {
201
+ const byId = new Map(store.items.map((i) => [i.id, i]));
202
+ for (const w of fetched) byId.set(w.id, toItem(w, byId.get(w.id)));
203
+ store.items = [...byId.values()];
204
+ store.nextSince = finalSince;
205
+ store.nextModeratedSince = finalModerated;
206
+ });
207
+ return { ok: true, fetched: fetched.length, pages };
208
+ }
209
+
210
+ /**
211
+ * @param {{ includeHidden?: boolean }} [opts] @param {Deps} [deps]
212
+ * @returns {Promise<{ ok: true, items: InboxItem[] } | { ok: false, error: string }>}
213
+ */
214
+ async function list(opts, deps) {
215
+ if (!loadOwnerConfig(deps)) return { ok: false, error: NO_TOKEN };
216
+ const store = await readStore(ctx(deps).storeFile);
217
+ const includeHidden = !!(opts && opts.includeHidden);
218
+ const items = sortItems(store.items).filter((i) => includeHidden || !(i.moderation && HIDDEN.has(i.moderation)));
219
+ return { ok: true, items };
220
+ }
221
+
222
+ /**
223
+ * @param {string} id @param {string} status @param {string} [note] @param {Deps} [deps]
224
+ * @returns {Promise<{ ok: true, item: InboxItem|null } | { ok: false, error: string }>}
225
+ */
226
+ async function setStatus(id, status, note, deps) {
227
+ const cfg = loadOwnerConfig(deps);
228
+ if (!cfg) return { ok: false, error: NO_TOKEN };
229
+ if (typeof id !== 'string' || !id) return { ok: false, error: 'Feedback id is required.' };
230
+ if (!STATUSES.includes(/** @type {any} */ (status))) return { ok: false, error: `Status must be one of: ${STATUSES.join(', ')}.` };
231
+ if (note !== undefined && note !== null && (typeof note !== 'string' || note.length > MAX_NOTE)) {
232
+ return { ok: false, error: `Note must be ${MAX_NOTE} characters or fewer.` };
233
+ }
234
+ const c = ctx(deps);
235
+ try {
236
+ const res = await c.fetchImpl(`${resolveFeedbackBase()}/api/projects/${FEEDBACK_SLUG}/feedback/${encodeURIComponent(id)}/status`, {
237
+ method: 'POST',
238
+ headers: { 'content-type': 'application/json', authorization: `Bearer ${cfg.token}` },
239
+ body: JSON.stringify(note ? { status, note } : { status }),
240
+ signal: AbortSignal.timeout(TIMEOUT_MS),
241
+ });
242
+ if (res.status === 404) return { ok: false, error: NO_STATUS_SUPPORT };
243
+ if (res.status !== 200) return { ok: false, error: await errorText(res, httpError(res.status)) };
244
+ /** @type {any} */
245
+ const j = await res.json().catch(() => null);
246
+ const statusAt = j && j.statusAt != null ? j.statusAt : c.now();
247
+ const item = await mutateStore(c.storeFile, (store) => {
248
+ const it = store.items.find((i) => i.id === id);
249
+ if (!it) return null;
250
+ it.status = status;
251
+ it.statusNote = note || null;
252
+ it.statusAt = statusAt;
253
+ return it;
254
+ });
255
+ return { ok: true, item };
256
+ } catch (e) {
257
+ return { ok: false, error: netError(e, cfg.token) };
258
+ }
259
+ }
260
+
261
+ /**
262
+ * Local-only: record which Epic an item was turned into.
263
+ * @param {string} id @param {string} epicId @param {Deps} [deps]
264
+ * @returns {Promise<{ ok: true } | { ok: false, error: string }>}
265
+ */
266
+ async function linkEpic(id, epicId, deps) {
267
+ if (!loadOwnerConfig(deps)) return { ok: false, error: NO_TOKEN };
268
+ if (typeof epicId !== 'string' || !epicId) return { ok: false, error: 'Epic id is required.' };
269
+ const found = await mutateStore(ctx(deps).storeFile, (store) => {
270
+ const it = store.items.find((i) => i.id === id);
271
+ if (!it) return false;
272
+ it.epicId = epicId;
273
+ return true;
274
+ });
275
+ return found ? { ok: true } : { ok: false, error: 'Unknown feedback id.' };
276
+ }
277
+
278
+ module.exports = {
279
+ STATUSES,
280
+ loadOwnerConfig,
281
+ getOwnerInfo,
282
+ pull,
283
+ list,
284
+ setStatus,
285
+ linkEpic,
286
+ };
@@ -0,0 +1,43 @@
1
+ // @ts-check
2
+ 'use strict';
3
+
4
+ const REFRESH_DELAY_MS = 30_000;
5
+
6
+ /**
7
+ * Renderer-facing IPC for the customer-feedback client + owner inbox.
8
+ * Handlers return the libs' `{ ok, ... }` results unchanged.
9
+ * @param {{ ipcMain?: any, client?: any, inbox?: any, setTimeoutImpl?: typeof setTimeout }} [deps]
10
+ */
11
+ function registerCustomerFeedbackHandlers(deps = {}) {
12
+ const ipcMain = deps.ipcMain || require('electron').ipcMain;
13
+ const { schemas: s, validated: v } = require('../ipcSchemas.cjs');
14
+ const client = deps.client || require('./customerFeedbackClient.cjs');
15
+ const inbox = deps.inbox || require('./customerFeedbackInbox.cjs');
16
+ const timer = deps.setTimeoutImpl || setTimeout;
17
+
18
+ ipcMain.handle('customerFeedback:submit', v(s.customerFeedbackSubmit, (p) => client.submit(p)));
19
+ ipcMain.handle('customerFeedback:list', () => client.list());
20
+ ipcMain.handle('customerFeedback:refresh-status', () => client.refreshStatuses());
21
+ ipcMain.handle('customerFeedback:mark-seen', v(s.customerFeedbackMarkSeen, ({ ids }) => client.markSeen(ids)));
22
+ ipcMain.handle('customerFeedback:owner-info', () => inbox.getOwnerInfo());
23
+ ipcMain.handle('customerFeedback:inbox-pull', () => inbox.pull());
24
+ ipcMain.handle('customerFeedback:inbox-list', v(s.customerFeedbackInboxList, ({ includeHidden }) => inbox.list({ includeHidden })));
25
+ ipcMain.handle('customerFeedback:inbox-set-status', v(s.customerFeedbackInboxSetStatus, ({ id, status, note }) => inbox.setStatus(id, status, note)));
26
+ ipcMain.handle('customerFeedback:inbox-link-epic', v(s.customerFeedbackInboxLinkEpic, ({ id, epicId }) => inbox.linkEpic(id, epicId)));
27
+
28
+ // One-shot deferred refresh so users see resolutions without opening anything.
29
+ try {
30
+ const t = timer(() => {
31
+ Promise.resolve()
32
+ .then(() => client.refreshStatuses())
33
+ .catch((/** @type {any} */ e) => {
34
+ console.warn('[customerFeedback] boot status refresh failed:', e && e.name ? e.name : 'error');
35
+ });
36
+ }, REFRESH_DELAY_MS);
37
+ if (t && typeof t.unref === 'function') t.unref();
38
+ } catch {
39
+ /* never throw at boot */
40
+ }
41
+ }
42
+
43
+ module.exports = { registerCustomerFeedbackHandlers, REFRESH_DELAY_MS };
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Pure E2E session state machine for the web-remote relay.
3
+ * No Electron, no I/O — importable in unit tests.
4
+ *
5
+ * State transitions:
6
+ * idle → pending_sas : successful deriveSessionKey + deriveSas
7
+ * idle → failed : crypto derivation error
8
+ * pending_sas → authenticated : user confirms SAS
9
+ * pending_sas → failed : deriveSas threw after sessionKey succeeded
10
+ * any → idle : disconnect / reset
11
+ */
12
+
13
+ /** @returns {{ state: string, sessionKey: Buffer|null, pendingSas: string|null }} */
14
+ function makeState(state = 'idle', sessionKey = null, pendingSas = null) {
15
+ return { state, sessionKey, pendingSas };
16
+ }
17
+
18
+ /**
19
+ * Attempt to confirm the SAS. Pure — does not mutate; returns the next state.
20
+ * @param {{ state: string, sessionKey: Buffer|null, pendingSas: string|null }} e2eState
21
+ * @returns {{ ok: boolean, error?: string, next: { state: string, sessionKey: Buffer|null, pendingSas: string|null } }}
22
+ */
23
+ function confirmSas(e2eState) {
24
+ if (e2eState.state !== 'pending_sas') {
25
+ const errorMap = {
26
+ idle: 'no_e2e_session',
27
+ failed: 'e2e_failed',
28
+ authenticated: 'already_authenticated',
29
+ };
30
+ const error = errorMap[e2eState.state] ?? 'unexpected_state';
31
+ return { ok: false, error, next: e2eState };
32
+ }
33
+ return {
34
+ ok: true,
35
+ next: makeState('authenticated', e2eState.sessionKey, null),
36
+ };
37
+ }
38
+
39
+ module.exports = { makeState, confirmSas };
@@ -64,7 +64,7 @@ function isContainedWin32(target, home) {
64
64
 
65
65
  function isContained(realPath, home = HOME_REAL, platform = process.platform) {
66
66
  if (platform === 'win32') return isContainedWin32(realPath, home);
67
- return realPath === home || realPath.startsWith(home + path.sep);
67
+ return realPath === home || realPath.startsWith(home + path.posix.sep);
68
68
  }
69
69
 
70
70
  function assertInsideHome(p) {
@@ -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),
@@ -50,11 +50,11 @@ const MCP_TOOL_CATALOG = [
50
50
  {
51
51
  name: 'scheduler_pause',
52
52
  group: 'scheduler',
53
- purpose: "Pause the scheduler (user-initiated) via the session-manager app's admin API.",
54
- whenToUse: 'Use to stop NEW job dispatch on request (e.g. before a risky change). Running jobs are never killed.',
55
- whenNotToUse: 'Do not use to cancel a running job (use scheduler_cancel_job) or as a workaround for a stuck queue.',
56
- exampleArgs: {},
57
- notes: 'A manual pause survives app restart and is never auto-cleared by the rate-limit timer or network recovery — only scheduler_resume clears it.',
53
+ purpose: "Pause NEW job dispatch for every project on this machine (machine-wide, not per-repo) via the session-manager app's admin API.",
54
+ whenToUse: 'Use only when the human asks you to stop dispatch everywhere, or for a machine-wide emergency. Pass a `reason`. Running jobs are never killed.',
55
+ whenNotToUse: "NEVER use it to protect one repo's git/branch state or a single Epic's work — it freezes every project's scheduler. Do not use it to cancel a running job (use scheduler_cancel_job) or as a workaround for a stuck queue.",
56
+ exampleArgs: { reason: 'machine-wide emergency: disk full' },
57
+ notes: 'An agent pause auto-expires after 30 minutes, is cleared on app restart, and is audited with the caller (session id + cwd). A human pause outranks it and is only cleared by scheduler_resume.',
58
58
  },
59
59
  {
60
60
  name: 'scheduler_resume',
@@ -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',
@@ -0,0 +1,89 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * projectFolder.cjs — create a new, empty project folder from a parent
5
+ * directory + a user-typed name. Electron's `createDirectory` picker property
6
+ * is macOS-only, so the app creates the folder itself.
7
+ *
8
+ * Never throws for validation / exists / io failures; returns a result object.
9
+ */
10
+
11
+ const fs = require('node:fs');
12
+ const path = require('node:path');
13
+
14
+ const MAX_NAME_LENGTH = 100;
15
+ // NUL + C0 controls + DEL.
16
+ // eslint-disable-next-line no-control-regex
17
+ const CONTROL_RE = /[\u0000-\u001f\u007f]/;
18
+
19
+ /**
20
+ * @param {unknown} name
21
+ * @returns {{ ok: true, name: string } | { ok: false, error: string }}
22
+ */
23
+ function validateProjectName(name) {
24
+ if (typeof name !== 'string') return { ok: false, error: 'Project name must be text.' };
25
+ const trimmed = name.trim();
26
+ if (!trimmed) return { ok: false, error: 'Project name cannot be empty.' };
27
+ if (trimmed.length > MAX_NAME_LENGTH) {
28
+ return { ok: false, error: `Project name must be ${MAX_NAME_LENGTH} characters or fewer.` };
29
+ }
30
+ if (trimmed === '.' || trimmed === '..') {
31
+ return { ok: false, error: 'Project name cannot be "." or "..".' };
32
+ }
33
+ if (trimmed.startsWith('-')) return { ok: false, error: 'Project name cannot start with "-".' };
34
+ if (trimmed.includes('/') || trimmed.includes('\\')) {
35
+ return { ok: false, error: 'Project name cannot contain "/" or "\\".' };
36
+ }
37
+ if (CONTROL_RE.test(trimmed)) {
38
+ return { ok: false, error: 'Project name cannot contain control characters.' };
39
+ }
40
+ return { ok: true, name: trimmed };
41
+ }
42
+
43
+ /**
44
+ * @param {{ parentDir: string, name: string }} input
45
+ * @returns {Promise<
46
+ * | { ok: true, path: string }
47
+ * | { ok: false, code: 'invalid-name' | 'invalid-parent' | 'io', error: string }
48
+ * | { ok: false, code: 'exists', path: string, error: string }
49
+ * >}
50
+ */
51
+ async function createProjectFolder({ parentDir, name } = /** @type {any} */ ({})) {
52
+ const v = validateProjectName(name);
53
+ if (!v.ok) return { ok: false, code: 'invalid-name', error: v.error };
54
+
55
+ if (typeof parentDir !== 'string' || !path.isAbsolute(parentDir)) {
56
+ return { ok: false, code: 'invalid-parent', error: 'Parent folder must be an absolute path.' };
57
+ }
58
+ try {
59
+ const st = await fs.promises.stat(parentDir);
60
+ if (!st.isDirectory()) {
61
+ return { ok: false, code: 'invalid-parent', error: `${parentDir} is not a folder.` };
62
+ }
63
+ } catch {
64
+ return { ok: false, code: 'invalid-parent', error: `Parent folder ${parentDir} does not exist.` };
65
+ }
66
+
67
+ const target = path.join(parentDir, v.name);
68
+ if (path.dirname(target) !== path.resolve(parentDir)) {
69
+ return { ok: false, code: 'invalid-name', error: 'Project name must be a single folder name.' };
70
+ }
71
+
72
+ try {
73
+ await fs.promises.mkdir(target); // non-recursive on purpose
74
+ return { ok: true, path: target };
75
+ } catch (err) {
76
+ const e = /** @type {NodeJS.ErrnoException} */ (err);
77
+ if (e && e.code === 'EEXIST') {
78
+ return {
79
+ ok: false,
80
+ code: 'exists',
81
+ path: target,
82
+ error: `A folder named "${v.name}" already exists in ${parentDir}`,
83
+ };
84
+ }
85
+ return { ok: false, code: 'io', error: (e && e.message) || String(err) };
86
+ }
87
+ }
88
+
89
+ module.exports = { createProjectFolder, validateProjectName };
@@ -137,6 +137,20 @@ function degradedBudget(lastGoodPayload, executorEvidence = {}) {
137
137
  return { utilization, concurrencyCap };
138
138
  }
139
139
 
140
+ /**
141
+ * Budget while the METER (not the account) is rate-limited: a 429 from the
142
+ * usage endpoint says nothing about account usage, so a recent-enough cached
143
+ * payload is trusted as-is. Returns `{ utilization, degraded: false }` from
144
+ * the cache, or null (no/old cache) so the caller falls back to degradedBudget().
145
+ */
146
+ function meterFailureBudget({ cachedPayload, staleSinceMs, now = Date.now(), maxStaleMs = 30 * 60 * 1000 } = {}) {
147
+ if (!cachedPayload || !Number.isFinite(staleSinceMs)) return null;
148
+ if (now - staleSinceMs > maxStaleMs) return null;
149
+ const window = bindingWindow(cachedPayload);
150
+ if (!Number.isFinite(window.utilization)) return null;
151
+ return { utilization: window.utilization, degraded: false, name: window.name };
152
+ }
153
+
140
154
  /** Wraps `fn` so concurrent callers awaiting an in-flight call share one promise. */
141
155
  function singleFlight(fn) {
142
156
  let inFlight = null;
@@ -229,6 +243,7 @@ module.exports = {
229
243
  isResetFresh,
230
244
  bindingWindow,
231
245
  degradedBudget,
246
+ meterFailureBudget,
232
247
  degradedConcurrencyCap,
233
248
  singleFlight,
234
249
  };
@@ -0,0 +1,22 @@
1
+ // Chromium resolves 'userData' via Windows known folders; with a stripped profile
2
+ // (no AppData) app.getPath('userData') throws "Failed to get 'userData' path" and the
3
+ // process dies 0x80000003. Fall back to an explicit %APPDATA%-based path.
4
+ const path = require('node:path');
5
+
6
+ function ensureWinUserDataPath({ app, env, platform, fs, os, log = () => {} }) {
7
+ if (platform !== 'win32') return false;
8
+ try {
9
+ app.getPath('userData');
10
+ return false;
11
+ } catch (err) {
12
+ const appData = (env && env.APPDATA) || path.win32.join(os.homedir(), 'AppData', 'Roaming');
13
+ const userData = path.win32.join(appData, 'Session Manager');
14
+ fs.mkdirSync(userData, { recursive: true });
15
+ app.setPath('appData', appData);
16
+ app.setPath('userData', userData);
17
+ log(`[win-userdata] getPath('userData') failed (${err && err.message}); using ${userData}`);
18
+ return true;
19
+ }
20
+ }
21
+
22
+ module.exports = { ensureWinUserDataPath };