claude-code-session-manager 0.20.1 → 0.21.1
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/{TiptapBody-Db7_uXrI.js → TiptapBody-C46DacIO.js} +1 -1
- package/dist/assets/{cssMode-DFKJhhi6.js → cssMode-CauFS5Bp.js} +1 -1
- package/dist/assets/{freemarker2-DUat8x8o.js → freemarker2-BxIPNQn-.js} +1 -1
- package/dist/assets/{handlebars-B2C1qhAI.js → handlebars-DnEVFUsu.js} +1 -1
- package/dist/assets/{html-khtg0DVs.js → html-S8NXUTqc.js} +1 -1
- package/dist/assets/{htmlMode-Jmhs-vfl.js → htmlMode-rSEyII9x.js} +1 -1
- package/dist/assets/{index-pqnuXM14.js → index-DMobTczM.js} +834 -827
- package/dist/assets/{index-BkkBX1z7.css → index-oGyPFfYZ.css} +1 -1
- package/dist/assets/{javascript-i1CXbgg4.js → javascript-BiWR68QP.js} +1 -1
- package/dist/assets/{jsonMode-DXZaj-kR.js → jsonMode-1FAJaHiX.js} +1 -1
- package/dist/assets/{liquid-Ds7jUF53.js → liquid-CEtOkbwI.js} +1 -1
- package/dist/assets/{lspLanguageFeatures-B_15vO6X.js → lspLanguageFeatures-CRF3U0x3.js} +1 -1
- package/dist/assets/{mdx-DgrrLgTE.js → mdx-C7C95Bzt.js} +1 -1
- package/dist/assets/{python-Cff3tPw3.js → python-CXvKcjLk.js} +1 -1
- package/dist/assets/{razor-DlyG7FmM.js → razor-tzZHfRy2.js} +1 -1
- package/dist/assets/{tsMode-DRmmmttS.js → tsMode-CLQIVays.js} +1 -1
- package/dist/assets/{typescript-DQFL2T1p.js → typescript-LxhyM9W2.js} +1 -1
- package/dist/assets/{xml-CwsJEzdU.js → xml-VS_m20VE.js} +1 -1
- package/dist/assets/{yaml-BDsDjf-y.js → yaml-BsjggdVD.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/main/ipcSchemas.cjs +15 -0
- package/src/main/kg.cjs +102 -16
- package/src/main/scheduler.cjs +24 -1
- package/src/main/transcripts.cjs +1 -0
- package/src/main/webRemote.cjs +317 -5
package/src/main/webRemote.cjs
CHANGED
|
@@ -32,8 +32,12 @@ const { schemas } = require('./ipcSchemas.cjs');
|
|
|
32
32
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
33
33
|
|
|
34
34
|
// Hard-coded wss:// — no configuration allows plaintext downgrade (ADR §5.1).
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
// v2: relay is same-origin on bilko.run (ARCHITECTURE-V2-MOBILE.md §1). REST under
|
|
36
|
+
// /api/sm-relay; WS upgrade at /projects/session-manager/relay (covered by host CSP
|
|
37
|
+
// connect-src 'self').
|
|
38
|
+
const RELAY_HTTPS_BASE = 'https://bilko.run';
|
|
39
|
+
const RELAY_API_BASE = `${RELAY_HTTPS_BASE}/api/sm-relay`;
|
|
40
|
+
const RELAY_WSS_URL = 'wss://bilko.run/projects/session-manager/relay';
|
|
37
41
|
|
|
38
42
|
const CONFIG_PATH = path.join(
|
|
39
43
|
os.homedir(), '.claude', 'session-manager', 'web-remote.json'
|
|
@@ -273,7 +277,7 @@ function httpsPost(url, body, headers = {}) {
|
|
|
273
277
|
// POST /api/device-ticket to exchange device-token for a one-time WS ticket.
|
|
274
278
|
async function getDeviceTicket(deviceToken) {
|
|
275
279
|
const result = await httpsPost(
|
|
276
|
-
`${
|
|
280
|
+
`${RELAY_API_BASE}/device-ticket`,
|
|
277
281
|
'{}',
|
|
278
282
|
{ Authorization: `Bearer ${deviceToken}` }
|
|
279
283
|
);
|
|
@@ -392,7 +396,7 @@ async function connect() {
|
|
|
392
396
|
// Step 2: open WSS connection with the ticket.
|
|
393
397
|
let ws;
|
|
394
398
|
try {
|
|
395
|
-
ws = new WebSocket(`${
|
|
399
|
+
ws = new WebSocket(`${RELAY_WSS_URL}?ticket=${encodeURIComponent(ticket)}`, {
|
|
396
400
|
rejectUnauthorized: true, // verify relay TLS cert
|
|
397
401
|
});
|
|
398
402
|
} catch (e) {
|
|
@@ -421,6 +425,8 @@ async function connect() {
|
|
|
421
425
|
broadcastStatus();
|
|
422
426
|
}).catch(() => {});
|
|
423
427
|
broadcastStatus();
|
|
428
|
+
// v2: begin pushing the live session list once connected.
|
|
429
|
+
startSessionListPush();
|
|
424
430
|
});
|
|
425
431
|
|
|
426
432
|
ws.on('message', (raw) => {
|
|
@@ -435,6 +441,7 @@ async function connect() {
|
|
|
435
441
|
|
|
436
442
|
ws.on('close', (code) => {
|
|
437
443
|
stopHeartbeat();
|
|
444
|
+
stopAllSessionWatches();
|
|
438
445
|
_e2eSessionKey = null;
|
|
439
446
|
if (_ws === ws) _ws = null;
|
|
440
447
|
logs.writeLine({ scope: 'webRemote', level: 'info', message: 'ws closed', meta: { code } });
|
|
@@ -463,6 +470,297 @@ async function handleTokenRevoked(deviceId) {
|
|
|
463
470
|
sendIfAlive(_window, 'webRemote:token-revoked', { deviceId });
|
|
464
471
|
}
|
|
465
472
|
|
|
473
|
+
// ─── v2 mobile: session live state + summary push ────────────────────────────
|
|
474
|
+
//
|
|
475
|
+
// For each subscribed tab the agent tails its transcript JSONL (reusing the
|
|
476
|
+
// canonical classifyLine + transcriptPath from transcripts.cjs — single source of
|
|
477
|
+
// truth), derives a coarse state, and pushes event:session:state on change.
|
|
478
|
+
// The last completed assistant turn drives the Haiku summary (see maybeSummarize).
|
|
479
|
+
|
|
480
|
+
const SESSION_POLL_MS = 1500;
|
|
481
|
+
const SESSION_LIST_PUSH_MS = 5000;
|
|
482
|
+
const SESSION_INIT_TAIL_BYTES = 512 * 1024; // bound the initial read
|
|
483
|
+
|
|
484
|
+
const _sessionWatchers = new Map(); // tabId → watcher
|
|
485
|
+
let _sessionListTimer = null;
|
|
486
|
+
|
|
487
|
+
/** Push an unsolicited event to the browser(s). Encrypts when an E2E key is active. */
|
|
488
|
+
function pushEvent(type, payload) {
|
|
489
|
+
if (!_ws || _ws.readyState !== WebSocket.OPEN) return;
|
|
490
|
+
const inner = { type, id: crypto.randomUUID(), payload, ts: Date.now() };
|
|
491
|
+
try {
|
|
492
|
+
if (_e2eSessionKey) {
|
|
493
|
+
const { nonce, ciphertext } = encryptBox(JSON.stringify(inner), _e2eSessionKey);
|
|
494
|
+
_ws.send(JSON.stringify({ type: 'e2e:box', id: inner.id, payload: { nonce, ciphertext }, ts: Date.now() }));
|
|
495
|
+
} else {
|
|
496
|
+
_ws.send(JSON.stringify(inner));
|
|
497
|
+
}
|
|
498
|
+
} catch (e) {
|
|
499
|
+
logs.writeLine({ scope: 'webRemote', level: 'warn', message: 'pushEvent failed', meta: { type, error: e?.message } });
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/** Extract concatenated text from an assistant transcript line, or '' if none. */
|
|
504
|
+
function extractAssistantText(raw) {
|
|
505
|
+
const msg = raw?.message || raw;
|
|
506
|
+
const content = msg?.content;
|
|
507
|
+
if (typeof content === 'string') return content;
|
|
508
|
+
if (!Array.isArray(content)) return '';
|
|
509
|
+
return content.filter((b) => b?.type === 'text' && typeof b.text === 'string').map((b) => b.text).join('\n').trim();
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/** Map a classified transcript event to a coarse session state. */
|
|
513
|
+
function deriveState(ev, raw) {
|
|
514
|
+
// API/usage errors surface as a flagged message line.
|
|
515
|
+
if (raw?.isApiErrorMessage || raw?.level === 'error') return 'error';
|
|
516
|
+
switch (ev.kind) {
|
|
517
|
+
case 'tool_use':
|
|
518
|
+
case 'agent_spawn':
|
|
519
|
+
return 'running'; // model invoked a tool, awaiting result
|
|
520
|
+
case 'tool_result':
|
|
521
|
+
return 'thinking'; // tool finished, model resuming
|
|
522
|
+
case 'user':
|
|
523
|
+
return 'thinking'; // input submitted, model will respond
|
|
524
|
+
case 'assistant':
|
|
525
|
+
return 'idle'; // assistant text turn complete → user's turn
|
|
526
|
+
default:
|
|
527
|
+
return null; // usage/todo/plan/etc. — no state change
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
async function tailLines(filePath, fromOffset) {
|
|
532
|
+
const stat = await fsp.stat(filePath).catch(() => null);
|
|
533
|
+
if (!stat) return { lines: [], size: 0, inode: undefined };
|
|
534
|
+
let start = fromOffset;
|
|
535
|
+
if (start == null || start > stat.size) start = Math.max(0, stat.size - SESSION_INIT_TAIL_BYTES);
|
|
536
|
+
if (stat.size <= start) return { lines: [], size: stat.size, inode: stat.ino };
|
|
537
|
+
const fd = await fsp.open(filePath, 'r');
|
|
538
|
+
try {
|
|
539
|
+
const len = stat.size - start;
|
|
540
|
+
const buf = Buffer.alloc(len);
|
|
541
|
+
await fd.read(buf, 0, len, start);
|
|
542
|
+
const parts = buf.toString('utf8').split('\n').filter(Boolean);
|
|
543
|
+
// If we started mid-file, the first fragment may be a partial line — drop it.
|
|
544
|
+
if (start > 0 && parts.length) parts.shift();
|
|
545
|
+
return { lines: parts, size: stat.size, inode: stat.ino };
|
|
546
|
+
} finally {
|
|
547
|
+
await fd.close();
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
async function pollSessionWatcher(w) {
|
|
552
|
+
let res;
|
|
553
|
+
try {
|
|
554
|
+
res = await tailLines(w.filePath, w.offset);
|
|
555
|
+
} catch { return; }
|
|
556
|
+
// Inode change = file replaced; restart from a bounded tail.
|
|
557
|
+
if (w.inode !== undefined && res.inode !== undefined && res.inode !== w.inode) {
|
|
558
|
+
w.offset = Math.max(0, res.size - SESSION_INIT_TAIL_BYTES);
|
|
559
|
+
w.inode = res.inode;
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
w.offset = res.size;
|
|
563
|
+
w.inode = res.inode;
|
|
564
|
+
|
|
565
|
+
let nextState = null;
|
|
566
|
+
let newAssistantText = null;
|
|
567
|
+
let newMsgId = null;
|
|
568
|
+
for (const line of res.lines) {
|
|
569
|
+
let obj;
|
|
570
|
+
try { obj = JSON.parse(line); } catch { continue; }
|
|
571
|
+
const ev = require('./transcripts.cjs').classifyLine(obj);
|
|
572
|
+
if (!ev) continue;
|
|
573
|
+
const s = deriveState(ev, obj);
|
|
574
|
+
if (s) nextState = s;
|
|
575
|
+
if (ev.kind === 'assistant') {
|
|
576
|
+
const text = extractAssistantText(obj);
|
|
577
|
+
if (text) { newAssistantText = text; newMsgId = obj.uuid || obj.message?.id || `${w.tabId}:${res.size}`; }
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (nextState && nextState !== w.state) {
|
|
582
|
+
w.state = nextState;
|
|
583
|
+
pushEvent('event:session:state', { tabId: w.tabId, state: w.state, since: Date.now() });
|
|
584
|
+
}
|
|
585
|
+
if (newAssistantText && newMsgId !== w.lastMsgId) {
|
|
586
|
+
w.lastAssistantText = newAssistantText;
|
|
587
|
+
w.lastMsgId = newMsgId;
|
|
588
|
+
}
|
|
589
|
+
// Summarize only a COMPLETED turn (state idle) — not assistant text mid-turn that
|
|
590
|
+
// is followed by a tool call. Cache by msgId so re-subscribe doesn't re-bill.
|
|
591
|
+
if (w.state === 'idle' && w.lastAssistantText && w.lastMsgId !== w.summarizedMsgId) {
|
|
592
|
+
w.summarizedMsgId = w.lastMsgId;
|
|
593
|
+
maybeSummarize(w).catch(() => {});
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function startSessionWatch(tabId, cwd) {
|
|
598
|
+
if (_sessionWatchers.has(tabId)) return;
|
|
599
|
+
const filePath = require('./transcripts.cjs').transcriptPath(cwd, tabId);
|
|
600
|
+
// Defense in depth: the schema restricts tabId charset, but re-validate the
|
|
601
|
+
// FINAL joined path against the home-dir boundary (validatePath resolves
|
|
602
|
+
// symlinks + rejects escapes) before any fs read. Throws → dispatch drops it.
|
|
603
|
+
validatePath(filePath);
|
|
604
|
+
const w = {
|
|
605
|
+
tabId, cwd, filePath,
|
|
606
|
+
offset: null, // null → first poll reads a bounded tail then tracks EOF
|
|
607
|
+
inode: undefined,
|
|
608
|
+
state: 'idle',
|
|
609
|
+
lastAssistantText: null,
|
|
610
|
+
lastMsgId: null,
|
|
611
|
+
summarizedMsgId: null,
|
|
612
|
+
timer: null,
|
|
613
|
+
};
|
|
614
|
+
_sessionWatchers.set(tabId, w);
|
|
615
|
+
// Prime once immediately (captures current state + last assistant turn), then poll.
|
|
616
|
+
pollSessionWatcher(w).catch(() => {});
|
|
617
|
+
w.timer = setInterval(() => pollSessionWatcher(w).catch(() => {}), SESSION_POLL_MS);
|
|
618
|
+
if (typeof w.timer.unref === 'function') w.timer.unref();
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function stopSessionWatch(tabId) {
|
|
622
|
+
const w = _sessionWatchers.get(tabId);
|
|
623
|
+
if (!w) return;
|
|
624
|
+
if (w.timer) clearInterval(w.timer);
|
|
625
|
+
_sessionWatchers.delete(tabId);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function stopAllSessionWatches() {
|
|
629
|
+
for (const tabId of Array.from(_sessionWatchers.keys())) stopSessionWatch(tabId);
|
|
630
|
+
if (_sessionListTimer) { clearInterval(_sessionListTimer); _sessionListTimer = null; }
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/** Push the current session list (reuses sessionsStore — the canonical source). */
|
|
634
|
+
async function pushSessionList() {
|
|
635
|
+
try {
|
|
636
|
+
// Honor the kill switch: when remote is disabled, push nothing (the project
|
|
637
|
+
// list = cwds/titles is sensitive). dispatchEnvelope already blocks cmd:*;
|
|
638
|
+
// this stops the unsolicited background push too.
|
|
639
|
+
const cfg = await loadConfig();
|
|
640
|
+
if (!cfg.remoteEnabled) return;
|
|
641
|
+
const sessionsStore = require('./sessionsStore.cjs');
|
|
642
|
+
const data = await sessionsStore.load();
|
|
643
|
+
// Normalize persisted tabs → SessionMeta. tabId === claudeSessionId so it
|
|
644
|
+
// matches the transcript JSONL name used by cmd:session:subscribe.
|
|
645
|
+
const sessions = (data?.tabs ?? []).map((t) => ({
|
|
646
|
+
tabId: t.claudeSessionId,
|
|
647
|
+
cwd: t.cwd,
|
|
648
|
+
title: t.label || t.cwd,
|
|
649
|
+
state: _sessionWatchers.get(t.claudeSessionId)?.state ?? null,
|
|
650
|
+
}));
|
|
651
|
+
pushEvent('event:session:list', { sessions, activeTabId: data?.activeTabId ?? null });
|
|
652
|
+
} catch (e) {
|
|
653
|
+
logs.writeLine({ scope: 'webRemote', level: 'warn', message: 'pushSessionList failed', meta: { error: e?.message } });
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function startSessionListPush() {
|
|
658
|
+
if (_sessionListTimer) return;
|
|
659
|
+
pushSessionList().catch(() => {});
|
|
660
|
+
_sessionListTimer = setInterval(() => pushSessionList().catch(() => {}), SESSION_LIST_PUSH_MS);
|
|
661
|
+
if (typeof _sessionListTimer.unref === 'function') _sessionListTimer.unref();
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// ─── SM-V2-03: mobile summary via Claude Haiku 4.5 ───────────────────────────
|
|
665
|
+
|
|
666
|
+
const SUMMARY_MIN_CHARS = 280; // below this, push raw — not worth an API call
|
|
667
|
+
const SUMMARY_MODEL = 'claude-haiku-4-5';
|
|
668
|
+
const SUMMARY_MAX_INPUT_CHARS = 24_000; // cap the turn text sent to Haiku (~6k tokens)
|
|
669
|
+
const SUMMARY_SYSTEM =
|
|
670
|
+
'Summarize this Claude Code assistant turn for a phone screen in 2 sentences max, ' +
|
|
671
|
+
'followed by an optional list of up to 3 short action items. Plain text only — no ' +
|
|
672
|
+
'markdown headers, no code blocks. Lead with what was done or decided.';
|
|
673
|
+
|
|
674
|
+
let _anthropicKeyCache = null; // memoized found key only (string); null = re-resolve
|
|
675
|
+
|
|
676
|
+
/** Resolve the Anthropic API key: env → web-remote.json → null (degrade to raw).
|
|
677
|
+
* Only a FOUND key is cached — if absent we re-resolve each call (cheap, loadConfig
|
|
678
|
+
* is TTL-cached) so adding the key to web-remote.json later takes effect without a restart. */
|
|
679
|
+
async function resolveAnthropicKey() {
|
|
680
|
+
if (_anthropicKeyCache) return _anthropicKeyCache;
|
|
681
|
+
const fromEnv = process.env.ANTHROPIC_API_KEY;
|
|
682
|
+
if (fromEnv && fromEnv.trim()) { _anthropicKeyCache = fromEnv.trim(); return _anthropicKeyCache; }
|
|
683
|
+
try {
|
|
684
|
+
const cfg = await loadConfig();
|
|
685
|
+
const k = cfg.anthropicApiKey;
|
|
686
|
+
if (typeof k === 'string' && k.trim()) { _anthropicKeyCache = k.trim(); return _anthropicKeyCache; }
|
|
687
|
+
} catch { /* fall through to null → re-resolve next time */ }
|
|
688
|
+
return null;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/** POST to the Anthropic Messages API. Returns the first text block, or throws. */
|
|
692
|
+
function anthropicSummarize(apiKey, text) {
|
|
693
|
+
const body = JSON.stringify({
|
|
694
|
+
model: SUMMARY_MODEL,
|
|
695
|
+
max_tokens: 320,
|
|
696
|
+
system: SUMMARY_SYSTEM,
|
|
697
|
+
messages: [{ role: 'user', content: text.slice(0, SUMMARY_MAX_INPUT_CHARS) }],
|
|
698
|
+
});
|
|
699
|
+
return new Promise((resolve, reject) => {
|
|
700
|
+
const req = https.request('https://api.anthropic.com/v1/messages', {
|
|
701
|
+
method: 'POST',
|
|
702
|
+
headers: {
|
|
703
|
+
'content-type': 'application/json',
|
|
704
|
+
'x-api-key': apiKey,
|
|
705
|
+
'anthropic-version': '2023-06-01',
|
|
706
|
+
'content-length': Buffer.byteLength(body),
|
|
707
|
+
},
|
|
708
|
+
timeout: 20_000,
|
|
709
|
+
}, (res) => {
|
|
710
|
+
let data = '';
|
|
711
|
+
res.on('data', (c) => { data += c; });
|
|
712
|
+
res.on('end', () => {
|
|
713
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
714
|
+
return reject(new Error(`anthropic HTTP ${res.statusCode}`));
|
|
715
|
+
}
|
|
716
|
+
try {
|
|
717
|
+
const json = JSON.parse(data);
|
|
718
|
+
const block = Array.isArray(json.content) ? json.content.find((b) => b.type === 'text') : null;
|
|
719
|
+
if (!block?.text) return reject(new Error('no text in response'));
|
|
720
|
+
resolve(block.text.trim());
|
|
721
|
+
} catch (e) { reject(e); }
|
|
722
|
+
});
|
|
723
|
+
});
|
|
724
|
+
req.on('error', reject);
|
|
725
|
+
req.on('timeout', () => req.destroy(new Error('anthropic request timed out')));
|
|
726
|
+
req.end(body);
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Produce a mobile summary of the watcher's last completed assistant turn and push it.
|
|
732
|
+
* Short turns are pushed raw (no API call). If no API key is configured, degrades to
|
|
733
|
+
* the raw message so core remote control is never blocked. Cost: Haiku in+out per
|
|
734
|
+
* completed turn per subscribed tab (~$1/$5 per 1M tokens).
|
|
735
|
+
*/
|
|
736
|
+
async function maybeSummarize(w) {
|
|
737
|
+
const text = w.lastAssistantText;
|
|
738
|
+
if (!text) return;
|
|
739
|
+
const ofMessageId = w.lastMsgId;
|
|
740
|
+
|
|
741
|
+
if (text.length < SUMMARY_MIN_CHARS) {
|
|
742
|
+
pushEvent('event:session:summary', { tabId: w.tabId, summary: text, ofMessageId, model: 'raw', ts: Date.now() });
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
const apiKey = await resolveAnthropicKey();
|
|
747
|
+
if (!apiKey) {
|
|
748
|
+
// Degrade gracefully: push a trimmed raw message + a hint flag the app can surface.
|
|
749
|
+
pushEvent('event:session:summary', {
|
|
750
|
+
tabId: w.tabId, summary: text.slice(0, 600), ofMessageId, model: 'raw', degraded: 'no_api_key', ts: Date.now(),
|
|
751
|
+
});
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
try {
|
|
756
|
+
const summary = await anthropicSummarize(apiKey, text);
|
|
757
|
+
pushEvent('event:session:summary', { tabId: w.tabId, summary, ofMessageId, model: SUMMARY_MODEL, ts: Date.now() });
|
|
758
|
+
} catch (e) {
|
|
759
|
+
logs.writeLine({ scope: 'webRemote', level: 'warn', message: 'summary failed; pushing raw', meta: { error: e?.message } });
|
|
760
|
+
pushEvent('event:session:summary', { tabId: w.tabId, summary: text.slice(0, 600), ofMessageId, model: 'raw', degraded: 'api_error', ts: Date.now() });
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
466
764
|
// ─── Message handling & command dispatch ─────────────────────────────────────
|
|
467
765
|
|
|
468
766
|
async function handleMessage(raw, device) {
|
|
@@ -724,6 +1022,20 @@ function getDispatchMap() {
|
|
|
724
1022
|
|
|
725
1023
|
'cmd:app:version': async () =>
|
|
726
1024
|
app.getVersion(),
|
|
1025
|
+
|
|
1026
|
+
// v2 mobile: start/stop pushing live state + summary for a session.
|
|
1027
|
+
'cmd:session:subscribe': async (payload) => {
|
|
1028
|
+
const parsed = schemas.sessionSubscribe.parse(payload);
|
|
1029
|
+
validatePath(parsed.cwd); // home-dir boundary before any fs access
|
|
1030
|
+
startSessionWatch(parsed.tabId, parsed.cwd);
|
|
1031
|
+
return { ok: true };
|
|
1032
|
+
},
|
|
1033
|
+
|
|
1034
|
+
'cmd:session:unsubscribe': async (payload) => {
|
|
1035
|
+
const parsed = schemas.ptyTabId.parse(payload);
|
|
1036
|
+
stopSessionWatch(parsed.tabId);
|
|
1037
|
+
return { ok: true };
|
|
1038
|
+
},
|
|
727
1039
|
};
|
|
728
1040
|
|
|
729
1041
|
return _dispatchMap;
|
|
@@ -753,7 +1065,7 @@ async function pair(otp) {
|
|
|
753
1065
|
|
|
754
1066
|
let response;
|
|
755
1067
|
try {
|
|
756
|
-
response = await httpsPost(`${
|
|
1068
|
+
response = await httpsPost(`${RELAY_API_BASE}/pair`, body);
|
|
757
1069
|
} catch (e) {
|
|
758
1070
|
return { ok: false, error: e?.message || 'pairing request failed' };
|
|
759
1071
|
}
|