@hiveai/mcp 0.9.3 → 0.9.6

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
@@ -321,6 +321,7 @@ import {
321
321
  loadMemoriesFromDir as loadMemoriesFromDir3,
322
322
  loadUsageIndex,
323
323
  pickSnippetNeedle,
324
+ rankMemoriesLexical,
324
325
  tokenizeQuery,
325
326
  trackReads
326
327
  } from "@hiveai/core";
@@ -337,6 +338,9 @@ var MemSearchInputSchema = {
337
338
  semantic: z5.boolean().default(false).describe(
338
339
  "Use semantic similarity from the embeddings index (requires `haive embeddings index`)."
339
340
  ),
341
+ lexical_rank: z5.boolean().default(false).describe(
342
+ "When true (and semantic is false), rank the filtered corpus with Okapi-BM25-style lexical scoring instead of literal AND/OR. Helps phrase-like queries without embeddings."
343
+ ),
340
344
  min_score: z5.number().min(0).max(1).default(0).describe("Minimum cosine similarity (semantic mode only)"),
341
345
  track: z5.boolean().default(true).describe("Increment read_count on returned memories (used for passive validation)")
342
346
  };
@@ -362,6 +366,27 @@ async function memSearch(input, ctx) {
362
366
  notice: "Semantic search unavailable (embeddings index missing or @hiveai/embeddings not installed). Falling back to literal search."
363
367
  };
364
368
  }
369
+ } else if (input.lexical_rank && input.query.trim()) {
370
+ const { ranked, scores } = rankMemoriesLexical(
371
+ filtered,
372
+ input.query,
373
+ input.limit
374
+ );
375
+ if (ranked.length > 0) {
376
+ const snippetNeedle = pickSnippetNeedle(input.query);
377
+ result = {
378
+ matches: ranked.map(
379
+ (loaded, i) => lexicalHit(loaded, snippetNeedle, usage, scores[i])
380
+ ),
381
+ total: ranked.length,
382
+ mode: "lexical_ranked"
383
+ };
384
+ } else {
385
+ result = {
386
+ ...buildLiteralResult(input, filtered, usage),
387
+ notice: "Lexical ranking found no BM25-positive hits \u2014 showing literal matches instead."
388
+ };
389
+ }
365
390
  } else {
366
391
  result = buildLiteralResult(input, filtered, usage);
367
392
  }
@@ -459,6 +484,9 @@ function toHit(loaded, needle, usage) {
459
484
  file_path: loaded.filePath
460
485
  };
461
486
  }
487
+ function lexicalHit(loaded, needle, usage, lexicalScore) {
488
+ return { ...toHit(loaded, needle, usage), score: lexicalScore };
489
+ }
462
490
 
463
491
  // src/tools/mem-verify.ts
464
492
  import { writeFile as writeFile3 } from "fs/promises";
@@ -1099,7 +1127,11 @@ import {
1099
1127
  import { z as z16 } from "zod";
1100
1128
 
1101
1129
  // src/session-tracker.ts
1102
- import { appendUsageEvent, loadConfig as loadConfig2 } from "@hiveai/core";
1130
+ import {
1131
+ appendUsageEvent,
1132
+ appendRuntimeJournalEntry,
1133
+ loadConfig as loadConfig2
1134
+ } from "@hiveai/core";
1103
1135
  import { mkdir as mkdir5, writeFile as writeFile9, rm } from "fs/promises";
1104
1136
  import { existsSync as existsSync16 } from "fs";
1105
1137
  import path7 from "path";
@@ -1172,6 +1204,14 @@ var SessionTracker = class {
1172
1204
  recapId = result.id;
1173
1205
  } catch {
1174
1206
  }
1207
+ void appendRuntimeJournalEntry(this.ctx.paths, {
1208
+ kind: "session_end",
1209
+ message: recapId ? `auto session close | ${toolSummary} | recap:${recapId}` : `auto session close | ${toolSummary}`,
1210
+ meta: {
1211
+ recap_id: recapId ?? null,
1212
+ total_tool_calls: totalCalls
1213
+ }
1214
+ });
1175
1215
  const ranPostTask = this.events.some(
1176
1216
  (e) => e.tool === "mem_session_end" && !e.summary?.startsWith("Auto-captured")
1177
1217
  );
@@ -3040,13 +3080,139 @@ function gitFileDiff(root, file, sinceDays) {
3040
3080
  }
3041
3081
  }
3042
3082
 
3043
- // src/prompts/bootstrap-project.ts
3083
+ // src/tools/mem-conflict-candidates.ts
3084
+ import { existsSync as existsSync27 } from "fs";
3085
+ import {
3086
+ findLexicalConflictPairs,
3087
+ findTopicStatusConflictPairs,
3088
+ loadMemoriesFromDir as loadMemoriesFromDir21
3089
+ } from "@hiveai/core";
3044
3090
  import { z as z30 } from "zod";
3091
+ var MemConflictCandidatesInputSchema = {
3092
+ since_days: z30.number().int().positive().max(3650).default(365).describe("Only memories created since N days ago"),
3093
+ types: z30.array(z30.enum(["decision", "architecture", "convention", "gotcha"])).default(["decision", "architecture"]).describe("Memory types scanned for pairwise lexical overlap"),
3094
+ min_jaccard: z30.number().min(0).max(1).default(0.45).describe("Minimum Jaccard token similarity to surface as a candidate pair"),
3095
+ max_pairs: z30.number().int().positive().max(100).default(20).describe("Cap pairs returned"),
3096
+ max_scan: z30.number().int().positive().max(2e3).default(500).describe("Maximum memories sampled for O(n\xB2) scan \u2014 excess dropped after chronological sort."),
3097
+ max_topic_pairs: z30.number().int().positive().max(100).default(20).describe(
3098
+ "Cap for extra signal: memories sharing the same topic with validated vs rejected status."
3099
+ )
3100
+ };
3101
+ async function memConflictCandidates(input, ctx) {
3102
+ if (!existsSync27(ctx.paths.memoriesDir)) {
3103
+ return {
3104
+ pairs: [],
3105
+ topic_status_pairs: [],
3106
+ scanned: 0,
3107
+ truncated: false,
3108
+ notice: "No .ai/memories directory."
3109
+ };
3110
+ }
3111
+ const all = await loadMemoriesFromDir21(ctx.paths.memoriesDir);
3112
+ const { pairs, scanned, truncated } = findLexicalConflictPairs(all, {
3113
+ sinceDays: input.since_days,
3114
+ types: input.types,
3115
+ minJaccard: input.min_jaccard,
3116
+ maxPairs: input.max_pairs,
3117
+ maxScan: input.max_scan
3118
+ });
3119
+ const topicStatusPairs = findTopicStatusConflictPairs(all, input.max_topic_pairs);
3120
+ const notice = pairs.length === 0 && topicStatusPairs.length === 0 ? "No lexical or topic-status candidates \u2014 widen since_days/types or lower min_jaccard." : void 0;
3121
+ return { pairs, topic_status_pairs: topicStatusPairs, scanned, truncated, notice };
3122
+ }
3123
+
3124
+ // src/tools/mem-resolve-project.ts
3125
+ import { resolveProjectInfo } from "@hiveai/core";
3126
+ import { z as z31 } from "zod";
3127
+ var MemResolveProjectInputSchema = {
3128
+ cwd: z31.string().optional().describe("Directory used for root discovery when HAIVE_PROJECT_ROOT is unset.")
3129
+ };
3130
+ async function memResolveProject(input, _ctx) {
3131
+ void _ctx;
3132
+ return {
3133
+ ok: true,
3134
+ info: resolveProjectInfo({
3135
+ cwd: input.cwd
3136
+ })
3137
+ };
3138
+ }
3139
+
3140
+ // src/tools/mem-suggest-topic.ts
3141
+ import { MemoryTypeSchema, suggestTopicKey } from "@hiveai/core";
3142
+ import { z as z32 } from "zod";
3143
+ var MemSuggestTopicInputSchema = {
3144
+ type: MemoryTypeSchema.describe("Memory kind \u2014 drives the suggested topic family."),
3145
+ title: z32.string().min(1).describe("Short title or phrase (headers, headings) \u2014 turned into slug")
3146
+ };
3147
+ async function memSuggestTopic(input, _ctx) {
3148
+ void _ctx;
3149
+ const suggestion = suggestTopicKey(input.type, input.title);
3150
+ return { topic_key: suggestion.topic_key, family: suggestion.family, type: input.type };
3151
+ }
3152
+
3153
+ // src/tools/mem-timeline.ts
3154
+ import { existsSync as existsSync28 } from "fs";
3155
+ import { collectTimelineEntries, loadMemoriesFromDir as loadMemoriesFromDir22 } from "@hiveai/core";
3156
+ import { z as z33 } from "zod";
3157
+ var MemTimelineInputSchema = {
3158
+ memory_id: z33.string().optional().describe("Seed id \u2014 expands via related_ids, topic, anchors"),
3159
+ topic: z33.string().optional().describe("Frontmatter.topic value \u2014 chronological list when memory_id omitted"),
3160
+ limit: z33.number().int().positive().max(100).default(30).describe("Max timeline entries returned")
3161
+ };
3162
+ async function memTimeline(input, ctx) {
3163
+ if (!existsSync28(ctx.paths.memoriesDir)) {
3164
+ return { entries: [], total: 0, notice: "No .ai/memories directory." };
3165
+ }
3166
+ const all = await loadMemoriesFromDir22(ctx.paths.memoriesDir);
3167
+ const { entries, notice } = collectTimelineEntries(all, {
3168
+ memoryId: input.memory_id,
3169
+ topic: input.topic,
3170
+ limit: input.limit
3171
+ });
3172
+ return { entries, total: entries.length, notice };
3173
+ }
3174
+
3175
+ // src/tools/runtime-journal-append.ts
3176
+ import { appendRuntimeJournalEntry as appendRuntimeJournalEntry2 } from "@hiveai/core";
3177
+ import { z as z34 } from "zod";
3178
+ var RuntimeJournalAppendInputSchema = {
3179
+ message: z34.string().min(1).describe("Short line to append to the runtime session journal"),
3180
+ kind: z34.enum(["note", "session_end", "mcp"]).default("note"),
3181
+ tool: z34.string().optional().describe("When kind=mcp, which tool name (optional)")
3182
+ };
3183
+ async function runtimeJournalAppend(input, ctx) {
3184
+ await appendRuntimeJournalEntry2(ctx.paths, {
3185
+ kind: input.kind,
3186
+ message: input.message,
3187
+ ...input.tool ? { tool: input.tool } : {}
3188
+ });
3189
+ return {
3190
+ ok: true,
3191
+ path_hint: `${ctx.paths.runtimeDir}/session-journal.ndjson`
3192
+ };
3193
+ }
3194
+
3195
+ // src/tools/runtime-journal-tail.ts
3196
+ import { readRuntimeJournalTail } from "@hiveai/core";
3197
+ import { z as z35 } from "zod";
3198
+ var RuntimeJournalTailInputSchema = {
3199
+ limit: z35.number().int().positive().max(500).default(30).describe("Last N journal entries to return")
3200
+ };
3201
+ async function runtimeJournalTail(input, ctx) {
3202
+ const entries = await readRuntimeJournalTail(ctx.paths, input.limit);
3203
+ if (entries.length === 0) {
3204
+ return { entries: [], empty: true };
3205
+ }
3206
+ return { entries };
3207
+ }
3208
+
3209
+ // src/prompts/bootstrap-project.ts
3210
+ import { z as z36 } from "zod";
3045
3211
  var BootstrapProjectArgsSchema = {
3046
- module: z30.string().optional().describe(
3212
+ module: z36.string().optional().describe(
3047
3213
  "Optional module name to scope the analysis to (writes to .ai/modules/<module>/context.md)"
3048
3214
  ),
3049
- focus: z30.string().optional().describe("Optional area to emphasize (e.g. 'data layer', 'API surface')")
3215
+ focus: z36.string().optional().describe("Optional area to emphasize (e.g. 'data layer', 'API surface')")
3050
3216
  };
3051
3217
  var ROOT_TEMPLATE = `# Project context
3052
3218
 
@@ -3128,10 +3294,10 @@ ${template}\`\`\`
3128
3294
  }
3129
3295
 
3130
3296
  // src/prompts/post-task.ts
3131
- import { z as z31 } from "zod";
3297
+ import { z as z37 } from "zod";
3132
3298
  var PostTaskArgsSchema = {
3133
- task_summary: z31.string().optional().describe("One sentence describing what you just did"),
3134
- files_touched: z31.array(z31.string()).optional().describe("Files you created or modified during the task")
3299
+ task_summary: z37.string().optional().describe("One sentence describing what you just did"),
3300
+ files_touched: z37.array(z37.string()).optional().describe("Files you created or modified during the task")
3135
3301
  };
3136
3302
  function postTaskPrompt(args, ctx) {
3137
3303
  const taskLine = args.task_summary ? `
@@ -3215,12 +3381,12 @@ When done, respond with a brief summary: "Saved N memories: [list of IDs]. Sessi
3215
3381
  }
3216
3382
 
3217
3383
  // src/prompts/import-docs.ts
3218
- import { z as z32 } from "zod";
3384
+ import { z as z38 } from "zod";
3219
3385
  var ImportDocsArgsSchema = {
3220
- content: z32.string().describe("The documentation content to analyze and import as memories (Markdown, README, ADR, etc.)"),
3221
- source: z32.string().optional().describe("Origin of the content (file path, URL, or document title) \u2014 used to anchor memories"),
3222
- scope: z32.enum(["personal", "team"]).default("team").describe("Scope to assign to created memories"),
3223
- dry_run: z32.boolean().default(false).describe("If true, describe what would be saved without actually calling mem_save")
3386
+ content: z38.string().describe("The documentation content to analyze and import as memories (Markdown, README, ADR, etc.)"),
3387
+ source: z38.string().optional().describe("Origin of the content (file path, URL, or document title) \u2014 used to anchor memories"),
3388
+ scope: z38.enum(["personal", "team"]).default("team").describe("Scope to assign to created memories"),
3389
+ dry_run: z38.boolean().default(false).describe("If true, describe what would be saved without actually calling mem_save")
3224
3390
  };
3225
3391
  function importDocsPrompt(args, ctx) {
3226
3392
  const sourceLine = args.source ? `
@@ -3285,7 +3451,7 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
3285
3451
 
3286
3452
  // src/server.ts
3287
3453
  var SERVER_NAME = "haive";
3288
- var SERVER_VERSION = "0.9.3";
3454
+ var SERVER_VERSION = "0.9.6";
3289
3455
  function jsonResult(data) {
3290
3456
  return {
3291
3457
  content: [
@@ -3336,6 +3502,23 @@ function createHaiveServer(options = {}) {
3336
3502
  return jsonResult(await memSave(input, context));
3337
3503
  }
3338
3504
  );
3505
+ server.tool(
3506
+ "mem_suggest_topic",
3507
+ [
3508
+ "Propose a stable `topic` key (topic-upsert) from type + short title.",
3509
+ "",
3510
+ "USE BEFORE mem_save when you want deterministic updates to the same memory over time;",
3511
+ "families mimic Engram-style grouping (architecture/*, bug/*, decision/*, \u2026).",
3512
+ "",
3513
+ "PARAMETERS:",
3514
+ " type \u2014 convention | decision | gotcha | architecture | glossary | attempt | session_recap",
3515
+ " title \u2014 phrase to slugify under the suggested family prefix",
3516
+ "",
3517
+ "RETURNS: { topic_key, family, type }"
3518
+ ].join("\n"),
3519
+ MemSuggestTopicInputSchema,
3520
+ async (input) => jsonResult(await memSuggestTopic(input, context))
3521
+ );
3339
3522
  server.tool(
3340
3523
  "mem_tried",
3341
3524
  [
@@ -3427,8 +3610,12 @@ function createHaiveServer(options = {}) {
3427
3610
  server.tool(
3428
3611
  "get_briefing",
3429
3612
  [
3430
- "\u2B50 CALL THIS FIRST at the start of every task. One-shot onboarding that returns",
3431
- "everything relevant in a single call under a token budget.",
3613
+ "\u2B50 DEFAULT-FIRST for coding agents on any repo where `haive init` ran: call this BEFORE",
3614
+ "changing source or project config for the current goal (unless the developer explicitly opts out).",
3615
+ "One-shot onboarding: everything relevant in a single call under a token budget.",
3616
+ "",
3617
+ "PROGRESSIVE DISCLOSURE \u2014 after this, drill down only if needed:",
3618
+ " mem_relevant_to / mem_search (compact lists) \u2192 mem_get (full body + anchors).",
3432
3619
  "",
3433
3620
  "RETURNS (in order of priority):",
3434
3621
  " 0. action_required \u2014 \u26A0\uFE0F HANDLE THIS FIRST if non-empty (see protocol below)",
@@ -3463,7 +3650,7 @@ function createHaiveServer(options = {}) {
3463
3650
  " low \u2014 proposed, few reads (take with caution)",
3464
3651
  " unverified \u2014 draft (unverified: true flag set)",
3465
3652
  "",
3466
- "Replaces 4\u20135 separate tool calls. Always call this before any other tool."
3653
+ "Replaces 4\u20135 separate tool calls. Prefer this first; use mem_search / mem_get only for follow-up."
3467
3654
  ].join("\n"),
3468
3655
  GetBriefingInputSchema,
3469
3656
  async (input) => {
@@ -3482,6 +3669,8 @@ function createHaiveServer(options = {}) {
3482
3669
  "SEARCH MODES:",
3483
3670
  " Literal (default): AND search across id, tags, and body \u2014 all tokens must match.",
3484
3671
  " Falls back to OR automatically if no AND results (partial match).",
3672
+ " Lexical rank (lexical_rank: true, semantic: false): Okapi-BM25-style scoring on the",
3673
+ " filtered corpus \u2014 good for phrase-like queries without embeddings.",
3485
3674
  " Semantic (semantic: true): embedding-based similarity \u2014 finds related memories",
3486
3675
  " even with different wording. Requires haive embeddings index to be built.",
3487
3676
  "",
@@ -3490,6 +3679,7 @@ function createHaiveServer(options = {}) {
3490
3679
  " scope \u2014 filter by personal | team | module",
3491
3680
  " type \u2014 filter by convention | decision | gotcha | architecture | glossary",
3492
3681
  " semantic \u2014 true for embedding-based search (requires @hiveai/embeddings)",
3682
+ " lexical_rank \u2014 BM25-style ranking (ignored when semantic is true)",
3493
3683
  " limit \u2014 max results (default 10)",
3494
3684
  "",
3495
3685
  "RETURNS: array of { id, type, scope, status, confidence, body, match_quality }"
@@ -3500,6 +3690,22 @@ function createHaiveServer(options = {}) {
3500
3690
  return jsonResult(await memSearch(input, context));
3501
3691
  }
3502
3692
  );
3693
+ server.tool(
3694
+ "mem_timeline",
3695
+ [
3696
+ "Chronological view of related memories: by shared frontmatter.topic OR expanded from a seed id",
3697
+ "(related_ids, same topic, overlapping anchor paths \u2014 one extra hop on related_ids).",
3698
+ "",
3699
+ "PARAMETERS:",
3700
+ " memory_id \u2014 optional seed memory id",
3701
+ " topic \u2014 optional topic key (required if memory_id omitted)",
3702
+ " limit \u2014 max entries (default 30)",
3703
+ "",
3704
+ "RETURNS: { entries: [{ id, type, scope, created_at, one_line, topic? }], total, notice? }"
3705
+ ].join("\n"),
3706
+ MemTimelineInputSchema,
3707
+ async (input) => jsonResult(await memTimeline(input, context))
3708
+ );
3503
3709
  server.tool(
3504
3710
  "mem_for_files",
3505
3711
  [
@@ -3529,7 +3735,7 @@ function createHaiveServer(options = {}) {
3529
3735
  [
3530
3736
  "Fetch a single memory by its full id with all details.",
3531
3737
  "",
3532
- "USE WHEN get_briefing returned a memory in 'compact' format and you need",
3738
+ "USE WHEN get_briefing / mem_relevant_to / mem_search returned a compact hit and you need",
3533
3739
  "the full body, or when you know the exact id of a memory.",
3534
3740
  "",
3535
3741
  "PARAMETERS:",
@@ -3617,6 +3823,22 @@ function createHaiveServer(options = {}) {
3617
3823
  CodeMapInputSchema,
3618
3824
  async (input) => jsonResult(await codeMapTool(input, context))
3619
3825
  );
3826
+ server.tool(
3827
+ "mem_resolve_project",
3828
+ [
3829
+ "Diagnostics: resolve which project root hAIve is using (never throws).",
3830
+ "",
3831
+ "USE IN multi-root workspaces or when the agent CWD may not be the repo root \u2014",
3832
+ "mirrors HAIVE_PROJECT_ROOT, findProjectRoot markers, and presence of .ai/memories.",
3833
+ "",
3834
+ "PARAMETERS:",
3835
+ " cwd \u2014 optional directory used for discovery when HAIVE_PROJECT_ROOT is unset",
3836
+ "",
3837
+ "RETURNS: { ok: true, info: { cwd, resolved_root, haive_project_root_env, \u2026 } }"
3838
+ ].join("\n"),
3839
+ MemResolveProjectInputSchema,
3840
+ async (input) => jsonResult(await memResolveProject(input, context))
3841
+ );
3620
3842
  server.tool(
3621
3843
  "mem_update",
3622
3844
  [
@@ -3750,6 +3972,8 @@ function createHaiveServer(options = {}) {
3750
3972
  "One-shot ranked memories for a task \u2014 use instead of get_briefing when",
3751
3973
  "project context is already loaded and you only want the relevant memory layer.",
3752
3974
  "",
3975
+ "Second step in progressive disclosure (after get_briefing): narrow here, then mem_get for full text.",
3976
+ "",
3753
3977
  "Reuses the same ranking pipeline (anchor / module / literal / semantic) but",
3754
3978
  "skips project_context, modules, action_required, etc.",
3755
3979
  "",
@@ -3907,6 +4131,53 @@ function createHaiveServer(options = {}) {
3907
4131
  return jsonResult(await memConflicts(input, context));
3908
4132
  }
3909
4133
  );
4134
+ server.tool(
4135
+ "mem_conflict_candidates",
4136
+ [
4137
+ "Bulk scan for conflict CANDIDATES (not proof):",
4138
+ "",
4139
+ " 1. Lexical similarity (Jaccard) on decision/architecture-like pairs",
4140
+ " 2. Same frontmatter.topic with validated vs rejected \u2014 quick human-review signal",
4141
+ "",
4142
+ "Advisory only \u2014 follow with mem_conflicts_with on specific ids.",
4143
+ "",
4144
+ "PARAMETERS:",
4145
+ " since_days, types, min_jaccard, max_pairs, max_scan, max_topic_pairs",
4146
+ "",
4147
+ "RETURNS: { pairs, topic_status_pairs, scanned, truncated, notice? }"
4148
+ ].join("\n"),
4149
+ MemConflictCandidatesInputSchema,
4150
+ async (input) => {
4151
+ tracker.record("mem_conflict_candidates", `${input.since_days}d`);
4152
+ return jsonResult(await memConflictCandidates(input, context));
4153
+ }
4154
+ );
4155
+ server.tool(
4156
+ "runtime_journal_append",
4157
+ [
4158
+ "Append one line to `.ai/.runtime/session-journal.ndjson` \u2014 machine-local session continuity.",
4159
+ "",
4160
+ "Does NOT replace team memories; complements mem_session_end recaps for local traces.",
4161
+ "",
4162
+ "PARAMETERS: message, kind (note|session_end|mcp), optional tool",
4163
+ "",
4164
+ "RETURNS: { ok, path_hint }"
4165
+ ].join("\n"),
4166
+ RuntimeJournalAppendInputSchema,
4167
+ async (input) => jsonResult(await runtimeJournalAppend(input, context))
4168
+ );
4169
+ server.tool(
4170
+ "runtime_journal_tail",
4171
+ [
4172
+ "Read the last N entries from the runtime session journal (parsed JSON lines).",
4173
+ "",
4174
+ "PARAMETERS: limit (default 30, max 500)",
4175
+ "",
4176
+ "RETURNS: { entries: [...], empty?: true }"
4177
+ ].join("\n"),
4178
+ RuntimeJournalTailInputSchema,
4179
+ async (input) => jsonResult(await runtimeJournalTail(input, context))
4180
+ );
3910
4181
  server.tool(
3911
4182
  "pre_commit_check",
3912
4183
  [