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