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.
- package/dist/assets/AgentLibrary-Cm6MrOnF.js +2 -0
- package/dist/assets/{Button-DC6VzovQ.js → Button-CGVncJ6a.js} +1 -1
- package/dist/assets/DataModel-C3hlMoa8.js +1 -0
- package/dist/assets/History-CTyfGNKn.js +2 -0
- package/dist/assets/Hooks-BXxCBibx.js +3 -0
- package/dist/assets/{Library-DLmnlfQt.js → Library-aCs7zxAg.js} +24 -24
- package/dist/assets/{ListDetail-YylbvUj_.js → ListDetail-B9Hqk1_f.js} +1 -1
- package/dist/assets/{MarkdownEditor-DMPKWHTF.js → MarkdownEditor-Djfgp32k.js} +1 -1
- package/dist/assets/{McpServers-Da-Ty5nc.js → McpServers-8HQC6QgR.js} +2 -2
- package/dist/assets/{Memory-C3rUQ_cg.js → Memory-c4SVE3l2.js} +5 -5
- package/dist/assets/{Panel-CC3JTnOI.js → Panel-BEQTwE4z.js} +1 -1
- package/dist/assets/Permissions-BaYzgZtc.js +3 -0
- package/dist/assets/Plugins-DnXsVau7.js +2 -0
- package/dist/assets/ProvenanceBadge-CT2vWps1.js +1 -0
- package/dist/assets/RemoteMachines-DHJZfdJC.js +3 -0
- package/dist/assets/{SaveBar-C06rFT34.js → SaveBar-DwcHt3iR.js} +1 -1
- package/dist/assets/Scheduler-D6Gcuo0M.js +16 -0
- package/dist/assets/{ScopeSwitcher-Cp5mxblN.js → ScopeSwitcher-CgvWDc9Y.js} +1 -1
- package/dist/assets/Settings-CJoogVAB.js +4 -0
- package/dist/assets/{SkillReferenceGraph-DuWRmT_f.js → SkillReferenceGraph-4fq01kxq.js} +14 -14
- package/dist/assets/Skills-BU4ZX4h4.js +2 -0
- package/dist/assets/SystemPrompt-BCEyMSEq.js +1 -0
- package/dist/assets/TagLibrary-m3It8Oal.js +1 -0
- package/dist/assets/{TiptapBody-1r4ODLJ7.js → TiptapBody-DVyBsfsT.js} +1 -1
- package/dist/assets/{Toggle-BGT68XHP.js → Toggle-In_AExPV.js} +1 -1
- package/dist/assets/{ViewTabs-T0NBc8uv.js → ViewTabs-BLHjKUv-.js} +1 -1
- package/dist/assets/historyTotals-BXDd3EzP.js +1 -0
- package/dist/assets/index-CuiU3I-8.js +3077 -0
- package/dist/assets/{index-tr1w3hHB.css → index-Dutcsly-.css} +1 -1
- package/dist/assets/listSkills-C1aWKf0T.js +1 -0
- package/dist/assets/{settingsSchema-CvoNk3Ye.js → settingsSchema-DD4bc5cT.js} +1 -1
- package/dist/assets/useAgentsDirWatch-DMQ_XZtr.js +1 -0
- package/dist/assets/useRemotePeers-C9NfteSD.js +1 -0
- package/dist/index.html +2 -2
- package/package.json +6 -2
- package/plugins/session-manager-dev/skills/develop/SKILL.md +3 -1
- package/plugins/session-manager-dev/skills/develop/standards.md +5 -0
- package/plugins/session-manager-dev/skills/send-feedback/SKILL.md +48 -15
- package/scripts/README.md +7 -0
- package/scripts/scheduler-mcp-server.cjs +29 -2
- package/src/main/agentLibrary.cjs +20 -9
- package/src/main/build-info.json +4 -4
- package/src/main/chatRunner.cjs +7 -0
- package/src/main/config.cjs +20 -10
- package/src/main/historyAggregator.cjs +46 -3
- package/src/main/index.cjs +8 -0
- package/src/main/ipcSchemas.cjs +30 -0
- package/src/main/lib/autoUpdater.cjs +190 -0
- package/src/main/lib/crossProjectFeedback.cjs +143 -6
- package/src/main/lib/mcpToolCatalog.cjs +12 -3
- package/src/main/lib/reaperHelpers.cjs +13 -1
- package/src/main/lib/relayClientAuth.cjs +133 -0
- package/src/main/lib/remoteClientSessions.cjs +84 -0
- package/src/main/lib/remoteE2eCrypto.cjs +109 -0
- package/src/main/lib/updaterSettings.cjs +64 -0
- package/src/main/pty.cjs +53 -0
- package/src/main/remoteClient.cjs +334 -0
- package/src/main/remoteClientIpc.cjs +133 -0
- package/src/main/scheduler.cjs +16 -0
- package/src/main/updaterIpc.cjs +110 -0
- package/src/main/webRemote.cjs +341 -215
- package/src/preload/api.d.ts +97 -7
- package/src/preload/index.cjs +51 -3
- package/dist/assets/AgentLibrary-Co54GThg.js +0 -2
- package/dist/assets/DataModel-DZ0vyJau.js +0 -1
- package/dist/assets/History-Df3YwpPe.js +0 -2
- package/dist/assets/Hooks-DplG6roH.js +0 -3
- package/dist/assets/Permissions-XvBiMj1-.js +0 -3
- package/dist/assets/Plugins-CzHY1mgS.js +0 -2
- package/dist/assets/ProvenanceBadge-DaJIWVcg.js +0 -1
- package/dist/assets/Scheduler-BehadU-0.js +0 -16
- package/dist/assets/Settings-Cg9LIni0.js +0 -4
- package/dist/assets/Skills-CMmCZfIv.js +0 -3
- package/dist/assets/SystemPrompt-D_-MV025.js +0 -1
- package/dist/assets/TagLibrary-D9Tv3yOJ.js +0 -1
- package/dist/assets/index-3_AtCAyd.js +0 -3077
- package/dist/assets/listSkills-QORduPIk.js +0 -1
- package/dist/assets/useAgentsDirWatch-1C2CBBya.js +0 -1
- 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: '
|
|
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
|
-
//
|
|
167
|
-
//
|
|
168
|
-
// existing file
|
|
169
|
-
// `
|
|
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
|
-
|
|
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
|
|
174
|
-
|
|
175
|
-
|
|
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`));
|
package/src/main/build-info.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
3
|
-
"gitSha": "
|
|
4
|
-
"gitShortSha": "
|
|
2
|
+
"version": "0.110.0",
|
|
3
|
+
"gitSha": "1c3befa7cbc4d8fe34535a0ea583bffd47af11a0",
|
|
4
|
+
"gitShortSha": "1c3befa7",
|
|
5
5
|
"gitBranch": "HEAD",
|
|
6
|
-
"builtAt": "2026-10-
|
|
6
|
+
"builtAt": "2026-10-11T00:20:48.379Z",
|
|
7
7
|
"dirty": false
|
|
8
8
|
}
|
package/src/main/chatRunner.cjs
CHANGED
|
@@ -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
|
};
|
package/src/main/config.cjs
CHANGED
|
@@ -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<
|
|
30
|
+
/** Map<watchKey, {watcher, refCount}> — one chokidar watcher per path (+ depth, see watchKeyOf). */
|
|
31
31
|
const watchers = new Map();
|
|
32
|
-
/** Map<senderId, Map<
|
|
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(
|
|
402
|
+
sm.set(mapKey, (sm.get(mapKey) || 0) + 1);
|
|
396
403
|
}
|
|
397
|
-
const existing = watchers.get(
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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,
|
package/src/main/index.cjs
CHANGED
|
@@ -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
|
package/src/main/ipcSchemas.cjs
CHANGED
|
@@ -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 };
|