@hiveai/mcp 0.5.0 → 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 +617 -12
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +184 -1
- package/dist/server.js +621 -12
- package/dist/server.js.map +1 -1
- package/package.json +3 -3
package/dist/server.js
CHANGED
|
@@ -2060,13 +2060,520 @@ async function antiPatternsCheck(input, ctx) {
|
|
|
2060
2060
|
};
|
|
2061
2061
|
}
|
|
2062
2062
|
|
|
2063
|
-
// src/
|
|
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";
|
|
2064
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";
|
|
2065
2572
|
var BootstrapProjectArgsSchema = {
|
|
2066
|
-
module:
|
|
2573
|
+
module: z29.string().optional().describe(
|
|
2067
2574
|
"Optional module name to scope the analysis to (writes to .ai/modules/<module>/context.md)"
|
|
2068
2575
|
),
|
|
2069
|
-
focus:
|
|
2576
|
+
focus: z29.string().optional().describe("Optional area to emphasize (e.g. 'data layer', 'API surface')")
|
|
2070
2577
|
};
|
|
2071
2578
|
var ROOT_TEMPLATE = `# Project context
|
|
2072
2579
|
|
|
@@ -2148,10 +2655,10 @@ ${template}\`\`\`
|
|
|
2148
2655
|
}
|
|
2149
2656
|
|
|
2150
2657
|
// src/prompts/post-task.ts
|
|
2151
|
-
import { z as
|
|
2658
|
+
import { z as z30 } from "zod";
|
|
2152
2659
|
var PostTaskArgsSchema = {
|
|
2153
|
-
task_summary:
|
|
2154
|
-
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")
|
|
2155
2662
|
};
|
|
2156
2663
|
function postTaskPrompt(args, ctx) {
|
|
2157
2664
|
const taskLine = args.task_summary ? `
|
|
@@ -2233,12 +2740,12 @@ When done, respond with a brief summary: "Saved N memories: [list of IDs]. Sessi
|
|
|
2233
2740
|
}
|
|
2234
2741
|
|
|
2235
2742
|
// src/prompts/import-docs.ts
|
|
2236
|
-
import { z as
|
|
2743
|
+
import { z as z31 } from "zod";
|
|
2237
2744
|
var ImportDocsArgsSchema = {
|
|
2238
|
-
content:
|
|
2239
|
-
source:
|
|
2240
|
-
scope:
|
|
2241
|
-
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")
|
|
2242
2749
|
};
|
|
2243
2750
|
function importDocsPrompt(args, ctx) {
|
|
2244
2751
|
const sourceLine = args.source ? `
|
|
@@ -2374,7 +2881,7 @@ function summarizeTools(events) {
|
|
|
2374
2881
|
|
|
2375
2882
|
// src/server.ts
|
|
2376
2883
|
var SERVER_NAME = "haive";
|
|
2377
|
-
var SERVER_VERSION = "0.
|
|
2884
|
+
var SERVER_VERSION = "0.6.0";
|
|
2378
2885
|
function jsonResult(data) {
|
|
2379
2886
|
return {
|
|
2380
2887
|
content: [
|
|
@@ -2921,6 +3428,104 @@ function createHaiveServer(options = {}) {
|
|
|
2921
3428
|
return jsonResult(await antiPatternsCheck(input, context));
|
|
2922
3429
|
}
|
|
2923
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
|
+
);
|
|
2924
3529
|
server.tool(
|
|
2925
3530
|
"mem_diff",
|
|
2926
3531
|
[
|
|
@@ -2983,7 +3588,11 @@ export {
|
|
|
2983
3588
|
createHaiveServer,
|
|
2984
3589
|
getBriefing,
|
|
2985
3590
|
getRecap,
|
|
3591
|
+
memConflicts,
|
|
3592
|
+
memDistill,
|
|
2986
3593
|
memRelevantTo,
|
|
3594
|
+
preCommitCheck,
|
|
3595
|
+
whyThisDecision,
|
|
2987
3596
|
whyThisFile
|
|
2988
3597
|
};
|
|
2989
3598
|
//# sourceMappingURL=server.js.map
|