@hiveai/mcp 0.2.11 → 0.2.12

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