@hiveai/mcp 0.9.2 → 0.9.5
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/dist/index.js +288 -29
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +95 -9
- package/dist/server.js +292 -29
- package/dist/server.js.map +1 -1
- package/package.json +3 -3
package/dist/server.js
CHANGED
|
@@ -155,6 +155,30 @@ var MemSaveInputSchema = {
|
|
|
155
155
|
function bodyHash(body) {
|
|
156
156
|
return createHash("sha256").update(body.trim()).digest("hex").slice(0, 12);
|
|
157
157
|
}
|
|
158
|
+
var WORD_RE = /\b[a-z0-9]{3,}\b/gi;
|
|
159
|
+
function bodyTokenSet(body) {
|
|
160
|
+
const raw = body.toLowerCase().match(WORD_RE) ?? [];
|
|
161
|
+
return new Set(raw);
|
|
162
|
+
}
|
|
163
|
+
function maxBodySimilarity(incomingTokens, memories, scope, type, excludeIds) {
|
|
164
|
+
if (incomingTokens.size < 6) return null;
|
|
165
|
+
let best = null;
|
|
166
|
+
const skip = excludeIds ?? /* @__PURE__ */ new Set();
|
|
167
|
+
for (const { memory } of memories) {
|
|
168
|
+
const fm = memory.frontmatter;
|
|
169
|
+
if (skip.has(fm.id)) continue;
|
|
170
|
+
if (fm.scope !== scope || fm.type !== type) continue;
|
|
171
|
+
if (fm.status === "rejected" || fm.status === "deprecated") continue;
|
|
172
|
+
const other = bodyTokenSet(memory.body);
|
|
173
|
+
if (other.size === 0) continue;
|
|
174
|
+
let inter = 0;
|
|
175
|
+
for (const t of incomingTokens) if (other.has(t)) inter++;
|
|
176
|
+
const uni = incomingTokens.size + other.size - inter;
|
|
177
|
+
const j = uni === 0 ? 0 : inter / uni;
|
|
178
|
+
if (j >= 0.72 && (!best || j > best.score)) best = { score: j, id: fm.id };
|
|
179
|
+
}
|
|
180
|
+
return best;
|
|
181
|
+
}
|
|
158
182
|
async function memSave(input, ctx) {
|
|
159
183
|
if (!existsSync4(ctx.paths.haiveDir)) {
|
|
160
184
|
throw new Error(
|
|
@@ -176,12 +200,26 @@ async function memSave(input, ctx) {
|
|
|
176
200
|
`Duplicate content detected \u2014 identical body already saved as "${hashDuplicate.memory.frontmatter.id}". Use mem_update to modify it, or change the body to add new information.`
|
|
177
201
|
);
|
|
178
202
|
}
|
|
203
|
+
const incomingTokens = bodyTokenSet(input.body);
|
|
204
|
+
function bodySimilarWarnings(excludeIds) {
|
|
205
|
+
const dup = maxBodySimilarity(incomingTokens, existing, resolvedScope, input.type, excludeIds);
|
|
206
|
+
if (!dup?.id) return {};
|
|
207
|
+
const body_similar = {
|
|
208
|
+
id: dup.id,
|
|
209
|
+
score: Math.round(dup.score * 100) / 100
|
|
210
|
+
};
|
|
211
|
+
return {
|
|
212
|
+
similarityWarning: `Body is ~${Math.round(dup.score * 100)}% similar (token overlap) to existing "${dup.id}" \u2014 consolidate if redundant.`,
|
|
213
|
+
body_similar
|
|
214
|
+
};
|
|
215
|
+
}
|
|
179
216
|
if (input.topic) {
|
|
180
217
|
const topicMatch = existing.find(
|
|
181
218
|
({ memory }) => memory.frontmatter.topic === input.topic && memory.frontmatter.scope === resolvedScope && (!input.module || memory.frontmatter.module === input.module)
|
|
182
219
|
);
|
|
183
220
|
if (topicMatch) {
|
|
184
221
|
const fm = topicMatch.memory.frontmatter;
|
|
222
|
+
const { similarityWarning: simW, body_similar: bs } = bodySimilarWarnings(/* @__PURE__ */ new Set([fm.id]));
|
|
185
223
|
const newFrontmatter = {
|
|
186
224
|
...fm,
|
|
187
225
|
body: input.body,
|
|
@@ -198,13 +236,19 @@ async function memSave(input, ctx) {
|
|
|
198
236
|
serializeMemory({ frontmatter: newFrontmatter, body: input.body }),
|
|
199
237
|
"utf8"
|
|
200
238
|
);
|
|
239
|
+
const mergedTw = [
|
|
240
|
+
invalidPaths.length > 0 ? `Anchor path(s) not found in project: ${invalidPaths.join(", ")}. They will be marked stale by haive sync.` : null,
|
|
241
|
+
simW ?? null
|
|
242
|
+
].filter(Boolean).join(" \u2014 ") || void 0;
|
|
201
243
|
return {
|
|
202
244
|
id: fm.id,
|
|
203
245
|
scope: fm.scope,
|
|
204
246
|
file_path: topicMatch.filePath,
|
|
205
247
|
action: "updated",
|
|
206
248
|
revision_count: newFrontmatter.revision_count,
|
|
207
|
-
...
|
|
249
|
+
...mergedTw ? { warning: mergedTw } : {},
|
|
250
|
+
...bs ? { body_similar: bs } : {},
|
|
251
|
+
...invalidPaths.length > 0 ? { invalid_paths: invalidPaths } : {}
|
|
208
252
|
};
|
|
209
253
|
}
|
|
210
254
|
}
|
|
@@ -246,9 +290,11 @@ async function memSave(input, ctx) {
|
|
|
246
290
|
}
|
|
247
291
|
}
|
|
248
292
|
await writeFile2(file, serializeMemory({ frontmatter, body: input.body }), "utf8");
|
|
293
|
+
const { similarityWarning: simWarnNew, body_similar: bsNew } = bodySimilarWarnings();
|
|
249
294
|
const finalWarning = [
|
|
250
295
|
invalidPaths.length > 0 ? `Anchor path(s) not found in project: ${invalidPaths.join(", ")}. They will be marked stale by \`haive sync\`.` : null,
|
|
251
|
-
warning ?? null
|
|
296
|
+
warning ?? null,
|
|
297
|
+
simWarnNew ?? null
|
|
252
298
|
].filter(Boolean).join(" \u2014 ") || void 0;
|
|
253
299
|
return {
|
|
254
300
|
id: frontmatter.id,
|
|
@@ -257,6 +303,7 @@ async function memSave(input, ctx) {
|
|
|
257
303
|
action: "created",
|
|
258
304
|
...finalWarning ? { warning: finalWarning } : {},
|
|
259
305
|
...similar_found ? { similar_found } : {},
|
|
306
|
+
...bsNew ? { body_similar: bsNew } : {},
|
|
260
307
|
...invalidPaths.length > 0 ? { invalid_paths: invalidPaths } : {}
|
|
261
308
|
};
|
|
262
309
|
}
|
|
@@ -272,6 +319,7 @@ import {
|
|
|
272
319
|
loadMemoriesFromDir as loadMemoriesFromDir3,
|
|
273
320
|
loadUsageIndex,
|
|
274
321
|
pickSnippetNeedle,
|
|
322
|
+
rankMemoriesLexical,
|
|
275
323
|
tokenizeQuery,
|
|
276
324
|
trackReads
|
|
277
325
|
} from "@hiveai/core";
|
|
@@ -288,6 +336,9 @@ var MemSearchInputSchema = {
|
|
|
288
336
|
semantic: z5.boolean().default(false).describe(
|
|
289
337
|
"Use semantic similarity from the embeddings index (requires `haive embeddings index`)."
|
|
290
338
|
),
|
|
339
|
+
lexical_rank: z5.boolean().default(false).describe(
|
|
340
|
+
"When true (and semantic is false), rank the filtered corpus with Okapi-BM25-style lexical scoring instead of literal AND/OR. Helps phrase-like queries without embeddings."
|
|
341
|
+
),
|
|
291
342
|
min_score: z5.number().min(0).max(1).default(0).describe("Minimum cosine similarity (semantic mode only)"),
|
|
292
343
|
track: z5.boolean().default(true).describe("Increment read_count on returned memories (used for passive validation)")
|
|
293
344
|
};
|
|
@@ -313,6 +364,27 @@ async function memSearch(input, ctx) {
|
|
|
313
364
|
notice: "Semantic search unavailable (embeddings index missing or @hiveai/embeddings not installed). Falling back to literal search."
|
|
314
365
|
};
|
|
315
366
|
}
|
|
367
|
+
} else if (input.lexical_rank && input.query.trim()) {
|
|
368
|
+
const { ranked, scores } = rankMemoriesLexical(
|
|
369
|
+
filtered,
|
|
370
|
+
input.query,
|
|
371
|
+
input.limit
|
|
372
|
+
);
|
|
373
|
+
if (ranked.length > 0) {
|
|
374
|
+
const snippetNeedle = pickSnippetNeedle(input.query);
|
|
375
|
+
result = {
|
|
376
|
+
matches: ranked.map(
|
|
377
|
+
(loaded, i) => lexicalHit(loaded, snippetNeedle, usage, scores[i])
|
|
378
|
+
),
|
|
379
|
+
total: ranked.length,
|
|
380
|
+
mode: "lexical_ranked"
|
|
381
|
+
};
|
|
382
|
+
} else {
|
|
383
|
+
result = {
|
|
384
|
+
...buildLiteralResult(input, filtered, usage),
|
|
385
|
+
notice: "Lexical ranking found no BM25-positive hits \u2014 showing literal matches instead."
|
|
386
|
+
};
|
|
387
|
+
}
|
|
316
388
|
} else {
|
|
317
389
|
result = buildLiteralResult(input, filtered, usage);
|
|
318
390
|
}
|
|
@@ -410,6 +482,9 @@ function toHit(loaded, needle, usage) {
|
|
|
410
482
|
file_path: loaded.filePath
|
|
411
483
|
};
|
|
412
484
|
}
|
|
485
|
+
function lexicalHit(loaded, needle, usage, lexicalScore) {
|
|
486
|
+
return { ...toHit(loaded, needle, usage), score: lexicalScore };
|
|
487
|
+
}
|
|
413
488
|
|
|
414
489
|
// src/tools/mem-verify.ts
|
|
415
490
|
import { writeFile as writeFile3 } from "fs/promises";
|
|
@@ -1292,6 +1367,7 @@ import {
|
|
|
1292
1367
|
DEFAULT_AUTO_PROMOTE_RULE,
|
|
1293
1368
|
deriveConfidence as deriveConfidence4,
|
|
1294
1369
|
estimateTokens,
|
|
1370
|
+
extractActionsBriefBody,
|
|
1295
1371
|
getUsage as getUsage5,
|
|
1296
1372
|
inferModulesFromPaths as inferModulesFromPaths2,
|
|
1297
1373
|
isAutoPromoteEligible,
|
|
@@ -1304,6 +1380,7 @@ import {
|
|
|
1304
1380
|
loadUsageIndex as loadUsageIndex7,
|
|
1305
1381
|
memoryMatchesAnchorPaths as memoryMatchesAnchorPaths2,
|
|
1306
1382
|
queryCodeMap,
|
|
1383
|
+
resolveBriefingBudget,
|
|
1307
1384
|
serializeMemory as serializeMemory9,
|
|
1308
1385
|
tokenizeQuery as tokenizeQuery2,
|
|
1309
1386
|
trackReads as trackReads3,
|
|
@@ -1326,17 +1403,29 @@ var GetBriefingInputSchema = {
|
|
|
1326
1403
|
),
|
|
1327
1404
|
include_stale: z17.boolean().default(false).describe("Include stale memories (excluded by default \u2014 they may be outdated)"),
|
|
1328
1405
|
track: z17.boolean().default(true).describe("Increment read_count on returned memories"),
|
|
1329
|
-
format: z17.enum(["full", "compact"]).default("full").describe(
|
|
1330
|
-
"Output format: 'full' returns
|
|
1406
|
+
format: z17.enum(["full", "compact", "actions"]).default("full").describe(
|
|
1407
|
+
"Output format: 'full' returns memory bodies (honors token budget via truncation); 'compact' returns a 1-line summary per memory (call mem_get for detail); 'actions' squeezes bodies to actionable bullet lines \u2014 fewer tokens vs full."
|
|
1331
1408
|
),
|
|
1332
1409
|
symbols: z17.array(z17.string()).default([]).describe(
|
|
1333
1410
|
"Symbol names to look up in the code-map (e.g. ['PaymentService', 'TenantFilter']). Returns the file(s) exporting each symbol so agents don't need to grep. Requires `haive index code` to have been run."
|
|
1334
1411
|
),
|
|
1335
1412
|
min_semantic_score: z17.number().min(0).max(1).default(0).describe(
|
|
1336
1413
|
"Drop semantic-only memory hits whose cosine score is below this threshold. Useful to avoid weakly-related noise when the task is short or the corpus is broad. Has no effect on memories matched via anchor/module/literal \u2014 those are always kept. Try 0.25\u20130.4 for stricter matching."
|
|
1414
|
+
),
|
|
1415
|
+
budget_preset: z17.enum(["quick", "balanced", "deep"]).optional().describe(
|
|
1416
|
+
"Shortcut token budget: 'quick' minimizes tokens/skip module CONTEXT slices; 'balanced' mirrors historical defaults; 'deep' uses a larger briefing. When set, overrides max_tokens, max_memories, and include_module_contexts."
|
|
1337
1417
|
)
|
|
1338
1418
|
};
|
|
1419
|
+
var GetBriefingZod = z17.object(GetBriefingInputSchema);
|
|
1339
1420
|
async function getBriefing(input, ctx) {
|
|
1421
|
+
const resolvedBudget = resolveBriefingBudget(input.budget_preset, {
|
|
1422
|
+
max_tokens: input.max_tokens,
|
|
1423
|
+
max_memories: input.max_memories,
|
|
1424
|
+
include_module_contexts: input.include_module_contexts
|
|
1425
|
+
});
|
|
1426
|
+
const briefingMaxTokens = resolvedBudget.max_tokens;
|
|
1427
|
+
const briefingMaxMemories = resolvedBudget.max_memories;
|
|
1428
|
+
const briefingIncludeModules = resolvedBudget.include_module_contexts;
|
|
1340
1429
|
const inferred = inferModulesFromPaths2(input.files);
|
|
1341
1430
|
const memories = [];
|
|
1342
1431
|
let searchMode = "literal";
|
|
@@ -1448,8 +1537,8 @@ async function getBriefing(input, ctx) {
|
|
|
1448
1537
|
const sb = reasonScore(b) + confidenceScore(b) + (b.semantic_score ?? 0);
|
|
1449
1538
|
return sb - sa;
|
|
1450
1539
|
});
|
|
1451
|
-
for (const mem of ranked.slice(0,
|
|
1452
|
-
if (seen.size >=
|
|
1540
|
+
for (const mem of ranked.slice(0, briefingMaxMemories)) {
|
|
1541
|
+
if (seen.size >= briefingMaxMemories * 2) break;
|
|
1453
1542
|
const loaded = byId.get(mem.id);
|
|
1454
1543
|
if (!loaded) continue;
|
|
1455
1544
|
for (const relId of loaded.memory.frontmatter.related_ids ?? []) {
|
|
@@ -1458,7 +1547,7 @@ async function getBriefing(input, ctx) {
|
|
|
1458
1547
|
if (related) addOrUpdate(related, "anchor", void 0, "partial");
|
|
1459
1548
|
}
|
|
1460
1549
|
}
|
|
1461
|
-
memories.push(...ranked.slice(0,
|
|
1550
|
+
memories.push(...ranked.slice(0, briefingMaxMemories));
|
|
1462
1551
|
if (input.track && memories.length > 0) {
|
|
1463
1552
|
await trackReads3(ctx.paths, memories.map((m) => m.id));
|
|
1464
1553
|
const freshUsage = await loadUsageIndex7(ctx.paths);
|
|
@@ -1538,7 +1627,7 @@ async function getBriefing(input, ctx) {
|
|
|
1538
1627
|
}
|
|
1539
1628
|
}
|
|
1540
1629
|
}
|
|
1541
|
-
const moduleContents =
|
|
1630
|
+
const moduleContents = briefingIncludeModules ? await loadModuleContexts2(ctx, inferred) : [];
|
|
1542
1631
|
const memoriesText = memories.map((m) => {
|
|
1543
1632
|
const unverified = m.status === "proposed" ? " [UNVERIFIED \u2014 not yet validated]" : "";
|
|
1544
1633
|
return `### ${m.id} (${m.scope}/${m.type}, ${m.confidence})${unverified}
|
|
@@ -1556,7 +1645,7 @@ ${m.content}`).join("\n\n---\n\n"),
|
|
|
1556
1645
|
},
|
|
1557
1646
|
{ key: "memories", text: memoriesText, weight: 4, mode: "head" }
|
|
1558
1647
|
],
|
|
1559
|
-
|
|
1648
|
+
briefingMaxTokens
|
|
1560
1649
|
);
|
|
1561
1650
|
const projectSlice = slices.find((s) => s.key === "project");
|
|
1562
1651
|
const modulesSlice = slices.find((s) => s.key === "modules");
|
|
@@ -1598,7 +1687,10 @@ ${m.content}`).join("\n\n---\n\n"),
|
|
|
1598
1687
|
const createdAt = loaded?.memory.frontmatter.created_at ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
1599
1688
|
if (isDecaying(u, createdAt)) decayWarnings.push(m.id);
|
|
1600
1689
|
}
|
|
1601
|
-
const outputMemories = input.format === "compact" ? trimmedMemories.map((m) => ({ ...m, body: compactSummary(m.body) })) : trimmedMemories
|
|
1690
|
+
const outputMemories = input.format === "compact" ? trimmedMemories.map((m) => ({ ...m, body: compactSummary(m.body) })) : input.format === "actions" ? trimmedMemories.map((m) => ({
|
|
1691
|
+
...m,
|
|
1692
|
+
body: extractActionsBriefBody(m.body)
|
|
1693
|
+
})) : trimmedMemories;
|
|
1602
1694
|
let symbolLocations;
|
|
1603
1695
|
const symbolsToLookup = new Set(input.symbols);
|
|
1604
1696
|
for (const m of outputMemories) {
|
|
@@ -1720,6 +1812,11 @@ When done, call \`mem_session_end\` to acknowledge \u2014 this clears the pendin
|
|
|
1720
1812
|
"After completing the task: capture new gotchas with mem_observe, failed approaches with mem_tried, validated patterns with mem_save."
|
|
1721
1813
|
);
|
|
1722
1814
|
}
|
|
1815
|
+
if (outputMemories.length > 2 && !input.budget_preset && input.task && !hints.some((h) => h.includes("budget_preset"))) {
|
|
1816
|
+
hints.push(
|
|
1817
|
+
"For tighter token budgets on small tasks pass budget_preset:'quick'; for refactor-sized work use budget_preset:'deep'."
|
|
1818
|
+
);
|
|
1819
|
+
}
|
|
1723
1820
|
}
|
|
1724
1821
|
return {
|
|
1725
1822
|
...input.task ? { task: input.task } : {},
|
|
@@ -1742,7 +1839,8 @@ When done, call \`mem_session_end\` to acknowledge \u2014 this clears the pendin
|
|
|
1742
1839
|
...hints.length > 0 ? { hints } : {},
|
|
1743
1840
|
estimated_tokens: totalTokens,
|
|
1744
1841
|
budget: {
|
|
1745
|
-
max_tokens:
|
|
1842
|
+
max_tokens: briefingMaxTokens,
|
|
1843
|
+
...input.budget_preset ? { preset_applied: input.budget_preset } : {},
|
|
1746
1844
|
spent: {
|
|
1747
1845
|
project: projectSlice.estimatedTokens,
|
|
1748
1846
|
modules: modulesSlice.estimatedTokens,
|
|
@@ -1955,7 +2053,7 @@ var MemRelevantToInputSchema = {
|
|
|
1955
2053
|
files: z21.array(z21.string()).default([]).describe("Optional: files you are about to edit \u2014 surfaces anchored memories."),
|
|
1956
2054
|
limit: z21.number().int().positive().max(30).default(8).describe("Cap on returned memories."),
|
|
1957
2055
|
min_semantic_score: z21.number().min(0).max(1).default(0.25).describe("Drop weakly-related semantic hits below this cosine threshold."),
|
|
1958
|
-
format: z21.enum(["full", "compact"]).default("full").describe("'compact' = id + 1-line summary; 'full' = complete bodies.")
|
|
2056
|
+
format: z21.enum(["full", "compact", "actions"]).default("full").describe("'compact' = id + 1-line summary; 'full' = complete bodies; 'actions' = bullet-first excerpts.")
|
|
1959
2057
|
};
|
|
1960
2058
|
async function memRelevantTo(input, ctx) {
|
|
1961
2059
|
const briefingInput = {
|
|
@@ -2968,13 +3066,96 @@ function gitFileDiff(root, file, sinceDays) {
|
|
|
2968
3066
|
}
|
|
2969
3067
|
}
|
|
2970
3068
|
|
|
2971
|
-
// src/
|
|
3069
|
+
// src/tools/mem-conflict-candidates.ts
|
|
3070
|
+
import { existsSync as existsSync27 } from "fs";
|
|
3071
|
+
import { findLexicalConflictPairs, loadMemoriesFromDir as loadMemoriesFromDir21 } from "@hiveai/core";
|
|
2972
3072
|
import { z as z30 } from "zod";
|
|
3073
|
+
var MemConflictCandidatesInputSchema = {
|
|
3074
|
+
since_days: z30.number().int().positive().max(3650).default(365).describe("Only memories created since N days ago"),
|
|
3075
|
+
types: z30.array(z30.enum(["decision", "architecture", "convention", "gotcha"])).default(["decision", "architecture"]).describe("Memory types scanned for pairwise lexical overlap"),
|
|
3076
|
+
min_jaccard: z30.number().min(0).max(1).default(0.45).describe("Minimum Jaccard token similarity to surface as a candidate pair"),
|
|
3077
|
+
max_pairs: z30.number().int().positive().max(100).default(20).describe("Cap pairs returned"),
|
|
3078
|
+
max_scan: z30.number().int().positive().max(2e3).default(500).describe("Maximum memories sampled for O(n\xB2) scan \u2014 excess dropped after chronological sort.")
|
|
3079
|
+
};
|
|
3080
|
+
async function memConflictCandidates(input, ctx) {
|
|
3081
|
+
if (!existsSync27(ctx.paths.memoriesDir)) {
|
|
3082
|
+
return {
|
|
3083
|
+
pairs: [],
|
|
3084
|
+
scanned: 0,
|
|
3085
|
+
truncated: false,
|
|
3086
|
+
notice: "No .ai/memories directory."
|
|
3087
|
+
};
|
|
3088
|
+
}
|
|
3089
|
+
const all = await loadMemoriesFromDir21(ctx.paths.memoriesDir);
|
|
3090
|
+
const { pairs, scanned, truncated } = findLexicalConflictPairs(all, {
|
|
3091
|
+
sinceDays: input.since_days,
|
|
3092
|
+
types: input.types,
|
|
3093
|
+
minJaccard: input.min_jaccard,
|
|
3094
|
+
maxPairs: input.max_pairs,
|
|
3095
|
+
maxScan: input.max_scan
|
|
3096
|
+
});
|
|
3097
|
+
const notice = pairs.length === 0 ? "No lexical candidate pairs \u2265 threshold \u2014 try lowering min_jaccard or widen since_days/types." : void 0;
|
|
3098
|
+
return { pairs, scanned, truncated, notice };
|
|
3099
|
+
}
|
|
3100
|
+
|
|
3101
|
+
// src/tools/mem-resolve-project.ts
|
|
3102
|
+
import { resolveProjectInfo } from "@hiveai/core";
|
|
3103
|
+
import { z as z31 } from "zod";
|
|
3104
|
+
var MemResolveProjectInputSchema = {
|
|
3105
|
+
cwd: z31.string().optional().describe("Directory used for root discovery when HAIVE_PROJECT_ROOT is unset.")
|
|
3106
|
+
};
|
|
3107
|
+
async function memResolveProject(input, _ctx) {
|
|
3108
|
+
void _ctx;
|
|
3109
|
+
return {
|
|
3110
|
+
ok: true,
|
|
3111
|
+
info: resolveProjectInfo({
|
|
3112
|
+
cwd: input.cwd
|
|
3113
|
+
})
|
|
3114
|
+
};
|
|
3115
|
+
}
|
|
3116
|
+
|
|
3117
|
+
// src/tools/mem-suggest-topic.ts
|
|
3118
|
+
import { MemoryTypeSchema, suggestTopicKey } from "@hiveai/core";
|
|
3119
|
+
import { z as z32 } from "zod";
|
|
3120
|
+
var MemSuggestTopicInputSchema = {
|
|
3121
|
+
type: MemoryTypeSchema.describe("Memory kind \u2014 drives the suggested topic family."),
|
|
3122
|
+
title: z32.string().min(1).describe("Short title or phrase (headers, headings) \u2014 turned into slug")
|
|
3123
|
+
};
|
|
3124
|
+
async function memSuggestTopic(input, _ctx) {
|
|
3125
|
+
void _ctx;
|
|
3126
|
+
const suggestion = suggestTopicKey(input.type, input.title);
|
|
3127
|
+
return { topic_key: suggestion.topic_key, family: suggestion.family, type: input.type };
|
|
3128
|
+
}
|
|
3129
|
+
|
|
3130
|
+
// src/tools/mem-timeline.ts
|
|
3131
|
+
import { existsSync as existsSync28 } from "fs";
|
|
3132
|
+
import { collectTimelineEntries, loadMemoriesFromDir as loadMemoriesFromDir22 } from "@hiveai/core";
|
|
3133
|
+
import { z as z33 } from "zod";
|
|
3134
|
+
var MemTimelineInputSchema = {
|
|
3135
|
+
memory_id: z33.string().optional().describe("Seed id \u2014 expands via related_ids, topic, anchors"),
|
|
3136
|
+
topic: z33.string().optional().describe("Frontmatter.topic value \u2014 chronological list when memory_id omitted"),
|
|
3137
|
+
limit: z33.number().int().positive().max(100).default(30).describe("Max timeline entries returned")
|
|
3138
|
+
};
|
|
3139
|
+
async function memTimeline(input, ctx) {
|
|
3140
|
+
if (!existsSync28(ctx.paths.memoriesDir)) {
|
|
3141
|
+
return { entries: [], total: 0, notice: "No .ai/memories directory." };
|
|
3142
|
+
}
|
|
3143
|
+
const all = await loadMemoriesFromDir22(ctx.paths.memoriesDir);
|
|
3144
|
+
const { entries, notice } = collectTimelineEntries(all, {
|
|
3145
|
+
memoryId: input.memory_id,
|
|
3146
|
+
topic: input.topic,
|
|
3147
|
+
limit: input.limit
|
|
3148
|
+
});
|
|
3149
|
+
return { entries, total: entries.length, notice };
|
|
3150
|
+
}
|
|
3151
|
+
|
|
3152
|
+
// src/prompts/bootstrap-project.ts
|
|
3153
|
+
import { z as z34 } from "zod";
|
|
2973
3154
|
var BootstrapProjectArgsSchema = {
|
|
2974
|
-
module:
|
|
3155
|
+
module: z34.string().optional().describe(
|
|
2975
3156
|
"Optional module name to scope the analysis to (writes to .ai/modules/<module>/context.md)"
|
|
2976
3157
|
),
|
|
2977
|
-
focus:
|
|
3158
|
+
focus: z34.string().optional().describe("Optional area to emphasize (e.g. 'data layer', 'API surface')")
|
|
2978
3159
|
};
|
|
2979
3160
|
var ROOT_TEMPLATE = `# Project context
|
|
2980
3161
|
|
|
@@ -3056,10 +3237,10 @@ ${template}\`\`\`
|
|
|
3056
3237
|
}
|
|
3057
3238
|
|
|
3058
3239
|
// src/prompts/post-task.ts
|
|
3059
|
-
import { z as
|
|
3240
|
+
import { z as z35 } from "zod";
|
|
3060
3241
|
var PostTaskArgsSchema = {
|
|
3061
|
-
task_summary:
|
|
3062
|
-
files_touched:
|
|
3242
|
+
task_summary: z35.string().optional().describe("One sentence describing what you just did"),
|
|
3243
|
+
files_touched: z35.array(z35.string()).optional().describe("Files you created or modified during the task")
|
|
3063
3244
|
};
|
|
3064
3245
|
function postTaskPrompt(args, ctx) {
|
|
3065
3246
|
const taskLine = args.task_summary ? `
|
|
@@ -3143,12 +3324,12 @@ When done, respond with a brief summary: "Saved N memories: [list of IDs]. Sessi
|
|
|
3143
3324
|
}
|
|
3144
3325
|
|
|
3145
3326
|
// src/prompts/import-docs.ts
|
|
3146
|
-
import { z as
|
|
3327
|
+
import { z as z36 } from "zod";
|
|
3147
3328
|
var ImportDocsArgsSchema = {
|
|
3148
|
-
content:
|
|
3149
|
-
source:
|
|
3150
|
-
scope:
|
|
3151
|
-
dry_run:
|
|
3329
|
+
content: z36.string().describe("The documentation content to analyze and import as memories (Markdown, README, ADR, etc.)"),
|
|
3330
|
+
source: z36.string().optional().describe("Origin of the content (file path, URL, or document title) \u2014 used to anchor memories"),
|
|
3331
|
+
scope: z36.enum(["personal", "team"]).default("team").describe("Scope to assign to created memories"),
|
|
3332
|
+
dry_run: z36.boolean().default(false).describe("If true, describe what would be saved without actually calling mem_save")
|
|
3152
3333
|
};
|
|
3153
3334
|
function importDocsPrompt(args, ctx) {
|
|
3154
3335
|
const sourceLine = args.source ? `
|
|
@@ -3213,7 +3394,7 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
|
|
|
3213
3394
|
|
|
3214
3395
|
// src/server.ts
|
|
3215
3396
|
var SERVER_NAME = "haive";
|
|
3216
|
-
var SERVER_VERSION = "0.9.
|
|
3397
|
+
var SERVER_VERSION = "0.9.5";
|
|
3217
3398
|
function jsonResult(data) {
|
|
3218
3399
|
return {
|
|
3219
3400
|
content: [
|
|
@@ -3264,6 +3445,23 @@ function createHaiveServer(options = {}) {
|
|
|
3264
3445
|
return jsonResult(await memSave(input, context));
|
|
3265
3446
|
}
|
|
3266
3447
|
);
|
|
3448
|
+
server.tool(
|
|
3449
|
+
"mem_suggest_topic",
|
|
3450
|
+
[
|
|
3451
|
+
"Propose a stable `topic` key (topic-upsert) from type + short title.",
|
|
3452
|
+
"",
|
|
3453
|
+
"USE BEFORE mem_save when you want deterministic updates to the same memory over time;",
|
|
3454
|
+
"families mimic Engram-style grouping (architecture/*, bug/*, decision/*, \u2026).",
|
|
3455
|
+
"",
|
|
3456
|
+
"PARAMETERS:",
|
|
3457
|
+
" type \u2014 convention | decision | gotcha | architecture | glossary | attempt | session_recap",
|
|
3458
|
+
" title \u2014 phrase to slugify under the suggested family prefix",
|
|
3459
|
+
"",
|
|
3460
|
+
"RETURNS: { topic_key, family, type }"
|
|
3461
|
+
].join("\n"),
|
|
3462
|
+
MemSuggestTopicInputSchema,
|
|
3463
|
+
async (input) => jsonResult(await memSuggestTopic(input, context))
|
|
3464
|
+
);
|
|
3267
3465
|
server.tool(
|
|
3268
3466
|
"mem_tried",
|
|
3269
3467
|
[
|
|
@@ -3355,8 +3553,12 @@ function createHaiveServer(options = {}) {
|
|
|
3355
3553
|
server.tool(
|
|
3356
3554
|
"get_briefing",
|
|
3357
3555
|
[
|
|
3358
|
-
"\u2B50
|
|
3359
|
-
"
|
|
3556
|
+
"\u2B50 DEFAULT-FIRST for coding agents on any repo where `haive init` ran: call this BEFORE",
|
|
3557
|
+
"changing source or project config for the current goal (unless the developer explicitly opts out).",
|
|
3558
|
+
"One-shot onboarding: everything relevant in a single call under a token budget.",
|
|
3559
|
+
"",
|
|
3560
|
+
"PROGRESSIVE DISCLOSURE \u2014 after this, drill down only if needed:",
|
|
3561
|
+
" mem_relevant_to / mem_search (compact lists) \u2192 mem_get (full body + anchors).",
|
|
3360
3562
|
"",
|
|
3361
3563
|
"RETURNS (in order of priority):",
|
|
3362
3564
|
" 0. action_required \u2014 \u26A0\uFE0F HANDLE THIS FIRST if non-empty (see protocol below)",
|
|
@@ -3379,7 +3581,8 @@ function createHaiveServer(options = {}) {
|
|
|
3379
3581
|
" task \u2014 what you are about to do (1\u20132 sentences) \u2014 ALWAYS provide this",
|
|
3380
3582
|
" files \u2014 files you are about to edit \u2014 surfaces anchored memories",
|
|
3381
3583
|
" symbols \u2014 symbol names to look up in the code-map (e.g. ['PaymentService'])",
|
|
3382
|
-
" format \u2014 'full' (default) | 'compact' (1-line
|
|
3584
|
+
" format \u2014 'full' (default) | 'compact' (1-line) | 'actions' (bullet-first excerpts)",
|
|
3585
|
+
" budget_preset \u2014 'quick' | 'balanced' | 'deep' \u2014 scales max_tokens/memories/module contexts",
|
|
3383
3586
|
"",
|
|
3384
3587
|
"EXAMPLE USAGE:",
|
|
3385
3588
|
" get_briefing({ task: 'add a Stripe payment integration', files: ['src/payments/'], symbols: ['PaymentService'] })",
|
|
@@ -3390,7 +3593,7 @@ function createHaiveServer(options = {}) {
|
|
|
3390
3593
|
" low \u2014 proposed, few reads (take with caution)",
|
|
3391
3594
|
" unverified \u2014 draft (unverified: true flag set)",
|
|
3392
3595
|
"",
|
|
3393
|
-
"Replaces 4\u20135 separate tool calls.
|
|
3596
|
+
"Replaces 4\u20135 separate tool calls. Prefer this first; use mem_search / mem_get only for follow-up."
|
|
3394
3597
|
].join("\n"),
|
|
3395
3598
|
GetBriefingInputSchema,
|
|
3396
3599
|
async (input) => {
|
|
@@ -3409,6 +3612,8 @@ function createHaiveServer(options = {}) {
|
|
|
3409
3612
|
"SEARCH MODES:",
|
|
3410
3613
|
" Literal (default): AND search across id, tags, and body \u2014 all tokens must match.",
|
|
3411
3614
|
" Falls back to OR automatically if no AND results (partial match).",
|
|
3615
|
+
" Lexical rank (lexical_rank: true, semantic: false): Okapi-BM25-style scoring on the",
|
|
3616
|
+
" filtered corpus \u2014 good for phrase-like queries without embeddings.",
|
|
3412
3617
|
" Semantic (semantic: true): embedding-based similarity \u2014 finds related memories",
|
|
3413
3618
|
" even with different wording. Requires haive embeddings index to be built.",
|
|
3414
3619
|
"",
|
|
@@ -3417,6 +3622,7 @@ function createHaiveServer(options = {}) {
|
|
|
3417
3622
|
" scope \u2014 filter by personal | team | module",
|
|
3418
3623
|
" type \u2014 filter by convention | decision | gotcha | architecture | glossary",
|
|
3419
3624
|
" semantic \u2014 true for embedding-based search (requires @hiveai/embeddings)",
|
|
3625
|
+
" lexical_rank \u2014 BM25-style ranking (ignored when semantic is true)",
|
|
3420
3626
|
" limit \u2014 max results (default 10)",
|
|
3421
3627
|
"",
|
|
3422
3628
|
"RETURNS: array of { id, type, scope, status, confidence, body, match_quality }"
|
|
@@ -3427,6 +3633,22 @@ function createHaiveServer(options = {}) {
|
|
|
3427
3633
|
return jsonResult(await memSearch(input, context));
|
|
3428
3634
|
}
|
|
3429
3635
|
);
|
|
3636
|
+
server.tool(
|
|
3637
|
+
"mem_timeline",
|
|
3638
|
+
[
|
|
3639
|
+
"Chronological view of related memories: by shared frontmatter.topic OR expanded from a seed id",
|
|
3640
|
+
"(related_ids, same topic, overlapping anchor paths \u2014 one extra hop on related_ids).",
|
|
3641
|
+
"",
|
|
3642
|
+
"PARAMETERS:",
|
|
3643
|
+
" memory_id \u2014 optional seed memory id",
|
|
3644
|
+
" topic \u2014 optional topic key (required if memory_id omitted)",
|
|
3645
|
+
" limit \u2014 max entries (default 30)",
|
|
3646
|
+
"",
|
|
3647
|
+
"RETURNS: { entries: [{ id, type, scope, created_at, one_line, topic? }], total, notice? }"
|
|
3648
|
+
].join("\n"),
|
|
3649
|
+
MemTimelineInputSchema,
|
|
3650
|
+
async (input) => jsonResult(await memTimeline(input, context))
|
|
3651
|
+
);
|
|
3430
3652
|
server.tool(
|
|
3431
3653
|
"mem_for_files",
|
|
3432
3654
|
[
|
|
@@ -3456,7 +3678,7 @@ function createHaiveServer(options = {}) {
|
|
|
3456
3678
|
[
|
|
3457
3679
|
"Fetch a single memory by its full id with all details.",
|
|
3458
3680
|
"",
|
|
3459
|
-
"USE WHEN get_briefing returned a
|
|
3681
|
+
"USE WHEN get_briefing / mem_relevant_to / mem_search returned a compact hit and you need",
|
|
3460
3682
|
"the full body, or when you know the exact id of a memory.",
|
|
3461
3683
|
"",
|
|
3462
3684
|
"PARAMETERS:",
|
|
@@ -3544,6 +3766,22 @@ function createHaiveServer(options = {}) {
|
|
|
3544
3766
|
CodeMapInputSchema,
|
|
3545
3767
|
async (input) => jsonResult(await codeMapTool(input, context))
|
|
3546
3768
|
);
|
|
3769
|
+
server.tool(
|
|
3770
|
+
"mem_resolve_project",
|
|
3771
|
+
[
|
|
3772
|
+
"Diagnostics: resolve which project root hAIve is using (never throws).",
|
|
3773
|
+
"",
|
|
3774
|
+
"USE IN multi-root workspaces or when the agent CWD may not be the repo root \u2014",
|
|
3775
|
+
"mirrors HAIVE_PROJECT_ROOT, findProjectRoot markers, and presence of .ai/memories.",
|
|
3776
|
+
"",
|
|
3777
|
+
"PARAMETERS:",
|
|
3778
|
+
" cwd \u2014 optional directory used for discovery when HAIVE_PROJECT_ROOT is unset",
|
|
3779
|
+
"",
|
|
3780
|
+
"RETURNS: { ok: true, info: { cwd, resolved_root, haive_project_root_env, \u2026 } }"
|
|
3781
|
+
].join("\n"),
|
|
3782
|
+
MemResolveProjectInputSchema,
|
|
3783
|
+
async (input) => jsonResult(await memResolveProject(input, context))
|
|
3784
|
+
);
|
|
3547
3785
|
server.tool(
|
|
3548
3786
|
"mem_update",
|
|
3549
3787
|
[
|
|
@@ -3677,6 +3915,8 @@ function createHaiveServer(options = {}) {
|
|
|
3677
3915
|
"One-shot ranked memories for a task \u2014 use instead of get_briefing when",
|
|
3678
3916
|
"project context is already loaded and you only want the relevant memory layer.",
|
|
3679
3917
|
"",
|
|
3918
|
+
"Second step in progressive disclosure (after get_briefing): narrow here, then mem_get for full text.",
|
|
3919
|
+
"",
|
|
3680
3920
|
"Reuses the same ranking pipeline (anchor / module / literal / semantic) but",
|
|
3681
3921
|
"skips project_context, modules, action_required, etc.",
|
|
3682
3922
|
"",
|
|
@@ -3685,6 +3925,7 @@ function createHaiveServer(options = {}) {
|
|
|
3685
3925
|
" files \u2014 files you'll edit (surfaces anchored memories)",
|
|
3686
3926
|
" limit \u2014 cap on returned memories (default 8)",
|
|
3687
3927
|
" min_semantic_score \u2014 drop weak semantic hits below this cosine (default 0.25)",
|
|
3928
|
+
" format \u2014 'full' | 'compact' | 'actions' (inherits get_briefing memory framing)",
|
|
3688
3929
|
"",
|
|
3689
3930
|
"RETURNS: { task, search_mode, memories: [...], hints?: [...], empty?: true }"
|
|
3690
3931
|
].join("\n"),
|
|
@@ -3833,6 +4074,24 @@ function createHaiveServer(options = {}) {
|
|
|
3833
4074
|
return jsonResult(await memConflicts(input, context));
|
|
3834
4075
|
}
|
|
3835
4076
|
);
|
|
4077
|
+
server.tool(
|
|
4078
|
+
"mem_conflict_candidates",
|
|
4079
|
+
[
|
|
4080
|
+
"Bulk lexical scan for decision/architecture-like pairs that look similar (Jaccard on tokens).",
|
|
4081
|
+
"",
|
|
4082
|
+
"Advisory only \u2014 follow with mem_conflicts_with on specific ids for real contradiction checks.",
|
|
4083
|
+
"",
|
|
4084
|
+
"PARAMETERS:",
|
|
4085
|
+
" since_days, types, min_jaccard, max_pairs, max_scan",
|
|
4086
|
+
"",
|
|
4087
|
+
"RETURNS: { pairs: [{ id_a, id_b, jaccard }], scanned, truncated, notice? }"
|
|
4088
|
+
].join("\n"),
|
|
4089
|
+
MemConflictCandidatesInputSchema,
|
|
4090
|
+
async (input) => {
|
|
4091
|
+
tracker.record("mem_conflict_candidates", `${input.since_days}d`);
|
|
4092
|
+
return jsonResult(await memConflictCandidates(input, context));
|
|
4093
|
+
}
|
|
4094
|
+
);
|
|
3836
4095
|
server.tool(
|
|
3837
4096
|
"pre_commit_check",
|
|
3838
4097
|
[
|
|
@@ -3979,9 +4238,13 @@ export {
|
|
|
3979
4238
|
createHaiveServer,
|
|
3980
4239
|
getBriefing,
|
|
3981
4240
|
getRecap,
|
|
4241
|
+
memConflictCandidates,
|
|
3982
4242
|
memConflicts,
|
|
3983
4243
|
memDistill,
|
|
3984
4244
|
memRelevantTo,
|
|
4245
|
+
memResolveProject,
|
|
4246
|
+
memSuggestTopic,
|
|
4247
|
+
memTimeline,
|
|
3985
4248
|
parseMcpCliArgs,
|
|
3986
4249
|
patternDetect,
|
|
3987
4250
|
preCommitCheck,
|