@cortexkit/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 (31) hide show
  1. package/README.md +1 -1
  2. package/dist/features/magic-context/compartment-chunk-embedding.d.ts.map +1 -1
  3. package/dist/features/magic-context/memory/embedding-openai.d.ts.map +1 -1
  4. package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
  5. package/dist/features/magic-context/recursive-text-splitter.d.ts +36 -0
  6. package/dist/features/magic-context/recursive-text-splitter.d.ts.map +1 -0
  7. package/dist/index.js +368 -117
  8. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  9. package/dist/shared/announcement.d.ts +1 -1
  10. package/dist/shared/data-path.d.ts.map +1 -1
  11. package/dist/shared/rpc-client.d.ts +8 -0
  12. package/dist/shared/rpc-client.d.ts.map +1 -1
  13. package/dist/shared/rpc-notifications.d.ts +28 -10
  14. package/dist/shared/rpc-notifications.d.ts.map +1 -1
  15. package/dist/shared/rpc-server.d.ts +22 -3
  16. package/dist/shared/rpc-server.d.ts.map +1 -1
  17. package/dist/tui/data/context-db.d.ts +4 -14
  18. package/dist/tui/data/context-db.d.ts.map +1 -1
  19. package/dist/tui/data/notification-socket.d.ts +39 -0
  20. package/dist/tui/data/notification-socket.d.ts.map +1 -0
  21. package/package.json +1 -1
  22. package/src/shared/announcement.ts +2 -2
  23. package/src/shared/data-path.test.ts +28 -0
  24. package/src/shared/data-path.ts +5 -0
  25. package/src/shared/rpc-client.ts +14 -0
  26. package/src/shared/rpc-notifications.test.ts +68 -11
  27. package/src/shared/rpc-notifications.ts +75 -36
  28. package/src/shared/rpc-server.ts +249 -150
  29. package/src/tui/data/context-db.ts +10 -64
  30. package/src/tui/data/notification-socket.ts +229 -0
  31. 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) {
@@ -165515,12 +165518,30 @@ var init_safe_notification_target = __esm(() => {
165515
165518
  // src/shared/rpc-notifications.ts
165516
165519
  var exports_rpc_notifications = {};
165517
165520
  __export(exports_rpc_notifications, {
165521
+ registerNotificationSink: () => registerNotificationSink,
165518
165522
  pushNotification: () => pushNotification,
165519
165523
  isTuiConnected: () => isTuiConnected,
165520
165524
  drainNotifications: () => drainNotifications
165521
165525
  });
165526
+ function registerNotificationSink(sink) {
165527
+ sinks.add(sink);
165528
+ return () => {
165529
+ sinks.delete(sink);
165530
+ };
165531
+ }
165532
+ function notificationMatchesSink(notification, sink) {
165533
+ return notification.sessionId === undefined || sink.sessionId === undefined || notification.sessionId === sink.sessionId;
165534
+ }
165522
165535
  function pushNotification(type, payload, sessionId) {
165523
- queue.push({ id: nextNotificationId++, type, payload, sessionId });
165536
+ const notification = { id: nextNotificationId++, type, payload, sessionId };
165537
+ queue.push(notification);
165538
+ for (const sink of sinks) {
165539
+ if (!notificationMatchesSink(notification, sink))
165540
+ continue;
165541
+ try {
165542
+ sink.send(notification);
165543
+ } catch {}
165544
+ }
165524
165545
  if (queue.length > 100) {
165525
165546
  const newestPerSession = new Map;
165526
165547
  for (const n of queue) {
@@ -165540,10 +165561,6 @@ function pushNotification(type, payload, sessionId) {
165540
165561
  }
165541
165562
  }
165542
165563
  function drainNotifications(lastReceivedId = 0, sessionId) {
165543
- const now = Date.now();
165544
- lastDrainAtAny = now;
165545
- if (sessionId !== undefined)
165546
- lastDrainAtBySession.set(sessionId, now);
165547
165564
  const matchesClient = (notification) => sessionId === undefined || notification.sessionId === undefined || notification.sessionId === sessionId;
165548
165565
  if (lastReceivedId > 0) {
165549
165566
  queue = queue.filter((notification) => !(notification.id <= lastReceivedId && matchesClient(notification)));
@@ -165551,17 +165568,20 @@ function drainNotifications(lastReceivedId = 0, sessionId) {
165551
165568
  return queue.filter((notification) => notification.id > lastReceivedId && matchesClient(notification));
165552
165569
  }
165553
165570
  function isTuiConnected(sessionId) {
165554
- const now = Date.now();
165555
- if (sessionId !== undefined) {
165556
- const at = lastDrainAtBySession.get(sessionId) ?? 0;
165557
- return at > 0 && now - at < TUI_CONNECTED_WINDOW_MS;
165571
+ if (sinks.size === 0)
165572
+ return false;
165573
+ if (sessionId === undefined)
165574
+ return true;
165575
+ for (const sink of sinks) {
165576
+ if (sink.sessionId === undefined || sink.sessionId === sessionId)
165577
+ return true;
165558
165578
  }
165559
- return lastDrainAtAny > 0 && now - lastDrainAtAny < TUI_CONNECTED_WINDOW_MS;
165579
+ return false;
165560
165580
  }
165561
- var queue, nextNotificationId = 1, lastDrainAtBySession, lastDrainAtAny = 0, TUI_CONNECTED_WINDOW_MS = 3000;
165581
+ var queue, nextNotificationId = 1, sinks;
165562
165582
  var init_rpc_notifications = __esm(() => {
165563
165583
  queue = [];
165564
- lastDrainAtBySession = new Map;
165584
+ sinks = new Set;
165565
165585
  });
165566
165586
 
165567
165587
  // src/plugin/conflict-warning-hook.ts
@@ -166237,6 +166257,94 @@ function parseFields(json2) {
166237
166257
  }
166238
166258
  var init_compartment_events = () => {};
166239
166259
 
166260
+ // src/features/magic-context/recursive-text-splitter.ts
166261
+ function splitOnSeparator(text, separator) {
166262
+ const splits = separator ? text.split(separator) : text.split("");
166263
+ return splits.filter((s) => s !== "");
166264
+ }
166265
+ function mergeSplits(splits, separator, chunkSize, lengthFunction) {
166266
+ const docs = [];
166267
+ const currentDoc = [];
166268
+ let total = 0;
166269
+ const joinDocs = (docsToJoin) => {
166270
+ const joined = docsToJoin.join(separator).trim();
166271
+ return joined === "" ? null : joined;
166272
+ };
166273
+ for (const d of splits) {
166274
+ const len = lengthFunction(d);
166275
+ if (total + len + currentDoc.length * separator.length > chunkSize) {
166276
+ if (currentDoc.length > 0) {
166277
+ const doc3 = joinDocs(currentDoc);
166278
+ if (doc3 !== null)
166279
+ docs.push(doc3);
166280
+ while (total > 0 && currentDoc.length > 0) {
166281
+ total -= lengthFunction(currentDoc[0]);
166282
+ currentDoc.shift();
166283
+ }
166284
+ }
166285
+ }
166286
+ currentDoc.push(d);
166287
+ total += len;
166288
+ }
166289
+ const doc2 = joinDocs(currentDoc);
166290
+ if (doc2 !== null)
166291
+ docs.push(doc2);
166292
+ return docs;
166293
+ }
166294
+ function splitTextRecursive(text, separators, chunkSize, lengthFunction) {
166295
+ const finalChunks = [];
166296
+ let separator = separators[separators.length - 1];
166297
+ let newSeparators;
166298
+ for (let i = 0;i < separators.length; i += 1) {
166299
+ const s = separators[i];
166300
+ if (s === "") {
166301
+ separator = s;
166302
+ break;
166303
+ }
166304
+ if (text.includes(s)) {
166305
+ separator = s;
166306
+ newSeparators = separators.slice(i + 1);
166307
+ break;
166308
+ }
166309
+ }
166310
+ const splits = splitOnSeparator(text, separator);
166311
+ let goodSplits = [];
166312
+ for (const s of splits) {
166313
+ if (lengthFunction(s) < chunkSize) {
166314
+ goodSplits.push(s);
166315
+ } else {
166316
+ if (goodSplits.length) {
166317
+ finalChunks.push(...mergeSplits(goodSplits, separator, chunkSize, lengthFunction));
166318
+ goodSplits = [];
166319
+ }
166320
+ if (!newSeparators) {
166321
+ finalChunks.push(s);
166322
+ } else {
166323
+ finalChunks.push(...splitTextRecursive(s, newSeparators, chunkSize, lengthFunction));
166324
+ }
166325
+ }
166326
+ }
166327
+ if (goodSplits.length) {
166328
+ finalChunks.push(...mergeSplits(goodSplits, separator, chunkSize, lengthFunction));
166329
+ }
166330
+ return finalChunks;
166331
+ }
166332
+ function recursiveCharacterSplit(text, options) {
166333
+ const chunkSize = options.chunkSize;
166334
+ const lengthFunction = options.lengthFunction ?? ((t) => t.length);
166335
+ const separators = options.separators ?? DEFAULT_SEPARATORS;
166336
+ if (text.length === 0)
166337
+ return [];
166338
+ return splitTextRecursive(text, separators, chunkSize, lengthFunction);
166339
+ }
166340
+ var DEFAULT_SEPARATORS;
166341
+ var init_recursive_text_splitter = __esm(() => {
166342
+ DEFAULT_SEPARATORS = [`
166343
+
166344
+ `, `
166345
+ `, " ", ""];
166346
+ });
166347
+
166240
166348
  // src/features/magic-context/compartment-chunk-embedding.ts
166241
166349
  import { createHash as createHash6 } from "node:crypto";
166242
166350
  function getLoadFtsRowsStatement(db) {
@@ -166504,6 +166612,19 @@ function chunkCanonicalText(canonicalText, startOrdinal, endOrdinal, maxInputTok
166504
166612
  const lineStart = range?.start ?? startOrdinal;
166505
166613
  const lineEnd = range?.end ?? lineStart;
166506
166614
  const lineTokens = estimateTokens(line);
166615
+ if (lineTokens > effectiveMax) {
166616
+ flush2();
166617
+ for (const slice of splitOversizedLine(line, effectiveMax)) {
166618
+ windows.push({
166619
+ windowIndex: windows.length + 1,
166620
+ startOrdinal: lineStart,
166621
+ endOrdinal: lineEnd,
166622
+ text: slice,
166623
+ chunkHash: hashChunkText(slice)
166624
+ });
166625
+ }
166626
+ continue;
166627
+ }
166507
166628
  if (currentLines.length > 0 && currentTokens + lineTokens > effectiveMax) {
166508
166629
  flush2();
166509
166630
  }
@@ -166517,6 +166638,56 @@ function chunkCanonicalText(canonicalText, startOrdinal, endOrdinal, maxInputTok
166517
166638
  flush2();
166518
166639
  return windows;
166519
166640
  }
166641
+ function splitOversizedLine(line, effectiveMax) {
166642
+ let slices = [];
166643
+ try {
166644
+ slices = recursiveCharacterSplit(line, {
166645
+ chunkSize: effectiveMax,
166646
+ lengthFunction: estimateTokens
166647
+ });
166648
+ } catch (error51) {
166649
+ log("[magic-context] recursiveCharacterSplit failed; using char-budget fallback:", error51);
166650
+ slices = [];
166651
+ }
166652
+ if (slices.length === 0) {
166653
+ slices = charBudgetSplit(line, effectiveMax);
166654
+ }
166655
+ const safe = [];
166656
+ const pushChecked = (slice) => {
166657
+ if (estimateTokens(slice) > effectiveMax && slice.length > 1) {
166658
+ safe.push(...charBudgetSplit(slice, effectiveMax));
166659
+ return;
166660
+ }
166661
+ safe.push(slice);
166662
+ };
166663
+ for (const slice of slices) {
166664
+ if (estimateTokens(slice) <= effectiveMax) {
166665
+ safe.push(slice);
166666
+ } else {
166667
+ for (const sub of charBudgetSplit(slice, effectiveMax))
166668
+ pushChecked(sub);
166669
+ }
166670
+ }
166671
+ return safe.filter((s) => s.length > 0);
166672
+ }
166673
+ function charBudgetSplit(text, effectiveMax) {
166674
+ const totalTokens = Math.max(1, estimateTokens(text));
166675
+ const charsPerToken = Math.max(1, Math.floor(text.length / totalTokens));
166676
+ const sliceChars = Math.max(1, effectiveMax * charsPerToken);
166677
+ const out = [];
166678
+ let pos = 0;
166679
+ while (pos < text.length) {
166680
+ let end = Math.min(text.length, pos + sliceChars);
166681
+ let slice = text.slice(pos, end);
166682
+ while (slice.length > 1 && estimateTokens(slice) > effectiveMax) {
166683
+ end = pos + Math.max(1, Math.floor((end - pos) / 2));
166684
+ slice = text.slice(pos, end);
166685
+ }
166686
+ out.push(slice);
166687
+ pos = end;
166688
+ }
166689
+ return out;
166690
+ }
166520
166691
  function getExistingChunkHashes(db, compartmentId, modelId, projectPath) {
166521
166692
  const scoped = typeof projectPath === "string" && projectPath.length > 0;
166522
166693
  const rows = scoped ? getExistingHashStatement(db, true).all(compartmentId, modelId, projectPath) : getExistingHashStatement(db, false).all(compartmentId, modelId);
@@ -166684,6 +166855,8 @@ function countSessionCompartmentEmbedCoverage(db, projectPath, sessionId, modelI
166684
166855
  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;
166685
166856
  var init_compartment_chunk_embedding = __esm(() => {
166686
166857
  init_read_session_formatting();
166858
+ init_logger();
166859
+ init_recursive_text_splitter();
166687
166860
  loadFtsRowsStatements = new WeakMap;
166688
166861
  existingHashStatements = new WeakMap;
166689
166862
  existingHashByProjectStatements = new WeakMap;
@@ -167268,6 +167441,7 @@ class OpenAICompatibleEmbeddingProvider {
167268
167441
  if (texts.length === 0) {
167269
167442
  return [];
167270
167443
  }
167444
+ const requestTexts = texts.map((t) => t.trim().length === 0 ? " " : t);
167271
167445
  if (!await this.initialize()) {
167272
167446
  return Array.from({ length: texts.length }, () => null);
167273
167447
  }
@@ -167299,7 +167473,7 @@ class OpenAICompatibleEmbeddingProvider {
167299
167473
  },
167300
167474
  body: JSON.stringify({
167301
167475
  model: this.model,
167302
- input: texts,
167476
+ input: requestTexts,
167303
167477
  ...inputTypeForRequest ? { input_type: inputTypeForRequest } : {},
167304
167478
  ...this.truncate ? { truncate: this.truncate } : {}
167305
167479
  }),
@@ -168387,6 +168561,32 @@ async function embedBatchForProject(projectIdentity, texts, signal, purpose = "p
168387
168561
  }
168388
168562
  return { vectors, modelId, generation };
168389
168563
  }
168564
+ async function embedTextsWindowBounded(projectIdentity, texts, signal) {
168565
+ if (texts.length <= MAX_WINDOWS_PER_EMBED_CALL) {
168566
+ return embedBatchForProject(projectIdentity, texts, signal);
168567
+ }
168568
+ const vectors = [];
168569
+ let modelId = null;
168570
+ let generation = null;
168571
+ for (let start = 0;start < texts.length; start += MAX_WINDOWS_PER_EMBED_CALL) {
168572
+ if (signal?.aborted)
168573
+ return null;
168574
+ const sub = texts.slice(start, start + MAX_WINDOWS_PER_EMBED_CALL);
168575
+ const result = await embedBatchForProject(projectIdentity, sub, signal);
168576
+ if (!result)
168577
+ return null;
168578
+ if (modelId === null) {
168579
+ modelId = result.modelId;
168580
+ generation = result.generation;
168581
+ } else if (result.modelId !== modelId || result.generation !== generation) {
168582
+ return null;
168583
+ }
168584
+ vectors.push(...result.vectors);
168585
+ }
168586
+ if (modelId === null || generation === null)
168587
+ return null;
168588
+ return { vectors, modelId, generation };
168589
+ }
168390
168590
  function isUnembeddedMemoryRow(row) {
168391
168591
  if (row === null || typeof row !== "object")
168392
168592
  return false;
@@ -168520,7 +168720,7 @@ async function embedCandidateChunkBatch(db, projectIdentity, modelId, candidates
168520
168720
  let result = null;
168521
168721
  const attemptStart = Date.now();
168522
168722
  try {
168523
- result = await embedBatchForProject(projectIdentity, texts, signal);
168723
+ result = await embedTextsWindowBounded(projectIdentity, texts, signal);
168524
168724
  } catch (error51) {
168525
168725
  log("[magic-context] failed to proactively embed compartment chunks:", error51);
168526
168726
  }
@@ -184723,11 +184923,11 @@ function shouldShowAnnouncement() {
184723
184923
  }
184724
184924
  return ordering > 0;
184725
184925
  }
184726
- var ANNOUNCEMENT_VERSION = "0.30.1", ANNOUNCEMENT_FEATURES, ANNOUNCEMENT_FOOTER = "Join us on Discord: https://discord.gg/F2uWxjGnU", STATE_FILENAME = "last_announced_version";
184926
+ var ANNOUNCEMENT_VERSION = "0.30.2", ANNOUNCEMENT_FEATURES, ANNOUNCEMENT_FOOTER = "Join us on Discord: https://discord.gg/F2uWxjGnU", STATE_FILENAME = "last_announced_version";
184727
184927
  var init_announcement = __esm(() => {
184728
184928
  init_data_path();
184729
184929
  ANNOUNCEMENT_FEATURES = [
184730
- "Local embeddings work on OpenCode Desktop again (#195): /ctx-embed no longer fails with 'Unsupported device: cpu' on the Desktop app."
184930
+ "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."
184731
184931
  ];
184732
184932
  });
184733
184933
 
@@ -202626,7 +202826,6 @@ function calibrateBuckets(input) {
202626
202826
  // src/plugin/rpc-handlers.ts
202627
202827
  init_announcement();
202628
202828
  init_logger();
202629
- init_rpc_notifications();
202630
202829
  var workMetricsCarryBySession = new Map;
202631
202830
  function resolveSidebarWorkMetrics(db, sessionId, persistedNewWork, persistedTotalInput) {
202632
202831
  if (!openCodeDbExists()) {
@@ -203138,12 +203337,6 @@ function registerRpcHandlers(rpcServer, args) {
203138
203337
  const resolved = typeof config2.toast_duration_ms === "number" && Number.isFinite(config2.toast_duration_ms) ? config2.toast_duration_ms : 5000;
203139
203338
  return { toastDurationMs: resolved };
203140
203339
  });
203141
- rpcServer.handle("pending-notifications", async (params) => {
203142
- const lastReceivedId = Number(params.lastReceivedId ?? 0);
203143
- const sessionId = typeof params.sessionId === "string" && params.sessionId.length > 0 ? params.sessionId : undefined;
203144
- const notifications = drainNotifications(Number.isFinite(lastReceivedId) ? lastReceivedId : 0, sessionId);
203145
- return { messages: notifications };
203146
- });
203147
203340
  rpcServer.handle("get-announcement", async () => {
203148
203341
  if (!shouldShowAnnouncement()) {
203149
203342
  return { show: false };
@@ -204536,6 +204729,7 @@ init_models_dev_cache();
204536
204729
 
204537
204730
  // src/shared/rpc-server.ts
204538
204731
  init_logger();
204732
+ init_rpc_notifications();
204539
204733
  import { randomBytes, timingSafeEqual } from "node:crypto";
204540
204734
  import {
204541
204735
  chmodSync as chmodSync3,
@@ -204547,7 +204741,6 @@ import {
204547
204741
  unlinkSync as unlinkSync4,
204548
204742
  writeFileSync as writeFileSync11
204549
204743
  } from "node:fs";
204550
- import { createServer } from "node:http";
204551
204744
  import { dirname as dirname9 } from "node:path";
204552
204745
 
204553
204746
  // src/shared/rpc-utils.ts
@@ -204605,6 +204798,9 @@ function isValidPort(port) {
204605
204798
  }
204606
204799
 
204607
204800
  // src/shared/rpc-server.ts
204801
+ var MAX_BODY_BYTES = 1048576;
204802
+ var WS_AUTH_TIMEOUT_MS = 5000;
204803
+ var WS_CLOSE_UNAUTHORIZED = 4401;
204608
204804
  function tokensMatch(presented, expected) {
204609
204805
  const a = Buffer.from(presented, "utf8");
204610
204806
  const b = Buffer.from(expected, "utf8");
@@ -204620,6 +204816,7 @@ class MagicContextRpcServer {
204620
204816
  portFilePath;
204621
204817
  portDir;
204622
204818
  startedAt = Date.now();
204819
+ sockets = new Set;
204623
204820
  token = randomBytes(32).toString("hex");
204624
204821
  constructor(storageDir, directory) {
204625
204822
  this.portFilePath = rpcPortFilePath(storageDir, directory);
@@ -204629,49 +204826,77 @@ class MagicContextRpcServer {
204629
204826
  this.handlers.set(method, handler);
204630
204827
  }
204631
204828
  async start() {
204632
- return new Promise((resolve4, reject) => {
204633
- const server = createServer((req, res) => this.dispatch(req, res));
204634
- server.on("error", (err) => {
204635
- log(`[rpc] server error: ${err.message}`);
204636
- reject(err);
204637
- });
204638
- server.listen(0, "127.0.0.1", () => {
204639
- const addr = server.address();
204640
- if (!addr || typeof addr === "string") {
204641
- reject(new Error("Failed to get server address"));
204642
- return;
204643
- }
204644
- this.port = addr.port;
204645
- this.server = server;
204646
- try {
204647
- this.warnIfOtherLiveInstance();
204648
- const dir = dirname9(this.portFilePath);
204649
- mkdirSync11(dir, { recursive: true, mode: 448 });
204650
- try {
204651
- chmodSync3(dir, 448);
204652
- } catch {}
204653
- const tmpPath = `${this.portFilePath}.tmp`;
204654
- try {
204655
- rmSync3(tmpPath, { force: true });
204656
- } catch {}
204657
- writeFileSync11(tmpPath, JSON.stringify({
204658
- port: this.port,
204659
- pid: process.pid,
204660
- started_at: this.startedAt,
204661
- token: this.token
204662
- }), { encoding: "utf-8", mode: 384 });
204663
- renameSync5(tmpPath, this.portFilePath);
204664
- try {
204665
- chmodSync3(this.portFilePath, 384);
204666
- } catch {}
204667
- log(`[rpc] server listening on 127.0.0.1:${this.port}`);
204668
- } catch (err) {
204669
- log(`[rpc] failed to write port file: ${err}`);
204829
+ const self2 = this;
204830
+ const server = Bun.serve({
204831
+ port: 0,
204832
+ hostname: "127.0.0.1",
204833
+ fetch(req, srv) {
204834
+ return self2.handleFetch(req, srv);
204835
+ },
204836
+ websocket: {
204837
+ open(ws) {
204838
+ ws.data.authTimer = setTimeout(() => {
204839
+ if (!ws.data.authed)
204840
+ ws.close(WS_CLOSE_UNAUTHORIZED, "auth timeout");
204841
+ }, WS_AUTH_TIMEOUT_MS);
204842
+ },
204843
+ message(ws, raw) {
204844
+ self2.handleWsMessage(ws, raw);
204845
+ },
204846
+ close(ws) {
204847
+ if (ws.data.authTimer)
204848
+ clearTimeout(ws.data.authTimer);
204849
+ ws.data.unregister?.();
204850
+ self2.sockets.delete(ws);
204670
204851
  }
204671
- resolve4(this.port);
204672
- });
204673
- server.unref();
204852
+ }
204674
204853
  });
204854
+ this.server = server;
204855
+ this.port = server.port ?? 0;
204856
+ try {
204857
+ this.warnIfOtherLiveInstance();
204858
+ const dir = dirname9(this.portFilePath);
204859
+ mkdirSync11(dir, { recursive: true, mode: 448 });
204860
+ try {
204861
+ chmodSync3(dir, 448);
204862
+ } catch {}
204863
+ const tmpPath = `${this.portFilePath}.tmp`;
204864
+ try {
204865
+ rmSync3(tmpPath, { force: true });
204866
+ } catch {}
204867
+ writeFileSync11(tmpPath, JSON.stringify({
204868
+ port: this.port,
204869
+ pid: process.pid,
204870
+ started_at: this.startedAt,
204871
+ token: this.token
204872
+ }), { encoding: "utf-8", mode: 384 });
204873
+ renameSync5(tmpPath, this.portFilePath);
204874
+ try {
204875
+ chmodSync3(this.portFilePath, 384);
204876
+ } catch {}
204877
+ log(`[rpc] server listening on 127.0.0.1:${this.port}`);
204878
+ } catch (err) {
204879
+ log(`[rpc] failed to write port file: ${err}`);
204880
+ }
204881
+ return this.port;
204882
+ }
204883
+ stop() {
204884
+ for (const ws of this.sockets) {
204885
+ try {
204886
+ if (ws.data.authTimer)
204887
+ clearTimeout(ws.data.authTimer);
204888
+ ws.data.unregister?.();
204889
+ ws.close();
204890
+ } catch {}
204891
+ }
204892
+ this.sockets.clear();
204893
+ if (this.server) {
204894
+ this.server.stop(true);
204895
+ this.server = null;
204896
+ }
204897
+ try {
204898
+ unlinkSync4(this.portFilePath);
204899
+ } catch {}
204675
204900
  }
204676
204901
  warnIfOtherLiveInstance() {
204677
204902
  try {
@@ -204686,73 +204911,99 @@ class MagicContextRpcServer {
204686
204911
  }
204687
204912
  } catch {}
204688
204913
  }
204689
- stop() {
204690
- if (this.server) {
204691
- this.server.close();
204692
- this.server = null;
204914
+ async handleFetch(req, srv) {
204915
+ const url2 = new URL(req.url);
204916
+ if (url2.pathname === "/ws") {
204917
+ const ok = srv.upgrade(req, { data: { authed: false } });
204918
+ if (ok)
204919
+ return;
204920
+ return new Response("upgrade failed", { status: 400 });
204693
204921
  }
204694
- try {
204695
- unlinkSync4(this.portFilePath);
204696
- } catch {}
204697
- }
204698
- dispatch(req, res) {
204699
- const url2 = req.url ?? "";
204700
- if (req.method === "GET" && url2 === "/health") {
204701
- res.writeHead(200, { "Content-Type": "application/json" });
204702
- res.end(JSON.stringify({ ok: true, pid: process.pid }));
204703
- return;
204922
+ if (req.method === "GET" && url2.pathname === "/health") {
204923
+ return json2({ ok: true, pid: process.pid });
204704
204924
  }
204705
- if (req.method !== "POST" || !url2.startsWith("/rpc/")) {
204706
- res.writeHead(404);
204707
- res.end("Not Found");
204708
- return;
204925
+ if (req.method !== "POST" || !url2.pathname.startsWith("/rpc/")) {
204926
+ return new Response("Not Found", { status: 404 });
204709
204927
  }
204710
- const auth = req.headers.authorization;
204928
+ const auth = req.headers.get("authorization");
204711
204929
  const presented = typeof auth === "string" ? auth.replace(/^Bearer\s+/i, "") : "";
204712
204930
  if (!tokensMatch(presented, this.token)) {
204713
- res.writeHead(401, { "Content-Type": "application/json" });
204714
- res.end(JSON.stringify({ error: "Unauthorized" }));
204715
- req.resume();
204716
- return;
204931
+ return json2({ error: "Unauthorized" }, 401);
204717
204932
  }
204718
- const method = url2.slice(5);
204933
+ const method = url2.pathname.slice(5);
204719
204934
  const handler = this.handlers.get(method);
204720
204935
  if (!handler) {
204721
- res.writeHead(404, { "Content-Type": "application/json" });
204722
- res.end(JSON.stringify({ error: `Unknown method: ${method}` }));
204723
- return;
204936
+ return json2({ error: `Unknown method: ${method}` }, 404);
204724
204937
  }
204725
- let body = "";
204726
- req.on("data", (chunk) => {
204727
- body += chunk.toString();
204728
- if (body.length > 1048576) {
204729
- res.writeHead(413);
204730
- res.end("Request too large");
204731
- req.destroy();
204732
- }
204733
- });
204734
- req.on("end", () => {
204735
- let params = {};
204938
+ const bodyText = await req.text();
204939
+ if (bodyText.length > MAX_BODY_BYTES) {
204940
+ return new Response("Request too large", { status: 413 });
204941
+ }
204942
+ let params = {};
204943
+ if (bodyText.length > 0) {
204736
204944
  try {
204737
- if (body.length > 0) {
204738
- params = JSON.parse(body);
204739
- }
204945
+ params = JSON.parse(bodyText);
204740
204946
  } catch {
204741
- res.writeHead(400, { "Content-Type": "application/json" });
204742
- res.end(JSON.stringify({ error: "Invalid JSON" }));
204947
+ return json2({ error: "Invalid JSON" }, 400);
204948
+ }
204949
+ }
204950
+ try {
204951
+ const result = await handler(params);
204952
+ return json2(result);
204953
+ } catch (err) {
204954
+ log(`[rpc] handler error: ${method} => ${err}`);
204955
+ return json2({ error: String(err) }, 500);
204956
+ }
204957
+ }
204958
+ handleWsMessage(ws, raw) {
204959
+ let msg;
204960
+ try {
204961
+ msg = JSON.parse(typeof raw === "string" ? raw : raw.toString("utf8"));
204962
+ } catch {
204963
+ return;
204964
+ }
204965
+ if (msg.type === "hello") {
204966
+ if (!tokensMatch(typeof msg.token === "string" ? msg.token : "", this.token)) {
204967
+ ws.send(JSON.stringify({ type: "error", error: "unauthorized" }));
204968
+ ws.close(WS_CLOSE_UNAUTHORIZED, "bad token");
204743
204969
  return;
204744
204970
  }
204745
- handler(params).then((result) => {
204746
- res.writeHead(200, { "Content-Type": "application/json" });
204747
- res.end(JSON.stringify(result));
204748
- }).catch((err) => {
204749
- log(`[rpc] handler error: ${method} => ${err}`);
204750
- res.writeHead(500, { "Content-Type": "application/json" });
204751
- res.end(JSON.stringify({ error: String(err) }));
204752
- });
204753
- });
204971
+ if (ws.data.authTimer) {
204972
+ clearTimeout(ws.data.authTimer);
204973
+ ws.data.authTimer = undefined;
204974
+ }
204975
+ ws.data.authed = true;
204976
+ ws.data.sessionId = typeof msg.sessionId === "string" && msg.sessionId.length > 0 ? msg.sessionId : undefined;
204977
+ const sink = {
204978
+ sessionId: ws.data.sessionId,
204979
+ send: (notification) => {
204980
+ ws.send(JSON.stringify({ type: "notification", notification }));
204981
+ }
204982
+ };
204983
+ ws.data.unregister = registerNotificationSink(sink);
204984
+ this.sockets.add(ws);
204985
+ const lastReceivedId = Number(msg.lastReceivedId ?? 0);
204986
+ const backlog = drainNotifications(Number.isFinite(lastReceivedId) ? lastReceivedId : 0, ws.data.sessionId);
204987
+ for (const notification of backlog) {
204988
+ ws.send(JSON.stringify({ type: "notification", notification }));
204989
+ }
204990
+ ws.send(JSON.stringify({ type: "hello-ack" }));
204991
+ return;
204992
+ }
204993
+ if (msg.type === "ack") {
204994
+ const lastReceivedId = Number(msg.lastReceivedId ?? 0);
204995
+ if (Number.isFinite(lastReceivedId) && lastReceivedId > 0) {
204996
+ drainNotifications(lastReceivedId, ws.data.sessionId);
204997
+ }
204998
+ }
204754
204999
  }
204755
205000
  }
205001
+ function json2(body, status = 200) {
205002
+ return new Response(JSON.stringify(body), {
205003
+ status,
205004
+ headers: { "Content-Type": "application/json" }
205005
+ });
205006
+ }
204756
205007
 
204757
205008
  // src/index.ts
204758
205009
  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"}