@wolfx/opencode-magic-context 0.30.1 → 0.30.3

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 (30) hide show
  1. package/dist/features/magic-context/compartment-chunk-embedding.d.ts.map +1 -1
  2. package/dist/features/magic-context/memory/embedding-openai.d.ts.map +1 -1
  3. package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
  4. package/dist/features/magic-context/recursive-text-splitter.d.ts +36 -0
  5. package/dist/features/magic-context/recursive-text-splitter.d.ts.map +1 -0
  6. package/dist/index.js +368 -117
  7. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  8. package/dist/shared/announcement.d.ts +1 -1
  9. package/dist/shared/data-path.d.ts.map +1 -1
  10. package/dist/shared/rpc-client.d.ts +8 -0
  11. package/dist/shared/rpc-client.d.ts.map +1 -1
  12. package/dist/shared/rpc-notifications.d.ts +28 -10
  13. package/dist/shared/rpc-notifications.d.ts.map +1 -1
  14. package/dist/shared/rpc-server.d.ts +22 -3
  15. package/dist/shared/rpc-server.d.ts.map +1 -1
  16. package/dist/tui/data/context-db.d.ts +4 -14
  17. package/dist/tui/data/context-db.d.ts.map +1 -1
  18. package/dist/tui/data/notification-socket.d.ts +39 -0
  19. package/dist/tui/data/notification-socket.d.ts.map +1 -0
  20. package/package.json +2 -2
  21. package/src/shared/announcement.ts +2 -2
  22. package/src/shared/data-path.test.ts +28 -0
  23. package/src/shared/data-path.ts +5 -0
  24. package/src/shared/rpc-client.ts +14 -0
  25. package/src/shared/rpc-notifications.test.ts +68 -11
  26. package/src/shared/rpc-notifications.ts +75 -36
  27. package/src/shared/rpc-server.ts +249 -150
  28. package/src/tui/data/context-db.ts +10 -64
  29. package/src/tui/data/notification-socket.ts +229 -0
  30. package/src/tui/index.tsx +68 -118
package/dist/index.js CHANGED
@@ -64,6 +64,9 @@ function getMagicContextTempDir(harness = getHarness()) {
64
64
  return path.join(os.tmpdir(), harness, "magic-context");
65
65
  }
66
66
  function getMagicContextLogPath(harness = getHarness()) {
67
+ const envPath = process.env.MAGIC_CONTEXT_LOG_PATH?.trim();
68
+ if (envPath)
69
+ return envPath;
67
70
  return path.join(getMagicContextTempDir(harness), "magic-context.log");
68
71
  }
69
72
  function getProjectMagicContextDir(directory) {
@@ -157659,12 +157662,30 @@ var init_safe_notification_target = __esm(() => {
157659
157662
  // src/shared/rpc-notifications.ts
157660
157663
  var exports_rpc_notifications = {};
157661
157664
  __export(exports_rpc_notifications, {
157665
+ registerNotificationSink: () => registerNotificationSink,
157662
157666
  pushNotification: () => pushNotification,
157663
157667
  isTuiConnected: () => isTuiConnected,
157664
157668
  drainNotifications: () => drainNotifications
157665
157669
  });
157670
+ function registerNotificationSink(sink) {
157671
+ sinks.add(sink);
157672
+ return () => {
157673
+ sinks.delete(sink);
157674
+ };
157675
+ }
157676
+ function notificationMatchesSink(notification, sink) {
157677
+ return notification.sessionId === undefined || sink.sessionId === undefined || notification.sessionId === sink.sessionId;
157678
+ }
157666
157679
  function pushNotification(type, payload, sessionId) {
157667
- queue.push({ id: nextNotificationId++, type, payload, sessionId });
157680
+ const notification = { id: nextNotificationId++, type, payload, sessionId };
157681
+ queue.push(notification);
157682
+ for (const sink of sinks) {
157683
+ if (!notificationMatchesSink(notification, sink))
157684
+ continue;
157685
+ try {
157686
+ sink.send(notification);
157687
+ } catch {}
157688
+ }
157668
157689
  if (queue.length > 100) {
157669
157690
  const newestPerSession = new Map;
157670
157691
  for (const n of queue) {
@@ -157684,10 +157705,6 @@ function pushNotification(type, payload, sessionId) {
157684
157705
  }
157685
157706
  }
157686
157707
  function drainNotifications(lastReceivedId = 0, sessionId) {
157687
- const now = Date.now();
157688
- lastDrainAtAny = now;
157689
- if (sessionId !== undefined)
157690
- lastDrainAtBySession.set(sessionId, now);
157691
157708
  const matchesClient = (notification) => sessionId === undefined || notification.sessionId === undefined || notification.sessionId === sessionId;
157692
157709
  if (lastReceivedId > 0) {
157693
157710
  queue = queue.filter((notification) => !(notification.id <= lastReceivedId && matchesClient(notification)));
@@ -157695,17 +157712,20 @@ function drainNotifications(lastReceivedId = 0, sessionId) {
157695
157712
  return queue.filter((notification) => notification.id > lastReceivedId && matchesClient(notification));
157696
157713
  }
157697
157714
  function isTuiConnected(sessionId) {
157698
- const now = Date.now();
157699
- if (sessionId !== undefined) {
157700
- const at = lastDrainAtBySession.get(sessionId) ?? 0;
157701
- return at > 0 && now - at < TUI_CONNECTED_WINDOW_MS;
157715
+ if (sinks.size === 0)
157716
+ return false;
157717
+ if (sessionId === undefined)
157718
+ return true;
157719
+ for (const sink of sinks) {
157720
+ if (sink.sessionId === undefined || sink.sessionId === sessionId)
157721
+ return true;
157702
157722
  }
157703
- return lastDrainAtAny > 0 && now - lastDrainAtAny < TUI_CONNECTED_WINDOW_MS;
157723
+ return false;
157704
157724
  }
157705
- var queue, nextNotificationId = 1, lastDrainAtBySession, lastDrainAtAny = 0, TUI_CONNECTED_WINDOW_MS = 3000;
157725
+ var queue, nextNotificationId = 1, sinks;
157706
157726
  var init_rpc_notifications = __esm(() => {
157707
157727
  queue = [];
157708
- lastDrainAtBySession = new Map;
157728
+ sinks = new Set;
157709
157729
  });
157710
157730
 
157711
157731
  // src/plugin/conflict-warning-hook.ts
@@ -158381,6 +158401,94 @@ function parseFields(json2) {
158381
158401
  }
158382
158402
  var init_compartment_events = () => {};
158383
158403
 
158404
+ // src/features/magic-context/recursive-text-splitter.ts
158405
+ function splitOnSeparator(text, separator) {
158406
+ const splits = separator ? text.split(separator) : text.split("");
158407
+ return splits.filter((s) => s !== "");
158408
+ }
158409
+ function mergeSplits(splits, separator, chunkSize, lengthFunction) {
158410
+ const docs = [];
158411
+ const currentDoc = [];
158412
+ let total = 0;
158413
+ const joinDocs = (docsToJoin) => {
158414
+ const joined = docsToJoin.join(separator).trim();
158415
+ return joined === "" ? null : joined;
158416
+ };
158417
+ for (const d of splits) {
158418
+ const len = lengthFunction(d);
158419
+ if (total + len + currentDoc.length * separator.length > chunkSize) {
158420
+ if (currentDoc.length > 0) {
158421
+ const doc3 = joinDocs(currentDoc);
158422
+ if (doc3 !== null)
158423
+ docs.push(doc3);
158424
+ while (total > 0 && currentDoc.length > 0) {
158425
+ total -= lengthFunction(currentDoc[0]);
158426
+ currentDoc.shift();
158427
+ }
158428
+ }
158429
+ }
158430
+ currentDoc.push(d);
158431
+ total += len;
158432
+ }
158433
+ const doc2 = joinDocs(currentDoc);
158434
+ if (doc2 !== null)
158435
+ docs.push(doc2);
158436
+ return docs;
158437
+ }
158438
+ function splitTextRecursive(text, separators, chunkSize, lengthFunction) {
158439
+ const finalChunks = [];
158440
+ let separator = separators[separators.length - 1];
158441
+ let newSeparators;
158442
+ for (let i = 0;i < separators.length; i += 1) {
158443
+ const s = separators[i];
158444
+ if (s === "") {
158445
+ separator = s;
158446
+ break;
158447
+ }
158448
+ if (text.includes(s)) {
158449
+ separator = s;
158450
+ newSeparators = separators.slice(i + 1);
158451
+ break;
158452
+ }
158453
+ }
158454
+ const splits = splitOnSeparator(text, separator);
158455
+ let goodSplits = [];
158456
+ for (const s of splits) {
158457
+ if (lengthFunction(s) < chunkSize) {
158458
+ goodSplits.push(s);
158459
+ } else {
158460
+ if (goodSplits.length) {
158461
+ finalChunks.push(...mergeSplits(goodSplits, separator, chunkSize, lengthFunction));
158462
+ goodSplits = [];
158463
+ }
158464
+ if (!newSeparators) {
158465
+ finalChunks.push(s);
158466
+ } else {
158467
+ finalChunks.push(...splitTextRecursive(s, newSeparators, chunkSize, lengthFunction));
158468
+ }
158469
+ }
158470
+ }
158471
+ if (goodSplits.length) {
158472
+ finalChunks.push(...mergeSplits(goodSplits, separator, chunkSize, lengthFunction));
158473
+ }
158474
+ return finalChunks;
158475
+ }
158476
+ function recursiveCharacterSplit(text, options) {
158477
+ const chunkSize = options.chunkSize;
158478
+ const lengthFunction = options.lengthFunction ?? ((t) => t.length);
158479
+ const separators = options.separators ?? DEFAULT_SEPARATORS;
158480
+ if (text.length === 0)
158481
+ return [];
158482
+ return splitTextRecursive(text, separators, chunkSize, lengthFunction);
158483
+ }
158484
+ var DEFAULT_SEPARATORS;
158485
+ var init_recursive_text_splitter = __esm(() => {
158486
+ DEFAULT_SEPARATORS = [`
158487
+
158488
+ `, `
158489
+ `, " ", ""];
158490
+ });
158491
+
158384
158492
  // src/features/magic-context/compartment-chunk-embedding.ts
158385
158493
  import { createHash as createHash6 } from "node:crypto";
158386
158494
  function getLoadFtsRowsStatement(db) {
@@ -158648,6 +158756,19 @@ function chunkCanonicalText(canonicalText, startOrdinal, endOrdinal, maxInputTok
158648
158756
  const lineStart = range?.start ?? startOrdinal;
158649
158757
  const lineEnd = range?.end ?? lineStart;
158650
158758
  const lineTokens = estimateTokens(line);
158759
+ if (lineTokens > effectiveMax) {
158760
+ flush2();
158761
+ for (const slice of splitOversizedLine(line, effectiveMax)) {
158762
+ windows.push({
158763
+ windowIndex: windows.length + 1,
158764
+ startOrdinal: lineStart,
158765
+ endOrdinal: lineEnd,
158766
+ text: slice,
158767
+ chunkHash: hashChunkText(slice)
158768
+ });
158769
+ }
158770
+ continue;
158771
+ }
158651
158772
  if (currentLines.length > 0 && currentTokens + lineTokens > effectiveMax) {
158652
158773
  flush2();
158653
158774
  }
@@ -158661,6 +158782,56 @@ function chunkCanonicalText(canonicalText, startOrdinal, endOrdinal, maxInputTok
158661
158782
  flush2();
158662
158783
  return windows;
158663
158784
  }
158785
+ function splitOversizedLine(line, effectiveMax) {
158786
+ let slices = [];
158787
+ try {
158788
+ slices = recursiveCharacterSplit(line, {
158789
+ chunkSize: effectiveMax,
158790
+ lengthFunction: estimateTokens
158791
+ });
158792
+ } catch (error51) {
158793
+ log("[magic-context] recursiveCharacterSplit failed; using char-budget fallback:", error51);
158794
+ slices = [];
158795
+ }
158796
+ if (slices.length === 0) {
158797
+ slices = charBudgetSplit(line, effectiveMax);
158798
+ }
158799
+ const safe = [];
158800
+ const pushChecked = (slice) => {
158801
+ if (estimateTokens(slice) > effectiveMax && slice.length > 1) {
158802
+ safe.push(...charBudgetSplit(slice, effectiveMax));
158803
+ return;
158804
+ }
158805
+ safe.push(slice);
158806
+ };
158807
+ for (const slice of slices) {
158808
+ if (estimateTokens(slice) <= effectiveMax) {
158809
+ safe.push(slice);
158810
+ } else {
158811
+ for (const sub of charBudgetSplit(slice, effectiveMax))
158812
+ pushChecked(sub);
158813
+ }
158814
+ }
158815
+ return safe.filter((s) => s.length > 0);
158816
+ }
158817
+ function charBudgetSplit(text, effectiveMax) {
158818
+ const totalTokens = Math.max(1, estimateTokens(text));
158819
+ const charsPerToken = Math.max(1, Math.floor(text.length / totalTokens));
158820
+ const sliceChars = Math.max(1, effectiveMax * charsPerToken);
158821
+ const out = [];
158822
+ let pos = 0;
158823
+ while (pos < text.length) {
158824
+ let end = Math.min(text.length, pos + sliceChars);
158825
+ let slice = text.slice(pos, end);
158826
+ while (slice.length > 1 && estimateTokens(slice) > effectiveMax) {
158827
+ end = pos + Math.max(1, Math.floor((end - pos) / 2));
158828
+ slice = text.slice(pos, end);
158829
+ }
158830
+ out.push(slice);
158831
+ pos = end;
158832
+ }
158833
+ return out;
158834
+ }
158664
158835
  function getExistingChunkHashes(db, compartmentId, modelId, projectPath) {
158665
158836
  const scoped = typeof projectPath === "string" && projectPath.length > 0;
158666
158837
  const rows = scoped ? getExistingHashStatement(db, true).all(compartmentId, modelId, projectPath) : getExistingHashStatement(db, false).all(compartmentId, modelId);
@@ -158828,6 +158999,8 @@ function countSessionCompartmentEmbedCoverage(db, projectPath, sessionId, modelI
158828
158999
  var DEFAULT_COMPARTMENT_CHUNK_MAX_INPUT_TOKENS = 512, CHUNK_WINDOW_SAFETY_RATIO = 0.9, loadFtsRowsStatements, existingHashStatements, existingHashByProjectStatements, deleteByCompartmentStatements, insertEmbeddingStatements, distinctModelStatements, clearProjectStatements, clearProjectModelStatements, searchRowsStatements, searchRowsByModelStatements, backfillCandidateStatements, sessionBackfillCandidateStatements;
158829
159000
  var init_compartment_chunk_embedding = __esm(() => {
158830
159001
  init_read_session_formatting();
159002
+ init_logger();
159003
+ init_recursive_text_splitter();
158831
159004
  loadFtsRowsStatements = new WeakMap;
158832
159005
  existingHashStatements = new WeakMap;
158833
159006
  existingHashByProjectStatements = new WeakMap;
@@ -159036,6 +159209,7 @@ class OpenAICompatibleEmbeddingProvider {
159036
159209
  if (texts.length === 0) {
159037
159210
  return [];
159038
159211
  }
159212
+ const requestTexts = texts.map((t) => t.trim().length === 0 ? " " : t);
159039
159213
  if (!await this.initialize()) {
159040
159214
  return Array.from({ length: texts.length }, () => null);
159041
159215
  }
@@ -159067,7 +159241,7 @@ class OpenAICompatibleEmbeddingProvider {
159067
159241
  },
159068
159242
  body: JSON.stringify({
159069
159243
  model: this.model,
159070
- input: texts,
159244
+ input: requestTexts,
159071
159245
  ...inputTypeForRequest ? { input_type: inputTypeForRequest } : {},
159072
159246
  ...this.truncate ? { truncate: this.truncate } : {}
159073
159247
  }),
@@ -160149,6 +160323,32 @@ async function embedBatchForProject(projectIdentity, texts, signal, purpose = "p
160149
160323
  }
160150
160324
  return { vectors, modelId, generation };
160151
160325
  }
160326
+ async function embedTextsWindowBounded(projectIdentity, texts, signal) {
160327
+ if (texts.length <= MAX_WINDOWS_PER_EMBED_CALL) {
160328
+ return embedBatchForProject(projectIdentity, texts, signal);
160329
+ }
160330
+ const vectors = [];
160331
+ let modelId = null;
160332
+ let generation = null;
160333
+ for (let start = 0;start < texts.length; start += MAX_WINDOWS_PER_EMBED_CALL) {
160334
+ if (signal?.aborted)
160335
+ return null;
160336
+ const sub = texts.slice(start, start + MAX_WINDOWS_PER_EMBED_CALL);
160337
+ const result = await embedBatchForProject(projectIdentity, sub, signal);
160338
+ if (!result)
160339
+ return null;
160340
+ if (modelId === null) {
160341
+ modelId = result.modelId;
160342
+ generation = result.generation;
160343
+ } else if (result.modelId !== modelId || result.generation !== generation) {
160344
+ return null;
160345
+ }
160346
+ vectors.push(...result.vectors);
160347
+ }
160348
+ if (modelId === null || generation === null)
160349
+ return null;
160350
+ return { vectors, modelId, generation };
160351
+ }
160152
160352
  function isUnembeddedMemoryRow(row) {
160153
160353
  if (row === null || typeof row !== "object")
160154
160354
  return false;
@@ -160282,7 +160482,7 @@ async function embedCandidateChunkBatch(db, projectIdentity, modelId, candidates
160282
160482
  let result = null;
160283
160483
  const attemptStart = Date.now();
160284
160484
  try {
160285
- result = await embedBatchForProject(projectIdentity, texts, signal);
160485
+ result = await embedTextsWindowBounded(projectIdentity, texts, signal);
160286
160486
  } catch (error51) {
160287
160487
  log("[magic-context] failed to proactively embed compartment chunks:", error51);
160288
160488
  }
@@ -176489,11 +176689,11 @@ function shouldShowAnnouncement() {
176489
176689
  }
176490
176690
  return ordering > 0;
176491
176691
  }
176492
- var ANNOUNCEMENT_VERSION = "0.30.1", ANNOUNCEMENT_FEATURES, ANNOUNCEMENT_FOOTER = "Join us on Discord: https://discord.gg/F2uWxjGnU", STATE_FILENAME = "last_announced_version";
176692
+ var ANNOUNCEMENT_VERSION = "0.30.2", ANNOUNCEMENT_FEATURES, ANNOUNCEMENT_FOOTER = "Join us on Discord: https://discord.gg/F2uWxjGnU", STATE_FILENAME = "last_announced_version";
176493
176693
  var init_announcement = __esm(() => {
176494
176694
  init_data_path();
176495
176695
  ANNOUNCEMENT_FEATURES = [
176496
- "Local embeddings work on OpenCode Desktop again (#195): /ctx-embed no longer fails with 'Unsupported device: cpu' on the Desktop app."
176696
+ "Fixed high idle CPU from the TUI sidebar (#200): it now uses a single persistent connection to the plugin instead of polling, so an idle session no longer burns CPU."
176497
176697
  ];
176498
176698
  });
176499
176699
 
@@ -193710,7 +193910,6 @@ function calibrateBuckets(input) {
193710
193910
  // src/plugin/rpc-handlers.ts
193711
193911
  init_announcement();
193712
193912
  init_logger();
193713
- init_rpc_notifications();
193714
193913
  var workMetricsCarryBySession = new Map;
193715
193914
  function resolveSidebarWorkMetrics(db, sessionId, persistedNewWork, persistedTotalInput) {
193716
193915
  if (!openCodeDbExists()) {
@@ -194222,12 +194421,6 @@ function registerRpcHandlers(rpcServer, args) {
194222
194421
  const resolved = typeof config2.toast_duration_ms === "number" && Number.isFinite(config2.toast_duration_ms) ? config2.toast_duration_ms : 5000;
194223
194422
  return { toastDurationMs: resolved };
194224
194423
  });
194225
- rpcServer.handle("pending-notifications", async (params) => {
194226
- const lastReceivedId = Number(params.lastReceivedId ?? 0);
194227
- const sessionId = typeof params.sessionId === "string" && params.sessionId.length > 0 ? params.sessionId : undefined;
194228
- const notifications = drainNotifications(Number.isFinite(lastReceivedId) ? lastReceivedId : 0, sessionId);
194229
- return { messages: notifications };
194230
- });
194231
194424
  rpcServer.handle("get-announcement", async () => {
194232
194425
  if (!shouldShowAnnouncement()) {
194233
194426
  return { show: false };
@@ -195597,6 +195790,7 @@ init_models_dev_cache();
195597
195790
 
195598
195791
  // src/shared/rpc-server.ts
195599
195792
  init_logger();
195793
+ init_rpc_notifications();
195600
195794
  import { randomBytes, timingSafeEqual } from "node:crypto";
195601
195795
  import {
195602
195796
  chmodSync as chmodSync2,
@@ -195608,7 +195802,6 @@ import {
195608
195802
  unlinkSync as unlinkSync4,
195609
195803
  writeFileSync as writeFileSync8
195610
195804
  } from "node:fs";
195611
- import { createServer } from "node:http";
195612
195805
  import { dirname as dirname5 } from "node:path";
195613
195806
 
195614
195807
  // src/shared/rpc-utils.ts
@@ -195666,6 +195859,9 @@ function isValidPort(port) {
195666
195859
  }
195667
195860
 
195668
195861
  // src/shared/rpc-server.ts
195862
+ var MAX_BODY_BYTES = 1048576;
195863
+ var WS_AUTH_TIMEOUT_MS = 5000;
195864
+ var WS_CLOSE_UNAUTHORIZED = 4401;
195669
195865
  function tokensMatch(presented, expected) {
195670
195866
  const a = Buffer.from(presented, "utf8");
195671
195867
  const b = Buffer.from(expected, "utf8");
@@ -195681,6 +195877,7 @@ class MagicContextRpcServer {
195681
195877
  portFilePath;
195682
195878
  portDir;
195683
195879
  startedAt = Date.now();
195880
+ sockets = new Set;
195684
195881
  token = randomBytes(32).toString("hex");
195685
195882
  constructor(storageDir, directory) {
195686
195883
  this.portFilePath = rpcPortFilePath(storageDir, directory);
@@ -195690,49 +195887,77 @@ class MagicContextRpcServer {
195690
195887
  this.handlers.set(method, handler);
195691
195888
  }
195692
195889
  async start() {
195693
- return new Promise((resolve3, reject) => {
195694
- const server = createServer((req, res) => this.dispatch(req, res));
195695
- server.on("error", (err) => {
195696
- log(`[rpc] server error: ${err.message}`);
195697
- reject(err);
195698
- });
195699
- server.listen(0, "127.0.0.1", () => {
195700
- const addr = server.address();
195701
- if (!addr || typeof addr === "string") {
195702
- reject(new Error("Failed to get server address"));
195703
- return;
195704
- }
195705
- this.port = addr.port;
195706
- this.server = server;
195707
- try {
195708
- this.warnIfOtherLiveInstance();
195709
- const dir = dirname5(this.portFilePath);
195710
- mkdirSync9(dir, { recursive: true, mode: 448 });
195711
- try {
195712
- chmodSync2(dir, 448);
195713
- } catch {}
195714
- const tmpPath = `${this.portFilePath}.tmp`;
195715
- try {
195716
- rmSync2(tmpPath, { force: true });
195717
- } catch {}
195718
- writeFileSync8(tmpPath, JSON.stringify({
195719
- port: this.port,
195720
- pid: process.pid,
195721
- started_at: this.startedAt,
195722
- token: this.token
195723
- }), { encoding: "utf-8", mode: 384 });
195724
- renameSync3(tmpPath, this.portFilePath);
195725
- try {
195726
- chmodSync2(this.portFilePath, 384);
195727
- } catch {}
195728
- log(`[rpc] server listening on 127.0.0.1:${this.port}`);
195729
- } catch (err) {
195730
- log(`[rpc] failed to write port file: ${err}`);
195890
+ const self2 = this;
195891
+ const server = Bun.serve({
195892
+ port: 0,
195893
+ hostname: "127.0.0.1",
195894
+ fetch(req, srv) {
195895
+ return self2.handleFetch(req, srv);
195896
+ },
195897
+ websocket: {
195898
+ open(ws) {
195899
+ ws.data.authTimer = setTimeout(() => {
195900
+ if (!ws.data.authed)
195901
+ ws.close(WS_CLOSE_UNAUTHORIZED, "auth timeout");
195902
+ }, WS_AUTH_TIMEOUT_MS);
195903
+ },
195904
+ message(ws, raw) {
195905
+ self2.handleWsMessage(ws, raw);
195906
+ },
195907
+ close(ws) {
195908
+ if (ws.data.authTimer)
195909
+ clearTimeout(ws.data.authTimer);
195910
+ ws.data.unregister?.();
195911
+ self2.sockets.delete(ws);
195731
195912
  }
195732
- resolve3(this.port);
195733
- });
195734
- server.unref();
195913
+ }
195735
195914
  });
195915
+ this.server = server;
195916
+ this.port = server.port ?? 0;
195917
+ try {
195918
+ this.warnIfOtherLiveInstance();
195919
+ const dir = dirname5(this.portFilePath);
195920
+ mkdirSync9(dir, { recursive: true, mode: 448 });
195921
+ try {
195922
+ chmodSync2(dir, 448);
195923
+ } catch {}
195924
+ const tmpPath = `${this.portFilePath}.tmp`;
195925
+ try {
195926
+ rmSync2(tmpPath, { force: true });
195927
+ } catch {}
195928
+ writeFileSync8(tmpPath, JSON.stringify({
195929
+ port: this.port,
195930
+ pid: process.pid,
195931
+ started_at: this.startedAt,
195932
+ token: this.token
195933
+ }), { encoding: "utf-8", mode: 384 });
195934
+ renameSync3(tmpPath, this.portFilePath);
195935
+ try {
195936
+ chmodSync2(this.portFilePath, 384);
195937
+ } catch {}
195938
+ log(`[rpc] server listening on 127.0.0.1:${this.port}`);
195939
+ } catch (err) {
195940
+ log(`[rpc] failed to write port file: ${err}`);
195941
+ }
195942
+ return this.port;
195943
+ }
195944
+ stop() {
195945
+ for (const ws of this.sockets) {
195946
+ try {
195947
+ if (ws.data.authTimer)
195948
+ clearTimeout(ws.data.authTimer);
195949
+ ws.data.unregister?.();
195950
+ ws.close();
195951
+ } catch {}
195952
+ }
195953
+ this.sockets.clear();
195954
+ if (this.server) {
195955
+ this.server.stop(true);
195956
+ this.server = null;
195957
+ }
195958
+ try {
195959
+ unlinkSync4(this.portFilePath);
195960
+ } catch {}
195736
195961
  }
195737
195962
  warnIfOtherLiveInstance() {
195738
195963
  try {
@@ -195747,73 +195972,99 @@ class MagicContextRpcServer {
195747
195972
  }
195748
195973
  } catch {}
195749
195974
  }
195750
- stop() {
195751
- if (this.server) {
195752
- this.server.close();
195753
- this.server = null;
195975
+ async handleFetch(req, srv) {
195976
+ const url2 = new URL(req.url);
195977
+ if (url2.pathname === "/ws") {
195978
+ const ok = srv.upgrade(req, { data: { authed: false } });
195979
+ if (ok)
195980
+ return;
195981
+ return new Response("upgrade failed", { status: 400 });
195754
195982
  }
195755
- try {
195756
- unlinkSync4(this.portFilePath);
195757
- } catch {}
195758
- }
195759
- dispatch(req, res) {
195760
- const url2 = req.url ?? "";
195761
- if (req.method === "GET" && url2 === "/health") {
195762
- res.writeHead(200, { "Content-Type": "application/json" });
195763
- res.end(JSON.stringify({ ok: true, pid: process.pid }));
195764
- return;
195983
+ if (req.method === "GET" && url2.pathname === "/health") {
195984
+ return json2({ ok: true, pid: process.pid });
195765
195985
  }
195766
- if (req.method !== "POST" || !url2.startsWith("/rpc/")) {
195767
- res.writeHead(404);
195768
- res.end("Not Found");
195769
- return;
195986
+ if (req.method !== "POST" || !url2.pathname.startsWith("/rpc/")) {
195987
+ return new Response("Not Found", { status: 404 });
195770
195988
  }
195771
- const auth = req.headers.authorization;
195989
+ const auth = req.headers.get("authorization");
195772
195990
  const presented = typeof auth === "string" ? auth.replace(/^Bearer\s+/i, "") : "";
195773
195991
  if (!tokensMatch(presented, this.token)) {
195774
- res.writeHead(401, { "Content-Type": "application/json" });
195775
- res.end(JSON.stringify({ error: "Unauthorized" }));
195776
- req.resume();
195777
- return;
195992
+ return json2({ error: "Unauthorized" }, 401);
195778
195993
  }
195779
- const method = url2.slice(5);
195994
+ const method = url2.pathname.slice(5);
195780
195995
  const handler = this.handlers.get(method);
195781
195996
  if (!handler) {
195782
- res.writeHead(404, { "Content-Type": "application/json" });
195783
- res.end(JSON.stringify({ error: `Unknown method: ${method}` }));
195784
- return;
195997
+ return json2({ error: `Unknown method: ${method}` }, 404);
195785
195998
  }
195786
- let body = "";
195787
- req.on("data", (chunk) => {
195788
- body += chunk.toString();
195789
- if (body.length > 1048576) {
195790
- res.writeHead(413);
195791
- res.end("Request too large");
195792
- req.destroy();
195793
- }
195794
- });
195795
- req.on("end", () => {
195796
- let params = {};
195999
+ const bodyText = await req.text();
196000
+ if (bodyText.length > MAX_BODY_BYTES) {
196001
+ return new Response("Request too large", { status: 413 });
196002
+ }
196003
+ let params = {};
196004
+ if (bodyText.length > 0) {
195797
196005
  try {
195798
- if (body.length > 0) {
195799
- params = JSON.parse(body);
195800
- }
196006
+ params = JSON.parse(bodyText);
195801
196007
  } catch {
195802
- res.writeHead(400, { "Content-Type": "application/json" });
195803
- res.end(JSON.stringify({ error: "Invalid JSON" }));
196008
+ return json2({ error: "Invalid JSON" }, 400);
196009
+ }
196010
+ }
196011
+ try {
196012
+ const result = await handler(params);
196013
+ return json2(result);
196014
+ } catch (err) {
196015
+ log(`[rpc] handler error: ${method} => ${err}`);
196016
+ return json2({ error: String(err) }, 500);
196017
+ }
196018
+ }
196019
+ handleWsMessage(ws, raw) {
196020
+ let msg;
196021
+ try {
196022
+ msg = JSON.parse(typeof raw === "string" ? raw : raw.toString("utf8"));
196023
+ } catch {
196024
+ return;
196025
+ }
196026
+ if (msg.type === "hello") {
196027
+ if (!tokensMatch(typeof msg.token === "string" ? msg.token : "", this.token)) {
196028
+ ws.send(JSON.stringify({ type: "error", error: "unauthorized" }));
196029
+ ws.close(WS_CLOSE_UNAUTHORIZED, "bad token");
195804
196030
  return;
195805
196031
  }
195806
- handler(params).then((result) => {
195807
- res.writeHead(200, { "Content-Type": "application/json" });
195808
- res.end(JSON.stringify(result));
195809
- }).catch((err) => {
195810
- log(`[rpc] handler error: ${method} => ${err}`);
195811
- res.writeHead(500, { "Content-Type": "application/json" });
195812
- res.end(JSON.stringify({ error: String(err) }));
195813
- });
195814
- });
196032
+ if (ws.data.authTimer) {
196033
+ clearTimeout(ws.data.authTimer);
196034
+ ws.data.authTimer = undefined;
196035
+ }
196036
+ ws.data.authed = true;
196037
+ ws.data.sessionId = typeof msg.sessionId === "string" && msg.sessionId.length > 0 ? msg.sessionId : undefined;
196038
+ const sink = {
196039
+ sessionId: ws.data.sessionId,
196040
+ send: (notification) => {
196041
+ ws.send(JSON.stringify({ type: "notification", notification }));
196042
+ }
196043
+ };
196044
+ ws.data.unregister = registerNotificationSink(sink);
196045
+ this.sockets.add(ws);
196046
+ const lastReceivedId = Number(msg.lastReceivedId ?? 0);
196047
+ const backlog = drainNotifications(Number.isFinite(lastReceivedId) ? lastReceivedId : 0, ws.data.sessionId);
196048
+ for (const notification of backlog) {
196049
+ ws.send(JSON.stringify({ type: "notification", notification }));
196050
+ }
196051
+ ws.send(JSON.stringify({ type: "hello-ack" }));
196052
+ return;
196053
+ }
196054
+ if (msg.type === "ack") {
196055
+ const lastReceivedId = Number(msg.lastReceivedId ?? 0);
196056
+ if (Number.isFinite(lastReceivedId) && lastReceivedId > 0) {
196057
+ drainNotifications(lastReceivedId, ws.data.sessionId);
196058
+ }
196059
+ }
195815
196060
  }
195816
196061
  }
196062
+ function json2(body, status = 200) {
196063
+ return new Response(JSON.stringify(body), {
196064
+ status,
196065
+ headers: { "Content-Type": "application/json" }
196066
+ });
196067
+ }
195817
196068
 
195818
196069
  // src/index.ts
195819
196070
  var server = async (ctx) => {
@@ -1 +1 @@
1
- {"version":3,"file":"rpc-handlers.d.ts","sourceRoot":"","sources":["../../src/plugin/rpc-handlers.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AAIzE,OAAO,EACH,KAAK,eAAe,IAAI,QAAQ,EAGnC,MAAM,mCAAmC,CAAC;AAc3C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2CAA2C,CAAC;AAqBlF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAClE,OAAO,KAAK,EAAe,eAAe,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AA0FtF,wBAAgB,oBAAoB,CAChC,EAAE,EAAE,QAAQ,EACZ,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,gBAAgB,CAAC,EAAE,gBAAgB,EACnC,qBAAqB,CAAC,EAAE,MAAM,EAK9B,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACjC,eAAe,CAuVjB;AAED,wBAAgB,iBAAiB,CAC7B,EAAE,EAAE,QAAQ,EACZ,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,QAAQ,CAAC,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChC,gBAAgB,CAAC,EAAE,gBAAgB,EACnC,qBAAqB,CAAC,EAAE,MAAM,GAC/B,YAAY,CAyKd;AA4BD;;GAEG;AACH,wBAAgB,mBAAmB,CAC/B,SAAS,EAAE,qBAAqB,EAChC,IAAI,EAAE;IACF,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,kBAAkB,CAAC;IAC3B,MAAM,EAAE,OAAO,CAAC;IAChB,gBAAgB,EAAE,gBAAgB,CAAC;CACtC,GACF,IAAI,CAyPN"}
1
+ {"version":3,"file":"rpc-handlers.d.ts","sourceRoot":"","sources":["../../src/plugin/rpc-handlers.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AAIzE,OAAO,EACH,KAAK,eAAe,IAAI,QAAQ,EAGnC,MAAM,mCAAmC,CAAC;AAc3C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2CAA2C,CAAC;AAoBlF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAClE,OAAO,KAAK,EAAe,eAAe,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AA0FtF,wBAAgB,oBAAoB,CAChC,EAAE,EAAE,QAAQ,EACZ,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,gBAAgB,CAAC,EAAE,gBAAgB,EACnC,qBAAqB,CAAC,EAAE,MAAM,EAK9B,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACjC,eAAe,CAuVjB;AAED,wBAAgB,iBAAiB,CAC7B,EAAE,EAAE,QAAQ,EACZ,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,QAAQ,CAAC,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChC,gBAAgB,CAAC,EAAE,gBAAgB,EACnC,qBAAqB,CAAC,EAAE,MAAM,GAC/B,YAAY,CAyKd;AA4BD;;GAEG;AACH,wBAAgB,mBAAmB,CAC/B,SAAS,EAAE,qBAAqB,EAChC,IAAI,EAAE;IACF,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,kBAAkB,CAAC;IAC3B,MAAM,EAAE,OAAO,CAAC;IAChB,gBAAgB,EAAE,gBAAgB,CAAC;CACtC,GACF,IAAI,CA4ON"}
@@ -18,7 +18,7 @@
18
18
  * Bump only when there are user-visible changes worth a startup dialog.
19
19
  * Does NOT need to match the published package version.
20
20
  */
21
- export declare const ANNOUNCEMENT_VERSION = "0.30.1";
21
+ export declare const ANNOUNCEMENT_VERSION = "0.30.2";
22
22
  /**
23
23
  * Short, user-facing bullet strings. Keep each line ~80 chars or shorter so the
24
24
  * TUI dialog renders cleanly without horizontal scroll on a typical terminal.
@@ -1 +1 @@
1
- {"version":3,"file":"data-path.d.ts","sourceRoot":"","sources":["../../src/shared/data-path.ts"],"names":[],"mappings":"AAGA,OAAO,EAAc,KAAK,SAAS,EAAE,MAAM,WAAW,CAAC;AAEvD,wBAAgB,UAAU,IAAI,MAAM,CAEnC;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,GAAE,SAAwB,GAAG,MAAM,CAEhF;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,GAAE,SAAwB,GAAG,MAAM,CAEhF;AAED;;;;;;GAMG;AACH,wBAAgB,2BAA2B,CAAC,OAAO,GAAE,SAAwB,GAAG,MAAM,CAErF;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,yBAAyB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEnE;AAKD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gCAAgC,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAkBxE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kCAAkC,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAE5E;AAED,wBAAgB,qBAAqB,IAAI,MAAM,CAE9C;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,yBAAyB,IAAI,MAAM,CAElD;AAED;;;;;;GAMG;AACH,wBAAgB,uCAAuC,IAAI,MAAM,CAEhE;AAED;;;;;;;;;GASG;AACH,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAED,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C"}
1
+ {"version":3,"file":"data-path.d.ts","sourceRoot":"","sources":["../../src/shared/data-path.ts"],"names":[],"mappings":"AAGA,OAAO,EAAc,KAAK,SAAS,EAAE,MAAM,WAAW,CAAC;AAEvD,wBAAgB,UAAU,IAAI,MAAM,CAEnC;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,GAAE,SAAwB,GAAG,MAAM,CAEhF;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,GAAE,SAAwB,GAAG,MAAM,CAOhF;AAED;;;;;;GAMG;AACH,wBAAgB,2BAA2B,CAAC,OAAO,GAAE,SAAwB,GAAG,MAAM,CAErF;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,yBAAyB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEnE;AAKD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gCAAgC,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAkBxE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kCAAkC,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAE5E;AAED,wBAAgB,qBAAqB,IAAI,MAAM,CAE9C;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,yBAAyB,IAAI,MAAM,CAElD;AAED;;;;;;GAMG;AACH,wBAAgB,uCAAuC,IAAI,MAAM,CAEhE;AAED;;;;;;;;;GASG;AACH,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAED,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C"}