@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.
- package/README.md +1 -1
- package/dist/features/magic-context/compartment-chunk-embedding.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-openai.d.ts.map +1 -1
- package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
- package/dist/features/magic-context/recursive-text-splitter.d.ts +36 -0
- package/dist/features/magic-context/recursive-text-splitter.d.ts.map +1 -0
- package/dist/index.js +368 -117
- package/dist/plugin/rpc-handlers.d.ts.map +1 -1
- package/dist/shared/announcement.d.ts +1 -1
- package/dist/shared/data-path.d.ts.map +1 -1
- package/dist/shared/rpc-client.d.ts +8 -0
- package/dist/shared/rpc-client.d.ts.map +1 -1
- package/dist/shared/rpc-notifications.d.ts +28 -10
- package/dist/shared/rpc-notifications.d.ts.map +1 -1
- package/dist/shared/rpc-server.d.ts +22 -3
- package/dist/shared/rpc-server.d.ts.map +1 -1
- package/dist/tui/data/context-db.d.ts +4 -14
- package/dist/tui/data/context-db.d.ts.map +1 -1
- package/dist/tui/data/notification-socket.d.ts +39 -0
- package/dist/tui/data/notification-socket.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/shared/announcement.ts +2 -2
- package/src/shared/data-path.test.ts +28 -0
- package/src/shared/data-path.ts +5 -0
- package/src/shared/rpc-client.ts +14 -0
- package/src/shared/rpc-notifications.test.ts +68 -11
- package/src/shared/rpc-notifications.ts +75 -36
- package/src/shared/rpc-server.ts +249 -150
- package/src/tui/data/context-db.ts +10 -64
- package/src/tui/data/notification-socket.ts +229 -0
- 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
|
-
|
|
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
|
-
|
|
165555
|
-
|
|
165556
|
-
|
|
165557
|
-
return
|
|
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
|
|
165579
|
+
return false;
|
|
165560
165580
|
}
|
|
165561
|
-
var queue, nextNotificationId = 1,
|
|
165581
|
+
var queue, nextNotificationId = 1, sinks;
|
|
165562
165582
|
var init_rpc_notifications = __esm(() => {
|
|
165563
165583
|
queue = [];
|
|
165564
|
-
|
|
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:
|
|
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
|
|
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.
|
|
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
|
-
"
|
|
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
|
-
|
|
204633
|
-
|
|
204634
|
-
|
|
204635
|
-
|
|
204636
|
-
|
|
204637
|
-
|
|
204638
|
-
|
|
204639
|
-
|
|
204640
|
-
|
|
204641
|
-
|
|
204642
|
-
|
|
204643
|
-
|
|
204644
|
-
|
|
204645
|
-
|
|
204646
|
-
|
|
204647
|
-
|
|
204648
|
-
|
|
204649
|
-
|
|
204650
|
-
|
|
204651
|
-
|
|
204652
|
-
|
|
204653
|
-
|
|
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
|
-
|
|
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
|
-
|
|
204690
|
-
|
|
204691
|
-
|
|
204692
|
-
|
|
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
|
-
|
|
204695
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
204722
|
-
res.end(JSON.stringify({ error: `Unknown method: ${method}` }));
|
|
204723
|
-
return;
|
|
204936
|
+
return json2({ error: `Unknown method: ${method}` }, 404);
|
|
204724
204937
|
}
|
|
204725
|
-
|
|
204726
|
-
|
|
204727
|
-
|
|
204728
|
-
|
|
204729
|
-
|
|
204730
|
-
|
|
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
|
-
|
|
204738
|
-
params = JSON.parse(body);
|
|
204739
|
-
}
|
|
204945
|
+
params = JSON.parse(bodyText);
|
|
204740
204946
|
} catch {
|
|
204741
|
-
|
|
204742
|
-
|
|
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
|
-
|
|
204746
|
-
|
|
204747
|
-
|
|
204748
|
-
}
|
|
204749
|
-
|
|
204750
|
-
|
|
204751
|
-
|
|
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;
|
|
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.
|
|
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,
|
|
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"}
|