opencode-orchestrator 1.2.70 → 1.3.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 +39 -1
- package/dist/core/knowledge/context-provider.d.ts +8 -0
- package/dist/core/knowledge/graph-parser.d.ts +39 -0
- package/dist/core/knowledge/hybrid-search.d.ts +65 -0
- package/dist/core/knowledge/index.d.ts +15 -0
- package/dist/core/knowledge/memory-consolidation.d.ts +38 -0
- package/dist/core/knowledge/safety-guards.d.ts +39 -0
- package/dist/core/knowledge/scratchpad.d.ts +38 -0
- package/dist/core/knowledge/tag-indexer.d.ts +73 -0
- package/dist/index.js +674 -98
- package/dist/plugin-handlers/assistant-done-handler.d.ts +3 -4
- package/dist/plugin-handlers/index.d.ts +0 -1
- package/dist/plugin-handlers/interfaces/session-state.d.ts +1 -0
- package/dist/plugin-handlers/interfaces/system-transform.d.ts +3 -0
- package/dist/shared/message/constants/plugin-hooks.d.ts +6 -2
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1903,10 +1903,14 @@ var init_plugin_hooks = __esm({
|
|
|
1903
1903
|
PLUGIN_HOOKS = {
|
|
1904
1904
|
/** Intercepts user messages before sending to LLM */
|
|
1905
1905
|
CHAT_MESSAGE: "chat.message",
|
|
1906
|
-
/** Runs
|
|
1907
|
-
|
|
1906
|
+
/** Runs before any tool call */
|
|
1907
|
+
TOOL_EXECUTE_BEFORE: "tool.execute.before",
|
|
1908
1908
|
/** Runs after any tool call completes */
|
|
1909
|
-
TOOL_EXECUTE_AFTER: "tool.execute.after"
|
|
1909
|
+
TOOL_EXECUTE_AFTER: "tool.execute.after",
|
|
1910
|
+
/** Preserves custom compaction context */
|
|
1911
|
+
EXPERIMENTAL_SESSION_COMPACTING: "experimental.session.compacting",
|
|
1912
|
+
/** Injects dynamic system prompt additions */
|
|
1913
|
+
EXPERIMENTAL_CHAT_SYSTEM_TRANSFORM: "experimental.chat.system.transform"
|
|
1910
1914
|
};
|
|
1911
1915
|
}
|
|
1912
1916
|
});
|
|
@@ -7966,10 +7970,10 @@ function mergeDefs(...defs) {
|
|
|
7966
7970
|
function cloneDef(schema) {
|
|
7967
7971
|
return mergeDefs(schema._zod.def);
|
|
7968
7972
|
}
|
|
7969
|
-
function getElementAtPath(obj,
|
|
7970
|
-
if (!
|
|
7973
|
+
function getElementAtPath(obj, path11) {
|
|
7974
|
+
if (!path11)
|
|
7971
7975
|
return obj;
|
|
7972
|
-
return
|
|
7976
|
+
return path11.reduce((acc, key) => acc?.[key], obj);
|
|
7973
7977
|
}
|
|
7974
7978
|
function promiseAllObject(promisesObj) {
|
|
7975
7979
|
const keys = Object.keys(promisesObj);
|
|
@@ -8378,11 +8382,11 @@ function explicitlyAborted(x, startIndex = 0) {
|
|
|
8378
8382
|
}
|
|
8379
8383
|
return false;
|
|
8380
8384
|
}
|
|
8381
|
-
function prefixIssues(
|
|
8385
|
+
function prefixIssues(path11, issues) {
|
|
8382
8386
|
return issues.map((iss) => {
|
|
8383
8387
|
var _a3;
|
|
8384
8388
|
(_a3 = iss).path ?? (_a3.path = []);
|
|
8385
|
-
iss.path.unshift(
|
|
8389
|
+
iss.path.unshift(path11);
|
|
8386
8390
|
return iss;
|
|
8387
8391
|
});
|
|
8388
8392
|
}
|
|
@@ -8529,16 +8533,16 @@ function flattenError(error95, mapper = (issue3) => issue3.message) {
|
|
|
8529
8533
|
}
|
|
8530
8534
|
function formatError(error95, mapper = (issue3) => issue3.message) {
|
|
8531
8535
|
const fieldErrors = { _errors: [] };
|
|
8532
|
-
const processError = (error96,
|
|
8536
|
+
const processError = (error96, path11 = []) => {
|
|
8533
8537
|
for (const issue3 of error96.issues) {
|
|
8534
8538
|
if (issue3.code === "invalid_union" && issue3.errors.length) {
|
|
8535
|
-
issue3.errors.map((issues) => processError({ issues }, [...
|
|
8539
|
+
issue3.errors.map((issues) => processError({ issues }, [...path11, ...issue3.path]));
|
|
8536
8540
|
} else if (issue3.code === "invalid_key") {
|
|
8537
|
-
processError({ issues: issue3.issues }, [...
|
|
8541
|
+
processError({ issues: issue3.issues }, [...path11, ...issue3.path]);
|
|
8538
8542
|
} else if (issue3.code === "invalid_element") {
|
|
8539
|
-
processError({ issues: issue3.issues }, [...
|
|
8543
|
+
processError({ issues: issue3.issues }, [...path11, ...issue3.path]);
|
|
8540
8544
|
} else {
|
|
8541
|
-
const fullpath = [...
|
|
8545
|
+
const fullpath = [...path11, ...issue3.path];
|
|
8542
8546
|
if (fullpath.length === 0) {
|
|
8543
8547
|
fieldErrors._errors.push(mapper(issue3));
|
|
8544
8548
|
} else {
|
|
@@ -8565,17 +8569,17 @@ function formatError(error95, mapper = (issue3) => issue3.message) {
|
|
|
8565
8569
|
}
|
|
8566
8570
|
function treeifyError(error95, mapper = (issue3) => issue3.message) {
|
|
8567
8571
|
const result = { errors: [] };
|
|
8568
|
-
const processError = (error96,
|
|
8572
|
+
const processError = (error96, path11 = []) => {
|
|
8569
8573
|
var _a3, _b;
|
|
8570
8574
|
for (const issue3 of error96.issues) {
|
|
8571
8575
|
if (issue3.code === "invalid_union" && issue3.errors.length) {
|
|
8572
|
-
issue3.errors.map((issues) => processError({ issues }, [...
|
|
8576
|
+
issue3.errors.map((issues) => processError({ issues }, [...path11, ...issue3.path]));
|
|
8573
8577
|
} else if (issue3.code === "invalid_key") {
|
|
8574
|
-
processError({ issues: issue3.issues }, [...
|
|
8578
|
+
processError({ issues: issue3.issues }, [...path11, ...issue3.path]);
|
|
8575
8579
|
} else if (issue3.code === "invalid_element") {
|
|
8576
|
-
processError({ issues: issue3.issues }, [...
|
|
8580
|
+
processError({ issues: issue3.issues }, [...path11, ...issue3.path]);
|
|
8577
8581
|
} else {
|
|
8578
|
-
const fullpath = [...
|
|
8582
|
+
const fullpath = [...path11, ...issue3.path];
|
|
8579
8583
|
if (fullpath.length === 0) {
|
|
8580
8584
|
result.errors.push(mapper(issue3));
|
|
8581
8585
|
continue;
|
|
@@ -8607,8 +8611,8 @@ function treeifyError(error95, mapper = (issue3) => issue3.message) {
|
|
|
8607
8611
|
}
|
|
8608
8612
|
function toDotPath(_path) {
|
|
8609
8613
|
const segs = [];
|
|
8610
|
-
const
|
|
8611
|
-
for (const seg of
|
|
8614
|
+
const path11 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
|
|
8615
|
+
for (const seg of path11) {
|
|
8612
8616
|
if (typeof seg === "number")
|
|
8613
8617
|
segs.push(`[${seg}]`);
|
|
8614
8618
|
else if (typeof seg === "symbol")
|
|
@@ -21300,13 +21304,13 @@ function resolveRef(ref, ctx) {
|
|
|
21300
21304
|
if (!ref.startsWith("#")) {
|
|
21301
21305
|
throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
|
|
21302
21306
|
}
|
|
21303
|
-
const
|
|
21304
|
-
if (
|
|
21307
|
+
const path11 = ref.slice(1).split("/").filter(Boolean);
|
|
21308
|
+
if (path11.length === 0) {
|
|
21305
21309
|
return ctx.rootSchema;
|
|
21306
21310
|
}
|
|
21307
21311
|
const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
|
|
21308
|
-
if (
|
|
21309
|
-
const key =
|
|
21312
|
+
if (path11[0] === defsKey) {
|
|
21313
|
+
const key = path11[1];
|
|
21310
21314
|
if (!key || !ctx.defs[key]) {
|
|
21311
21315
|
throw new Error(`Reference not found: ${ref}`);
|
|
21312
21316
|
}
|
|
@@ -23909,10 +23913,10 @@ function mergeDefs2(...defs) {
|
|
|
23909
23913
|
function cloneDef2(schema) {
|
|
23910
23914
|
return mergeDefs2(schema._zod.def);
|
|
23911
23915
|
}
|
|
23912
|
-
function getElementAtPath2(obj,
|
|
23913
|
-
if (!
|
|
23916
|
+
function getElementAtPath2(obj, path11) {
|
|
23917
|
+
if (!path11)
|
|
23914
23918
|
return obj;
|
|
23915
|
-
return
|
|
23919
|
+
return path11.reduce((acc, key) => acc?.[key], obj);
|
|
23916
23920
|
}
|
|
23917
23921
|
function promiseAllObject2(promisesObj) {
|
|
23918
23922
|
const keys = Object.keys(promisesObj);
|
|
@@ -24273,11 +24277,11 @@ function aborted2(x, startIndex = 0) {
|
|
|
24273
24277
|
}
|
|
24274
24278
|
return false;
|
|
24275
24279
|
}
|
|
24276
|
-
function prefixIssues2(
|
|
24280
|
+
function prefixIssues2(path11, issues) {
|
|
24277
24281
|
return issues.map((iss) => {
|
|
24278
24282
|
var _a3;
|
|
24279
24283
|
(_a3 = iss).path ?? (_a3.path = []);
|
|
24280
|
-
iss.path.unshift(
|
|
24284
|
+
iss.path.unshift(path11);
|
|
24281
24285
|
return iss;
|
|
24282
24286
|
});
|
|
24283
24287
|
}
|
|
@@ -24445,7 +24449,7 @@ function treeifyError2(error95, _mapper) {
|
|
|
24445
24449
|
return issue3.message;
|
|
24446
24450
|
};
|
|
24447
24451
|
const result = { errors: [] };
|
|
24448
|
-
const processError = (error96,
|
|
24452
|
+
const processError = (error96, path11 = []) => {
|
|
24449
24453
|
var _a3, _b;
|
|
24450
24454
|
for (const issue3 of error96.issues) {
|
|
24451
24455
|
if (issue3.code === "invalid_union" && issue3.errors.length) {
|
|
@@ -24455,7 +24459,7 @@ function treeifyError2(error95, _mapper) {
|
|
|
24455
24459
|
} else if (issue3.code === "invalid_element") {
|
|
24456
24460
|
processError({ issues: issue3.issues }, issue3.path);
|
|
24457
24461
|
} else {
|
|
24458
|
-
const fullpath = [...
|
|
24462
|
+
const fullpath = [...path11, ...issue3.path];
|
|
24459
24463
|
if (fullpath.length === 0) {
|
|
24460
24464
|
result.errors.push(mapper(issue3));
|
|
24461
24465
|
continue;
|
|
@@ -24487,8 +24491,8 @@ function treeifyError2(error95, _mapper) {
|
|
|
24487
24491
|
}
|
|
24488
24492
|
function toDotPath2(_path) {
|
|
24489
24493
|
const segs = [];
|
|
24490
|
-
const
|
|
24491
|
-
for (const seg of
|
|
24494
|
+
const path11 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
|
|
24495
|
+
for (const seg of path11) {
|
|
24492
24496
|
if (typeof seg === "number")
|
|
24493
24497
|
segs.push(`[${seg}]`);
|
|
24494
24498
|
else if (typeof seg === "symbol")
|
|
@@ -36638,6 +36642,7 @@ function ensureSessionInitialized(sessions, sessionID, directory) {
|
|
|
36638
36642
|
timestamp: now,
|
|
36639
36643
|
startTime: now,
|
|
36640
36644
|
lastStepTime: now,
|
|
36645
|
+
lastCompletedMessageID: void 0,
|
|
36641
36646
|
tokens: { totalInput: 0, totalOutput: 0, estimatedCost: 0 }
|
|
36642
36647
|
};
|
|
36643
36648
|
if (directory) {
|
|
@@ -37284,10 +37289,10 @@ async function resolveCommandPath(key, commandName) {
|
|
|
37284
37289
|
const currentPending = pending.get(key);
|
|
37285
37290
|
if (currentPending) return currentPending;
|
|
37286
37291
|
const promise3 = (async () => {
|
|
37287
|
-
const
|
|
37288
|
-
cache[key] =
|
|
37292
|
+
const path11 = await findCommand(commandName);
|
|
37293
|
+
cache[key] = path11;
|
|
37289
37294
|
pending.delete(key);
|
|
37290
|
-
return
|
|
37295
|
+
return path11;
|
|
37291
37296
|
})();
|
|
37292
37297
|
pending.set(key, promise3);
|
|
37293
37298
|
return promise3;
|
|
@@ -37296,14 +37301,14 @@ async function resolveCommandPath(key, commandName) {
|
|
|
37296
37301
|
// src/core/notification/os-notify/notifier.ts
|
|
37297
37302
|
var execAsync2 = promisify2(exec2);
|
|
37298
37303
|
async function notifyDarwin(title, message) {
|
|
37299
|
-
const
|
|
37304
|
+
const path11 = await resolveCommandPath(
|
|
37300
37305
|
NOTIFICATION_COMMAND_KEYS.OSASCRIPT,
|
|
37301
37306
|
NOTIFICATION_COMMANDS.OSASCRIPT
|
|
37302
37307
|
);
|
|
37303
|
-
if (!
|
|
37308
|
+
if (!path11) return;
|
|
37304
37309
|
const escT = title.replace(/"/g, '\\"');
|
|
37305
37310
|
const escM = message.replace(/"/g, '\\"');
|
|
37306
|
-
await execAsync2(`${
|
|
37311
|
+
await execAsync2(`${path11} -e 'display notification "${escM}" with title "${escT}" sound name "Glass"' >/dev/null 2>/dev/null`);
|
|
37307
37312
|
}
|
|
37308
37313
|
function isWSL() {
|
|
37309
37314
|
try {
|
|
@@ -37316,11 +37321,11 @@ function isWSL() {
|
|
|
37316
37321
|
}
|
|
37317
37322
|
async function notifyLinux(title, message) {
|
|
37318
37323
|
if (isWSL()) return;
|
|
37319
|
-
const
|
|
37324
|
+
const path11 = await resolveCommandPath(
|
|
37320
37325
|
NOTIFICATION_COMMAND_KEYS.NOTIFY_SEND,
|
|
37321
37326
|
NOTIFICATION_COMMANDS.NOTIFY_SEND
|
|
37322
37327
|
);
|
|
37323
|
-
if (
|
|
37328
|
+
if (path11) await execAsync2(`${path11} "${title}" "${message}" >/dev/null 2>/dev/null`);
|
|
37324
37329
|
}
|
|
37325
37330
|
async function notifyWindows(title, message) {
|
|
37326
37331
|
const ps = await resolveCommandPath(
|
|
@@ -37368,11 +37373,11 @@ init_os();
|
|
|
37368
37373
|
async function playDarwin(soundPath) {
|
|
37369
37374
|
if (!soundPath) return;
|
|
37370
37375
|
try {
|
|
37371
|
-
const
|
|
37376
|
+
const path11 = await resolveCommandPath(
|
|
37372
37377
|
NOTIFICATION_COMMAND_KEYS.AFPLAY,
|
|
37373
37378
|
NOTIFICATION_COMMANDS.AFPLAY
|
|
37374
37379
|
);
|
|
37375
|
-
if (
|
|
37380
|
+
if (path11) exec3(`"${path11}" "${soundPath}" >/dev/null 2>/dev/null`);
|
|
37376
37381
|
} catch (err) {
|
|
37377
37382
|
log(`[session-notify] Error playing sound (Darwin): ${err}`);
|
|
37378
37383
|
}
|
|
@@ -41541,6 +41546,55 @@ function handleSessionCompacted(sessionID) {
|
|
|
41541
41546
|
|
|
41542
41547
|
// src/plugin-handlers/event-handler.ts
|
|
41543
41548
|
init_shared();
|
|
41549
|
+
|
|
41550
|
+
// src/plugin-handlers/assistant-done-handler.ts
|
|
41551
|
+
init_logger();
|
|
41552
|
+
init_shared();
|
|
41553
|
+
async function handleCompletedAssistantMessage(ctx, sessionID, messageID) {
|
|
41554
|
+
const { client, directory, sessions } = ctx;
|
|
41555
|
+
const hooks = HookRegistry.getInstance();
|
|
41556
|
+
const session = sessions.get(sessionID);
|
|
41557
|
+
if (!session?.active || session.lastCompletedMessageID === messageID) {
|
|
41558
|
+
return;
|
|
41559
|
+
}
|
|
41560
|
+
const textContent = await readAssistantText(client, sessionID, messageID);
|
|
41561
|
+
session.lastCompletedMessageID = messageID;
|
|
41562
|
+
const result = await hooks.executeDone(
|
|
41563
|
+
{ sessionID, directory, sessions },
|
|
41564
|
+
textContent
|
|
41565
|
+
);
|
|
41566
|
+
if (result.action !== "inject" || result.prompts.length === 0) {
|
|
41567
|
+
return;
|
|
41568
|
+
}
|
|
41569
|
+
const now = Date.now();
|
|
41570
|
+
session.step++;
|
|
41571
|
+
session.timestamp = now;
|
|
41572
|
+
session.lastStepTime = now;
|
|
41573
|
+
try {
|
|
41574
|
+
const parts = result.prompts.map((text) => ({ type: PART_TYPES.TEXT, text }));
|
|
41575
|
+
client.session.prompt({
|
|
41576
|
+
path: { id: sessionID },
|
|
41577
|
+
body: { parts }
|
|
41578
|
+
}).catch((error95) => {
|
|
41579
|
+
log("[assistant-done-handler] Failed to inject continuation prompts", { sessionID, error: error95 });
|
|
41580
|
+
});
|
|
41581
|
+
} catch (error95) {
|
|
41582
|
+
log("[assistant-done-handler] Failed to inject continuation prompts", { sessionID, error: error95 });
|
|
41583
|
+
}
|
|
41584
|
+
}
|
|
41585
|
+
async function readAssistantText(client, sessionID, messageID) {
|
|
41586
|
+
try {
|
|
41587
|
+
const response = await client.session.message({
|
|
41588
|
+
path: { id: sessionID, messageID }
|
|
41589
|
+
});
|
|
41590
|
+
return (response.parts ?? []).filter((part) => part.type === PART_TYPES.TEXT || part.type === PART_TYPES.REASONING).map((part) => part.text ?? "").join("\n");
|
|
41591
|
+
} catch (error95) {
|
|
41592
|
+
log("[assistant-done-handler] Failed to read assistant message", { sessionID, messageID, error: error95 });
|
|
41593
|
+
return "";
|
|
41594
|
+
}
|
|
41595
|
+
}
|
|
41596
|
+
|
|
41597
|
+
// src/plugin-handlers/event-handler.ts
|
|
41544
41598
|
function createEventHandler(ctx) {
|
|
41545
41599
|
const { client, directory, sessions, state: state2 } = ctx;
|
|
41546
41600
|
return async (input) => {
|
|
@@ -41591,7 +41645,7 @@ function createEventHandler(ctx) {
|
|
|
41591
41645
|
if (event.type === MESSAGE_EVENTS.UPDATED) {
|
|
41592
41646
|
const messageProperties = event.properties;
|
|
41593
41647
|
const messageInfo = messageProperties?.info;
|
|
41594
|
-
const sessionID = messageInfo?.sessionID;
|
|
41648
|
+
const sessionID = messageProperties.sessionID || messageInfo?.sessionID;
|
|
41595
41649
|
const role = messageInfo?.role;
|
|
41596
41650
|
if (sessionID && messageProperties?.usage) {
|
|
41597
41651
|
const totalTokens = messageProperties.usage.totalTokens ?? (messageProperties.usage.inputTokens ?? 0) + (messageProperties.usage.outputTokens ?? 0);
|
|
@@ -41601,6 +41655,9 @@ function createEventHandler(ctx) {
|
|
|
41601
41655
|
}
|
|
41602
41656
|
if (sessionID && role === MESSAGE_ROLES.ASSISTANT) {
|
|
41603
41657
|
markRecoveryComplete(sessionID);
|
|
41658
|
+
if (messageInfo?.id && messageInfo.time?.completed) {
|
|
41659
|
+
await handleCompletedAssistantMessage(ctx, sessionID, messageInfo.id);
|
|
41660
|
+
}
|
|
41604
41661
|
}
|
|
41605
41662
|
if (sessionID && role === MESSAGE_ROLES.USER) {
|
|
41606
41663
|
handleUserMessage(sessionID);
|
|
@@ -41682,53 +41739,6 @@ function createToolExecuteAfterHandler(ctx) {
|
|
|
41682
41739
|
};
|
|
41683
41740
|
}
|
|
41684
41741
|
|
|
41685
|
-
// src/plugin-handlers/assistant-done-handler.ts
|
|
41686
|
-
init_logger();
|
|
41687
|
-
init_shared();
|
|
41688
|
-
function createAssistantDoneHandler(ctx) {
|
|
41689
|
-
const { client, directory, sessions } = ctx;
|
|
41690
|
-
const hooks = HookRegistry.getInstance();
|
|
41691
|
-
return async (assistantInput, assistantOutput) => {
|
|
41692
|
-
const sessionID = assistantInput.sessionID;
|
|
41693
|
-
const session = sessions.get(sessionID);
|
|
41694
|
-
if (!session?.active) return;
|
|
41695
|
-
const parts = assistantOutput.parts;
|
|
41696
|
-
const textContent = parts?.filter((p) => p.type === PART_TYPES.TEXT || p.type === PART_TYPES.REASONING).map((p) => p.text || "").join("\n") || "";
|
|
41697
|
-
const hookContext = {
|
|
41698
|
-
sessionID,
|
|
41699
|
-
directory,
|
|
41700
|
-
sessions
|
|
41701
|
-
// Cast because types might slightly differ in strict mode, but it's the same object
|
|
41702
|
-
};
|
|
41703
|
-
const result = await hooks.executeDone(hookContext, textContent);
|
|
41704
|
-
if (result.action === "stop") {
|
|
41705
|
-
return;
|
|
41706
|
-
}
|
|
41707
|
-
if (result.action === "inject" && result.prompts) {
|
|
41708
|
-
const now = Date.now();
|
|
41709
|
-
session.step++;
|
|
41710
|
-
session.timestamp = now;
|
|
41711
|
-
session.lastStepTime = now;
|
|
41712
|
-
try {
|
|
41713
|
-
if (client?.session?.prompt) {
|
|
41714
|
-
const parts2 = result.prompts.map((p) => ({
|
|
41715
|
-
type: PART_TYPES.TEXT,
|
|
41716
|
-
text: p
|
|
41717
|
-
}));
|
|
41718
|
-
client.session.prompt({
|
|
41719
|
-
path: { id: sessionID },
|
|
41720
|
-
body: { parts: parts2 }
|
|
41721
|
-
}).catch((error95) => {
|
|
41722
|
-
log("[assistant-done-handler] Failed to inject continuation prompts", { sessionID, error: error95 });
|
|
41723
|
-
});
|
|
41724
|
-
}
|
|
41725
|
-
} catch (error95) {
|
|
41726
|
-
log("[assistant-done-handler] Failed to inject continuation prompts", { sessionID, error: error95 });
|
|
41727
|
-
}
|
|
41728
|
-
}
|
|
41729
|
-
};
|
|
41730
|
-
}
|
|
41731
|
-
|
|
41732
41742
|
// src/plugin-handlers/session-compacting-handler.ts
|
|
41733
41743
|
init_shared();
|
|
41734
41744
|
function createSessionCompactingHandler(ctx) {
|
|
@@ -41805,6 +41815,564 @@ Wait for these tasks to complete before concluding the mission.
|
|
|
41805
41815
|
|
|
41806
41816
|
// src/plugin-handlers/system-transform-handler.ts
|
|
41807
41817
|
init_shared();
|
|
41818
|
+
|
|
41819
|
+
// src/core/knowledge/context-provider.ts
|
|
41820
|
+
import { existsSync as existsSync9, readFileSync as readFileSync5, readdirSync } from "node:fs";
|
|
41821
|
+
import path10 from "node:path";
|
|
41822
|
+
|
|
41823
|
+
// src/core/knowledge/graph-parser.ts
|
|
41824
|
+
var GraphParser = class _GraphParser {
|
|
41825
|
+
/** Heading marker used for the auto-generated backlinks section */
|
|
41826
|
+
static BACKLINKS_HEADING = "## \u{1F517} Backlinks";
|
|
41827
|
+
// Maps note name -> set of target note names it links to
|
|
41828
|
+
forwardLinks = /* @__PURE__ */ new Map();
|
|
41829
|
+
// Maps note name -> set of source note names that link to it
|
|
41830
|
+
backlinks = /* @__PURE__ */ new Map();
|
|
41831
|
+
// Maps note name -> file path
|
|
41832
|
+
noteToPath = /* @__PURE__ */ new Map();
|
|
41833
|
+
// Maps file path -> note name
|
|
41834
|
+
pathToNote = /* @__PURE__ */ new Map();
|
|
41835
|
+
/**
|
|
41836
|
+
* Resolve file path or relative link to a note name (basename without extension).
|
|
41837
|
+
*/
|
|
41838
|
+
getNoteName(filePath) {
|
|
41839
|
+
const basename = filePath.split(/[/\\]/).pop() || "";
|
|
41840
|
+
const dotIdx = basename.lastIndexOf(".");
|
|
41841
|
+
return dotIdx !== -1 ? basename.slice(0, dotIdx) : basename;
|
|
41842
|
+
}
|
|
41843
|
+
/**
|
|
41844
|
+
* Parse content and extract all unique referenced target note names.
|
|
41845
|
+
*/
|
|
41846
|
+
parseLinks(content) {
|
|
41847
|
+
const targets = [];
|
|
41848
|
+
const wikiRegex = /\[\[([^[\]|#]+)(?:\|[^[\]]+)?(?:#[^[\]]+)?\]\]/g;
|
|
41849
|
+
let match;
|
|
41850
|
+
while ((match = wikiRegex.exec(content)) !== null) {
|
|
41851
|
+
const target = match[1].trim();
|
|
41852
|
+
if (target && !targets.includes(target)) {
|
|
41853
|
+
targets.push(target);
|
|
41854
|
+
}
|
|
41855
|
+
}
|
|
41856
|
+
const mdLinkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
|
|
41857
|
+
while ((match = mdLinkRegex.exec(content)) !== null) {
|
|
41858
|
+
const url3 = match[2].trim();
|
|
41859
|
+
if (!url3.includes("://") && (url3.endsWith(".md") || url3.startsWith(".") || url3.startsWith("/"))) {
|
|
41860
|
+
const targetName = this.getNoteName(url3);
|
|
41861
|
+
if (targetName && !targets.includes(targetName)) {
|
|
41862
|
+
targets.push(targetName);
|
|
41863
|
+
}
|
|
41864
|
+
}
|
|
41865
|
+
}
|
|
41866
|
+
return targets;
|
|
41867
|
+
}
|
|
41868
|
+
/**
|
|
41869
|
+
* Index file and register its bi-directional links.
|
|
41870
|
+
*/
|
|
41871
|
+
indexFile(filePath, content) {
|
|
41872
|
+
const sourceNote = this.getNoteName(filePath);
|
|
41873
|
+
this.noteToPath.set(sourceNote, filePath);
|
|
41874
|
+
this.pathToNote.set(filePath, sourceNote);
|
|
41875
|
+
this.clearIndexForNote(sourceNote);
|
|
41876
|
+
const targets = this.parseLinks(content);
|
|
41877
|
+
if (targets.length > 0) {
|
|
41878
|
+
const forwardSet = new Set(targets);
|
|
41879
|
+
this.forwardLinks.set(sourceNote, forwardSet);
|
|
41880
|
+
for (const target of targets) {
|
|
41881
|
+
let backSet = this.backlinks.get(target);
|
|
41882
|
+
if (!backSet) {
|
|
41883
|
+
backSet = /* @__PURE__ */ new Set();
|
|
41884
|
+
this.backlinks.set(target, backSet);
|
|
41885
|
+
}
|
|
41886
|
+
backSet.add(sourceNote);
|
|
41887
|
+
}
|
|
41888
|
+
}
|
|
41889
|
+
}
|
|
41890
|
+
/**
|
|
41891
|
+
* Retrieve backlinks for a note name or file path.
|
|
41892
|
+
*/
|
|
41893
|
+
getBacklinks(noteOrPath) {
|
|
41894
|
+
const note = noteOrPath.includes("/") || noteOrPath.endsWith(".md") ? this.getNoteName(noteOrPath) : noteOrPath;
|
|
41895
|
+
const sources = this.backlinks.get(note);
|
|
41896
|
+
return sources ? Array.from(sources).sort() : [];
|
|
41897
|
+
}
|
|
41898
|
+
/**
|
|
41899
|
+
* Retrieve forward links for a note name or file path.
|
|
41900
|
+
*/
|
|
41901
|
+
getForwardLinks(noteOrPath) {
|
|
41902
|
+
const note = noteOrPath.includes("/") || noteOrPath.endsWith(".md") ? this.getNoteName(noteOrPath) : noteOrPath;
|
|
41903
|
+
const targets = this.forwardLinks.get(note);
|
|
41904
|
+
return targets ? Array.from(targets).sort() : [];
|
|
41905
|
+
}
|
|
41906
|
+
/**
|
|
41907
|
+
* Synchronize the ## 🔗 Backlinks section in a file's content.
|
|
41908
|
+
*/
|
|
41909
|
+
syncBacklinksSection(content, backlinksList) {
|
|
41910
|
+
const backlinksHeading = _GraphParser.BACKLINKS_HEADING;
|
|
41911
|
+
const headingRegex = new RegExp(`(?:\\r?\\n|^)${backlinksHeading}\\b[\\s\\S]*$`);
|
|
41912
|
+
let backlinksSectionContent = `
|
|
41913
|
+
|
|
41914
|
+
${backlinksHeading}
|
|
41915
|
+
|
|
41916
|
+
`;
|
|
41917
|
+
if (backlinksList.length > 0) {
|
|
41918
|
+
backlinksSectionContent += backlinksList.map((b) => `- [[${b}]]`).join("\n");
|
|
41919
|
+
} else {
|
|
41920
|
+
backlinksSectionContent += "*(No backlinks found)*";
|
|
41921
|
+
}
|
|
41922
|
+
if (content.includes(backlinksHeading)) {
|
|
41923
|
+
return content.replace(headingRegex, backlinksSectionContent);
|
|
41924
|
+
} else {
|
|
41925
|
+
return content.trimEnd() + backlinksSectionContent;
|
|
41926
|
+
}
|
|
41927
|
+
}
|
|
41928
|
+
/**
|
|
41929
|
+
* Clear all indexes for a note.
|
|
41930
|
+
*/
|
|
41931
|
+
clearIndexForNote(sourceNote) {
|
|
41932
|
+
const targets = this.forwardLinks.get(sourceNote);
|
|
41933
|
+
if (targets) {
|
|
41934
|
+
for (const target of targets) {
|
|
41935
|
+
const backSet = this.backlinks.get(target);
|
|
41936
|
+
if (backSet) {
|
|
41937
|
+
backSet.delete(sourceNote);
|
|
41938
|
+
if (backSet.size === 0) {
|
|
41939
|
+
this.backlinks.delete(target);
|
|
41940
|
+
}
|
|
41941
|
+
}
|
|
41942
|
+
}
|
|
41943
|
+
this.forwardLinks.delete(sourceNote);
|
|
41944
|
+
}
|
|
41945
|
+
}
|
|
41946
|
+
};
|
|
41947
|
+
|
|
41948
|
+
// src/core/knowledge/hybrid-search.ts
|
|
41949
|
+
var RRF_K = 60;
|
|
41950
|
+
var DEFAULT_MAX_RESULTS = 20;
|
|
41951
|
+
var BM25_K1 = 1.2;
|
|
41952
|
+
var BM25_B = 0.75;
|
|
41953
|
+
var GRAPH_HOP_DEPTH = 2;
|
|
41954
|
+
var HybridSearch = class {
|
|
41955
|
+
tagIndexer;
|
|
41956
|
+
graphParser;
|
|
41957
|
+
/** Stores indexed note content keyed by note name. */
|
|
41958
|
+
contentMap = /* @__PURE__ */ new Map();
|
|
41959
|
+
constructor(tagIndexer, graphParser) {
|
|
41960
|
+
this.tagIndexer = tagIndexer;
|
|
41961
|
+
this.graphParser = graphParser;
|
|
41962
|
+
}
|
|
41963
|
+
/**
|
|
41964
|
+
* Register note content for lexical search.
|
|
41965
|
+
* Must be called after TagIndexer.indexFile / GraphParser.indexFile.
|
|
41966
|
+
*/
|
|
41967
|
+
indexContent(noteName, content) {
|
|
41968
|
+
this.contentMap.set(noteName, content.toLowerCase());
|
|
41969
|
+
}
|
|
41970
|
+
/**
|
|
41971
|
+
* Fuse lexical, tag, and graph rankings via RRF to produce a single list.
|
|
41972
|
+
*/
|
|
41973
|
+
search(query, maxResults) {
|
|
41974
|
+
const limit = maxResults ?? DEFAULT_MAX_RESULTS;
|
|
41975
|
+
const terms = this.tokenize(query);
|
|
41976
|
+
if (terms.length === 0) return [];
|
|
41977
|
+
const lexicalRanked = this.lexicalSearch(terms);
|
|
41978
|
+
const tagRanked = this.tagSearch(terms);
|
|
41979
|
+
const graphRanked = this.graphSearch(terms);
|
|
41980
|
+
return this.fuseResults(lexicalRanked, tagRanked, graphRanked, limit);
|
|
41981
|
+
}
|
|
41982
|
+
/**
|
|
41983
|
+
* BM25-inspired term-frequency scoring across all indexed documents.
|
|
41984
|
+
* Approximates IDF via corpus size and document frequency.
|
|
41985
|
+
*/
|
|
41986
|
+
lexicalSearch(terms) {
|
|
41987
|
+
const scores = /* @__PURE__ */ new Map();
|
|
41988
|
+
const corpusSize = this.contentMap.size;
|
|
41989
|
+
if (corpusSize === 0) return [];
|
|
41990
|
+
const avgLen = this.computeAverageLength();
|
|
41991
|
+
for (const term of terms) {
|
|
41992
|
+
const df = this.documentFrequency(term);
|
|
41993
|
+
const idf = Math.log((corpusSize - df + 0.5) / (df + 0.5) + 1);
|
|
41994
|
+
for (const [name, content] of this.contentMap) {
|
|
41995
|
+
const tf = this.countOccurrences(content, term);
|
|
41996
|
+
if (tf === 0) continue;
|
|
41997
|
+
const docLen = content.length;
|
|
41998
|
+
const tfNorm = tf * (BM25_K1 + 1) / (tf + BM25_K1 * (1 - BM25_B + BM25_B * (docLen / avgLen)));
|
|
41999
|
+
const prev = scores.get(name) ?? 0;
|
|
42000
|
+
scores.set(name, prev + idf * tfNorm);
|
|
42001
|
+
}
|
|
42002
|
+
}
|
|
42003
|
+
return this.sortByScore(scores);
|
|
42004
|
+
}
|
|
42005
|
+
/**
|
|
42006
|
+
* Match query terms against the tag index to find tagged notes.
|
|
42007
|
+
*/
|
|
42008
|
+
tagSearch(terms) {
|
|
42009
|
+
const scores = /* @__PURE__ */ new Map();
|
|
42010
|
+
for (const term of terms) {
|
|
42011
|
+
const files = this.tagIndexer.getFilesWithTag(term);
|
|
42012
|
+
for (const file3 of files) {
|
|
42013
|
+
const noteName = this.graphParser.getNoteName(file3);
|
|
42014
|
+
const prev = scores.get(noteName) ?? 0;
|
|
42015
|
+
scores.set(noteName, prev + 1);
|
|
42016
|
+
}
|
|
42017
|
+
}
|
|
42018
|
+
return this.sortByScore(scores);
|
|
42019
|
+
}
|
|
42020
|
+
/**
|
|
42021
|
+
* 2-hop graph traversal from tag-matched seed notes to discover related notes.
|
|
42022
|
+
*/
|
|
42023
|
+
graphSearch(terms) {
|
|
42024
|
+
const seeds = new Set(this.tagSearch(terms));
|
|
42025
|
+
const visited = /* @__PURE__ */ new Set();
|
|
42026
|
+
const scores = /* @__PURE__ */ new Map();
|
|
42027
|
+
for (const seed of seeds) {
|
|
42028
|
+
this.traverseGraph(seed, GRAPH_HOP_DEPTH, visited, scores);
|
|
42029
|
+
}
|
|
42030
|
+
return this.sortByScore(scores);
|
|
42031
|
+
}
|
|
42032
|
+
/**
|
|
42033
|
+
* Depth-limited BFS from a seed note, scoring neighbors by proximity.
|
|
42034
|
+
*/
|
|
42035
|
+
traverseGraph(note, depth, visited, scores) {
|
|
42036
|
+
if (depth <= 0 || visited.has(note)) return;
|
|
42037
|
+
visited.add(note);
|
|
42038
|
+
const neighbors = [
|
|
42039
|
+
...this.graphParser.getForwardLinks(note),
|
|
42040
|
+
...this.graphParser.getBacklinks(note)
|
|
42041
|
+
];
|
|
42042
|
+
for (const neighbor of neighbors) {
|
|
42043
|
+
const prev = scores.get(neighbor) ?? 0;
|
|
42044
|
+
scores.set(neighbor, prev + depth);
|
|
42045
|
+
this.traverseGraph(neighbor, depth - 1, visited, scores);
|
|
42046
|
+
}
|
|
42047
|
+
}
|
|
42048
|
+
/**
|
|
42049
|
+
* Reciprocal Rank Fusion: combine three ranked lists into a single ranking.
|
|
42050
|
+
* Formula: score(d) = Σ 1/(k + rank_i) for each list where d appears.
|
|
42051
|
+
*/
|
|
42052
|
+
fuseResults(lexical, tags, graph, limit) {
|
|
42053
|
+
const fused = /* @__PURE__ */ new Map();
|
|
42054
|
+
this.addRrfScores(fused, lexical, "lexical");
|
|
42055
|
+
this.addRrfScores(fused, tags, "tag");
|
|
42056
|
+
this.addRrfScores(fused, graph, "graph");
|
|
42057
|
+
return Array.from(fused.entries()).map(([noteName, { score, matchType }]) => ({ noteName, score, matchType })).sort((a, b) => b.score - a.score).slice(0, limit);
|
|
42058
|
+
}
|
|
42059
|
+
/**
|
|
42060
|
+
* Accumulate RRF scores from a single ranked list into the fused map.
|
|
42061
|
+
* The matchType is set to the source with the highest individual contribution.
|
|
42062
|
+
*/
|
|
42063
|
+
addRrfScores(fused, ranked, matchType) {
|
|
42064
|
+
for (let i = 0; i < ranked.length; i++) {
|
|
42065
|
+
const rrfScore = 1 / (RRF_K + i + 1);
|
|
42066
|
+
const existing = fused.get(ranked[i]);
|
|
42067
|
+
if (existing) {
|
|
42068
|
+
existing.score += rrfScore;
|
|
42069
|
+
if (rrfScore > 1 / (RRF_K + 1)) {
|
|
42070
|
+
existing.matchType = matchType;
|
|
42071
|
+
}
|
|
42072
|
+
} else {
|
|
42073
|
+
fused.set(ranked[i], { score: rrfScore, matchType });
|
|
42074
|
+
}
|
|
42075
|
+
}
|
|
42076
|
+
}
|
|
42077
|
+
/** Split query into lowercase tokens for matching. */
|
|
42078
|
+
tokenize(query) {
|
|
42079
|
+
return query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
42080
|
+
}
|
|
42081
|
+
/** Count non-overlapping occurrences of a term in text. */
|
|
42082
|
+
countOccurrences(text, term) {
|
|
42083
|
+
let count = 0;
|
|
42084
|
+
let pos = 0;
|
|
42085
|
+
while ((pos = text.indexOf(term, pos)) !== -1) {
|
|
42086
|
+
count++;
|
|
42087
|
+
pos += term.length;
|
|
42088
|
+
}
|
|
42089
|
+
return count;
|
|
42090
|
+
}
|
|
42091
|
+
/** Average document length across the corpus (character-based). */
|
|
42092
|
+
computeAverageLength() {
|
|
42093
|
+
let total = 0;
|
|
42094
|
+
for (const content of this.contentMap.values()) {
|
|
42095
|
+
total += content.length;
|
|
42096
|
+
}
|
|
42097
|
+
return total / Math.max(this.contentMap.size, 1);
|
|
42098
|
+
}
|
|
42099
|
+
/** Number of documents containing the given term. */
|
|
42100
|
+
documentFrequency(term) {
|
|
42101
|
+
let count = 0;
|
|
42102
|
+
for (const content of this.contentMap.values()) {
|
|
42103
|
+
if (content.includes(term)) count++;
|
|
42104
|
+
}
|
|
42105
|
+
return count;
|
|
42106
|
+
}
|
|
42107
|
+
/** Sort entries by descending score and return the keys in order. */
|
|
42108
|
+
sortByScore(scores) {
|
|
42109
|
+
return Array.from(scores.entries()).sort((a, b) => b[1] - a[1]).map(([name]) => name);
|
|
42110
|
+
}
|
|
42111
|
+
};
|
|
42112
|
+
|
|
42113
|
+
// src/core/knowledge/tag-indexer.ts
|
|
42114
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
42115
|
+
var TagIndexer = class {
|
|
42116
|
+
tagMap = /* @__PURE__ */ new Map();
|
|
42117
|
+
fileCache = /* @__PURE__ */ new Map();
|
|
42118
|
+
/**
|
|
42119
|
+
* Parse frontmatter using regular expressions as a primary safe parser.
|
|
42120
|
+
* This avoids library dependencies and provides deterministic error recovery.
|
|
42121
|
+
*/
|
|
42122
|
+
parseFrontmatter(content) {
|
|
42123
|
+
const data = {};
|
|
42124
|
+
const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---/;
|
|
42125
|
+
const match = content.match(frontmatterRegex);
|
|
42126
|
+
if (!match) {
|
|
42127
|
+
return { data, body: content };
|
|
42128
|
+
}
|
|
42129
|
+
const rawYaml = match[1];
|
|
42130
|
+
const body = content.replace(frontmatterRegex, "").trim();
|
|
42131
|
+
const lines = rawYaml.split(/\r?\n/);
|
|
42132
|
+
let activeKey = null;
|
|
42133
|
+
for (const line of lines) {
|
|
42134
|
+
activeKey = this.parseYamlLine(line, data, activeKey);
|
|
42135
|
+
}
|
|
42136
|
+
return { data, body };
|
|
42137
|
+
}
|
|
42138
|
+
/**
|
|
42139
|
+
* Parse a single YAML line and update data object in-place.
|
|
42140
|
+
* Returns the currently active key for block list continuation.
|
|
42141
|
+
*/
|
|
42142
|
+
parseYamlLine(line, data, activeKey) {
|
|
42143
|
+
const trimmed = line.trim();
|
|
42144
|
+
if (!trimmed || trimmed.startsWith("#")) return activeKey;
|
|
42145
|
+
const colonIdx = trimmed.indexOf(":");
|
|
42146
|
+
if (colonIdx !== -1) {
|
|
42147
|
+
const key = trimmed.slice(0, colonIdx).trim();
|
|
42148
|
+
const val = trimmed.slice(colonIdx + 1).trim();
|
|
42149
|
+
if (val.startsWith("[") && val.endsWith("]")) {
|
|
42150
|
+
data[key] = val.slice(1, -1).split(",").map((s) => s.trim()).filter(Boolean);
|
|
42151
|
+
} else if (val) {
|
|
42152
|
+
data[key] = this.parseScalar(val);
|
|
42153
|
+
} else {
|
|
42154
|
+
data[key] = [];
|
|
42155
|
+
}
|
|
42156
|
+
return key;
|
|
42157
|
+
}
|
|
42158
|
+
if (trimmed.startsWith("-") && activeKey) {
|
|
42159
|
+
const listVal = trimmed.slice(1).trim();
|
|
42160
|
+
if (listVal) {
|
|
42161
|
+
const currentList = Array.isArray(data[activeKey]) ? data[activeKey] : [];
|
|
42162
|
+
currentList.push(listVal);
|
|
42163
|
+
data[activeKey] = currentList;
|
|
42164
|
+
}
|
|
42165
|
+
}
|
|
42166
|
+
return activeKey;
|
|
42167
|
+
}
|
|
42168
|
+
/**
|
|
42169
|
+
* Index a single markdown file content dynamically.
|
|
42170
|
+
*/
|
|
42171
|
+
indexFile(filePath, fileContent) {
|
|
42172
|
+
this.clearIndexForFile(filePath);
|
|
42173
|
+
const { data } = this.parseFrontmatter(fileContent);
|
|
42174
|
+
this.fileCache.set(filePath, data);
|
|
42175
|
+
if (data.tags && Array.isArray(data.tags)) {
|
|
42176
|
+
for (const tag of data.tags) {
|
|
42177
|
+
if (typeof tag === "string") {
|
|
42178
|
+
this.addTagEntry(tag.toLowerCase(), filePath);
|
|
42179
|
+
}
|
|
42180
|
+
}
|
|
42181
|
+
}
|
|
42182
|
+
}
|
|
42183
|
+
/**
|
|
42184
|
+
* Index a file directly from the filesystem.
|
|
42185
|
+
*/
|
|
42186
|
+
indexFileFromDisk(filePath) {
|
|
42187
|
+
try {
|
|
42188
|
+
const content = readFileSync4(filePath, "utf8");
|
|
42189
|
+
this.indexFile(filePath, content);
|
|
42190
|
+
} catch {
|
|
42191
|
+
this.clearIndexForFile(filePath);
|
|
42192
|
+
}
|
|
42193
|
+
}
|
|
42194
|
+
/**
|
|
42195
|
+
* Get all files associated with a specific tag O(1).
|
|
42196
|
+
*/
|
|
42197
|
+
getFilesWithTag(tag) {
|
|
42198
|
+
return this.tagMap.get(tag.toLowerCase()) || /* @__PURE__ */ new Set();
|
|
42199
|
+
}
|
|
42200
|
+
/**
|
|
42201
|
+
* Get intersection of files containing all specified tags.
|
|
42202
|
+
*/
|
|
42203
|
+
getFilesWithAllTags(tags) {
|
|
42204
|
+
if (tags.length === 0) return /* @__PURE__ */ new Set();
|
|
42205
|
+
let result = new Set(this.getFilesWithTag(tags[0]));
|
|
42206
|
+
for (let i = 1; i < tags.length; i++) {
|
|
42207
|
+
const currentSet = this.getFilesWithTag(tags[i]);
|
|
42208
|
+
result = new Set([...result].filter((x) => currentSet.has(x)));
|
|
42209
|
+
}
|
|
42210
|
+
return result;
|
|
42211
|
+
}
|
|
42212
|
+
/**
|
|
42213
|
+
* Get union of files containing any of the specified tags.
|
|
42214
|
+
*/
|
|
42215
|
+
getFilesWithAnyTags(tags) {
|
|
42216
|
+
const result = /* @__PURE__ */ new Set();
|
|
42217
|
+
for (const tag of tags) {
|
|
42218
|
+
const files = this.getFilesWithTag(tag);
|
|
42219
|
+
for (const file3 of files) {
|
|
42220
|
+
result.add(file3);
|
|
42221
|
+
}
|
|
42222
|
+
}
|
|
42223
|
+
return result;
|
|
42224
|
+
}
|
|
42225
|
+
/**
|
|
42226
|
+
* Get the cached frontmatter metadata of a file.
|
|
42227
|
+
*/
|
|
42228
|
+
getMetadata(filePath) {
|
|
42229
|
+
return this.fileCache.get(filePath);
|
|
42230
|
+
}
|
|
42231
|
+
/**
|
|
42232
|
+
* Return all file paths that have been indexed.
|
|
42233
|
+
*/
|
|
42234
|
+
getIndexedFiles() {
|
|
42235
|
+
return Array.from(this.fileCache.keys());
|
|
42236
|
+
}
|
|
42237
|
+
/**
|
|
42238
|
+
* Return all distinct tags currently present across all indexed files.
|
|
42239
|
+
*/
|
|
42240
|
+
getAllTags() {
|
|
42241
|
+
return Array.from(this.tagMap.keys());
|
|
42242
|
+
}
|
|
42243
|
+
/**
|
|
42244
|
+
* Remove file references cleanly from tag mappings and metadata cache.
|
|
42245
|
+
*/
|
|
42246
|
+
clearIndexForFile(filePath) {
|
|
42247
|
+
this.fileCache.delete(filePath);
|
|
42248
|
+
for (const [tag, files] of this.tagMap.entries()) {
|
|
42249
|
+
if (files.has(filePath)) {
|
|
42250
|
+
files.delete(filePath);
|
|
42251
|
+
if (files.size === 0) {
|
|
42252
|
+
this.tagMap.delete(tag);
|
|
42253
|
+
}
|
|
42254
|
+
}
|
|
42255
|
+
}
|
|
42256
|
+
}
|
|
42257
|
+
/**
|
|
42258
|
+
* Helper to insert tags atomically.
|
|
42259
|
+
*/
|
|
42260
|
+
addTagEntry(tag, filePath) {
|
|
42261
|
+
let files = this.tagMap.get(tag);
|
|
42262
|
+
if (!files) {
|
|
42263
|
+
files = /* @__PURE__ */ new Set();
|
|
42264
|
+
this.tagMap.set(tag, files);
|
|
42265
|
+
}
|
|
42266
|
+
files.add(filePath);
|
|
42267
|
+
}
|
|
42268
|
+
/**
|
|
42269
|
+
* Basic scalar value parser for YAML frontmatter fields.
|
|
42270
|
+
*/
|
|
42271
|
+
parseScalar(val) {
|
|
42272
|
+
if (val === "true") return true;
|
|
42273
|
+
if (val === "false") return false;
|
|
42274
|
+
const num = Number(val);
|
|
42275
|
+
if (!isNaN(num)) return num;
|
|
42276
|
+
if (val.startsWith('"') && val.endsWith('"') || val.startsWith("'") && val.endsWith("'")) {
|
|
42277
|
+
return val.slice(1, -1);
|
|
42278
|
+
}
|
|
42279
|
+
return val;
|
|
42280
|
+
}
|
|
42281
|
+
};
|
|
42282
|
+
|
|
42283
|
+
// src/core/knowledge/context-provider.ts
|
|
42284
|
+
var MAX_RESULTS = 3;
|
|
42285
|
+
var MAX_SNIPPET_CHARS = 220;
|
|
42286
|
+
var KNOWLEDGE_ROOTS = ["docs", path10.join(".opencode", "docs")];
|
|
42287
|
+
var SKIP_SEGMENTS = /* @__PURE__ */ new Set(["node_modules", "dist", "bin", ".git", "archive"]);
|
|
42288
|
+
var KnowledgeContextProvider = class {
|
|
42289
|
+
buildPrompt(directory, query) {
|
|
42290
|
+
const normalizedQuery = query.trim();
|
|
42291
|
+
if (!normalizedQuery) return null;
|
|
42292
|
+
const markdownFiles = this.collectMarkdownFiles(directory);
|
|
42293
|
+
if (markdownFiles.length === 0) return null;
|
|
42294
|
+
const indexed = this.indexKnowledge(directory, markdownFiles);
|
|
42295
|
+
const results = indexed.search.search(normalizedQuery, MAX_RESULTS);
|
|
42296
|
+
if (results.length === 0) return null;
|
|
42297
|
+
return this.formatPrompt(normalizedQuery, results, indexed);
|
|
42298
|
+
}
|
|
42299
|
+
collectMarkdownFiles(directory) {
|
|
42300
|
+
const files = [];
|
|
42301
|
+
for (const root of KNOWLEDGE_ROOTS) {
|
|
42302
|
+
const fullRoot = path10.join(directory, root);
|
|
42303
|
+
if (!existsSync9(fullRoot)) continue;
|
|
42304
|
+
files.push(...this.walkDirectory(fullRoot));
|
|
42305
|
+
}
|
|
42306
|
+
return files.sort();
|
|
42307
|
+
}
|
|
42308
|
+
walkDirectory(directory) {
|
|
42309
|
+
const files = [];
|
|
42310
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
42311
|
+
const fullPath = path10.join(directory, entry.name);
|
|
42312
|
+
if (entry.isDirectory()) {
|
|
42313
|
+
if (SKIP_SEGMENTS.has(entry.name)) continue;
|
|
42314
|
+
files.push(...this.walkDirectory(fullPath));
|
|
42315
|
+
continue;
|
|
42316
|
+
}
|
|
42317
|
+
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
42318
|
+
files.push(fullPath);
|
|
42319
|
+
}
|
|
42320
|
+
}
|
|
42321
|
+
return files;
|
|
42322
|
+
}
|
|
42323
|
+
indexKnowledge(directory, files) {
|
|
42324
|
+
const tagIndexer = new TagIndexer();
|
|
42325
|
+
const graphParser = new GraphParser();
|
|
42326
|
+
const search = new HybridSearch(tagIndexer, graphParser);
|
|
42327
|
+
const noteToPath = /* @__PURE__ */ new Map();
|
|
42328
|
+
const noteToSnippet = /* @__PURE__ */ new Map();
|
|
42329
|
+
for (const filePath of files) {
|
|
42330
|
+
try {
|
|
42331
|
+
const content = readFileSync5(filePath, "utf8");
|
|
42332
|
+
const noteName = graphParser.getNoteName(filePath);
|
|
42333
|
+
const { body } = tagIndexer.parseFrontmatter(content);
|
|
42334
|
+
const normalizedBody = body.trim();
|
|
42335
|
+
tagIndexer.indexFile(filePath, content);
|
|
42336
|
+
graphParser.indexFile(filePath, content);
|
|
42337
|
+
search.indexContent(noteName, normalizedBody);
|
|
42338
|
+
noteToPath.set(noteName, path10.relative(directory, filePath) || filePath);
|
|
42339
|
+
noteToSnippet.set(noteName, this.buildSnippet(normalizedBody));
|
|
42340
|
+
} catch {
|
|
42341
|
+
continue;
|
|
42342
|
+
}
|
|
42343
|
+
}
|
|
42344
|
+
return { search, noteToPath, noteToSnippet };
|
|
42345
|
+
}
|
|
42346
|
+
buildSnippet(content) {
|
|
42347
|
+
const normalized = content.replace(/\s+/g, " ").trim();
|
|
42348
|
+
if (normalized.length <= MAX_SNIPPET_CHARS) return normalized;
|
|
42349
|
+
return `${normalized.slice(0, MAX_SNIPPET_CHARS)}...`;
|
|
42350
|
+
}
|
|
42351
|
+
formatPrompt(query, results, indexed) {
|
|
42352
|
+
const lines = [
|
|
42353
|
+
"<knowledge_rag_context>",
|
|
42354
|
+
`Query: ${query}`,
|
|
42355
|
+
"Repository knowledge matches:",
|
|
42356
|
+
""
|
|
42357
|
+
];
|
|
42358
|
+
for (const [index, result] of results.entries()) {
|
|
42359
|
+
const filePath = indexed.noteToPath.get(result.noteName) ?? result.noteName;
|
|
42360
|
+
const snippet = indexed.noteToSnippet.get(result.noteName) ?? "";
|
|
42361
|
+
lines.push(`${index + 1}. ${result.noteName} [${result.matchType}]`);
|
|
42362
|
+
lines.push(` Source: ${filePath}`);
|
|
42363
|
+
if (snippet) {
|
|
42364
|
+
lines.push(` Snippet: ${snippet}`);
|
|
42365
|
+
}
|
|
42366
|
+
}
|
|
42367
|
+
lines.push("");
|
|
42368
|
+
lines.push("Use this as supplemental repository memory. Verify against source files before making claims.");
|
|
42369
|
+
lines.push("</knowledge_rag_context>");
|
|
42370
|
+
return lines.join("\n");
|
|
42371
|
+
}
|
|
42372
|
+
};
|
|
42373
|
+
|
|
42374
|
+
// src/plugin-handlers/system-transform-handler.ts
|
|
42375
|
+
var knowledgeContextProvider = new KnowledgeContextProvider();
|
|
41808
42376
|
function createSystemTransformHandler(ctx) {
|
|
41809
42377
|
const { directory, sessions, state: state2 } = ctx;
|
|
41810
42378
|
return async (input, output) => {
|
|
@@ -41825,6 +42393,14 @@ function createSystemTransformHandler(ctx) {
|
|
|
41825
42393
|
if (session?.active) {
|
|
41826
42394
|
systemAdditions.push(buildActiveSessionPrompt(session.step));
|
|
41827
42395
|
}
|
|
42396
|
+
const knowledgePrompt = buildKnowledgeContextPrompt(
|
|
42397
|
+
directory,
|
|
42398
|
+
loopState?.prompt,
|
|
42399
|
+
state2.sessions.get(sessionID)?.currentTask
|
|
42400
|
+
);
|
|
42401
|
+
if (knowledgePrompt) {
|
|
42402
|
+
systemAdditions.push(knowledgePrompt);
|
|
42403
|
+
}
|
|
41828
42404
|
try {
|
|
41829
42405
|
const manager = ParallelAgentManager.getInstance();
|
|
41830
42406
|
const tasks = manager.getTasksByParent(sessionID);
|
|
@@ -41840,6 +42416,10 @@ function createSystemTransformHandler(ctx) {
|
|
|
41840
42416
|
}
|
|
41841
42417
|
};
|
|
41842
42418
|
}
|
|
42419
|
+
function buildKnowledgeContextPrompt(directory, missionPrompt, currentTask) {
|
|
42420
|
+
const queryParts = [missionPrompt ?? "", currentTask ?? ""].filter(Boolean);
|
|
42421
|
+
return knowledgeContextProvider.buildPrompt(directory, queryParts.join(" ").trim());
|
|
42422
|
+
}
|
|
41843
42423
|
function buildMissionLoopSystemPrompt(iteration, maxIterations) {
|
|
41844
42424
|
return `<orchestrator_mission_loop>
|
|
41845
42425
|
\u{1F3AF} MISSION LOOP ACTIVE: Iteration ${iteration}/${maxIterations}
|
|
@@ -41945,23 +42525,19 @@ var OrchestratorPlugin = async (input) => {
|
|
|
41945
42525
|
// -----------------------------------------------------------------
|
|
41946
42526
|
// tool.execute.before hook - runs before any tool call
|
|
41947
42527
|
// -----------------------------------------------------------------
|
|
41948
|
-
|
|
42528
|
+
[PLUGIN_HOOKS.TOOL_EXECUTE_BEFORE]: createToolExecuteBeforeHandler(handlerContext),
|
|
41949
42529
|
// -----------------------------------------------------------------
|
|
41950
42530
|
// tool.execute.after hook - runs after any tool call completes
|
|
41951
42531
|
// -----------------------------------------------------------------
|
|
41952
42532
|
[PLUGIN_HOOKS.TOOL_EXECUTE_AFTER]: createToolExecuteAfterHandler(handlerContext),
|
|
41953
42533
|
// -----------------------------------------------------------------
|
|
41954
|
-
// assistant.done hook - runs when the LLM finishes responding
|
|
41955
|
-
// -----------------------------------------------------------------
|
|
41956
|
-
[PLUGIN_HOOKS.ASSISTANT_DONE]: createAssistantDoneHandler(handlerContext),
|
|
41957
|
-
// -----------------------------------------------------------------
|
|
41958
42534
|
// experimental.session.compacting hook - preserves mission context during compaction
|
|
41959
42535
|
// -----------------------------------------------------------------
|
|
41960
|
-
|
|
42536
|
+
[PLUGIN_HOOKS.EXPERIMENTAL_SESSION_COMPACTING]: createSessionCompactingHandler(handlerContext),
|
|
41961
42537
|
// -----------------------------------------------------------------
|
|
41962
42538
|
// experimental.chat.system.transform hook - dynamic system prompt injection
|
|
41963
42539
|
// -----------------------------------------------------------------
|
|
41964
|
-
|
|
42540
|
+
[PLUGIN_HOOKS.EXPERIMENTAL_CHAT_SYSTEM_TRANSFORM]: createSystemTransformHandler(handlerContext),
|
|
41965
42541
|
// -----------------------------------------------------------------
|
|
41966
42542
|
// shutdown hook - cleanup resources on plugin unload
|
|
41967
42543
|
// -----------------------------------------------------------------
|