claude-code-session-manager 0.122.0 → 0.123.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 (93) hide show
  1. package/dist/assets/{AgentLibrary-BmJ8hIPw.js → AgentLibrary-DxWN3FSN.js} +2 -2
  2. package/dist/assets/{Button-BS4kmz4p.js → Button-CISLFSKb.js} +1 -1
  3. package/dist/assets/DataModel-CYPbhGrx.js +1 -0
  4. package/dist/assets/{History-pg00b2Gd.js → History-Fhouektw.js} +2 -2
  5. package/dist/assets/{Hooks-BTEs0UZY.js → Hooks-COfIcyYV.js} +3 -3
  6. package/dist/assets/{Library-DuaJ-VSn.js → Library-Bt5WOyyu.js} +7 -7
  7. package/dist/assets/ListDetail-DH7uzYd5.js +1 -0
  8. package/dist/assets/{McpServers-Dy-wnG_X.js → McpServers-CwyVEzMN.js} +2 -2
  9. package/dist/assets/{Memory-Bs1T1Jo7.js → Memory-k9TWT9N4.js} +8 -8
  10. package/dist/assets/{Panel-8UhLXEOW.js → Panel-Di8JqCYN.js} +1 -1
  11. package/dist/assets/{Permissions-BfwzvmBS.js → Permissions-FFs6Rq1H.js} +3 -3
  12. package/dist/assets/{Plugins-McjyvvKh.js → Plugins-uK5vl_RE.js} +2 -2
  13. package/dist/assets/{ProvenanceBadge-D0Ov_6WJ.js → ProvenanceBadge-CGDyGDoN.js} +1 -1
  14. package/dist/assets/{SaveBar-Cd6hJR6K.js → SaveBar-Cnwc_XfF.js} +1 -1
  15. package/dist/assets/{Scheduler-BZXKqYJP.js → Scheduler-aACa6_mC.js} +7 -7
  16. package/dist/assets/{ScopeSwitcher-C24ysV1K.js → ScopeSwitcher-CoG3CSlC.js} +1 -1
  17. package/dist/assets/Settings-BG0RM6Di.js +4 -0
  18. package/dist/assets/{SkillReferenceGraph-BsWuiTbw.js → SkillReferenceGraph-COffGu_A.js} +1 -1
  19. package/dist/assets/Skills--qQpFjjf.js +2 -0
  20. package/dist/assets/SystemPrompt-Cr9RRSiZ.js +1 -0
  21. package/dist/assets/TagLibrary-D_ZbXwBv.js +1 -0
  22. package/dist/assets/{TiptapBody-C4kIvIMH.js → TiptapBody-CCHsa8Jv.js} +1 -1
  23. package/dist/assets/{Toggle-pP19zlyk.js → Toggle-BUOj4pws.js} +1 -1
  24. package/dist/assets/{ViewTabs-CpzN8e5I.js → ViewTabs-BdpsL7yr.js} +1 -1
  25. package/dist/assets/{historyTotals-Caq78bb5.js → historyTotals-Dx6ca881.js} +1 -1
  26. package/dist/assets/{index-Dd7UQabV.css → index-BnKaYERU.css} +1 -1
  27. package/dist/assets/{index-Bjtguv6I.js → index-CkNGj8FA.js} +151 -151
  28. package/dist/assets/{settingsSchema-CC2ySu3d.js → settingsSchema-BZhkpr9e.js} +1 -1
  29. package/dist/assets/{skillFrontmatter-hDhNo8jE.js → skillFrontmatter-N13KGqbs.js} +1 -1
  30. package/dist/assets/{useAgentsDirWatch-BuOtY25U.js → useAgentsDirWatch-De9UsbDt.js} +1 -1
  31. package/dist/index.html +2 -2
  32. package/package.json +1 -1
  33. package/plugins/session-manager-dev/skills/develop/SKILL.md +29 -24
  34. package/scripts/scheduler-mcp-server.cjs +142 -78
  35. package/src/main/agentLibrary.cjs +8 -3
  36. package/src/main/build-info.json +4 -4
  37. package/src/main/crashDiagnostics.cjs +24 -0
  38. package/src/main/historyAggregator.cjs +41 -7
  39. package/src/main/index.cjs +13 -3
  40. package/src/main/ipcSchemas.cjs +30 -22
  41. package/src/main/lib/activeSessions.cjs +93 -32
  42. package/src/main/lib/epic/delegationReadiness.cjs +2 -0
  43. package/src/main/lib/epic/delegationSelfHeal.cjs +77 -0
  44. package/src/main/lib/epic/epicMint.cjs +1 -1
  45. package/src/main/lib/epic/epicStandingInstructions.cjs +73 -0
  46. package/src/main/lib/epic/guardSpawnSettings.cjs +76 -0
  47. package/src/main/lib/feedback-remote/crossProjectFeedback.cjs +9 -2
  48. package/src/main/lib/feedback-remote/remoteClientSessions.cjs +3 -11
  49. package/src/main/lib/feedback-remote/remoteE2eCrypto.cjs +1 -1
  50. package/src/main/lib/git/gitWorktree.cjs +9 -4
  51. package/src/main/lib/git/worktreeDeps.cjs +13 -2
  52. package/src/main/lib/git/worktreeSetup.cjs +212 -0
  53. package/src/main/lib/mcpToolCatalog.cjs +36 -9
  54. package/src/main/lib/persona-macro/agentPersonaSchema.cjs +4 -0
  55. package/src/main/lib/persona-macro/shippedPersonaSeeds.cjs +23 -0
  56. package/src/main/lib/prd/prdAgentType.cjs +28 -0
  57. package/src/main/lib/prd/prdBodyPatch.cjs +138 -0
  58. package/src/main/lib/prd/prdCreate.cjs +245 -27
  59. package/src/main/lib/prd/prdGateFiles.cjs +31 -0
  60. package/src/main/lib/scheduler/definitionOfDone.cjs +24 -3
  61. package/src/main/lib/scheduler/queueStore.cjs +5 -4
  62. package/src/main/lib/scheduler/scheduleJobSchema.cjs +1 -0
  63. package/src/main/lib/scheduler/schedulerBatch.cjs +43 -5
  64. package/src/main/lib/scheduler/schedulerPaths.cjs +2 -0
  65. package/src/main/lib/scheduler/watchdogHelpers.cjs +114 -22
  66. package/src/main/lib/telemetry/mainHeapSnapshot.cjs +69 -0
  67. package/src/main/lib/telemetry/telemetryBoot.cjs +25 -3
  68. package/src/main/lib/telemetry/telemetryClient.cjs +12 -0
  69. package/src/main/lib/telemetry/telemetryCounters.cjs +25 -2
  70. package/src/main/pty.cjs +0 -53
  71. package/src/main/runVerify.cjs +38 -11
  72. package/src/main/scheduler/prdParser.cjs +3 -0
  73. package/src/main/scheduler/prdPaths.cjs +48 -2
  74. package/src/main/scheduler.cjs +135 -31
  75. package/src/main/seedDevPlugin.cjs +1 -1
  76. package/src/main/seedSchedulerMcp.cjs +1 -1
  77. package/src/main/templates/PRD_AUTHORING.md +8 -4
  78. package/src/main/webRemote.cjs +38 -132
  79. package/src/preload/api.d.ts +4 -52
  80. package/src/preload/index.cjs +0 -24
  81. package/src/seed/agents/dev-lead.md +2 -1
  82. package/src/seed/agents/validator.md +2 -1
  83. package/dist/assets/DataModel-DZHBZrfL.js +0 -1
  84. package/dist/assets/ListDetail-Cud72s-O.js +0 -1
  85. package/dist/assets/RemoteMachines-gOlFTvWq.js +0 -3
  86. package/dist/assets/Settings-DWnN2qxn.js +0 -4
  87. package/dist/assets/Skills-6pHn6glu.js +0 -2
  88. package/dist/assets/SystemPrompt-DT0LBH-K.js +0 -1
  89. package/dist/assets/TagLibrary-DbJ0zbYL.js +0 -1
  90. package/dist/assets/useRemotePeers-CNQmRBpt.js +0 -1
  91. package/src/main/lib/feedback-remote/relayClientAuth.cjs +0 -133
  92. package/src/main/remoteClient.cjs +0 -334
  93. package/src/main/remoteClientIpc.cjs +0 -133
@@ -1,334 +0,0 @@
1
- 'use strict';
2
-
3
- // Desktop-to-desktop remote: the CLIENT role. Drives a paired Session Manager
4
- // agent (webRemote.cjs) over the relay using the same envelope protocol the
5
- // phone app speaks (web/remote-app/src/ws.ts). No Electron imports — IPC wiring
6
- // lives elsewhere.
7
-
8
- const crypto = require('node:crypto');
9
- const path = require('node:path');
10
- const { claudeHome } = require('./lib/scheduler/schedulerPaths.cjs');
11
- const { writeJsonAtomicSync, readJsonOrSync } = require('./lib/util/atomicFs.cjs');
12
- const { generateE2EKeyPair, deriveSessionKey, deriveSas, encryptBox, decryptBox } = require('./lib/feedback-remote/remoteE2eCrypto.cjs');
13
- const { ALLOWED_COMMANDS } = require('./ipcSchemas.cjs');
14
- const { relayClientAuth } = require('./lib/feedback-remote/relayClientAuth.cjs');
15
-
16
- // Hard-coded wss:// — same relay URL as webRemote.cjs, no plaintext downgrade.
17
- const RELAY_WSS_URL = 'wss://bilko.run/projects/session-manager/relay';
18
- const SEND_TIMEOUT_MS = 15_000;
19
- const WS_OPEN = 1;
20
-
21
- // The relay closes a revoked desktop client's socket with this code.
22
- const CLOSE_TOKEN_REVOKED = 4001;
23
- // Relay errors that end the handshake with a reason the UI can explain.
24
- const FATAL_RELAY_ERRORS = new Set(['device_offline', 'not_your_device']);
25
-
26
- function defaultWsFactory(url) {
27
- const WebSocket = require('ws');
28
- return new WebSocket(url, { rejectUnauthorized: true });
29
- }
30
-
31
- function defaultConfigPath() {
32
- return path.join(claudeHome(), 'session-manager', 'remote-peers.json');
33
- }
34
-
35
- function createRemoteClient({ relayAuth = relayClientAuth, wsFactory = defaultWsFactory, configPath } = {}) {
36
- const file = configPath || defaultConfigPath();
37
- /** @type {Map<string, any>} peerId -> runtime state */
38
- const runtime = new Map();
39
- /** @type {Map<string, Set<Function>>} */
40
- const subscribers = new Map();
41
-
42
- function loadPeers() {
43
- const data = readJsonOrSync(file, { peers: [] });
44
- return Array.isArray(data?.peers) ? data.peers : [];
45
- }
46
- function savePeers(peers) {
47
- writeJsonAtomicSync(file, { peers }, { mode: 0o600 });
48
- }
49
- function getPeer(peerId) {
50
- return loadPeers().find((p) => p.peerId === peerId) || null;
51
- }
52
- function updatePeer(peerId, patch) {
53
- const peers = loadPeers();
54
- const i = peers.findIndex((p) => p.peerId === peerId);
55
- if (i < 0) return;
56
- peers[i] = { ...peers[i], ...patch };
57
- savePeers(peers);
58
- }
59
-
60
- function rt(peerId) {
61
- let s = runtime.get(peerId);
62
- if (!s) {
63
- s = {
64
- status: 'idle', reason: null, sas: null, ws: null, sessionKey: null,
65
- devicePubKey: null, pending: new Map(), attempt: 0, osRequested: false,
66
- };
67
- runtime.set(peerId, s);
68
- }
69
- return s;
70
- }
71
-
72
- function failPending(s, err) {
73
- for (const p of s.pending.values()) { clearTimeout(p.timer); p.reject(err); }
74
- s.pending.clear();
75
- }
76
-
77
- function fail(peerId, reason) {
78
- const s = rt(peerId);
79
- s.status = 'failed';
80
- s.reason = reason;
81
- s.sessionKey = null;
82
- s.sas = null;
83
- failPending(s, new Error(reason));
84
- const ws = s.ws;
85
- s.ws = null;
86
- try { ws?.close(); } catch { /* already closed */ }
87
- }
88
-
89
- function addPeer({ peerId, deviceId, machineName }) {
90
- if (!peerId || !deviceId) throw new Error('peerId and deviceId required');
91
- const peers = loadPeers();
92
- const existing = peers.find((p) => p.peerId === peerId);
93
- if (existing) return existing;
94
- const kp = generateE2EKeyPair();
95
- const peer = {
96
- peerId,
97
- deviceId,
98
- machineName: machineName || '',
99
- pairedAt: new Date().toISOString(),
100
- clientPrivateKey: kp.e2ePrivateKey,
101
- clientPublicKey: kp.e2ePublicKey,
102
- verifiedDevicePubKey: null,
103
- os: null,
104
- };
105
- peers.push(peer);
106
- savePeers(peers);
107
- return peer;
108
- }
109
-
110
- function listPeers() {
111
- // Never expose the private key to callers.
112
- return loadPeers().map(({ clientPrivateKey, ...rest }) => rest);
113
- }
114
-
115
- function getStatus(peerId) {
116
- const s = rt(peerId);
117
- return { status: s.status, reason: s.reason, sas: s.sas };
118
- }
119
-
120
- // Fire-and-forget, once per connection: remember the remote's platform so the
121
- // Remote view can show it offline. Any failure leaves `os` untouched.
122
- function learnOs(peerId, s) {
123
- if (s.osRequested) return;
124
- s.osRequested = true;
125
- send(peerId, 'cmd:app:info').then((res) => {
126
- if (typeof res?.platform === 'string' && res.platform) updatePeer(peerId, { os: res.platform });
127
- }).catch(() => { /* older remote or timeout: never fails the connection */ });
128
- }
129
-
130
- function markAuthenticated(peerId, s) {
131
- s.status = 'authenticated';
132
- learnOs(peerId, s);
133
- }
134
-
135
- function rawSend(ws, msg) {
136
- ws.send(JSON.stringify(msg));
137
- }
138
-
139
- function handleInner(peerId, s, inner) {
140
- if (!inner || typeof inner.type !== 'string') return;
141
- if (inner.type.startsWith('resp:')) {
142
- const p = s.pending.get(inner.id);
143
- if (!p) return;
144
- s.pending.delete(inner.id);
145
- clearTimeout(p.timer);
146
- p.resolve(inner.payload);
147
- return;
148
- }
149
- if (inner.type.startsWith('event:')) {
150
- for (const cb of subscribers.get(peerId) || []) {
151
- try { cb(inner); } catch { /* subscriber errors never break the socket */ }
152
- }
153
- }
154
- }
155
-
156
- function handleMessage(peerId, s, peer, raw) {
157
- let msg;
158
- try { msg = JSON.parse(raw.toString()); } catch { return; }
159
- if (!msg || typeof msg.type !== 'string') return;
160
-
161
- if (msg.type === 'ping') {
162
- if (s.ws && s.ws.readyState === WS_OPEN) rawSend(s.ws, { type: 'pong', id: msg.id, ts: Date.now() });
163
- return;
164
- }
165
- if (msg.type === 'error') {
166
- const code = msg.code || msg.payload?.code;
167
- if (code === 'token_revoked') {
168
- try { relayAuth.clearToken?.(); } catch { /* best effort */ }
169
- fail(peerId, 'token_revoked');
170
- } else if (FATAL_RELAY_ERRORS.has(code)) {
171
- fail(peerId, code);
172
- }
173
- return;
174
- }
175
- if (msg.type === 'auth:ok') {
176
- // Relay accepted the ticket: start the E2E handshake.
177
- rawSend(s.ws, {
178
- type: 'e2e:hello',
179
- id: crypto.randomUUID(),
180
- deviceId: peer.deviceId,
181
- payload: { pubKey: peer.clientPublicKey },
182
- ts: Date.now(),
183
- });
184
- return;
185
- }
186
- if (msg.type === 'e2e:ready') {
187
- if (s.sessionKey) return;
188
- try {
189
- s.sessionKey = deriveSessionKey(peer.clientPrivateKey, s.devicePubKey, peer.deviceId);
190
- s.sas = deriveSas(peer.clientPrivateKey, s.devicePubKey, peer.deviceId);
191
- } catch {
192
- fail(peerId, 'e2e_derivation_failed');
193
- return;
194
- }
195
- if (peer.verifiedDevicePubKey && peer.verifiedDevicePubKey === s.devicePubKey) {
196
- markAuthenticated(peerId, s);
197
- } else {
198
- s.status = 'pending_sas';
199
- }
200
- return;
201
- }
202
- if (msg.type === 'e2e:box') {
203
- if (!s.sessionKey) return;
204
- const { nonce, ciphertext } = msg.payload || {};
205
- if (!nonce || !ciphertext) return;
206
- const plain = decryptBox(nonce, ciphertext, s.sessionKey);
207
- if (!plain) return;
208
- let inner;
209
- try { inner = JSON.parse(plain); } catch { return; }
210
- handleInner(peerId, s, inner);
211
- }
212
- }
213
-
214
- async function connect(peerId) {
215
- const peer = getPeer(peerId);
216
- if (!peer) throw new Error('unknown_peer');
217
- const s = rt(peerId);
218
- const attempt = ++s.attempt;
219
- try { s.ws?.close(); } catch { /* already closed */ }
220
- s.ws = null;
221
- s.sessionKey = null;
222
- s.sas = null;
223
- s.reason = null;
224
- s.osRequested = false;
225
- s.status = 'connecting';
226
-
227
- let ticket;
228
- let devicePubKey;
229
- try {
230
- ticket = await relayAuth.getBrowserTicket(peer);
231
- devicePubKey = await relayAuth.getDevicePubKey(peer.deviceId);
232
- } catch (e) {
233
- fail(peerId, e?.message || 'relay_auth_failed');
234
- return getStatus(peerId);
235
- }
236
- if (attempt !== s.attempt) return getStatus(peerId); // superseded or removed
237
- if (!ticket || !devicePubKey) {
238
- fail(peerId, 'relay_auth_failed');
239
- return getStatus(peerId);
240
- }
241
- // TOFU: a changed device key after pinning fails closed, before any socket opens.
242
- if (peer.verifiedDevicePubKey && peer.verifiedDevicePubKey !== devicePubKey) {
243
- fail(peerId, 'device_key_changed');
244
- return getStatus(peerId);
245
- }
246
- s.devicePubKey = devicePubKey;
247
-
248
- let ws;
249
- try {
250
- ws = wsFactory(`${RELAY_WSS_URL}?ticket=${encodeURIComponent(ticket)}`);
251
- } catch {
252
- fail(peerId, 'ws_create_failed');
253
- return getStatus(peerId);
254
- }
255
- s.ws = ws;
256
- ws.on('message', (data) => { if (s.ws === ws) handleMessage(peerId, s, peer, data); });
257
- ws.on('close', (code) => {
258
- if (s.ws !== ws) return;
259
- if (code === CLOSE_TOKEN_REVOKED) {
260
- try { relayAuth.clearToken?.(); } catch { /* best effort */ }
261
- fail(peerId, 'token_revoked');
262
- return;
263
- }
264
- s.ws = null;
265
- s.sessionKey = null;
266
- failPending(s, new Error('connection_closed'));
267
- if (s.status !== 'failed') { s.status = 'idle'; s.sas = null; }
268
- });
269
- ws.on('error', () => { if (s.ws === ws) fail(peerId, 'ws_error'); });
270
- return getStatus(peerId);
271
- }
272
-
273
- function confirmSas(peerId) {
274
- const s = rt(peerId);
275
- if (s.status !== 'pending_sas' || !s.devicePubKey) return { ok: false, reason: 'not_pending_sas' };
276
- updatePeer(peerId, { verifiedDevicePubKey: s.devicePubKey });
277
- markAuthenticated(peerId, s);
278
- return { ok: true };
279
- }
280
-
281
- function send(peerId, type, payload) {
282
- if (typeof type !== 'string' || !ALLOWED_COMMANDS.has(type)) {
283
- return Promise.reject(new Error('command_not_allowed'));
284
- }
285
- const s = rt(peerId);
286
- if (!s.ws || s.ws.readyState !== WS_OPEN || !s.sessionKey) {
287
- return Promise.reject(new Error('not_connected'));
288
- }
289
- const id = crypto.randomUUID();
290
- const peer = getPeer(peerId);
291
- if (!peer) return Promise.reject(new Error('unknown_peer'));
292
- const inner = { type, id, payload: payload ?? {}, ts: Date.now() };
293
- const { nonce, ciphertext } = encryptBox(JSON.stringify(inner), s.sessionKey);
294
- return new Promise((resolve, reject) => {
295
- const timer = setTimeout(() => {
296
- s.pending.delete(id);
297
- reject(new Error('timeout'));
298
- }, SEND_TIMEOUT_MS);
299
- s.pending.set(id, { resolve, reject, timer });
300
- try {
301
- rawSend(s.ws, { type: 'e2e:box', id, deviceId: peer.deviceId, payload: { nonce, ciphertext }, ts: Date.now() });
302
- } catch (e) {
303
- s.pending.delete(id);
304
- clearTimeout(timer);
305
- reject(e);
306
- }
307
- });
308
- }
309
-
310
- function onEvent(peerId, cb) {
311
- let set = subscribers.get(peerId);
312
- if (!set) { set = new Set(); subscribers.set(peerId, set); }
313
- set.add(cb);
314
- return () => { set.delete(cb); };
315
- }
316
-
317
- function removePeer(peerId) {
318
- const s = runtime.get(peerId);
319
- if (s) {
320
- s.attempt++;
321
- failPending(s, new Error('peer_removed'));
322
- try { s.ws?.close(); } catch { /* already closed */ }
323
- runtime.delete(peerId);
324
- }
325
- subscribers.delete(peerId);
326
- savePeers(loadPeers().filter((p) => p.peerId !== peerId));
327
- }
328
-
329
- return { addPeer, listPeers, connect, confirmSas, getStatus, send, onEvent, removePeer };
330
- }
331
-
332
- const remoteClient = createRemoteClient();
333
-
334
- module.exports = { createRemoteClient, remoteClient, SEND_TIMEOUT_MS };
@@ -1,133 +0,0 @@
1
- 'use strict';
2
-
3
- /**
4
- * remoteClientIpc.cjs — IPC surface for the desktop-to-desktop remote client
5
- * (remoteClient.cjs). Every payload is Zod-validated here, in main. Status
6
- * changes and peer events are pushed to the renderer on `remoteClient:status`
7
- * and `remoteClient:event`.
8
- */
9
-
10
- const os = require('node:os');
11
- const { ipcMain } = require('electron');
12
- const { z } = require('zod');
13
- const { remoteClient } = require('./remoteClient.cjs');
14
- const { relayClientAuth } = require('./lib/feedback-remote/relayClientAuth.cjs');
15
- const { ALLOWED_COMMANDS } = require('./ipcSchemas.cjs');
16
- const { sendIfAlive } = require('./lib/sendToRenderer.cjs');
17
-
18
- const STATUS_POLL_MS = 1000;
19
-
20
- const peerId = z.string().min(1).max(128);
21
- const pairCode = z.string().trim()
22
- .transform((c) => c.replace(/[-\s]/g, '').toUpperCase())
23
- .refine((c) => /^[A-Za-z0-9]{8}$/.test(c), { message: 'invalid_code' });
24
- const schemas = {
25
- pair: z.object({ code: pairCode, clientName: z.string().max(64).optional() }),
26
- add: z.object({ deviceId: z.string().min(1).max(128), machineName: z.string().min(1).max(64) }),
27
- peer: z.object({ peerId }),
28
- send: z.object({
29
- peerId,
30
- type: z.string().min(1).max(128).refine((t) => ALLOWED_COMMANDS.has(t), { message: 'command_not_allowed' }),
31
- payload: z.unknown().optional(),
32
- }),
33
- };
34
-
35
- function validated(schema, handler) {
36
- return (_event, payload) => handler(schema.parse(payload));
37
- }
38
-
39
- function registerRemoteClientHandlers(getWindow, client = remoteClient, auth = relayClientAuth) {
40
- const unsubs = new Map(); // peerId -> unsubscribe for event forwarding
41
- const lastStatus = new Map(); // peerId -> serialized status
42
-
43
- const toWindow = (channel, payload) => sendIfAlive(getWindow(), channel, payload);
44
-
45
- function publishStatus(id) {
46
- const status = client.getStatus(id);
47
- const key = JSON.stringify(status);
48
- if (lastStatus.get(id) === key) return status;
49
- lastStatus.set(id, key);
50
- toWindow('remoteClient:status', { peerId: id, ...status });
51
- return status;
52
- }
53
-
54
- function ensureForwarding(id) {
55
- if (unsubs.has(id)) return;
56
- unsubs.set(id, client.onEvent(id, (ev) => {
57
- toWindow('remoteClient:event', { peerId: id, type: ev.type, payload: ev.payload });
58
- }));
59
- }
60
-
61
- // remoteClient has no status callback; the socket moves status on its own
62
- // (pending_sas, failed, idle), so poll for changes on known peers.
63
- const timer = setInterval(() => {
64
- for (const p of client.listPeers()) publishStatus(p.peerId);
65
- }, STATUS_POLL_MS);
66
- if (typeof timer.unref === 'function') timer.unref();
67
-
68
- ipcMain.handle('remoteClient:list', () => client.listPeers().map((p) => {
69
- ensureForwarding(p.peerId);
70
- return { ...p, ...client.getStatus(p.peerId) };
71
- }));
72
-
73
- ipcMain.handle('remoteClient:connect', validated(schemas.peer, async ({ peerId: id }) => {
74
- ensureForwarding(id);
75
- const connecting = client.connect(id);
76
- publishStatus(id);
77
- const status = await connecting;
78
- publishStatus(id);
79
- return status;
80
- }));
81
-
82
- ipcMain.handle('remoteClient:confirm-sas', validated(schemas.peer, ({ peerId: id }) => {
83
- const res = client.confirmSas(id);
84
- publishStatus(id);
85
- return res;
86
- }));
87
-
88
- ipcMain.handle('remoteClient:remove', validated(schemas.peer, ({ peerId: id }) => {
89
- client.removePeer(id);
90
- const off = unsubs.get(id);
91
- if (off) off();
92
- unsubs.delete(id);
93
- lastStatus.delete(id);
94
- toWindow('remoteClient:status', { peerId: id, status: 'removed', reason: null, sas: null });
95
- return { ok: true };
96
- }));
97
-
98
- ipcMain.handle('remoteClient:send', validated(schemas.send, ({ peerId: id, type, payload }) =>
99
- client.send(id, type, payload)));
100
-
101
- ipcMain.handle('remoteClient:auth-status', () => ({ paired: auth.isPaired(), clientId: auth.getClientId() }));
102
-
103
- ipcMain.handle('remoteClient:pair', validated(schemas.pair, ({ code, clientName }) =>
104
- auth.pair(code, clientName ?? os.hostname())));
105
-
106
- ipcMain.handle('remoteClient:unpair', () => {
107
- auth.clearToken();
108
- return { ok: true };
109
- });
110
-
111
- ipcMain.handle('remoteClient:devices', async () => {
112
- try {
113
- return { ok: true, devices: await auth.listDevices() };
114
- } catch (e) {
115
- return { ok: false, error: e.message };
116
- }
117
- });
118
-
119
- ipcMain.handle('remoteClient:add', validated(schemas.add, ({ deviceId, machineName }) => {
120
- const { clientPrivateKey: _omit, ...peer } = client.addPeer({ peerId: deviceId, deviceId, machineName });
121
- ensureForwarding(deviceId);
122
- publishStatus(deviceId);
123
- return peer;
124
- }));
125
-
126
- return () => {
127
- clearInterval(timer);
128
- for (const off of unsubs.values()) off();
129
- unsubs.clear();
130
- };
131
- }
132
-
133
- module.exports = { registerRemoteClientHandlers, schemas };