claude-code-session-manager 0.21.2 → 0.21.4

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 (54) hide show
  1. package/bin/cli.cjs +5 -0
  2. package/dist/assets/{TiptapBody-CepFtp62.js → TiptapBody-CZLSQ6pj.js} +2 -2
  3. package/dist/assets/cssMode-DfqZGMQs.js +1 -0
  4. package/dist/assets/{freemarker2-DqQlU_4i.js → freemarker2-XTPYh37h.js} +1 -1
  5. package/dist/assets/handlebars-DKUF5VyH.js +1 -0
  6. package/dist/assets/html-uqoqsIeI.js +1 -0
  7. package/dist/assets/htmlMode-aMTQs1su.js +1 -0
  8. package/dist/assets/index-BUrrcj7x.js +3525 -0
  9. package/dist/assets/index-DeQI4oVI.css +32 -0
  10. package/dist/assets/javascript-BVxRZMds.js +1 -0
  11. package/dist/assets/{jsonMode-CFEryxme.js → jsonMode-D04xP2s5.js} +4 -4
  12. package/dist/assets/liquid-BkQHTH2P.js +1 -0
  13. package/dist/assets/lspLanguageFeatures-By9uLznH.js +4 -0
  14. package/dist/assets/mdx-Du1IlbjV.js +1 -0
  15. package/dist/assets/{index-CrE67_1W.css → monaco-editor-BTnBOi8r.css} +1 -32
  16. package/dist/assets/monaco-editor-BW5C4Iv1.js +908 -0
  17. package/dist/assets/python-DSlImqXd.js +1 -0
  18. package/dist/assets/razor-BmUVyvSK.js +1 -0
  19. package/dist/assets/{tsMode-CNLm8WAZ.js → tsMode-Btj0TTH7.js} +1 -1
  20. package/dist/assets/typescript-Bzelq9vO.js +1 -0
  21. package/dist/assets/xml-Whd9EaSd.js +1 -0
  22. package/dist/assets/yaml-QYf0-IN8.js +1 -0
  23. package/dist/index.html +4 -2
  24. package/package.json +1 -1
  25. package/src/main/__tests__/runVerify.test.cjs +138 -0
  26. package/src/main/config.cjs +36 -4
  27. package/src/main/historyAggregator.cjs +400 -149
  28. package/src/main/index.cjs +8 -0
  29. package/src/main/ipcSchemas.cjs +42 -13
  30. package/src/main/kg.cjs +87 -30
  31. package/src/main/lib/credentials.cjs +7 -0
  32. package/src/main/lib/e2eStateMachine.cjs +39 -0
  33. package/src/main/runVerify.cjs +51 -5
  34. package/src/main/scheduler/prdParser.cjs +16 -1
  35. package/src/main/scheduler.cjs +171 -13
  36. package/src/main/transcripts.cjs +141 -19
  37. package/src/main/usageMatrix.cjs +7 -3
  38. package/src/main/webRemote.cjs +196 -31
  39. package/src/preload/api.d.ts +40 -0
  40. package/src/preload/index.cjs +7 -0
  41. package/dist/assets/cssMode-8hR_Zezu.js +0 -1
  42. package/dist/assets/handlebars-Ts2NzFcS.js +0 -1
  43. package/dist/assets/html-QjLxt2p6.js +0 -1
  44. package/dist/assets/htmlMode-Dst38sy3.js +0 -1
  45. package/dist/assets/index-XKsJ4Pk3.js +0 -4431
  46. package/dist/assets/javascript-CNxLjNGz.js +0 -1
  47. package/dist/assets/liquid-BBfKLTB_.js +0 -1
  48. package/dist/assets/lspLanguageFeatures-BNyh7ouG.js +0 -4
  49. package/dist/assets/mdx-SaTyS1xC.js +0 -1
  50. package/dist/assets/python-C84TNhMd.js +0 -1
  51. package/dist/assets/razor-BaVJM3L8.js +0 -1
  52. package/dist/assets/typescript-BdrDpzPy.js +0 -1
  53. package/dist/assets/xml-CHJ3Xjjj.js +0 -1
  54. package/dist/assets/yaml-Cg2-K8t3.js +0 -1
@@ -7,7 +7,7 @@
7
7
  * - Outbound-only: opens a WS TO the relay, never listens.
8
8
  * - OFF by default: remoteEnabled must be explicitly true in web-remote.json.
9
9
  * - Kill switch: synchronous — drops socket + refuses all commands instantly.
10
- * - Strict allowlist: 15 enumerated command types; unknown → silent drop.
10
+ * - Strict allowlist: enumerated command types; unknown → opaque rejected response.
11
11
  * - Zod validation: every payload parsed before any dispatch.
12
12
  * - Path safety: cwd/path fields go through validatePath (home-dir boundary).
13
13
  * - Token at rest: web-remote.json written at 0600 via writeTextAtomic.
@@ -28,6 +28,7 @@ const { writeTextAtomic, validatePath } = require('./config.cjs');
28
28
  const logs = require('./logs.cjs');
29
29
  const { sendIfAlive } = require('./lib/sendToRenderer.cjs');
30
30
  const { schemas } = require('./ipcSchemas.cjs');
31
+ const { makeState, confirmSas: confirmSasLogic } = require('./lib/e2eStateMachine.cjs');
31
32
 
32
33
  // ─── Constants ───────────────────────────────────────────────────────────────
33
34
 
@@ -66,7 +67,7 @@ const MSG_MAX_BYTES = 256 * 1024;
66
67
 
67
68
  // Single source of truth lives in ipcSchemas.cjs — imported here so the test
68
69
  // can verify the same Set without depending on Electron-linked modules.
69
- const { ALLOWED_COMMANDS } = require('./ipcSchemas.cjs');
70
+ const { ALLOWED_COMMANDS, MUTATE_COMMANDS, SAS_GATED_READS } = require('./ipcSchemas.cjs');
70
71
 
71
72
  // ─── E2E encryption helpers (P-256 ECDH + AES-256-GCM, ADR §5.2) ────────────
72
73
 
@@ -111,6 +112,30 @@ function deriveSessionKey(myPrivateKeyB64, peerPublicKeyB64, deviceId) {
111
112
  return Buffer.from(crypto.hkdfSync('sha256', sharedSecret, salt, info, 32));
112
113
  }
113
114
 
115
+ /**
116
+ * Derive a 6-digit Short Authentication String from the ECDH shared secret.
117
+ * Uses a separate HKDF info label ('sm-sas-v1') so the SAS is independent of
118
+ * the session key. Both sides compute the same value; user confirms they match.
119
+ */
120
+ function deriveSas(myPrivateKeyB64, peerPublicKeyB64, deviceId) {
121
+ const myPrivKey = crypto.createPrivateKey({
122
+ key: Buffer.from(myPrivateKeyB64, 'base64url'),
123
+ format: 'der',
124
+ type: 'pkcs8',
125
+ });
126
+ const peerPubKey = crypto.createPublicKey({
127
+ key: Buffer.from(peerPublicKeyB64, 'base64url'),
128
+ format: 'der',
129
+ type: 'spki',
130
+ });
131
+ const sharedSecret = crypto.diffieHellman({ privateKey: myPrivKey, publicKey: peerPubKey });
132
+ const salt = Buffer.from(deviceId, 'utf8');
133
+ const info = Buffer.from('sm-sas-v1', 'utf8');
134
+ const sasBytes = Buffer.from(crypto.hkdfSync('sha256', sharedSecret, salt, info, 3));
135
+ const sasNum = ((sasBytes[0] << 16) | (sasBytes[1] << 8) | sasBytes[2]) % 1_000_000;
136
+ return sasNum.toString().padStart(6, '0');
137
+ }
138
+
114
139
  /**
115
140
  * Encrypt a plaintext JSON string into an AES-256-GCM box.
116
141
  * @returns { nonce: base64url, ciphertext: base64url } — the nonce is 12 random bytes
@@ -161,12 +186,13 @@ let _configCacheAt = 0;
161
186
  let _destroyed = false; // set at app shutdown to stop reconnect loops
162
187
 
163
188
  // E2E session state — reset on each new WS connection.
164
- let _e2eSessionKey = null; // Buffer | null
189
+ // .state: 'idle' | 'pending_sas' | 'authenticated' | 'failed'
190
+ let _e2e = makeState();
165
191
 
166
192
  // ─── Config helpers ───────────────────────────────────────────────────────────
167
193
 
168
194
  function defaultConfig() {
169
- return { remoteEnabled: false, devices: [] };
195
+ return { remoteEnabled: false, remoteControlEnabled: false, devices: [] };
170
196
  }
171
197
 
172
198
  function loadConfigSync() {
@@ -219,7 +245,8 @@ async function auditLog(ts, type, deviceId, msgId, result) {
219
245
  try {
220
246
  const ymd = ts.slice(0, 10);
221
247
  const logPath = path.join(AUDIT_LOG_DIR, `remote-audit-${ymd}.log`);
222
- const line = `${ts} ${type} deviceId=${deviceId || '-'} msgId=${msgId || '-'} result=${result}\n`;
248
+ const clean = (s) => String(s ?? '').replace(/[\r\n\t]+/g, ' ').slice(0, 200);
249
+ const line = `${ts} ${clean(type)} deviceId=${clean(deviceId) || '-'} msgId=${clean(msgId) || '-'} result=${result}\n`;
223
250
  const handle = await fsp.open(logPath, 'a', 0o600);
224
251
  try {
225
252
  await handle.write(line);
@@ -285,6 +312,12 @@ async function getDeviceTicket(deviceToken) {
285
312
  return result.ticket;
286
313
  }
287
314
 
315
+ // ─── E2E state helpers ────────────────────────────────────────────────────────
316
+
317
+ function resetE2e(state = 'idle') {
318
+ _e2e = makeState(state);
319
+ }
320
+
288
321
  // ─── WebSocket lifecycle ──────────────────────────────────────────────────────
289
322
 
290
323
  function broadcastStatus() {
@@ -293,8 +326,12 @@ function broadcastStatus() {
293
326
  const connected = _ws !== null && _ws.readyState === WebSocket.OPEN;
294
327
  sendIfAlive(_window, 'webRemote:status', {
295
328
  enabled: cfg.remoteEnabled,
329
+ remoteControlEnabled: cfg.remoteControlEnabled ?? false,
296
330
  connected,
297
- e2eActive: connected && _e2eSessionKey !== null,
331
+ e2eActive: connected && _e2e.sessionKey !== null,
332
+ e2eAuthenticated: connected && _e2e.state === 'authenticated',
333
+ e2eState: connected ? _e2e.state : 'idle',
334
+ pendingSas: connected && _e2e.state === 'pending_sas' ? _e2e.pendingSas : null,
298
335
  devices: (cfg.devices || []).map(({ deviceId, deviceName, issuedAt, lastConnectedAt }) => ({
299
336
  deviceId, deviceName, issuedAt, lastConnectedAt,
300
337
  })),
@@ -358,7 +395,7 @@ function scheduleReconnect() {
358
395
  async function disconnect() {
359
396
  cancelReconnect();
360
397
  stopHeartbeat();
361
- _e2eSessionKey = null;
398
+ resetE2e();
362
399
  if (_ws) {
363
400
  const ws = _ws;
364
401
  _ws = null;
@@ -407,7 +444,7 @@ async function connect() {
407
444
 
408
445
  _ws = ws;
409
446
  _missedPongs = 0;
410
- _e2eSessionKey = null; // reset session key on new connection
447
+ resetE2e(); // reset E2E state on new connection
411
448
 
412
449
  ws.on('open', () => {
413
450
  logs.writeLine({ scope: 'webRemote', level: 'info', message: 'connected to relay' });
@@ -426,6 +463,8 @@ async function connect() {
426
463
  }).catch(() => {});
427
464
  broadcastStatus();
428
465
  // v2: begin pushing the live session list once connected.
466
+ // Reset diff-guard so the new client always receives a full session-list push.
467
+ _lastSessionListJson = null;
429
468
  startSessionListPush();
430
469
  });
431
470
 
@@ -442,7 +481,7 @@ async function connect() {
442
481
  ws.on('close', (code) => {
443
482
  stopHeartbeat();
444
483
  stopAllSessionWatches();
445
- _e2eSessionKey = null;
484
+ resetE2e();
446
485
  if (_ws === ws) _ws = null;
447
486
  logs.writeLine({ scope: 'webRemote', level: 'info', message: 'ws closed', meta: { code } });
448
487
  broadcastStatus();
@@ -483,14 +522,15 @@ const SESSION_INIT_TAIL_BYTES = 512 * 1024; // bound the initial read
483
522
 
484
523
  const _sessionWatchers = new Map(); // tabId → watcher
485
524
  let _sessionListTimer = null;
525
+ let _lastSessionListJson = null; // diff-guard: skip push when snapshot unchanged
486
526
 
487
527
  /** Push an unsolicited event to the browser(s). Encrypts when an E2E key is active. */
488
528
  function pushEvent(type, payload) {
489
529
  if (!_ws || _ws.readyState !== WebSocket.OPEN) return;
490
530
  const inner = { type, id: crypto.randomUUID(), payload, ts: Date.now() };
491
531
  try {
492
- if (_e2eSessionKey) {
493
- const { nonce, ciphertext } = encryptBox(JSON.stringify(inner), _e2eSessionKey);
532
+ if (_e2e.sessionKey) {
533
+ const { nonce, ciphertext } = encryptBox(JSON.stringify(inner), _e2e.sessionKey);
494
534
  _ws.send(JSON.stringify({ type: 'e2e:box', id: inner.id, payload: { nonce, ciphertext }, ts: Date.now() }));
495
535
  } else {
496
536
  _ws.send(JSON.stringify(inner));
@@ -536,13 +576,26 @@ async function tailLines(filePath, fromOffset) {
536
576
  if (stat.size <= start) return { lines: [], size: stat.size, inode: stat.ino };
537
577
  const fd = await fsp.open(filePath, 'r');
538
578
  try {
579
+ // Drop the first fragment only when offset genuinely landed mid-line —
580
+ // i.e. the byte immediately before `start` is not '\n'. When `start`
581
+ // sits right on a newline boundary (previous poll ended at a complete
582
+ // line) the first split-part is already a full line and must be kept.
583
+ let dropFirst = false;
584
+ if (start > 0) {
585
+ const prev = Buffer.alloc(1);
586
+ await fd.read(prev, 0, 1, start - 1);
587
+ dropFirst = prev[0] !== 0x0a; // 0x0a = '\n'
588
+ }
539
589
  const len = stat.size - start;
540
590
  const buf = Buffer.alloc(len);
541
591
  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 };
592
+ // Shift before filter: the fragment may be an empty string (when the
593
+ // buffer starts with '\n', completing the previous partial line). If we
594
+ // filter(Boolean) first, the empty fragment disappears and shift() would
595
+ // remove the first valid line instead.
596
+ const parts = buf.toString('utf8').split('\n');
597
+ if (dropFirst && parts.length) parts.shift();
598
+ return { lines: parts.filter(Boolean), size: stat.size, inode: stat.ino };
546
599
  } finally {
547
600
  await fd.close();
548
601
  }
@@ -580,7 +633,10 @@ async function pollSessionWatcher(w) {
580
633
 
581
634
  if (nextState && nextState !== w.state) {
582
635
  w.state = nextState;
583
- pushEvent('event:session:state', { tabId: w.tabId, state: w.state, since: Date.now() });
636
+ // Guard: don't push state if SAS not yet confirmed (watcher may outlive an auth reset).
637
+ if (_e2e.state === 'authenticated') {
638
+ pushEvent('event:session:state', { tabId: w.tabId, state: w.state, since: Date.now() });
639
+ }
584
640
  }
585
641
  if (newAssistantText && newMsgId !== w.lastMsgId) {
586
642
  w.lastAssistantText = newAssistantText;
@@ -638,6 +694,11 @@ async function pushSessionList() {
638
694
  // this stops the unsolicited background push too.
639
695
  const cfg = await loadConfig();
640
696
  if (!cfg.remoteEnabled) return;
697
+ // Don't push before SAS is confirmed — session cwds/titles are sensitive user data.
698
+ // A relay that completes e2e:hello before the user confirms the SAS would otherwise
699
+ // receive the full session list immediately (same threat SAS_GATED_READS blocks for
700
+ // cmd:sessions:load). Guard here so _lastSessionListJson is not poisoned either.
701
+ if (_e2e.state !== 'authenticated') return;
641
702
  const sessionsStore = require('./sessionsStore.cjs');
642
703
  const data = await sessionsStore.load();
643
704
  // Normalize persisted tabs → SessionMeta. tabId === claudeSessionId so it
@@ -648,7 +709,11 @@ async function pushSessionList() {
648
709
  title: t.label || t.cwd,
649
710
  state: _sessionWatchers.get(t.claudeSessionId)?.state ?? null,
650
711
  }));
651
- pushEvent('event:session:list', { sessions, activeTabId: data?.activeTabId ?? null });
712
+ const payload = { sessions, activeTabId: data?.activeTabId ?? null };
713
+ const json = JSON.stringify(payload);
714
+ if (json === _lastSessionListJson) return;
715
+ _lastSessionListJson = json;
716
+ pushEvent('event:session:list', payload);
652
717
  } catch (e) {
653
718
  logs.writeLine({ scope: 'webRemote', level: 'warn', message: 'pushSessionList failed', meta: { error: e?.message } });
654
719
  }
@@ -734,6 +799,8 @@ function anthropicSummarize(apiKey, text) {
734
799
  * completed turn per subscribed tab (~$1/$5 per 1M tokens).
735
800
  */
736
801
  async function maybeSummarize(w) {
802
+ // Guard: don't push summaries (session transcript content) before SAS confirmed.
803
+ if (_e2e.state !== 'authenticated') return;
737
804
  const text = w.lastAssistantText;
738
805
  if (!text) return;
739
806
  const ofMessageId = w.lastMsgId;
@@ -822,28 +889,69 @@ async function handleMessage(raw, device) {
822
889
  logs.writeLine({ scope: 'webRemote', level: 'warn', message: 'e2e:hello but no device private key — skipping E2E' });
823
890
  return;
824
891
  }
892
+ // Explicit P-256 curve validation — do not rely on Node's implicit throw.
893
+ // Rejects wrong-curve keys (e.g. P-384), malformed DER, and the all-zero
894
+ // identity point. Must happen before deriveSessionKey so a bad key drops
895
+ // the session here, not silently in a crypto catch-all below.
825
896
  try {
826
- _e2eSessionKey = deriveSessionKey(device.e2ePrivateKey, browserPubKey, device.deviceId);
827
- logs.writeLine({ scope: 'webRemote', level: 'info', message: 'E2E session key established' });
897
+ const derBytes = Buffer.from(browserPubKey, 'base64url');
898
+ const importedPub = crypto.createPublicKey({ key: derBytes, format: 'der', type: 'spki' });
899
+ if (importedPub.asymmetricKeyDetails?.namedCurve !== 'prime256v1') {
900
+ throw new Error(`wrong curve: ${importedPub.asymmetricKeyDetails?.namedCurve}`);
901
+ }
902
+ // P-256 SPKI DER is always 91 bytes; raw EC point (04 || x || y) starts at offset 26.
903
+ // Defense-in-depth: reject the identity point (x=0, y=0) explicitly even though
904
+ // Node's ECDH would also reject it — P-256 is a prime-order group, so the identity
905
+ // is the only low-order point.
906
+ if (derBytes.length === 91) {
907
+ const x = derBytes.subarray(27, 59);
908
+ const y = derBytes.subarray(59, 91);
909
+ if (x.every((b) => b === 0) && y.every((b) => b === 0)) {
910
+ throw new Error('identity point rejected');
911
+ }
912
+ }
913
+ } catch (e) {
914
+ logs.writeLine({ scope: 'webRemote', level: 'warn', message: 'e2e:hello peer key validation failed — session dropped', meta: { error: e?.message } });
915
+ await auditLog(new Date().toISOString(), 'e2e:hello', device.deviceId, undefined, 'error:invalid_peer_key');
916
+ resetE2e('failed'); // surface key-validation failure to UI — user must reconnect
917
+ broadcastStatus();
918
+ return;
919
+ }
920
+ try {
921
+ const sessionKey = deriveSessionKey(device.e2ePrivateKey, browserPubKey, device.deviceId);
922
+ let pendingSas;
923
+ try {
924
+ pendingSas = deriveSas(device.e2ePrivateKey, browserPubKey, device.deviceId);
925
+ } catch (sasErr) {
926
+ // SAS derivation failed — session cannot be verified by the user.
927
+ // Mark failed so confirm-sas returns ok:false and the UI prompts retry.
928
+ resetE2e('failed');
929
+ logs.writeLine({ scope: 'webRemote', level: 'warn', message: 'E2E SAS derivation failed — session marked failed', meta: { error: sasErr?.message } });
930
+ broadcastStatus();
931
+ return;
932
+ }
933
+ _e2e = makeState('pending_sas', sessionKey, pendingSas);
934
+ logs.writeLine({ scope: 'webRemote', level: 'info', message: 'E2E session key established — SAS pending confirmation' });
828
935
  broadcastStatus();
829
936
  // Acknowledge with e2e:ready (unencrypted — session just started)
830
937
  respond(id, undefined, 'e2e:ready');
831
938
  } catch (e) {
832
939
  logs.writeLine({ scope: 'webRemote', level: 'warn', message: 'E2E key derivation failed', meta: { error: e?.message } });
833
- _e2eSessionKey = null;
940
+ resetE2e('failed');
941
+ broadcastStatus();
834
942
  }
835
943
  return;
836
944
  }
837
945
 
838
946
  // ── Decrypt e2e:box messages ──────────────────────────────────────────────
839
947
  if (type === 'e2e:box') {
840
- if (!_e2eSessionKey) {
948
+ if (!_e2e.sessionKey) {
841
949
  logs.writeLine({ scope: 'webRemote', level: 'warn', message: 'e2e:box received but no session key — dropping' });
842
950
  return;
843
951
  }
844
952
  const { nonce, ciphertext } = payload || {};
845
953
  if (!nonce || !ciphertext) return;
846
- const plaintext = decryptBox(nonce, ciphertext, _e2eSessionKey);
954
+ const plaintext = decryptBox(nonce, ciphertext, _e2e.sessionKey);
847
955
  if (!plaintext) {
848
956
  logs.writeLine({ scope: 'webRemote', level: 'warn', message: 'e2e:box decryption failed (auth tag mismatch)' });
849
957
  return;
@@ -864,7 +972,7 @@ async function handleMessage(raw, device) {
864
972
  if (!type.startsWith('cmd:')) return;
865
973
  // After E2E is established, reject plaintext commands — a malicious relay cannot
866
974
  // silently downgrade the session by stripping e2e:hello (PENTEST.md §H1).
867
- if (_e2eSessionKey) {
975
+ if (_e2e.sessionKey) {
868
976
  logs.writeLine({ scope: 'webRemote', level: 'warn', message: 'plaintext cmd rejected — e2e session active' });
869
977
  return;
870
978
  }
@@ -881,13 +989,31 @@ async function dispatchEnvelope(envelope, device) {
881
989
  const cfg = await loadConfig();
882
990
  if (!cfg.remoteEnabled) {
883
991
  await auditLog(ts, type, device.deviceId, id, 'error:disabled');
884
- respond(id, { error: 'disabled' });
992
+ respond(id, { error: 'rejected' }); // opaque — reason stays in audit log only
885
993
  return;
886
994
  }
887
995
 
888
- // Allowlist check — unknown types are dropped without error feedback (ADR §6.2)
996
+ // Allowlist check — reject unknown cmd:* types with opaque error (oracle prevention)
889
997
  if (!ALLOWED_COMMANDS.has(type)) {
890
998
  await auditLog(ts, type, device.deviceId, id, 'error:not_allowed');
999
+ respond(id, { error: 'rejected' });
1000
+ return;
1001
+ }
1002
+
1003
+ // MUTATE tier gate — write/exec commands require remoteControlEnabled=true (default false)
1004
+ if (MUTATE_COMMANDS.has(type) && !cfg.remoteControlEnabled) {
1005
+ await auditLog(ts, type, device.deviceId, id, 'error:not_allowed');
1006
+ respond(id, { error: 'rejected' }); // opaque — reason stays in audit log only
1007
+ return;
1008
+ }
1009
+
1010
+ // E2E auth gate — MUTATE and sensitive READ commands are blocked until the
1011
+ // user confirms the SAS on the desktop. This prevents a compromised relay
1012
+ // from exfiltrating session lists, PRDs, run logs, or transcript summaries
1013
+ // by completing the ECDH handshake without the user's knowledge.
1014
+ if ((MUTATE_COMMANDS.has(type) || SAS_GATED_READS.has(type)) && _e2e.state !== 'authenticated') {
1015
+ await auditLog(ts, type, device.deviceId, id, 'error:e2e_not_authenticated');
1016
+ respond(id, { error: 'rejected' }); // opaque — reason stays in audit log only
891
1017
  return;
892
1018
  }
893
1019
 
@@ -899,7 +1025,7 @@ async function dispatchEnvelope(envelope, device) {
899
1025
  } catch (e) {
900
1026
  const code = e?.name === 'ZodError' ? 'schema_invalid' : 'dispatch_error';
901
1027
  await auditLog(ts, type, device.deviceId, id, `error:${code}`);
902
- result = { error: code }; // never leak internal error messages to the remote caller
1028
+ result = { error: 'rejected' }; // opaque on-wire reason stays in audit log only
903
1029
  }
904
1030
 
905
1031
  respond(id, result);
@@ -919,8 +1045,8 @@ function respond(msgId, payload, typeOverride) {
919
1045
 
920
1046
  try {
921
1047
  // Encrypt the response if a session key is active.
922
- if (_e2eSessionKey && !typeOverride) {
923
- const { nonce, ciphertext } = encryptBox(JSON.stringify(inner), _e2eSessionKey);
1048
+ if (_e2e.sessionKey && !typeOverride) {
1049
+ const { nonce, ciphertext } = encryptBox(JSON.stringify(inner), _e2e.sessionKey);
924
1050
  _ws.send(JSON.stringify({
925
1051
  type: 'e2e:box',
926
1052
  id: msgId,
@@ -1139,16 +1265,41 @@ function registerRemoteHandlers() {
1139
1265
  // Returns current status without tokens — safe to expose to renderer.
1140
1266
  ipcMain.handle('webRemote:get-status', async () => {
1141
1267
  const cfg = await loadConfig();
1268
+ const connected = _ws !== null && _ws.readyState === WebSocket.OPEN;
1142
1269
  return {
1143
1270
  enabled: cfg.remoteEnabled,
1144
- connected: _ws !== null && _ws.readyState === WebSocket.OPEN,
1145
- e2eActive: _ws !== null && _ws.readyState === WebSocket.OPEN && _e2eSessionKey !== null,
1271
+ remoteControlEnabled: cfg.remoteControlEnabled ?? false,
1272
+ connected,
1273
+ e2eActive: connected && _e2e.sessionKey !== null,
1274
+ e2eAuthenticated: connected && _e2e.state === 'authenticated',
1275
+ e2eState: connected ? _e2e.state : 'idle',
1276
+ pendingSas: connected && _e2e.state === 'pending_sas' ? _e2e.pendingSas : null,
1146
1277
  devices: (cfg.devices || []).map(({ deviceId, deviceName, issuedAt, lastConnectedAt }) => ({
1147
1278
  deviceId, deviceName, issuedAt, lastConnectedAt,
1148
1279
  })),
1149
1280
  };
1150
1281
  });
1151
1282
 
1283
+ // User confirmed that the SAS shown on both desktop and browser match.
1284
+ // Returns ok:false if the session is not in the pending_sas state (e.g. key
1285
+ // missing, already failed, already authenticated, or reconnected).
1286
+ ipcMain.handle('webRemote:confirm-sas', async () => {
1287
+ const { ok, error, next } = confirmSasLogic(_e2e);
1288
+ if (!ok) {
1289
+ logs.writeLine({ scope: 'webRemote', level: 'warn', message: 'confirm-sas rejected — wrong state', meta: { state: _e2e.state, error } });
1290
+ return { ok: false, error };
1291
+ }
1292
+ _e2e = next;
1293
+ logs.writeLine({ scope: 'webRemote', level: 'info', message: 'E2E session authenticated — SAS confirmed by user' });
1294
+ broadcastStatus();
1295
+ // Flush the session list immediately — the push loop was suppressed while
1296
+ // state !== 'authenticated', so the mobile app would otherwise wait up to
1297
+ // SESSION_LIST_PUSH_MS for the first useful data.
1298
+ _lastSessionListJson = null;
1299
+ pushSessionList().catch(() => {});
1300
+ return { ok: true };
1301
+ });
1302
+
1152
1303
  ipcMain.handle('webRemote:enable', async () => {
1153
1304
  const cfg = await loadConfig();
1154
1305
  await saveConfig({ ...cfg, remoteEnabled: true });
@@ -1165,6 +1316,20 @@ function registerRemoteHandlers() {
1165
1316
  return { ok: true };
1166
1317
  });
1167
1318
 
1319
+ ipcMain.handle('webRemote:enable-control', async () => {
1320
+ const cfg = await loadConfig();
1321
+ await saveConfig({ ...cfg, remoteControlEnabled: true });
1322
+ broadcastStatus();
1323
+ return { ok: true };
1324
+ });
1325
+
1326
+ ipcMain.handle('webRemote:disable-control', async () => {
1327
+ const cfg = await loadConfig();
1328
+ await saveConfig({ ...cfg, remoteControlEnabled: false });
1329
+ broadcastStatus();
1330
+ return { ok: true };
1331
+ });
1332
+
1168
1333
  ipcMain.handle('webRemote:pair', validated(schemas.webRemotePair, async ({ otp }) => {
1169
1334
  return pair(otp);
1170
1335
  }));
@@ -1212,7 +1377,7 @@ async function init() {
1212
1377
  function destroy() {
1213
1378
  _destroyed = true;
1214
1379
  cancelReconnect();
1215
- _e2eSessionKey = null;
1380
+ resetE2e();
1216
1381
  if (_ws) {
1217
1382
  try { _ws.terminate(); } catch { /* */ }
1218
1383
  _ws = null;
@@ -393,6 +393,13 @@ export interface SupervisorLogEntry {
393
393
  costUsd: number | null;
394
394
  }
395
395
 
396
+ export interface SchedulePollHealth {
397
+ lastPollAt: number | null;
398
+ lastPollOk: boolean;
399
+ consecutiveFailures: number;
400
+ lastFailureKind: string | null;
401
+ }
402
+
396
403
  export interface ScheduleStateSnapshot {
397
404
  config: ScheduleConfig & { supervisor?: SupervisorConfig };
398
405
  jobs: ScheduleJob[];
@@ -403,6 +410,8 @@ export interface ScheduleStateSnapshot {
403
410
  paused: SchedulePauseInfo | null;
404
411
  /** Latest five_hour utilization percent (0–100) cached from billing.fetchUsage. null if unknown. */
405
412
  utilization: number | null;
413
+ /** Poll health — last billing poll result; used to detect stale utilization. */
414
+ pollHealth?: SchedulePollHealth;
406
415
  /** Returned only by the initial state() call, not the broadcast event. */
407
416
  paths?: SchedulePaths;
408
417
  }
@@ -452,6 +461,19 @@ export interface ListConversationsResult {
452
461
  scannedMs: number;
453
462
  }
454
463
 
464
+ export interface SessionScanEntry {
465
+ sessionId: string;
466
+ projectEncoded: string;
467
+ path: string;
468
+ mtimeMs: number;
469
+ sizeBytes: number;
470
+ }
471
+
472
+ export interface SessionScanResult {
473
+ sessions: SessionScanEntry[];
474
+ scannedMs: number;
475
+ }
476
+
455
477
 
456
478
  export interface FileEntry {
457
479
  name: string;
@@ -793,8 +815,15 @@ export interface WebRemoteDevice {
793
815
 
794
816
  export interface WebRemoteStatus {
795
817
  enabled: boolean;
818
+ remoteControlEnabled: boolean;
796
819
  connected: boolean;
797
820
  e2eActive: boolean;
821
+ /** True once the user has confirmed the SAS on the desktop. */
822
+ e2eAuthenticated: boolean;
823
+ /** Explicit E2E session state for UI rendering. */
824
+ e2eState: 'idle' | 'pending_sas' | 'authenticated' | 'failed';
825
+ /** 6-digit Short Authentication String pending user confirmation, or null. */
826
+ pendingSas: string | null;
798
827
  devices: WebRemoteDevice[];
799
828
  }
800
829
 
@@ -858,7 +887,10 @@ export interface SessionManagerAPI {
858
887
  };
859
888
  transcripts: {
860
889
  subscribe: (payload: { tabId: string; cwd: string; sessionUuid: string }) => Promise<SubscribeResult>;
890
+ /** Release the sub back to the LRU cache (view-switch). Does not destroy the watcher. */
861
891
  unsubscribe: (tabId: string) => Promise<{ ok: boolean }>;
892
+ /** Permanently destroy the sub (genuine tab close). */
893
+ closeTab: (tabId: string) => Promise<{ ok: boolean }>;
862
894
  buffer: (tabId: string) => Promise<TranscriptEvent[]>;
863
895
  pathFor: (cwd: string, sessionUuid: string) => Promise<string>;
864
896
  onEvent: (tabId: string, handler: (ev: TranscriptEvent) => void) => () => void;
@@ -949,6 +981,7 @@ export interface SessionManagerAPI {
949
981
  history: {
950
982
  aggregate: (req?: HistoryAggregateRequest) => Promise<HistoryAggregateResult>;
951
983
  listConversations: () => Promise<ListConversationsResult>;
984
+ scanProjects: () => Promise<SessionScanResult>;
952
985
  };
953
986
  schedule: {
954
987
  state: () => Promise<ScheduleStateSnapshot>;
@@ -1062,6 +1095,10 @@ export interface SessionManagerAPI {
1062
1095
  getStatus: () => Promise<WebRemoteStatus>;
1063
1096
  enable: () => Promise<WebRemoteMutationResult>;
1064
1097
  disable: () => Promise<WebRemoteMutationResult>;
1098
+ /** Allow MUTATE-tier commands (pty spawn/write, scheduler writes). Default off. */
1099
+ enableControl: () => Promise<WebRemoteMutationResult>;
1100
+ /** Block MUTATE-tier commands — mobile becomes read-only mirror. */
1101
+ disableControl: () => Promise<WebRemoteMutationResult>;
1065
1102
  pair: (otp: string) => Promise<WebRemotePairResult>;
1066
1103
  revokeDevice: (deviceId: string) => Promise<WebRemoteMutationResult>;
1067
1104
  auditTail: (lines?: number) => Promise<WebRemoteAuditTailResult>;
@@ -1071,6 +1108,9 @@ export interface SessionManagerAPI {
1071
1108
  revokeAll: () => Promise<WebRemoteMutationResult>;
1072
1109
  /** Subscribe to the completion event broadcast after revokeAll. */
1073
1110
  onRevokedAll: (handler: (ev: { revokedCount: number }) => void) => () => void;
1111
+ /** Confirm that the SAS shown on the desktop matches the browser — marks the
1112
+ * E2E session as authenticated and unblocks MUTATE-tier commands. */
1113
+ confirmSas: () => Promise<WebRemoteMutationResult>;
1074
1114
  };
1075
1115
  kg: {
1076
1116
  /** Distilled knowledge graph + ingest status for ONE project (`cwd`).
@@ -59,6 +59,7 @@ contextBridge.exposeInMainWorld('api', {
59
59
  transcripts: {
60
60
  subscribe: (payload) => ipcRenderer.invoke('transcript:subscribe', payload),
61
61
  unsubscribe: (tabId) => ipcRenderer.invoke('transcript:unsubscribe', { tabId }),
62
+ closeTab: (tabId) => ipcRenderer.invoke('transcript:close', { tabId }),
62
63
  buffer: (tabId) => ipcRenderer.invoke('transcript:buffer', { tabId }),
63
64
  pathFor: (cwd, sessionUuid) =>
64
65
  ipcRenderer.invoke('transcript:path', { cwd, sessionUuid }),
@@ -155,6 +156,7 @@ contextBridge.exposeInMainWorld('api', {
155
156
  history: {
156
157
  aggregate: (req) => ipcRenderer.invoke('history:aggregate', req),
157
158
  listConversations: () => ipcRenderer.invoke('history:list-conversations'),
159
+ scanProjects: () => ipcRenderer.invoke('history:scan-projects'),
158
160
  },
159
161
  files: {
160
162
  list: (path, showHidden) => ipcRenderer.invoke('files:list', { path, showHidden }),
@@ -283,6 +285,10 @@ contextBridge.exposeInMainWorld('api', {
283
285
  enable: () => ipcRenderer.invoke('webRemote:enable'),
284
286
  /** Turn remote control off. Immediately drops relay connection. */
285
287
  disable: () => ipcRenderer.invoke('webRemote:disable'),
288
+ /** Allow MUTATE-tier commands (pty spawn/write, scheduler writes). Default off. */
289
+ enableControl: () => ipcRenderer.invoke('webRemote:enable-control'),
290
+ /** Block MUTATE-tier commands — mobile becomes read-only mirror. */
291
+ disableControl: () => ipcRenderer.invoke('webRemote:disable-control'),
286
292
  /** Pair a new device using the 8-character OTP shown in the web UI. */
287
293
  pair: (otp) => ipcRenderer.invoke('webRemote:pair', { otp }),
288
294
  /** Revoke a paired device by its deviceId. */
@@ -303,6 +309,7 @@ contextBridge.exposeInMainWorld('api', {
303
309
  },
304
310
  /** Revoke ALL paired devices and tear down every session immediately. */
305
311
  revokeAll: () => ipcRenderer.invoke('webRemote:revoke-all'),
312
+ confirmSas: () => ipcRenderer.invoke('webRemote:confirm-sas'),
306
313
  /** Push event when revokeAll completes (main broadcasts webRemote:revoked-all). */
307
314
  onRevokedAll: (handler) => {
308
315
  const listener = (_e, payload) => handler(payload);
@@ -1 +0,0 @@
1
- import{d as h,l as s}from"./index-XKsJ4Pk3.js";import{C as c,H as u,d as p,D as m,R as f,g as _,h as w,b as k,F as v,a as D,S as P,c as R,f as I}from"./lspLanguageFeatures-BNyh7ouG.js";import{e as b,i as H,j as U,t as y,k as T}from"./lspLanguageFeatures-BNyh7ouG.js";const C=120*1e3;class A{constructor(o){this._defaults=o,this._worker=null,this._client=null,this._idleCheckInterval=window.setInterval(()=>this._checkIfIdle(),30*1e3),this._lastUsedTime=0,this._configChangeListener=this._defaults.onDidChange(()=>this._stopWorker())}_stopWorker(){this._worker&&(this._worker.dispose(),this._worker=null),this._client=null}dispose(){clearInterval(this._idleCheckInterval),this._configChangeListener.dispose(),this._stopWorker()}_checkIfIdle(){if(!this._worker)return;Date.now()-this._lastUsedTime>C&&this._stopWorker()}_getClient(){return this._lastUsedTime=Date.now(),this._client||(this._worker=h({moduleId:"vs/language/css/cssWorker",createWorker:()=>new Worker(new URL(""+new URL("css.worker-B4z49cGk.js",import.meta.url).href,import.meta.url),{type:"module"}),label:this._defaults.languageId,createData:{options:this._defaults.options,languageId:this._defaults.languageId}}),this._client=this._worker.getProxy()),this._client}getLanguageServiceWorker(...o){let e;return this._getClient().then(a=>{e=a}).then(a=>{if(this._worker)return this._worker.withSyncedResources(o)}).then(a=>e)}}function F(n){const o=[],e=[],a=new A(n);o.push(a);const r=(...t)=>a.getLanguageServiceWorker(...t);function l(){const{languageId:t,modeConfiguration:i}=n;g(e),i.completionItems&&e.push(s.registerCompletionItemProvider(t,new c(r,["/","-",":"]))),i.hovers&&e.push(s.registerHoverProvider(t,new u(r))),i.documentHighlights&&e.push(s.registerDocumentHighlightProvider(t,new p(r))),i.definitions&&e.push(s.registerDefinitionProvider(t,new m(r))),i.references&&e.push(s.registerReferenceProvider(t,new f(r))),i.documentSymbols&&e.push(s.registerDocumentSymbolProvider(t,new _(r))),i.rename&&e.push(s.registerRenameProvider(t,new w(r))),i.colors&&e.push(s.registerColorProvider(t,new k(r))),i.foldingRanges&&e.push(s.registerFoldingRangeProvider(t,new v(r))),i.diagnostics&&e.push(new D(t,r,n.onDidChange)),i.selectionRanges&&e.push(s.registerSelectionRangeProvider(t,new P(r))),i.documentFormattingEdits&&e.push(s.registerDocumentFormattingEditProvider(t,new R(r))),i.documentRangeFormattingEdits&&e.push(s.registerDocumentRangeFormattingEditProvider(t,new I(r)))}return l(),o.push(d(e)),d(o)}function d(n){return{dispose:()=>g(n)}}function g(n){for(;n.length;)n.pop().dispose()}export{c as CompletionAdapter,m as DefinitionAdapter,D as DiagnosticsAdapter,k as DocumentColorAdapter,R as DocumentFormattingEditProvider,p as DocumentHighlightAdapter,b as DocumentLinkAdapter,I as DocumentRangeFormattingEditProvider,_ as DocumentSymbolAdapter,v as FoldingRangeAdapter,u as HoverAdapter,f as ReferenceAdapter,w as RenameAdapter,P as SelectionRangeAdapter,A as WorkerManager,H as fromPosition,U as fromRange,F as setupMode,y as toRange,T as toTextEdit};
@@ -1 +0,0 @@
1
- import{l as e}from"./index-XKsJ4Pk3.js";const t=["area","base","br","col","embed","hr","img","input","keygen","link","menuitem","meta","param","source","track","wbr"],a={wordPattern:/(-?\d*\.\d\w*)|([^\`\~\!\@\$\^\&\*\(\)\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\s]+)/g,comments:{blockComment:["{{!--","--}}"]},brackets:[["<!--","-->"],["<",">"],["{{","}}"],["{","}"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],surroundingPairs:[{open:"<",close:">"},{open:'"',close:'"'},{open:"'",close:"'"}],onEnterRules:[{beforeText:new RegExp(`<(?!(?:${t.join("|")}))(\\w[\\w\\d]*)([^/>]*(?!/)>)[^<]*$`,"i"),afterText:/^<\/(\w[\w\d]*)\s*>$/i,action:{indentAction:e.IndentAction.IndentOutdent}},{beforeText:new RegExp(`<(?!(?:${t.join("|")}))(\\w[\\w\\d]*)([^/>]*(?!/)>)[^<]*$`,"i"),action:{indentAction:e.IndentAction.Indent}}]},m={defaultToken:"",tokenPostfix:"",tokenizer:{root:[[/\{\{!--/,"comment.block.start.handlebars","@commentBlock"],[/\{\{!/,"comment.start.handlebars","@comment"],[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.root"}],[/<!DOCTYPE/,"metatag.html","@doctype"],[/<!--/,"comment.html","@commentHtml"],[/(<)(\w+)(\/>)/,["delimiter.html","tag.html","delimiter.html"]],[/(<)(script)/,["delimiter.html",{token:"tag.html",next:"@script"}]],[/(<)(style)/,["delimiter.html",{token:"tag.html",next:"@style"}]],[/(<)([:\w]+)/,["delimiter.html",{token:"tag.html",next:"@otherTag"}]],[/(<\/)(\w+)/,["delimiter.html",{token:"tag.html",next:"@otherTag"}]],[/</,"delimiter.html"],[/\{/,"delimiter.html"],[/[^<{]+/]],doctype:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.comment"}],[/[^>]+/,"metatag.content.html"],[/>/,"metatag.html","@pop"]],comment:[[/\}\}/,"comment.end.handlebars","@pop"],[/./,"comment.content.handlebars"]],commentBlock:[[/--\}\}/,"comment.block.end.handlebars","@pop"],[/./,"comment.content.handlebars"]],commentHtml:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.comment"}],[/-->/,"comment.html","@pop"],[/[^-]+/,"comment.content.html"],[/./,"comment.content.html"]],otherTag:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.otherTag"}],[/\/?>/,"delimiter.html","@pop"],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/[ \t\r\n]+/]],script:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.script"}],[/type/,"attribute.name","@scriptAfterType"],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/>/,{token:"delimiter.html",next:"@scriptEmbedded.text/javascript",nextEmbedded:"text/javascript"}],[/[ \t\r\n]+/],[/(<\/)(script\s*)(>)/,["delimiter.html","tag.html",{token:"delimiter.html",next:"@pop"}]]],scriptAfterType:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.scriptAfterType"}],[/=/,"delimiter","@scriptAfterTypeEquals"],[/>/,{token:"delimiter.html",next:"@scriptEmbedded.text/javascript",nextEmbedded:"text/javascript"}],[/[ \t\r\n]+/],[/<\/script\s*>/,{token:"@rematch",next:"@pop"}]],scriptAfterTypeEquals:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.scriptAfterTypeEquals"}],[/"([^"]*)"/,{token:"attribute.value",switchTo:"@scriptWithCustomType.$1"}],[/'([^']*)'/,{token:"attribute.value",switchTo:"@scriptWithCustomType.$1"}],[/>/,{token:"delimiter.html",next:"@scriptEmbedded.text/javascript",nextEmbedded:"text/javascript"}],[/[ \t\r\n]+/],[/<\/script\s*>/,{token:"@rematch",next:"@pop"}]],scriptWithCustomType:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.scriptWithCustomType.$S2"}],[/>/,{token:"delimiter.html",next:"@scriptEmbedded.$S2",nextEmbedded:"$S2"}],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/[ \t\r\n]+/],[/<\/script\s*>/,{token:"@rematch",next:"@pop"}]],scriptEmbedded:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInEmbeddedState.scriptEmbedded.$S2",nextEmbedded:"@pop"}],[/<\/script/,{token:"@rematch",next:"@pop",nextEmbedded:"@pop"}]],style:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.style"}],[/type/,"attribute.name","@styleAfterType"],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/>/,{token:"delimiter.html",next:"@styleEmbedded.text/css",nextEmbedded:"text/css"}],[/[ \t\r\n]+/],[/(<\/)(style\s*)(>)/,["delimiter.html","tag.html",{token:"delimiter.html",next:"@pop"}]]],styleAfterType:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.styleAfterType"}],[/=/,"delimiter","@styleAfterTypeEquals"],[/>/,{token:"delimiter.html",next:"@styleEmbedded.text/css",nextEmbedded:"text/css"}],[/[ \t\r\n]+/],[/<\/style\s*>/,{token:"@rematch",next:"@pop"}]],styleAfterTypeEquals:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.styleAfterTypeEquals"}],[/"([^"]*)"/,{token:"attribute.value",switchTo:"@styleWithCustomType.$1"}],[/'([^']*)'/,{token:"attribute.value",switchTo:"@styleWithCustomType.$1"}],[/>/,{token:"delimiter.html",next:"@styleEmbedded.text/css",nextEmbedded:"text/css"}],[/[ \t\r\n]+/],[/<\/style\s*>/,{token:"@rematch",next:"@pop"}]],styleWithCustomType:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.styleWithCustomType.$S2"}],[/>/,{token:"delimiter.html",next:"@styleEmbedded.$S2",nextEmbedded:"$S2"}],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/[ \t\r\n]+/],[/<\/style\s*>/,{token:"@rematch",next:"@pop"}]],styleEmbedded:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInEmbeddedState.styleEmbedded.$S2",nextEmbedded:"@pop"}],[/<\/style/,{token:"@rematch",next:"@pop",nextEmbedded:"@pop"}]],handlebarsInSimpleState:[[/\{\{\{?/,"delimiter.handlebars"],[/\}\}\}?/,{token:"delimiter.handlebars",switchTo:"@$S2.$S3"}],{include:"handlebarsRoot"}],handlebarsInEmbeddedState:[[/\{\{\{?/,"delimiter.handlebars"],[/\}\}\}?/,{token:"delimiter.handlebars",switchTo:"@$S2.$S3",nextEmbedded:"$S3"}],{include:"handlebarsRoot"}],handlebarsRoot:[[/"[^"]*"/,"string.handlebars"],[/[#/][^\s}]+/,"keyword.helper.handlebars"],[/else\b/,"keyword.helper.handlebars"],[/[\s]+/],[/[^}]/,"variable.parameter.handlebars"]]}};export{a as conf,m as language};
@@ -1 +0,0 @@
1
- import{l as e}from"./index-XKsJ4Pk3.js";const t=["area","base","br","col","embed","hr","img","input","keygen","link","menuitem","meta","param","source","track","wbr"],i={wordPattern:/(-?\d*\.\d\w*)|([^\`\~\!\@\$\^\&\*\(\)\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\s]+)/g,comments:{blockComment:["<!--","-->"]},brackets:[["<!--","-->"],["<",">"],["{","}"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],surroundingPairs:[{open:'"',close:'"'},{open:"'",close:"'"},{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:"<",close:">"}],onEnterRules:[{beforeText:new RegExp(`<(?!(?:${t.join("|")}))([_:\\w][_:\\w-.\\d]*)([^/>]*(?!/)>)[^<]*$`,"i"),afterText:/^<\/([_:\w][_:\w-.\d]*)\s*>$/i,action:{indentAction:e.IndentAction.IndentOutdent}},{beforeText:new RegExp(`<(?!(?:${t.join("|")}))(\\w[\\w\\d]*)([^/>]*(?!/)>)[^<]*$`,"i"),action:{indentAction:e.IndentAction.Indent}}],folding:{markers:{start:new RegExp("^\\s*<!--\\s*#region\\b.*-->"),end:new RegExp("^\\s*<!--\\s*#endregion\\b.*-->")}}},r={defaultToken:"",tokenPostfix:".html",ignoreCase:!0,tokenizer:{root:[[/<!DOCTYPE/,"metatag","@doctype"],[/<!--/,"comment","@comment"],[/(<)((?:[\w\-]+:)?[\w\-]+)(\s*)(\/>)/,["delimiter","tag","","delimiter"]],[/(<)(script)/,["delimiter",{token:"tag",next:"@script"}]],[/(<)(style)/,["delimiter",{token:"tag",next:"@style"}]],[/(<)((?:[\w\-]+:)?[\w\-]+)/,["delimiter",{token:"tag",next:"@otherTag"}]],[/(<\/)((?:[\w\-]+:)?[\w\-]+)/,["delimiter",{token:"tag",next:"@otherTag"}]],[/</,"delimiter"],[/[^<]+/]],doctype:[[/[^>]+/,"metatag.content"],[/>/,"metatag","@pop"]],comment:[[/-->/,"comment","@pop"],[/[^-]+/,"comment.content"],[/./,"comment.content"]],otherTag:[[/\/?>/,"delimiter","@pop"],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/[ \t\r\n]+/]],script:[[/type/,"attribute.name","@scriptAfterType"],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/>/,{token:"delimiter",next:"@scriptEmbedded",nextEmbedded:"text/javascript"}],[/[ \t\r\n]+/],[/(<\/)(script\s*)(>)/,["delimiter","tag",{token:"delimiter",next:"@pop"}]]],scriptAfterType:[[/=/,"delimiter","@scriptAfterTypeEquals"],[/>/,{token:"delimiter",next:"@scriptEmbedded",nextEmbedded:"text/javascript"}],[/[ \t\r\n]+/],[/<\/script\s*>/,{token:"@rematch",next:"@pop"}]],scriptAfterTypeEquals:[[/"module"/,{token:"attribute.value",switchTo:"@scriptWithCustomType.text/javascript"}],[/'module'/,{token:"attribute.value",switchTo:"@scriptWithCustomType.text/javascript"}],[/"([^"]*)"/,{token:"attribute.value",switchTo:"@scriptWithCustomType.$1"}],[/'([^']*)'/,{token:"attribute.value",switchTo:"@scriptWithCustomType.$1"}],[/>/,{token:"delimiter",next:"@scriptEmbedded",nextEmbedded:"text/javascript"}],[/[ \t\r\n]+/],[/<\/script\s*>/,{token:"@rematch",next:"@pop"}]],scriptWithCustomType:[[/>/,{token:"delimiter",next:"@scriptEmbedded.$S2",nextEmbedded:"$S2"}],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/[ \t\r\n]+/],[/<\/script\s*>/,{token:"@rematch",next:"@pop"}]],scriptEmbedded:[[/<\/script/,{token:"@rematch",next:"@pop",nextEmbedded:"@pop"}],[/[^<]+/,""]],style:[[/type/,"attribute.name","@styleAfterType"],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/>/,{token:"delimiter",next:"@styleEmbedded",nextEmbedded:"text/css"}],[/[ \t\r\n]+/],[/(<\/)(style\s*)(>)/,["delimiter","tag",{token:"delimiter",next:"@pop"}]]],styleAfterType:[[/=/,"delimiter","@styleAfterTypeEquals"],[/>/,{token:"delimiter",next:"@styleEmbedded",nextEmbedded:"text/css"}],[/[ \t\r\n]+/],[/<\/style\s*>/,{token:"@rematch",next:"@pop"}]],styleAfterTypeEquals:[[/"([^"]*)"/,{token:"attribute.value",switchTo:"@styleWithCustomType.$1"}],[/'([^']*)'/,{token:"attribute.value",switchTo:"@styleWithCustomType.$1"}],[/>/,{token:"delimiter",next:"@styleEmbedded",nextEmbedded:"text/css"}],[/[ \t\r\n]+/],[/<\/style\s*>/,{token:"@rematch",next:"@pop"}]],styleWithCustomType:[[/>/,{token:"delimiter",next:"@styleEmbedded.$S2",nextEmbedded:"$S2"}],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/[ \t\r\n]+/],[/<\/style\s*>/,{token:"@rematch",next:"@pop"}]],styleEmbedded:[[/<\/style/,{token:"@rematch",next:"@pop",nextEmbedded:"@pop"}],[/[^<]+/,""]]}};export{i as conf,r as language};
@@ -1 +0,0 @@
1
- import{d as D,l as t}from"./index-XKsJ4Pk3.js";import{H as d,d as l,e as u,F as c,g as h,S as m,h as p,c as w,f as _,C as R}from"./lspLanguageFeatures-BNyh7ouG.js";import{D as E,a as H,b,R as y,i as U,j as T,t as x,k as M}from"./lspLanguageFeatures-BNyh7ouG.js";const I=120*1e3;class f{constructor(n){this._defaults=n,this._worker=null,this._client=null,this._idleCheckInterval=window.setInterval(()=>this._checkIfIdle(),30*1e3),this._lastUsedTime=0,this._configChangeListener=this._defaults.onDidChange(()=>this._stopWorker())}_stopWorker(){this._worker&&(this._worker.dispose(),this._worker=null),this._client=null}dispose(){clearInterval(this._idleCheckInterval),this._configChangeListener.dispose(),this._stopWorker()}_checkIfIdle(){if(!this._worker)return;Date.now()-this._lastUsedTime>I&&this._stopWorker()}_getClient(){return this._lastUsedTime=Date.now(),this._client||(this._worker=D({moduleId:"vs/language/html/htmlWorker",createWorker:()=>new Worker(new URL(""+new URL("html.worker-DtiGdgqp.js",import.meta.url).href,import.meta.url),{type:"module"}),createData:{languageSettings:this._defaults.options,languageId:this._defaults.languageId},label:this._defaults.languageId}),this._client=this._worker.getProxy()),this._client}getLanguageServiceWorker(...n){let e;return this._getClient().then(r=>{e=r}).then(r=>{if(this._worker)return this._worker.withSyncedResources(n)}).then(r=>e)}}class v extends R{constructor(n){super(n,[".",":","<",'"',"=","/"])}}function C(i){const n=new f(i),e=(...o)=>n.getLanguageServiceWorker(...o);let r=i.languageId;t.registerCompletionItemProvider(r,new v(e)),t.registerHoverProvider(r,new d(e)),t.registerDocumentHighlightProvider(r,new l(e)),t.registerLinkProvider(r,new u(e)),t.registerFoldingRangeProvider(r,new c(e)),t.registerDocumentSymbolProvider(r,new h(e)),t.registerSelectionRangeProvider(r,new m(e)),t.registerRenameProvider(r,new p(e)),r==="html"&&(t.registerDocumentFormattingEditProvider(r,new w(e)),t.registerDocumentRangeFormattingEditProvider(r,new _(e)))}function L(i){const n=[],e=[],r=new f(i);n.push(r);const o=(...s)=>r.getLanguageServiceWorker(...s);function P(){const{languageId:s,modeConfiguration:a}=i;k(e),a.completionItems&&e.push(t.registerCompletionItemProvider(s,new v(o))),a.hovers&&e.push(t.registerHoverProvider(s,new d(o))),a.documentHighlights&&e.push(t.registerDocumentHighlightProvider(s,new l(o))),a.links&&e.push(t.registerLinkProvider(s,new u(o))),a.documentSymbols&&e.push(t.registerDocumentSymbolProvider(s,new h(o))),a.rename&&e.push(t.registerRenameProvider(s,new p(o))),a.foldingRanges&&e.push(t.registerFoldingRangeProvider(s,new c(o))),a.selectionRanges&&e.push(t.registerSelectionRangeProvider(s,new m(o))),a.documentFormattingEdits&&e.push(t.registerDocumentFormattingEditProvider(s,new w(o))),a.documentRangeFormattingEdits&&e.push(t.registerDocumentRangeFormattingEditProvider(s,new _(o)))}return P(),n.push(g(e)),g(n)}function g(i){return{dispose:()=>k(i)}}function k(i){for(;i.length;)i.pop().dispose()}export{R as CompletionAdapter,E as DefinitionAdapter,H as DiagnosticsAdapter,b as DocumentColorAdapter,w as DocumentFormattingEditProvider,l as DocumentHighlightAdapter,u as DocumentLinkAdapter,_ as DocumentRangeFormattingEditProvider,h as DocumentSymbolAdapter,c as FoldingRangeAdapter,d as HoverAdapter,y as ReferenceAdapter,p as RenameAdapter,m as SelectionRangeAdapter,f as WorkerManager,U as fromPosition,T as fromRange,L as setupMode,C as setupMode1,x as toRange,M as toTextEdit};