@hiveai/mcp 0.2.11 → 0.2.13

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/server.js CHANGED
@@ -119,6 +119,7 @@ async function memList(input, ctx) {
119
119
  }
120
120
 
121
121
  // src/tools/mem-save.ts
122
+ import { createHash } from "crypto";
122
123
  import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
123
124
  import { existsSync as existsSync4 } from "fs";
124
125
  import path3 from "path";
@@ -130,7 +131,9 @@ import {
130
131
  } from "@hiveai/core";
131
132
  import { z as z4 } from "zod";
132
133
  var MemSaveInputSchema = {
133
- type: z4.enum(["convention", "decision", "gotcha", "architecture", "glossary", "attempt"]).describe("Kind of memory being saved. Use 'attempt' for failed approaches (auto-validated)."),
134
+ type: z4.enum(["convention", "decision", "gotcha", "architecture", "glossary", "attempt", "session_recap"]).describe(
135
+ "Kind of memory being saved. Use 'attempt' for failed approaches (auto-validated). Use 'session_recap' via mem_session_end instead."
136
+ ),
134
137
  slug: z4.string().min(1).describe("Short human-readable identifier \u2014 becomes part of the filename"),
135
138
  body: z4.string().describe("Markdown body of the memory"),
136
139
  scope: z4.enum(["personal", "team", "module"]).default("personal").describe("Visibility scope: personal | team | module"),
@@ -140,14 +143,61 @@ var MemSaveInputSchema = {
140
143
  author: z4.string().optional().describe("Author handle or email"),
141
144
  paths: z4.array(z4.string()).default([]).describe("Anchor paths (file paths this memory references)"),
142
145
  symbols: z4.array(z4.string()).default([]).describe("Anchor symbols (function/class names this memory references)"),
143
- commit: z4.string().optional().describe("Anchor commit SHA (for staleness detection later)")
146
+ commit: z4.string().optional().describe("Anchor commit SHA (for staleness detection later)"),
147
+ topic: z4.string().optional().describe(
148
+ "Stable key for this memory. If a memory with the same topic already exists in this scope, it is updated in-place (revision_count++). Use for knowledge that evolves over time."
149
+ )
144
150
  };
151
+ function bodyHash(body) {
152
+ return createHash("sha256").update(body.trim()).digest("hex").slice(0, 12);
153
+ }
145
154
  async function memSave(input, ctx) {
146
155
  if (!existsSync4(ctx.paths.haiveDir)) {
147
156
  throw new Error(
148
157
  `No .ai/ directory at ${ctx.paths.root}. Run 'haive init' first.`
149
158
  );
150
159
  }
160
+ const existing = existsSync4(ctx.paths.memoriesDir) ? await loadMemoriesFromDir2(ctx.paths.memoriesDir) : [];
161
+ const incomingHash = bodyHash(input.body);
162
+ const hashDuplicate = existing.find(
163
+ ({ memory }) => bodyHash(memory.body) === incomingHash && memory.frontmatter.scope === input.scope
164
+ );
165
+ if (hashDuplicate) {
166
+ throw new Error(
167
+ `Duplicate content detected \u2014 identical body already saved as "${hashDuplicate.memory.frontmatter.id}". Use mem_update to modify it, or change the body to add new information.`
168
+ );
169
+ }
170
+ if (input.topic) {
171
+ const topicMatch = existing.find(
172
+ ({ memory }) => memory.frontmatter.topic === input.topic && memory.frontmatter.scope === input.scope && (!input.module || memory.frontmatter.module === input.module)
173
+ );
174
+ if (topicMatch) {
175
+ const fm = topicMatch.memory.frontmatter;
176
+ const newFrontmatter = {
177
+ ...fm,
178
+ body: input.body,
179
+ tags: input.tags.length ? input.tags : fm.tags,
180
+ revision_count: (fm.revision_count ?? 0) + 1,
181
+ anchor: {
182
+ commit: input.commit ?? fm.anchor.commit,
183
+ paths: input.paths.length ? input.paths : fm.anchor.paths,
184
+ symbols: input.symbols.length ? input.symbols : fm.anchor.symbols
185
+ }
186
+ };
187
+ await writeFile2(
188
+ topicMatch.filePath,
189
+ serializeMemory({ frontmatter: newFrontmatter, body: input.body }),
190
+ "utf8"
191
+ );
192
+ return {
193
+ id: fm.id,
194
+ scope: fm.scope,
195
+ file_path: topicMatch.filePath,
196
+ action: "updated",
197
+ revision_count: newFrontmatter.revision_count
198
+ };
199
+ }
200
+ }
151
201
  const frontmatter = buildFrontmatter({
152
202
  type: input.type,
153
203
  slug: input.slug,
@@ -158,7 +208,8 @@ async function memSave(input, ctx) {
158
208
  author: input.author,
159
209
  paths: input.paths,
160
210
  symbols: input.symbols,
161
- commit: input.commit
211
+ commit: input.commit,
212
+ topic: input.topic
162
213
  });
163
214
  const file = memoryFilePath(
164
215
  ctx.paths,
@@ -171,15 +222,16 @@ async function memSave(input, ctx) {
171
222
  throw new Error(`Memory already exists at ${file}`);
172
223
  }
173
224
  let warning;
174
- if (existsSync4(ctx.paths.memoriesDir)) {
175
- const existing = await loadMemoriesFromDir2(ctx.paths.memoriesDir);
225
+ let similar_found;
226
+ if (existing.length > 0) {
176
227
  const slugTokens = input.slug.toLowerCase().split(/[-_\s]+/).filter(Boolean);
177
228
  const similar = existing.filter(({ memory }) => {
178
229
  const id = memory.frontmatter.id.toLowerCase();
179
230
  return slugTokens.length >= 2 && slugTokens.filter((t) => id.includes(t)).length >= Math.ceil(slugTokens.length * 0.6);
180
231
  });
181
232
  if (similar.length > 0) {
182
- warning = `Possible duplicate detected. Similar memories: ${similar.map((m) => m.memory.frontmatter.id).join(", ")}. Consider updating one of these instead.`;
233
+ similar_found = similar.map((m) => m.memory.frontmatter.id);
234
+ warning = `Possible duplicate: similar memories already exist (${similar_found.join(", ")}). Consider updating one of these instead.`;
183
235
  }
184
236
  }
185
237
  await writeFile2(file, serializeMemory({ frontmatter, body: input.body }), "utf8");
@@ -187,7 +239,9 @@ async function memSave(input, ctx) {
187
239
  id: frontmatter.id,
188
240
  scope: frontmatter.scope,
189
241
  file_path: file,
190
- ...warning ? { warning } : {}
242
+ action: "created",
243
+ ...warning ? { warning } : {},
244
+ ...similar_found ? { similar_found } : {}
191
245
  };
192
246
  }
193
247
 
@@ -947,10 +1001,120 @@ async function memObserve(input, ctx) {
947
1001
  return { id: frontmatter.id, scope: frontmatter.scope, file_path: file };
948
1002
  }
949
1003
 
950
- // src/tools/get-briefing.ts
951
- import { readFile as readFile3, readdir as readdir3 } from "fs/promises";
1004
+ // src/tools/mem-session-end.ts
1005
+ import { writeFile as writeFile9, mkdir as mkdir5 } from "fs/promises";
952
1006
  import { existsSync as existsSync16 } from "fs";
953
1007
  import path7 from "path";
1008
+ import {
1009
+ buildFrontmatter as buildFrontmatter4,
1010
+ loadMemoriesFromDir as loadMemoriesFromDir12,
1011
+ memoryFilePath as memoryFilePath4,
1012
+ serializeMemory as serializeMemory8
1013
+ } from "@hiveai/core";
1014
+ import { z as z16 } from "zod";
1015
+ var MemSessionEndInputSchema = {
1016
+ goal: z16.string().min(1).describe("What you were trying to accomplish this session (1\u20132 sentences)"),
1017
+ accomplished: z16.string().describe("What was actually done \u2014 bullet list recommended"),
1018
+ discoveries: z16.string().default("").describe(
1019
+ "Any bugs, inconsistencies, surprises, or missing knowledge found during this session. Empty if nothing surprising was found."
1020
+ ),
1021
+ files_touched: z16.array(z16.string()).default([]).describe("Key files that were read or modified \u2014 used as anchor paths"),
1022
+ next_steps: z16.string().default("").describe("What should happen next (for the next session or a teammate)"),
1023
+ scope: z16.enum(["personal", "team", "module"]).default("personal").describe("Visibility: personal = private to you, team = shared with the team"),
1024
+ module: z16.string().optional().describe("Module name (required when scope=module)")
1025
+ };
1026
+ function recapTopic(scope, module) {
1027
+ return module ? `session-recap-${scope}-${module}` : `session-recap-${scope}`;
1028
+ }
1029
+ function buildBody(input) {
1030
+ const lines = [];
1031
+ lines.push(`## Goal
1032
+ ${input.goal}`);
1033
+ lines.push(`
1034
+ ## Accomplished
1035
+ ${input.accomplished}`);
1036
+ if (input.discoveries.trim()) {
1037
+ lines.push(`
1038
+ ## Discoveries & surprises
1039
+ ${input.discoveries}`);
1040
+ }
1041
+ if (input.files_touched.length > 0) {
1042
+ lines.push(`
1043
+ ## Files touched
1044
+ ${input.files_touched.map((f) => `- \`${f}\``).join("\n")}`);
1045
+ }
1046
+ if (input.next_steps.trim()) {
1047
+ lines.push(`
1048
+ ## Next steps
1049
+ ${input.next_steps}`);
1050
+ }
1051
+ return lines.join("\n");
1052
+ }
1053
+ async function memSessionEnd(input, ctx) {
1054
+ if (!existsSync16(ctx.paths.haiveDir)) {
1055
+ throw new Error(`No .ai/ directory at ${ctx.paths.root}. Run 'haive init' first.`);
1056
+ }
1057
+ const body = buildBody(input);
1058
+ const topic = recapTopic(input.scope, input.module);
1059
+ const existing = existsSync16(ctx.paths.memoriesDir) ? await loadMemoriesFromDir12(ctx.paths.memoriesDir) : [];
1060
+ const topicMatch = existing.find(
1061
+ ({ memory }) => memory.frontmatter.topic === topic && memory.frontmatter.scope === input.scope && (!input.module || memory.frontmatter.module === input.module)
1062
+ );
1063
+ if (topicMatch) {
1064
+ const fm = topicMatch.memory.frontmatter;
1065
+ const revisionCount = (fm.revision_count ?? 0) + 1;
1066
+ const newFrontmatter = {
1067
+ ...fm,
1068
+ revision_count: revisionCount,
1069
+ anchor: {
1070
+ ...fm.anchor,
1071
+ paths: input.files_touched.length ? input.files_touched : fm.anchor.paths
1072
+ }
1073
+ };
1074
+ await writeFile9(
1075
+ topicMatch.filePath,
1076
+ serializeMemory8({ frontmatter: newFrontmatter, body }),
1077
+ "utf8"
1078
+ );
1079
+ return {
1080
+ id: fm.id,
1081
+ scope: fm.scope,
1082
+ file_path: topicMatch.filePath,
1083
+ action: "updated",
1084
+ revision_count: revisionCount
1085
+ };
1086
+ }
1087
+ const frontmatter = buildFrontmatter4({
1088
+ type: "session_recap",
1089
+ slug: "recap",
1090
+ scope: input.scope,
1091
+ module: input.module,
1092
+ tags: ["session", "recap"],
1093
+ paths: input.files_touched,
1094
+ topic,
1095
+ status: "validated"
1096
+ });
1097
+ const file = memoryFilePath4(
1098
+ ctx.paths,
1099
+ frontmatter.scope,
1100
+ frontmatter.id,
1101
+ frontmatter.module
1102
+ );
1103
+ await mkdir5(path7.dirname(file), { recursive: true });
1104
+ await writeFile9(file, serializeMemory8({ frontmatter, body }), "utf8");
1105
+ return {
1106
+ id: frontmatter.id,
1107
+ scope: frontmatter.scope,
1108
+ file_path: file,
1109
+ action: "created",
1110
+ revision_count: 0
1111
+ };
1112
+ }
1113
+
1114
+ // src/tools/get-briefing.ts
1115
+ import { readFile as readFile3, readdir as readdir3 } from "fs/promises";
1116
+ import { existsSync as existsSync17 } from "fs";
1117
+ import path8 from "path";
954
1118
  import {
955
1119
  allocateBudget,
956
1120
  deriveConfidence as deriveConfidence4,
@@ -959,31 +1123,31 @@ import {
959
1123
  inferModulesFromPaths as inferModulesFromPaths2,
960
1124
  isDecaying,
961
1125
  literalMatchesAllTokens as literalMatchesAllTokens2,
962
- loadMemoriesFromDir as loadMemoriesFromDir12,
1126
+ loadMemoriesFromDir as loadMemoriesFromDir13,
963
1127
  loadUsageIndex as loadUsageIndex7,
964
1128
  memoryMatchesAnchorPaths as memoryMatchesAnchorPaths2,
965
1129
  tokenizeQuery as tokenizeQuery2,
966
1130
  trackReads as trackReads3,
967
1131
  truncateToTokens
968
1132
  } from "@hiveai/core";
969
- import { z as z16 } from "zod";
1133
+ import { z as z17 } from "zod";
970
1134
  var GetBriefingInputSchema = {
971
- task: z16.string().optional().describe(
1135
+ task: z17.string().optional().describe(
972
1136
  "What you are about to do, in 1\u20132 sentences. Used to rank relevant memories semantically."
973
1137
  ),
974
- files: z16.array(z16.string()).default([]).describe("Project-relative file paths the agent is currently looking at or about to edit"),
975
- max_tokens: z16.number().int().positive().default(8e3).describe(
1138
+ files: z17.array(z17.string()).default([]).describe("Project-relative file paths the agent is currently looking at or about to edit"),
1139
+ max_tokens: z17.number().int().positive().default(8e3).describe(
976
1140
  "Approximate token budget for the entire briefing. Each section is allocated a share and truncated to fit."
977
1141
  ),
978
- max_memories: z16.number().int().positive().default(8).describe("Cap on memories surfaced regardless of token budget"),
979
- include_project_context: z16.boolean().default(true),
980
- include_module_contexts: z16.boolean().default(true),
981
- semantic: z16.boolean().default(true).describe(
1142
+ max_memories: z17.number().int().positive().default(8).describe("Cap on memories surfaced regardless of token budget"),
1143
+ include_project_context: z17.boolean().default(true),
1144
+ include_module_contexts: z17.boolean().default(true),
1145
+ semantic: z17.boolean().default(true).describe(
982
1146
  "Use semantic ranking when a task is provided (requires `haive embeddings index`)."
983
1147
  ),
984
- include_stale: z16.boolean().default(false).describe("Include stale memories (excluded by default \u2014 they may be outdated)"),
985
- track: z16.boolean().default(true).describe("Increment read_count on returned memories"),
986
- format: z16.enum(["full", "compact"]).default("full").describe(
1148
+ include_stale: z17.boolean().default(false).describe("Include stale memories (excluded by default \u2014 they may be outdated)"),
1149
+ track: z17.boolean().default(true).describe("Increment read_count on returned memories"),
1150
+ format: z17.enum(["full", "compact"]).default("full").describe(
987
1151
  "Output format: 'full' returns complete memory bodies; 'compact' returns id + 1-line summary only (call mem_get for details)."
988
1152
  )
989
1153
  };
@@ -993,12 +1157,27 @@ async function getBriefing(input, ctx) {
993
1157
  let searchMode = "literal";
994
1158
  let usage = { version: 1, updated_at: "", by_id: {} };
995
1159
  let byId = /* @__PURE__ */ new Map();
996
- if (existsSync16(ctx.paths.memoriesDir)) {
997
- const allLoaded = await loadMemoriesFromDir12(ctx.paths.memoriesDir);
1160
+ let lastSession;
1161
+ if (existsSync17(ctx.paths.memoriesDir)) {
1162
+ const allLoaded = await loadMemoriesFromDir13(ctx.paths.memoriesDir);
1163
+ const recaps = allLoaded.filter(({ memory }) => memory.frontmatter.type === "session_recap").sort(
1164
+ (a, b) => new Date(b.memory.frontmatter.created_at).getTime() - new Date(a.memory.frontmatter.created_at).getTime()
1165
+ );
1166
+ if (recaps.length > 0) {
1167
+ const r = recaps[0];
1168
+ const fm = r.memory.frontmatter;
1169
+ lastSession = {
1170
+ id: fm.id,
1171
+ scope: fm.scope,
1172
+ revision_count: fm.revision_count ?? 0,
1173
+ body: r.memory.body
1174
+ };
1175
+ }
998
1176
  const allMemories = allLoaded.filter(({ memory }) => {
999
1177
  const s = memory.frontmatter.status;
1000
1178
  if (s === "rejected" || s === "deprecated") return false;
1001
1179
  if (!input.include_stale && s === "stale") return false;
1180
+ if (memory.frontmatter.type === "session_recap") return false;
1002
1181
  return true;
1003
1182
  });
1004
1183
  usage = await loadUsageIndex7(ctx.paths);
@@ -1088,7 +1267,7 @@ async function getBriefing(input, ctx) {
1088
1267
  await trackReads3(ctx.paths, memories.map((m) => m.id));
1089
1268
  }
1090
1269
  }
1091
- const projectContext = input.include_project_context && existsSync16(ctx.paths.projectContext) ? await readFile3(ctx.paths.projectContext, "utf8") : "";
1270
+ const projectContext = input.include_project_context && existsSync17(ctx.paths.projectContext) ? await readFile3(ctx.paths.projectContext, "utf8") : "";
1092
1271
  const moduleContents = input.include_module_contexts ? await loadModuleContexts2(ctx, inferred) : [];
1093
1272
  const memoriesText = memories.map((m) => {
1094
1273
  const unverified = m.status === "proposed" ? " [UNVERIFIED \u2014 not yet validated]" : "";
@@ -1154,6 +1333,7 @@ ${m.content}`).join("\n\n---\n\n"),
1154
1333
  ...input.task ? { task: input.task } : {},
1155
1334
  search_mode: searchMode,
1156
1335
  inferred_modules: inferred,
1336
+ ...lastSession ? { last_session: lastSession } : {},
1157
1337
  project_context: projectContext ? { content: projectSlice.text, truncated: projectSlice.truncated } : null,
1158
1338
  module_contexts: trimmedModules,
1159
1339
  memories: outputMemories,
@@ -1189,15 +1369,15 @@ async function trySemanticHits(ctx, task, limit) {
1189
1369
  }
1190
1370
  async function loadModuleContexts2(ctx, modules) {
1191
1371
  if (modules.length === 0) return [];
1192
- if (!existsSync16(ctx.paths.modulesContextDir)) return [];
1372
+ if (!existsSync17(ctx.paths.modulesContextDir)) return [];
1193
1373
  const available = new Set(
1194
1374
  (await readdir3(ctx.paths.modulesContextDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name)
1195
1375
  );
1196
1376
  const out = [];
1197
1377
  for (const m of modules) {
1198
1378
  if (!available.has(m)) continue;
1199
- const file = path7.join(ctx.paths.modulesContextDir, m, "context.md");
1200
- if (existsSync16(file)) {
1379
+ const file = path8.join(ctx.paths.modulesContextDir, m, "context.md");
1380
+ if (existsSync17(file)) {
1201
1381
  out.push({ name: m, content: await readFile3(file, "utf8") });
1202
1382
  }
1203
1383
  }
@@ -1206,11 +1386,11 @@ async function loadModuleContexts2(ctx, modules) {
1206
1386
 
1207
1387
  // src/tools/code-map.ts
1208
1388
  import { loadCodeMap, queryCodeMap } from "@hiveai/core";
1209
- import { z as z17 } from "zod";
1389
+ import { z as z18 } from "zod";
1210
1390
  var CodeMapInputSchema = {
1211
- file: z17.string().optional().describe("Filter to files whose path contains this substring"),
1212
- symbol: z17.string().optional().describe("Filter to files exporting a symbol whose name contains this substring"),
1213
- max_files: z17.number().int().positive().default(40).describe("Cap on returned files")
1391
+ file: z18.string().optional().describe("Filter to files whose path contains this substring"),
1392
+ symbol: z18.string().optional().describe("Filter to files exporting a symbol whose name contains this substring"),
1393
+ max_files: z18.number().int().positive().default(40).describe("Cap on returned files")
1214
1394
  };
1215
1395
  async function codeMapTool(input, ctx) {
1216
1396
  const map = await loadCodeMap(ctx.paths);
@@ -1236,18 +1416,18 @@ async function codeMapTool(input, ctx) {
1236
1416
  }
1237
1417
 
1238
1418
  // src/tools/mem-diff.ts
1239
- import { existsSync as existsSync17 } from "fs";
1240
- import { loadMemoriesFromDir as loadMemoriesFromDir13 } from "@hiveai/core";
1241
- import { z as z18 } from "zod";
1419
+ import { existsSync as existsSync18 } from "fs";
1420
+ import { loadMemoriesFromDir as loadMemoriesFromDir14 } from "@hiveai/core";
1421
+ import { z as z19 } from "zod";
1242
1422
  var MemDiffInputSchema = {
1243
- id_a: z18.string().min(1).describe("First memory id"),
1244
- id_b: z18.string().min(1).describe("Second memory id")
1423
+ id_a: z19.string().min(1).describe("First memory id"),
1424
+ id_b: z19.string().min(1).describe("Second memory id")
1245
1425
  };
1246
1426
  async function memDiff(input, ctx) {
1247
- if (!existsSync17(ctx.paths.memoriesDir)) {
1427
+ if (!existsSync18(ctx.paths.memoriesDir)) {
1248
1428
  throw new Error(`No .ai/memories at ${ctx.paths.root}.`);
1249
1429
  }
1250
- const all = await loadMemoriesFromDir13(ctx.paths.memoriesDir);
1430
+ const all = await loadMemoriesFromDir14(ctx.paths.memoriesDir);
1251
1431
  const foundA = all.find((m) => m.memory.frontmatter.id === input.id_a);
1252
1432
  const foundB = all.find((m) => m.memory.frontmatter.id === input.id_b);
1253
1433
  if (!foundA) throw new Error(`No memory with id "${input.id_a}".`);
@@ -1281,12 +1461,12 @@ async function memDiff(input, ctx) {
1281
1461
  }
1282
1462
 
1283
1463
  // src/prompts/bootstrap-project.ts
1284
- import { z as z19 } from "zod";
1464
+ import { z as z20 } from "zod";
1285
1465
  var BootstrapProjectArgsSchema = {
1286
- module: z19.string().optional().describe(
1466
+ module: z20.string().optional().describe(
1287
1467
  "Optional module name to scope the analysis to (writes to .ai/modules/<module>/context.md)"
1288
1468
  ),
1289
- focus: z19.string().optional().describe("Optional area to emphasize (e.g. 'data layer', 'API surface')")
1469
+ focus: z20.string().optional().describe("Optional area to emphasize (e.g. 'data layer', 'API surface')")
1290
1470
  };
1291
1471
  var ROOT_TEMPLATE = `# Project context
1292
1472
 
@@ -1368,10 +1548,10 @@ ${template}\`\`\`
1368
1548
  }
1369
1549
 
1370
1550
  // src/prompts/post-task.ts
1371
- import { z as z20 } from "zod";
1551
+ import { z as z21 } from "zod";
1372
1552
  var PostTaskArgsSchema = {
1373
- task_summary: z20.string().optional().describe("One sentence describing what you just did"),
1374
- files_touched: z20.array(z20.string()).optional().describe("Files you created or modified during the task")
1553
+ task_summary: z21.string().optional().describe("One sentence describing what you just did"),
1554
+ files_touched: z21.array(z21.string()).optional().describe("Files you created or modified during the task")
1375
1555
  };
1376
1556
  function postTaskPrompt(args, ctx) {
1377
1557
  const taskLine = args.task_summary ? `
@@ -1428,7 +1608,18 @@ Examples of things to look for:
1428
1608
  - Skip sections where you genuinely have nothing to add. Don't fabricate memories.
1429
1609
  - **Question 0 is not optional** \u2014 always scan your exploration history for code-level discoveries.
1430
1610
 
1431
- When done, respond with a brief summary: "Saved N memories: [list of IDs]" or "Nothing new to save."
1611
+ ### 6. Close the session \u2014 always
1612
+ Call **\`mem_session_end\`** with:
1613
+ - \`goal\`: what you set out to do
1614
+ - \`accomplished\`: what was actually done (bullet list)
1615
+ - \`discoveries\`: anything surprising or broken found during this session (leave empty if none)
1616
+ - \`files_touched\`: the key files you read or modified
1617
+ - \`next_steps\`: what remains for the next session or a teammate
1618
+ - \`scope\`: "team" if this task affects the whole team, "personal" otherwise
1619
+
1620
+ This creates/updates a single rolling recap that **get_briefing automatically surfaces** at the start of every subsequent session \u2014 no token waste re-explaining what happened.
1621
+
1622
+ When done, respond with a brief summary: "Saved N memories: [list of IDs]. Session recap saved."
1432
1623
  `;
1433
1624
  return {
1434
1625
  description: "Post-task reflection: capture what you learned before closing the session",
@@ -1442,12 +1633,12 @@ When done, respond with a brief summary: "Saved N memories: [list of IDs]" or "N
1442
1633
  }
1443
1634
 
1444
1635
  // src/prompts/import-docs.ts
1445
- import { z as z21 } from "zod";
1636
+ import { z as z22 } from "zod";
1446
1637
  var ImportDocsArgsSchema = {
1447
- content: z21.string().describe("The documentation content to analyze and import as memories (Markdown, README, ADR, etc.)"),
1448
- source: z21.string().optional().describe("Origin of the content (file path, URL, or document title) \u2014 used to anchor memories"),
1449
- scope: z21.enum(["personal", "team"]).default("team").describe("Scope to assign to created memories"),
1450
- dry_run: z21.boolean().default(false).describe("If true, describe what would be saved without actually calling mem_save")
1638
+ content: z22.string().describe("The documentation content to analyze and import as memories (Markdown, README, ADR, etc.)"),
1639
+ source: z22.string().optional().describe("Origin of the content (file path, URL, or document title) \u2014 used to anchor memories"),
1640
+ scope: z22.enum(["personal", "team"]).default("team").describe("Scope to assign to created memories"),
1641
+ dry_run: z22.boolean().default(false).describe("If true, describe what would be saved without actually calling mem_save")
1451
1642
  };
1452
1643
  function importDocsPrompt(args, ctx) {
1453
1644
  const sourceLine = args.source ? `
@@ -1512,7 +1703,7 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
1512
1703
 
1513
1704
  // src/server.ts
1514
1705
  var SERVER_NAME = "haive";
1515
- var SERVER_VERSION = "0.2.11";
1706
+ var SERVER_VERSION = "0.2.13";
1516
1707
  function jsonResult(data) {
1517
1708
  return {
1518
1709
  content: [
@@ -1637,6 +1828,12 @@ function createHaiveServer(options = {}) {
1637
1828
  MemObserveInputSchema,
1638
1829
  async (input) => jsonResult(await memObserve(input, context))
1639
1830
  );
1831
+ server.tool(
1832
+ "mem_session_end",
1833
+ "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.",
1834
+ MemSessionEndInputSchema,
1835
+ async (input) => jsonResult(await memSessionEnd(input, context))
1836
+ );
1640
1837
  server.prompt(
1641
1838
  "bootstrap_project",
1642
1839
  "Instructions for the AI client to analyze the project and save the context.",