@hiveai/mcp 0.4.5 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +558 -21
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +305 -2
- package/dist/server.js +566 -22
- package/dist/server.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1200,6 +1200,9 @@ var GetBriefingInputSchema = {
|
|
|
1200
1200
|
),
|
|
1201
1201
|
symbols: z17.array(z17.string()).default([]).describe(
|
|
1202
1202
|
"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."
|
|
1203
|
+
),
|
|
1204
|
+
min_semantic_score: z17.number().min(0).max(1).default(0).describe(
|
|
1205
|
+
"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."
|
|
1203
1206
|
)
|
|
1204
1207
|
};
|
|
1205
1208
|
async function getBriefing(input, ctx) {
|
|
@@ -1232,6 +1235,7 @@ async function getBriefing(input, ctx) {
|
|
|
1232
1235
|
return true;
|
|
1233
1236
|
});
|
|
1234
1237
|
usage = await loadUsageIndex7(ctx.paths);
|
|
1238
|
+
byId = new Map(allMemories.map((m) => [m.memory.frontmatter.id, m]));
|
|
1235
1239
|
const semanticHits = input.task && input.semantic ? await trySemanticHits(ctx, input.task, allMemories.length * 2) : null;
|
|
1236
1240
|
if (input.task && input.semantic) {
|
|
1237
1241
|
searchMode = semanticHits ? "semantic" : "literal_fallback";
|
|
@@ -1296,6 +1300,10 @@ async function getBriefing(input, ctx) {
|
|
|
1296
1300
|
}
|
|
1297
1301
|
if (semanticHits) {
|
|
1298
1302
|
for (const hit of semanticHits) {
|
|
1303
|
+
if (hit.score < input.min_semantic_score) {
|
|
1304
|
+
const existing = seen.get(hit.id);
|
|
1305
|
+
if (!existing) continue;
|
|
1306
|
+
}
|
|
1299
1307
|
const loaded = byId.get(hit.id);
|
|
1300
1308
|
if (loaded) addOrUpdate(loaded, "semantic", hit.score, "semantic");
|
|
1301
1309
|
}
|
|
@@ -1309,7 +1317,6 @@ async function getBriefing(input, ctx) {
|
|
|
1309
1317
|
const sb = reasonScore(b) + confidenceScore(b) + (b.semantic_score ?? 0);
|
|
1310
1318
|
return sb - sa;
|
|
1311
1319
|
});
|
|
1312
|
-
byId = new Map(allMemories.map((m) => [m.memory.frontmatter.id, m]));
|
|
1313
1320
|
for (const mem of ranked.slice(0, input.max_memories)) {
|
|
1314
1321
|
if (seen.size >= input.max_memories * 2) break;
|
|
1315
1322
|
const loaded = byId.get(mem.id);
|
|
@@ -1501,6 +1508,36 @@ ${m.content}`).join("\n\n---\n\n"),
|
|
|
1501
1508
|
});
|
|
1502
1509
|
}
|
|
1503
1510
|
}
|
|
1511
|
+
const memoriesEmpty = outputMemories.length === 0;
|
|
1512
|
+
const hasMemoriesDir = existsSync17(ctx.paths.memoriesDir);
|
|
1513
|
+
const isColdStart = isTemplateContext && memoriesEmpty && !lastSession && !autoContextGenerated;
|
|
1514
|
+
const hints = [];
|
|
1515
|
+
if (isColdStart) {
|
|
1516
|
+
hints.push(
|
|
1517
|
+
"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."
|
|
1518
|
+
);
|
|
1519
|
+
} else {
|
|
1520
|
+
if (outputMemories.some((m) => m.type === "attempt")) {
|
|
1521
|
+
hints.push(
|
|
1522
|
+
"\u26A0\uFE0F One or more 'attempt' memories matched \u2014 these document failed approaches. Read them BEFORE writing code to avoid repeating the mistake."
|
|
1523
|
+
);
|
|
1524
|
+
}
|
|
1525
|
+
if (outputMemories.some((m) => m.type === "gotcha")) {
|
|
1526
|
+
hints.push(
|
|
1527
|
+
"Gotcha memories matched \u2014 non-obvious traps. Verify the 'how to apply' line still holds before assuming behavior."
|
|
1528
|
+
);
|
|
1529
|
+
}
|
|
1530
|
+
if (memoriesEmpty && hasMemoriesDir && input.task) {
|
|
1531
|
+
hints.push(
|
|
1532
|
+
"No memories matched this task. Try mem_search with broader/different terms, or call mem_for_files with the files you intend to edit."
|
|
1533
|
+
);
|
|
1534
|
+
}
|
|
1535
|
+
if (input.task && outputMemories.length > 0 && actionRequired.length === 0) {
|
|
1536
|
+
hints.push(
|
|
1537
|
+
"After completing the task: capture new gotchas with mem_observe, failed approaches with mem_tried, validated patterns with mem_save."
|
|
1538
|
+
);
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1504
1541
|
return {
|
|
1505
1542
|
...input.task ? { task: input.task } : {},
|
|
1506
1543
|
search_mode: searchMode,
|
|
@@ -1518,6 +1555,8 @@ ${m.content}`).join("\n\n---\n\n"),
|
|
|
1518
1555
|
action_required: actionRequired,
|
|
1519
1556
|
decay_warnings: decayWarnings,
|
|
1520
1557
|
setup_warnings: setupWarnings,
|
|
1558
|
+
...isColdStart ? { low_value: true } : {},
|
|
1559
|
+
...hints.length > 0 ? { hints } : {},
|
|
1521
1560
|
estimated_tokens: totalTokens,
|
|
1522
1561
|
budget: {
|
|
1523
1562
|
max_tokens: input.max_tokens,
|
|
@@ -1565,12 +1604,18 @@ async function loadModuleContexts2(ctx, modules) {
|
|
|
1565
1604
|
}
|
|
1566
1605
|
|
|
1567
1606
|
// src/tools/code-map.ts
|
|
1568
|
-
import { loadCodeMap as loadCodeMap2, queryCodeMap as queryCodeMap2 } from "@hiveai/core";
|
|
1607
|
+
import { estimateTokens as estimateTokens2, loadCodeMap as loadCodeMap2, queryCodeMap as queryCodeMap2 } from "@hiveai/core";
|
|
1569
1608
|
import { z as z18 } from "zod";
|
|
1570
1609
|
var CodeMapInputSchema = {
|
|
1571
1610
|
file: z18.string().optional().describe("Filter to files whose path contains this substring"),
|
|
1572
1611
|
symbol: z18.string().optional().describe("Filter to files exporting a symbol whose name contains this substring"),
|
|
1573
|
-
|
|
1612
|
+
paths: z18.array(z18.string()).default([]).describe(
|
|
1613
|
+
"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."
|
|
1614
|
+
),
|
|
1615
|
+
max_files: z18.number().int().positive().default(40).describe("Cap on returned files (hard limit, applied after token budget)"),
|
|
1616
|
+
max_tokens: z18.number().int().positive().optional().describe(
|
|
1617
|
+
"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)."
|
|
1618
|
+
)
|
|
1574
1619
|
};
|
|
1575
1620
|
async function codeMapTool(input, ctx) {
|
|
1576
1621
|
const map = await loadCodeMap2(ctx.paths);
|
|
@@ -1581,19 +1626,63 @@ async function codeMapTool(input, ctx) {
|
|
|
1581
1626
|
notice: "No code map found. Run `haive index code` to generate `.ai/code-map.json`."
|
|
1582
1627
|
};
|
|
1583
1628
|
}
|
|
1584
|
-
const { files } = queryCodeMap2(map, { file: input.file, symbol: input.symbol });
|
|
1629
|
+
const { files: matched } = queryCodeMap2(map, { file: input.file, symbol: input.symbol });
|
|
1630
|
+
const pathsFiltered = input.paths.length === 0 ? matched : matched.filter((f) => input.paths.some((p) => f.path.startsWith(stripLeadingSlash(p))));
|
|
1631
|
+
const alphabetical = [...pathsFiltered].sort((a, b) => a.path.localeCompare(b.path));
|
|
1632
|
+
let kept = alphabetical;
|
|
1633
|
+
let budgetClipped = false;
|
|
1634
|
+
if (input.max_tokens !== void 0) {
|
|
1635
|
+
const byDensity = [...alphabetical].sort((a, b) => {
|
|
1636
|
+
const da = density(a.entry.exports.length, a.entry.loc);
|
|
1637
|
+
const db = density(b.entry.exports.length, b.entry.loc);
|
|
1638
|
+
if (da !== db) return db - da;
|
|
1639
|
+
return a.path.localeCompare(b.path);
|
|
1640
|
+
});
|
|
1641
|
+
const keepSet = /* @__PURE__ */ new Set();
|
|
1642
|
+
let spent = 0;
|
|
1643
|
+
for (const f of byDensity) {
|
|
1644
|
+
const cost = estimateFileEntryTokens(f);
|
|
1645
|
+
if (spent + cost > input.max_tokens && keepSet.size > 0) {
|
|
1646
|
+
budgetClipped = true;
|
|
1647
|
+
break;
|
|
1648
|
+
}
|
|
1649
|
+
keepSet.add(f.path);
|
|
1650
|
+
spent += cost;
|
|
1651
|
+
}
|
|
1652
|
+
if (budgetClipped) {
|
|
1653
|
+
kept = alphabetical.filter((f) => keepSet.has(f.path));
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
const finalFiles = kept.slice(0, input.max_files);
|
|
1657
|
+
const totalDropped = pathsFiltered.length - finalFiles.length;
|
|
1585
1658
|
return {
|
|
1586
1659
|
available: true,
|
|
1587
1660
|
generated_at: map.generated_at,
|
|
1588
1661
|
total_files: Object.keys(map.files).length,
|
|
1589
|
-
files:
|
|
1662
|
+
files: finalFiles.map((f) => ({
|
|
1590
1663
|
path: f.path,
|
|
1591
1664
|
...f.entry.summary ? { summary: f.entry.summary } : {},
|
|
1592
1665
|
loc: f.entry.loc,
|
|
1593
1666
|
exports: f.entry.exports
|
|
1594
|
-
}))
|
|
1667
|
+
})),
|
|
1668
|
+
...totalDropped > 0 ? { truncated: totalDropped } : {},
|
|
1669
|
+
...budgetClipped ? { budget_clipped: true } : {}
|
|
1595
1670
|
};
|
|
1596
1671
|
}
|
|
1672
|
+
function density(exports, loc) {
|
|
1673
|
+
if (loc <= 0) return 0;
|
|
1674
|
+
return exports / Math.max(loc, 1);
|
|
1675
|
+
}
|
|
1676
|
+
function stripLeadingSlash(p) {
|
|
1677
|
+
return p.startsWith("/") ? p.slice(1) : p;
|
|
1678
|
+
}
|
|
1679
|
+
function estimateFileEntryTokens(f) {
|
|
1680
|
+
const exportsCost = f.entry.exports.reduce(
|
|
1681
|
+
(acc, e) => acc + 6 + estimateTokens2(e.description ?? ""),
|
|
1682
|
+
0
|
|
1683
|
+
);
|
|
1684
|
+
return estimateTokens2(f.path) + estimateTokens2(f.entry.summary ?? "") + exportsCost + 4;
|
|
1685
|
+
}
|
|
1597
1686
|
|
|
1598
1687
|
// src/tools/mem-diff.ts
|
|
1599
1688
|
import { existsSync as existsSync18 } from "fs";
|
|
@@ -1640,13 +1729,349 @@ async function memDiff(input, ctx) {
|
|
|
1640
1729
|
};
|
|
1641
1730
|
}
|
|
1642
1731
|
|
|
1643
|
-
// src/
|
|
1732
|
+
// src/tools/get-recap.ts
|
|
1733
|
+
import { existsSync as existsSync19 } from "fs";
|
|
1734
|
+
import { loadMemoriesFromDir as loadMemoriesFromDir15 } from "@hiveai/core";
|
|
1644
1735
|
import { z as z20 } from "zod";
|
|
1736
|
+
var GetRecapInputSchema = {
|
|
1737
|
+
scope: z20.enum(["personal", "team", "any"]).default("any").describe(
|
|
1738
|
+
"Limit to a specific scope's recap. Default 'any' returns the most recent recap across both personal and team scopes."
|
|
1739
|
+
)
|
|
1740
|
+
};
|
|
1741
|
+
async function getRecap(input, ctx) {
|
|
1742
|
+
if (!existsSync19(ctx.paths.memoriesDir)) {
|
|
1743
|
+
return { recap: null, notice: "No .ai/memories directory \u2014 haive not initialized here." };
|
|
1744
|
+
}
|
|
1745
|
+
const all = await loadMemoriesFromDir15(ctx.paths.memoriesDir);
|
|
1746
|
+
const recaps = all.filter(({ memory }) => memory.frontmatter.type === "session_recap").filter(({ memory }) => input.scope === "any" || memory.frontmatter.scope === input.scope).sort(
|
|
1747
|
+
(a, b) => new Date(b.memory.frontmatter.created_at).getTime() - new Date(a.memory.frontmatter.created_at).getTime()
|
|
1748
|
+
);
|
|
1749
|
+
if (recaps.length === 0) {
|
|
1750
|
+
return {
|
|
1751
|
+
recap: null,
|
|
1752
|
+
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}'.`
|
|
1753
|
+
};
|
|
1754
|
+
}
|
|
1755
|
+
const r = recaps[0];
|
|
1756
|
+
const fm = r.memory.frontmatter;
|
|
1757
|
+
return {
|
|
1758
|
+
recap: {
|
|
1759
|
+
id: fm.id,
|
|
1760
|
+
scope: fm.scope,
|
|
1761
|
+
revision_count: fm.revision_count ?? 0,
|
|
1762
|
+
created_at: fm.created_at,
|
|
1763
|
+
body: r.memory.body
|
|
1764
|
+
}
|
|
1765
|
+
};
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
// src/tools/mem-relevant-to.ts
|
|
1769
|
+
import { z as z21 } from "zod";
|
|
1770
|
+
var MemRelevantToInputSchema = {
|
|
1771
|
+
task: z21.string().min(1).describe("What you are about to do, in 1\u20132 sentences. Used to rank relevant memories."),
|
|
1772
|
+
files: z21.array(z21.string()).default([]).describe("Optional: files you are about to edit \u2014 surfaces anchored memories."),
|
|
1773
|
+
limit: z21.number().int().positive().max(30).default(8).describe("Cap on returned memories."),
|
|
1774
|
+
min_semantic_score: z21.number().min(0).max(1).default(0.25).describe("Drop weakly-related semantic hits below this cosine threshold."),
|
|
1775
|
+
format: z21.enum(["full", "compact"]).default("full").describe("'compact' = id + 1-line summary; 'full' = complete bodies.")
|
|
1776
|
+
};
|
|
1777
|
+
async function memRelevantTo(input, ctx) {
|
|
1778
|
+
const briefingInput = {
|
|
1779
|
+
task: input.task,
|
|
1780
|
+
files: input.files,
|
|
1781
|
+
max_tokens: 16e3,
|
|
1782
|
+
max_memories: input.limit,
|
|
1783
|
+
include_project_context: false,
|
|
1784
|
+
include_module_contexts: false,
|
|
1785
|
+
semantic: true,
|
|
1786
|
+
include_stale: false,
|
|
1787
|
+
track: true,
|
|
1788
|
+
format: input.format,
|
|
1789
|
+
symbols: [],
|
|
1790
|
+
min_semantic_score: input.min_semantic_score
|
|
1791
|
+
};
|
|
1792
|
+
const briefing = await getBriefing(briefingInput, ctx);
|
|
1793
|
+
const out = {
|
|
1794
|
+
task: input.task,
|
|
1795
|
+
search_mode: briefing.search_mode,
|
|
1796
|
+
memories: briefing.memories
|
|
1797
|
+
};
|
|
1798
|
+
if (briefing.hints && briefing.hints.length > 0) out.hints = briefing.hints;
|
|
1799
|
+
if (briefing.memories.length === 0) out.empty = true;
|
|
1800
|
+
return out;
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
// src/tools/code-search.ts
|
|
1804
|
+
import { z as z22 } from "zod";
|
|
1805
|
+
var CodeSearchInputSchema = {
|
|
1806
|
+
query: z22.string().min(1).describe(
|
|
1807
|
+
"Natural-language description of what you are looking for in the codebase (e.g. 'function that hashes passwords', 'JWT signing logic', 'route registration')."
|
|
1808
|
+
),
|
|
1809
|
+
k: z22.number().int().positive().max(50).default(5).describe("Number of top hits to return."),
|
|
1810
|
+
min_score: z22.number().min(0).max(1).default(0.2).describe(
|
|
1811
|
+
"Minimum cosine similarity. Hits below this threshold are dropped to avoid noise. Try 0.3+ for stricter matching."
|
|
1812
|
+
)
|
|
1813
|
+
};
|
|
1814
|
+
async function codeSearch(input, ctx) {
|
|
1815
|
+
let mod;
|
|
1816
|
+
try {
|
|
1817
|
+
mod = await import("@hiveai/embeddings");
|
|
1818
|
+
} catch {
|
|
1819
|
+
return {
|
|
1820
|
+
available: false,
|
|
1821
|
+
hits: [],
|
|
1822
|
+
notice: "@hiveai/embeddings is not installed. Install it (`pnpm add @hiveai/embeddings`) and run `haive index code-search` to enable semantic code search."
|
|
1823
|
+
};
|
|
1824
|
+
}
|
|
1825
|
+
const result = await mod.codeSemanticSearch(ctx.paths, input.query, {
|
|
1826
|
+
limit: input.k,
|
|
1827
|
+
minScore: input.min_score
|
|
1828
|
+
});
|
|
1829
|
+
if (!result) {
|
|
1830
|
+
return {
|
|
1831
|
+
available: false,
|
|
1832
|
+
hits: [],
|
|
1833
|
+
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)."
|
|
1834
|
+
};
|
|
1835
|
+
}
|
|
1836
|
+
return { available: true, hits: result.hits };
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
// src/tools/why-this-file.ts
|
|
1840
|
+
import { existsSync as existsSync20 } from "fs";
|
|
1841
|
+
import { spawn } from "child_process";
|
|
1842
|
+
import path9 from "path";
|
|
1843
|
+
import {
|
|
1844
|
+
deriveConfidence as deriveConfidence5,
|
|
1845
|
+
getUsage as getUsage6,
|
|
1846
|
+
loadCodeMap as loadCodeMap3,
|
|
1847
|
+
loadMemoriesFromDir as loadMemoriesFromDir16,
|
|
1848
|
+
loadUsageIndex as loadUsageIndex8,
|
|
1849
|
+
memoryMatchesAnchorPaths as memoryMatchesAnchorPaths3
|
|
1850
|
+
} from "@hiveai/core";
|
|
1851
|
+
import { z as z23 } from "zod";
|
|
1852
|
+
var WhyThisFileInputSchema = {
|
|
1853
|
+
path: z23.string().min(1).describe(
|
|
1854
|
+
"Project-relative path to the file you want context on (e.g. 'packages/mcp/src/tools/mem-save.ts')."
|
|
1855
|
+
),
|
|
1856
|
+
git_log_limit: z23.number().int().positive().max(20).default(5).describe("How many recent commits touching this file to include."),
|
|
1857
|
+
memory_limit: z23.number().int().positive().max(20).default(5).describe("Cap on memories anchored to this path.")
|
|
1858
|
+
};
|
|
1859
|
+
async function whyThisFile(input, ctx) {
|
|
1860
|
+
const fileExists = existsSync20(path9.join(ctx.paths.root, input.path));
|
|
1861
|
+
const [commits, memories, codeMap] = await Promise.all([
|
|
1862
|
+
runGitLog(ctx.paths.root, input.path, input.git_log_limit).catch(() => []),
|
|
1863
|
+
collectAnchoredMemories(ctx, input.path, input.memory_limit),
|
|
1864
|
+
loadCodeMap3(ctx.paths)
|
|
1865
|
+
]);
|
|
1866
|
+
const codeMapEntry = codeMap?.files[input.path];
|
|
1867
|
+
const hints = [];
|
|
1868
|
+
if (!fileExists) {
|
|
1869
|
+
hints.push(`File '${input.path}' does not exist on disk \u2014 path may be wrong or file removed.`);
|
|
1870
|
+
}
|
|
1871
|
+
if (commits.length === 0 && fileExists) {
|
|
1872
|
+
hints.push("No git history found \u2014 file may be untracked or git not initialized.");
|
|
1873
|
+
}
|
|
1874
|
+
if (memories.length === 0 && fileExists) {
|
|
1875
|
+
hints.push(
|
|
1876
|
+
"No memories anchored here. If you discover something non-obvious while editing, use mem_observe (with where=" + input.path + ") to capture it."
|
|
1877
|
+
);
|
|
1878
|
+
}
|
|
1879
|
+
if (memories.some((m) => m.type === "attempt" || m.type === "gotcha")) {
|
|
1880
|
+
hints.push("\u26A0\uFE0F attempt/gotcha memories anchored to this file \u2014 read them BEFORE editing.");
|
|
1881
|
+
}
|
|
1882
|
+
return {
|
|
1883
|
+
file: input.path,
|
|
1884
|
+
exists: fileExists,
|
|
1885
|
+
recent_commits: commits,
|
|
1886
|
+
memories,
|
|
1887
|
+
code_map_entry: codeMapEntry ? {
|
|
1888
|
+
...codeMapEntry.summary ? { summary: codeMapEntry.summary } : {},
|
|
1889
|
+
loc: codeMapEntry.loc,
|
|
1890
|
+
exports: codeMapEntry.exports.map((e) => ({
|
|
1891
|
+
name: e.name,
|
|
1892
|
+
kind: e.kind,
|
|
1893
|
+
line: e.line,
|
|
1894
|
+
...e.description ? { description: e.description } : {}
|
|
1895
|
+
}))
|
|
1896
|
+
} : null,
|
|
1897
|
+
...hints.length > 0 ? { hints } : {}
|
|
1898
|
+
};
|
|
1899
|
+
}
|
|
1900
|
+
async function collectAnchoredMemories(ctx, filePath, limit) {
|
|
1901
|
+
if (!existsSync20(ctx.paths.memoriesDir)) return [];
|
|
1902
|
+
const all = await loadMemoriesFromDir16(ctx.paths.memoriesDir);
|
|
1903
|
+
const usage = await loadUsageIndex8(ctx.paths);
|
|
1904
|
+
const out = [];
|
|
1905
|
+
for (const { memory } of all) {
|
|
1906
|
+
const fm = memory.frontmatter;
|
|
1907
|
+
if (fm.status === "rejected" || fm.status === "deprecated") continue;
|
|
1908
|
+
if (fm.type === "session_recap") continue;
|
|
1909
|
+
if (!memoryMatchesAnchorPaths3(memory, [filePath])) continue;
|
|
1910
|
+
const u = getUsage6(usage, fm.id);
|
|
1911
|
+
out.push({
|
|
1912
|
+
id: fm.id,
|
|
1913
|
+
type: fm.type,
|
|
1914
|
+
scope: fm.scope,
|
|
1915
|
+
confidence: deriveConfidence5(fm, u),
|
|
1916
|
+
body_preview: memory.body.split("\n").slice(0, 6).join("\n")
|
|
1917
|
+
});
|
|
1918
|
+
if (out.length >= limit) break;
|
|
1919
|
+
}
|
|
1920
|
+
return out;
|
|
1921
|
+
}
|
|
1922
|
+
async function runGitLog(cwd, filePath, limit) {
|
|
1923
|
+
const sep = "<<HV>>";
|
|
1924
|
+
const fmt = `%h${sep}%an${sep}%ar${sep}%s`;
|
|
1925
|
+
const output = await runCommand(
|
|
1926
|
+
"git",
|
|
1927
|
+
["log", `-n`, String(limit), `--pretty=format:${fmt}`, "--", filePath],
|
|
1928
|
+
cwd
|
|
1929
|
+
);
|
|
1930
|
+
if (!output.trim()) return [];
|
|
1931
|
+
return output.split("\n").map((line) => {
|
|
1932
|
+
const [sha = "", author = "", relative_date = "", subject = ""] = line.split(sep);
|
|
1933
|
+
return { sha, author, relative_date, subject };
|
|
1934
|
+
}).filter((c) => c.sha);
|
|
1935
|
+
}
|
|
1936
|
+
function runCommand(cmd, args, cwd) {
|
|
1937
|
+
return new Promise((resolve, reject) => {
|
|
1938
|
+
const proc = spawn(cmd, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
1939
|
+
let stdout = "";
|
|
1940
|
+
let stderr = "";
|
|
1941
|
+
proc.stdout.on("data", (chunk) => {
|
|
1942
|
+
stdout += chunk.toString();
|
|
1943
|
+
});
|
|
1944
|
+
proc.stderr.on("data", (chunk) => {
|
|
1945
|
+
stderr += chunk.toString();
|
|
1946
|
+
});
|
|
1947
|
+
proc.on("error", reject);
|
|
1948
|
+
proc.on("close", (code) => {
|
|
1949
|
+
if (code === 0) resolve(stdout);
|
|
1950
|
+
else reject(new Error(stderr || `${cmd} exited with code ${code}`));
|
|
1951
|
+
});
|
|
1952
|
+
});
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
// src/tools/anti-patterns-check.ts
|
|
1956
|
+
import { existsSync as existsSync21 } from "fs";
|
|
1957
|
+
import {
|
|
1958
|
+
deriveConfidence as deriveConfidence6,
|
|
1959
|
+
getUsage as getUsage7,
|
|
1960
|
+
loadMemoriesFromDir as loadMemoriesFromDir17,
|
|
1961
|
+
loadUsageIndex as loadUsageIndex9,
|
|
1962
|
+
literalMatchesAnyToken as literalMatchesAnyToken3,
|
|
1963
|
+
memoryMatchesAnchorPaths as memoryMatchesAnchorPaths4,
|
|
1964
|
+
tokenizeQuery as tokenizeQuery3
|
|
1965
|
+
} from "@hiveai/core";
|
|
1966
|
+
import { z as z24 } from "zod";
|
|
1967
|
+
var AntiPatternsCheckInputSchema = {
|
|
1968
|
+
diff: z24.string().optional().describe(
|
|
1969
|
+
"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."
|
|
1970
|
+
),
|
|
1971
|
+
paths: z24.array(z24.string()).default([]).describe(
|
|
1972
|
+
"File paths affected by the change. Memories anchored to any of these paths are surfaced regardless of the diff content."
|
|
1973
|
+
),
|
|
1974
|
+
limit: z24.number().int().positive().max(20).default(8).describe("Cap on returned warnings."),
|
|
1975
|
+
semantic: z24.boolean().default(true).describe(
|
|
1976
|
+
"When true, also use semantic search (requires @hiveai/embeddings + memory index) to find related anti-patterns."
|
|
1977
|
+
)
|
|
1978
|
+
};
|
|
1979
|
+
async function antiPatternsCheck(input, ctx) {
|
|
1980
|
+
if (!input.diff && input.paths.length === 0) {
|
|
1981
|
+
return {
|
|
1982
|
+
scanned: 0,
|
|
1983
|
+
warnings: [],
|
|
1984
|
+
notice: "Nothing to check \u2014 provide either `diff` text or `paths`."
|
|
1985
|
+
};
|
|
1986
|
+
}
|
|
1987
|
+
if (!existsSync21(ctx.paths.memoriesDir)) {
|
|
1988
|
+
return { scanned: 0, warnings: [], notice: "No .ai/memories directory \u2014 nothing to check against." };
|
|
1989
|
+
}
|
|
1990
|
+
const all = await loadMemoriesFromDir17(ctx.paths.memoriesDir);
|
|
1991
|
+
const negative = all.filter(({ memory }) => {
|
|
1992
|
+
const t = memory.frontmatter.type;
|
|
1993
|
+
if (t !== "attempt" && t !== "gotcha") return false;
|
|
1994
|
+
const s = memory.frontmatter.status;
|
|
1995
|
+
return s !== "rejected" && s !== "deprecated" && s !== "stale";
|
|
1996
|
+
});
|
|
1997
|
+
if (negative.length === 0) {
|
|
1998
|
+
return { scanned: 0, warnings: [], notice: "No attempt/gotcha memories found yet." };
|
|
1999
|
+
}
|
|
2000
|
+
const usage = await loadUsageIndex9(ctx.paths);
|
|
2001
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2002
|
+
const upsert = (fm, body, reason, score) => {
|
|
2003
|
+
const existing = seen.get(fm.id);
|
|
2004
|
+
if (existing) {
|
|
2005
|
+
if (!existing.reasons.includes(reason)) existing.reasons.push(reason);
|
|
2006
|
+
if (score !== void 0 && (existing.semantic_score ?? 0) < score) {
|
|
2007
|
+
existing.semantic_score = score;
|
|
2008
|
+
}
|
|
2009
|
+
return;
|
|
2010
|
+
}
|
|
2011
|
+
const u = getUsage7(usage, fm.id);
|
|
2012
|
+
seen.set(fm.id, {
|
|
2013
|
+
id: fm.id,
|
|
2014
|
+
type: fm.type,
|
|
2015
|
+
scope: fm.scope,
|
|
2016
|
+
confidence: deriveConfidence6(fm, u),
|
|
2017
|
+
body_preview: body.split("\n").slice(0, 5).join("\n").slice(0, 400),
|
|
2018
|
+
reasons: [reason],
|
|
2019
|
+
...score !== void 0 ? { semantic_score: score } : {}
|
|
2020
|
+
});
|
|
2021
|
+
};
|
|
2022
|
+
if (input.paths.length > 0) {
|
|
2023
|
+
for (const { memory } of negative) {
|
|
2024
|
+
if (memoryMatchesAnchorPaths4(memory, input.paths)) {
|
|
2025
|
+
upsert(memory.frontmatter, memory.body, "anchor");
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
if (input.diff) {
|
|
2030
|
+
const tokens = tokenizeQuery3(input.diff);
|
|
2031
|
+
if (tokens.length > 0) {
|
|
2032
|
+
for (const { memory } of negative) {
|
|
2033
|
+
if (literalMatchesAnyToken3(memory, tokens)) {
|
|
2034
|
+
upsert(memory.frontmatter, memory.body, "literal");
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
if (input.semantic && input.diff) {
|
|
2040
|
+
try {
|
|
2041
|
+
const mod = await import("@hiveai/embeddings");
|
|
2042
|
+
const result = await mod.semanticSearch(ctx.paths, input.diff, { limit: input.limit * 2 });
|
|
2043
|
+
if (result) {
|
|
2044
|
+
const negativeIds = new Set(negative.map(({ memory }) => memory.frontmatter.id));
|
|
2045
|
+
for (const hit of result.hits) {
|
|
2046
|
+
if (!negativeIds.has(hit.id)) continue;
|
|
2047
|
+
const found = negative.find(({ memory }) => memory.frontmatter.id === hit.id);
|
|
2048
|
+
if (found) upsert(found.memory.frontmatter, found.memory.body, "semantic", hit.score);
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
} catch {
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
const warnings = [...seen.values()].sort((a, b) => {
|
|
2055
|
+
const score = (w) => {
|
|
2056
|
+
const reasonW = (w.reasons.includes("anchor") ? 4 : 0) + (w.reasons.includes("literal") ? 2 : 0) + (w.reasons.includes("semantic") ? 1 : 0);
|
|
2057
|
+
const confW = w.confidence === "authoritative" ? 3 : w.confidence === "trusted" ? 2 : w.confidence === "low" ? 1 : 0;
|
|
2058
|
+
return reasonW + confW + (w.semantic_score ?? 0);
|
|
2059
|
+
};
|
|
2060
|
+
return score(b) - score(a);
|
|
2061
|
+
}).slice(0, input.limit);
|
|
2062
|
+
return {
|
|
2063
|
+
scanned: negative.length,
|
|
2064
|
+
warnings
|
|
2065
|
+
};
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
// src/prompts/bootstrap-project.ts
|
|
2069
|
+
import { z as z25 } from "zod";
|
|
1645
2070
|
var BootstrapProjectArgsSchema = {
|
|
1646
|
-
module:
|
|
2071
|
+
module: z25.string().optional().describe(
|
|
1647
2072
|
"Optional module name to scope the analysis to (writes to .ai/modules/<module>/context.md)"
|
|
1648
2073
|
),
|
|
1649
|
-
focus:
|
|
2074
|
+
focus: z25.string().optional().describe("Optional area to emphasize (e.g. 'data layer', 'API surface')")
|
|
1650
2075
|
};
|
|
1651
2076
|
var ROOT_TEMPLATE = `# Project context
|
|
1652
2077
|
|
|
@@ -1728,10 +2153,10 @@ ${template}\`\`\`
|
|
|
1728
2153
|
}
|
|
1729
2154
|
|
|
1730
2155
|
// src/prompts/post-task.ts
|
|
1731
|
-
import { z as
|
|
2156
|
+
import { z as z26 } from "zod";
|
|
1732
2157
|
var PostTaskArgsSchema = {
|
|
1733
|
-
task_summary:
|
|
1734
|
-
files_touched:
|
|
2158
|
+
task_summary: z26.string().optional().describe("One sentence describing what you just did"),
|
|
2159
|
+
files_touched: z26.array(z26.string()).optional().describe("Files you created or modified during the task")
|
|
1735
2160
|
};
|
|
1736
2161
|
function postTaskPrompt(args, ctx) {
|
|
1737
2162
|
const taskLine = args.task_summary ? `
|
|
@@ -1813,12 +2238,12 @@ When done, respond with a brief summary: "Saved N memories: [list of IDs]. Sessi
|
|
|
1813
2238
|
}
|
|
1814
2239
|
|
|
1815
2240
|
// src/prompts/import-docs.ts
|
|
1816
|
-
import { z as
|
|
2241
|
+
import { z as z27 } from "zod";
|
|
1817
2242
|
var ImportDocsArgsSchema = {
|
|
1818
|
-
content:
|
|
1819
|
-
source:
|
|
1820
|
-
scope:
|
|
1821
|
-
dry_run:
|
|
2243
|
+
content: z27.string().describe("The documentation content to analyze and import as memories (Markdown, README, ADR, etc.)"),
|
|
2244
|
+
source: z27.string().optional().describe("Origin of the content (file path, URL, or document title) \u2014 used to anchor memories"),
|
|
2245
|
+
scope: z27.enum(["personal", "team"]).default("team").describe("Scope to assign to created memories"),
|
|
2246
|
+
dry_run: z27.boolean().default(false).describe("If true, describe what would be saved without actually calling mem_save")
|
|
1822
2247
|
};
|
|
1823
2248
|
function importDocsPrompt(args, ctx) {
|
|
1824
2249
|
const sourceLine = args.source ? `
|
|
@@ -1882,7 +2307,7 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
|
|
|
1882
2307
|
}
|
|
1883
2308
|
|
|
1884
2309
|
// src/session-tracker.ts
|
|
1885
|
-
import { loadConfig as loadConfig3 } from "@hiveai/core";
|
|
2310
|
+
import { appendUsageEvent, loadConfig as loadConfig3 } from "@hiveai/core";
|
|
1886
2311
|
var SessionTracker = class {
|
|
1887
2312
|
events = [];
|
|
1888
2313
|
startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -1899,7 +2324,9 @@ var SessionTracker = class {
|
|
|
1899
2324
|
}
|
|
1900
2325
|
}
|
|
1901
2326
|
record(tool, summary) {
|
|
1902
|
-
|
|
2327
|
+
const event = { tool, at: (/* @__PURE__ */ new Date()).toISOString(), summary };
|
|
2328
|
+
this.events.push(event);
|
|
2329
|
+
void appendUsageEvent(this.ctx.paths, event);
|
|
1903
2330
|
}
|
|
1904
2331
|
registerShutdownHandler() {
|
|
1905
2332
|
if (this.shutdownRegistered) return;
|
|
@@ -1952,7 +2379,7 @@ function summarizeTools(events) {
|
|
|
1952
2379
|
|
|
1953
2380
|
// src/server.ts
|
|
1954
2381
|
var SERVER_NAME = "haive";
|
|
1955
|
-
var SERVER_VERSION = "0.
|
|
2382
|
+
var SERVER_VERSION = "0.5.0";
|
|
1956
2383
|
function jsonResult(data) {
|
|
1957
2384
|
return {
|
|
1958
2385
|
content: [
|
|
@@ -2161,7 +2588,10 @@ function createHaiveServer(options = {}) {
|
|
|
2161
2588
|
"RETURNS: array of { id, type, scope, status, confidence, body, match_quality }"
|
|
2162
2589
|
].join("\n"),
|
|
2163
2590
|
MemSearchInputSchema,
|
|
2164
|
-
async (input) =>
|
|
2591
|
+
async (input) => {
|
|
2592
|
+
tracker.record("mem_search", input.query.slice(0, 80));
|
|
2593
|
+
return jsonResult(await memSearch(input, context));
|
|
2594
|
+
}
|
|
2165
2595
|
);
|
|
2166
2596
|
server.tool(
|
|
2167
2597
|
"mem_for_files",
|
|
@@ -2389,6 +2819,113 @@ function createHaiveServer(options = {}) {
|
|
|
2389
2819
|
MemDeleteInputSchema,
|
|
2390
2820
|
async (input) => jsonResult(await memDelete(input, context))
|
|
2391
2821
|
);
|
|
2822
|
+
server.tool(
|
|
2823
|
+
"get_recap",
|
|
2824
|
+
[
|
|
2825
|
+
"Return ONLY the most recent session_recap. Cheaper than get_briefing when",
|
|
2826
|
+
"you just want to know 'what was I doing last time?' and don't need project",
|
|
2827
|
+
"context, modules, or memory ranking.",
|
|
2828
|
+
"",
|
|
2829
|
+
"PARAMETERS:",
|
|
2830
|
+
" scope \u2014 'personal' | 'team' | 'any' (default 'any', returns the most recent across both)",
|
|
2831
|
+
"",
|
|
2832
|
+
"RETURNS: { recap: { id, scope, revision_count, created_at, body } | null, notice? }"
|
|
2833
|
+
].join("\n"),
|
|
2834
|
+
GetRecapInputSchema,
|
|
2835
|
+
async (input) => {
|
|
2836
|
+
tracker.record("get_recap", input.scope);
|
|
2837
|
+
return jsonResult(await getRecap(input, context));
|
|
2838
|
+
}
|
|
2839
|
+
);
|
|
2840
|
+
server.tool(
|
|
2841
|
+
"mem_relevant_to",
|
|
2842
|
+
[
|
|
2843
|
+
"One-shot ranked memories for a task \u2014 use instead of get_briefing when",
|
|
2844
|
+
"project context is already loaded and you only want the relevant memory layer.",
|
|
2845
|
+
"",
|
|
2846
|
+
"Reuses the same ranking pipeline (anchor / module / literal / semantic) but",
|
|
2847
|
+
"skips project_context, modules, action_required, etc.",
|
|
2848
|
+
"",
|
|
2849
|
+
"PARAMETERS:",
|
|
2850
|
+
" task \u2014 1\u20132 sentences describing what you are about to do (required)",
|
|
2851
|
+
" files \u2014 files you'll edit (surfaces anchored memories)",
|
|
2852
|
+
" limit \u2014 cap on returned memories (default 8)",
|
|
2853
|
+
" min_semantic_score \u2014 drop weak semantic hits below this cosine (default 0.25)",
|
|
2854
|
+
"",
|
|
2855
|
+
"RETURNS: { task, search_mode, memories: [...], hints?: [...], empty?: true }"
|
|
2856
|
+
].join("\n"),
|
|
2857
|
+
MemRelevantToInputSchema,
|
|
2858
|
+
async (input) => {
|
|
2859
|
+
tracker.record("mem_relevant_to", input.task.slice(0, 80));
|
|
2860
|
+
return jsonResult(await memRelevantTo(input, context));
|
|
2861
|
+
}
|
|
2862
|
+
);
|
|
2863
|
+
server.tool(
|
|
2864
|
+
"code_search",
|
|
2865
|
+
[
|
|
2866
|
+
"Semantic search over the codebase \u2014 finds exported symbols (functions, classes,",
|
|
2867
|
+
"interfaces) related to a natural-language query. Replaces blind grep when you",
|
|
2868
|
+
"don't know the exact symbol name.",
|
|
2869
|
+
"",
|
|
2870
|
+
"Requires `haive index code-search` to have been run (builds embeddings for every",
|
|
2871
|
+
"exported symbol from the code-map). Falls back to a notice when index is missing.",
|
|
2872
|
+
"",
|
|
2873
|
+
"PARAMETERS:",
|
|
2874
|
+
" query \u2014 natural language (e.g. 'function that hashes passwords', 'JWT signing')",
|
|
2875
|
+
" k \u2014 number of top hits (default 5)",
|
|
2876
|
+
" min_score \u2014 minimum cosine similarity (default 0.2; try 0.3+ for stricter)",
|
|
2877
|
+
"",
|
|
2878
|
+
"RETURNS: { available: bool, hits: [{ file, name, kind, line, description?, score }] }"
|
|
2879
|
+
].join("\n"),
|
|
2880
|
+
CodeSearchInputSchema,
|
|
2881
|
+
async (input) => {
|
|
2882
|
+
tracker.record("code_search", input.query.slice(0, 80));
|
|
2883
|
+
return jsonResult(await codeSearch(input, context));
|
|
2884
|
+
}
|
|
2885
|
+
);
|
|
2886
|
+
server.tool(
|
|
2887
|
+
"why_this_file",
|
|
2888
|
+
[
|
|
2889
|
+
"One-shot file-context lookup: combines recent git history, memories anchored",
|
|
2890
|
+
"to the path, and the code-map entry. Answers 'why is this file the way it is?'",
|
|
2891
|
+
"in a single call instead of 3-4 manual ones.",
|
|
2892
|
+
"",
|
|
2893
|
+
"PARAMETERS:",
|
|
2894
|
+
" path \u2014 project-relative path (required)",
|
|
2895
|
+
" git_log_limit \u2014 recent commits to include (default 5)",
|
|
2896
|
+
" memory_limit \u2014 anchored memories cap (default 5)",
|
|
2897
|
+
"",
|
|
2898
|
+
"RETURNS: { file, exists, recent_commits: [...], memories: [...], code_map_entry, hints? }"
|
|
2899
|
+
].join("\n"),
|
|
2900
|
+
WhyThisFileInputSchema,
|
|
2901
|
+
async (input) => {
|
|
2902
|
+
tracker.record("why_this_file", input.path);
|
|
2903
|
+
return jsonResult(await whyThisFile(input, context));
|
|
2904
|
+
}
|
|
2905
|
+
);
|
|
2906
|
+
server.tool(
|
|
2907
|
+
"anti_patterns_check",
|
|
2908
|
+
[
|
|
2909
|
+
"Scan a diff (or set of paths) against documented attempt/gotcha memories.",
|
|
2910
|
+
"Surfaces 'you are about to repeat a known mistake' warnings BEFORE you commit.",
|
|
2911
|
+
"",
|
|
2912
|
+
"USE BEFORE finalizing a non-trivial change. Cheap and high-signal: the only",
|
|
2913
|
+
"memories scanned are 'attempt' and 'gotcha' types.",
|
|
2914
|
+
"",
|
|
2915
|
+
"PARAMETERS:",
|
|
2916
|
+
" diff \u2014 raw unified diff text (or any code snippet) \u2014 optional if `paths` provided",
|
|
2917
|
+
" paths \u2014 affected file paths (optional if `diff` provided)",
|
|
2918
|
+
" limit \u2014 cap on returned warnings (default 8)",
|
|
2919
|
+
" semantic \u2014 also use semantic search (default true; requires embeddings index)",
|
|
2920
|
+
"",
|
|
2921
|
+
"RETURNS: { scanned, warnings: [{ id, type, scope, confidence, body_preview, reasons, semantic_score? }] }"
|
|
2922
|
+
].join("\n"),
|
|
2923
|
+
AntiPatternsCheckInputSchema,
|
|
2924
|
+
async (input) => {
|
|
2925
|
+
tracker.record("anti_patterns_check", input.paths.join(",").slice(0, 80));
|
|
2926
|
+
return jsonResult(await antiPatternsCheck(input, context));
|
|
2927
|
+
}
|
|
2928
|
+
);
|
|
2392
2929
|
server.tool(
|
|
2393
2930
|
"mem_diff",
|
|
2394
2931
|
[
|