@hiveai/mcp 0.4.5 → 0.6.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/dist/index.js +1163 -21
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +488 -2
- package/dist/server.js +1175 -22
- package/dist/server.js.map +1 -1
- package/package.json +4 -4
package/dist/server.js
CHANGED
|
@@ -1195,6 +1195,9 @@ var GetBriefingInputSchema = {
|
|
|
1195
1195
|
),
|
|
1196
1196
|
symbols: z17.array(z17.string()).default([]).describe(
|
|
1197
1197
|
"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."
|
|
1198
|
+
),
|
|
1199
|
+
min_semantic_score: z17.number().min(0).max(1).default(0).describe(
|
|
1200
|
+
"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."
|
|
1198
1201
|
)
|
|
1199
1202
|
};
|
|
1200
1203
|
async function getBriefing(input, ctx) {
|
|
@@ -1227,6 +1230,7 @@ async function getBriefing(input, ctx) {
|
|
|
1227
1230
|
return true;
|
|
1228
1231
|
});
|
|
1229
1232
|
usage = await loadUsageIndex7(ctx.paths);
|
|
1233
|
+
byId = new Map(allMemories.map((m) => [m.memory.frontmatter.id, m]));
|
|
1230
1234
|
const semanticHits = input.task && input.semantic ? await trySemanticHits(ctx, input.task, allMemories.length * 2) : null;
|
|
1231
1235
|
if (input.task && input.semantic) {
|
|
1232
1236
|
searchMode = semanticHits ? "semantic" : "literal_fallback";
|
|
@@ -1291,6 +1295,10 @@ async function getBriefing(input, ctx) {
|
|
|
1291
1295
|
}
|
|
1292
1296
|
if (semanticHits) {
|
|
1293
1297
|
for (const hit of semanticHits) {
|
|
1298
|
+
if (hit.score < input.min_semantic_score) {
|
|
1299
|
+
const existing = seen.get(hit.id);
|
|
1300
|
+
if (!existing) continue;
|
|
1301
|
+
}
|
|
1294
1302
|
const loaded = byId.get(hit.id);
|
|
1295
1303
|
if (loaded) addOrUpdate(loaded, "semantic", hit.score, "semantic");
|
|
1296
1304
|
}
|
|
@@ -1304,7 +1312,6 @@ async function getBriefing(input, ctx) {
|
|
|
1304
1312
|
const sb = reasonScore(b) + confidenceScore(b) + (b.semantic_score ?? 0);
|
|
1305
1313
|
return sb - sa;
|
|
1306
1314
|
});
|
|
1307
|
-
byId = new Map(allMemories.map((m) => [m.memory.frontmatter.id, m]));
|
|
1308
1315
|
for (const mem of ranked.slice(0, input.max_memories)) {
|
|
1309
1316
|
if (seen.size >= input.max_memories * 2) break;
|
|
1310
1317
|
const loaded = byId.get(mem.id);
|
|
@@ -1496,6 +1503,36 @@ ${m.content}`).join("\n\n---\n\n"),
|
|
|
1496
1503
|
});
|
|
1497
1504
|
}
|
|
1498
1505
|
}
|
|
1506
|
+
const memoriesEmpty = outputMemories.length === 0;
|
|
1507
|
+
const hasMemoriesDir = existsSync17(ctx.paths.memoriesDir);
|
|
1508
|
+
const isColdStart = isTemplateContext && memoriesEmpty && !lastSession && !autoContextGenerated;
|
|
1509
|
+
const hints = [];
|
|
1510
|
+
if (isColdStart) {
|
|
1511
|
+
hints.push(
|
|
1512
|
+
"haive is uninitialized for this project (project-context.md is template, 0 memories, no past session). Skip future get_briefing calls until memories exist \u2014 use Read/Grep directly. Run `haive init` and the bootstrap_project prompt to fix."
|
|
1513
|
+
);
|
|
1514
|
+
} else {
|
|
1515
|
+
if (outputMemories.some((m) => m.type === "attempt")) {
|
|
1516
|
+
hints.push(
|
|
1517
|
+
"\u26A0\uFE0F One or more 'attempt' memories matched \u2014 these document failed approaches. Read them BEFORE writing code to avoid repeating the mistake."
|
|
1518
|
+
);
|
|
1519
|
+
}
|
|
1520
|
+
if (outputMemories.some((m) => m.type === "gotcha")) {
|
|
1521
|
+
hints.push(
|
|
1522
|
+
"Gotcha memories matched \u2014 non-obvious traps. Verify the 'how to apply' line still holds before assuming behavior."
|
|
1523
|
+
);
|
|
1524
|
+
}
|
|
1525
|
+
if (memoriesEmpty && hasMemoriesDir && input.task) {
|
|
1526
|
+
hints.push(
|
|
1527
|
+
"No memories matched this task. Try mem_search with broader/different terms, or call mem_for_files with the files you intend to edit."
|
|
1528
|
+
);
|
|
1529
|
+
}
|
|
1530
|
+
if (input.task && outputMemories.length > 0 && actionRequired.length === 0) {
|
|
1531
|
+
hints.push(
|
|
1532
|
+
"After completing the task: capture new gotchas with mem_observe, failed approaches with mem_tried, validated patterns with mem_save."
|
|
1533
|
+
);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1499
1536
|
return {
|
|
1500
1537
|
...input.task ? { task: input.task } : {},
|
|
1501
1538
|
search_mode: searchMode,
|
|
@@ -1513,6 +1550,8 @@ ${m.content}`).join("\n\n---\n\n"),
|
|
|
1513
1550
|
action_required: actionRequired,
|
|
1514
1551
|
decay_warnings: decayWarnings,
|
|
1515
1552
|
setup_warnings: setupWarnings,
|
|
1553
|
+
...isColdStart ? { low_value: true } : {},
|
|
1554
|
+
...hints.length > 0 ? { hints } : {},
|
|
1516
1555
|
estimated_tokens: totalTokens,
|
|
1517
1556
|
budget: {
|
|
1518
1557
|
max_tokens: input.max_tokens,
|
|
@@ -1560,12 +1599,18 @@ async function loadModuleContexts2(ctx, modules) {
|
|
|
1560
1599
|
}
|
|
1561
1600
|
|
|
1562
1601
|
// src/tools/code-map.ts
|
|
1563
|
-
import { loadCodeMap as loadCodeMap2, queryCodeMap as queryCodeMap2 } from "@hiveai/core";
|
|
1602
|
+
import { estimateTokens as estimateTokens2, loadCodeMap as loadCodeMap2, queryCodeMap as queryCodeMap2 } from "@hiveai/core";
|
|
1564
1603
|
import { z as z18 } from "zod";
|
|
1565
1604
|
var CodeMapInputSchema = {
|
|
1566
1605
|
file: z18.string().optional().describe("Filter to files whose path contains this substring"),
|
|
1567
1606
|
symbol: z18.string().optional().describe("Filter to files exporting a symbol whose name contains this substring"),
|
|
1568
|
-
|
|
1607
|
+
paths: z18.array(z18.string()).default([]).describe(
|
|
1608
|
+
"Filter to files under any of these path prefixes (e.g. ['packages/mcp/src/tools/', 'src/auth/']). OR-joined with `file` substring; useful to get a focused view of one module."
|
|
1609
|
+
),
|
|
1610
|
+
max_files: z18.number().int().positive().default(40).describe("Cap on returned files (hard limit, applied after token budget)"),
|
|
1611
|
+
max_tokens: z18.number().int().positive().optional().describe(
|
|
1612
|
+
"Approximate token budget for the response. When the matching set exceeds it, files are ranked by export density (exports per LOC) and the highest-signal ones are kept first. Omit to disable budgeting (legacy behavior)."
|
|
1613
|
+
)
|
|
1569
1614
|
};
|
|
1570
1615
|
async function codeMapTool(input, ctx) {
|
|
1571
1616
|
const map = await loadCodeMap2(ctx.paths);
|
|
@@ -1576,19 +1621,63 @@ async function codeMapTool(input, ctx) {
|
|
|
1576
1621
|
notice: "No code map found. Run `haive index code` to generate `.ai/code-map.json`."
|
|
1577
1622
|
};
|
|
1578
1623
|
}
|
|
1579
|
-
const { files } = queryCodeMap2(map, { file: input.file, symbol: input.symbol });
|
|
1624
|
+
const { files: matched } = queryCodeMap2(map, { file: input.file, symbol: input.symbol });
|
|
1625
|
+
const pathsFiltered = input.paths.length === 0 ? matched : matched.filter((f) => input.paths.some((p) => f.path.startsWith(stripLeadingSlash(p))));
|
|
1626
|
+
const alphabetical = [...pathsFiltered].sort((a, b) => a.path.localeCompare(b.path));
|
|
1627
|
+
let kept = alphabetical;
|
|
1628
|
+
let budgetClipped = false;
|
|
1629
|
+
if (input.max_tokens !== void 0) {
|
|
1630
|
+
const byDensity = [...alphabetical].sort((a, b) => {
|
|
1631
|
+
const da = density(a.entry.exports.length, a.entry.loc);
|
|
1632
|
+
const db = density(b.entry.exports.length, b.entry.loc);
|
|
1633
|
+
if (da !== db) return db - da;
|
|
1634
|
+
return a.path.localeCompare(b.path);
|
|
1635
|
+
});
|
|
1636
|
+
const keepSet = /* @__PURE__ */ new Set();
|
|
1637
|
+
let spent = 0;
|
|
1638
|
+
for (const f of byDensity) {
|
|
1639
|
+
const cost = estimateFileEntryTokens(f);
|
|
1640
|
+
if (spent + cost > input.max_tokens && keepSet.size > 0) {
|
|
1641
|
+
budgetClipped = true;
|
|
1642
|
+
break;
|
|
1643
|
+
}
|
|
1644
|
+
keepSet.add(f.path);
|
|
1645
|
+
spent += cost;
|
|
1646
|
+
}
|
|
1647
|
+
if (budgetClipped) {
|
|
1648
|
+
kept = alphabetical.filter((f) => keepSet.has(f.path));
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
const finalFiles = kept.slice(0, input.max_files);
|
|
1652
|
+
const totalDropped = pathsFiltered.length - finalFiles.length;
|
|
1580
1653
|
return {
|
|
1581
1654
|
available: true,
|
|
1582
1655
|
generated_at: map.generated_at,
|
|
1583
1656
|
total_files: Object.keys(map.files).length,
|
|
1584
|
-
files:
|
|
1657
|
+
files: finalFiles.map((f) => ({
|
|
1585
1658
|
path: f.path,
|
|
1586
1659
|
...f.entry.summary ? { summary: f.entry.summary } : {},
|
|
1587
1660
|
loc: f.entry.loc,
|
|
1588
1661
|
exports: f.entry.exports
|
|
1589
|
-
}))
|
|
1662
|
+
})),
|
|
1663
|
+
...totalDropped > 0 ? { truncated: totalDropped } : {},
|
|
1664
|
+
...budgetClipped ? { budget_clipped: true } : {}
|
|
1590
1665
|
};
|
|
1591
1666
|
}
|
|
1667
|
+
function density(exports, loc) {
|
|
1668
|
+
if (loc <= 0) return 0;
|
|
1669
|
+
return exports / Math.max(loc, 1);
|
|
1670
|
+
}
|
|
1671
|
+
function stripLeadingSlash(p) {
|
|
1672
|
+
return p.startsWith("/") ? p.slice(1) : p;
|
|
1673
|
+
}
|
|
1674
|
+
function estimateFileEntryTokens(f) {
|
|
1675
|
+
const exportsCost = f.entry.exports.reduce(
|
|
1676
|
+
(acc, e) => acc + 6 + estimateTokens2(e.description ?? ""),
|
|
1677
|
+
0
|
|
1678
|
+
);
|
|
1679
|
+
return estimateTokens2(f.path) + estimateTokens2(f.entry.summary ?? "") + exportsCost + 4;
|
|
1680
|
+
}
|
|
1592
1681
|
|
|
1593
1682
|
// src/tools/mem-diff.ts
|
|
1594
1683
|
import { existsSync as existsSync18 } from "fs";
|
|
@@ -1635,13 +1724,856 @@ async function memDiff(input, ctx) {
|
|
|
1635
1724
|
};
|
|
1636
1725
|
}
|
|
1637
1726
|
|
|
1638
|
-
// src/
|
|
1727
|
+
// src/tools/get-recap.ts
|
|
1728
|
+
import { existsSync as existsSync19 } from "fs";
|
|
1729
|
+
import { loadMemoriesFromDir as loadMemoriesFromDir15 } from "@hiveai/core";
|
|
1639
1730
|
import { z as z20 } from "zod";
|
|
1731
|
+
var GetRecapInputSchema = {
|
|
1732
|
+
scope: z20.enum(["personal", "team", "any"]).default("any").describe(
|
|
1733
|
+
"Limit to a specific scope's recap. Default 'any' returns the most recent recap across both personal and team scopes."
|
|
1734
|
+
)
|
|
1735
|
+
};
|
|
1736
|
+
async function getRecap(input, ctx) {
|
|
1737
|
+
if (!existsSync19(ctx.paths.memoriesDir)) {
|
|
1738
|
+
return { recap: null, notice: "No .ai/memories directory \u2014 haive not initialized here." };
|
|
1739
|
+
}
|
|
1740
|
+
const all = await loadMemoriesFromDir15(ctx.paths.memoriesDir);
|
|
1741
|
+
const recaps = all.filter(({ memory }) => memory.frontmatter.type === "session_recap").filter(({ memory }) => input.scope === "any" || memory.frontmatter.scope === input.scope).sort(
|
|
1742
|
+
(a, b) => new Date(b.memory.frontmatter.created_at).getTime() - new Date(a.memory.frontmatter.created_at).getTime()
|
|
1743
|
+
);
|
|
1744
|
+
if (recaps.length === 0) {
|
|
1745
|
+
return {
|
|
1746
|
+
recap: null,
|
|
1747
|
+
notice: input.scope === "any" ? "No session recap saved yet. Run mem_session_end (or post_task prompt) to capture one." : `No session recap found in scope '${input.scope}'.`
|
|
1748
|
+
};
|
|
1749
|
+
}
|
|
1750
|
+
const r = recaps[0];
|
|
1751
|
+
const fm = r.memory.frontmatter;
|
|
1752
|
+
return {
|
|
1753
|
+
recap: {
|
|
1754
|
+
id: fm.id,
|
|
1755
|
+
scope: fm.scope,
|
|
1756
|
+
revision_count: fm.revision_count ?? 0,
|
|
1757
|
+
created_at: fm.created_at,
|
|
1758
|
+
body: r.memory.body
|
|
1759
|
+
}
|
|
1760
|
+
};
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
// src/tools/mem-relevant-to.ts
|
|
1764
|
+
import { z as z21 } from "zod";
|
|
1765
|
+
var MemRelevantToInputSchema = {
|
|
1766
|
+
task: z21.string().min(1).describe("What you are about to do, in 1\u20132 sentences. Used to rank relevant memories."),
|
|
1767
|
+
files: z21.array(z21.string()).default([]).describe("Optional: files you are about to edit \u2014 surfaces anchored memories."),
|
|
1768
|
+
limit: z21.number().int().positive().max(30).default(8).describe("Cap on returned memories."),
|
|
1769
|
+
min_semantic_score: z21.number().min(0).max(1).default(0.25).describe("Drop weakly-related semantic hits below this cosine threshold."),
|
|
1770
|
+
format: z21.enum(["full", "compact"]).default("full").describe("'compact' = id + 1-line summary; 'full' = complete bodies.")
|
|
1771
|
+
};
|
|
1772
|
+
async function memRelevantTo(input, ctx) {
|
|
1773
|
+
const briefingInput = {
|
|
1774
|
+
task: input.task,
|
|
1775
|
+
files: input.files,
|
|
1776
|
+
max_tokens: 16e3,
|
|
1777
|
+
max_memories: input.limit,
|
|
1778
|
+
include_project_context: false,
|
|
1779
|
+
include_module_contexts: false,
|
|
1780
|
+
semantic: true,
|
|
1781
|
+
include_stale: false,
|
|
1782
|
+
track: true,
|
|
1783
|
+
format: input.format,
|
|
1784
|
+
symbols: [],
|
|
1785
|
+
min_semantic_score: input.min_semantic_score
|
|
1786
|
+
};
|
|
1787
|
+
const briefing = await getBriefing(briefingInput, ctx);
|
|
1788
|
+
const out = {
|
|
1789
|
+
task: input.task,
|
|
1790
|
+
search_mode: briefing.search_mode,
|
|
1791
|
+
memories: briefing.memories
|
|
1792
|
+
};
|
|
1793
|
+
if (briefing.hints && briefing.hints.length > 0) out.hints = briefing.hints;
|
|
1794
|
+
if (briefing.memories.length === 0) out.empty = true;
|
|
1795
|
+
return out;
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
// src/tools/code-search.ts
|
|
1799
|
+
import { z as z22 } from "zod";
|
|
1800
|
+
var CodeSearchInputSchema = {
|
|
1801
|
+
query: z22.string().min(1).describe(
|
|
1802
|
+
"Natural-language description of what you are looking for in the codebase (e.g. 'function that hashes passwords', 'JWT signing logic', 'route registration')."
|
|
1803
|
+
),
|
|
1804
|
+
k: z22.number().int().positive().max(50).default(5).describe("Number of top hits to return."),
|
|
1805
|
+
min_score: z22.number().min(0).max(1).default(0.2).describe(
|
|
1806
|
+
"Minimum cosine similarity. Hits below this threshold are dropped to avoid noise. Try 0.3+ for stricter matching."
|
|
1807
|
+
)
|
|
1808
|
+
};
|
|
1809
|
+
async function codeSearch(input, ctx) {
|
|
1810
|
+
let mod;
|
|
1811
|
+
try {
|
|
1812
|
+
mod = await import("@hiveai/embeddings");
|
|
1813
|
+
} catch {
|
|
1814
|
+
return {
|
|
1815
|
+
available: false,
|
|
1816
|
+
hits: [],
|
|
1817
|
+
notice: "@hiveai/embeddings is not installed. Install it (`pnpm add @hiveai/embeddings`) and run `haive index code-search` to enable semantic code search."
|
|
1818
|
+
};
|
|
1819
|
+
}
|
|
1820
|
+
const result = await mod.codeSemanticSearch(ctx.paths, input.query, {
|
|
1821
|
+
limit: input.k,
|
|
1822
|
+
minScore: input.min_score
|
|
1823
|
+
});
|
|
1824
|
+
if (!result) {
|
|
1825
|
+
return {
|
|
1826
|
+
available: false,
|
|
1827
|
+
hits: [],
|
|
1828
|
+
notice: "Code semantic-search index not built. Run `haive index code-search` to generate it (builds embeddings for every exported symbol in the code-map)."
|
|
1829
|
+
};
|
|
1830
|
+
}
|
|
1831
|
+
return { available: true, hits: result.hits };
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
// src/tools/why-this-file.ts
|
|
1835
|
+
import { existsSync as existsSync20 } from "fs";
|
|
1836
|
+
import { spawn } from "child_process";
|
|
1837
|
+
import path9 from "path";
|
|
1838
|
+
import {
|
|
1839
|
+
deriveConfidence as deriveConfidence5,
|
|
1840
|
+
getUsage as getUsage6,
|
|
1841
|
+
loadCodeMap as loadCodeMap3,
|
|
1842
|
+
loadMemoriesFromDir as loadMemoriesFromDir16,
|
|
1843
|
+
loadUsageIndex as loadUsageIndex8,
|
|
1844
|
+
memoryMatchesAnchorPaths as memoryMatchesAnchorPaths3
|
|
1845
|
+
} from "@hiveai/core";
|
|
1846
|
+
import { z as z23 } from "zod";
|
|
1847
|
+
var WhyThisFileInputSchema = {
|
|
1848
|
+
path: z23.string().min(1).describe(
|
|
1849
|
+
"Project-relative path to the file you want context on (e.g. 'packages/mcp/src/tools/mem-save.ts')."
|
|
1850
|
+
),
|
|
1851
|
+
git_log_limit: z23.number().int().positive().max(20).default(5).describe("How many recent commits touching this file to include."),
|
|
1852
|
+
memory_limit: z23.number().int().positive().max(20).default(5).describe("Cap on memories anchored to this path.")
|
|
1853
|
+
};
|
|
1854
|
+
async function whyThisFile(input, ctx) {
|
|
1855
|
+
const fileExists = existsSync20(path9.join(ctx.paths.root, input.path));
|
|
1856
|
+
const [commits, memories, codeMap] = await Promise.all([
|
|
1857
|
+
runGitLog(ctx.paths.root, input.path, input.git_log_limit).catch(() => []),
|
|
1858
|
+
collectAnchoredMemories(ctx, input.path, input.memory_limit),
|
|
1859
|
+
loadCodeMap3(ctx.paths)
|
|
1860
|
+
]);
|
|
1861
|
+
const codeMapEntry = codeMap?.files[input.path];
|
|
1862
|
+
const hints = [];
|
|
1863
|
+
if (!fileExists) {
|
|
1864
|
+
hints.push(`File '${input.path}' does not exist on disk \u2014 path may be wrong or file removed.`);
|
|
1865
|
+
}
|
|
1866
|
+
if (commits.length === 0 && fileExists) {
|
|
1867
|
+
hints.push("No git history found \u2014 file may be untracked or git not initialized.");
|
|
1868
|
+
}
|
|
1869
|
+
if (memories.length === 0 && fileExists) {
|
|
1870
|
+
hints.push(
|
|
1871
|
+
"No memories anchored here. If you discover something non-obvious while editing, use mem_observe (with where=" + input.path + ") to capture it."
|
|
1872
|
+
);
|
|
1873
|
+
}
|
|
1874
|
+
if (memories.some((m) => m.type === "attempt" || m.type === "gotcha")) {
|
|
1875
|
+
hints.push("\u26A0\uFE0F attempt/gotcha memories anchored to this file \u2014 read them BEFORE editing.");
|
|
1876
|
+
}
|
|
1877
|
+
return {
|
|
1878
|
+
file: input.path,
|
|
1879
|
+
exists: fileExists,
|
|
1880
|
+
recent_commits: commits,
|
|
1881
|
+
memories,
|
|
1882
|
+
code_map_entry: codeMapEntry ? {
|
|
1883
|
+
...codeMapEntry.summary ? { summary: codeMapEntry.summary } : {},
|
|
1884
|
+
loc: codeMapEntry.loc,
|
|
1885
|
+
exports: codeMapEntry.exports.map((e) => ({
|
|
1886
|
+
name: e.name,
|
|
1887
|
+
kind: e.kind,
|
|
1888
|
+
line: e.line,
|
|
1889
|
+
...e.description ? { description: e.description } : {}
|
|
1890
|
+
}))
|
|
1891
|
+
} : null,
|
|
1892
|
+
...hints.length > 0 ? { hints } : {}
|
|
1893
|
+
};
|
|
1894
|
+
}
|
|
1895
|
+
async function collectAnchoredMemories(ctx, filePath, limit) {
|
|
1896
|
+
if (!existsSync20(ctx.paths.memoriesDir)) return [];
|
|
1897
|
+
const all = await loadMemoriesFromDir16(ctx.paths.memoriesDir);
|
|
1898
|
+
const usage = await loadUsageIndex8(ctx.paths);
|
|
1899
|
+
const out = [];
|
|
1900
|
+
for (const { memory } of all) {
|
|
1901
|
+
const fm = memory.frontmatter;
|
|
1902
|
+
if (fm.status === "rejected" || fm.status === "deprecated") continue;
|
|
1903
|
+
if (fm.type === "session_recap") continue;
|
|
1904
|
+
if (!memoryMatchesAnchorPaths3(memory, [filePath])) continue;
|
|
1905
|
+
const u = getUsage6(usage, fm.id);
|
|
1906
|
+
out.push({
|
|
1907
|
+
id: fm.id,
|
|
1908
|
+
type: fm.type,
|
|
1909
|
+
scope: fm.scope,
|
|
1910
|
+
confidence: deriveConfidence5(fm, u),
|
|
1911
|
+
body_preview: memory.body.split("\n").slice(0, 6).join("\n")
|
|
1912
|
+
});
|
|
1913
|
+
if (out.length >= limit) break;
|
|
1914
|
+
}
|
|
1915
|
+
return out;
|
|
1916
|
+
}
|
|
1917
|
+
async function runGitLog(cwd, filePath, limit) {
|
|
1918
|
+
const sep = "<<HV>>";
|
|
1919
|
+
const fmt = `%h${sep}%an${sep}%ar${sep}%s`;
|
|
1920
|
+
const output = await runCommand(
|
|
1921
|
+
"git",
|
|
1922
|
+
["log", `-n`, String(limit), `--pretty=format:${fmt}`, "--", filePath],
|
|
1923
|
+
cwd
|
|
1924
|
+
);
|
|
1925
|
+
if (!output.trim()) return [];
|
|
1926
|
+
return output.split("\n").map((line) => {
|
|
1927
|
+
const [sha = "", author = "", relative_date = "", subject = ""] = line.split(sep);
|
|
1928
|
+
return { sha, author, relative_date, subject };
|
|
1929
|
+
}).filter((c) => c.sha);
|
|
1930
|
+
}
|
|
1931
|
+
function runCommand(cmd, args, cwd) {
|
|
1932
|
+
return new Promise((resolve, reject) => {
|
|
1933
|
+
const proc = spawn(cmd, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
1934
|
+
let stdout = "";
|
|
1935
|
+
let stderr = "";
|
|
1936
|
+
proc.stdout.on("data", (chunk) => {
|
|
1937
|
+
stdout += chunk.toString();
|
|
1938
|
+
});
|
|
1939
|
+
proc.stderr.on("data", (chunk) => {
|
|
1940
|
+
stderr += chunk.toString();
|
|
1941
|
+
});
|
|
1942
|
+
proc.on("error", reject);
|
|
1943
|
+
proc.on("close", (code) => {
|
|
1944
|
+
if (code === 0) resolve(stdout);
|
|
1945
|
+
else reject(new Error(stderr || `${cmd} exited with code ${code}`));
|
|
1946
|
+
});
|
|
1947
|
+
});
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
// src/tools/anti-patterns-check.ts
|
|
1951
|
+
import { existsSync as existsSync21 } from "fs";
|
|
1952
|
+
import {
|
|
1953
|
+
deriveConfidence as deriveConfidence6,
|
|
1954
|
+
getUsage as getUsage7,
|
|
1955
|
+
loadMemoriesFromDir as loadMemoriesFromDir17,
|
|
1956
|
+
loadUsageIndex as loadUsageIndex9,
|
|
1957
|
+
literalMatchesAnyToken as literalMatchesAnyToken3,
|
|
1958
|
+
memoryMatchesAnchorPaths as memoryMatchesAnchorPaths4,
|
|
1959
|
+
tokenizeQuery as tokenizeQuery3
|
|
1960
|
+
} from "@hiveai/core";
|
|
1961
|
+
import { z as z24 } from "zod";
|
|
1962
|
+
var AntiPatternsCheckInputSchema = {
|
|
1963
|
+
diff: z24.string().optional().describe(
|
|
1964
|
+
"Raw unified diff text (or any code/text snippet) to scan for previously documented anti-patterns. Tokens from the diff are used to match memory bodies and the embeddings index."
|
|
1965
|
+
),
|
|
1966
|
+
paths: z24.array(z24.string()).default([]).describe(
|
|
1967
|
+
"File paths affected by the change. Memories anchored to any of these paths are surfaced regardless of the diff content."
|
|
1968
|
+
),
|
|
1969
|
+
limit: z24.number().int().positive().max(20).default(8).describe("Cap on returned warnings."),
|
|
1970
|
+
semantic: z24.boolean().default(true).describe(
|
|
1971
|
+
"When true, also use semantic search (requires @hiveai/embeddings + memory index) to find related anti-patterns."
|
|
1972
|
+
)
|
|
1973
|
+
};
|
|
1974
|
+
async function antiPatternsCheck(input, ctx) {
|
|
1975
|
+
if (!input.diff && input.paths.length === 0) {
|
|
1976
|
+
return {
|
|
1977
|
+
scanned: 0,
|
|
1978
|
+
warnings: [],
|
|
1979
|
+
notice: "Nothing to check \u2014 provide either `diff` text or `paths`."
|
|
1980
|
+
};
|
|
1981
|
+
}
|
|
1982
|
+
if (!existsSync21(ctx.paths.memoriesDir)) {
|
|
1983
|
+
return { scanned: 0, warnings: [], notice: "No .ai/memories directory \u2014 nothing to check against." };
|
|
1984
|
+
}
|
|
1985
|
+
const all = await loadMemoriesFromDir17(ctx.paths.memoriesDir);
|
|
1986
|
+
const negative = all.filter(({ memory }) => {
|
|
1987
|
+
const t = memory.frontmatter.type;
|
|
1988
|
+
if (t !== "attempt" && t !== "gotcha") return false;
|
|
1989
|
+
const s = memory.frontmatter.status;
|
|
1990
|
+
return s !== "rejected" && s !== "deprecated" && s !== "stale";
|
|
1991
|
+
});
|
|
1992
|
+
if (negative.length === 0) {
|
|
1993
|
+
return { scanned: 0, warnings: [], notice: "No attempt/gotcha memories found yet." };
|
|
1994
|
+
}
|
|
1995
|
+
const usage = await loadUsageIndex9(ctx.paths);
|
|
1996
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1997
|
+
const upsert = (fm, body, reason, score) => {
|
|
1998
|
+
const existing = seen.get(fm.id);
|
|
1999
|
+
if (existing) {
|
|
2000
|
+
if (!existing.reasons.includes(reason)) existing.reasons.push(reason);
|
|
2001
|
+
if (score !== void 0 && (existing.semantic_score ?? 0) < score) {
|
|
2002
|
+
existing.semantic_score = score;
|
|
2003
|
+
}
|
|
2004
|
+
return;
|
|
2005
|
+
}
|
|
2006
|
+
const u = getUsage7(usage, fm.id);
|
|
2007
|
+
seen.set(fm.id, {
|
|
2008
|
+
id: fm.id,
|
|
2009
|
+
type: fm.type,
|
|
2010
|
+
scope: fm.scope,
|
|
2011
|
+
confidence: deriveConfidence6(fm, u),
|
|
2012
|
+
body_preview: body.split("\n").slice(0, 5).join("\n").slice(0, 400),
|
|
2013
|
+
reasons: [reason],
|
|
2014
|
+
...score !== void 0 ? { semantic_score: score } : {}
|
|
2015
|
+
});
|
|
2016
|
+
};
|
|
2017
|
+
if (input.paths.length > 0) {
|
|
2018
|
+
for (const { memory } of negative) {
|
|
2019
|
+
if (memoryMatchesAnchorPaths4(memory, input.paths)) {
|
|
2020
|
+
upsert(memory.frontmatter, memory.body, "anchor");
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
if (input.diff) {
|
|
2025
|
+
const tokens = tokenizeQuery3(input.diff);
|
|
2026
|
+
if (tokens.length > 0) {
|
|
2027
|
+
for (const { memory } of negative) {
|
|
2028
|
+
if (literalMatchesAnyToken3(memory, tokens)) {
|
|
2029
|
+
upsert(memory.frontmatter, memory.body, "literal");
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
if (input.semantic && input.diff) {
|
|
2035
|
+
try {
|
|
2036
|
+
const mod = await import("@hiveai/embeddings");
|
|
2037
|
+
const result = await mod.semanticSearch(ctx.paths, input.diff, { limit: input.limit * 2 });
|
|
2038
|
+
if (result) {
|
|
2039
|
+
const negativeIds = new Set(negative.map(({ memory }) => memory.frontmatter.id));
|
|
2040
|
+
for (const hit of result.hits) {
|
|
2041
|
+
if (!negativeIds.has(hit.id)) continue;
|
|
2042
|
+
const found = negative.find(({ memory }) => memory.frontmatter.id === hit.id);
|
|
2043
|
+
if (found) upsert(found.memory.frontmatter, found.memory.body, "semantic", hit.score);
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
} catch {
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
const warnings = [...seen.values()].sort((a, b) => {
|
|
2050
|
+
const score = (w) => {
|
|
2051
|
+
const reasonW = (w.reasons.includes("anchor") ? 4 : 0) + (w.reasons.includes("literal") ? 2 : 0) + (w.reasons.includes("semantic") ? 1 : 0);
|
|
2052
|
+
const confW = w.confidence === "authoritative" ? 3 : w.confidence === "trusted" ? 2 : w.confidence === "low" ? 1 : 0;
|
|
2053
|
+
return reasonW + confW + (w.semantic_score ?? 0);
|
|
2054
|
+
};
|
|
2055
|
+
return score(b) - score(a);
|
|
2056
|
+
}).slice(0, input.limit);
|
|
2057
|
+
return {
|
|
2058
|
+
scanned: negative.length,
|
|
2059
|
+
warnings
|
|
2060
|
+
};
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
// src/tools/mem-distill.ts
|
|
2064
|
+
import { existsSync as existsSync22 } from "fs";
|
|
2065
|
+
import {
|
|
2066
|
+
loadMemoriesFromDir as loadMemoriesFromDir18,
|
|
2067
|
+
tokenizeQuery as tokenizeQuery4
|
|
2068
|
+
} from "@hiveai/core";
|
|
2069
|
+
import { z as z25 } from "zod";
|
|
2070
|
+
var MemDistillInputSchema = {
|
|
2071
|
+
since_days: z25.number().int().positive().default(30).describe("Only consider memories created in the last N days."),
|
|
2072
|
+
min_cluster: z25.number().int().min(2).default(3).describe("Minimum cluster size to surface."),
|
|
2073
|
+
type_filter: z25.enum(["gotcha", "attempt", "all"]).default("gotcha").describe(
|
|
2074
|
+
"Memory type to scan. 'gotcha' targets observe-style discoveries that recur, 'attempt' surfaces failed approaches that repeat, 'all' considers both."
|
|
2075
|
+
),
|
|
2076
|
+
scope: z25.enum(["personal", "team", "module", "any"]).default("any").describe("Restrict to a specific scope.")
|
|
2077
|
+
};
|
|
2078
|
+
var MS_PER_DAY = 24 * 60 * 60 * 1e3;
|
|
2079
|
+
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
2080
|
+
"the",
|
|
2081
|
+
"and",
|
|
2082
|
+
"for",
|
|
2083
|
+
"with",
|
|
2084
|
+
"that",
|
|
2085
|
+
"this",
|
|
2086
|
+
"from",
|
|
2087
|
+
"into",
|
|
2088
|
+
"when",
|
|
2089
|
+
"then",
|
|
2090
|
+
"also",
|
|
2091
|
+
"must",
|
|
2092
|
+
"have",
|
|
2093
|
+
"does",
|
|
2094
|
+
"not",
|
|
2095
|
+
"but",
|
|
2096
|
+
"you",
|
|
2097
|
+
"your",
|
|
2098
|
+
"its",
|
|
2099
|
+
"because",
|
|
2100
|
+
"why",
|
|
2101
|
+
"how",
|
|
2102
|
+
"what",
|
|
2103
|
+
"use",
|
|
2104
|
+
"using",
|
|
2105
|
+
"used",
|
|
2106
|
+
"add",
|
|
2107
|
+
"added",
|
|
2108
|
+
"make",
|
|
2109
|
+
"made",
|
|
2110
|
+
"fix",
|
|
2111
|
+
"fixed",
|
|
2112
|
+
"bug",
|
|
2113
|
+
"error"
|
|
2114
|
+
]);
|
|
2115
|
+
async function memDistill(input, ctx) {
|
|
2116
|
+
if (!existsSync22(ctx.paths.memoriesDir)) {
|
|
2117
|
+
return { scanned: 0, singletons: 0, clusters: [], notice: "No .ai/memories directory." };
|
|
2118
|
+
}
|
|
2119
|
+
const cutoff = Date.now() - input.since_days * MS_PER_DAY;
|
|
2120
|
+
const all = await loadMemoriesFromDir18(ctx.paths.memoriesDir);
|
|
2121
|
+
const candidates = all.filter(({ memory }) => {
|
|
2122
|
+
const fm = memory.frontmatter;
|
|
2123
|
+
if (fm.status === "rejected" || fm.status === "deprecated" || fm.status === "stale") return false;
|
|
2124
|
+
if (input.scope !== "any" && fm.scope !== input.scope) return false;
|
|
2125
|
+
if (input.type_filter === "gotcha" && fm.type !== "gotcha") return false;
|
|
2126
|
+
if (input.type_filter === "attempt" && fm.type !== "attempt") return false;
|
|
2127
|
+
if (input.type_filter === "all" && fm.type !== "gotcha" && fm.type !== "attempt") return false;
|
|
2128
|
+
if (Date.parse(fm.created_at) < cutoff) return false;
|
|
2129
|
+
return true;
|
|
2130
|
+
});
|
|
2131
|
+
if (candidates.length < input.min_cluster) {
|
|
2132
|
+
return {
|
|
2133
|
+
scanned: candidates.length,
|
|
2134
|
+
singletons: candidates.length,
|
|
2135
|
+
clusters: [],
|
|
2136
|
+
notice: candidates.length === 0 ? `No matching memories in the last ${input.since_days} days.` : `Only ${candidates.length} candidate${candidates.length === 1 ? "" : "s"} \u2014 below min_cluster=${input.min_cluster}.`
|
|
2137
|
+
};
|
|
2138
|
+
}
|
|
2139
|
+
const features = candidates.map((loaded) => ({
|
|
2140
|
+
loaded,
|
|
2141
|
+
keywords: keywordSet(loaded),
|
|
2142
|
+
paths: new Set(loaded.memory.frontmatter.anchor.paths)
|
|
2143
|
+
}));
|
|
2144
|
+
const parent = features.map((_, i) => i);
|
|
2145
|
+
const find = (i) => parent[i] === i ? i : parent[i] = find(parent[i] ?? 0);
|
|
2146
|
+
const union = (a, b) => {
|
|
2147
|
+
const ra = find(a), rb = find(b);
|
|
2148
|
+
if (ra !== rb) parent[ra] = rb;
|
|
2149
|
+
};
|
|
2150
|
+
for (let i = 0; i < features.length; i++) {
|
|
2151
|
+
for (let j = i + 1; j < features.length; j++) {
|
|
2152
|
+
const fi = features[i], fj = features[j];
|
|
2153
|
+
const pathSim = jaccard(fi.paths, fj.paths);
|
|
2154
|
+
const kwSim = jaccard(fi.keywords, fj.keywords);
|
|
2155
|
+
if (pathSim >= 0.5 || kwSim >= 0.4) union(i, j);
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2159
|
+
for (let i = 0; i < features.length; i++) {
|
|
2160
|
+
const root = find(i);
|
|
2161
|
+
const arr = groups.get(root) ?? [];
|
|
2162
|
+
arr.push(i);
|
|
2163
|
+
groups.set(root, arr);
|
|
2164
|
+
}
|
|
2165
|
+
const clusters = [];
|
|
2166
|
+
let singletons = 0;
|
|
2167
|
+
for (const indices of groups.values()) {
|
|
2168
|
+
if (indices.length < input.min_cluster) {
|
|
2169
|
+
singletons += indices.length;
|
|
2170
|
+
continue;
|
|
2171
|
+
}
|
|
2172
|
+
const members = indices.map((i) => features[i]);
|
|
2173
|
+
const allPaths = /* @__PURE__ */ new Set();
|
|
2174
|
+
const allKeywords = /* @__PURE__ */ new Map();
|
|
2175
|
+
let latest = 0;
|
|
2176
|
+
for (const m of members) {
|
|
2177
|
+
for (const p of m.paths) allPaths.add(p);
|
|
2178
|
+
for (const k of m.keywords) allKeywords.set(k, (allKeywords.get(k) ?? 0) + 1);
|
|
2179
|
+
const t = Date.parse(m.loaded.memory.frontmatter.created_at);
|
|
2180
|
+
if (t > latest) latest = t;
|
|
2181
|
+
}
|
|
2182
|
+
const commonKeywords = [...allKeywords.entries()].filter(([, n]) => n >= Math.max(2, Math.floor(members.length / 2))).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([k]) => k);
|
|
2183
|
+
const titles = members.map((m) => firstHeading(m.loaded.memory.body) ?? m.loaded.memory.frontmatter.id).slice(0, 5);
|
|
2184
|
+
const suggestedType = members.every((m) => m.loaded.memory.frontmatter.type === "attempt") ? "gotcha" : "convention";
|
|
2185
|
+
clusters.push({
|
|
2186
|
+
suggested_topic: commonKeywords.slice(0, 3).join("-") || "merged-observations",
|
|
2187
|
+
suggested_type: suggestedType,
|
|
2188
|
+
member_ids: members.map((m) => m.loaded.memory.frontmatter.id),
|
|
2189
|
+
overlapping_paths: [...allPaths].slice(0, 10),
|
|
2190
|
+
common_keywords: commonKeywords,
|
|
2191
|
+
sample_titles: titles,
|
|
2192
|
+
latest_at: new Date(latest).toISOString()
|
|
2193
|
+
});
|
|
2194
|
+
}
|
|
2195
|
+
clusters.sort((a, b) => b.member_ids.length - a.member_ids.length);
|
|
2196
|
+
return {
|
|
2197
|
+
scanned: candidates.length,
|
|
2198
|
+
singletons,
|
|
2199
|
+
clusters
|
|
2200
|
+
};
|
|
2201
|
+
}
|
|
2202
|
+
function keywordSet(loaded) {
|
|
2203
|
+
const text = (loaded.memory.body + " " + loaded.memory.frontmatter.tags.join(" ")).slice(0, 800);
|
|
2204
|
+
const tokens = tokenizeQuery4(text).filter((t) => t.length >= 4 && !STOP_WORDS.has(t));
|
|
2205
|
+
return new Set(tokens);
|
|
2206
|
+
}
|
|
2207
|
+
function jaccard(a, b) {
|
|
2208
|
+
if (a.size === 0 && b.size === 0) return 0;
|
|
2209
|
+
let intersect = 0;
|
|
2210
|
+
for (const x of a) if (b.has(x)) intersect++;
|
|
2211
|
+
const union = a.size + b.size - intersect;
|
|
2212
|
+
return union === 0 ? 0 : intersect / union;
|
|
2213
|
+
}
|
|
2214
|
+
function firstHeading(body) {
|
|
2215
|
+
for (const line of body.split("\n")) {
|
|
2216
|
+
const t = line.trim();
|
|
2217
|
+
if (t.startsWith("#")) return t.replace(/^#+\s*/, "").slice(0, 80);
|
|
2218
|
+
if (t.length > 0) return t.slice(0, 80);
|
|
2219
|
+
}
|
|
2220
|
+
return void 0;
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
// src/tools/why-this-decision.ts
|
|
2224
|
+
import { existsSync as existsSync23 } from "fs";
|
|
2225
|
+
import { spawn as spawn2 } from "child_process";
|
|
2226
|
+
import {
|
|
2227
|
+
deriveConfidence as deriveConfidence7,
|
|
2228
|
+
getUsage as getUsage8,
|
|
2229
|
+
loadMemoriesFromDir as loadMemoriesFromDir19,
|
|
2230
|
+
loadUsageIndex as loadUsageIndex10,
|
|
2231
|
+
pathsOverlap as singlePathsOverlap
|
|
2232
|
+
} from "@hiveai/core";
|
|
2233
|
+
import { z as z26 } from "zod";
|
|
2234
|
+
var WhyThisDecisionInputSchema = {
|
|
2235
|
+
id: z26.string().min(1).describe("Memory id to inspect (e.g. '2026-04-25-decision-esm-only')."),
|
|
2236
|
+
git_log_limit: z26.number().int().positive().max(20).default(5).describe("How many recent commits per anchor path to surface.")
|
|
2237
|
+
};
|
|
2238
|
+
async function whyThisDecision(input, ctx) {
|
|
2239
|
+
if (!existsSync23(ctx.paths.memoriesDir)) {
|
|
2240
|
+
return {
|
|
2241
|
+
found: false,
|
|
2242
|
+
related: [],
|
|
2243
|
+
path_neighbors: [],
|
|
2244
|
+
recent_commits: [],
|
|
2245
|
+
notice: "No .ai/memories directory."
|
|
2246
|
+
};
|
|
2247
|
+
}
|
|
2248
|
+
const all = await loadMemoriesFromDir19(ctx.paths.memoriesDir);
|
|
2249
|
+
const usage = await loadUsageIndex10(ctx.paths);
|
|
2250
|
+
const target = all.find(({ memory }) => memory.frontmatter.id === input.id);
|
|
2251
|
+
if (!target) {
|
|
2252
|
+
return {
|
|
2253
|
+
found: false,
|
|
2254
|
+
related: [],
|
|
2255
|
+
path_neighbors: [],
|
|
2256
|
+
recent_commits: [],
|
|
2257
|
+
notice: `Memory '${input.id}' not found.`
|
|
2258
|
+
};
|
|
2259
|
+
}
|
|
2260
|
+
const fm = target.memory.frontmatter;
|
|
2261
|
+
const targetUsage = getUsage8(usage, fm.id);
|
|
2262
|
+
const decision = {
|
|
2263
|
+
id: fm.id,
|
|
2264
|
+
type: fm.type,
|
|
2265
|
+
scope: fm.scope,
|
|
2266
|
+
status: fm.status,
|
|
2267
|
+
confidence: deriveConfidence7(fm, targetUsage),
|
|
2268
|
+
body: target.memory.body,
|
|
2269
|
+
created_at: fm.created_at
|
|
2270
|
+
};
|
|
2271
|
+
const relatedSet = new Set(fm.related_ids ?? []);
|
|
2272
|
+
const related = [];
|
|
2273
|
+
for (const { memory } of all) {
|
|
2274
|
+
if (memory.frontmatter.id === fm.id) continue;
|
|
2275
|
+
const isExplicit = relatedSet.has(memory.frontmatter.id);
|
|
2276
|
+
const isBackLink = (memory.frontmatter.related_ids ?? []).includes(fm.id);
|
|
2277
|
+
if (!isExplicit && !isBackLink) continue;
|
|
2278
|
+
const u = getUsage8(usage, memory.frontmatter.id);
|
|
2279
|
+
related.push({
|
|
2280
|
+
id: memory.frontmatter.id,
|
|
2281
|
+
type: memory.frontmatter.type,
|
|
2282
|
+
scope: memory.frontmatter.scope,
|
|
2283
|
+
confidence: deriveConfidence7(memory.frontmatter, u),
|
|
2284
|
+
body_preview: memory.body.split("\n").slice(0, 4).join("\n").slice(0, 300),
|
|
2285
|
+
relation: isExplicit ? "explicit" : "back-link"
|
|
2286
|
+
});
|
|
2287
|
+
}
|
|
2288
|
+
const targetPaths = fm.anchor.paths;
|
|
2289
|
+
const path_neighbors = [];
|
|
2290
|
+
if (targetPaths.length > 0) {
|
|
2291
|
+
for (const { memory } of all) {
|
|
2292
|
+
if (memory.frontmatter.id === fm.id) continue;
|
|
2293
|
+
if (relatedSet.has(memory.frontmatter.id)) continue;
|
|
2294
|
+
const overlappingPaths = memory.frontmatter.anchor.paths.filter(
|
|
2295
|
+
(p) => targetPaths.some((tp) => singlePathsOverlap(p, tp))
|
|
2296
|
+
);
|
|
2297
|
+
if (overlappingPaths.length === 0) continue;
|
|
2298
|
+
const u = getUsage8(usage, memory.frontmatter.id);
|
|
2299
|
+
path_neighbors.push({
|
|
2300
|
+
id: memory.frontmatter.id,
|
|
2301
|
+
type: memory.frontmatter.type,
|
|
2302
|
+
scope: memory.frontmatter.scope,
|
|
2303
|
+
confidence: deriveConfidence7(memory.frontmatter, u),
|
|
2304
|
+
overlap: overlappingPaths,
|
|
2305
|
+
body_preview: memory.body.split("\n").slice(0, 3).join("\n").slice(0, 200)
|
|
2306
|
+
});
|
|
2307
|
+
if (path_neighbors.length >= 10) break;
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
const recent_commits = [];
|
|
2311
|
+
for (const p of targetPaths.slice(0, 5)) {
|
|
2312
|
+
try {
|
|
2313
|
+
const commits = await runGitLog2(ctx.paths.root, p, input.git_log_limit);
|
|
2314
|
+
for (const c of commits) recent_commits.push({ path: p, ...c });
|
|
2315
|
+
} catch {
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
const hints = [];
|
|
2319
|
+
if (decision.confidence === "low" || decision.confidence === "stale") {
|
|
2320
|
+
hints.push(`\u26A0\uFE0F Confidence is ${decision.confidence}. Verify this decision still applies before quoting it.`);
|
|
2321
|
+
}
|
|
2322
|
+
if (related.length === 0 && path_neighbors.length === 0 && targetPaths.length === 0) {
|
|
2323
|
+
hints.push("No related memories and no anchored paths \u2014 this decision is isolated; consider adding related_ids or paths.");
|
|
2324
|
+
}
|
|
2325
|
+
if (fm.type !== "decision" && fm.type !== "architecture") {
|
|
2326
|
+
hints.push(`Memory type is '${fm.type}', not 'decision'/'architecture' \u2014 output may be less informative.`);
|
|
2327
|
+
}
|
|
2328
|
+
return {
|
|
2329
|
+
found: true,
|
|
2330
|
+
decision,
|
|
2331
|
+
related,
|
|
2332
|
+
path_neighbors,
|
|
2333
|
+
recent_commits,
|
|
2334
|
+
...hints.length > 0 ? { hints } : {}
|
|
2335
|
+
};
|
|
2336
|
+
}
|
|
2337
|
+
async function runGitLog2(cwd, filePath, limit) {
|
|
2338
|
+
const sep = "<<HV>>";
|
|
2339
|
+
const fmt = `%h${sep}%an${sep}%ar${sep}%s`;
|
|
2340
|
+
const output = await runCommand2(
|
|
2341
|
+
"git",
|
|
2342
|
+
["log", "-n", String(limit), `--pretty=format:${fmt}`, "--", filePath],
|
|
2343
|
+
cwd
|
|
2344
|
+
);
|
|
2345
|
+
if (!output.trim()) return [];
|
|
2346
|
+
return output.split("\n").map((line) => {
|
|
2347
|
+
const [sha = "", author = "", relative_date = "", subject = ""] = line.split(sep);
|
|
2348
|
+
return { sha, author, relative_date, subject };
|
|
2349
|
+
}).filter((c) => c.sha);
|
|
2350
|
+
}
|
|
2351
|
+
function runCommand2(cmd, args, cwd) {
|
|
2352
|
+
return new Promise((resolve, reject) => {
|
|
2353
|
+
const proc = spawn2(cmd, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
2354
|
+
let stdout = "";
|
|
2355
|
+
let stderr = "";
|
|
2356
|
+
proc.stdout.on("data", (chunk) => {
|
|
2357
|
+
stdout += chunk.toString();
|
|
2358
|
+
});
|
|
2359
|
+
proc.stderr.on("data", (chunk) => {
|
|
2360
|
+
stderr += chunk.toString();
|
|
2361
|
+
});
|
|
2362
|
+
proc.on("error", reject);
|
|
2363
|
+
proc.on("close", (code) => {
|
|
2364
|
+
if (code === 0) resolve(stdout);
|
|
2365
|
+
else reject(new Error(stderr || `${cmd} exited with code ${code}`));
|
|
2366
|
+
});
|
|
2367
|
+
});
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
// src/tools/mem-conflicts.ts
|
|
2371
|
+
import { existsSync as existsSync24 } from "fs";
|
|
2372
|
+
import {
|
|
2373
|
+
deriveConfidence as deriveConfidence8,
|
|
2374
|
+
getUsage as getUsage9,
|
|
2375
|
+
loadMemoriesFromDir as loadMemoriesFromDir20,
|
|
2376
|
+
loadUsageIndex as loadUsageIndex11,
|
|
2377
|
+
pathsOverlap,
|
|
2378
|
+
tokenizeQuery as tokenizeQuery5
|
|
2379
|
+
} from "@hiveai/core";
|
|
2380
|
+
import { z as z27 } from "zod";
|
|
2381
|
+
var MemConflictsInputSchema = {
|
|
2382
|
+
id: z27.string().min(1).describe("Memory id to check for conflicts."),
|
|
2383
|
+
min_score: z27.number().min(0).max(1).default(0.5).describe("Minimum cosine similarity to consider a memory as a potential conflict (semantic mode)."),
|
|
2384
|
+
semantic: z27.boolean().default(true).describe("Use embeddings for similarity. Falls back to keyword overlap when embeddings are not installed.")
|
|
2385
|
+
};
|
|
2386
|
+
var POSITIVE_PATTERNS = /\b(use|prefer|always|should use|do this|recommended|ok to)\b/i;
|
|
2387
|
+
var NEGATIVE_PATTERNS = /\b(do not use|don'?t use|never|avoid|forbidden|deprecated|stop using|do NOT|❌)\b/i;
|
|
2388
|
+
async function memConflicts(input, ctx) {
|
|
2389
|
+
if (!existsSync24(ctx.paths.memoriesDir)) {
|
|
2390
|
+
return { found: false, scanned: 0, conflicts: [], notice: "No .ai/memories directory." };
|
|
2391
|
+
}
|
|
2392
|
+
const all = await loadMemoriesFromDir20(ctx.paths.memoriesDir);
|
|
2393
|
+
const target = all.find(({ memory }) => memory.frontmatter.id === input.id);
|
|
2394
|
+
if (!target) {
|
|
2395
|
+
return { found: false, scanned: 0, conflicts: [], notice: `Memory '${input.id}' not found.` };
|
|
2396
|
+
}
|
|
2397
|
+
const usage = await loadUsageIndex11(ctx.paths);
|
|
2398
|
+
const others = all.filter(
|
|
2399
|
+
({ memory }) => memory.frontmatter.id !== input.id && memory.frontmatter.type !== "session_recap"
|
|
2400
|
+
);
|
|
2401
|
+
const simScores = input.semantic ? await trySemanticSimilarities(ctx, target, others) : null;
|
|
2402
|
+
const targetText = (target.memory.body + " " + target.memory.frontmatter.tags.join(" ")).toLowerCase();
|
|
2403
|
+
const targetTokens = new Set(tokenizeQuery5(targetText));
|
|
2404
|
+
const targetPolarity = polarity(targetText);
|
|
2405
|
+
const targetPaths = target.memory.frontmatter.anchor.paths;
|
|
2406
|
+
const explicitContradicts = extractContradictsTags(target.memory.body);
|
|
2407
|
+
const conflicts = [];
|
|
2408
|
+
for (const other of others) {
|
|
2409
|
+
const fm = other.memory.frontmatter;
|
|
2410
|
+
const otherText = (other.memory.body + " " + fm.tags.join(" ")).toLowerCase();
|
|
2411
|
+
const reasons = [];
|
|
2412
|
+
const sim = simScores?.get(fm.id) ?? null;
|
|
2413
|
+
const hasPathOverlap = fm.anchor.paths.some((p) => targetPaths.some((tp) => pathsOverlap(p, tp)));
|
|
2414
|
+
const otherTokens = new Set(tokenizeQuery5(otherText));
|
|
2415
|
+
const tokenOverlap = countIntersection(targetTokens, otherTokens);
|
|
2416
|
+
const isSemanticNeighbor = sim !== null && sim >= input.min_score;
|
|
2417
|
+
if (!hasPathOverlap && tokenOverlap < 4 && !isSemanticNeighbor) continue;
|
|
2418
|
+
const otherContradicts = extractContradictsTags(other.memory.body);
|
|
2419
|
+
if (explicitContradicts.has(fm.id) || otherContradicts.has(input.id)) {
|
|
2420
|
+
reasons.push("explicit-contradiction-tag");
|
|
2421
|
+
}
|
|
2422
|
+
if (target.memory.frontmatter.status === "validated" && fm.status === "rejected" || target.memory.frontmatter.status === "rejected" && fm.status === "validated") {
|
|
2423
|
+
if (tokenOverlap >= 4 || isSemanticNeighbor) reasons.push("opposite-status");
|
|
2424
|
+
}
|
|
2425
|
+
if (hasPathOverlap) {
|
|
2426
|
+
const tType = target.memory.frontmatter.type;
|
|
2427
|
+
const oType = fm.type;
|
|
2428
|
+
const isAttemptVsRule = tType === "attempt" && (oType === "convention" || oType === "decision") || oType === "attempt" && (tType === "convention" || tType === "decision");
|
|
2429
|
+
if (isAttemptVsRule) reasons.push("attempt-vs-convention-same-paths");
|
|
2430
|
+
}
|
|
2431
|
+
if (isSemanticNeighbor) {
|
|
2432
|
+
const otherPolarity = polarity(otherText);
|
|
2433
|
+
if (targetPolarity === "positive" && otherPolarity === "negative" || targetPolarity === "negative" && otherPolarity === "positive") {
|
|
2434
|
+
reasons.push("polarity-keywords");
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
if (reasons.length === 0) continue;
|
|
2438
|
+
const u = getUsage9(usage, fm.id);
|
|
2439
|
+
conflicts.push({
|
|
2440
|
+
id: fm.id,
|
|
2441
|
+
type: fm.type,
|
|
2442
|
+
scope: fm.scope,
|
|
2443
|
+
status: fm.status,
|
|
2444
|
+
confidence: deriveConfidence8(fm, u),
|
|
2445
|
+
body_preview: other.memory.body.split("\n").slice(0, 4).join("\n").slice(0, 300),
|
|
2446
|
+
similarity: sim,
|
|
2447
|
+
reasons,
|
|
2448
|
+
shared_paths: fm.anchor.paths.filter((p) => targetPaths.some((tp) => pathsOverlap(p, tp)))
|
|
2449
|
+
});
|
|
2450
|
+
}
|
|
2451
|
+
conflicts.sort((a, b) => {
|
|
2452
|
+
const score = (c) => (c.reasons.includes("explicit-contradiction-tag") ? 100 : 0) + (c.reasons.includes("opposite-status") ? 50 : 0) + (c.reasons.includes("attempt-vs-convention-same-paths") ? 25 : 0) + (c.reasons.includes("polarity-keywords") ? 10 : 0) + (c.similarity ?? 0) * 5;
|
|
2453
|
+
return score(b) - score(a);
|
|
2454
|
+
});
|
|
2455
|
+
return {
|
|
2456
|
+
found: true,
|
|
2457
|
+
target: {
|
|
2458
|
+
id: target.memory.frontmatter.id,
|
|
2459
|
+
type: target.memory.frontmatter.type,
|
|
2460
|
+
status: target.memory.frontmatter.status
|
|
2461
|
+
},
|
|
2462
|
+
scanned: others.length,
|
|
2463
|
+
conflicts: conflicts.slice(0, 10)
|
|
2464
|
+
};
|
|
2465
|
+
}
|
|
2466
|
+
function polarity(text) {
|
|
2467
|
+
const neg = NEGATIVE_PATTERNS.test(text);
|
|
2468
|
+
const pos = POSITIVE_PATTERNS.test(text);
|
|
2469
|
+
if (neg && !pos) return "negative";
|
|
2470
|
+
if (pos && !neg) return "positive";
|
|
2471
|
+
return "neutral";
|
|
2472
|
+
}
|
|
2473
|
+
function extractContradictsTags(body) {
|
|
2474
|
+
const out = /* @__PURE__ */ new Set();
|
|
2475
|
+
for (const m of body.matchAll(/#contradicts:([\w-]+)/g)) {
|
|
2476
|
+
if (m[1]) out.add(m[1]);
|
|
2477
|
+
}
|
|
2478
|
+
return out;
|
|
2479
|
+
}
|
|
2480
|
+
function countIntersection(a, b) {
|
|
2481
|
+
let n = 0;
|
|
2482
|
+
for (const x of a) if (b.has(x)) n++;
|
|
2483
|
+
return n;
|
|
2484
|
+
}
|
|
2485
|
+
async function trySemanticSimilarities(ctx, target, others) {
|
|
2486
|
+
let mod;
|
|
2487
|
+
try {
|
|
2488
|
+
mod = await import("@hiveai/embeddings");
|
|
2489
|
+
} catch {
|
|
2490
|
+
return null;
|
|
2491
|
+
}
|
|
2492
|
+
const result = await mod.semanticSearch(
|
|
2493
|
+
ctx.paths,
|
|
2494
|
+
target.memory.body,
|
|
2495
|
+
{ limit: others.length }
|
|
2496
|
+
);
|
|
2497
|
+
if (!result) return null;
|
|
2498
|
+
const map = /* @__PURE__ */ new Map();
|
|
2499
|
+
for (const hit of result.hits) map.set(hit.id, hit.score);
|
|
2500
|
+
return map;
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
// src/tools/precommit-check.ts
|
|
2504
|
+
import { z as z28 } from "zod";
|
|
2505
|
+
var PreCommitCheckInputSchema = {
|
|
2506
|
+
diff: z28.string().optional().describe(
|
|
2507
|
+
"Raw unified diff text to scan. If omitted, only `paths` is used. When called from a pre-commit hook, pipe the output of `git diff --cached`."
|
|
2508
|
+
),
|
|
2509
|
+
paths: z28.array(z28.string()).default([]).describe("Project-relative paths affected by the change. At least one of `diff` or `paths` should be provided."),
|
|
2510
|
+
block_on: z28.enum(["any", "high-confidence", "never"]).default("high-confidence").describe(
|
|
2511
|
+
"When to set should_block=true: 'any' = any warning blocks; 'high-confidence' = only warnings from authoritative/trusted memories block; 'never' = report only, never block."
|
|
2512
|
+
),
|
|
2513
|
+
semantic: z28.boolean().default(true).describe("Enable semantic search in anti_patterns_check (requires embeddings index).")
|
|
2514
|
+
};
|
|
2515
|
+
async function preCommitCheck(input, ctx) {
|
|
2516
|
+
if (!input.diff && input.paths.length === 0) {
|
|
2517
|
+
return {
|
|
2518
|
+
should_block: false,
|
|
2519
|
+
summary: { anti_patterns: 0, relevant_memories: 0, stale_anchors: 0 },
|
|
2520
|
+
warnings: [],
|
|
2521
|
+
relevant_memories: [],
|
|
2522
|
+
stale_anchors: [],
|
|
2523
|
+
notice: "Nothing to check \u2014 provide either `diff` or `paths`."
|
|
2524
|
+
};
|
|
2525
|
+
}
|
|
2526
|
+
const apResult = await antiPatternsCheck({
|
|
2527
|
+
diff: input.diff,
|
|
2528
|
+
paths: input.paths,
|
|
2529
|
+
limit: 20,
|
|
2530
|
+
semantic: input.semantic
|
|
2531
|
+
}, ctx);
|
|
2532
|
+
const relevant = input.paths.length > 0 ? await memForFiles({ files: input.paths, include_module_contexts: false, track: false }, ctx) : { by_anchor: [], by_module: [], by_domain: [], module_contexts: [], inferred_modules: [] };
|
|
2533
|
+
const relevantMatches = [...relevant.by_anchor, ...relevant.by_module];
|
|
2534
|
+
const verifyResult = input.paths.length > 0 ? await memVerify({ update: false, id: void 0 }, ctx) : { results: [], summary: { checked: 0, fresh: 0, stale: 0, anchorless_skipped: 0, updated: 0 } };
|
|
2535
|
+
const filesTouching = new Set(relevantMatches.map((m) => m.id));
|
|
2536
|
+
const staleHits = verifyResult.results.filter((r) => r.stale && filesTouching.has(r.id));
|
|
2537
|
+
const blockOn = input.block_on;
|
|
2538
|
+
let should_block = false;
|
|
2539
|
+
if (blockOn !== "never") {
|
|
2540
|
+
const high = apResult.warnings.filter(
|
|
2541
|
+
(w) => w.confidence === "authoritative" || w.confidence === "trusted"
|
|
2542
|
+
);
|
|
2543
|
+
if (blockOn === "any" && (apResult.warnings.length > 0 || staleHits.length > 0)) should_block = true;
|
|
2544
|
+
if (blockOn === "high-confidence" && (high.length > 0 || staleHits.length > 0)) should_block = true;
|
|
2545
|
+
}
|
|
2546
|
+
const relevant_memories = relevantMatches.slice(0, 8).map((m) => ({
|
|
2547
|
+
id: m.id,
|
|
2548
|
+
type: m.type,
|
|
2549
|
+
confidence: String(m.confidence),
|
|
2550
|
+
body_preview: (m.body ?? "").split("\n").slice(0, 4).join("\n").slice(0, 250)
|
|
2551
|
+
}));
|
|
2552
|
+
return {
|
|
2553
|
+
should_block,
|
|
2554
|
+
summary: {
|
|
2555
|
+
anti_patterns: apResult.warnings.length,
|
|
2556
|
+
relevant_memories: relevant_memories.length,
|
|
2557
|
+
stale_anchors: staleHits.length
|
|
2558
|
+
},
|
|
2559
|
+
warnings: apResult.warnings,
|
|
2560
|
+
relevant_memories,
|
|
2561
|
+
stale_anchors: staleHits.map((r) => ({
|
|
2562
|
+
id: r.id,
|
|
2563
|
+
// The matching `relevantMatches` entry tells us which paths overlap.
|
|
2564
|
+
paths: relevantMatches.find((m) => m.id === r.id) ? input.paths.filter((p) => relevantMatches.some((m) => m.id === r.id)) : [],
|
|
2565
|
+
body_preview: r.reason ?? "anchored code drifted; verify before relying on this memory"
|
|
2566
|
+
}))
|
|
2567
|
+
};
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
// src/prompts/bootstrap-project.ts
|
|
2571
|
+
import { z as z29 } from "zod";
|
|
1640
2572
|
var BootstrapProjectArgsSchema = {
|
|
1641
|
-
module:
|
|
2573
|
+
module: z29.string().optional().describe(
|
|
1642
2574
|
"Optional module name to scope the analysis to (writes to .ai/modules/<module>/context.md)"
|
|
1643
2575
|
),
|
|
1644
|
-
focus:
|
|
2576
|
+
focus: z29.string().optional().describe("Optional area to emphasize (e.g. 'data layer', 'API surface')")
|
|
1645
2577
|
};
|
|
1646
2578
|
var ROOT_TEMPLATE = `# Project context
|
|
1647
2579
|
|
|
@@ -1723,10 +2655,10 @@ ${template}\`\`\`
|
|
|
1723
2655
|
}
|
|
1724
2656
|
|
|
1725
2657
|
// src/prompts/post-task.ts
|
|
1726
|
-
import { z as
|
|
2658
|
+
import { z as z30 } from "zod";
|
|
1727
2659
|
var PostTaskArgsSchema = {
|
|
1728
|
-
task_summary:
|
|
1729
|
-
files_touched:
|
|
2660
|
+
task_summary: z30.string().optional().describe("One sentence describing what you just did"),
|
|
2661
|
+
files_touched: z30.array(z30.string()).optional().describe("Files you created or modified during the task")
|
|
1730
2662
|
};
|
|
1731
2663
|
function postTaskPrompt(args, ctx) {
|
|
1732
2664
|
const taskLine = args.task_summary ? `
|
|
@@ -1808,12 +2740,12 @@ When done, respond with a brief summary: "Saved N memories: [list of IDs]. Sessi
|
|
|
1808
2740
|
}
|
|
1809
2741
|
|
|
1810
2742
|
// src/prompts/import-docs.ts
|
|
1811
|
-
import { z as
|
|
2743
|
+
import { z as z31 } from "zod";
|
|
1812
2744
|
var ImportDocsArgsSchema = {
|
|
1813
|
-
content:
|
|
1814
|
-
source:
|
|
1815
|
-
scope:
|
|
1816
|
-
dry_run:
|
|
2745
|
+
content: z31.string().describe("The documentation content to analyze and import as memories (Markdown, README, ADR, etc.)"),
|
|
2746
|
+
source: z31.string().optional().describe("Origin of the content (file path, URL, or document title) \u2014 used to anchor memories"),
|
|
2747
|
+
scope: z31.enum(["personal", "team"]).default("team").describe("Scope to assign to created memories"),
|
|
2748
|
+
dry_run: z31.boolean().default(false).describe("If true, describe what would be saved without actually calling mem_save")
|
|
1817
2749
|
};
|
|
1818
2750
|
function importDocsPrompt(args, ctx) {
|
|
1819
2751
|
const sourceLine = args.source ? `
|
|
@@ -1877,7 +2809,7 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
|
|
|
1877
2809
|
}
|
|
1878
2810
|
|
|
1879
2811
|
// src/session-tracker.ts
|
|
1880
|
-
import { loadConfig as loadConfig3 } from "@hiveai/core";
|
|
2812
|
+
import { appendUsageEvent, loadConfig as loadConfig3 } from "@hiveai/core";
|
|
1881
2813
|
var SessionTracker = class {
|
|
1882
2814
|
events = [];
|
|
1883
2815
|
startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -1894,7 +2826,9 @@ var SessionTracker = class {
|
|
|
1894
2826
|
}
|
|
1895
2827
|
}
|
|
1896
2828
|
record(tool, summary) {
|
|
1897
|
-
|
|
2829
|
+
const event = { tool, at: (/* @__PURE__ */ new Date()).toISOString(), summary };
|
|
2830
|
+
this.events.push(event);
|
|
2831
|
+
void appendUsageEvent(this.ctx.paths, event);
|
|
1898
2832
|
}
|
|
1899
2833
|
registerShutdownHandler() {
|
|
1900
2834
|
if (this.shutdownRegistered) return;
|
|
@@ -1947,7 +2881,7 @@ function summarizeTools(events) {
|
|
|
1947
2881
|
|
|
1948
2882
|
// src/server.ts
|
|
1949
2883
|
var SERVER_NAME = "haive";
|
|
1950
|
-
var SERVER_VERSION = "0.
|
|
2884
|
+
var SERVER_VERSION = "0.6.0";
|
|
1951
2885
|
function jsonResult(data) {
|
|
1952
2886
|
return {
|
|
1953
2887
|
content: [
|
|
@@ -2156,7 +3090,10 @@ function createHaiveServer(options = {}) {
|
|
|
2156
3090
|
"RETURNS: array of { id, type, scope, status, confidence, body, match_quality }"
|
|
2157
3091
|
].join("\n"),
|
|
2158
3092
|
MemSearchInputSchema,
|
|
2159
|
-
async (input) =>
|
|
3093
|
+
async (input) => {
|
|
3094
|
+
tracker.record("mem_search", input.query.slice(0, 80));
|
|
3095
|
+
return jsonResult(await memSearch(input, context));
|
|
3096
|
+
}
|
|
2160
3097
|
);
|
|
2161
3098
|
server.tool(
|
|
2162
3099
|
"mem_for_files",
|
|
@@ -2384,6 +3321,211 @@ function createHaiveServer(options = {}) {
|
|
|
2384
3321
|
MemDeleteInputSchema,
|
|
2385
3322
|
async (input) => jsonResult(await memDelete(input, context))
|
|
2386
3323
|
);
|
|
3324
|
+
server.tool(
|
|
3325
|
+
"get_recap",
|
|
3326
|
+
[
|
|
3327
|
+
"Return ONLY the most recent session_recap. Cheaper than get_briefing when",
|
|
3328
|
+
"you just want to know 'what was I doing last time?' and don't need project",
|
|
3329
|
+
"context, modules, or memory ranking.",
|
|
3330
|
+
"",
|
|
3331
|
+
"PARAMETERS:",
|
|
3332
|
+
" scope \u2014 'personal' | 'team' | 'any' (default 'any', returns the most recent across both)",
|
|
3333
|
+
"",
|
|
3334
|
+
"RETURNS: { recap: { id, scope, revision_count, created_at, body } | null, notice? }"
|
|
3335
|
+
].join("\n"),
|
|
3336
|
+
GetRecapInputSchema,
|
|
3337
|
+
async (input) => {
|
|
3338
|
+
tracker.record("get_recap", input.scope);
|
|
3339
|
+
return jsonResult(await getRecap(input, context));
|
|
3340
|
+
}
|
|
3341
|
+
);
|
|
3342
|
+
server.tool(
|
|
3343
|
+
"mem_relevant_to",
|
|
3344
|
+
[
|
|
3345
|
+
"One-shot ranked memories for a task \u2014 use instead of get_briefing when",
|
|
3346
|
+
"project context is already loaded and you only want the relevant memory layer.",
|
|
3347
|
+
"",
|
|
3348
|
+
"Reuses the same ranking pipeline (anchor / module / literal / semantic) but",
|
|
3349
|
+
"skips project_context, modules, action_required, etc.",
|
|
3350
|
+
"",
|
|
3351
|
+
"PARAMETERS:",
|
|
3352
|
+
" task \u2014 1\u20132 sentences describing what you are about to do (required)",
|
|
3353
|
+
" files \u2014 files you'll edit (surfaces anchored memories)",
|
|
3354
|
+
" limit \u2014 cap on returned memories (default 8)",
|
|
3355
|
+
" min_semantic_score \u2014 drop weak semantic hits below this cosine (default 0.25)",
|
|
3356
|
+
"",
|
|
3357
|
+
"RETURNS: { task, search_mode, memories: [...], hints?: [...], empty?: true }"
|
|
3358
|
+
].join("\n"),
|
|
3359
|
+
MemRelevantToInputSchema,
|
|
3360
|
+
async (input) => {
|
|
3361
|
+
tracker.record("mem_relevant_to", input.task.slice(0, 80));
|
|
3362
|
+
return jsonResult(await memRelevantTo(input, context));
|
|
3363
|
+
}
|
|
3364
|
+
);
|
|
3365
|
+
server.tool(
|
|
3366
|
+
"code_search",
|
|
3367
|
+
[
|
|
3368
|
+
"Semantic search over the codebase \u2014 finds exported symbols (functions, classes,",
|
|
3369
|
+
"interfaces) related to a natural-language query. Replaces blind grep when you",
|
|
3370
|
+
"don't know the exact symbol name.",
|
|
3371
|
+
"",
|
|
3372
|
+
"Requires `haive index code-search` to have been run (builds embeddings for every",
|
|
3373
|
+
"exported symbol from the code-map). Falls back to a notice when index is missing.",
|
|
3374
|
+
"",
|
|
3375
|
+
"PARAMETERS:",
|
|
3376
|
+
" query \u2014 natural language (e.g. 'function that hashes passwords', 'JWT signing')",
|
|
3377
|
+
" k \u2014 number of top hits (default 5)",
|
|
3378
|
+
" min_score \u2014 minimum cosine similarity (default 0.2; try 0.3+ for stricter)",
|
|
3379
|
+
"",
|
|
3380
|
+
"RETURNS: { available: bool, hits: [{ file, name, kind, line, description?, score }] }"
|
|
3381
|
+
].join("\n"),
|
|
3382
|
+
CodeSearchInputSchema,
|
|
3383
|
+
async (input) => {
|
|
3384
|
+
tracker.record("code_search", input.query.slice(0, 80));
|
|
3385
|
+
return jsonResult(await codeSearch(input, context));
|
|
3386
|
+
}
|
|
3387
|
+
);
|
|
3388
|
+
server.tool(
|
|
3389
|
+
"why_this_file",
|
|
3390
|
+
[
|
|
3391
|
+
"One-shot file-context lookup: combines recent git history, memories anchored",
|
|
3392
|
+
"to the path, and the code-map entry. Answers 'why is this file the way it is?'",
|
|
3393
|
+
"in a single call instead of 3-4 manual ones.",
|
|
3394
|
+
"",
|
|
3395
|
+
"PARAMETERS:",
|
|
3396
|
+
" path \u2014 project-relative path (required)",
|
|
3397
|
+
" git_log_limit \u2014 recent commits to include (default 5)",
|
|
3398
|
+
" memory_limit \u2014 anchored memories cap (default 5)",
|
|
3399
|
+
"",
|
|
3400
|
+
"RETURNS: { file, exists, recent_commits: [...], memories: [...], code_map_entry, hints? }"
|
|
3401
|
+
].join("\n"),
|
|
3402
|
+
WhyThisFileInputSchema,
|
|
3403
|
+
async (input) => {
|
|
3404
|
+
tracker.record("why_this_file", input.path);
|
|
3405
|
+
return jsonResult(await whyThisFile(input, context));
|
|
3406
|
+
}
|
|
3407
|
+
);
|
|
3408
|
+
server.tool(
|
|
3409
|
+
"anti_patterns_check",
|
|
3410
|
+
[
|
|
3411
|
+
"Scan a diff (or set of paths) against documented attempt/gotcha memories.",
|
|
3412
|
+
"Surfaces 'you are about to repeat a known mistake' warnings BEFORE you commit.",
|
|
3413
|
+
"",
|
|
3414
|
+
"USE BEFORE finalizing a non-trivial change. Cheap and high-signal: the only",
|
|
3415
|
+
"memories scanned are 'attempt' and 'gotcha' types.",
|
|
3416
|
+
"",
|
|
3417
|
+
"PARAMETERS:",
|
|
3418
|
+
" diff \u2014 raw unified diff text (or any code snippet) \u2014 optional if `paths` provided",
|
|
3419
|
+
" paths \u2014 affected file paths (optional if `diff` provided)",
|
|
3420
|
+
" limit \u2014 cap on returned warnings (default 8)",
|
|
3421
|
+
" semantic \u2014 also use semantic search (default true; requires embeddings index)",
|
|
3422
|
+
"",
|
|
3423
|
+
"RETURNS: { scanned, warnings: [{ id, type, scope, confidence, body_preview, reasons, semantic_score? }] }"
|
|
3424
|
+
].join("\n"),
|
|
3425
|
+
AntiPatternsCheckInputSchema,
|
|
3426
|
+
async (input) => {
|
|
3427
|
+
tracker.record("anti_patterns_check", input.paths.join(",").slice(0, 80));
|
|
3428
|
+
return jsonResult(await antiPatternsCheck(input, context));
|
|
3429
|
+
}
|
|
3430
|
+
);
|
|
3431
|
+
server.tool(
|
|
3432
|
+
"mem_distill",
|
|
3433
|
+
[
|
|
3434
|
+
"Cluster recurring observations / failed attempts so a human can collapse",
|
|
3435
|
+
"N similar memories into one richer convention/gotcha. Cheap heuristic",
|
|
3436
|
+
"(anchor path overlap + body keyword overlap) \u2014 no embeddings required.",
|
|
3437
|
+
"",
|
|
3438
|
+
"USE periodically (e.g. monthly) to prevent memory pollution from agents",
|
|
3439
|
+
"saving the same observation many times.",
|
|
3440
|
+
"",
|
|
3441
|
+
"PARAMETERS:",
|
|
3442
|
+
" since_days \u2014 only consider memories from the last N days (default 30)",
|
|
3443
|
+
" min_cluster \u2014 minimum cluster size to surface (default 3)",
|
|
3444
|
+
" type_filter \u2014 'gotcha' | 'attempt' | 'all' (default 'gotcha')",
|
|
3445
|
+
" scope \u2014 'personal' | 'team' | 'module' | 'any' (default 'any')",
|
|
3446
|
+
"",
|
|
3447
|
+
"RETURNS: { scanned, singletons, clusters: [{ suggested_topic, member_ids, ... }] }",
|
|
3448
|
+
"Output is advisory \u2014 nothing is written to disk."
|
|
3449
|
+
].join("\n"),
|
|
3450
|
+
MemDistillInputSchema,
|
|
3451
|
+
async (input) => {
|
|
3452
|
+
tracker.record("mem_distill", `${input.type_filter}/since=${input.since_days}d`);
|
|
3453
|
+
return jsonResult(await memDistill(input, context));
|
|
3454
|
+
}
|
|
3455
|
+
);
|
|
3456
|
+
server.tool(
|
|
3457
|
+
"why_this_decision",
|
|
3458
|
+
[
|
|
3459
|
+
"Trace the genealogy of a memory (especially decision/architecture):",
|
|
3460
|
+
"the memory itself + memories explicitly linked via related_ids + memories",
|
|
3461
|
+
"anchored to overlapping paths + recent commits touching those paths.",
|
|
3462
|
+
"",
|
|
3463
|
+
"USE WHEN you find a memory and need to understand WHY it was made and",
|
|
3464
|
+
"what surrounds it. One call instead of 4-5 manual lookups.",
|
|
3465
|
+
"",
|
|
3466
|
+
"PARAMETERS:",
|
|
3467
|
+
" id \u2014 memory id (required)",
|
|
3468
|
+
" git_log_limit \u2014 how many recent commits per anchor path (default 5)",
|
|
3469
|
+
"",
|
|
3470
|
+
"RETURNS: { decision, related: [...], path_neighbors: [...], recent_commits: [...] }"
|
|
3471
|
+
].join("\n"),
|
|
3472
|
+
WhyThisDecisionInputSchema,
|
|
3473
|
+
async (input) => {
|
|
3474
|
+
tracker.record("why_this_decision", input.id);
|
|
3475
|
+
return jsonResult(await whyThisDecision(input, context));
|
|
3476
|
+
}
|
|
3477
|
+
);
|
|
3478
|
+
server.tool(
|
|
3479
|
+
"mem_conflicts_with",
|
|
3480
|
+
[
|
|
3481
|
+
"Detect memories that potentially CONTRADICT a given memory.",
|
|
3482
|
+
"",
|
|
3483
|
+
"USE BEFORE relying on a memory's advice \u2014 surfaces 'another memory says",
|
|
3484
|
+
"the opposite'. Detection uses several heuristics layered together:",
|
|
3485
|
+
"",
|
|
3486
|
+
" 1. Opposite status \u2014 validated vs rejected on overlapping topic",
|
|
3487
|
+
" 2. attempt-vs-convention on overlapping anchor paths",
|
|
3488
|
+
" 3. Polarity keywords \u2014 'use X' vs 'do not use X' among semantic neighbors",
|
|
3489
|
+
" 4. Explicit #contradicts:<id> tags in either body",
|
|
3490
|
+
"",
|
|
3491
|
+
"PARAMETERS:",
|
|
3492
|
+
" id \u2014 memory id to check (required)",
|
|
3493
|
+
" min_score \u2014 minimum cosine similarity for semantic neighbors (default 0.5)",
|
|
3494
|
+
" semantic \u2014 use embeddings (default true)",
|
|
3495
|
+
"",
|
|
3496
|
+
"RETURNS: { found, target, scanned, conflicts: [{ id, reasons, similarity, ... }] }"
|
|
3497
|
+
].join("\n"),
|
|
3498
|
+
MemConflictsInputSchema,
|
|
3499
|
+
async (input) => {
|
|
3500
|
+
tracker.record("mem_conflicts_with", input.id);
|
|
3501
|
+
return jsonResult(await memConflicts(input, context));
|
|
3502
|
+
}
|
|
3503
|
+
);
|
|
3504
|
+
server.tool(
|
|
3505
|
+
"pre_commit_check",
|
|
3506
|
+
[
|
|
3507
|
+
"One-shot 'should I block this commit?' check. Combines three signals:",
|
|
3508
|
+
"",
|
|
3509
|
+
" 1. anti_patterns_check \u2014 known gotchas/attempts that match the diff",
|
|
3510
|
+
" 2. mem_for_files \u2014 conventions/decisions anchored to touched files",
|
|
3511
|
+
" 3. mem_verify \u2014 memories whose anchors are stale (knowledge may be wrong)",
|
|
3512
|
+
"",
|
|
3513
|
+
"USE FROM A GIT HOOK or before finalizing a non-trivial change.",
|
|
3514
|
+
"",
|
|
3515
|
+
"PARAMETERS:",
|
|
3516
|
+
" diff \u2014 raw unified diff text (e.g. `git diff --cached`)",
|
|
3517
|
+
" paths \u2014 affected file paths (project-relative)",
|
|
3518
|
+
" block_on \u2014 'any' | 'high-confidence' (default) | 'never'",
|
|
3519
|
+
" semantic \u2014 use embeddings in anti_patterns_check (default true)",
|
|
3520
|
+
"",
|
|
3521
|
+
"RETURNS: { should_block, summary, warnings, relevant_memories, stale_anchors }"
|
|
3522
|
+
].join("\n"),
|
|
3523
|
+
PreCommitCheckInputSchema,
|
|
3524
|
+
async (input) => {
|
|
3525
|
+
tracker.record("pre_commit_check", `${input.paths.length}p`);
|
|
3526
|
+
return jsonResult(await preCommitCheck(input, context));
|
|
3527
|
+
}
|
|
3528
|
+
);
|
|
2387
3529
|
server.tool(
|
|
2388
3530
|
"mem_diff",
|
|
2389
3531
|
[
|
|
@@ -2440,6 +3582,17 @@ function createHaiveServer(options = {}) {
|
|
|
2440
3582
|
export {
|
|
2441
3583
|
SERVER_NAME,
|
|
2442
3584
|
SERVER_VERSION,
|
|
2443
|
-
|
|
3585
|
+
antiPatternsCheck,
|
|
3586
|
+
codeMapTool,
|
|
3587
|
+
codeSearch,
|
|
3588
|
+
createHaiveServer,
|
|
3589
|
+
getBriefing,
|
|
3590
|
+
getRecap,
|
|
3591
|
+
memConflicts,
|
|
3592
|
+
memDistill,
|
|
3593
|
+
memRelevantTo,
|
|
3594
|
+
preCommitCheck,
|
|
3595
|
+
whyThisDecision,
|
|
3596
|
+
whyThisFile
|
|
2444
3597
|
};
|
|
2445
3598
|
//# sourceMappingURL=server.js.map
|