@theglitchking/semantic-memory 1.2.2 → 1.5.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/.claude-plugin/marketplace.json +4 -4
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +137 -0
- package/ROADMAP.md +5 -2
- package/commands/healthcheck.md +13 -11
- package/dist/{chunk-UQSASB7Q.js → chunk-2EELGTLX.js} +32 -3
- package/dist/chunk-2EELGTLX.js.map +1 -0
- package/dist/chunk-5ZKNO6ZD.js +96 -0
- package/dist/chunk-5ZKNO6ZD.js.map +1 -0
- package/dist/chunk-7M3JZOYH.js +105 -0
- package/dist/chunk-7M3JZOYH.js.map +1 -0
- package/dist/chunk-A3KYHKB5.js +176 -0
- package/dist/chunk-A3KYHKB5.js.map +1 -0
- package/dist/{chunk-CYWPDFNI.js → chunk-CSXZ7QUU.js} +4 -203
- package/dist/chunk-CSXZ7QUU.js.map +1 -0
- package/dist/chunk-FM26NFTR.js +272 -0
- package/dist/chunk-FM26NFTR.js.map +1 -0
- package/dist/chunk-NTYYS53O.js +91 -0
- package/dist/chunk-NTYYS53O.js.map +1 -0
- package/dist/{chunk-E2Z4J4MK.js → chunk-OWZWMB4P.js} +70 -2
- package/dist/chunk-OWZWMB4P.js.map +1 -0
- package/dist/chunk-S4KW7VH4.js +203 -0
- package/dist/chunk-S4KW7VH4.js.map +1 -0
- package/dist/{chunk-HODDRXJK.js → chunk-V45NJIHI.js} +4 -2
- package/dist/chunk-V45NJIHI.js.map +1 -0
- package/dist/chunk-WSU4QIPB.js +311 -0
- package/dist/chunk-WSU4QIPB.js.map +1 -0
- package/dist/cli/index.js +265 -65
- package/dist/cli/index.js.map +1 -1
- package/dist/core/index.d.ts +39 -2
- package/dist/core/index.js +17 -11
- package/dist/decay-ELJUKVQG.js +14 -0
- package/dist/dossier-2UG724LB.js +39 -0
- package/dist/healthcheck-IMB47LZE.js +19 -0
- package/dist/indexer-YW4O3VVM.js +8 -0
- package/dist/lexicon-5PXQEMUF.js +26 -0
- package/dist/lexicon-5PXQEMUF.js.map +1 -0
- package/dist/lint-XUQ7JDYX.js +16 -0
- package/dist/lint-XUQ7JDYX.js.map +1 -0
- package/dist/mcp/server.js +478 -40
- package/dist/mcp/server.js.map +1 -1
- package/dist/profile-LXUEF7TX.js +20 -0
- package/dist/profile-LXUEF7TX.js.map +1 -0
- package/dist/{schema-F3RTY6SQ.js → schema-ATFCLC6Y.js} +2 -2
- package/dist/schema-ATFCLC6Y.js.map +1 -0
- package/dist/{search-27WYAI24.js → search-AHKEFI2B.js} +2 -2
- package/dist/telemetry-GB2TEHVE.js +21 -0
- package/dist/telemetry-GB2TEHVE.js.map +1 -0
- package/hooks/vault-context.js +270 -11
- package/package.json +2 -1
- package/skills/vault-first/SKILL.md +18 -0
- package/dist/chunk-CAN2GWUG.js +0 -88
- package/dist/chunk-CAN2GWUG.js.map +0 -1
- package/dist/chunk-CYWPDFNI.js.map +0 -1
- package/dist/chunk-E2Z4J4MK.js.map +0 -1
- package/dist/chunk-HODDRXJK.js.map +0 -1
- package/dist/chunk-UQSASB7Q.js.map +0 -1
- package/dist/healthcheck-E4GI5RMZ.js +0 -14
- package/dist/indexer-KD6UTXZB.js +0 -8
- package/dist/lint-GMXQF4RW.js +0 -11
- /package/dist/{healthcheck-E4GI5RMZ.js.map → decay-ELJUKVQG.js.map} +0 -0
- /package/dist/{indexer-KD6UTXZB.js.map → dossier-2UG724LB.js.map} +0 -0
- /package/dist/{lint-GMXQF4RW.js.map → healthcheck-IMB47LZE.js.map} +0 -0
- /package/dist/{schema-F3RTY6SQ.js.map → indexer-YW4O3VVM.js.map} +0 -0
- /package/dist/{search-27WYAI24.js.map → search-AHKEFI2B.js.map} +0 -0
package/dist/mcp/server.js
CHANGED
|
@@ -1,19 +1,53 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SPEAKER_PROFILE_PATH,
|
|
3
|
+
initSpeakerProfile,
|
|
4
|
+
readSpeakerProfile,
|
|
5
|
+
updateProfileSection
|
|
6
|
+
} from "../chunk-7M3JZOYH.js";
|
|
1
7
|
import {
|
|
2
8
|
FrontmatterManager,
|
|
3
9
|
NoteCrud,
|
|
4
|
-
SessionManager,
|
|
5
10
|
TagManager,
|
|
6
11
|
Watcher,
|
|
7
12
|
applyPatch,
|
|
8
13
|
buildIngestChangeSet,
|
|
9
14
|
buildPromoteChangeSet,
|
|
10
15
|
buildSynthesizeChangeSet,
|
|
11
|
-
deriveSessionDir,
|
|
12
16
|
inspectAgentsContract,
|
|
13
17
|
regenDirectoryIndex,
|
|
14
18
|
regenIndexesForPaths,
|
|
15
19
|
regenerateAgentsContract
|
|
16
|
-
} from "../chunk-
|
|
20
|
+
} from "../chunk-CSXZ7QUU.js";
|
|
21
|
+
import {
|
|
22
|
+
lintVault
|
|
23
|
+
} from "../chunk-FM26NFTR.js";
|
|
24
|
+
import {
|
|
25
|
+
appendEvent,
|
|
26
|
+
readSelectionLog,
|
|
27
|
+
recordSearchForCorrelation,
|
|
28
|
+
wasRecentlySearched
|
|
29
|
+
} from "../chunk-NTYYS53O.js";
|
|
30
|
+
import {
|
|
31
|
+
addAlias,
|
|
32
|
+
compileLexicon,
|
|
33
|
+
expandQuery,
|
|
34
|
+
loadLexiconCache,
|
|
35
|
+
removeAlias
|
|
36
|
+
} from "../chunk-A3KYHKB5.js";
|
|
37
|
+
import {
|
|
38
|
+
appendIncident,
|
|
39
|
+
compileDossiers,
|
|
40
|
+
extractSection,
|
|
41
|
+
initDossier,
|
|
42
|
+
listDossiers,
|
|
43
|
+
resolveDossierPath,
|
|
44
|
+
setCurrentState
|
|
45
|
+
} from "../chunk-WSU4QIPB.js";
|
|
46
|
+
import {
|
|
47
|
+
SessionManager,
|
|
48
|
+
deriveProjectRoot,
|
|
49
|
+
deriveSessionDir
|
|
50
|
+
} from "../chunk-S4KW7VH4.js";
|
|
17
51
|
import {
|
|
18
52
|
Embedder,
|
|
19
53
|
GraphBuilder,
|
|
@@ -22,17 +56,19 @@ import {
|
|
|
22
56
|
} from "../chunk-25EIQWXL.js";
|
|
23
57
|
import {
|
|
24
58
|
Indexer
|
|
25
|
-
} from "../chunk-
|
|
59
|
+
} from "../chunk-V45NJIHI.js";
|
|
26
60
|
import {
|
|
27
61
|
logEvent,
|
|
28
62
|
logQuery
|
|
29
63
|
} from "../chunk-JPQHNG3R.js";
|
|
30
|
-
import {
|
|
31
|
-
lintVault
|
|
32
|
-
} from "../chunk-CAN2GWUG.js";
|
|
33
64
|
import {
|
|
34
65
|
installDefaultSchema
|
|
35
|
-
} from "../chunk-
|
|
66
|
+
} from "../chunk-OWZWMB4P.js";
|
|
67
|
+
import {
|
|
68
|
+
computeDecay,
|
|
69
|
+
loadDecayConfig,
|
|
70
|
+
normalizeVerifiedDate
|
|
71
|
+
} from "../chunk-5ZKNO6ZD.js";
|
|
36
72
|
import {
|
|
37
73
|
__export
|
|
38
74
|
} from "../chunk-PZ5AY32C.js";
|
|
@@ -43,10 +79,83 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
43
79
|
|
|
44
80
|
// src/mcp/context.ts
|
|
45
81
|
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
82
|
+
import { join as join3 } from "path";
|
|
83
|
+
import { existsSync as existsSync3 } from "fs";
|
|
84
|
+
|
|
85
|
+
// src/core/path-class.ts
|
|
86
|
+
import { existsSync, readFileSync } from "fs";
|
|
46
87
|
import { join } from "path";
|
|
47
|
-
import {
|
|
88
|
+
import { parse as parseYaml } from "yaml";
|
|
89
|
+
import { minimatch } from "minimatch";
|
|
90
|
+
var DEFAULT_PATH_CLASS_CONFIG = {
|
|
91
|
+
enabled: true,
|
|
92
|
+
rules: [{ glob: "archive/**", multiplier: 0.3 }]
|
|
93
|
+
};
|
|
94
|
+
function pathClassMultiplier(path, config) {
|
|
95
|
+
if (!config.enabled) return 1;
|
|
96
|
+
for (const rule of config.rules) {
|
|
97
|
+
if (typeof rule.multiplier !== "number") continue;
|
|
98
|
+
if (minimatch(path, rule.glob)) return rule.multiplier;
|
|
99
|
+
}
|
|
100
|
+
return 1;
|
|
101
|
+
}
|
|
102
|
+
function loadPathClassConfig(vaultPath) {
|
|
103
|
+
try {
|
|
104
|
+
const schemaPath = join(vaultPath, "vault.schema.yml");
|
|
105
|
+
if (!existsSync(schemaPath)) return DEFAULT_PATH_CLASS_CONFIG;
|
|
106
|
+
const parsed = parseYaml(readFileSync(schemaPath, "utf-8"));
|
|
107
|
+
const p = parsed?.path_class;
|
|
108
|
+
if (!p || typeof p !== "object") return DEFAULT_PATH_CLASS_CONFIG;
|
|
109
|
+
return {
|
|
110
|
+
enabled: typeof p.enabled === "boolean" ? p.enabled : DEFAULT_PATH_CLASS_CONFIG.enabled,
|
|
111
|
+
rules: Array.isArray(p.rules) ? p.rules.filter((r) => !!r && typeof r.glob === "string" && typeof r.multiplier === "number") : DEFAULT_PATH_CLASS_CONFIG.rules
|
|
112
|
+
};
|
|
113
|
+
} catch {
|
|
114
|
+
return DEFAULT_PATH_CLASS_CONFIG;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// src/core/usage-boost.ts
|
|
119
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
120
|
+
import { join as join2 } from "path";
|
|
121
|
+
import { parse as parseYaml2 } from "yaml";
|
|
122
|
+
var DEFAULT_USAGE_BOOST_CONFIG = {
|
|
123
|
+
enabled: true,
|
|
124
|
+
cap: 1.5,
|
|
125
|
+
per_citation: 0.1
|
|
126
|
+
};
|
|
127
|
+
function loadUsageBoostConfig(vaultPath) {
|
|
128
|
+
try {
|
|
129
|
+
const schemaPath = join2(vaultPath, "vault.schema.yml");
|
|
130
|
+
if (!existsSync2(schemaPath)) return { ...DEFAULT_USAGE_BOOST_CONFIG };
|
|
131
|
+
const parsed = parseYaml2(readFileSync2(schemaPath, "utf-8"));
|
|
132
|
+
const u = parsed?.usage_boost;
|
|
133
|
+
if (!u || typeof u !== "object") return { ...DEFAULT_USAGE_BOOST_CONFIG };
|
|
134
|
+
return {
|
|
135
|
+
enabled: typeof u.enabled === "boolean" ? u.enabled : DEFAULT_USAGE_BOOST_CONFIG.enabled,
|
|
136
|
+
cap: typeof u.cap === "number" && u.cap >= 1 ? u.cap : DEFAULT_USAGE_BOOST_CONFIG.cap,
|
|
137
|
+
per_citation: typeof u.per_citation === "number" && u.per_citation >= 0 ? u.per_citation : DEFAULT_USAGE_BOOST_CONFIG.per_citation
|
|
138
|
+
};
|
|
139
|
+
} catch {
|
|
140
|
+
return { ...DEFAULT_USAGE_BOOST_CONFIG };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function computeUsageBoost(citations, config) {
|
|
144
|
+
if (!config.enabled || citations <= 0) return 1;
|
|
145
|
+
return Math.min(config.cap, 1 + citations * config.per_citation);
|
|
146
|
+
}
|
|
147
|
+
async function loadCitationCounts(vaultPath) {
|
|
148
|
+
const events = await readSelectionLog(vaultPath);
|
|
149
|
+
const counts = {};
|
|
150
|
+
for (const e of events) {
|
|
151
|
+
if (e.kind === "selection") counts[e.note_path] = (counts[e.note_path] ?? 0) + 1;
|
|
152
|
+
}
|
|
153
|
+
return counts;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// src/mcp/context.ts
|
|
48
157
|
async function buildContext(notesPath, options = {}) {
|
|
49
|
-
const indexPath =
|
|
158
|
+
const indexPath = join3(notesPath, ".semantic-sidekick-index");
|
|
50
159
|
await mkdir(indexPath, { recursive: true });
|
|
51
160
|
const indexer = new Indexer(notesPath);
|
|
52
161
|
const embedder = new Embedder(options.model, options.workers, options.batchSize, options.quantized);
|
|
@@ -66,9 +175,9 @@ async function buildContext(notesPath, options = {}) {
|
|
|
66
175
|
try {
|
|
67
176
|
indexState = "loading";
|
|
68
177
|
await embedder.init();
|
|
69
|
-
const metaPath =
|
|
178
|
+
const metaPath = join3(indexPath, "meta.json");
|
|
70
179
|
let meta = null;
|
|
71
|
-
if (
|
|
180
|
+
if (existsSync3(metaPath)) {
|
|
72
181
|
try {
|
|
73
182
|
meta = JSON.parse(await readFile(metaPath, "utf-8"));
|
|
74
183
|
if (meta?.model && meta.model !== embedder.getModel()) {
|
|
@@ -168,7 +277,7 @@ async function buildContext(notesPath, options = {}) {
|
|
|
168
277
|
newVector.save(indexPath),
|
|
169
278
|
embedder.saveEmbeddings(cumulativeEmbeddings, indexPath),
|
|
170
279
|
indexer.saveDocsCache(indexPath, newDocs),
|
|
171
|
-
writeFile(
|
|
280
|
+
writeFile(join3(indexPath, "meta.json"), JSON.stringify({
|
|
172
281
|
model: embedder.getModel(),
|
|
173
282
|
dimensions: embedder.getDimensions(),
|
|
174
283
|
totalChunks: allChunks.length,
|
|
@@ -212,6 +321,72 @@ async function buildContext(notesPath, options = {}) {
|
|
|
212
321
|
if (doc?.loadPriority === void 0) return score;
|
|
213
322
|
return score * (1 + (doc.loadPriority - 5) * 0.04);
|
|
214
323
|
}
|
|
324
|
+
let decayConfig = null;
|
|
325
|
+
function getDecayConfig() {
|
|
326
|
+
if (!decayConfig) decayConfig = loadDecayConfig(notesPath);
|
|
327
|
+
return decayConfig;
|
|
328
|
+
}
|
|
329
|
+
function applyDecay(r) {
|
|
330
|
+
const cfg = getDecayConfig();
|
|
331
|
+
if (!cfg.enabled) return r;
|
|
332
|
+
const doc = docByPath.get(r.path);
|
|
333
|
+
if (!doc) return r;
|
|
334
|
+
const fm = doc.frontmatter ?? {};
|
|
335
|
+
const d = computeDecay({
|
|
336
|
+
type: typeof fm.type === "string" ? fm.type : void 0,
|
|
337
|
+
last_verified: normalizeVerifiedDate(fm.last_verified),
|
|
338
|
+
evergreen: fm.evergreen === true,
|
|
339
|
+
config: cfg
|
|
340
|
+
});
|
|
341
|
+
const scored = { ...r, score: r.score * d.multiplier };
|
|
342
|
+
if (d.multiplier < 1) {
|
|
343
|
+
return {
|
|
344
|
+
...scored,
|
|
345
|
+
decay: {
|
|
346
|
+
multiplier: Math.round(d.multiplier * 1e3) / 1e3,
|
|
347
|
+
age_days: Math.round(d.age_days),
|
|
348
|
+
effective_half_life: d.effective_half_life,
|
|
349
|
+
reason: d.reason
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
return scored;
|
|
354
|
+
}
|
|
355
|
+
let pathClassConfig = null;
|
|
356
|
+
function getPathClassConfig() {
|
|
357
|
+
if (!pathClassConfig) pathClassConfig = loadPathClassConfig(notesPath);
|
|
358
|
+
return pathClassConfig;
|
|
359
|
+
}
|
|
360
|
+
function applyPathClass(r) {
|
|
361
|
+
const m = pathClassMultiplier(r.path, getPathClassConfig());
|
|
362
|
+
return m === 1 ? r : { ...r, score: r.score * m };
|
|
363
|
+
}
|
|
364
|
+
let usageBoostConfig = null;
|
|
365
|
+
function getUsageBoostConfig() {
|
|
366
|
+
if (!usageBoostConfig) usageBoostConfig = loadUsageBoostConfig(notesPath);
|
|
367
|
+
return usageBoostConfig;
|
|
368
|
+
}
|
|
369
|
+
let citationCounts = {};
|
|
370
|
+
let citationLoadedAtMs = 0;
|
|
371
|
+
const CITATION_TTL_MS = 15e3;
|
|
372
|
+
async function refreshUsageBoost() {
|
|
373
|
+
if (!getUsageBoostConfig().enabled) return;
|
|
374
|
+
const now = Date.now();
|
|
375
|
+
if (now - citationLoadedAtMs < CITATION_TTL_MS && citationLoadedAtMs > 0) return;
|
|
376
|
+
try {
|
|
377
|
+
citationCounts = await loadCitationCounts(notesPath);
|
|
378
|
+
citationLoadedAtMs = now;
|
|
379
|
+
} catch {
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
function applyUsageBoost(r) {
|
|
383
|
+
const cfg = getUsageBoostConfig();
|
|
384
|
+
if (!cfg.enabled) return r;
|
|
385
|
+
const citations = citationCounts[r.path] ?? 0;
|
|
386
|
+
const m = computeUsageBoost(citations, cfg);
|
|
387
|
+
if (m === 1) return r;
|
|
388
|
+
return { ...r, score: r.score * m, usage: { citations, multiplier: Math.round(m * 1e3) / 1e3 } };
|
|
389
|
+
}
|
|
215
390
|
function applyDateFilter(results, modifiedAfter, modifiedBefore) {
|
|
216
391
|
if (!modifiedAfter && !modifiedBefore) return results;
|
|
217
392
|
const after = modifiedAfter ? new Date(modifiedAfter).getTime() : -Infinity;
|
|
@@ -248,6 +423,10 @@ async function buildContext(notesPath, options = {}) {
|
|
|
248
423
|
indexingMessage,
|
|
249
424
|
enrichResult,
|
|
250
425
|
applyPriorityBoost,
|
|
426
|
+
applyDecay,
|
|
427
|
+
applyPathClass,
|
|
428
|
+
applyUsageBoost,
|
|
429
|
+
refreshUsageBoost,
|
|
251
430
|
applyDateFilter
|
|
252
431
|
};
|
|
253
432
|
}
|
|
@@ -4294,10 +4473,19 @@ var coerce = {
|
|
|
4294
4473
|
var NEVER = INVALID;
|
|
4295
4474
|
|
|
4296
4475
|
// src/mcp/tools/search.ts
|
|
4476
|
+
async function logSearch(ctx, tool, query, enriched) {
|
|
4477
|
+
const results = enriched.map((r) => ({
|
|
4478
|
+
path: r.path,
|
|
4479
|
+
score: r.score,
|
|
4480
|
+
...r.decay ? { decay: r.decay.multiplier } : {}
|
|
4481
|
+
}));
|
|
4482
|
+
recordSearchForCorrelation(ctx.notesPath, results.map((r) => r.path));
|
|
4483
|
+
await appendEvent(ctx.notesPath, { kind: "search", tool, query, results });
|
|
4484
|
+
}
|
|
4297
4485
|
function registerSearchTools(server, ctx) {
|
|
4298
4486
|
server.tool(
|
|
4299
4487
|
"search_semantic",
|
|
4300
|
-
"Vector similarity search \u2014 find notes similar to a query by meaning. Scores are boosted by load_priority
|
|
4488
|
+
"Vector similarity search \u2014 find notes similar to a query by meaning. Scores are boosted by load_priority and down-weighted by confidence decay (time since last_verified); decayed results carry a `decay` block. Set decay.enabled:false in vault.schema.yml to disable.",
|
|
4301
4489
|
{
|
|
4302
4490
|
query: external_exports.string(),
|
|
4303
4491
|
limit: external_exports.coerce.number().optional().default(10),
|
|
@@ -4315,8 +4503,9 @@ function registerSearchTools(server, ctx) {
|
|
|
4315
4503
|
);
|
|
4316
4504
|
}
|
|
4317
4505
|
const queryEmbed = await ctx.embedder.embed(query);
|
|
4506
|
+
await ctx.refreshUsageBoost();
|
|
4318
4507
|
let results = vectorIndex.search(queryEmbed, limit * 3);
|
|
4319
|
-
results = results.map((r) => ({ ...r, score: ctx.applyPriorityBoost(r.score, r.path) }));
|
|
4508
|
+
results = results.map((r) => ctx.applyUsageBoost(ctx.applyPathClass(ctx.applyDecay({ ...r, score: ctx.applyPriorityBoost(r.score, r.path) }))));
|
|
4320
4509
|
results.sort((a, b) => b.score - a.score);
|
|
4321
4510
|
results = ctx.applyDateFilter(results, modifiedAfter, modifiedBefore);
|
|
4322
4511
|
const docByPath = ctx.getDocByPath();
|
|
@@ -4324,6 +4513,7 @@ function registerSearchTools(server, ctx) {
|
|
|
4324
4513
|
if (tier) results = results.filter((r) => docByPath.get(r.path)?.tier === tier);
|
|
4325
4514
|
if (domain) results = results.filter((r) => docByPath.get(r.path)?.domains?.includes(domain));
|
|
4326
4515
|
const enriched = results.slice(0, limit).map((r) => ctx.enrichResult(r));
|
|
4516
|
+
await logSearch(ctx, "search_semantic", query, enriched);
|
|
4327
4517
|
return ctx.textResponse(JSON.stringify(enriched, null, 2));
|
|
4328
4518
|
}
|
|
4329
4519
|
);
|
|
@@ -4373,7 +4563,7 @@ function registerSearchTools(server, ctx) {
|
|
|
4373
4563
|
);
|
|
4374
4564
|
server.tool(
|
|
4375
4565
|
"search_hybrid",
|
|
4376
|
-
"Combined semantic + graph search \u2014 vector results re-ranked by graph proximity and load_priority",
|
|
4566
|
+
"Combined semantic + graph search \u2014 vector results re-ranked by graph proximity and load_priority, and down-weighted by confidence decay (time since last_verified); decayed results carry a `decay` block.",
|
|
4377
4567
|
{
|
|
4378
4568
|
query: external_exports.string(),
|
|
4379
4569
|
limit: external_exports.coerce.number().optional().default(10),
|
|
@@ -4391,16 +4581,20 @@ function registerSearchTools(server, ctx) {
|
|
|
4391
4581
|
);
|
|
4392
4582
|
}
|
|
4393
4583
|
const queryEmbed = await ctx.embedder.embed(query);
|
|
4584
|
+
await ctx.refreshUsageBoost();
|
|
4394
4585
|
const semanticResults = vectorIndex.search(queryEmbed, limit * 3);
|
|
4395
4586
|
const graphResults = ctx.graph.searchGraph(query, 2);
|
|
4396
4587
|
const graphPaths = new Set(graphResults.map((r) => r.path));
|
|
4397
|
-
let hybrid = semanticResults.map(
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4588
|
+
let hybrid = semanticResults.map(
|
|
4589
|
+
(r) => ctx.applyUsageBoost(
|
|
4590
|
+
ctx.applyPathClass(
|
|
4591
|
+
ctx.applyDecay({
|
|
4592
|
+
...r,
|
|
4593
|
+
score: ctx.applyPriorityBoost(graphPaths.has(r.path) ? r.score * 1.3 : r.score, r.path)
|
|
4594
|
+
})
|
|
4595
|
+
)
|
|
4402
4596
|
)
|
|
4403
|
-
|
|
4597
|
+
);
|
|
4404
4598
|
hybrid.sort((a, b) => b.score - a.score);
|
|
4405
4599
|
hybrid = ctx.applyDateFilter(hybrid, modifiedAfter, modifiedBefore);
|
|
4406
4600
|
const docByPath = ctx.getDocByPath();
|
|
@@ -4408,6 +4602,7 @@ function registerSearchTools(server, ctx) {
|
|
|
4408
4602
|
if (tier) hybrid = hybrid.filter((r) => docByPath.get(r.path)?.tier === tier);
|
|
4409
4603
|
if (domain) hybrid = hybrid.filter((r) => docByPath.get(r.path)?.domains?.includes(domain));
|
|
4410
4604
|
const enriched = hybrid.slice(0, limit).map((r) => ctx.enrichResult(r));
|
|
4605
|
+
await logSearch(ctx, "search_hybrid", query, enriched);
|
|
4411
4606
|
return ctx.textResponse(JSON.stringify(enriched, null, 2));
|
|
4412
4607
|
}
|
|
4413
4608
|
);
|
|
@@ -4417,10 +4612,20 @@ function registerSearchTools(server, ctx) {
|
|
|
4417
4612
|
function registerReadTools(server, ctx) {
|
|
4418
4613
|
server.tool(
|
|
4419
4614
|
"read_note",
|
|
4420
|
-
"Read
|
|
4421
|
-
{ path: external_exports.string() },
|
|
4422
|
-
async ({ path }) => {
|
|
4615
|
+
"Read a note by path. Pass `section` (a heading name) to read only that heading's section instead of the whole file \u2014 cheaper when you only need one part (e.g. a dossier's 'Knobs' section).",
|
|
4616
|
+
{ path: external_exports.string(), section: external_exports.string().optional().describe("Heading name \u2014 return only that section (heading through the next same-or-higher heading).") },
|
|
4617
|
+
async ({ path, section }) => {
|
|
4423
4618
|
const content = await ctx.crud.read(path);
|
|
4619
|
+
await appendEvent(ctx.notesPath, {
|
|
4620
|
+
kind: "selection",
|
|
4621
|
+
note_path: path,
|
|
4622
|
+
via: "read_note",
|
|
4623
|
+
correlated: wasRecentlySearched(ctx.notesPath, path)
|
|
4624
|
+
});
|
|
4625
|
+
if (section) {
|
|
4626
|
+
const extracted = extractSection(content, section);
|
|
4627
|
+
return ctx.textResponse(extracted ?? `Section "${section}" not found in ${path}. Read without \`section\` for the full note.`);
|
|
4628
|
+
}
|
|
4424
4629
|
return ctx.textResponse(content);
|
|
4425
4630
|
}
|
|
4426
4631
|
);
|
|
@@ -4528,7 +4733,7 @@ function registerWriteTools(server, ctx) {
|
|
|
4528
4733
|
|
|
4529
4734
|
// src/mcp/tools/patch.ts
|
|
4530
4735
|
import { readFile as readFile2 } from "fs/promises";
|
|
4531
|
-
import { join as
|
|
4736
|
+
import { join as join4 } from "path";
|
|
4532
4737
|
import matter from "gray-matter";
|
|
4533
4738
|
function registerPatchTools(server, ctx) {
|
|
4534
4739
|
if (ctx.options.readOnly) return;
|
|
@@ -4601,6 +4806,7 @@ function registerPatchTools(server, ctx) {
|
|
|
4601
4806
|
decision_maker: external_exports.string().optional(),
|
|
4602
4807
|
decided_on: external_exports.string().optional(),
|
|
4603
4808
|
severity: external_exports.string().optional(),
|
|
4809
|
+
symptoms: external_exports.array(external_exports.string()).optional().describe("Verbatim symptom phrases in the user's own words (e.g. 'it hangs after the second reindex'). Stored as `symptoms:` frontmatter and symptom-keyed at index time so terse symptom queries match this note \u2014 the retrieval key for next time."),
|
|
4604
4810
|
dry_run: external_exports.boolean().optional().default(false),
|
|
4605
4811
|
proposal: external_exports.boolean().optional().default(false).describe("Write to proposals/<date>-<slug>.md with status:proposal instead of the canonical destination. Use for review-first workflows."),
|
|
4606
4812
|
proposal_subdir: external_exports.string().optional().describe("Override the proposal subdirectory (default 'proposals'). Useful for source segregation, e.g. 'proposals/research'."),
|
|
@@ -4644,6 +4850,7 @@ ${verifLines.join("\n")}
|
|
|
4644
4850
|
decision_maker: args.decision_maker,
|
|
4645
4851
|
decided_on: args.decided_on,
|
|
4646
4852
|
severity: args.severity,
|
|
4853
|
+
symptoms: args.symptoms,
|
|
4647
4854
|
proposal: args.proposal,
|
|
4648
4855
|
proposal_subdir: args.proposal_subdir
|
|
4649
4856
|
});
|
|
@@ -4698,7 +4905,7 @@ ${verifLines.join("\n")}
|
|
|
4698
4905
|
dry_run: external_exports.boolean().optional().default(false)
|
|
4699
4906
|
},
|
|
4700
4907
|
async (args) => {
|
|
4701
|
-
const absoluteProposalPath =
|
|
4908
|
+
const absoluteProposalPath = join4(ctx.notesPath, args.proposal_path);
|
|
4702
4909
|
let raw;
|
|
4703
4910
|
try {
|
|
4704
4911
|
raw = await readFile2(absoluteProposalPath, "utf-8");
|
|
@@ -4825,6 +5032,160 @@ ${verifLines.join("\n")}
|
|
|
4825
5032
|
);
|
|
4826
5033
|
}
|
|
4827
5034
|
);
|
|
5035
|
+
server.tool(
|
|
5036
|
+
"verify_note",
|
|
5037
|
+
"Re-verify a note: stamp `last_verified` to today WITHOUT changing content or any other frontmatter field, and log a `verify` event. Resets the confidence-decay clock so the note ranks fresh again. Returns the note's new decay multiplier (typically 1.0).",
|
|
5038
|
+
{ path: external_exports.string() },
|
|
5039
|
+
async ({ path }) => {
|
|
5040
|
+
let fm;
|
|
5041
|
+
try {
|
|
5042
|
+
fm = await ctx.frontmatterManager.get(path);
|
|
5043
|
+
} catch {
|
|
5044
|
+
return ctx.textResponse(`verify_note: note not found: ${path}`);
|
|
5045
|
+
}
|
|
5046
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
5047
|
+
await ctx.frontmatterManager.update(path, { last_verified: today });
|
|
5048
|
+
await logEvent(ctx.notesPath, {
|
|
5049
|
+
kind: "verify",
|
|
5050
|
+
summary: `verified ${path}`,
|
|
5051
|
+
payload: { path, last_verified: today }
|
|
5052
|
+
});
|
|
5053
|
+
const d = computeDecay({
|
|
5054
|
+
type: typeof fm.type === "string" ? fm.type : void 0,
|
|
5055
|
+
last_verified: today,
|
|
5056
|
+
evergreen: fm.evergreen === true,
|
|
5057
|
+
config: loadDecayConfig(ctx.notesPath)
|
|
5058
|
+
});
|
|
5059
|
+
return ctx.textResponse(
|
|
5060
|
+
JSON.stringify({ path, last_verified: today, decay_multiplier: d.multiplier }, null, 2)
|
|
5061
|
+
);
|
|
5062
|
+
}
|
|
5063
|
+
);
|
|
5064
|
+
server.tool(
|
|
5065
|
+
"manage_lexicon",
|
|
5066
|
+
"Manage the learned human\u2192artifact lexicon (v1.4). `action`: add (upsert a canonical target + the human phrases that refer to it; bumps evidence_count on repeats), lookup (expand a query via matching alias phrases), list (all aliases), remove (by canonical), compile (rebuild the derived cache). The lexicon maps how THIS human names things to concrete paths/symbols.",
|
|
5067
|
+
{
|
|
5068
|
+
action: external_exports.enum(["add", "lookup", "list", "remove", "compile"]),
|
|
5069
|
+
canonical: external_exports.string().optional().describe("For add/remove: the concrete target (note path or code symbol)."),
|
|
5070
|
+
phrases: external_exports.array(external_exports.string()).optional().describe("For add: the human's verbatim phrases for the canonical target."),
|
|
5071
|
+
query: external_exports.string().optional().describe("For lookup: the utterance to expand via alias phrases."),
|
|
5072
|
+
source: external_exports.enum(["learned", "authored"]).optional().describe("For add: provenance of the binding (default learned).")
|
|
5073
|
+
},
|
|
5074
|
+
async ({ action, canonical, phrases, query, source }) => {
|
|
5075
|
+
switch (action) {
|
|
5076
|
+
case "add": {
|
|
5077
|
+
if (!canonical || !phrases || phrases.length === 0) return ctx.textResponse("manage_lexicon add requires `canonical` and non-empty `phrases`");
|
|
5078
|
+
const r = await addAlias(ctx.notesPath, { canonical, phrases, source });
|
|
5079
|
+
return ctx.textResponse(JSON.stringify(r, null, 2));
|
|
5080
|
+
}
|
|
5081
|
+
case "lookup": {
|
|
5082
|
+
if (!query) return ctx.textResponse("manage_lexicon lookup requires `query`");
|
|
5083
|
+
const aliases = await loadLexiconCache(ctx.notesPath);
|
|
5084
|
+
const { expanded, matched } = expandQuery(aliases, query);
|
|
5085
|
+
return ctx.textResponse(JSON.stringify({ query, expanded, matched }, null, 2));
|
|
5086
|
+
}
|
|
5087
|
+
case "list": {
|
|
5088
|
+
return ctx.textResponse(JSON.stringify(await loadLexiconCache(ctx.notesPath), null, 2));
|
|
5089
|
+
}
|
|
5090
|
+
case "remove": {
|
|
5091
|
+
if (!canonical) return ctx.textResponse("manage_lexicon remove requires `canonical`");
|
|
5092
|
+
const removed = await removeAlias(ctx.notesPath, canonical);
|
|
5093
|
+
return ctx.textResponse(removed ? `removed alias: ${canonical}` : `no alias for canonical: ${canonical}`);
|
|
5094
|
+
}
|
|
5095
|
+
case "compile": {
|
|
5096
|
+
const entries = await compileLexicon(ctx.notesPath);
|
|
5097
|
+
return ctx.textResponse(`compiled ${entries.length} alias(es) to lexicon-cache.json`);
|
|
5098
|
+
}
|
|
5099
|
+
}
|
|
5100
|
+
}
|
|
5101
|
+
);
|
|
5102
|
+
server.tool(
|
|
5103
|
+
"manage_dossier",
|
|
5104
|
+
"Manage entity dossiers (v1.5) \u2014 the per-component living notes that organize memory by entity instead of by file. `action`: init (scaffold a dossier for an entity with the fixed Purpose/Failure modes/Knobs/Incident log/Current state sections), append_incident (append a dated line to the Incident log \u2014 accretion, never a new note), set_state (replace the Current state one-liner), get (read a dossier by entity/alias/path), list (all dossiers with their current state). Knowledge about a critical component accretes IN PLACE here.",
|
|
5105
|
+
{
|
|
5106
|
+
action: external_exports.enum(["init", "append_incident", "set_state", "get", "list"]),
|
|
5107
|
+
entity: external_exports.string().optional().describe("For init/append_incident/set_state/get: the entity name, one of its aliases, or a dossier path."),
|
|
5108
|
+
aliases: external_exports.array(external_exports.string()).optional().describe("For init: the human's phrases for this entity (fed to the lexicon compiler for query expansion)."),
|
|
5109
|
+
purpose: external_exports.string().optional().describe("For init: seed the Purpose section instead of the placeholder."),
|
|
5110
|
+
seeded_from: external_exports.string().optional().describe("For init: provenance of the scaffold (e.g. a babel-fish project-map entry)."),
|
|
5111
|
+
incident: external_exports.string().optional().describe("For append_incident: what happened \u2192 cause \u2192 fix, in one line. Dated automatically."),
|
|
5112
|
+
state: external_exports.string().optional().describe("For set_state: the single freshest sentence about where the entity stands.")
|
|
5113
|
+
},
|
|
5114
|
+
async ({ action, entity, aliases, purpose, seeded_from, incident, state }) => {
|
|
5115
|
+
switch (action) {
|
|
5116
|
+
case "init": {
|
|
5117
|
+
if (!entity) return ctx.textResponse("manage_dossier init requires `entity`");
|
|
5118
|
+
const r = await initDossier(ctx.notesPath, entity, { aliases, purpose, seeded_from });
|
|
5119
|
+
if (r.created) await ctx.sessions.recordNotesTouched([r.path]).catch(() => {
|
|
5120
|
+
});
|
|
5121
|
+
return ctx.textResponse(JSON.stringify(r, null, 2));
|
|
5122
|
+
}
|
|
5123
|
+
case "append_incident": {
|
|
5124
|
+
if (!entity || !incident) return ctx.textResponse("manage_dossier append_incident requires `entity` and `incident`");
|
|
5125
|
+
const r = await appendIncident(ctx.notesPath, entity, incident);
|
|
5126
|
+
if (!r) return ctx.textResponse(`no dossier for entity: ${entity} (run action:init first)`);
|
|
5127
|
+
await ctx.sessions.recordNotesTouched([r.path]).catch(() => {
|
|
5128
|
+
});
|
|
5129
|
+
await logEvent(ctx.notesPath, { kind: "synthesis", summary: `dossier incident: ${entity}`, payload: { path: r.path, incident } }).catch(() => {
|
|
5130
|
+
});
|
|
5131
|
+
return ctx.textResponse(JSON.stringify(r, null, 2));
|
|
5132
|
+
}
|
|
5133
|
+
case "set_state": {
|
|
5134
|
+
if (!entity || !state) return ctx.textResponse("manage_dossier set_state requires `entity` and `state`");
|
|
5135
|
+
const r = await setCurrentState(ctx.notesPath, entity, state);
|
|
5136
|
+
if (!r) return ctx.textResponse(`no dossier for entity: ${entity} (run action:init first)`);
|
|
5137
|
+
await ctx.sessions.recordNotesTouched([r.path]).catch(() => {
|
|
5138
|
+
});
|
|
5139
|
+
return ctx.textResponse(JSON.stringify(r, null, 2));
|
|
5140
|
+
}
|
|
5141
|
+
case "get": {
|
|
5142
|
+
if (!entity) return ctx.textResponse("manage_dossier get requires `entity`");
|
|
5143
|
+
const rel = await resolveDossierPath(ctx.notesPath, entity);
|
|
5144
|
+
if (!rel) return ctx.textResponse(`no dossier for entity: ${entity}`);
|
|
5145
|
+
const content = await readFile2(join4(ctx.notesPath, rel), "utf-8");
|
|
5146
|
+
return ctx.textResponse(JSON.stringify({ path: rel, content }, null, 2));
|
|
5147
|
+
}
|
|
5148
|
+
case "list": {
|
|
5149
|
+
await compileDossiers(ctx.notesPath);
|
|
5150
|
+
const entries = await listDossiers(ctx.notesPath);
|
|
5151
|
+
return ctx.textResponse(JSON.stringify(entries, null, 2));
|
|
5152
|
+
}
|
|
5153
|
+
}
|
|
5154
|
+
}
|
|
5155
|
+
);
|
|
5156
|
+
server.tool(
|
|
5157
|
+
"manage_profile",
|
|
5158
|
+
'Manage the speaker profile (v1.5) \u2014 the model of how THIS human communicates (severity calibration, chronic omissions, verbosity preference, non-entity shorthand). `action`: init (scaffold profile/speaker.md with the fixed sections), get (read it), update_section (append a learned line to \u2014 or replace \u2014 one section; scaffolds first if absent). Update it when the user corrects an interpretation ("no, when I say X I mean Y"). One human, one profile.',
|
|
5159
|
+
{
|
|
5160
|
+
action: external_exports.enum(["init", "get", "update_section"]),
|
|
5161
|
+
section: external_exports.string().optional().describe("For update_section: the section heading (e.g. 'Severity calibration', 'Shorthand & terms')."),
|
|
5162
|
+
text: external_exports.string().optional().describe("For update_section: the learned line or replacement body."),
|
|
5163
|
+
mode: external_exports.enum(["append", "replace"]).optional().describe("For update_section: append a line (default) or replace the section body.")
|
|
5164
|
+
},
|
|
5165
|
+
async ({ action, section, text, mode }) => {
|
|
5166
|
+
switch (action) {
|
|
5167
|
+
case "init": {
|
|
5168
|
+
const r = await initSpeakerProfile(ctx.notesPath);
|
|
5169
|
+
if (r.created) await ctx.sessions.recordNotesTouched([r.path]).catch(() => {
|
|
5170
|
+
});
|
|
5171
|
+
return ctx.textResponse(JSON.stringify(r, null, 2));
|
|
5172
|
+
}
|
|
5173
|
+
case "get": {
|
|
5174
|
+
const p = await readSpeakerProfile(ctx.notesPath);
|
|
5175
|
+
if (!p) return ctx.textResponse(`no speaker profile yet (run action:init or manage_profile update_section)`);
|
|
5176
|
+
const content = await readFile2(join4(ctx.notesPath, SPEAKER_PROFILE_PATH), "utf-8");
|
|
5177
|
+
return ctx.textResponse(JSON.stringify({ path: p.path, head: p.head, content }, null, 2));
|
|
5178
|
+
}
|
|
5179
|
+
case "update_section": {
|
|
5180
|
+
if (!section || !text) return ctx.textResponse("manage_profile update_section requires `section` and `text`");
|
|
5181
|
+
const r = await updateProfileSection(ctx.notesPath, section, text, mode ?? "append");
|
|
5182
|
+
await ctx.sessions.recordNotesTouched([r.path]).catch(() => {
|
|
5183
|
+
});
|
|
5184
|
+
return ctx.textResponse(JSON.stringify(r, null, 2));
|
|
5185
|
+
}
|
|
5186
|
+
}
|
|
5187
|
+
}
|
|
5188
|
+
);
|
|
4828
5189
|
}
|
|
4829
5190
|
|
|
4830
5191
|
// src/mcp/tools/lint.ts
|
|
@@ -4833,7 +5194,11 @@ var RULE_KEY_FOR_CHECK = {
|
|
|
4833
5194
|
schema: "schema_violations",
|
|
4834
5195
|
provenance: "missing_provenance",
|
|
4835
5196
|
stale: "stale",
|
|
4836
|
-
broken_links: "broken_links"
|
|
5197
|
+
broken_links: "broken_links",
|
|
5198
|
+
code_symbols: "code_symbols",
|
|
5199
|
+
decay_candidates: "decay_candidates",
|
|
5200
|
+
alias_conflicts: "alias_conflicts",
|
|
5201
|
+
decoys: "decoys"
|
|
4837
5202
|
};
|
|
4838
5203
|
function registerLintTools(server, ctx) {
|
|
4839
5204
|
server.tool(
|
|
@@ -4874,13 +5239,19 @@ function registerLintTools(server, ctx) {
|
|
|
4874
5239
|
);
|
|
4875
5240
|
server.tool(
|
|
4876
5241
|
"lint_vault",
|
|
4877
|
-
"Run lint rules across the vault. Default returns the full LintReport with all findings grouped by rule. Pass `checks` to filter to specific rules (schema, provenance, stale, broken_links) \u2014 when filtered, byRule contains only the requested rules.",
|
|
5242
|
+
"Run lint rules across the vault. Default returns the full LintReport with all findings grouped by rule. Pass `checks` to filter to specific rules (schema, provenance, stale, broken_links, code_symbols, decay_candidates) \u2014 when filtered, byRule contains only the requested rules. Two checks are opt-in only (never in the default report): `code_symbols` flags inline-code file-path references that no longer exist under the project root (no-op outside a code repo); `decay_candidates` cross-references the selection log against current decay to surface frequently-retrieved-but-decayed notes (no-op with no selection log). `decoys` flags notes retrieved 3+ times but never cited - surfaced for review, never auto-down-ranked (v1.5 Q5).",
|
|
4878
5243
|
{
|
|
4879
5244
|
pathGlob: external_exports.string().optional(),
|
|
4880
|
-
checks: external_exports.array(external_exports.enum(["schema", "provenance", "stale", "broken_links"])).optional().describe("Filter to specific lint rules. Omit for full report.")
|
|
5245
|
+
checks: external_exports.array(external_exports.enum(["schema", "provenance", "stale", "broken_links", "code_symbols", "decay_candidates", "alias_conflicts", "decoys"])).optional().describe("Filter to specific lint rules. Omit for full report (excludes the opt-in code_symbols / decay_candidates / alias_conflicts / decoys).")
|
|
4881
5246
|
},
|
|
4882
5247
|
async ({ pathGlob, checks }) => {
|
|
4883
|
-
const report = await lintVault(ctx.notesPath, {
|
|
5248
|
+
const report = await lintVault(ctx.notesPath, {
|
|
5249
|
+
pathGlob,
|
|
5250
|
+
checkCodeSymbols: !!checks?.includes("code_symbols"),
|
|
5251
|
+
checkDecayCandidates: !!checks?.includes("decay_candidates"),
|
|
5252
|
+
checkAliasConflicts: !!checks?.includes("alias_conflicts"),
|
|
5253
|
+
checkDecoys: !!checks?.includes("decoys")
|
|
5254
|
+
});
|
|
4884
5255
|
if (!checks || checks.length === 0) {
|
|
4885
5256
|
return ctx.textResponse(JSON.stringify(report, null, 2));
|
|
4886
5257
|
}
|
|
@@ -5138,6 +5509,10 @@ var CURRENT_TOOL_INVENTORY = [
|
|
|
5138
5509
|
{ name: "synthesize_promote", description: "Move a reviewed proposal note to its canonical destination, stripping proposal markers.", group: "patch" },
|
|
5139
5510
|
{ name: "ingest_source", description: "Build + apply a ChangeSet that ingests a source and the notes extracted from it.", group: "patch" },
|
|
5140
5511
|
{ name: "install_schema", description: "Bootstrap vault.schema.yml in the vault root with the default schema.", group: "patch" },
|
|
5512
|
+
{ name: "verify_note", description: "Stamp last_verified to today (content unchanged) to reset the confidence-decay clock; logs a verify event.", group: "patch" },
|
|
5513
|
+
{ name: "manage_lexicon", description: "Manage the learned human\u2192artifact lexicon (add/lookup/list/remove/compile aliases).", group: "patch" },
|
|
5514
|
+
{ name: "manage_dossier", description: "Manage entity dossiers \u2014 per-component living notes (init/append_incident/set_state/get/list). Knowledge accretes in place.", group: "patch" },
|
|
5515
|
+
{ name: "manage_profile", description: "Manage the speaker profile \u2014 how this human communicates (init/get/update_section).", group: "patch" },
|
|
5141
5516
|
// lint
|
|
5142
5517
|
{ name: "find_schema_violations", description: "Scan for schema violations.", group: "lint", deprecated: true },
|
|
5143
5518
|
{ name: "find_missing_provenance", description: "Scan for notes lacking sources/derived_from.", group: "lint", deprecated: true },
|
|
@@ -5172,6 +5547,29 @@ var CURRENT_TOOL_INVENTORY = [
|
|
|
5172
5547
|
];
|
|
5173
5548
|
|
|
5174
5549
|
// src/mcp/tools/contract.ts
|
|
5550
|
+
import { fileURLToPath } from "url";
|
|
5551
|
+
import { dirname, join as join5 } from "path";
|
|
5552
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
|
|
5553
|
+
function pluginVersion() {
|
|
5554
|
+
try {
|
|
5555
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
5556
|
+
for (let i = 0; i < 6; i++) {
|
|
5557
|
+
const pkgPath = join5(dir, "package.json");
|
|
5558
|
+
if (existsSync4(pkgPath)) {
|
|
5559
|
+
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
5560
|
+
if (pkg?.name === "@theglitchking/semantic-memory" && typeof pkg.version === "string") {
|
|
5561
|
+
return pkg.version;
|
|
5562
|
+
}
|
|
5563
|
+
}
|
|
5564
|
+
const parent = dirname(dir);
|
|
5565
|
+
if (parent === dir) break;
|
|
5566
|
+
dir = parent;
|
|
5567
|
+
}
|
|
5568
|
+
return "0.0.0";
|
|
5569
|
+
} catch {
|
|
5570
|
+
return "0.0.0";
|
|
5571
|
+
}
|
|
5572
|
+
}
|
|
5175
5573
|
function registerContractTools(server, ctx) {
|
|
5176
5574
|
if (ctx.options.readOnly) return;
|
|
5177
5575
|
server.tool(
|
|
@@ -5195,7 +5593,7 @@ function registerContractTools(server, ctx) {
|
|
|
5195
5593
|
];
|
|
5196
5594
|
const result = await regenerateAgentsContract({
|
|
5197
5595
|
projectRoot: root,
|
|
5198
|
-
pluginVersion:
|
|
5596
|
+
pluginVersion: pluginVersion(),
|
|
5199
5597
|
tools,
|
|
5200
5598
|
modes,
|
|
5201
5599
|
force
|
|
@@ -5310,6 +5708,43 @@ function registerSessionTools(server, ctx) {
|
|
|
5310
5708
|
);
|
|
5311
5709
|
}
|
|
5312
5710
|
|
|
5711
|
+
// src/core/tools-config.ts
|
|
5712
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
|
|
5713
|
+
import { join as join6 } from "path";
|
|
5714
|
+
import { parse as parseYaml3 } from "yaml";
|
|
5715
|
+
function loadToolsConfig(vaultPath) {
|
|
5716
|
+
try {
|
|
5717
|
+
const schemaPath = join6(vaultPath, "vault.schema.yml");
|
|
5718
|
+
if (!existsSync5(schemaPath)) return { conditional: false };
|
|
5719
|
+
const parsed = parseYaml3(readFileSync4(schemaPath, "utf-8"));
|
|
5720
|
+
return { conditional: parsed?.tools?.conditional === true };
|
|
5721
|
+
} catch {
|
|
5722
|
+
return { conditional: false };
|
|
5723
|
+
}
|
|
5724
|
+
}
|
|
5725
|
+
function readActiveMode(vaultPath) {
|
|
5726
|
+
const root = deriveProjectRoot(vaultPath);
|
|
5727
|
+
const candidates = [
|
|
5728
|
+
join6(root, ".claude", ".semantic-memory", "mode"),
|
|
5729
|
+
join6(root, ".claude", ".sidekick-mode")
|
|
5730
|
+
];
|
|
5731
|
+
for (const p of candidates) {
|
|
5732
|
+
try {
|
|
5733
|
+
if (existsSync5(p)) {
|
|
5734
|
+
const v = readFileSync4(p, "utf-8").trim();
|
|
5735
|
+
if (v) return v;
|
|
5736
|
+
}
|
|
5737
|
+
} catch {
|
|
5738
|
+
}
|
|
5739
|
+
}
|
|
5740
|
+
return "vault-first";
|
|
5741
|
+
}
|
|
5742
|
+
function useMinimalToolSurface(vaultPath) {
|
|
5743
|
+
const cfg = loadToolsConfig(vaultPath);
|
|
5744
|
+
if (!cfg.conditional) return false;
|
|
5745
|
+
return readActiveMode(vaultPath) === "outage-silence";
|
|
5746
|
+
}
|
|
5747
|
+
|
|
5313
5748
|
// src/mcp/server.ts
|
|
5314
5749
|
async function createServer(notesPath, options = {}) {
|
|
5315
5750
|
const ctx = await buildContext(notesPath, options);
|
|
@@ -5317,17 +5752,20 @@ async function createServer(notesPath, options = {}) {
|
|
|
5317
5752
|
name: "semantic-memory",
|
|
5318
5753
|
version: "1.0.0"
|
|
5319
5754
|
});
|
|
5755
|
+
const minimal = useMinimalToolSurface(notesPath);
|
|
5320
5756
|
registerSearchTools(server, ctx);
|
|
5321
5757
|
registerReadTools(server, ctx);
|
|
5322
|
-
registerWriteTools(server, ctx);
|
|
5323
|
-
registerPatchTools(server, ctx);
|
|
5324
|
-
registerLintTools(server, ctx);
|
|
5325
|
-
registerLogTools(server, ctx);
|
|
5326
|
-
registerMetadataTools(server, ctx);
|
|
5327
|
-
registerGraphTools(server, ctx);
|
|
5328
5758
|
registerSystemTools(server, ctx);
|
|
5329
|
-
|
|
5330
|
-
|
|
5759
|
+
if (!minimal) {
|
|
5760
|
+
registerWriteTools(server, ctx);
|
|
5761
|
+
registerPatchTools(server, ctx);
|
|
5762
|
+
registerLintTools(server, ctx);
|
|
5763
|
+
registerLogTools(server, ctx);
|
|
5764
|
+
registerMetadataTools(server, ctx);
|
|
5765
|
+
registerGraphTools(server, ctx);
|
|
5766
|
+
registerContractTools(server, ctx);
|
|
5767
|
+
registerSessionTools(server, ctx);
|
|
5768
|
+
}
|
|
5331
5769
|
if (options.waitForReady) {
|
|
5332
5770
|
await ctx.tryLoadCachedIndex();
|
|
5333
5771
|
await ctx.fullIndex();
|