@hiveai/mcp 0.2.16 → 0.3.2

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/README.md CHANGED
@@ -99,6 +99,7 @@ One-shot onboarding: returns project context + module contexts + ranked relevant
99
99
  {
100
100
  "task": "add a Stripe payment integration",
101
101
  "files": ["src/payments/PaymentService.ts"],
102
+ "symbols": ["PaymentService", "TenantFilter"],
102
103
  "max_tokens": 8000,
103
104
  "max_memories": 8,
104
105
  "format": "full",
@@ -112,6 +113,7 @@ One-shot onboarding: returns project context + module contexts + ranked relevant
112
113
  |---|---|---|
113
114
  | `task` | — | What you're about to do. Used to rank memories by relevance. |
114
115
  | `files` | `[]` | Files you're editing. Surfaces memories anchored to these files. |
116
+ | `symbols` | `[]` | Symbol names to look up in the code-map (e.g. `["PaymentService"]`). Returns file + line + kind without grepping. Requires `haive index code`. |
115
117
  | `max_tokens` | `8000` | Token budget for the entire response. Sections are truncated to fit. |
116
118
  | `max_memories` | `8` | Max memories to include. |
117
119
  | `format` | `"full"` | `"full"` = complete bodies · `"compact"` = 1-line summaries (call `mem_get` for details) |
@@ -120,10 +122,13 @@ One-shot onboarding: returns project context + module contexts + ranked relevant
120
122
  | `track` | `true` | Increment read_count for returned memories. |
121
123
 
122
124
  **Response includes:**
123
- - `project_context` — the contents of `.ai/project-context.md`
125
+ - `last_session` — most recent `haive session end` recap (surfaced first so agents start with fresh context)
126
+ - `project_context` — `.ai/project-context.md` (suppressed if still template — `is_template: true`)
124
127
  - `module_contexts` — relevant `.ai/modules/<name>/context.md` files
125
- - `memories` — ranked list of relevant memories with body, confidence, and match reason
126
- - `decay_warnings` — memory IDs not read in >90 days (review or deprecate)
128
+ - `memories` — ranked memories with `confidence`, `unverified` flag (for draft/proposed), and `match reason`
129
+ - `symbol_locations` — file:line:kind results for each requested symbol (from code-map)
130
+ - `decay_warnings` — memory IDs not read in >90 days
131
+ - `setup_warnings` — actionable warnings (e.g. template project-context, missing init)
127
132
  - `search_mode` — `"semantic"` | `"literal_fallback"` | `"literal"`
128
133
 
129
134
  ---
@@ -149,12 +154,15 @@ Save a new memory. For failed approaches, use `mem_tried` instead — it enforce
149
154
  | `slug` | ✅ | Short kebab-case identifier |
150
155
  | `scope` | — | `personal` (default) · `team` · `module` |
151
156
  | `body` | ✅ | Markdown content |
152
- | `paths` | — | File paths to anchor to (enables staleness detection) |
157
+ | `paths` | — | File paths to anchor to (enables staleness detection). **Warning returned if path doesn't exist in project.** |
153
158
  | `symbols` | — | Function/class names to anchor to |
154
159
  | `tags` | — | Tags for filtering |
160
+ | `topic` | — | Stable key for upsert: if a memory with this `topic`+`scope` already exists, it is updated in-place (`revision_count++`) |
155
161
  | `domain` | — | Business domain (e.g. `payments`) |
156
162
  | `author` | — | Author handle |
157
163
 
164
+ **Deduplication:** identical body content (same SHA-256 hash) within the same scope is rejected with an error. Use `mem_update` to modify it instead.
165
+
158
166
  ---
159
167
 
160
168
  ### `mem_tried` ⭐ Record failures immediately
@@ -263,6 +271,38 @@ Compare two memories side-by-side: shows frontmatter fields that differ and line
263
271
 
264
272
  ---
265
273
 
274
+ ### `mem_session_end`
275
+
276
+ Save a structured end-of-session recap. Topic-upsert: one recap per scope is kept and updated with `revision_count++`. Automatically surfaced at the top of the next `get_briefing`.
277
+
278
+ ```json
279
+ {
280
+ "goal": "Add Stripe payment integration",
281
+ "accomplished": "PaymentService done, tests passing, deployed to staging",
282
+ "discoveries": "Webhook signature requires raw body, not parsed JSON",
283
+ "files_touched": ["src/payments/PaymentService.ts", "src/payments/webhook.ts"],
284
+ "next_steps": "Add retry logic for failed webhooks",
285
+ "scope": "team"
286
+ }
287
+ ```
288
+
289
+ ---
290
+
291
+ ### `mem_observe`
292
+
293
+ Capture a code-level discovery in structured form (found-while, not a convention or decision).
294
+
295
+ ```json
296
+ {
297
+ "file": "src/payments/PaymentService.ts",
298
+ "symbol": "processPayment",
299
+ "observation": "This method calls the external API synchronously — any timeout blocks the entire request thread.",
300
+ "scope": "team"
301
+ }
302
+ ```
303
+
304
+ ---
305
+
266
306
  ### `mem_approve` / `mem_reject` / `mem_pending` / `mem_delete`
267
307
 
268
308
  Lifecycle operations:
package/dist/index.js CHANGED
@@ -130,6 +130,7 @@ import { existsSync as existsSync4 } from "fs";
130
130
  import path3 from "path";
131
131
  import {
132
132
  buildFrontmatter,
133
+ loadConfig,
133
134
  loadMemoriesFromDir as loadMemoriesFromDir2,
134
135
  memoryFilePath,
135
136
  serializeMemory
@@ -207,10 +208,12 @@ async function memSave(input, ctx) {
207
208
  };
208
209
  }
209
210
  }
211
+ const haiveConfig = await loadConfig(ctx.paths);
212
+ const resolvedScope = input.scope !== "personal" ? input.scope : haiveConfig.defaultScope ?? "personal";
210
213
  const frontmatter = buildFrontmatter({
211
214
  type: input.type,
212
215
  slug: input.slug,
213
- scope: input.scope,
216
+ scope: resolvedScope,
214
217
  module: input.module,
215
218
  tags: input.tags,
216
219
  domain: input.domain,
@@ -218,7 +221,8 @@ async function memSave(input, ctx) {
218
221
  paths: input.paths,
219
222
  symbols: input.symbols,
220
223
  commit: input.commit,
221
- topic: input.topic
224
+ topic: input.topic,
225
+ status: haiveConfig.defaultStatus === "validated" ? "validated" : void 0
222
226
  });
223
227
  const file = memoryFilePath(
224
228
  ctx.paths,
@@ -1164,9 +1168,12 @@ import {
1164
1168
  isDecaying,
1165
1169
  literalMatchesAllTokens as literalMatchesAllTokens2,
1166
1170
  literalMatchesAnyToken as literalMatchesAnyToken2,
1171
+ loadCodeMap,
1172
+ loadConfig as loadConfig2,
1167
1173
  loadMemoriesFromDir as loadMemoriesFromDir13,
1168
1174
  loadUsageIndex as loadUsageIndex7,
1169
1175
  memoryMatchesAnchorPaths as memoryMatchesAnchorPaths2,
1176
+ queryCodeMap,
1170
1177
  tokenizeQuery as tokenizeQuery2,
1171
1178
  trackReads as trackReads3,
1172
1179
  truncateToTokens
@@ -1190,6 +1197,9 @@ var GetBriefingInputSchema = {
1190
1197
  track: z17.boolean().default(true).describe("Increment read_count on returned memories"),
1191
1198
  format: z17.enum(["full", "compact"]).default("full").describe(
1192
1199
  "Output format: 'full' returns complete memory bodies; 'compact' returns id + 1-line summary only (call mem_get for details)."
1200
+ ),
1201
+ symbols: z17.array(z17.string()).default([]).describe(
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."
1193
1203
  )
1194
1204
  };
1195
1205
  async function getBriefing(input, ctx) {
@@ -1251,6 +1261,7 @@ async function getBriefing(input, ctx) {
1251
1261
  tags: fm.tags,
1252
1262
  status: fm.status,
1253
1263
  confidence: deriveConfidence4(fm, u),
1264
+ ...fm.status === "draft" || fm.status === "proposed" ? { unverified: true } : {},
1254
1265
  read_count: u.read_count,
1255
1266
  reasons: [reason],
1256
1267
  match_quality: matchQuality ?? "partial",
@@ -1316,17 +1327,55 @@ async function getBriefing(input, ctx) {
1316
1327
  }
1317
1328
  const projectContextRaw = input.include_project_context && existsSync17(ctx.paths.projectContext) ? await readFile3(ctx.paths.projectContext, "utf8") : "";
1318
1329
  const isTemplateContext = projectContextRaw.includes("TODO \u2014 high-level overview") || projectContextRaw.includes("Generated by `haive init`");
1319
- const projectContext = isTemplateContext ? "" : projectContextRaw;
1320
1330
  const setupWarnings = [];
1321
- if (isTemplateContext) {
1322
- setupWarnings.push(
1323
- "project-context.md still contains the default template. Invoke the bootstrap_project MCP prompt to auto-fill it from your codebase. Until then, get_briefing returns no project context."
1324
- );
1325
- }
1326
- if (!existsSync17(ctx.paths.projectContext)) {
1327
- setupWarnings.push(
1328
- "No project-context.md found. Run `haive init` then invoke the bootstrap_project MCP prompt."
1329
- );
1331
+ let autoContextGenerated = false;
1332
+ let projectContext = isTemplateContext ? "" : projectContextRaw;
1333
+ if ((isTemplateContext || !existsSync17(ctx.paths.projectContext)) && input.include_project_context) {
1334
+ const haiveConfig = await loadConfig2(ctx.paths);
1335
+ if (haiveConfig.autoContext) {
1336
+ const codeMap = await loadCodeMap(ctx.paths);
1337
+ if (codeMap) {
1338
+ const totalFiles = Object.keys(codeMap.files).length;
1339
+ const extensions = /* @__PURE__ */ new Map();
1340
+ for (const filePath of Object.keys(codeMap.files)) {
1341
+ const ext = filePath.slice(filePath.lastIndexOf(".") + 1).toLowerCase();
1342
+ extensions.set(ext, (extensions.get(ext) ?? 0) + 1);
1343
+ }
1344
+ const topExts = [...extensions.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([e, n]) => `${e} (${n})`).join(", ");
1345
+ const topSymbols = Object.entries(codeMap.files).flatMap(
1346
+ ([fp, entry]) => entry.exports.slice(0, 3).map((e) => `${e.name} (${fp.split("/").slice(-2).join("/")})`)
1347
+ ).slice(0, 15).join(", ");
1348
+ projectContext = `# Project context (auto-generated by hAIve)
1349
+
1350
+ > \u26A0 This is a minimal auto-generated context based on the code-map. Invoke the \`bootstrap_project\` MCP prompt to replace it with a full analysis.
1351
+
1352
+ ## Codebase overview
1353
+ - **${totalFiles} files** indexed in code-map
1354
+ - **Main file types:** ${topExts}
1355
+ - **Generated at:** ${codeMap.generated_at}
1356
+
1357
+ ## Key exports (sample)
1358
+ ` + topSymbols + "\n";
1359
+ autoContextGenerated = true;
1360
+ setupWarnings.push(
1361
+ "project-context.md is still the default template. A minimal auto-generated context has been injected from the code-map. Invoke bootstrap_project to replace it with a full AI-analyzed context."
1362
+ );
1363
+ } else {
1364
+ setupWarnings.push(
1365
+ "project-context.md is still the default template and no code-map found. Run `haive index code` then invoke bootstrap_project for a full context."
1366
+ );
1367
+ }
1368
+ } else {
1369
+ if (isTemplateContext) {
1370
+ setupWarnings.push(
1371
+ "project-context.md still contains the default template. Invoke the bootstrap_project MCP prompt to auto-fill it from your codebase. Until then, get_briefing returns no project context."
1372
+ );
1373
+ } else {
1374
+ setupWarnings.push(
1375
+ "No project-context.md found. Run `haive init` then invoke the bootstrap_project MCP prompt."
1376
+ );
1377
+ }
1378
+ }
1330
1379
  }
1331
1380
  const moduleContents = input.include_module_contexts ? await loadModuleContexts2(ctx, inferred) : [];
1332
1381
  const memoriesText = memories.map((m) => {
@@ -1389,18 +1438,51 @@ ${m.content}`).join("\n\n---\n\n"),
1389
1438
  if (isDecaying(u, createdAt)) decayWarnings.push(m.id);
1390
1439
  }
1391
1440
  const outputMemories = input.format === "compact" ? trimmedMemories.map((m) => ({ ...m, body: compactSummary(m.body) })) : trimmedMemories;
1441
+ let symbolLocations;
1442
+ const symbolsToLookup = new Set(input.symbols);
1443
+ for (const m of outputMemories) {
1444
+ const loaded = byId.get(m.id);
1445
+ for (const sym of loaded?.memory.frontmatter.anchor.symbols ?? []) {
1446
+ symbolsToLookup.add(sym);
1447
+ }
1448
+ }
1449
+ if (symbolsToLookup.size > 0) {
1450
+ const codeMap = await loadCodeMap(ctx.paths);
1451
+ if (codeMap) {
1452
+ symbolLocations = [];
1453
+ for (const sym of symbolsToLookup) {
1454
+ const { files } = queryCodeMap(codeMap, { symbol: sym });
1455
+ if (files.length > 0) {
1456
+ symbolLocations.push({
1457
+ symbol: sym,
1458
+ locations: files.flatMap(
1459
+ (f) => f.entry.exports.filter((e) => e.name.toLowerCase().includes(sym.toLowerCase())).map((e) => ({
1460
+ file: f.path,
1461
+ kind: e.kind,
1462
+ line: e.line,
1463
+ ...e.description ? { description: e.description } : {}
1464
+ }))
1465
+ )
1466
+ });
1467
+ }
1468
+ }
1469
+ if (symbolLocations.length === 0) symbolLocations = void 0;
1470
+ }
1471
+ }
1392
1472
  return {
1393
1473
  ...input.task ? { task: input.task } : {},
1394
1474
  search_mode: searchMode,
1395
1475
  inferred_modules: inferred,
1396
1476
  ...lastSession ? { last_session: lastSession } : {},
1397
- project_context: projectContextRaw ? {
1477
+ project_context: projectContextRaw || autoContextGenerated ? {
1398
1478
  content: projectSlice.text,
1399
1479
  truncated: projectSlice.truncated,
1400
- ...isTemplateContext ? { is_template: true } : {}
1480
+ ...isTemplateContext && !autoContextGenerated ? { is_template: true } : {},
1481
+ ...autoContextGenerated ? { auto_generated: true } : {}
1401
1482
  } : null,
1402
1483
  module_contexts: trimmedModules,
1403
1484
  memories: outputMemories,
1485
+ ...symbolLocations ? { symbol_locations: symbolLocations } : {},
1404
1486
  decay_warnings: decayWarnings,
1405
1487
  setup_warnings: setupWarnings,
1406
1488
  estimated_tokens: totalTokens,
@@ -1450,7 +1532,7 @@ async function loadModuleContexts2(ctx, modules) {
1450
1532
  }
1451
1533
 
1452
1534
  // src/tools/code-map.ts
1453
- import { loadCodeMap, queryCodeMap } from "@hiveai/core";
1535
+ import { loadCodeMap as loadCodeMap2, queryCodeMap as queryCodeMap2 } from "@hiveai/core";
1454
1536
  import { z as z18 } from "zod";
1455
1537
  var CodeMapInputSchema = {
1456
1538
  file: z18.string().optional().describe("Filter to files whose path contains this substring"),
@@ -1458,7 +1540,7 @@ var CodeMapInputSchema = {
1458
1540
  max_files: z18.number().int().positive().default(40).describe("Cap on returned files")
1459
1541
  };
1460
1542
  async function codeMapTool(input, ctx) {
1461
- const map = await loadCodeMap(ctx.paths);
1543
+ const map = await loadCodeMap2(ctx.paths);
1462
1544
  if (!map) {
1463
1545
  return {
1464
1546
  available: false,
@@ -1466,7 +1548,7 @@ async function codeMapTool(input, ctx) {
1466
1548
  notice: "No code map found. Run `haive index code` to generate `.ai/code-map.json`."
1467
1549
  };
1468
1550
  }
1469
- const { files } = queryCodeMap(map, { file: input.file, symbol: input.symbol });
1551
+ const { files } = queryCodeMap2(map, { file: input.file, symbol: input.symbol });
1470
1552
  return {
1471
1553
  available: true,
1472
1554
  generated_at: map.generated_at,
@@ -1766,9 +1848,78 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
1766
1848
  };
1767
1849
  }
1768
1850
 
1851
+ // src/session-tracker.ts
1852
+ import { loadConfig as loadConfig3 } from "@hiveai/core";
1853
+ var SessionTracker = class {
1854
+ events = [];
1855
+ startedAt = (/* @__PURE__ */ new Date()).toISOString();
1856
+ config = null;
1857
+ ctx;
1858
+ shutdownRegistered = false;
1859
+ constructor(ctx) {
1860
+ this.ctx = ctx;
1861
+ }
1862
+ async init() {
1863
+ this.config = await loadConfig3(this.ctx.paths);
1864
+ if (this.config.autoSessionEnd) {
1865
+ this.registerShutdownHandler();
1866
+ }
1867
+ }
1868
+ record(tool, summary) {
1869
+ this.events.push({ tool, at: (/* @__PURE__ */ new Date()).toISOString(), summary });
1870
+ }
1871
+ registerShutdownHandler() {
1872
+ if (this.shutdownRegistered) return;
1873
+ this.shutdownRegistered = true;
1874
+ const save = async () => {
1875
+ const writingTools = this.events.filter(
1876
+ (e) => ["mem_save", "mem_tried", "mem_observe", "mem_update", "bootstrap_project_save"].includes(e.tool)
1877
+ );
1878
+ const totalCalls = this.events.length;
1879
+ if (totalCalls === 0) return;
1880
+ const toolSummary = summarizeTools(this.events);
1881
+ const filesSet = /* @__PURE__ */ new Set();
1882
+ for (const e of this.events) {
1883
+ if (e.summary) {
1884
+ const matches = e.summary.match(/[^\s"',]+\.[a-zA-Z]{1,6}/g) ?? [];
1885
+ for (const m of matches) filesSet.add(m);
1886
+ }
1887
+ }
1888
+ try {
1889
+ await memSessionEnd(
1890
+ {
1891
+ goal: `Auto-captured session (${totalCalls} tool call${totalCalls === 1 ? "" : "s"})`,
1892
+ accomplished: toolSummary,
1893
+ discoveries: writingTools.length > 0 ? `${writingTools.length} memor${writingTools.length === 1 ? "y" : "ies"} saved during this session.` : "No new memories saved this session.",
1894
+ files_touched: [...filesSet].slice(0, 10),
1895
+ next_steps: "",
1896
+ scope: this.config?.defaultScope ?? "personal",
1897
+ module: void 0
1898
+ },
1899
+ this.ctx
1900
+ );
1901
+ } catch {
1902
+ }
1903
+ };
1904
+ process.once("SIGTERM", () => {
1905
+ void save().finally(() => process.exit(0));
1906
+ });
1907
+ process.once("SIGINT", () => {
1908
+ void save().finally(() => process.exit(0));
1909
+ });
1910
+ }
1911
+ };
1912
+ function summarizeTools(events) {
1913
+ const counts = /* @__PURE__ */ new Map();
1914
+ for (const e of events) {
1915
+ counts.set(e.tool, (counts.get(e.tool) ?? 0) + 1);
1916
+ }
1917
+ return [...counts.entries()].sort((a, b) => b[1] - a[1]).map(([t, n]) => `${t} \xD7${n}`).join(", ");
1918
+ }
1919
+
1769
1920
  // src/server.ts
1770
1921
  var SERVER_NAME = "haive";
1771
- var SERVER_VERSION = "0.2.16";
1922
+ var SERVER_VERSION = "0.3.2";
1772
1923
  function jsonResult(data) {
1773
1924
  return {
1774
1925
  content: [
@@ -1781,6 +1932,8 @@ function jsonResult(data) {
1781
1932
  }
1782
1933
  function createHaiveServer(options = {}) {
1783
1934
  const context = createContext(options);
1935
+ const tracker = new SessionTracker(context);
1936
+ void tracker.init();
1784
1937
  const server = new McpServer(
1785
1938
  { name: SERVER_NAME, version: SERVER_VERSION },
1786
1939
  { capabilities: { tools: {}, prompts: {} } }
@@ -1789,7 +1942,10 @@ function createHaiveServer(options = {}) {
1789
1942
  "mem_save",
1790
1943
  "Save a new memory (convention, decision, gotcha, architecture, glossary). For failed approaches use mem_tried instead \u2014 it enforces a structured format that is more useful to future agents. Use scope=team to share with the whole team.",
1791
1944
  MemSaveInputSchema,
1792
- async (input) => jsonResult(await memSave(input, context))
1945
+ async (input) => {
1946
+ tracker.record("mem_save", input.slug);
1947
+ return jsonResult(await memSave(input, context));
1948
+ }
1793
1949
  );
1794
1950
  server.tool(
1795
1951
  "mem_search",
@@ -1813,7 +1969,10 @@ function createHaiveServer(options = {}) {
1813
1969
  "get_briefing",
1814
1970
  "One-shot onboarding: returns project context + module contexts + ranked relevant memories under a token budget. Replaces 4\u20135 separate calls when an agent starts a task.",
1815
1971
  GetBriefingInputSchema,
1816
- async (input) => jsonResult(await getBriefing(input, context))
1972
+ async (input) => {
1973
+ tracker.record("get_briefing", input.task ?? "");
1974
+ return jsonResult(await getBriefing(input, context));
1975
+ }
1817
1976
  );
1818
1977
  server.tool(
1819
1978
  "code_map",
@@ -1879,7 +2038,10 @@ function createHaiveServer(options = {}) {
1879
2038
  "mem_tried",
1880
2039
  "Preferred way to record a failed approach. Enforces a structured what/why_failed/instead format that is immediately actionable for future agents. Auto-validated (no approval cycle). Use whenever you tried an approach and it failed \u2014 prevents the same mistake from happening in the next session.",
1881
2040
  MemTriedInputSchema,
1882
- async (input) => jsonResult(await memTried(input, context))
2041
+ async (input) => {
2042
+ tracker.record("mem_tried", input.what.slice(0, 80));
2043
+ return jsonResult(await memTried(input, context));
2044
+ }
1883
2045
  );
1884
2046
  server.tool(
1885
2047
  "mem_diff",
@@ -1891,13 +2053,19 @@ function createHaiveServer(options = {}) {
1891
2053
  "mem_observe",
1892
2054
  "Capture a code-level discovery made during exploration: a bug, inconsistency, missing config, or security gap found by reading existing code that was NOT in the briefing. Use this whenever you read code and spot something that could silently break in production. Auto-validated, anchored to file paths for staleness detection. Prefer this over mem_save for reactive discoveries during code reading.",
1893
2055
  MemObserveInputSchema,
1894
- async (input) => jsonResult(await memObserve(input, context))
2056
+ async (input) => {
2057
+ tracker.record("mem_observe", input.where);
2058
+ return jsonResult(await memObserve(input, context));
2059
+ }
1895
2060
  );
1896
2061
  server.tool(
1897
2062
  "mem_session_end",
1898
2063
  "Save a structured end-of-session recap (goal / accomplished / discoveries / next steps). Uses topic-upsert: one recap per scope is kept and updated in-place so the next session always has fresh context. Call this before closing every significant session. get_briefing automatically surfaces the latest recap at the top of the next session's briefing.",
1899
2064
  MemSessionEndInputSchema,
1900
- async (input) => jsonResult(await memSessionEnd(input, context))
2065
+ async (input) => {
2066
+ tracker.record("mem_session_end", input.goal.slice(0, 80));
2067
+ return jsonResult(await memSessionEnd(input, context));
2068
+ }
1901
2069
  );
1902
2070
  server.prompt(
1903
2071
  "bootstrap_project",
@@ -1917,7 +2085,7 @@ function createHaiveServer(options = {}) {
1917
2085
  ImportDocsArgsSchema,
1918
2086
  (args) => importDocsPrompt(args, context)
1919
2087
  );
1920
- return { server, context };
2088
+ return { server, context, tracker };
1921
2089
  }
1922
2090
 
1923
2091
  // src/index.ts