@danielmarbach/mnemonic-mcp 0.9.0 → 0.11.0
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/CHANGELOG.md +22 -0
- package/build/branch-tracker.d.ts +5 -0
- package/build/branch-tracker.d.ts.map +1 -0
- package/build/branch-tracker.js +26 -0
- package/build/branch-tracker.js.map +1 -0
- package/build/git.d.ts +4 -3
- package/build/git.d.ts.map +1 -1
- package/build/git.js +6 -2
- package/build/git.js.map +1 -1
- package/build/index.js +281 -37
- package/build/index.js.map +1 -1
- package/build/storage.d.ts.map +1 -1
- package/build/storage.js +7 -12
- package/build/storage.js.map +1 -1
- package/build/structured-content.d.ts +166 -6
- package/build/structured-content.d.ts.map +1 -1
- package/build/structured-content.js +20 -1
- package/build/structured-content.js.map +1 -1
- package/build/vault.d.ts.map +1 -1
- package/build/vault.js +10 -9
- package/build/vault.js.map +1 -1
- package/package.json +1 -1
package/build/index.js
CHANGED
|
@@ -15,6 +15,7 @@ import { CONSOLIDATION_MODES, PROTECTED_BRANCH_BEHAVIORS, PROJECT_POLICY_SCOPES,
|
|
|
15
15
|
import { classifyTheme, summarizePreview, titleCaseTheme } from "./project-introspection.js";
|
|
16
16
|
import { detectProject, getCurrentGitBranch, resolveProjectIdentity } from "./project.js";
|
|
17
17
|
import { VaultManager } from "./vault.js";
|
|
18
|
+
import { checkBranchChange } from "./branch-tracker.js";
|
|
18
19
|
import { Migrator } from "./migration.js";
|
|
19
20
|
import { parseMemorySections } from "./import.js";
|
|
20
21
|
import { defaultClaudeHome, defaultVaultPath, resolveUserPath } from "./paths.js";
|
|
@@ -331,6 +332,33 @@ async function resolveWriteVault(cwd, scope) {
|
|
|
331
332
|
function describeProject(project) {
|
|
332
333
|
return project ? `project '${project.name}' (${project.id})` : "global";
|
|
333
334
|
}
|
|
335
|
+
/**
|
|
336
|
+
* Checks if the git branch has changed since the last operation for a directory.
|
|
337
|
+
* If a branch change is detected, automatically triggers sync to rebuild embeddings.
|
|
338
|
+
* Returns true if sync was triggered, false otherwise.
|
|
339
|
+
*/
|
|
340
|
+
async function ensureBranchSynced(cwd) {
|
|
341
|
+
if (!cwd)
|
|
342
|
+
return false;
|
|
343
|
+
const previousBranch = await checkBranchChange(cwd);
|
|
344
|
+
if (!previousBranch)
|
|
345
|
+
return false; // No branch change or not in git repo
|
|
346
|
+
console.error(`[branch] Detected branch change from '${previousBranch}' — auto-syncing`);
|
|
347
|
+
// Trigger sync to rebuild embeddings
|
|
348
|
+
const mainResult = await vaultManager.main.git.sync();
|
|
349
|
+
console.error(`[branch] Main vault sync: ${JSON.stringify(mainResult)}`);
|
|
350
|
+
const projectVault = await vaultManager.getProjectVaultIfExists(cwd);
|
|
351
|
+
if (projectVault) {
|
|
352
|
+
const projectResult = await projectVault.git.sync();
|
|
353
|
+
console.error(`[branch] Project vault sync: ${JSON.stringify(projectResult)}`);
|
|
354
|
+
// Backfill embeddings after sync
|
|
355
|
+
const backfill = await backfillEmbeddingsAfterSync(projectVault.storage, "project vault", [], true);
|
|
356
|
+
console.error(`[branch] Project vault embedded ${backfill.embedded} notes`);
|
|
357
|
+
}
|
|
358
|
+
const mainBackfill = await backfillEmbeddingsAfterSync(vaultManager.main.storage, "main vault", [], true);
|
|
359
|
+
console.error(`[branch] Main vault embedded ${mainBackfill.embedded} notes`);
|
|
360
|
+
return true;
|
|
361
|
+
}
|
|
334
362
|
function formatProjectIdentityText(identity) {
|
|
335
363
|
const lines = [
|
|
336
364
|
`Project identity:`,
|
|
@@ -592,12 +620,34 @@ function buildPersistenceStatus(args) {
|
|
|
592
620
|
commitMessage: args.commitMessage,
|
|
593
621
|
commitBody: args.commitBody,
|
|
594
622
|
commitReason: args.commit.reason,
|
|
623
|
+
commitError: args.commit.error,
|
|
595
624
|
pushReason: args.push.reason,
|
|
596
625
|
pushError: args.push.error,
|
|
597
626
|
},
|
|
627
|
+
retry: args.retry,
|
|
598
628
|
durability: resolveDurability(args.commit, args.push),
|
|
599
629
|
};
|
|
600
630
|
}
|
|
631
|
+
function buildMutationRetryContract(args) {
|
|
632
|
+
if (args.commit.status !== "failed") {
|
|
633
|
+
return undefined;
|
|
634
|
+
}
|
|
635
|
+
return {
|
|
636
|
+
attemptedCommit: {
|
|
637
|
+
message: args.commitMessage,
|
|
638
|
+
body: args.commitBody,
|
|
639
|
+
files: args.files,
|
|
640
|
+
cwd: args.cwd,
|
|
641
|
+
vault: storageLabel(args.vault),
|
|
642
|
+
error: args.commit.error ?? "Unknown git commit failure",
|
|
643
|
+
},
|
|
644
|
+
mutationApplied: args.mutationApplied,
|
|
645
|
+
retrySafe: args.mutationApplied,
|
|
646
|
+
rationale: args.mutationApplied
|
|
647
|
+
? "Mutation is already persisted on disk; commit can be retried deterministically."
|
|
648
|
+
: "Mutation was not applied; retry may require re-running the operation.",
|
|
649
|
+
};
|
|
650
|
+
}
|
|
601
651
|
function formatPersistenceSummary(persistence) {
|
|
602
652
|
const parts = [
|
|
603
653
|
`Persistence: embedding ${persistence.embedding.status}`,
|
|
@@ -716,7 +766,7 @@ async function formatProjectPolicyLine(projectId) {
|
|
|
716
766
|
}
|
|
717
767
|
return `Policy: default write scope ${policy.defaultScope} (updated ${policy.updatedAt})`;
|
|
718
768
|
}
|
|
719
|
-
async function moveNoteBetweenVaults(found, targetVault, noteToWrite) {
|
|
769
|
+
async function moveNoteBetweenVaults(found, targetVault, noteToWrite, cwd) {
|
|
720
770
|
const { note, vault: sourceVault } = found;
|
|
721
771
|
const finalNote = noteToWrite ?? note;
|
|
722
772
|
const embedding = await sourceVault.storage.readEmbedding(note.id);
|
|
@@ -733,7 +783,9 @@ async function moveNoteBetweenVaults(found, targetVault, noteToWrite) {
|
|
|
733
783
|
noteTitle: finalNote.title,
|
|
734
784
|
projectName: finalNote.projectName,
|
|
735
785
|
});
|
|
736
|
-
const
|
|
786
|
+
const targetCommitMessage = `move: ${finalNote.title}`;
|
|
787
|
+
const targetCommitFiles = [vaultManager.noteRelPath(targetVault, finalNote.id)];
|
|
788
|
+
const targetCommit = await targetVault.git.commitWithStatus(targetCommitMessage, targetCommitFiles, targetCommitBody);
|
|
737
789
|
const sourceCommitBody = formatCommitBody({
|
|
738
790
|
summary: `Moved to ${targetVaultLabel}`,
|
|
739
791
|
noteId: finalNote.id,
|
|
@@ -741,7 +793,18 @@ async function moveNoteBetweenVaults(found, targetVault, noteToWrite) {
|
|
|
741
793
|
projectName: finalNote.projectName,
|
|
742
794
|
});
|
|
743
795
|
await sourceVault.git.commitWithStatus(`move: ${finalNote.title}`, [vaultManager.noteRelPath(sourceVault, finalNote.id)], sourceCommitBody);
|
|
744
|
-
const targetPush =
|
|
796
|
+
const targetPush = targetCommit.status === "committed"
|
|
797
|
+
? await pushAfterMutation(targetVault)
|
|
798
|
+
: { status: "skipped", reason: "commit-failed" };
|
|
799
|
+
const retry = buildMutationRetryContract({
|
|
800
|
+
commit: targetCommit,
|
|
801
|
+
commitMessage: targetCommitMessage,
|
|
802
|
+
commitBody: targetCommitBody,
|
|
803
|
+
files: targetCommitFiles,
|
|
804
|
+
cwd,
|
|
805
|
+
vault: targetVault,
|
|
806
|
+
mutationApplied: true,
|
|
807
|
+
});
|
|
745
808
|
if (sourceVault !== targetVault) {
|
|
746
809
|
await pushAfterMutation(sourceVault);
|
|
747
810
|
}
|
|
@@ -753,8 +816,9 @@ async function moveNoteBetweenVaults(found, targetVault, noteToWrite) {
|
|
|
753
816
|
embedding: embedding ? { status: "written" } : { status: "skipped", reason: "no-source-embedding" },
|
|
754
817
|
commit: targetCommit,
|
|
755
818
|
push: targetPush,
|
|
756
|
-
commitMessage:
|
|
819
|
+
commitMessage: targetCommitMessage,
|
|
757
820
|
commitBody: targetCommitBody,
|
|
821
|
+
retry,
|
|
758
822
|
}),
|
|
759
823
|
};
|
|
760
824
|
}
|
|
@@ -952,8 +1016,21 @@ server.registerTool("set_project_identity", {
|
|
|
952
1016
|
`Resolved identity: ${candidateIdentity.project.id}\n` +
|
|
953
1017
|
`Remote: ${remoteName}`,
|
|
954
1018
|
});
|
|
955
|
-
|
|
956
|
-
|
|
1019
|
+
const commitMessage = `identity: ${defaultProject.name} use remote ${remoteName}`;
|
|
1020
|
+
const commitFiles = ["config.json"];
|
|
1021
|
+
const commitStatus = await vaultManager.main.git.commitWithStatus(commitMessage, commitFiles, commitBody);
|
|
1022
|
+
const pushStatus = commitStatus.status === "committed"
|
|
1023
|
+
? await pushAfterMutation(vaultManager.main)
|
|
1024
|
+
: { status: "skipped", reason: "commit-failed" };
|
|
1025
|
+
const retry = buildMutationRetryContract({
|
|
1026
|
+
commit: commitStatus,
|
|
1027
|
+
commitMessage,
|
|
1028
|
+
commitBody,
|
|
1029
|
+
files: commitFiles,
|
|
1030
|
+
cwd,
|
|
1031
|
+
vault: vaultManager.main,
|
|
1032
|
+
mutationApplied: true,
|
|
1033
|
+
});
|
|
957
1034
|
const structuredContent = {
|
|
958
1035
|
action: "project_identity_set",
|
|
959
1036
|
project: {
|
|
@@ -971,12 +1048,14 @@ server.registerTool("set_project_identity", {
|
|
|
971
1048
|
remoteName,
|
|
972
1049
|
updatedAt: now,
|
|
973
1050
|
},
|
|
1051
|
+
retry,
|
|
974
1052
|
};
|
|
975
1053
|
return {
|
|
976
1054
|
content: [{
|
|
977
1055
|
type: "text",
|
|
978
1056
|
text: `Project identity override set for ${defaultProject.name}: ` +
|
|
979
|
-
`default=\`${defaultProject.id}\`, effective=\`${candidateIdentity.project.id}\`, remote=${remoteName}
|
|
1057
|
+
`default=\`${defaultProject.id}\`, effective=\`${candidateIdentity.project.id}\`, remote=${remoteName}` +
|
|
1058
|
+
`${commitStatus.status === "failed" ? `\nCommit failed; retry data included. Push status: ${pushStatus.status}.` : ""}`,
|
|
980
1059
|
}],
|
|
981
1060
|
structuredContent,
|
|
982
1061
|
};
|
|
@@ -1067,6 +1146,7 @@ server.registerTool("execute_migration", {
|
|
|
1067
1146
|
}),
|
|
1068
1147
|
outputSchema: MigrationExecuteResultSchema,
|
|
1069
1148
|
}, async ({ migrationName, dryRun, backup, cwd }) => {
|
|
1149
|
+
await ensureBranchSynced(cwd);
|
|
1070
1150
|
try {
|
|
1071
1151
|
const { results, vaultsProcessed } = await migrator.runMigration(migrationName, {
|
|
1072
1152
|
dryRun,
|
|
@@ -1182,6 +1262,7 @@ server.registerTool("remember", {
|
|
|
1182
1262
|
}),
|
|
1183
1263
|
outputSchema: RememberResultSchema,
|
|
1184
1264
|
}, async ({ title, content, tags, lifecycle, summary, cwd, scope, allowProtectedBranch = false }) => {
|
|
1265
|
+
await ensureBranchSynced(cwd);
|
|
1185
1266
|
const project = await resolveProject(cwd);
|
|
1186
1267
|
const cleanedContent = await cleanMarkdown(content);
|
|
1187
1268
|
const policy = project ? await configStore.getProjectPolicy(project.id) : undefined;
|
|
@@ -1195,7 +1276,7 @@ server.registerTool("remember", {
|
|
|
1195
1276
|
const protectedBranchCheck = await shouldBlockProtectedBranchCommit({
|
|
1196
1277
|
cwd,
|
|
1197
1278
|
writeScope,
|
|
1198
|
-
automaticCommit:
|
|
1279
|
+
automaticCommit: true,
|
|
1199
1280
|
projectLabel: project ? `${project.name} (${project.id})` : "this context",
|
|
1200
1281
|
policy,
|
|
1201
1282
|
allowProtectedBranch,
|
|
@@ -1236,16 +1317,30 @@ server.registerTool("remember", {
|
|
|
1236
1317
|
scope: writeScope,
|
|
1237
1318
|
tags: tags,
|
|
1238
1319
|
});
|
|
1239
|
-
const
|
|
1240
|
-
const
|
|
1320
|
+
const commitMessage = `remember: ${title}`;
|
|
1321
|
+
const commitFiles = [vaultManager.noteRelPath(vault, id)];
|
|
1322
|
+
const commitStatus = await vault.git.commitWithStatus(commitMessage, commitFiles, commitBody);
|
|
1323
|
+
const pushStatus = commitStatus.status === "committed"
|
|
1324
|
+
? await pushAfterMutation(vault)
|
|
1325
|
+
: { status: "skipped", reason: "commit-failed" };
|
|
1326
|
+
const retry = buildMutationRetryContract({
|
|
1327
|
+
commit: commitStatus,
|
|
1328
|
+
commitMessage,
|
|
1329
|
+
commitBody,
|
|
1330
|
+
files: commitFiles,
|
|
1331
|
+
cwd,
|
|
1332
|
+
vault,
|
|
1333
|
+
mutationApplied: true,
|
|
1334
|
+
});
|
|
1241
1335
|
const persistence = buildPersistenceStatus({
|
|
1242
1336
|
storage: vault.storage,
|
|
1243
1337
|
id,
|
|
1244
1338
|
embedding: embeddingStatus,
|
|
1245
1339
|
commit: commitStatus,
|
|
1246
1340
|
push: pushStatus,
|
|
1247
|
-
commitMessage
|
|
1341
|
+
commitMessage,
|
|
1248
1342
|
commitBody,
|
|
1343
|
+
retry,
|
|
1249
1344
|
});
|
|
1250
1345
|
const vaultLabel = vault.isProject ? " [project vault]" : " [main vault]";
|
|
1251
1346
|
const textContent = `Remembered as \`${id}\` [${projectScope}, stored=${writeScope}]${vaultLabel}\n${formatPersistenceSummary(persistence)}`;
|
|
@@ -1345,8 +1440,21 @@ server.registerTool("set_project_memory_policy", {
|
|
|
1345
1440
|
? `\nProtected branch patterns: ${effectiveProtectedBranchPatterns.join(", ")}`
|
|
1346
1441
|
: ""}`,
|
|
1347
1442
|
});
|
|
1348
|
-
|
|
1349
|
-
|
|
1443
|
+
const commitMessage = `policy: ${project.name} default scope ${effectiveDefaultScope}`;
|
|
1444
|
+
const commitFiles = ["config.json"];
|
|
1445
|
+
const commitStatus = await vaultManager.main.git.commitWithStatus(commitMessage, commitFiles, commitBody);
|
|
1446
|
+
const pushStatus = commitStatus.status === "committed"
|
|
1447
|
+
? await pushAfterMutation(vaultManager.main)
|
|
1448
|
+
: { status: "skipped", reason: "commit-failed" };
|
|
1449
|
+
const retry = buildMutationRetryContract({
|
|
1450
|
+
commit: commitStatus,
|
|
1451
|
+
commitMessage,
|
|
1452
|
+
commitBody,
|
|
1453
|
+
files: commitFiles,
|
|
1454
|
+
cwd,
|
|
1455
|
+
vault: vaultManager.main,
|
|
1456
|
+
mutationApplied: true,
|
|
1457
|
+
});
|
|
1350
1458
|
const structuredContent = {
|
|
1351
1459
|
action: "policy_set",
|
|
1352
1460
|
project: { id: project.id, name: project.name },
|
|
@@ -1354,13 +1462,15 @@ server.registerTool("set_project_memory_policy", {
|
|
|
1354
1462
|
consolidationMode: effectiveConsolidationMode,
|
|
1355
1463
|
protectedBranchBehavior: effectiveProtectedBranchBehavior,
|
|
1356
1464
|
protectedBranchPatterns: effectiveProtectedBranchPatterns,
|
|
1357
|
-
|
|
1465
|
+
updatedAt: now,
|
|
1466
|
+
retry,
|
|
1358
1467
|
};
|
|
1359
1468
|
return {
|
|
1360
1469
|
content: [{
|
|
1361
1470
|
type: "text",
|
|
1362
1471
|
text: `Project memory policy set for ${project.name}: defaultScope=${effectiveDefaultScope}` +
|
|
1363
|
-
`${modeStr}${branchBehaviorStr}${branchPatternsStr}
|
|
1472
|
+
`${modeStr}${branchBehaviorStr}${branchPatternsStr}` +
|
|
1473
|
+
`${commitStatus.status === "failed" ? `\nCommit failed; retry data included. Push status: ${pushStatus.status}.` : ""}`,
|
|
1364
1474
|
}],
|
|
1365
1475
|
structuredContent,
|
|
1366
1476
|
};
|
|
@@ -1470,9 +1580,24 @@ server.registerTool("recall", {
|
|
|
1470
1580
|
}),
|
|
1471
1581
|
outputSchema: RecallResultSchema,
|
|
1472
1582
|
}, async ({ query, cwd, limit, minSimilarity, tags, scope }) => {
|
|
1583
|
+
await ensureBranchSynced(cwd);
|
|
1473
1584
|
const project = await resolveProject(cwd);
|
|
1474
1585
|
const queryVec = await embed(query);
|
|
1475
1586
|
const vaults = await vaultManager.searchOrder(cwd);
|
|
1587
|
+
const noteCache = new Map();
|
|
1588
|
+
const noteCacheKey = (vault, id) => `${vault.storage.vaultPath}::${id}`;
|
|
1589
|
+
const readCachedNote = async (vault, id) => {
|
|
1590
|
+
const key = noteCacheKey(vault, id);
|
|
1591
|
+
const cached = noteCache.get(key);
|
|
1592
|
+
if (cached) {
|
|
1593
|
+
return cached;
|
|
1594
|
+
}
|
|
1595
|
+
const note = await vault.storage.readNote(id);
|
|
1596
|
+
if (note) {
|
|
1597
|
+
noteCache.set(key, note);
|
|
1598
|
+
}
|
|
1599
|
+
return note;
|
|
1600
|
+
};
|
|
1476
1601
|
for (const vault of vaults) {
|
|
1477
1602
|
await embedMissingNotes(vault.storage).catch(() => { });
|
|
1478
1603
|
}
|
|
@@ -1483,7 +1608,7 @@ server.registerTool("recall", {
|
|
|
1483
1608
|
const rawScore = cosineSimilarity(queryVec, rec.embedding);
|
|
1484
1609
|
if (rawScore < minSimilarity)
|
|
1485
1610
|
continue;
|
|
1486
|
-
const note = await vault
|
|
1611
|
+
const note = await readCachedNote(vault, rec.id);
|
|
1487
1612
|
if (!note)
|
|
1488
1613
|
continue;
|
|
1489
1614
|
if (tags && tags.length > 0) {
|
|
@@ -1512,7 +1637,7 @@ server.registerTool("recall", {
|
|
|
1512
1637
|
}
|
|
1513
1638
|
const sections = [];
|
|
1514
1639
|
for (const { id, score, vault } of top) {
|
|
1515
|
-
const note = await vault
|
|
1640
|
+
const note = await readCachedNote(vault, id);
|
|
1516
1641
|
if (note)
|
|
1517
1642
|
sections.push(formatNote(note, score));
|
|
1518
1643
|
}
|
|
@@ -1523,7 +1648,7 @@ server.registerTool("recall", {
|
|
|
1523
1648
|
// Build structured results array
|
|
1524
1649
|
const structuredResults = [];
|
|
1525
1650
|
for (const { id, score, vault, boosted } of top) {
|
|
1526
|
-
const note = await vault
|
|
1651
|
+
const note = await readCachedNote(vault, id);
|
|
1527
1652
|
if (note) {
|
|
1528
1653
|
structuredResults.push({
|
|
1529
1654
|
id,
|
|
@@ -1590,6 +1715,7 @@ server.registerTool("update", {
|
|
|
1590
1715
|
}),
|
|
1591
1716
|
outputSchema: UpdateResultSchema,
|
|
1592
1717
|
}, async ({ id, content, title, tags, lifecycle, summary, cwd, allowProtectedBranch = false }) => {
|
|
1718
|
+
await ensureBranchSynced(cwd);
|
|
1593
1719
|
const found = await vaultManager.findNote(id, cwd);
|
|
1594
1720
|
if (!found) {
|
|
1595
1721
|
return { content: [{ type: "text", text: `No memory found with id '${id}'` }], isError: true };
|
|
@@ -1656,16 +1782,30 @@ server.registerTool("update", {
|
|
|
1656
1782
|
projectName: updated.projectName,
|
|
1657
1783
|
tags: updated.tags,
|
|
1658
1784
|
});
|
|
1659
|
-
const
|
|
1660
|
-
const
|
|
1785
|
+
const commitMessage = `update: ${updated.title}`;
|
|
1786
|
+
const commitFiles = [vaultManager.noteRelPath(vault, id)];
|
|
1787
|
+
const commitStatus = await vault.git.commitWithStatus(commitMessage, commitFiles, commitBody);
|
|
1788
|
+
const pushStatus = commitStatus.status === "committed"
|
|
1789
|
+
? await pushAfterMutation(vault)
|
|
1790
|
+
: { status: "skipped", reason: "commit-failed" };
|
|
1791
|
+
const retry = buildMutationRetryContract({
|
|
1792
|
+
commit: commitStatus,
|
|
1793
|
+
commitMessage,
|
|
1794
|
+
commitBody,
|
|
1795
|
+
files: commitFiles,
|
|
1796
|
+
cwd,
|
|
1797
|
+
vault,
|
|
1798
|
+
mutationApplied: true,
|
|
1799
|
+
});
|
|
1661
1800
|
const persistence = buildPersistenceStatus({
|
|
1662
1801
|
storage: vault.storage,
|
|
1663
1802
|
id,
|
|
1664
1803
|
embedding: embeddingStatus,
|
|
1665
1804
|
commit: commitStatus,
|
|
1666
1805
|
push: pushStatus,
|
|
1667
|
-
commitMessage
|
|
1806
|
+
commitMessage,
|
|
1668
1807
|
commitBody,
|
|
1808
|
+
retry,
|
|
1669
1809
|
});
|
|
1670
1810
|
const structuredContent = {
|
|
1671
1811
|
action: "updated",
|
|
@@ -1712,6 +1852,7 @@ server.registerTool("forget", {
|
|
|
1712
1852
|
}),
|
|
1713
1853
|
outputSchema: ForgetResultSchema,
|
|
1714
1854
|
}, async ({ id, cwd, allowProtectedBranch = false }) => {
|
|
1855
|
+
await ensureBranchSynced(cwd);
|
|
1715
1856
|
const found = await vaultManager.findNote(id, cwd);
|
|
1716
1857
|
if (!found) {
|
|
1717
1858
|
return { content: [{ type: "text", text: `No memory found with id '${id}'` }], isError: true };
|
|
@@ -1745,6 +1886,7 @@ server.registerTool("forget", {
|
|
|
1745
1886
|
const vaultChanges = await removeRelationshipsToNoteIds([id]);
|
|
1746
1887
|
// Always include the deleted note's path (git add on a deleted file stages the removal)
|
|
1747
1888
|
addVaultChange(vaultChanges, noteVault, vaultManager.noteRelPath(noteVault, id));
|
|
1889
|
+
let retry;
|
|
1748
1890
|
for (const [v, files] of vaultChanges) {
|
|
1749
1891
|
const isPrimaryVault = v === noteVault;
|
|
1750
1892
|
const summary = isPrimaryVault ? `Deleted note and cleaned up ${files.length - 1} reference(s)` : "Cleaned up dangling reference";
|
|
@@ -1754,8 +1896,22 @@ server.registerTool("forget", {
|
|
|
1754
1896
|
noteTitle: note.title,
|
|
1755
1897
|
projectName: note.projectName,
|
|
1756
1898
|
});
|
|
1757
|
-
|
|
1758
|
-
await
|
|
1899
|
+
const commitMessage = `forget: ${note.title}`;
|
|
1900
|
+
const commitStatus = await v.git.commitWithStatus(commitMessage, files, commitBody);
|
|
1901
|
+
if (!retry) {
|
|
1902
|
+
retry = buildMutationRetryContract({
|
|
1903
|
+
commit: commitStatus,
|
|
1904
|
+
commitMessage,
|
|
1905
|
+
commitBody,
|
|
1906
|
+
files,
|
|
1907
|
+
cwd,
|
|
1908
|
+
vault: v,
|
|
1909
|
+
mutationApplied: true,
|
|
1910
|
+
});
|
|
1911
|
+
}
|
|
1912
|
+
if (commitStatus.status === "committed") {
|
|
1913
|
+
await pushAfterMutation(v);
|
|
1914
|
+
}
|
|
1759
1915
|
}
|
|
1760
1916
|
const structuredContent = {
|
|
1761
1917
|
action: "forgotten",
|
|
@@ -1765,6 +1921,7 @@ server.registerTool("forget", {
|
|
|
1765
1921
|
projectName: note.projectName,
|
|
1766
1922
|
relationshipsCleaned: vaultChanges.size > 0 ? Array.from(vaultChanges.values()).reduce((sum, files) => sum + files.length - 1, 0) : 0,
|
|
1767
1923
|
vaultsModified: Array.from(vaultChanges.keys()).map(v => storageLabel(v)),
|
|
1924
|
+
retry,
|
|
1768
1925
|
};
|
|
1769
1926
|
return { content: [{ type: "text", text: `Forgotten '${id}' (${note.title})` }], structuredContent };
|
|
1770
1927
|
});
|
|
@@ -1794,6 +1951,7 @@ server.registerTool("get", {
|
|
|
1794
1951
|
}),
|
|
1795
1952
|
outputSchema: GetResultSchema,
|
|
1796
1953
|
}, async ({ ids, cwd }) => {
|
|
1954
|
+
await ensureBranchSynced(cwd);
|
|
1797
1955
|
const found = [];
|
|
1798
1956
|
const notFound = [];
|
|
1799
1957
|
for (const id of ids) {
|
|
@@ -1864,6 +2022,7 @@ server.registerTool("where_is_memory", {
|
|
|
1864
2022
|
}),
|
|
1865
2023
|
outputSchema: WhereIsResultSchema,
|
|
1866
2024
|
}, async ({ id, cwd }) => {
|
|
2025
|
+
await ensureBranchSynced(cwd);
|
|
1867
2026
|
const found = await vaultManager.findNote(id, cwd);
|
|
1868
2027
|
if (!found) {
|
|
1869
2028
|
return { content: [{ type: "text", text: `No memory found with id '${id}'` }], isError: true };
|
|
@@ -1937,6 +2096,7 @@ server.registerTool("list", {
|
|
|
1937
2096
|
}),
|
|
1938
2097
|
outputSchema: ListResultSchema,
|
|
1939
2098
|
}, async ({ cwd, scope, storedIn, tags, includeRelations, includePreview, includeStorage, includeUpdated }) => {
|
|
2099
|
+
await ensureBranchSynced(cwd);
|
|
1940
2100
|
const { project, entries } = await collectVisibleNotes(cwd, scope, tags, storedIn);
|
|
1941
2101
|
if (entries.length === 0) {
|
|
1942
2102
|
const structuredContent = { action: "listed", count: 0, scope: scope || "all", storedIn: storedIn || "any", project: project ? { id: project.id, name: project.name } : undefined, notes: [] };
|
|
@@ -2009,6 +2169,7 @@ server.registerTool("recent_memories", {
|
|
|
2009
2169
|
}),
|
|
2010
2170
|
outputSchema: RecentResultSchema,
|
|
2011
2171
|
}, async ({ cwd, scope, storedIn, limit, includePreview, includeStorage }) => {
|
|
2172
|
+
await ensureBranchSynced(cwd);
|
|
2012
2173
|
const { project, entries } = await collectVisibleNotes(cwd, scope, undefined, storedIn);
|
|
2013
2174
|
const recent = [...entries]
|
|
2014
2175
|
.sort((a, b) => b.note.updatedAt.localeCompare(a.note.updatedAt))
|
|
@@ -2075,6 +2236,7 @@ server.registerTool("memory_graph", {
|
|
|
2075
2236
|
}),
|
|
2076
2237
|
outputSchema: MemoryGraphResultSchema,
|
|
2077
2238
|
}, async ({ cwd, scope, storedIn, limit }) => {
|
|
2239
|
+
await ensureBranchSynced(cwd);
|
|
2078
2240
|
const { project, entries } = await collectVisibleNotes(cwd, scope, undefined, storedIn);
|
|
2079
2241
|
if (entries.length === 0) {
|
|
2080
2242
|
const structuredContent = { action: "graph_shown", project: project?.id, projectName: project?.name, nodes: [], limit, truncated: false };
|
|
@@ -2151,6 +2313,7 @@ server.registerTool("project_memory_summary", {
|
|
|
2151
2313
|
}),
|
|
2152
2314
|
outputSchema: ProjectSummaryResultSchema,
|
|
2153
2315
|
}, async ({ cwd, maxPerTheme, recentLimit }) => {
|
|
2316
|
+
await ensureBranchSynced(cwd);
|
|
2154
2317
|
const { project, entries } = await collectVisibleNotes(cwd, "all");
|
|
2155
2318
|
if (!project) {
|
|
2156
2319
|
return { content: [{ type: "text", text: `Could not detect a project for: ${cwd}` }], isError: true };
|
|
@@ -2342,6 +2505,7 @@ server.registerTool("move_memory", {
|
|
|
2342
2505
|
}),
|
|
2343
2506
|
outputSchema: MoveResultSchema,
|
|
2344
2507
|
}, async ({ id, target, vaultFolder, cwd, allowProtectedBranch = false }) => {
|
|
2508
|
+
await ensureBranchSynced(cwd);
|
|
2345
2509
|
const found = await vaultManager.findNote(id, cwd);
|
|
2346
2510
|
if (!found) {
|
|
2347
2511
|
return { content: [{ type: "text", text: `No memory found with id '${id}'` }], isError: true };
|
|
@@ -2432,7 +2596,7 @@ server.registerTool("move_memory", {
|
|
|
2432
2596
|
updatedAt: new Date().toISOString(),
|
|
2433
2597
|
};
|
|
2434
2598
|
}
|
|
2435
|
-
const moveResult = await moveNoteBetweenVaults(found, targetVault, noteToWrite);
|
|
2599
|
+
const moveResult = await moveNoteBetweenVaults(found, targetVault, noteToWrite, cwd);
|
|
2436
2600
|
const movedNote = moveResult.note;
|
|
2437
2601
|
const associationValue = movedNote.projectName && movedNote.project
|
|
2438
2602
|
? `${movedNote.projectName} (${movedNote.project})`
|
|
@@ -2494,6 +2658,7 @@ server.registerTool("relate", {
|
|
|
2494
2658
|
}),
|
|
2495
2659
|
outputSchema: RelateResultSchema,
|
|
2496
2660
|
}, async ({ fromId, toId, type, bidirectional, cwd }) => {
|
|
2661
|
+
await ensureBranchSynced(cwd);
|
|
2497
2662
|
const [foundFrom, foundTo] = await Promise.all([
|
|
2498
2663
|
vaultManager.findNote(fromId, cwd),
|
|
2499
2664
|
vaultManager.findNote(toId, cwd),
|
|
@@ -2527,6 +2692,7 @@ server.registerTool("relate", {
|
|
|
2527
2692
|
return { content: [{ type: "text", text: `Relationship already exists between '${fromId}' and '${toId}'` }], isError: true };
|
|
2528
2693
|
}
|
|
2529
2694
|
const modifiedNoteIds = [];
|
|
2695
|
+
let retry;
|
|
2530
2696
|
for (const [vault, files] of vaultChanges) {
|
|
2531
2697
|
const isFromVault = vault === fromVault;
|
|
2532
2698
|
const thisNote = isFromVault ? fromNote : toNote;
|
|
@@ -2541,8 +2707,22 @@ server.registerTool("relate", {
|
|
|
2541
2707
|
type,
|
|
2542
2708
|
},
|
|
2543
2709
|
});
|
|
2544
|
-
|
|
2545
|
-
await
|
|
2710
|
+
const commitMessage = `relate: ${fromNote.title} ↔ ${toNote.title}`;
|
|
2711
|
+
const commitStatus = await vault.git.commitWithStatus(commitMessage, files, commitBody);
|
|
2712
|
+
if (!retry) {
|
|
2713
|
+
retry = buildMutationRetryContract({
|
|
2714
|
+
commit: commitStatus,
|
|
2715
|
+
commitMessage,
|
|
2716
|
+
commitBody,
|
|
2717
|
+
files,
|
|
2718
|
+
cwd,
|
|
2719
|
+
vault,
|
|
2720
|
+
mutationApplied: true,
|
|
2721
|
+
});
|
|
2722
|
+
}
|
|
2723
|
+
if (commitStatus.status === "committed") {
|
|
2724
|
+
await pushAfterMutation(vault);
|
|
2725
|
+
}
|
|
2546
2726
|
modifiedNoteIds.push(...files.map(f => path.basename(f, '.md')));
|
|
2547
2727
|
}
|
|
2548
2728
|
const dirStr = bidirectional ? "↔" : "→";
|
|
@@ -2553,6 +2733,7 @@ server.registerTool("relate", {
|
|
|
2553
2733
|
type,
|
|
2554
2734
|
bidirectional,
|
|
2555
2735
|
notesModified: modifiedNoteIds,
|
|
2736
|
+
retry,
|
|
2556
2737
|
};
|
|
2557
2738
|
return {
|
|
2558
2739
|
content: [{ type: "text", text: `Linked \`${fromId}\` ${dirStr} \`${toId}\` (${type})` }],
|
|
@@ -2587,6 +2768,7 @@ server.registerTool("unrelate", {
|
|
|
2587
2768
|
}),
|
|
2588
2769
|
outputSchema: RelateResultSchema,
|
|
2589
2770
|
}, async ({ fromId, toId, bidirectional, cwd }) => {
|
|
2771
|
+
await ensureBranchSynced(cwd);
|
|
2590
2772
|
const [foundFrom, foundTo] = await Promise.all([
|
|
2591
2773
|
vaultManager.findNote(fromId, cwd),
|
|
2592
2774
|
vaultManager.findNote(toId, cwd),
|
|
@@ -2616,6 +2798,7 @@ server.registerTool("unrelate", {
|
|
|
2616
2798
|
if (vaultChanges.size === 0) {
|
|
2617
2799
|
return { content: [{ type: "text", text: `No relationship found between '${fromId}' and '${toId}'` }], isError: true };
|
|
2618
2800
|
}
|
|
2801
|
+
let retry;
|
|
2619
2802
|
for (const [vault, files] of vaultChanges) {
|
|
2620
2803
|
const found = foundFrom?.vault === vault ? foundFrom : foundTo;
|
|
2621
2804
|
const commitBody = found
|
|
@@ -2625,8 +2808,22 @@ server.registerTool("unrelate", {
|
|
|
2625
2808
|
projectName: found.note.projectName,
|
|
2626
2809
|
})
|
|
2627
2810
|
: undefined;
|
|
2628
|
-
|
|
2629
|
-
await
|
|
2811
|
+
const commitMessage = `unrelate: ${fromId} ↔ ${toId}`;
|
|
2812
|
+
const commitStatus = await vault.git.commitWithStatus(commitMessage, files, commitBody);
|
|
2813
|
+
if (!retry) {
|
|
2814
|
+
retry = buildMutationRetryContract({
|
|
2815
|
+
commit: commitStatus,
|
|
2816
|
+
commitMessage,
|
|
2817
|
+
commitBody,
|
|
2818
|
+
files,
|
|
2819
|
+
cwd,
|
|
2820
|
+
vault,
|
|
2821
|
+
mutationApplied: true,
|
|
2822
|
+
});
|
|
2823
|
+
}
|
|
2824
|
+
if (commitStatus.status === "committed") {
|
|
2825
|
+
await pushAfterMutation(vault);
|
|
2826
|
+
}
|
|
2630
2827
|
}
|
|
2631
2828
|
const modifiedNoteIds = [];
|
|
2632
2829
|
for (const [vault, files] of vaultChanges) {
|
|
@@ -2639,6 +2836,7 @@ server.registerTool("unrelate", {
|
|
|
2639
2836
|
type: "related-to", // not tracked for unrelate
|
|
2640
2837
|
bidirectional,
|
|
2641
2838
|
notesModified: modifiedNoteIds,
|
|
2839
|
+
retry,
|
|
2642
2840
|
};
|
|
2643
2841
|
return { content: [{ type: "text", text: `Removed relationship between \`${fromId}\` and \`${toId}\`` }], structuredContent };
|
|
2644
2842
|
});
|
|
@@ -2707,6 +2905,7 @@ server.registerTool("consolidate", {
|
|
|
2707
2905
|
}),
|
|
2708
2906
|
outputSchema: ConsolidateResultSchema,
|
|
2709
2907
|
}, async ({ cwd, strategy, mode, threshold, mergePlan, allowProtectedBranch = false }) => {
|
|
2908
|
+
await ensureBranchSynced(cwd);
|
|
2710
2909
|
const project = await resolveProject(cwd);
|
|
2711
2910
|
if (!project && cwd) {
|
|
2712
2911
|
return { content: [{ type: "text", text: `Could not detect a project for: ${cwd}` }], isError: true };
|
|
@@ -2751,21 +2950,22 @@ async function detectDuplicates(entries, threshold, project) {
|
|
|
2751
2950
|
const checked = new Set();
|
|
2752
2951
|
let foundCount = 0;
|
|
2753
2952
|
const duplicates = [];
|
|
2953
|
+
const embeddings = await loadEmbeddingsByNoteId(entries);
|
|
2754
2954
|
for (let i = 0; i < entries.length; i++) {
|
|
2755
2955
|
const entryA = entries[i];
|
|
2756
2956
|
if (checked.has(entryA.note.id))
|
|
2757
2957
|
continue;
|
|
2758
|
-
const embeddingA =
|
|
2958
|
+
const embeddingA = embeddings.get(entryA.note.id);
|
|
2759
2959
|
if (!embeddingA)
|
|
2760
2960
|
continue;
|
|
2761
2961
|
for (let j = i + 1; j < entries.length; j++) {
|
|
2762
2962
|
const entryB = entries[j];
|
|
2763
2963
|
if (checked.has(entryB.note.id))
|
|
2764
2964
|
continue;
|
|
2765
|
-
const embeddingB =
|
|
2965
|
+
const embeddingB = embeddings.get(entryB.note.id);
|
|
2766
2966
|
if (!embeddingB)
|
|
2767
2967
|
continue;
|
|
2768
|
-
const similarity = cosineSimilarity(embeddingA
|
|
2968
|
+
const similarity = cosineSimilarity(embeddingA, embeddingB);
|
|
2769
2969
|
if (similarity >= threshold) {
|
|
2770
2970
|
foundCount++;
|
|
2771
2971
|
lines.push(`${foundCount}. ${entryA.note.title} (${entryA.note.id})`);
|
|
@@ -2895,11 +3095,12 @@ async function suggestMerges(entries, threshold, defaultConsolidationMode, proje
|
|
|
2895
3095
|
const checked = new Set();
|
|
2896
3096
|
let suggestionCount = 0;
|
|
2897
3097
|
const suggestions = [];
|
|
3098
|
+
const embeddings = await loadEmbeddingsByNoteId(entries);
|
|
2898
3099
|
for (let i = 0; i < entries.length; i++) {
|
|
2899
3100
|
const entryA = entries[i];
|
|
2900
3101
|
if (checked.has(entryA.note.id))
|
|
2901
3102
|
continue;
|
|
2902
|
-
const embeddingA =
|
|
3103
|
+
const embeddingA = embeddings.get(entryA.note.id);
|
|
2903
3104
|
if (!embeddingA)
|
|
2904
3105
|
continue;
|
|
2905
3106
|
const similar = [];
|
|
@@ -2907,10 +3108,10 @@ async function suggestMerges(entries, threshold, defaultConsolidationMode, proje
|
|
|
2907
3108
|
const entryB = entries[j];
|
|
2908
3109
|
if (checked.has(entryB.note.id))
|
|
2909
3110
|
continue;
|
|
2910
|
-
const embeddingB =
|
|
3111
|
+
const embeddingB = embeddings.get(entryB.note.id);
|
|
2911
3112
|
if (!embeddingB)
|
|
2912
3113
|
continue;
|
|
2913
|
-
const similarity = cosineSimilarity(embeddingA
|
|
3114
|
+
const similarity = cosineSimilarity(embeddingA, embeddingB);
|
|
2914
3115
|
if (similarity >= threshold) {
|
|
2915
3116
|
similar.push({ entry: entryB, similarity });
|
|
2916
3117
|
}
|
|
@@ -2972,6 +3173,16 @@ async function suggestMerges(entries, threshold, defaultConsolidationMode, proje
|
|
|
2972
3173
|
};
|
|
2973
3174
|
return { content: [{ type: "text", text: lines.join("\n") }], structuredContent };
|
|
2974
3175
|
}
|
|
3176
|
+
async function loadEmbeddingsByNoteId(entries) {
|
|
3177
|
+
const embeddings = new Map();
|
|
3178
|
+
await Promise.all(entries.map(async (entry) => {
|
|
3179
|
+
const record = await entry.vault.storage.readEmbedding(entry.note.id);
|
|
3180
|
+
if (record) {
|
|
3181
|
+
embeddings.set(entry.note.id, record.embedding);
|
|
3182
|
+
}
|
|
3183
|
+
}));
|
|
3184
|
+
return embeddings;
|
|
3185
|
+
}
|
|
2975
3186
|
async function executeMerge(entries, mergePlan, defaultConsolidationMode, project, cwd, explicitMode, policy, allowProtectedBranch = false) {
|
|
2976
3187
|
const sourceIds = normalizeMergePlanSourceIds(mergePlan.sourceIds);
|
|
2977
3188
|
const targetTitle = mergePlan.targetTitle.trim();
|
|
@@ -3162,6 +3373,7 @@ async function executeMerge(entries, mergePlan, defaultConsolidationMode, projec
|
|
|
3162
3373
|
let targetPushStatus = { status: "skipped", reason: "no-remote" };
|
|
3163
3374
|
let targetCommitBody;
|
|
3164
3375
|
let targetCommitMessage;
|
|
3376
|
+
let targetCommitFiles;
|
|
3165
3377
|
for (const [vault, files] of vaultChanges) {
|
|
3166
3378
|
const isTargetVault = vault === targetVault;
|
|
3167
3379
|
// Determine action and summary based on mode
|
|
@@ -3199,14 +3411,28 @@ async function executeMerge(entries, mergePlan, defaultConsolidationMode, projec
|
|
|
3199
3411
|
});
|
|
3200
3412
|
const commitMessage = `${action}: ${targetTitle}`;
|
|
3201
3413
|
const commitStatus = await vault.git.commitWithStatus(commitMessage, files, commitBody);
|
|
3202
|
-
const pushStatus =
|
|
3414
|
+
const pushStatus = commitStatus.status === "committed"
|
|
3415
|
+
? await pushAfterMutation(vault)
|
|
3416
|
+
: { status: "skipped", reason: "commit-failed" };
|
|
3203
3417
|
if (isTargetVault) {
|
|
3204
3418
|
targetCommitStatus = commitStatus;
|
|
3205
3419
|
targetPushStatus = pushStatus;
|
|
3206
3420
|
targetCommitBody = commitBody;
|
|
3207
3421
|
targetCommitMessage = commitMessage;
|
|
3422
|
+
targetCommitFiles = [...files];
|
|
3208
3423
|
}
|
|
3209
3424
|
}
|
|
3425
|
+
const retry = targetCommitMessage && targetCommitFiles
|
|
3426
|
+
? buildMutationRetryContract({
|
|
3427
|
+
commit: targetCommitStatus,
|
|
3428
|
+
commitMessage: targetCommitMessage,
|
|
3429
|
+
commitBody: targetCommitBody,
|
|
3430
|
+
files: targetCommitFiles,
|
|
3431
|
+
cwd,
|
|
3432
|
+
vault: targetVault,
|
|
3433
|
+
mutationApplied: true,
|
|
3434
|
+
})
|
|
3435
|
+
: undefined;
|
|
3210
3436
|
const persistence = buildPersistenceStatus({
|
|
3211
3437
|
storage: targetVault.storage,
|
|
3212
3438
|
id: targetId,
|
|
@@ -3215,6 +3441,7 @@ async function executeMerge(entries, mergePlan, defaultConsolidationMode, projec
|
|
|
3215
3441
|
push: targetPushStatus,
|
|
3216
3442
|
commitMessage: targetCommitMessage,
|
|
3217
3443
|
commitBody: targetCommitBody,
|
|
3444
|
+
retry,
|
|
3218
3445
|
});
|
|
3219
3446
|
const lines = [];
|
|
3220
3447
|
lines.push(`Consolidated ${sourceIds.length} notes into '${targetId}'`);
|
|
@@ -3245,6 +3472,7 @@ async function executeMerge(entries, mergePlan, defaultConsolidationMode, projec
|
|
|
3245
3472
|
notesProcessed: entries.length,
|
|
3246
3473
|
notesModified: vaultChanges.size,
|
|
3247
3474
|
persistence,
|
|
3475
|
+
retry,
|
|
3248
3476
|
};
|
|
3249
3477
|
return { content: [{ type: "text", text: lines.join("\n") }], structuredContent };
|
|
3250
3478
|
}
|
|
@@ -3370,14 +3598,29 @@ async function pruneSuperseded(entries, consolidationMode, project, cwd, policy,
|
|
|
3370
3598
|
}
|
|
3371
3599
|
}
|
|
3372
3600
|
// Commit changes per vault
|
|
3601
|
+
let retry;
|
|
3373
3602
|
for (const [vault, files] of vaultChanges) {
|
|
3374
3603
|
const prunedIds = files.map((f) => f.replace(/\.mnemonic\/notes\/(.+)\.md$/, "$1").replace(/notes\/(.+)\.md$/, "$1"));
|
|
3375
3604
|
const commitBody = formatCommitBody({
|
|
3376
3605
|
noteIds: prunedIds,
|
|
3377
3606
|
description: `Pruned ${prunedIds.length} superseded note(s)\nNotes: ${prunedIds.join(", ")}`,
|
|
3378
3607
|
});
|
|
3379
|
-
|
|
3380
|
-
await
|
|
3608
|
+
const commitMessage = `prune: removed ${files.length} superseded note(s)`;
|
|
3609
|
+
const commitStatus = await vault.git.commitWithStatus(commitMessage, files, commitBody);
|
|
3610
|
+
if (!retry) {
|
|
3611
|
+
retry = buildMutationRetryContract({
|
|
3612
|
+
commit: commitStatus,
|
|
3613
|
+
commitMessage,
|
|
3614
|
+
commitBody,
|
|
3615
|
+
files,
|
|
3616
|
+
cwd,
|
|
3617
|
+
vault,
|
|
3618
|
+
mutationApplied: true,
|
|
3619
|
+
});
|
|
3620
|
+
}
|
|
3621
|
+
if (commitStatus.status === "committed") {
|
|
3622
|
+
await pushAfterMutation(vault);
|
|
3623
|
+
}
|
|
3381
3624
|
}
|
|
3382
3625
|
lines.push("");
|
|
3383
3626
|
lines.push(`Pruned ${supersededIds.size} note(s).`);
|
|
@@ -3388,6 +3631,7 @@ async function pruneSuperseded(entries, consolidationMode, project, cwd, policy,
|
|
|
3388
3631
|
projectName: project?.name,
|
|
3389
3632
|
notesProcessed: entries.length,
|
|
3390
3633
|
notesModified: vaultChanges.size,
|
|
3634
|
+
retry,
|
|
3391
3635
|
};
|
|
3392
3636
|
return { content: [{ type: "text", text: lines.join("\n") }], structuredContent };
|
|
3393
3637
|
}
|