claude-code-session-manager 0.108.0 → 0.110.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 (79) hide show
  1. package/dist/assets/AgentLibrary-Cm6MrOnF.js +2 -0
  2. package/dist/assets/{Button-DC6VzovQ.js → Button-CGVncJ6a.js} +1 -1
  3. package/dist/assets/DataModel-C3hlMoa8.js +1 -0
  4. package/dist/assets/History-CTyfGNKn.js +2 -0
  5. package/dist/assets/Hooks-BXxCBibx.js +3 -0
  6. package/dist/assets/{Library-DLmnlfQt.js → Library-aCs7zxAg.js} +24 -24
  7. package/dist/assets/{ListDetail-YylbvUj_.js → ListDetail-B9Hqk1_f.js} +1 -1
  8. package/dist/assets/{MarkdownEditor-DMPKWHTF.js → MarkdownEditor-Djfgp32k.js} +1 -1
  9. package/dist/assets/{McpServers-Da-Ty5nc.js → McpServers-8HQC6QgR.js} +2 -2
  10. package/dist/assets/{Memory-C3rUQ_cg.js → Memory-c4SVE3l2.js} +5 -5
  11. package/dist/assets/{Panel-CC3JTnOI.js → Panel-BEQTwE4z.js} +1 -1
  12. package/dist/assets/Permissions-BaYzgZtc.js +3 -0
  13. package/dist/assets/Plugins-DnXsVau7.js +2 -0
  14. package/dist/assets/ProvenanceBadge-CT2vWps1.js +1 -0
  15. package/dist/assets/RemoteMachines-DHJZfdJC.js +3 -0
  16. package/dist/assets/{SaveBar-C06rFT34.js → SaveBar-DwcHt3iR.js} +1 -1
  17. package/dist/assets/Scheduler-D6Gcuo0M.js +16 -0
  18. package/dist/assets/{ScopeSwitcher-Cp5mxblN.js → ScopeSwitcher-CgvWDc9Y.js} +1 -1
  19. package/dist/assets/Settings-CJoogVAB.js +4 -0
  20. package/dist/assets/{SkillReferenceGraph-DuWRmT_f.js → SkillReferenceGraph-4fq01kxq.js} +14 -14
  21. package/dist/assets/Skills-BU4ZX4h4.js +2 -0
  22. package/dist/assets/SystemPrompt-BCEyMSEq.js +1 -0
  23. package/dist/assets/TagLibrary-m3It8Oal.js +1 -0
  24. package/dist/assets/{TiptapBody-1r4ODLJ7.js → TiptapBody-DVyBsfsT.js} +1 -1
  25. package/dist/assets/{Toggle-BGT68XHP.js → Toggle-In_AExPV.js} +1 -1
  26. package/dist/assets/{ViewTabs-T0NBc8uv.js → ViewTabs-BLHjKUv-.js} +1 -1
  27. package/dist/assets/historyTotals-BXDd3EzP.js +1 -0
  28. package/dist/assets/index-CuiU3I-8.js +3077 -0
  29. package/dist/assets/{index-tr1w3hHB.css → index-Dutcsly-.css} +1 -1
  30. package/dist/assets/listSkills-C1aWKf0T.js +1 -0
  31. package/dist/assets/{settingsSchema-CvoNk3Ye.js → settingsSchema-DD4bc5cT.js} +1 -1
  32. package/dist/assets/useAgentsDirWatch-DMQ_XZtr.js +1 -0
  33. package/dist/assets/useRemotePeers-C9NfteSD.js +1 -0
  34. package/dist/index.html +2 -2
  35. package/package.json +6 -2
  36. package/plugins/session-manager-dev/skills/develop/SKILL.md +3 -1
  37. package/plugins/session-manager-dev/skills/develop/standards.md +5 -0
  38. package/plugins/session-manager-dev/skills/send-feedback/SKILL.md +48 -15
  39. package/scripts/README.md +7 -0
  40. package/scripts/scheduler-mcp-server.cjs +29 -2
  41. package/src/main/agentLibrary.cjs +20 -9
  42. package/src/main/build-info.json +4 -4
  43. package/src/main/chatRunner.cjs +7 -0
  44. package/src/main/config.cjs +20 -10
  45. package/src/main/historyAggregator.cjs +46 -3
  46. package/src/main/index.cjs +8 -0
  47. package/src/main/ipcSchemas.cjs +30 -0
  48. package/src/main/lib/autoUpdater.cjs +190 -0
  49. package/src/main/lib/crossProjectFeedback.cjs +143 -6
  50. package/src/main/lib/mcpToolCatalog.cjs +12 -3
  51. package/src/main/lib/reaperHelpers.cjs +13 -1
  52. package/src/main/lib/relayClientAuth.cjs +133 -0
  53. package/src/main/lib/remoteClientSessions.cjs +84 -0
  54. package/src/main/lib/remoteE2eCrypto.cjs +109 -0
  55. package/src/main/lib/updaterSettings.cjs +64 -0
  56. package/src/main/pty.cjs +53 -0
  57. package/src/main/remoteClient.cjs +334 -0
  58. package/src/main/remoteClientIpc.cjs +133 -0
  59. package/src/main/scheduler.cjs +16 -0
  60. package/src/main/updaterIpc.cjs +110 -0
  61. package/src/main/webRemote.cjs +341 -215
  62. package/src/preload/api.d.ts +97 -7
  63. package/src/preload/index.cjs +51 -3
  64. package/dist/assets/AgentLibrary-Co54GThg.js +0 -2
  65. package/dist/assets/DataModel-DZ0vyJau.js +0 -1
  66. package/dist/assets/History-Df3YwpPe.js +0 -2
  67. package/dist/assets/Hooks-DplG6roH.js +0 -3
  68. package/dist/assets/Permissions-XvBiMj1-.js +0 -3
  69. package/dist/assets/Plugins-CzHY1mgS.js +0 -2
  70. package/dist/assets/ProvenanceBadge-DaJIWVcg.js +0 -1
  71. package/dist/assets/Scheduler-BehadU-0.js +0 -16
  72. package/dist/assets/Settings-Cg9LIni0.js +0 -4
  73. package/dist/assets/Skills-CMmCZfIv.js +0 -3
  74. package/dist/assets/SystemPrompt-D_-MV025.js +0 -1
  75. package/dist/assets/TagLibrary-D9Tv3yOJ.js +0 -1
  76. package/dist/assets/index-3_AtCAyd.js +0 -3077
  77. package/dist/assets/listSkills-QORduPIk.js +0 -1
  78. package/dist/assets/useAgentsDirWatch-1C2CBBya.js +0 -1
  79. package/dist/assets/useConfigDirWatch-Cd5PnTkB.js +0 -1
package/scripts/README.md CHANGED
@@ -10,6 +10,8 @@ scripts/lib was moved into src/main/lib (2026-09); nothing under src/ may requir
10
10
  | --- | --- | --- |
11
11
  | audit-ops-hygiene.cjs | manual; named in `ops-sweep` SKILL.md | yes |
12
12
  | bench/main-bench.cjs | `npm run bench` (main-process micro-benches, plain Node); renderer half is `npm run bench:renderer` (`tests/bench/`, `vitest.bench.config.ts`, not in `test:unit`) | no |
13
+ | page-surface.cjs | manual and by the page steward personas `.claude/agents/page-*.md`; derives a page's owned/shared files, window.api methods, main handlers and tests | no |
14
+ | check-page-stewards.cjs | `npm run lint:page-stewards` (in `lint`) — every nav page has a valid `.claude/agents/page-<key>.md` steward persona; ratchets PENDING | no |
13
15
  | check-conditional-hooks.cjs | `npm run lint:hooks` (in `lint`) | no |
14
16
  | check-epic-transcripts.cjs | manual read-only diagnostic (`--json`); also surfaced as a non-fatal warning in `npm run health` | no |
15
17
  | check-doc-hierarchy.cjs | `npm run lint:docs` (in `lint`) | no |
@@ -32,6 +34,11 @@ scripts/lib was moved into src/main/lib (2026-09); nothing under src/ may requir
32
34
  | sm-ps.cjs | manual process-tree inspector | no |
33
35
  | sync-settings-schema.cjs | `npm run sync:settings-schema` | no |
34
36
  | write-build-info.cjs | npm `prepack` and `prepublishOnly` | no |
37
+ | test-quality/smells.cjs | `npm run lint:test-smells` (in `lint`) — static test-smell scan; ratchets source-text/no-assert against `tests/quality/smell-allowlist.json` | no |
38
+ | test-quality/coverage-map.cjs | manual, dev-only — per-test-file v8 coverage and unique-coverage attribution | no |
39
+ | test-quality/mutants.cjs | manual, dev-only — seeded mutation sampler; which test files kill which mutants | no |
40
+ | test-quality/coverage-guard.cjs | manual, dev-only — `--base <ref>`; fails if a test-only diff drops covered statements/branches | no |
41
+ | test-quality/report.cjs | manual, dev-only — keep/review/merge/bloat verdict per test file from the coverage map | no |
35
42
  | hooks/guard-destructive-git.cjs | `.claude/settings.json` PreToolUse + shim | yes |
36
43
  | hooks/guard-inline-implementation.cjs | `.claude/settings.json` PreToolUse + shim | yes |
37
44
  | hooks/guard-prd-writes.cjs | `.claude/settings.json` PreToolUse + shim | yes |
@@ -365,9 +365,36 @@ const TOOLS = [
365
365
  description: 'Absolute cwd of YOUR project (the sender). Must differ from toCwd.',
366
366
  },
367
367
  title: { type: 'string', description: 'One line naming the finding — becomes the receiving queue row' },
368
+ motivation: {
369
+ type: 'string',
370
+ description: 'WHY this is needed, in the originating human\'s terms (max 2000 chars). Lead with this — the approving human reads it first. When a request is split into a series, summarise the overall plan here.',
371
+ },
372
+ plan: {
373
+ type: 'array', items: { type: 'string' },
374
+ description: "Ordered proposed steps (max 20). REQUIRED when tag is 'feature'.",
375
+ },
376
+ acceptanceCriteria: {
377
+ type: 'array', items: { type: 'string' },
378
+ description: 'How the receiving human will know this is done (1-20 items).',
379
+ },
380
+ outOfScope: {
381
+ type: 'array', items: { type: 'string' },
382
+ description: 'Optional: what this proposal deliberately does not cover (max 20).',
383
+ },
384
+ series: {
385
+ type: 'object',
386
+ description: 'Optional: set on EVERY proposal when one request is split into several. Share the same key/title/total across them; index is this proposal\'s position.',
387
+ properties: {
388
+ key: { type: 'string', description: 'Shared kebab-case series id (max 60 chars)' },
389
+ title: { type: 'string', description: 'Shared series title (max 120 chars)' },
390
+ index: { type: 'integer', description: 'This proposal\'s 1-based position in the series' },
391
+ total: { type: 'integer', description: 'Number of proposals in the series (2-50)' },
392
+ },
393
+ required: ['key', 'title', 'index', 'total'],
394
+ },
368
395
  body: {
369
396
  type: 'string',
370
- description: 'The report itself: symptom, where observed, expected behavior, suspected cause. Self-contained — the reader cannot see your project.',
397
+ description: 'Supporting detail: evidence, symptom, where observed, references. Self-contained — the reader cannot see your project — but no longer the whole report; motivation/plan/acceptanceCriteria carry the proposal.',
371
398
  },
372
399
  tag: {
373
400
  type: 'string',
@@ -384,7 +411,7 @@ const TOOLS = [
384
411
  description: 'Optional absolute paths worth reading, appended as Reference lines',
385
412
  },
386
413
  },
387
- required: ['toCwd', 'fromCwd', 'title', 'body'],
414
+ required: ['toCwd', 'fromCwd', 'title', 'motivation', 'acceptanceCriteria', 'body'],
388
415
  },
389
416
  },
390
417
  {
@@ -163,19 +163,30 @@ async function savePersona({
163
163
  return saveOverlay({ name, projectName, model, effort, loadSessions, validatePath, writeTextAtomic });
164
164
  }
165
165
  const target = validatePath(path.join(globalDir, `${name}.md`));
166
- // The Agent Library editor's form has no `seedVersion` field, so a save
167
- // through it never passes one explicitly — carry over whatever stamp the
168
- // existing file already has rather than silently erasing it. An explicit
169
- // `seedVersion` argument (e.g. the seeder's own future callers) still wins.
166
+ // A caller that omits seedVersion/title/projects/action/actionLabel (the
167
+ // Agent Library form has no seedVersion field) must not erase what the
168
+ // existing file holds — carry each undefined one over. An explicit value,
169
+ // including `projects: []`, still wins. `effort` is deliberately not
170
+ // carried: omitted means "write no effort line".
170
171
  let resolvedSeedVersion = seedVersion;
171
- if (resolvedSeedVersion === undefined) {
172
+ let resolvedTitle = title;
173
+ let resolvedProjects = projects;
174
+ let resolvedAction = action;
175
+ let resolvedActionLabel = actionLabel;
176
+ if ([seedVersion, title, projects, action, actionLabel].some((v) => v === undefined)) {
172
177
  try {
173
- const existing = splitFrontmatter(fsSync.readFileSync(target, 'utf8'));
174
- const existingVersion = Number(existing.fm.seedVersion);
175
- if (Number.isFinite(existingVersion)) resolvedSeedVersion = existingVersion;
178
+ const { fm } = splitFrontmatter(fsSync.readFileSync(target, 'utf8'));
179
+ if (resolvedSeedVersion === undefined) {
180
+ const existingVersion = Number(fm.seedVersion);
181
+ if (Number.isFinite(existingVersion)) resolvedSeedVersion = existingVersion;
182
+ }
183
+ if (resolvedTitle === undefined && fm.title) resolvedTitle = fm.title;
184
+ if (resolvedProjects === undefined && fm.projects) resolvedProjects = parseTools(fm.projects);
185
+ if (resolvedAction === undefined && fm.action) resolvedAction = decodeLine(fm.action);
186
+ if (resolvedActionLabel === undefined && fm.actionLabel) resolvedActionLabel = decodeLine(fm.actionLabel);
176
187
  } catch { /* no existing file (or unreadable) — nothing to carry over */ }
177
188
  }
178
- const text = serializePersona({ name, description, tools, model, effort, color, tags, projects, action, actionLabel, title, seedVersion: resolvedSeedVersion, body });
189
+ const text = serializePersona({ name, description, tools, model, effort, color, tags, projects: resolvedProjects, action: resolvedAction, actionLabel: resolvedActionLabel, title: resolvedTitle, seedVersion: resolvedSeedVersion, body });
179
190
  await writeTextAtomic(target, text);
180
191
  if (originalName && originalName !== name) {
181
192
  const oldReal = validatePath(path.join(globalDir, `${originalName}.md`));
@@ -1,8 +1,8 @@
1
1
  {
2
- "version": "0.108.0",
3
- "gitSha": "6abcc4e355e214ed60fe76291081712aa98d44c2",
4
- "gitShortSha": "6abcc4e3",
2
+ "version": "0.110.0",
3
+ "gitSha": "1c3befa7cbc4d8fe34535a0ea583bffd47af11a0",
4
+ "gitShortSha": "1c3befa7",
5
5
  "gitBranch": "HEAD",
6
- "builtAt": "2026-10-09T19:19:08.109Z",
6
+ "builtAt": "2026-10-11T00:20:48.379Z",
7
7
  "dirty": false
8
8
  }
@@ -970,6 +970,12 @@ function enqueueExternalPrompt(tabId, prompt) {
970
970
  broadcast('chat:external-send', { tabId, prompt });
971
971
  }
972
972
 
973
+ // Asks the renderer to start a new Session (same launchSession a macro press
974
+ // uses) on behalf of the paired phone. Payload is pre-validated by the caller.
975
+ function requestExternalNewSession(payload) {
976
+ broadcast('chat:external-new-session', payload);
977
+ }
978
+
973
979
  /**
974
980
  * Registers the admin-HTTP entry point for enqueueExternalPrompt, mirroring
975
981
  * prdCreate.cjs's registerAdminRoute pattern (auth is the injected transport's
@@ -1050,5 +1056,6 @@ module.exports = {
1050
1056
  WARN_CEILING_MS,
1051
1057
  __setExecutor,
1052
1058
  enqueueExternalPrompt,
1059
+ requestExternalNewSession,
1053
1060
  registerAdminRoute,
1054
1061
  };
@@ -27,9 +27,9 @@ const { expandHome } = require('./lib/expandHome.cjs');
27
27
  const { listReferencedFiles } = require('./lib/importReferences.cjs');
28
28
  const atomicFs = require('./lib/atomicFs.cjs');
29
29
 
30
- /** Map<absPath, {watcher, refCount}> — one chokidar watcher per path. */
30
+ /** Map<watchKey, {watcher, refCount}> — one chokidar watcher per path (+ depth, see watchKeyOf). */
31
31
  const watchers = new Map();
32
- /** Map<senderId, Map<absPath, count>> — per-sender watch contributions for reload teardown. */
32
+ /** Map<senderId, Map<watchKey, count>> — per-sender watch contributions for reload teardown. */
33
33
  const senderWatches = new Map();
34
34
  let window = null;
35
35
 
@@ -380,26 +380,34 @@ function normalizePathKey(p) {
380
380
  return path.resolve(expandHome(p));
381
381
  }
382
382
 
383
+ /** Watcher-map key: the resolved path, plus a depth suffix for non-default (bounded) watches. */
384
+ function watchKeyOf(key, depth) {
385
+ return depth === undefined ? key : `${key}\u0000d${depth}`;
386
+ }
387
+
383
388
  /**
384
389
  * Watch one or more absolute paths. Each call increments a refcount per path.
385
390
  * Unwatch decrements; when refcount hits zero the underlying watcher is closed.
386
391
  * Fires IPC event `config:changed` with {path, mtimeMs, kind} on any change.
387
392
  */
388
- function watch(paths, senderId) {
393
+ function watch(paths, senderId, opts) {
394
+ const depth = opts?.depth;
389
395
  for (const rawPath of paths) {
390
396
  validatePath(expandHome(rawPath)); // security check — throws if out of bounds
391
397
  const key = normalizePathKey(rawPath);
398
+ const mapKey = watchKeyOf(key, depth);
392
399
  if (senderId != null) {
393
400
  if (!senderWatches.has(senderId)) senderWatches.set(senderId, new Map());
394
401
  const sm = senderWatches.get(senderId);
395
- sm.set(key, (sm.get(key) || 0) + 1);
402
+ sm.set(mapKey, (sm.get(mapKey) || 0) + 1);
396
403
  }
397
- const existing = watchers.get(key);
404
+ const existing = watchers.get(mapKey);
398
405
  if (existing) {
399
406
  existing.refCount++;
400
407
  continue;
401
408
  }
402
409
  const w = chokidar.watch(key, {
410
+ ...(depth === undefined ? {} : { depth }),
403
411
  ignoreInitial: true,
404
412
  persistent: true,
405
413
  awaitWriteFinish: { stabilityThreshold: 50, pollInterval: 25 },
@@ -420,13 +428,13 @@ function watch(paths, senderId) {
420
428
  w.on('error', (err) => {
421
429
  console.warn('[config] watcher error for', key, err.message);
422
430
  });
423
- watchers.set(key, { watcher: w, refCount: 1 });
431
+ watchers.set(mapKey, { watcher: w, refCount: 1 });
424
432
  }
425
433
  }
426
434
 
427
- function unwatch(paths, senderId) {
435
+ function unwatch(paths, senderId, opts) {
428
436
  for (const rawPath of paths) {
429
- const key = normalizePathKey(rawPath);
437
+ const key = watchKeyOf(normalizePathKey(rawPath), opts?.depth);
430
438
  if (senderId != null) {
431
439
  const sm = senderWatches.get(senderId);
432
440
  if (sm) {
@@ -489,8 +497,8 @@ function registerConfigHandlers() {
489
497
  return { ok: false, error: e.message };
490
498
  }
491
499
  }));
492
- ipcMain.on('config:watch', (e, { paths }) => { try { s.configWatch.parse(paths); watch(paths, e.sender.id); } catch (err) { logs.writeLine({ level: 'warn', scope: 'config', message: 'config:watch schema reject', meta: { error: err?.message } }); } });
493
- ipcMain.on('config:unwatch', (e, { paths }) => { try { s.configWatch.parse(paths); unwatch(paths, e.sender.id); } catch (err) { logs.writeLine({ level: 'warn', scope: 'config', message: 'config:unwatch schema reject', meta: { error: err?.message } }); } });
500
+ ipcMain.on('config:watch', (e, { paths, opts }) => { try { s.configWatch.parse(paths); s.configWatchOpts.parse(opts); watch(paths, e.sender.id, opts); } catch (err) { logs.writeLine({ level: 'warn', scope: 'config', message: 'config:watch schema reject', meta: { error: err?.message } }); } });
501
+ ipcMain.on('config:unwatch', (e, { paths, opts }) => { try { s.configWatch.parse(paths); s.configWatchOpts.parse(opts); unwatch(paths, e.sender.id, opts); } catch (err) { logs.writeLine({ level: 'warn', scope: 'config', message: 'config:unwatch schema reject', meta: { error: err?.message } }); } });
494
502
  }
495
503
 
496
504
  module.exports = {
@@ -498,6 +506,8 @@ module.exports = {
498
506
  registerConfigHandlers,
499
507
  closeAllWatchers,
500
508
  releaseWatchesForSender,
509
+ watch,
510
+ unwatch,
501
511
  // exported for tests / direct use
502
512
  readJson,
503
513
  readText,
@@ -6,6 +6,7 @@ const path = require('node:path');
6
6
  const os = require('node:os');
7
7
  const { schemas } = require('./ipcSchemas.cjs');
8
8
  const historyRollup = require('./lib/historyRollup.cjs');
9
+ const { sendIfAlive } = require('./lib/sendToRenderer.cjs');
9
10
 
10
11
  const PROJECTS_DIR = path.join(os.homedir(), '.claude', 'projects');
11
12
  const PARSE_BUDGET_MS = 2_000;
@@ -52,6 +53,47 @@ function resolvePricingKey(modelId) {
52
53
  // Shared implementation: lib/lruCache.cjs (re-exported below for existing callers).
53
54
  const { LRUCache } = require('./lib/lruCache.cjs');
54
55
 
56
+ // ── history:updated broadcast ─────────────────────────────────────────────────
57
+ // Open History pages re-fetch on this event. Throttled leading+trailing so a
58
+ // backfill burst announces its first and last write only.
59
+ const HISTORY_UPDATED_THROTTLE_MS = 5000;
60
+ let historyNotifyTimer = null;
61
+ let historyNotifyPending = false;
62
+
63
+ function broadcastHistoryUpdated() {
64
+ try {
65
+ const { BrowserWindow } = require('electron');
66
+ for (const win of BrowserWindow.getAllWindows()) sendIfAlive(win, 'history:updated', {});
67
+ } catch {
68
+ // Never let a failed broadcast break aggregation.
69
+ }
70
+ }
71
+
72
+ function notifyHistoryUpdated() {
73
+ if (historyNotifyTimer) {
74
+ historyNotifyPending = true;
75
+ return;
76
+ }
77
+ broadcastHistoryUpdated();
78
+ const arm = () => {
79
+ historyNotifyTimer = setTimeout(() => {
80
+ historyNotifyTimer = null;
81
+ if (historyNotifyPending) {
82
+ historyNotifyPending = false;
83
+ broadcastHistoryUpdated();
84
+ arm();
85
+ }
86
+ }, HISTORY_UPDATED_THROTTLE_MS);
87
+ if (historyNotifyTimer.unref) historyNotifyTimer.unref();
88
+ };
89
+ arm();
90
+ }
91
+
92
+ async function appendRollupAndNotify(entries) {
93
+ await historyRollup.appendRollupDays(entries);
94
+ if (entries.length) notifyHistoryUpdated();
95
+ }
96
+
55
97
  /**
56
98
  * Cache for parseJSONL results.
57
99
  * Entry shape: { mtimeMs: number, size: number, readOffset: number, inode: number, result: AggrResult }
@@ -574,7 +616,7 @@ async function finalizeClosedDays({ budgetMs, dryRun = false } = {}) {
574
616
  }
575
617
 
576
618
  if (!dryRun) {
577
- await historyRollup.appendRollupDays(entries);
619
+ await appendRollupAndNotify(entries);
578
620
  }
579
621
  return { finalizedDates: partial ? [] : Array.from(dates), partial };
580
622
  }
@@ -741,7 +783,7 @@ async function refreshIntradayToday() {
741
783
 
742
784
  const entries = [];
743
785
  for (const b of buckets.values()) entries.push(...bucketToRollupLines(b));
744
- if (entries.length) await historyRollup.appendRollupDays(entries);
786
+ if (entries.length) await appendRollupAndNotify(entries);
745
787
 
746
788
  return { date: today, projectsUpdated: buckets.size, strategy: INTRADAY_STRATEGY };
747
789
  }
@@ -859,7 +901,7 @@ async function aggregate(req) {
859
901
  if (b.date < today) backfillEntries.push(...bucketToRollupLines(b));
860
902
  }
861
903
  if (backfillEntries.length) {
862
- await historyRollup.appendRollupDays(backfillEntries);
904
+ await appendRollupAndNotify(backfillEntries);
863
905
  }
864
906
 
865
907
  const rollupRows = rollupMapToRows(rollupMap, finalizedDates);
@@ -951,6 +993,7 @@ module.exports = {
951
993
  finalizeClosedDays,
952
994
  decodeCwd,
953
995
  refreshIntradayToday,
996
+ appendRollupAndNotify,
954
997
  computeIntradayBuckets,
955
998
  // exported for tests
956
999
  scanAggrLines,
@@ -129,6 +129,8 @@ const heapSnapshot = require('./heapSnapshot.cjs');
129
129
  const filesIpc = require('./files.cjs');
130
130
  const { registerDocEditHandlers, attachWindow: attachDocEditWindow } = require('./docEdit.cjs');
131
131
  const webRemote = require('./webRemote.cjs');
132
+ const { registerRemoteClientHandlers } = require('./remoteClientIpc.cjs');
133
+ const { registerUpdaterIpc } = require('./updaterIpc.cjs');
132
134
  const { listExchanges } = require('./exchanges.cjs');
133
135
  const { resolveClaudeBin } = require('./lib/claudeBin.cjs');
134
136
  const { checkInsideHome, assertInsideHome } = require('./lib/insideHome.cjs');
@@ -1098,6 +1100,12 @@ git.register(ipcMain);
1098
1100
  filesIpc.registerFilesHandlers();
1099
1101
  registerDocEditHandlers();
1100
1102
  webRemote.registerRemoteHandlers();
1103
+ registerRemoteClientHandlers(() => mainWindow);
1104
+ try {
1105
+ registerUpdaterIpc({ ipcMain, getWindow: () => mainWindow, ptyManager });
1106
+ } catch (e) {
1107
+ logs.writeLine({ scope: 'main', level: 'error', message: 'updater IPC registration failed', meta: { error: e?.message } });
1108
+ }
1101
1109
  chatRunner.registerChatHandlers();
1102
1110
 
1103
1111
  // Direct in-process PRD authoring for a queued PromptTicket classified
@@ -139,6 +139,7 @@ const configListDir = z.object({
139
139
  });
140
140
 
141
141
  const configWatch = z.array(z.string().min(1).max(4096));
142
+ const configWatchOpts = z.object({ depth: z.number().int().min(0).max(2) }).strict().optional();
142
143
 
143
144
  const configParseImports = z.object({ path: z.string().min(1).max(4096) });
144
145
 
@@ -943,6 +944,19 @@ const chatExternalSend = z.object({
943
944
  ),
944
945
  });
945
946
 
947
+ // Phone-initiated new Session. cwd/agentName are re-checked against the open
948
+ // tabs / persona list in webRemote.cjs before anything touches the filesystem.
949
+ const remoteSessionCreate = z.object({
950
+ cwd: z.string().min(1).max(4096),
951
+ agentName: z.string().regex(/^[A-Za-z0-9._-]{1,64}$/),
952
+ tag: EpicTagSchema,
953
+ title: z.string().min(1).max(120),
954
+ prompt: z.string().min(1).refine(
955
+ (s) => Buffer.byteLength(s, 'utf8') <= CHAT_PROMPT_MAX_BYTES,
956
+ `prompt must be ≤ ${CHAT_PROMPT_MAX_BYTES} bytes`,
957
+ ),
958
+ });
959
+
946
960
  // ──────────────────────────────────────────── Web Remote
947
961
  // OTP is 8 uppercase alphanumeric chars (case-insensitive entry, normalised to upper in handler).
948
962
  const WEB_REMOTE_OTP_RE = /^[A-Z0-9]{8}$/i;
@@ -956,6 +970,11 @@ const webRemoteRevokeDevice = z.object({
956
970
  deviceId: z.string().regex(DEVICE_ID_RE),
957
971
  }).strict();
958
972
 
973
+ // Optional payload: omit (undefined) to confirm the single pending client.
974
+ const webRemoteConfirmSas = z.object({
975
+ clientKey: z.string().max(64).optional(),
976
+ }).strict().optional();
977
+
959
978
  const webRemoteAuditTail = z.object({
960
979
  lines: z.number().int().min(1).max(500).optional(),
961
980
  }).strict();
@@ -1136,9 +1155,11 @@ function validated(schema, handler) {
1136
1155
  //
1137
1156
  // Ungated READ_COMMANDS (justify each):
1138
1157
  // cmd:app:version — exposes only the app semver string; no user data.
1158
+ // cmd:app:info — exposes only app semver + OS platform string; no user data.
1139
1159
  // cmd:session:unsubscribe — teardown lifecycle; returns nothing sensitive.
1140
1160
  const READ_COMMANDS = new Set([
1141
1161
  'cmd:app:version',
1162
+ 'cmd:app:info',
1142
1163
  // v2 mobile: unsubscribe is a teardown lifecycle call with no data payload.
1143
1164
  'cmd:session:unsubscribe',
1144
1165
  ]);
@@ -1146,9 +1167,13 @@ const READ_COMMANDS = new Set([
1146
1167
  // Sensitive reads — return user data; require SAS confirmation same as MUTATE.
1147
1168
  const SAS_GATED_READS = new Set([
1148
1169
  'cmd:sessions:load',
1170
+ // Same data as the event:session:list push (projects + their active Epics).
1171
+ 'cmd:sessions:list',
1149
1172
  'cmd:history:aggregate',
1150
1173
  // subscribe initiates a live stream of session state/summary — sensitive.
1151
1174
  'cmd:session:subscribe',
1175
+ // Picker data (projects, personas, tags, macros) for the phone's New Session.
1176
+ 'cmd:session:options',
1152
1177
  // NOTE: cmd:exchanges:list is intentionally NOT allowlisted — webRemote.cjs has
1153
1178
  // no dispatch handler for it, so an allowlist entry would only fail closed with
1154
1179
  // an opaque reject. Re-add here together with the handler when remote exchanges
@@ -1174,6 +1199,8 @@ const MUTATE_COMMANDS = new Set([
1174
1199
  // Pushes a prompt into an open tab's chat queue — a real mutation of that
1175
1200
  // tab's session, same tier as pty:write.
1176
1201
  'cmd:chat:send',
1202
+ // Starts a new Session on the desktop via the renderer's launchSession.
1203
+ 'cmd:session:create',
1177
1204
  ]);
1178
1205
 
1179
1206
  const ALLOWED_COMMANDS = new Set([...READ_COMMANDS, ...SAS_GATED_READS, ...MUTATE_COMMANDS]);
@@ -1192,6 +1219,7 @@ module.exports = {
1192
1219
  webRemotePair,
1193
1220
  webRemoteRevokeDevice,
1194
1221
  webRemoteAuditTail,
1222
+ webRemoteConfirmSas,
1195
1223
  sessionSubscribe,
1196
1224
  ptySpawn,
1197
1225
  ptyTabId,
@@ -1211,6 +1239,7 @@ module.exports = {
1211
1239
  configWriteText,
1212
1240
  configListDir,
1213
1241
  configWatch,
1242
+ configWatchOpts,
1214
1243
  configParseImports,
1215
1244
  sessionsPayload,
1216
1245
  layoutEnvelope,
@@ -1282,6 +1311,7 @@ module.exports = {
1282
1311
  chatCancel,
1283
1312
  chatClassifyTicket,
1284
1313
  chatExternalSend,
1314
+ remoteSessionCreate,
1285
1315
  exchangesList,
1286
1316
  agentsSavePersona,
1287
1317
  macrosSave,
@@ -0,0 +1,190 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * autoUpdater.cjs — pure, dependency-injected state machine around electron-updater's autoUpdater.
5
+ *
6
+ * Requires neither electron nor electron-updater: the caller injects the updater instance. The feed is
7
+ * whatever app-update.yml electron-builder baked into the installer; this module never supplies a feed URL.
8
+ * Checks on start() and every 4h, downloads in the background, and installs ONLY after requestInstall() —
9
+ * deferring (polling isBusy every 60s) while scheduler jobs or terminals are busy.
10
+ */
11
+
12
+ const CHECK_INTERVAL_MS = 4 * 60 * 60 * 1000;
13
+ const BUSY_POLL_MS = 60 * 1000;
14
+
15
+ /** Unsigned mac builds cannot use Squirrel.Mac, so darwin is always unsupported. */
16
+ function isAutoUpdateSupported({ packaged, platform, env }) {
17
+ const e = env || {};
18
+ if (packaged !== true) return false;
19
+ if (e.SM_AUTO_UPDATE === '0' || e.SM_E2E === '1') return false;
20
+ if (platform === 'win32') return true;
21
+ return platform === 'linux' && typeof e.APPIMAGE === 'string' && e.APPIMAGE.length > 0;
22
+ }
23
+
24
+ function createAutoUpdater({
25
+ updater, supported, currentVersion, isBusy, getEnabled,
26
+ setInterval: setIntervalFn = setInterval, clearInterval: clearIntervalFn = clearInterval,
27
+ now = Date.now, log = () => {},
28
+ }) {
29
+ let enabledOverride = null;
30
+ let started = false;
31
+ let listening = false;
32
+ let checkTimer = null;
33
+ let pollTimer = null;
34
+ const subscribers = new Set();
35
+
36
+ const isEnabled = () => {
37
+ if (enabledOverride !== null) return enabledOverride;
38
+ return typeof getEnabled === 'function' ? getEnabled() !== false : true;
39
+ };
40
+
41
+ let snapshot = {
42
+ state: supported ? 'idle' : 'unsupported',
43
+ current: currentVersion,
44
+ latest: null,
45
+ progress: null,
46
+ error: null,
47
+ busyReason: null,
48
+ lastCheckedAt: null,
49
+ };
50
+
51
+ function setState(patch) {
52
+ const next = { ...snapshot, ...patch };
53
+ if (Object.keys(next).every((k) => next[k] === snapshot[k])) return;
54
+ snapshot = next;
55
+ for (const fn of [...subscribers]) {
56
+ try { fn(snapshot); } catch (e) { log('autoUpdater subscriber threw', e); }
57
+ }
58
+ }
59
+
60
+ const holding = () => snapshot.state === 'ready' || snapshot.state === 'deferred';
61
+
62
+ function fail(e) {
63
+ const message = e && e.message ? e.message : String(e);
64
+ log('autoUpdater error', message);
65
+ setState({ state: 'error', error: message, progress: null });
66
+ }
67
+
68
+ function clearPoll() {
69
+ if (pollTimer !== null) { clearIntervalFn(pollTimer); pollTimer = null; }
70
+ }
71
+
72
+ function clearChecks() {
73
+ if (checkTimer !== null) { clearIntervalFn(checkTimer); checkTimer = null; }
74
+ }
75
+
76
+ function attach() {
77
+ if (listening) return;
78
+ listening = true;
79
+ updater.autoDownload = true;
80
+ updater.autoInstallOnAppQuit = true;
81
+ updater.allowPrerelease = false;
82
+ updater.allowDowngrade = false;
83
+ updater.on('checking-for-update', () => {
84
+ if (!holding()) setState({ state: 'checking', error: null });
85
+ });
86
+ updater.on('update-available', (info) => {
87
+ if (!holding()) setState({ state: 'downloading', latest: info && info.version ? info.version : null, progress: null, error: null });
88
+ });
89
+ updater.on('download-progress', (p) => {
90
+ if (!holding()) setState({ state: 'downloading', progress: p && typeof p.percent === 'number' ? p.percent : null });
91
+ });
92
+ updater.on('update-downloaded', (info) => {
93
+ setState({ state: 'ready', latest: info && info.version ? info.version : snapshot.latest, progress: 100, error: null, busyReason: null });
94
+ });
95
+ updater.on('update-not-available', () => {
96
+ if (!holding()) setState({ state: 'idle', latest: null, progress: null, error: null });
97
+ });
98
+ updater.on('error', (e) => {
99
+ if (!holding()) fail(e);
100
+ });
101
+ }
102
+
103
+ async function checkNow() {
104
+ if (!supported || !isEnabled()) return;
105
+ if (holding() || snapshot.state === 'checking' || snapshot.state === 'downloading') return;
106
+ attach();
107
+ setState({ state: 'checking', error: null, lastCheckedAt: now() });
108
+ try {
109
+ await updater.checkForUpdates();
110
+ } catch (e) {
111
+ fail(e);
112
+ }
113
+ }
114
+
115
+ function startChecks() {
116
+ clearChecks();
117
+ checkTimer = setIntervalFn(() => { checkNow(); }, CHECK_INTERVAL_MS);
118
+ if (checkTimer && typeof checkTimer.unref === 'function') checkTimer.unref();
119
+ checkNow();
120
+ }
121
+
122
+ function applyEnabled() {
123
+ if (!supported) return;
124
+ if (!isEnabled()) {
125
+ clearChecks();
126
+ clearPoll();
127
+ setState({ state: 'disabled', busyReason: null, progress: null, error: null });
128
+ return;
129
+ }
130
+ if (snapshot.state === 'disabled') setState({ state: 'idle' });
131
+ if (started) startChecks();
132
+ }
133
+
134
+ function start() {
135
+ if (!supported || started) return;
136
+ started = true;
137
+ applyEnabled();
138
+ }
139
+
140
+ function stop() {
141
+ started = false;
142
+ clearChecks();
143
+ clearPoll();
144
+ }
145
+
146
+ function setEnabled(value) {
147
+ enabledOverride = !!value;
148
+ applyEnabled();
149
+ }
150
+
151
+ function install() {
152
+ clearPoll();
153
+ try {
154
+ updater.quitAndInstall(false, true);
155
+ } catch (e) {
156
+ fail(e);
157
+ }
158
+ }
159
+
160
+ function requestInstall({ force } = {}) {
161
+ if (!supported || !holding()) return;
162
+ if (snapshot.state === 'deferred' && !force) return;
163
+ if (force) { install(); return; }
164
+ const b = isBusy ? isBusy() : { busy: false };
165
+ if (!b || !b.busy) { install(); return; }
166
+ setState({ state: 'deferred', busyReason: b.reason || null });
167
+ clearPoll();
168
+ pollTimer = setIntervalFn(() => {
169
+ const cur = isBusy ? isBusy() : { busy: false };
170
+ if (cur && cur.busy) { setState({ busyReason: cur.reason || null }); return; }
171
+ install();
172
+ }, BUSY_POLL_MS);
173
+ if (pollTimer && typeof pollTimer.unref === 'function') pollTimer.unref();
174
+ }
175
+
176
+ function cancelDeferred() {
177
+ if (snapshot.state !== 'deferred') return;
178
+ clearPoll();
179
+ setState({ state: 'ready', busyReason: null });
180
+ }
181
+
182
+ function subscribe(fn) {
183
+ subscribers.add(fn);
184
+ return () => { subscribers.delete(fn); };
185
+ }
186
+
187
+ return { start, stop, checkNow, requestInstall, cancelDeferred, setEnabled, status: () => snapshot, subscribe };
188
+ }
189
+
190
+ module.exports = { isAutoUpdateSupported, createAutoUpdater, CHECK_INTERVAL_MS, BUSY_POLL_MS };