@contextstream/mcp-server 0.4.64 → 0.4.66

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
@@ -1720,7 +1720,7 @@ function buildHooksConfig(options) {
1720
1720
  {
1721
1721
  type: "command",
1722
1722
  command: getHookCommand("session-start"),
1723
- timeout: 10
1723
+ timeout: 15
1724
1724
  }
1725
1725
  ]
1726
1726
  }
@@ -4879,9 +4879,11 @@ function readHookInput() {
4879
4879
  }
4880
4880
  }
4881
4881
  function writeHookOutput(output) {
4882
+ const eventName = output?.hookEventName || (typeof process.env.HOOK_EVENT_NAME === "string" ? process.env.HOOK_EVENT_NAME.trim() : "");
4883
+ const canUseHookSpecificOutput = HOOK_SPECIFIC_OUTPUT_EVENTS.has(eventName);
4882
4884
  const payload = output && (output.additionalContext || output.blocked || output.reason) ? {
4883
- hookSpecificOutput: output.additionalContext ? {
4884
- hookEventName: output.hookEventName,
4885
+ hookSpecificOutput: output.additionalContext && canUseHookSpecificOutput ? {
4886
+ hookEventName: eventName,
4885
4887
  additionalContext: output.additionalContext
4886
4888
  } : void 0,
4887
4889
  additionalContext: output.additionalContext,
@@ -5082,11 +5084,12 @@ async function fetchFastContext(config, body) {
5082
5084
  return null;
5083
5085
  }
5084
5086
  }
5085
- var DEFAULT_API_URL2;
5087
+ var DEFAULT_API_URL2, HOOK_SPECIFIC_OUTPUT_EVENTS;
5086
5088
  var init_common = __esm({
5087
5089
  "src/hooks/common.ts"() {
5088
5090
  "use strict";
5089
5091
  DEFAULT_API_URL2 = "https://api.contextstream.io";
5092
+ HOOK_SPECIFIC_OUTPUT_EVENTS = /* @__PURE__ */ new Set(["PreToolUse", "UserPromptSubmit", "PostToolUse"]);
5090
5093
  }
5091
5094
  });
5092
5095
 
@@ -7107,10 +7110,7 @@ After compaction, call session_init(is_post_compact=true) to restore context.${c
7107
7110
  User instructions: ${customInstructions}` : ""}`;
7108
7111
  console.log(
7109
7112
  JSON.stringify({
7110
- hookSpecificOutput: {
7111
- hookEventName: "PreCompact",
7112
- additionalContext: context
7113
- }
7113
+ additionalContext: context
7114
7114
  })
7115
7115
  );
7116
7116
  process.exit(0);
@@ -7542,10 +7542,7 @@ async function runSessionInitHook() {
7542
7542
  });
7543
7543
  console.log(
7544
7544
  JSON.stringify({
7545
- hookSpecificOutput: {
7546
- hookEventName: "SessionStart",
7547
- additionalContext: formattedContext
7548
- }
7545
+ additionalContext: formattedContext
7549
7546
  })
7550
7547
  );
7551
7548
  process.exit(0);
@@ -12913,6 +12910,46 @@ function normalizeNodeType(input) {
12913
12910
  );
12914
12911
  }
12915
12912
  }
12913
+ function normalizeTags(tags) {
12914
+ if (!tags || tags.length === 0) return void 0;
12915
+ const normalized = Array.from(
12916
+ new Set(
12917
+ tags.map((tag) => String(tag ?? "").trim()).filter(Boolean)
12918
+ )
12919
+ );
12920
+ return normalized.length > 0 ? normalized : void 0;
12921
+ }
12922
+ function extractEventTags(item) {
12923
+ const tags = [];
12924
+ if (Array.isArray(item.tags)) {
12925
+ tags.push(...item.tags.filter((t) => typeof t === "string"));
12926
+ }
12927
+ const metaTags = item.metadata?.tags;
12928
+ if (Array.isArray(metaTags)) {
12929
+ tags.push(...metaTags.filter((t) => typeof t === "string"));
12930
+ }
12931
+ return tags;
12932
+ }
12933
+ function extractEffectiveEventType(item) {
12934
+ for (const field of ["event_type", "node_type", "type"]) {
12935
+ const val = item[field];
12936
+ if (typeof val === "string" && val.trim()) return val.trim();
12937
+ }
12938
+ for (const field of ["original_type", "node_type", "event_type", "type"]) {
12939
+ const val = item.metadata?.[field];
12940
+ if (typeof val === "string" && val.trim()) return val.trim();
12941
+ }
12942
+ return "unknown";
12943
+ }
12944
+ function isLessonResult(item) {
12945
+ const effectiveType = extractEffectiveEventType(item);
12946
+ if (effectiveType === "lesson") return true;
12947
+ const tags = extractEventTags(item);
12948
+ if (tags.some((t) => t === "lesson" || t === "lesson_system")) return true;
12949
+ const content = typeof item.content === "string" ? item.content : "";
12950
+ if (content.includes("### Prevention") && content.includes("### Trigger")) return true;
12951
+ return false;
12952
+ }
12916
12953
  function pickString(value) {
12917
12954
  if (typeof value !== "string") return null;
12918
12955
  const trimmed = value.trim();
@@ -13448,7 +13485,12 @@ var ContextStreamClient = class _ContextStreamClient {
13448
13485
  if (!body.content || body.content.trim().length === 0) {
13449
13486
  throw new Error("content is required and cannot be empty");
13450
13487
  }
13451
- return request(this.config, "/memory/events", { body: withDefaults });
13488
+ const normalizedTags = normalizeTags(body.tags);
13489
+ const apiBody = {
13490
+ ...withDefaults,
13491
+ ...normalizedTags ? { tags: normalizedTags } : {}
13492
+ };
13493
+ return request(this.config, "/memory/events", { body: apiBody });
13452
13494
  }
13453
13495
  bulkIngestEvents(body) {
13454
13496
  return request(this.config, "/memory/events/ingest", { body: this.withDefaults(body) });
@@ -13461,6 +13503,8 @@ var ContextStreamClient = class _ContextStreamClient {
13461
13503
  const query = new URLSearchParams();
13462
13504
  if (params?.limit) query.set("limit", String(params.limit));
13463
13505
  if (withDefaults.project_id) query.set("project_id", withDefaults.project_id);
13506
+ if (params?.event_type) query.set("event_type", params.event_type);
13507
+ if (params?.tags && params.tags.length > 0) query.set("tags", params.tags.join(","));
13464
13508
  const suffix = query.toString() ? `?${query.toString()}` : "";
13465
13509
  return request(this.config, `/memory/events/workspace/${withDefaults.workspace_id}${suffix}`, {
13466
13510
  method: "GET"
@@ -14863,7 +14907,7 @@ var ContextStreamClient = class _ContextStreamClient {
14863
14907
  async captureContext(params) {
14864
14908
  const withDefaults = this.withDefaults(params);
14865
14909
  let apiEventType = "manual_note";
14866
- const tags = params.tags || [];
14910
+ const tags = [...params.tags || []];
14867
14911
  switch (params.event_type) {
14868
14912
  case "conversation":
14869
14913
  apiEventType = "chat";
@@ -14906,18 +14950,20 @@ var ContextStreamClient = class _ContextStreamClient {
14906
14950
  apiEventType = "manual_note";
14907
14951
  tags.push(params.event_type);
14908
14952
  }
14953
+ const normalizedTags = normalizeTags(tags);
14909
14954
  return this.createMemoryEvent({
14910
14955
  workspace_id: withDefaults.workspace_id,
14911
14956
  project_id: withDefaults.project_id,
14912
14957
  event_type: apiEventType,
14913
14958
  title: params.title,
14914
14959
  content: params.content,
14960
+ tags: normalizedTags,
14915
14961
  provenance: params.provenance,
14916
14962
  code_refs: params.code_refs,
14917
14963
  metadata: {
14918
14964
  original_type: params.event_type,
14919
14965
  session_id: params.session_id,
14920
- tags,
14966
+ ...normalizedTags ? { tags: normalizedTags } : {},
14921
14967
  importance: params.importance || "medium",
14922
14968
  captured_at: (/* @__PURE__ */ new Date()).toISOString(),
14923
14969
  source: "mcp_auto_capture"
@@ -14933,8 +14979,9 @@ var ContextStreamClient = class _ContextStreamClient {
14933
14979
  const metadata = {
14934
14980
  ...params.metadata || {}
14935
14981
  };
14936
- if (params.tags && params.tags.length > 0) {
14937
- metadata.tags = params.tags;
14982
+ const normalizedTags = normalizeTags(params.tags);
14983
+ if (normalizedTags) {
14984
+ metadata.tags = normalizedTags;
14938
14985
  }
14939
14986
  if (!metadata.captured_at) {
14940
14987
  metadata.captured_at = (/* @__PURE__ */ new Date()).toISOString();
@@ -14948,6 +14995,7 @@ var ContextStreamClient = class _ContextStreamClient {
14948
14995
  event_type: params.event_type,
14949
14996
  title: params.title,
14950
14997
  content: params.content,
14998
+ tags: normalizedTags,
14951
14999
  provenance: params.provenance,
14952
15000
  code_refs: params.code_refs,
14953
15001
  metadata
@@ -14967,6 +15015,7 @@ var ContextStreamClient = class _ContextStreamClient {
14967
15015
  */
14968
15016
  async sessionRemember(params) {
14969
15017
  const withDefaults = this.withDefaults(params);
15018
+ const normalizedTags = normalizeTags(params.tags);
14970
15019
  if (!withDefaults.workspace_id) {
14971
15020
  throw new Error(
14972
15021
  "workspace_id is required for session_remember. Set defaultWorkspaceId in config or provide workspace_id."
@@ -14978,7 +15027,8 @@ var ContextStreamClient = class _ContextStreamClient {
14978
15027
  workspace_id: withDefaults.workspace_id,
14979
15028
  project_id: withDefaults.project_id,
14980
15029
  importance: params.importance,
14981
- await_indexing: params.await_indexing
15030
+ await_indexing: params.await_indexing,
15031
+ ...normalizedTags ? { tags: normalizedTags } : {}
14982
15032
  }
14983
15033
  });
14984
15034
  }
@@ -15818,14 +15868,13 @@ ${context}`;
15818
15868
  });
15819
15869
  if (!searchResult?.results) return [];
15820
15870
  const lessons = searchResult.results.filter((item) => {
15821
- const tags = item.metadata?.tags || [];
15822
- const isLesson = tags.includes("lesson") || tags.includes("lesson_system");
15823
- if (!isLesson) return false;
15871
+ if (!isLessonResult(item)) return false;
15872
+ const tags = extractEventTags(item);
15824
15873
  const severityTag = tags.find((t) => t.startsWith("severity:"));
15825
15874
  const severity = severityTag?.split(":")[1] || item.metadata?.importance || "medium";
15826
15875
  return severity === "critical" || severity === "high";
15827
15876
  }).slice(0, limit).map((item) => {
15828
- const tags = item.metadata?.tags || [];
15877
+ const tags = extractEventTags(item);
15829
15878
  const severityTag = tags.find((t) => t.startsWith("severity:"));
15830
15879
  const severity = severityTag?.split(":")[1] || item.metadata?.importance || "medium";
15831
15880
  const category = tags.find(
@@ -15839,7 +15888,7 @@ ${context}`;
15839
15888
  ) || "unknown";
15840
15889
  const content = item.content || "";
15841
15890
  const preventionMatch = content.match(/### Prevention\n([\s\S]*?)(?:\n\n|\n\*\*|$)/);
15842
- const prevention = preventionMatch?.[1]?.trim() || content.slice(0, 200);
15891
+ const prevention = preventionMatch?.[1]?.trim() || content.slice(0, 1e3);
15843
15892
  return {
15844
15893
  title: item.title || "Lesson",
15845
15894
  severity,
@@ -17045,6 +17094,7 @@ ${context}`;
17045
17094
  if (withDefaults.project_id) query.set("project_id", withDefaults.project_id);
17046
17095
  if (params?.doc_type) query.set("doc_type", params.doc_type);
17047
17096
  if (params?.is_personal !== void 0) query.set("is_personal", String(params.is_personal));
17097
+ if (params?.query) query.set("query", params.query);
17048
17098
  if (params?.page) query.set("page", String(params.page));
17049
17099
  if (params?.per_page) query.set("per_page", String(params.per_page));
17050
17100
  const suffix = query.toString() ? `?${query.toString()}` : "";
@@ -17093,6 +17143,55 @@ ${context}`;
17093
17143
  return request(this.config, `/docs/${params.doc_id}`, { method: "DELETE" });
17094
17144
  }
17095
17145
  // -------------------------------------------------------------------------
17146
+ // Skill methods (portable instruction + action bundles)
17147
+ // -------------------------------------------------------------------------
17148
+ async listSkills(params) {
17149
+ const withDefaults = this.withDefaults(params || {});
17150
+ const query = new URLSearchParams();
17151
+ if (withDefaults.workspace_id) query.set("workspace_id", withDefaults.workspace_id);
17152
+ if (params?.project_id) query.set("project_id", params.project_id);
17153
+ if (params?.scope) query.set("scope", params.scope);
17154
+ if (params?.status) query.set("status", params.status);
17155
+ if (params?.category) query.set("category", params.category);
17156
+ if (params?.query) query.set("query", params.query);
17157
+ if (params?.is_personal !== void 0) query.set("is_personal", String(params.is_personal));
17158
+ if (params?.limit) query.set("limit", String(params.limit));
17159
+ const suffix = query.toString() ? `?${query.toString()}` : "";
17160
+ return request(this.config, `/skills${suffix}`, { method: "GET" });
17161
+ }
17162
+ async getSkill(skillId) {
17163
+ return request(this.config, `/skills/${skillId}`, { method: "GET" });
17164
+ }
17165
+ async createSkill(params) {
17166
+ return request(this.config, "/skills", { body: this.withDefaults(params) });
17167
+ }
17168
+ async updateSkill(skillId, params) {
17169
+ return request(this.config, `/skills/${skillId}`, {
17170
+ method: "PATCH",
17171
+ body: params
17172
+ });
17173
+ }
17174
+ async runSkill(skillId, params) {
17175
+ return request(this.config, `/skills/${skillId}/run`, {
17176
+ body: params || {}
17177
+ });
17178
+ }
17179
+ async deleteSkill(skillId) {
17180
+ return request(this.config, `/skills/${skillId}`, { method: "DELETE" });
17181
+ }
17182
+ async importSkills(params) {
17183
+ return request(this.config, "/skills/import", { body: this.withDefaults(params) });
17184
+ }
17185
+ async exportSkills(params) {
17186
+ const withDefaults = this.withDefaults(params || {});
17187
+ return request(this.config, "/skills/export", { body: withDefaults });
17188
+ }
17189
+ async shareSkill(skillId, scope) {
17190
+ return request(this.config, `/skills/${skillId}/share`, {
17191
+ body: { scope }
17192
+ });
17193
+ }
17194
+ // -------------------------------------------------------------------------
17096
17195
  // Transcript methods (conversation session storage)
17097
17196
  // -------------------------------------------------------------------------
17098
17197
  /**
@@ -17184,14 +17283,14 @@ ${context}`;
17184
17283
  };
17185
17284
 
17186
17285
  // src/tools.ts
17187
- init_files();
17188
- init_rules_templates();
17189
- init_version();
17190
17286
  import * as fs5 from "node:fs";
17191
17287
  import * as path6 from "node:path";
17192
17288
  import { execFile } from "node:child_process";
17193
17289
  import { homedir as homedir4 } from "node:os";
17194
17290
  import { promisify as promisify2 } from "node:util";
17291
+ init_files();
17292
+ init_rules_templates();
17293
+ init_version();
17195
17294
 
17196
17295
  // src/tool-catalog.ts
17197
17296
  var TOOL_CATALOG = [
@@ -17694,6 +17793,56 @@ function classifyIndexConfidence(indexed, apiIndexed, locallyIndexed, freshness)
17694
17793
  reason: "Index state is inferred but lacks corroborating API/local metadata."
17695
17794
  };
17696
17795
  }
17796
+ function classifyGraphIngestIndexState(input) {
17797
+ const { statusResult, locallyIndexed } = input;
17798
+ const candidates = candidateObjects(statusResult);
17799
+ const projectIndexState = readString(candidates, "project_index_state")?.toLowerCase();
17800
+ const indexInProgress = apiResultIsIndexing(statusResult);
17801
+ const indexed = apiResultReportsIndexed(statusResult) || locallyIndexed;
17802
+ const indexedAt = extractIndexTimestamp(statusResult);
17803
+ const ageHours = indexedAt !== void 0 ? Math.floor((Date.now() - indexedAt.getTime()) / (1e3 * 60 * 60)) : void 0;
17804
+ const freshness = classifyIndexFreshness(indexed, ageHours);
17805
+ if (indexInProgress) {
17806
+ return {
17807
+ state: "indexing",
17808
+ freshness,
17809
+ indexInProgress,
17810
+ indexed,
17811
+ projectIndexState,
17812
+ ageHours
17813
+ };
17814
+ }
17815
+ const explicitlyMissing = projectIndexState === "missing" || projectIndexState === "not_indexed" || projectIndexState === "unindexed";
17816
+ if (!indexed || explicitlyMissing) {
17817
+ return {
17818
+ state: "missing",
17819
+ freshness,
17820
+ indexInProgress,
17821
+ indexed,
17822
+ projectIndexState,
17823
+ ageHours
17824
+ };
17825
+ }
17826
+ const explicitlyStale = projectIndexState === "stale";
17827
+ if (freshness === "stale" || explicitlyStale) {
17828
+ return {
17829
+ state: "stale",
17830
+ freshness,
17831
+ indexInProgress,
17832
+ indexed,
17833
+ projectIndexState,
17834
+ ageHours
17835
+ };
17836
+ }
17837
+ return {
17838
+ state: "ready",
17839
+ freshness,
17840
+ indexInProgress,
17841
+ indexed,
17842
+ projectIndexState,
17843
+ ageHours
17844
+ };
17845
+ }
17697
17846
 
17698
17847
  // src/todo-utils.ts
17699
17848
  function normalizeTodoStatus(status) {
@@ -17942,6 +18091,32 @@ After updating, restart the AI tool to use the new version.
17942
18091
  \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
17943
18092
  `.trim();
17944
18093
  }
18094
+ function generateSuggestedRulesNotice(result) {
18095
+ const suggestedRules = result.suggested_rules;
18096
+ if (!suggestedRules || suggestedRules.length === 0) {
18097
+ return "";
18098
+ }
18099
+ const ruleLines = suggestedRules.slice(0, 3).map((rule, i) => {
18100
+ const cat = rule.category || "general";
18101
+ const confidence = rule.confidence ? `${Math.round(rule.confidence * 100)}%` : "?";
18102
+ const count = rule.occurrence_count || 0;
18103
+ const keywords = (rule.keywords || []).join(", ");
18104
+ return `${i + 1}. [${cat}] ${rule.instruction || ""} (confidence: ${confidence}, seen ${count}x)
18105
+ Keywords: ${keywords}
18106
+ Rule ID: ${rule.id}`;
18107
+ });
18108
+ return `
18109
+
18110
+ \u{1F4A1} [SUGGESTED_RULES] ContextStream detected recurring patterns and generated rule suggestions.
18111
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
18112
+ Present these to the user. They can accept or reject each one.
18113
+
18114
+ ${ruleLines.join("\n")}
18115
+
18116
+ To accept: session(action="suggested_rule_action", rule_id="<id>", rule_action="accept")
18117
+ To reject: session(action="suggested_rule_action", rule_id="<id>", rule_action="reject")
18118
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`;
18119
+ }
17945
18120
  var DEFAULT_PARAM_DESCRIPTIONS = {
17946
18121
  api_key: "ContextStream API key.",
17947
18122
  apiKey: "ContextStream API key.",
@@ -19219,6 +19394,8 @@ var CONSOLIDATED_TOOLS = /* @__PURE__ */ new Set([
19219
19394
  // Consolidates slack_*, github_*, notion_*, integrations_*
19220
19395
  "media",
19221
19396
  // Consolidates media indexing, search, and clip retrieval for Remotion/FFmpeg
19397
+ "skill",
19398
+ // Skill management: list, get, create, update, run, delete, import, export, share
19222
19399
  "help"
19223
19400
  // Consolidates session_tools, auth_me, mcp_server_version, etc.
19224
19401
  ]);
@@ -19238,6 +19415,7 @@ function mapToolToConsolidatedDomain(toolName) {
19238
19415
  return "integration";
19239
19416
  }
19240
19417
  if (toolName.startsWith("media_")) return "media";
19418
+ if (toolName.startsWith("skill_")) return "skill";
19241
19419
  if (toolName === "session_tools" || toolName === "auth_me" || toolName === "mcp_server_version" || toolName === "tools_enable_bundle") {
19242
19420
  return "help";
19243
19421
  }
@@ -19333,6 +19511,31 @@ function toStructured(data) {
19333
19511
  }
19334
19512
  return void 0;
19335
19513
  }
19514
+ function formatSkillDetail(skill) {
19515
+ const lines = [
19516
+ `**${skill.title || skill.name || "?"}** (${skill.name || "?"})`,
19517
+ `- ID: ${skill.id || "?"}`,
19518
+ `- Scope: ${skill.scope || "personal"} | Status: ${skill.status || "active"}`
19519
+ ];
19520
+ if (skill.description) lines.push(`- Description: ${skill.description}`);
19521
+ if (skill.trigger_patterns?.length) lines.push(`- Triggers: ${skill.trigger_patterns.join(", ")}`);
19522
+ if (skill.categories?.length) lines.push(`- Categories: ${skill.categories.join(", ")}`);
19523
+ if (skill.priority != null) lines.push(`- Priority: ${skill.priority}`);
19524
+ if (skill.version) lines.push(`- Version: ${skill.version}`);
19525
+ if (skill.instruction_body) {
19526
+ const body = String(skill.instruction_body);
19527
+ lines.push(`
19528
+ ### Instruction
19529
+ ${body.length > 2e3 ? body.slice(0, 2e3) + "\u2026" : body}`);
19530
+ }
19531
+ return lines.join("\n");
19532
+ }
19533
+ function formatRunResult(result) {
19534
+ if (result.instruction) return result.instruction;
19535
+ if (result.output) return `Skill output:
19536
+ ${result.output}`;
19537
+ return JSON.stringify(result, null, 2);
19538
+ }
19336
19539
  function readStatNumber(payload, key) {
19337
19540
  if (!payload || typeof payload !== "object") return void 0;
19338
19541
  const direct = payload[key];
@@ -20231,6 +20434,9 @@ Hint: Run session_init(folder_path="<your_project_path>") first to establish a s
20231
20434
  if ("data" in value) return extractCollectionArray(value.data);
20232
20435
  return void 0;
20233
20436
  }
20437
+ function normalizeDocLookupText(value) {
20438
+ return value.toLowerCase().replace(/[^a-z0-9\s]/g, " ").replace(/\s+/g, " ").trim();
20439
+ }
20234
20440
  function tokenizeForDocMatch(query) {
20235
20441
  const stopWords = /* @__PURE__ */ new Set([
20236
20442
  "the",
@@ -20248,23 +20454,99 @@ Hint: Run session_init(folder_path="<your_project_path>") first to establish a s
20248
20454
  "find",
20249
20455
  "plan",
20250
20456
  "phase",
20251
- "phases"
20457
+ "phases",
20458
+ "get",
20459
+ "open",
20460
+ "show",
20461
+ "read"
20252
20462
  ]);
20253
- return query.split(/[^A-Za-z0-9]+/).map((term) => term.trim().toLowerCase()).filter((term) => term.length >= 3 && !stopWords.has(term));
20463
+ return normalizeDocLookupText(query).split(/\s+/).map((term) => term.trim()).filter((term) => term.length >= 3 && !stopWords.has(term));
20254
20464
  }
20255
- function scoreDocMatch(doc, terms) {
20256
- const title = String(doc?.title ?? doc?.name ?? doc?.summary ?? "").toLowerCase();
20257
- const content = String(doc?.content ?? "").toLowerCase();
20258
- const haystack = `${title} ${content}`;
20259
- return terms.reduce((score, term) => haystack.includes(term) ? score + 1 : score, 0);
20465
+ function scoreDocMatch(doc, query, terms) {
20466
+ const rawQuery = query.trim();
20467
+ const normalizedQuery = normalizeDocLookupText(rawQuery);
20468
+ const titleRaw = String(doc?.title ?? doc?.name ?? doc?.summary ?? "").trim();
20469
+ const title = normalizeDocLookupText(titleRaw);
20470
+ const docId = String(doc?.id ?? "").trim();
20471
+ if (docId && rawQuery && docId.toLowerCase() === rawQuery.toLowerCase()) {
20472
+ return { doc, score: 100, exact: true, source: "doc_id" };
20473
+ }
20474
+ if (title && normalizedQuery && title === normalizedQuery) {
20475
+ return { doc, score: 95, exact: true, source: "exact_title" };
20476
+ }
20477
+ if (title && normalizedQuery && title.includes(normalizedQuery) && normalizedQuery.length >= 8) {
20478
+ return { doc, score: 80, exact: false, source: "title_contains_query" };
20479
+ }
20480
+ if (terms.length === 0) {
20481
+ return { doc, score: 0, exact: false, source: "term_overlap" };
20482
+ }
20483
+ const matchedTerms = terms.filter((term) => title.includes(term)).length;
20484
+ if (matchedTerms === 0) {
20485
+ return { doc, score: 0, exact: false, source: "term_overlap" };
20486
+ }
20487
+ if (matchedTerms === terms.length) {
20488
+ return {
20489
+ doc,
20490
+ score: 60 + matchedTerms * 5,
20491
+ exact: false,
20492
+ source: "all_terms"
20493
+ };
20494
+ }
20495
+ return {
20496
+ doc,
20497
+ score: matchedTerms * 10,
20498
+ exact: false,
20499
+ source: "term_overlap"
20500
+ };
20260
20501
  }
20261
- function rankDocsForQuery(docs, query, limit) {
20502
+ function rankDocsForQueryMatches(docs, query, limit) {
20262
20503
  const terms = tokenizeForDocMatch(query);
20263
- if (terms.length === 0) return docs.slice(0, limit);
20264
- const scored = docs.map((doc, idx) => ({ idx, score: scoreDocMatch(doc, terms) }));
20504
+ const scored = docs.map((doc, idx) => ({ idx, ...scoreDocMatch(doc, query, terms) }));
20265
20505
  scored.sort((a, b) => b.score - a.score || a.idx - b.idx);
20266
- const matched = scored.filter((entry) => entry.score > 0).slice(0, limit).map((entry) => docs[entry.idx]);
20267
- return matched.length > 0 ? matched : docs.slice(0, limit);
20506
+ return scored.filter((entry) => entry.score > 0).slice(0, limit);
20507
+ }
20508
+ function rankDocsForQuery(docs, query, limit) {
20509
+ const rankedMatches = rankDocsForQueryMatches(docs, query, limit);
20510
+ return rankedMatches.length > 0 ? rankedMatches.map((entry) => entry.doc) : docs.slice(0, limit);
20511
+ }
20512
+ function selectResolvedDocMatch(matches) {
20513
+ if (matches.length === 0) return void 0;
20514
+ if (matches[0].exact) return matches[0];
20515
+ const top = matches[0];
20516
+ const second = matches[1];
20517
+ if (top.score >= 80 && (!second || second.score <= 40)) {
20518
+ return top;
20519
+ }
20520
+ return void 0;
20521
+ }
20522
+ function extractMemorySearchResults(response) {
20523
+ const result = response?.data ?? response;
20524
+ if (Array.isArray(result?.results)) return result.results;
20525
+ if (Array.isArray(result?.items)) return result.items;
20526
+ return [];
20527
+ }
20528
+ function buildHybridMemoryDocResults(memoryResults, docMatches, limit) {
20529
+ const docs = docMatches.map((match) => ({
20530
+ entity_type: "doc",
20531
+ id: match.doc?.id ?? "unknown",
20532
+ title: match.doc?.title ?? match.doc?.name ?? "Untitled",
20533
+ preview: String(match.doc?.content ?? "").slice(0, 150),
20534
+ score: match.score,
20535
+ match_source: match.source,
20536
+ doc_type: match.doc?.doc_type ?? "general"
20537
+ }));
20538
+ const memory = memoryResults.map((item) => ({
20539
+ entity_type: "memory",
20540
+ id: item?.id ?? item?.node_id ?? "unknown",
20541
+ title: item?.title ?? item?.summary ?? item?.name ?? "Untitled",
20542
+ preview: String(item?.content ?? item?.details ?? "").slice(0, 150),
20543
+ score: Math.round(Number(item?.score ?? 0) * 100),
20544
+ match_source: "memory_search",
20545
+ node_type: item?.node_type ?? item?.event_type ?? item?.type ?? "unknown"
20546
+ }));
20547
+ const combined = [...docs, ...memory];
20548
+ combined.sort((a, b) => Number(b.score || 0) - Number(a.score || 0));
20549
+ return combined.slice(0, limit);
20268
20550
  }
20269
20551
  async function findDocsFallback(workspaceId, candidateProjectIds, query, limit) {
20270
20552
  const uniqueCandidates = [];
@@ -20398,6 +20680,137 @@ Hint: Run session_init(folder_path="<your_project_path>") first to establish a s
20398
20680
  }
20399
20681
  })();
20400
20682
  }
20683
+ async function runGraphIngestWithPreflight(projectId, wait) {
20684
+ const folderPath = resolveFolderPath(void 0, sessionManager);
20685
+ const localIndexProjectId = folderPath ? await indexedProjectIdForFolder(folderPath) : void 0;
20686
+ const locallyIndexed = localIndexProjectId === projectId;
20687
+ const suggestedPath = folderPath || "<your_project_path>";
20688
+ let statusResult = {};
20689
+ try {
20690
+ statusResult = await client.projectIndexStatus(projectId);
20691
+ } catch (error) {
20692
+ if (!isNotFoundError(error)) throw error;
20693
+ }
20694
+ const preflight = classifyGraphIngestIndexState({
20695
+ statusResult,
20696
+ locallyIndexed
20697
+ });
20698
+ const preflightData = {
20699
+ index_state: preflight.state,
20700
+ index_freshness: preflight.freshness,
20701
+ index_age_hours: preflight.ageHours ?? null,
20702
+ index_in_progress: preflight.indexInProgress,
20703
+ project_index_state: preflight.projectIndexState ?? null,
20704
+ locally_indexed: locallyIndexed
20705
+ };
20706
+ if (preflight.state === "indexing") {
20707
+ const deferred = {
20708
+ status: "deferred",
20709
+ wait,
20710
+ reason: "Project index refresh is in progress. Graph ingest is deferred until index refresh completes.",
20711
+ preflight: {
20712
+ ...preflightData,
20713
+ auto_refresh_started: false,
20714
+ graph_ingest_executed: false
20715
+ },
20716
+ next_step: `Run project(action="index_status") and retry graph(action="ingest") when freshness is not stale.`
20717
+ };
20718
+ return {
20719
+ content: [
20720
+ {
20721
+ type: "text",
20722
+ text: `Project indexing is currently in progress; graph ingest was deferred to avoid rebuilding edges from stale embeddings.
20723
+
20724
+ ${formatContent(deferred)}`
20725
+ }
20726
+ ],
20727
+ structuredContent: deferred
20728
+ };
20729
+ }
20730
+ if (preflight.state === "stale" || preflight.state === "missing") {
20731
+ const validPath = folderPath ? await validateReadableDirectory(folderPath) : void 0;
20732
+ if (!validPath?.ok) {
20733
+ return errorResult(
20734
+ `Graph ingest is blocked because the project index is ${preflight.state} and no readable project folder is available for auto-refresh.
20735
+ Run project(action="ingest_local", path="${suggestedPath}") first, then retry graph(action="ingest").`
20736
+ );
20737
+ }
20738
+ if (!preflight.indexInProgress) {
20739
+ startBackgroundIngest(projectId, validPath.resolvedPath, { force: true }, { preflight: true });
20740
+ }
20741
+ const deferred = {
20742
+ status: "deferred",
20743
+ wait,
20744
+ reason: `Project index is ${preflight.state}. Started index refresh before graph ingest to avoid destructive edge rebuilds from stale embeddings.`,
20745
+ preflight: {
20746
+ ...preflightData,
20747
+ auto_refresh_started: !preflight.indexInProgress,
20748
+ auto_refresh_path: validPath.resolvedPath,
20749
+ auto_refresh_force: true,
20750
+ graph_ingest_executed: false
20751
+ },
20752
+ next_step: `Monitor with project(action="index_status"), then rerun graph(action="ingest").`
20753
+ };
20754
+ return {
20755
+ content: [
20756
+ {
20757
+ type: "text",
20758
+ text: `Project index is ${preflight.state}; started index refresh and deferred graph ingest to prevent stale-edge rebuild.
20759
+
20760
+ ${formatContent(deferred)}`
20761
+ }
20762
+ ],
20763
+ structuredContent: deferred
20764
+ };
20765
+ }
20766
+ let estimate = null;
20767
+ try {
20768
+ const stats = await client.projectStatistics(projectId);
20769
+ estimate = estimateGraphIngestMinutes(stats);
20770
+ } catch (error) {
20771
+ logDebug(`Failed to fetch project statistics for graph estimate: ${error}`);
20772
+ }
20773
+ const result = await client.graphIngest({ project_id: projectId, wait });
20774
+ const estimateText = estimate ? `Estimated time: ${estimate.min}-${estimate.max} min${estimate.basis ? ` (based on ${estimate.basis})` : ""}.` : "Estimated time varies with repo size.";
20775
+ const note = `Graph ingestion is running ${wait ? "synchronously" : "asynchronously"} and can take a few minutes. ${estimateText}`;
20776
+ const structured = toStructured(result);
20777
+ const structuredContent = structured && typeof structured === "object" ? {
20778
+ ...structured,
20779
+ wait,
20780
+ note,
20781
+ preflight: {
20782
+ ...preflightData,
20783
+ auto_refresh_started: false,
20784
+ graph_ingest_executed: true
20785
+ },
20786
+ ...estimate ? {
20787
+ estimate_minutes: { min: estimate.min, max: estimate.max },
20788
+ estimate_basis: estimate.basis
20789
+ } : {}
20790
+ } : {
20791
+ wait,
20792
+ note,
20793
+ preflight: {
20794
+ ...preflightData,
20795
+ auto_refresh_started: false,
20796
+ graph_ingest_executed: true
20797
+ },
20798
+ ...estimate ? {
20799
+ estimate_minutes: { min: estimate.min, max: estimate.max },
20800
+ estimate_basis: estimate.basis
20801
+ } : {}
20802
+ };
20803
+ return {
20804
+ content: [
20805
+ {
20806
+ type: "text",
20807
+ text: `${note}
20808
+ ${formatContent(result)}`
20809
+ }
20810
+ ],
20811
+ structuredContent
20812
+ };
20813
+ }
20401
20814
  function summarizeVideoTextExtraction(metadata) {
20402
20815
  if (!metadata || typeof metadata !== "object") return void 0;
20403
20816
  const extraction = metadata.video_text_extraction;
@@ -21446,6 +21859,201 @@ Access: Free`,
21446
21859
  };
21447
21860
  }
21448
21861
  );
21862
+ registerTool(
21863
+ "skill",
21864
+ {
21865
+ title: "Manage reusable skills",
21866
+ description: `Manage and execute reusable skills (instruction + action bundles). Skills are portable across projects, sessions, and tools.
21867
+
21868
+ Actions:
21869
+ - list: Browse skills (filter by scope, status, category)
21870
+ - get: Get skill details by ID or name
21871
+ - create: Define a new skill with name, instruction, and triggers
21872
+ - update: Modify an existing skill
21873
+ - run: Execute a skill (by ID or name)
21874
+ - delete: Remove a skill
21875
+ - import: Import skills from file or content (supports markdown, JSON, cursorrules, claude_md)
21876
+ - export: Export skills in various formats
21877
+ - share: Change skill visibility scope`,
21878
+ inputSchema: external_exports.object({
21879
+ action: external_exports.enum(["list", "get", "create", "update", "run", "delete", "import", "export", "share"]).describe("The action to perform"),
21880
+ skill_id: external_exports.string().optional().describe("Skill ID (UUID)"),
21881
+ name: external_exports.string().optional().describe("Skill name (slug, e.g. 'deploy-checker')"),
21882
+ title: external_exports.string().optional().describe("Skill display title"),
21883
+ description: external_exports.string().optional().describe("Skill description"),
21884
+ instruction_body: external_exports.string().optional().describe("Markdown instruction text (the prompt)"),
21885
+ trigger_patterns: external_exports.array(external_exports.string()).optional().describe("Keywords/phrases for auto-activation"),
21886
+ trigger_regex: external_exports.string().optional().describe("Optional regex for advanced trigger matching"),
21887
+ categories: external_exports.array(external_exports.string()).optional().describe("Tags for discovery/filtering"),
21888
+ actions: external_exports.any().optional().describe("Action steps array [{type, tool, params, ...}]"),
21889
+ params: external_exports.any().optional().describe("Parameters passed to skill execution"),
21890
+ dry_run: external_exports.boolean().optional().describe("Preview execution without running"),
21891
+ scope: external_exports.enum(["personal", "team", "public", "all"]).optional().describe("Visibility scope"),
21892
+ status: external_exports.enum(["active", "draft", "archived"]).optional().describe("Skill status"),
21893
+ is_personal: external_exports.boolean().optional().describe("Whether skill is personal"),
21894
+ priority: external_exports.number().optional().describe("Skill priority 0-100 (higher = matched first)"),
21895
+ content: external_exports.string().optional().describe("Content string for import"),
21896
+ file_path: external_exports.string().optional().describe("Local file path for import"),
21897
+ format: external_exports.enum(["auto", "json", "markdown", "skills_md", "cursorrules", "claude_md", "aider", "zip"]).optional().describe("Import/export format"),
21898
+ source_tool: external_exports.string().optional().describe("Source tool name (for import provenance)"),
21899
+ source_file: external_exports.string().optional().describe("Source filename (for import provenance)"),
21900
+ skill_ids: external_exports.array(external_exports.string()).optional().describe("Skill IDs for export"),
21901
+ change_summary: external_exports.string().optional().describe("Summary of changes (for version history)"),
21902
+ workspace_id: external_exports.string().optional().describe("Workspace ID (UUID)"),
21903
+ project_id: external_exports.string().optional().describe("Project ID (UUID)"),
21904
+ query: external_exports.string().optional().describe("Search query"),
21905
+ category: external_exports.string().optional().describe("Filter by category tag"),
21906
+ limit: external_exports.number().optional().describe("Max results to return")
21907
+ })
21908
+ },
21909
+ async (input) => {
21910
+ const action = input.action;
21911
+ switch (action) {
21912
+ case "list": {
21913
+ const result = await client.listSkills({
21914
+ workspace_id: input.workspace_id,
21915
+ project_id: input.project_id,
21916
+ scope: input.scope,
21917
+ status: input.status,
21918
+ category: input.category,
21919
+ query: input.query,
21920
+ is_personal: input.is_personal,
21921
+ limit: input.limit
21922
+ });
21923
+ const items = result.items || [];
21924
+ let text = `Found ${items.length} skill(s).
21925
+ `;
21926
+ for (const item of items) {
21927
+ const name = item.name || "?";
21928
+ const title = item.title || "?";
21929
+ const scope = item.scope || "?";
21930
+ const status = item.status || "?";
21931
+ const id = item.id || "?";
21932
+ text += `- ${title} (${name}) [${scope}|${status}] id=${id}
21933
+ `;
21934
+ }
21935
+ return { content: [{ type: "text", text }] };
21936
+ }
21937
+ case "get": {
21938
+ let skillData;
21939
+ if (input.skill_id) {
21940
+ skillData = await client.getSkill(input.skill_id);
21941
+ } else if (input.name) {
21942
+ const result = await client.listSkills({
21943
+ workspace_id: input.workspace_id,
21944
+ query: input.name,
21945
+ limit: 1
21946
+ });
21947
+ skillData = result.items?.find((s) => s.name === input.name) || result.items?.[0];
21948
+ if (!skillData) throw new Error(`Skill '${input.name}' not found`);
21949
+ } else {
21950
+ throw new Error("Either skill_id or name is required for 'get'");
21951
+ }
21952
+ const detail = formatSkillDetail(skillData);
21953
+ return { content: [{ type: "text", text: detail }] };
21954
+ }
21955
+ case "create": {
21956
+ if (!input.name) throw new Error("'name' is required for create");
21957
+ if (!input.instruction_body) throw new Error("'instruction_body' is required for create");
21958
+ const result = await client.createSkill({
21959
+ name: input.name,
21960
+ title: input.title || input.name,
21961
+ instruction_body: input.instruction_body,
21962
+ description: input.description,
21963
+ trigger_patterns: input.trigger_patterns,
21964
+ trigger_regex: input.trigger_regex,
21965
+ categories: input.categories,
21966
+ actions: input.actions,
21967
+ scope: input.scope,
21968
+ is_personal: input.is_personal,
21969
+ priority: input.priority,
21970
+ workspace_id: input.scope === "team" ? input.workspace_id : void 0,
21971
+ project_id: void 0,
21972
+ // Skills are account-level by default
21973
+ source_tool: input.source_tool,
21974
+ source_file: input.source_file
21975
+ });
21976
+ return { content: [{ type: "text", text: `Skill '${input.name}' created (id=${result.id || "?"}).` }] };
21977
+ }
21978
+ case "update": {
21979
+ if (!input.skill_id) throw new Error("'skill_id' is required for update");
21980
+ const result = await client.updateSkill(input.skill_id, {
21981
+ title: input.title,
21982
+ description: input.description,
21983
+ instruction_body: input.instruction_body,
21984
+ trigger_patterns: input.trigger_patterns,
21985
+ trigger_regex: input.trigger_regex,
21986
+ categories: input.categories,
21987
+ actions: input.actions,
21988
+ scope: input.scope,
21989
+ status: input.status,
21990
+ is_personal: input.is_personal,
21991
+ priority: input.priority,
21992
+ change_summary: input.change_summary
21993
+ });
21994
+ return { content: [{ type: "text", text: `Skill ${input.skill_id} updated (version=${result.version || 0}).` }] };
21995
+ }
21996
+ case "run": {
21997
+ let resolvedId = input.skill_id;
21998
+ if (!resolvedId && input.name) {
21999
+ const result2 = await client.listSkills({
22000
+ workspace_id: input.workspace_id,
22001
+ query: input.name,
22002
+ limit: 1
22003
+ });
22004
+ const found = result2.items?.find((s) => s.name === input.name) || result2.items?.[0];
22005
+ if (!found?.id) throw new Error(`Skill '${input.name}' not found`);
22006
+ resolvedId = found.id;
22007
+ }
22008
+ if (!resolvedId) throw new Error("Either skill_id or name is required for 'run'");
22009
+ const result = await client.runSkill(resolvedId, {
22010
+ params: input.params,
22011
+ dry_run: input.dry_run
22012
+ });
22013
+ return { content: [{ type: "text", text: formatRunResult(result) }] };
22014
+ }
22015
+ case "delete": {
22016
+ if (!input.skill_id) throw new Error("'skill_id' is required for delete");
22017
+ await client.deleteSkill(input.skill_id);
22018
+ return { content: [{ type: "text", text: `Skill ${input.skill_id} deleted.` }] };
22019
+ }
22020
+ case "import": {
22021
+ let importContent = input.content;
22022
+ if (!importContent && input.file_path) {
22023
+ const { readFile: readFile4 } = await import("fs/promises");
22024
+ importContent = await readFile4(input.file_path, "utf-8");
22025
+ }
22026
+ if (!importContent) throw new Error("Either 'content' or 'file_path' is required for import");
22027
+ const result = await client.importSkills({
22028
+ content: importContent,
22029
+ format: input.format,
22030
+ source_tool: input.source_tool,
22031
+ source_file: input.source_file || input.file_path,
22032
+ scope: input.scope,
22033
+ workspace_id: input.workspace_id
22034
+ });
22035
+ return { content: [{ type: "text", text: `Import complete: ${result.imported || 0} imported, ${result.skipped || 0} skipped (duplicates).` }] };
22036
+ }
22037
+ case "export": {
22038
+ const result = await client.exportSkills({
22039
+ skill_ids: input.skill_ids,
22040
+ format: input.format,
22041
+ scope: input.scope,
22042
+ workspace_id: input.workspace_id
22043
+ });
22044
+ return { content: [{ type: "text", text: result.content || JSON.stringify(result, null, 2) }] };
22045
+ }
22046
+ case "share": {
22047
+ if (!input.skill_id) throw new Error("'skill_id' is required for share");
22048
+ if (!input.scope) throw new Error("'scope' is required for share");
22049
+ const result = await client.shareSkill(input.skill_id, input.scope);
22050
+ return { content: [{ type: "text", text: `Skill ${input.skill_id} shared with scope=${result.scope || input.scope}.` }] };
22051
+ }
22052
+ default:
22053
+ throw new Error(`Invalid skill action: '${action}'. Valid: list, get, create, update, run, delete, import, export, share`);
22054
+ }
22055
+ }
22056
+ );
21449
22057
  registerTool(
21450
22058
  "memory_bulk_ingest",
21451
22059
  {
@@ -21468,15 +22076,24 @@ Access: Free`,
21468
22076
  "memory_list_events",
21469
22077
  {
21470
22078
  title: "List memory events",
21471
- description: "List memory events (optionally scoped)",
22079
+ description: "List memory events (optionally scoped). Supports tag-based and event_type filtering for precise provenance tracking.",
21472
22080
  inputSchema: external_exports.object({
21473
22081
  workspace_id: external_exports.string().uuid().optional(),
21474
22082
  project_id: external_exports.string().uuid().optional(),
21475
- limit: external_exports.number().optional()
22083
+ limit: external_exports.number().optional(),
22084
+ tags: external_exports.array(external_exports.string()).optional().describe("Filter events that contain ALL of these tags"),
22085
+ event_type: external_exports.string().optional().describe("Filter by event type (e.g. decision, lesson, manual_note)")
21476
22086
  })
21477
22087
  },
21478
22088
  async (input) => {
21479
22089
  const result = await client.listMemoryEvents(input);
22090
+ if (input.tags && input.tags.length > 0 && result.items) {
22091
+ const requiredTags = input.tags;
22092
+ result.items = result.items.filter((item) => {
22093
+ const itemTags = extractEventTags(item);
22094
+ return requiredTags.every((tag) => itemTags.includes(tag));
22095
+ });
22096
+ }
21480
22097
  return {
21481
22098
  content: [{ type: "text", text: formatContent(result) }]
21482
22099
  };
@@ -21530,16 +22147,24 @@ Access: Free`,
21530
22147
  "memory_search",
21531
22148
  {
21532
22149
  title: "Memory-aware search",
21533
- description: "Search memory events/notes",
22150
+ description: "Search memory events/notes. Supports optional tag-based pre-filtering.",
21534
22151
  inputSchema: external_exports.object({
21535
22152
  query: external_exports.string(),
21536
22153
  workspace_id: external_exports.string().uuid().optional(),
21537
22154
  project_id: external_exports.string().uuid().optional(),
21538
- limit: external_exports.number().optional()
22155
+ limit: external_exports.number().optional(),
22156
+ tags: external_exports.array(external_exports.string()).optional().describe("Filter results that contain ALL of these tags")
21539
22157
  })
21540
22158
  },
21541
22159
  async (input) => {
21542
22160
  const result = await client.memorySearch(input);
22161
+ if (input.tags && input.tags.length > 0 && result.results) {
22162
+ const requiredTags = input.tags;
22163
+ result.results = result.results.filter((item) => {
22164
+ const itemTags = extractEventTags(item);
22165
+ return requiredTags.every((tag) => itemTags.includes(tag));
22166
+ });
22167
+ }
21543
22168
  return {
21544
22169
  content: [{ type: "text", text: formatContent(result) }]
21545
22170
  };
@@ -21753,43 +22378,7 @@ Access: Free`,
21753
22378
  );
21754
22379
  }
21755
22380
  const wait = input.wait ?? false;
21756
- let estimate = null;
21757
- try {
21758
- const stats = await client.projectStatistics(projectId);
21759
- estimate = estimateGraphIngestMinutes(stats);
21760
- } catch (error) {
21761
- logDebug(`Failed to fetch project statistics for graph estimate: ${error}`);
21762
- }
21763
- const result = await client.graphIngest({ project_id: projectId, wait });
21764
- const estimateText = estimate ? `Estimated time: ${estimate.min}-${estimate.max} min${estimate.basis ? ` (based on ${estimate.basis})` : ""}.` : "Estimated time varies with repo size.";
21765
- const note = `Graph ingestion is running ${wait ? "synchronously" : "asynchronously"} and can take a few minutes. ${estimateText}`;
21766
- const structured = toStructured(result);
21767
- const structuredContent = structured && typeof structured === "object" ? {
21768
- ...structured,
21769
- wait,
21770
- note,
21771
- ...estimate ? {
21772
- estimate_minutes: { min: estimate.min, max: estimate.max },
21773
- estimate_basis: estimate.basis
21774
- } : {}
21775
- } : {
21776
- wait,
21777
- note,
21778
- ...estimate ? {
21779
- estimate_minutes: { min: estimate.min, max: estimate.max },
21780
- estimate_basis: estimate.basis
21781
- } : {}
21782
- };
21783
- return {
21784
- content: [
21785
- {
21786
- type: "text",
21787
- text: `${note}
21788
- ${formatContent(result)}`
21789
- }
21790
- ],
21791
- structuredContent
21792
- };
22381
+ return runGraphIngestWithPreflight(projectId, wait);
21793
22382
  }
21794
22383
  );
21795
22384
  registerTool(
@@ -23363,9 +23952,8 @@ Returns lessons filtered by:
23363
23952
  // Fetch more to filter
23364
23953
  });
23365
23954
  const lessons = (searchResult.results || []).filter((item) => {
23366
- const tags = item.metadata?.tags || [];
23367
- const isLesson = tags.includes("lesson");
23368
- if (!isLesson) return false;
23955
+ if (!isLessonResult(item)) return false;
23956
+ const tags = extractEventTags(item);
23369
23957
  if (input.category && !tags.includes(input.category)) {
23370
23958
  return false;
23371
23959
  }
@@ -23384,7 +23972,7 @@ Returns lessons filtered by:
23384
23972
  };
23385
23973
  }
23386
23974
  const formattedLessons = lessons.map((lesson, i) => {
23387
- const tags = lesson.metadata?.tags || [];
23975
+ const tags = extractEventTags(lesson);
23388
23976
  const severity = tags.find((t) => t.startsWith("severity:"))?.split(":")[1] || "medium";
23389
23977
  const category = tags.find(
23390
23978
  (t) => [
@@ -23403,7 +23991,7 @@ Returns lessons filtered by:
23403
23991
  }[severity] || "\u26AA";
23404
23992
  return `${i + 1}. ${severityEmoji} **${lesson.title}**
23405
23993
  Category: ${category} | Severity: ${severity}
23406
- ${lesson.content?.slice(0, 200)}...`;
23994
+ ${lesson.content?.slice(0, 500)}...`;
23407
23995
  }).join("\n\n");
23408
23996
  return {
23409
23997
  content: [
@@ -24406,6 +24994,7 @@ Action: ${cp.suggested_action === "prepare_save" ? "Consider saving important de
24406
24994
  const instructionsLine = result.instructions ? `
24407
24995
 
24408
24996
  [INSTRUCTIONS] ${result.instructions}` : "";
24997
+ const suggestedRulesLine = generateSuggestedRulesNotice(result);
24409
24998
  const contextRulesLine = `
24410
24999
 
24411
25000
  ${CONTEXT_CALL_REMINDER}`;
@@ -24418,6 +25007,7 @@ ${rulesWarningLine}` : "",
24418
25007
  versionWarningLine ? `
24419
25008
 
24420
25009
  ${versionWarningLine}` : "",
25010
+ suggestedRulesLine,
24421
25011
  contextPressureWarning,
24422
25012
  semanticHints,
24423
25013
  instructionsLine,
@@ -26150,7 +26740,8 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
26150
26740
  workspace_id: workspaceId,
26151
26741
  project_id: projectId,
26152
26742
  content: input.content,
26153
- importance
26743
+ importance,
26744
+ tags: input.tags
26154
26745
  });
26155
26746
  const rememberHint = getCaptureHint("preference");
26156
26747
  const resultWithHint = { ...result, hint: rememberHint };
@@ -26787,7 +27378,7 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
26787
27378
  order: external_exports.number().optional().describe("Task order within plan"),
26788
27379
  task_ids: external_exports.array(external_exports.string().uuid()).optional().describe("Task IDs for reorder_tasks"),
26789
27380
  blocked_reason: external_exports.string().optional().describe("Reason when task is blocked"),
26790
- tags: external_exports.array(external_exports.string()).optional().describe("Tags for task"),
27381
+ tags: external_exports.array(external_exports.string()).optional().describe("Tags for event or task categorization"),
26791
27382
  // Batch import params
26792
27383
  events: external_exports.array(
26793
27384
  external_exports.object({
@@ -26824,7 +27415,7 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
26824
27415
  diagram_id: external_exports.string().uuid().optional().describe("Diagram ID for get_diagram/update_diagram/delete_diagram"),
26825
27416
  diagram_type: external_exports.enum(["flowchart", "sequence", "class", "er", "gantt", "mindmap", "pie", "other"]).optional().describe("Mermaid diagram type"),
26826
27417
  // Doc params
26827
- doc_id: external_exports.string().uuid().optional().describe("Doc ID for get_doc/update_doc/delete_doc"),
27418
+ doc_id: external_exports.string().optional().describe("Doc ID for get_doc/update_doc/delete_doc. For get_doc, accepts UUID or title/query text."),
26828
27419
  doc_type: external_exports.enum(["roadmap", "spec", "general"]).optional().describe("Document type"),
26829
27420
  milestones: external_exports.array(
26830
27421
  external_exports.object({
@@ -26859,6 +27450,7 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
26859
27450
  event_type: input.event_type,
26860
27451
  title: input.title,
26861
27452
  content: input.content,
27453
+ tags: input.tags,
26862
27454
  metadata: input.metadata,
26863
27455
  provenance: input.provenance,
26864
27456
  code_refs: input.code_refs
@@ -27020,13 +27612,42 @@ ${formatContent(result)}`
27020
27612
  if (!input.query) {
27021
27613
  return errorResult("search requires: query");
27022
27614
  }
27023
- const result = await client.memorySearch({
27024
- workspace_id: workspaceId,
27025
- project_id: projectId,
27615
+ const [memoryResult, docsResult] = await Promise.all([
27616
+ client.memorySearch({
27617
+ workspace_id: workspaceId,
27618
+ project_id: projectId,
27619
+ query: input.query,
27620
+ limit: input.limit
27621
+ }),
27622
+ workspaceId ? client.docsList({
27623
+ workspace_id: workspaceId,
27624
+ project_id: projectId,
27625
+ per_page: Math.max(10, Math.min(input.limit ?? 10, 25))
27626
+ }) : Promise.resolve(void 0)
27627
+ ]);
27628
+ const memoryItems = extractMemorySearchResults(memoryResult);
27629
+ const docsItems = extractCollectionArray(docsResult ?? {}) ?? [];
27630
+ const docMatches = rankDocsForQueryMatches(
27631
+ docsItems,
27632
+ input.query,
27633
+ Math.max(1, Math.min(input.limit ?? 10, 25))
27634
+ );
27635
+ const hybridResults = buildHybridMemoryDocResults(
27636
+ memoryItems,
27637
+ docMatches,
27638
+ Math.max(1, Math.min(input.limit ?? 10, 25))
27639
+ );
27640
+ const outputText = formatContent({
27026
27641
  query: input.query,
27027
- limit: input.limit
27642
+ results: hybridResults,
27643
+ memory_results: memoryItems,
27644
+ doc_matches: docMatches.map((entry) => ({
27645
+ ...entry.doc,
27646
+ match_score: entry.score,
27647
+ exact_match: entry.exact,
27648
+ match_source: entry.source
27649
+ }))
27028
27650
  });
27029
- const outputText = formatContent(result);
27030
27651
  trackToolTokenSavings(client, "memory_search", outputText, {
27031
27652
  workspace_id: workspaceId,
27032
27653
  project_id: projectId
@@ -27402,17 +28023,80 @@ ${formatContent(result)}`
27402
28023
  if (!input.doc_id) {
27403
28024
  return errorResult("get_doc requires: doc_id");
27404
28025
  }
27405
- const getDocResult = await client.docsGet({ doc_id: input.doc_id });
28026
+ const directDocId = normalizeUuid(input.doc_id);
28027
+ if (directDocId) {
28028
+ const getDocResult = await client.docsGet({ doc_id: directDocId });
28029
+ return {
28030
+ content: [{ type: "text", text: formatContent(getDocResult) }]
28031
+ };
28032
+ }
28033
+ if (!workspaceId) {
28034
+ return errorResult(
28035
+ "get_doc title/query lookups require workspace_id. Call session_init first or pass a doc UUID."
28036
+ );
28037
+ }
28038
+ const candidateProjectIds = [projectId, explicitProjectId, void 0];
28039
+ const fallback = await findDocsFallback(
28040
+ workspaceId,
28041
+ candidateProjectIds,
28042
+ input.doc_id,
28043
+ Math.max(10, Math.min(input.limit ?? 20, 30))
28044
+ );
28045
+ const rankedMatches = rankDocsForQueryMatches(
28046
+ fallback?.docs ?? [],
28047
+ input.doc_id,
28048
+ Math.max(1, Math.min(input.limit ?? 10, 10))
28049
+ );
28050
+ const resolved = selectResolvedDocMatch(rankedMatches);
28051
+ if (resolved) {
28052
+ const resolvedDocId = String(resolved.doc?.id ?? "");
28053
+ if (normalizeUuid(resolvedDocId)) {
28054
+ const getDocResult = await client.docsGet({ doc_id: resolvedDocId });
28055
+ return {
28056
+ content: [
28057
+ {
28058
+ type: "text",
28059
+ text: `Resolved doc query "${input.doc_id}" to doc ID ${resolvedDocId}.
28060
+
28061
+ ${formatContent(
28062
+ getDocResult
28063
+ )}`
28064
+ }
28065
+ ]
28066
+ };
28067
+ }
28068
+ }
28069
+ const topMatches = rankedMatches.slice(0, 5).map((entry) => ({
28070
+ ...entry.doc ?? {},
28071
+ match_score: entry.score,
28072
+ exact_match: entry.exact,
28073
+ match_source: entry.source
28074
+ }));
28075
+ const noMatchMessage = topMatches.length > 0 ? `Could not resolve "${input.doc_id}" to a single doc confidently.` : `No docs found matching "${input.doc_id}".`;
27406
28076
  return {
27407
- content: [{ type: "text", text: formatContent(getDocResult) }]
28077
+ content: [
28078
+ {
28079
+ type: "text",
28080
+ text: `${noMatchMessage}
28081
+
28082
+ ${formatContent({
28083
+ query: input.doc_id,
28084
+ doc_matches: topMatches
28085
+ })}`
28086
+ }
28087
+ ]
27408
28088
  };
27409
28089
  }
27410
28090
  case "update_doc": {
27411
28091
  if (!input.doc_id) {
27412
28092
  return errorResult("update_doc requires: doc_id");
27413
28093
  }
28094
+ const updateDocId = normalizeUuid(input.doc_id);
28095
+ if (!updateDocId) {
28096
+ return errorResult("update_doc requires a valid UUID doc_id.");
28097
+ }
27414
28098
  const updateDocResult = await client.docsUpdate({
27415
- doc_id: input.doc_id,
28099
+ doc_id: updateDocId,
27416
28100
  title: input.title,
27417
28101
  content: input.content,
27418
28102
  doc_type: input.doc_type,
@@ -27426,7 +28110,11 @@ ${formatContent(result)}`
27426
28110
  if (!input.doc_id) {
27427
28111
  return errorResult("delete_doc requires: doc_id");
27428
28112
  }
27429
- const deleteDocResult = await client.docsDelete({ doc_id: input.doc_id });
28113
+ const deleteDocId = normalizeUuid(input.doc_id);
28114
+ if (!deleteDocId) {
28115
+ return errorResult("delete_doc requires a valid UUID doc_id.");
28116
+ }
28117
+ const deleteDocResult = await client.docsDelete({ doc_id: deleteDocId });
27430
28118
  return {
27431
28119
  content: [{ type: "text", text: formatContent(deleteDocResult) }]
27432
28120
  };
@@ -27841,13 +28529,7 @@ ${formatContent(result)}`
27841
28529
  if (!projectId) {
27842
28530
  return errorResult("ingest requires: project_id");
27843
28531
  }
27844
- const result = await client.graphIngest({
27845
- project_id: projectId,
27846
- wait: input.wait
27847
- });
27848
- return {
27849
- content: [{ type: "text", text: formatContent(result) }]
27850
- };
28532
+ return runGraphIngestWithPreflight(projectId, input.wait ?? false);
27851
28533
  }
27852
28534
  case "circular_dependencies": {
27853
28535
  if (!projectId) {
@@ -31705,6 +32387,7 @@ init_hooks_config();
31705
32387
  init_files();
31706
32388
  var EDITOR_LABELS = {
31707
32389
  codex: "Codex CLI",
32390
+ opencode: "OpenCode",
31708
32391
  claude: "Claude Code",
31709
32392
  cursor: "Cursor / VS Code",
31710
32393
  cline: "Cline",
@@ -31714,7 +32397,7 @@ var EDITOR_LABELS = {
31714
32397
  antigravity: "Antigravity (Google)"
31715
32398
  };
31716
32399
  function supportsProjectMcpConfig(editor) {
31717
- return editor === "cursor" || editor === "claude" || editor === "kilo" || editor === "roo" || editor === "antigravity";
32400
+ return editor === "opencode" || editor === "cursor" || editor === "claude" || editor === "kilo" || editor === "roo" || editor === "antigravity";
31718
32401
  }
31719
32402
  function normalizeInput(value) {
31720
32403
  return value.trim();
@@ -31960,6 +32643,28 @@ async function isCodexInstalled() {
31960
32643
  ].filter((candidate) => Boolean(candidate));
31961
32644
  return anyPathExists(candidates);
31962
32645
  }
32646
+ function openCodeConfigPath() {
32647
+ const home = homedir6();
32648
+ const xdgConfigHome = process.env.XDG_CONFIG_HOME;
32649
+ if (process.platform === "win32") {
32650
+ const appData = process.env.APPDATA || path8.join(home, "AppData", "Roaming");
32651
+ return path8.join(appData, "opencode", "opencode.json");
32652
+ }
32653
+ const configRoot = xdgConfigHome || path8.join(home, ".config");
32654
+ return path8.join(configRoot, "opencode", "opencode.json");
32655
+ }
32656
+ async function isOpenCodeInstalled() {
32657
+ const configPath = openCodeConfigPath();
32658
+ const configDir = path8.dirname(configPath);
32659
+ const home = homedir6();
32660
+ const candidates = [
32661
+ configDir,
32662
+ configPath,
32663
+ path8.join(home, ".bun", "bin", "opencode"),
32664
+ path8.join(home, ".local", "bin", "opencode")
32665
+ ];
32666
+ return anyPathExists(candidates);
32667
+ }
31963
32668
  async function isClaudeInstalled() {
31964
32669
  const home = homedir6();
31965
32670
  const candidates = [path8.join(home, ".claude"), path8.join(home, ".config", "claude")];
@@ -32031,10 +32736,12 @@ async function isAntigravityInstalled() {
32031
32736
  const localApp = process.env.LOCALAPPDATA;
32032
32737
  const programFiles = process.env.ProgramFiles;
32033
32738
  const programFilesX86 = process.env["ProgramFiles(x86)"];
32034
- if (localApp) candidates.push(path8.join(localApp, "Programs", "Antigravity", "Antigravity.exe"));
32739
+ if (localApp)
32740
+ candidates.push(path8.join(localApp, "Programs", "Antigravity", "Antigravity.exe"));
32035
32741
  if (localApp) candidates.push(path8.join(localApp, "Antigravity", "Antigravity.exe"));
32036
32742
  if (programFiles) candidates.push(path8.join(programFiles, "Antigravity", "Antigravity.exe"));
32037
- if (programFilesX86) candidates.push(path8.join(programFilesX86, "Antigravity", "Antigravity.exe"));
32743
+ if (programFilesX86)
32744
+ candidates.push(path8.join(programFilesX86, "Antigravity", "Antigravity.exe"));
32038
32745
  } else {
32039
32746
  candidates.push("/usr/bin/antigravity");
32040
32747
  candidates.push("/usr/local/bin/antigravity");
@@ -32047,6 +32754,8 @@ async function isEditorInstalled(editor) {
32047
32754
  switch (editor) {
32048
32755
  case "codex":
32049
32756
  return isCodexInstalled();
32757
+ case "opencode":
32758
+ return isOpenCodeInstalled();
32050
32759
  case "claude":
32051
32760
  return isClaudeInstalled();
32052
32761
  case "cursor":
@@ -32066,6 +32775,8 @@ async function isEditorInstalled(editor) {
32066
32775
  }
32067
32776
  }
32068
32777
  var IS_WINDOWS = process.platform === "win32";
32778
+ var DEFAULT_CONTEXTSTREAM_API_URL = "https://api.contextstream.io";
32779
+ var OPENCODE_CONFIG_SCHEMA_URL = "https://opencode.ai/config.json";
32069
32780
  function escapeTomlString(value) {
32070
32781
  return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
32071
32782
  }
@@ -32112,6 +32823,26 @@ function buildContextStreamVsCodeServer(params) {
32112
32823
  env
32113
32824
  };
32114
32825
  }
32826
+ function buildContextStreamOpenCodeEnvironment(params) {
32827
+ const environment = {
32828
+ CONTEXTSTREAM_API_KEY: "{env:CONTEXTSTREAM_API_KEY}"
32829
+ };
32830
+ if (normalizeApiUrl(params.apiUrl) !== DEFAULT_CONTEXTSTREAM_API_URL) {
32831
+ environment.CONTEXTSTREAM_API_URL = params.apiUrl;
32832
+ }
32833
+ if (params.contextPackEnabled === false) {
32834
+ environment.CONTEXTSTREAM_CONTEXT_PACK = "false";
32835
+ }
32836
+ return environment;
32837
+ }
32838
+ function buildContextStreamOpenCodeLocalServer(params) {
32839
+ return {
32840
+ type: "local",
32841
+ command: ["npx", "-y", "contextstream-mcp"],
32842
+ environment: buildContextStreamOpenCodeEnvironment(params),
32843
+ enabled: true
32844
+ };
32845
+ }
32115
32846
  function stripJsonComments(input) {
32116
32847
  return input.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1");
32117
32848
  }
@@ -32170,6 +32901,32 @@ async function upsertJsonVsCodeMcpConfig(filePath, server) {
32170
32901
  if (!exists) return "created";
32171
32902
  return before === after ? "skipped" : "updated";
32172
32903
  }
32904
+ async function upsertOpenCodeMcpConfig(filePath, server) {
32905
+ await fs7.mkdir(path8.dirname(filePath), { recursive: true });
32906
+ const exists = await fileExists(filePath);
32907
+ let root = {};
32908
+ if (exists) {
32909
+ const raw = await fs7.readFile(filePath, "utf8").catch(() => "");
32910
+ const parsed = tryParseJsonLike(raw);
32911
+ if (!parsed.ok) throw new Error(`Invalid JSON in ${filePath}: ${parsed.error}`);
32912
+ root = parsed.value;
32913
+ }
32914
+ if (!root || typeof root !== "object" || Array.isArray(root)) root = {};
32915
+ if (!root.mcp || typeof root.mcp !== "object" || Array.isArray(root.mcp)) root.mcp = {};
32916
+ const before = JSON.stringify({
32917
+ schema: root.$schema ?? null,
32918
+ contextstream: root.mcp.contextstream ?? null
32919
+ });
32920
+ root.$schema = OPENCODE_CONFIG_SCHEMA_URL;
32921
+ root.mcp.contextstream = server;
32922
+ const after = JSON.stringify({
32923
+ schema: root.$schema ?? null,
32924
+ contextstream: root.mcp.contextstream ?? null
32925
+ });
32926
+ await fs7.writeFile(filePath, JSON.stringify(root, null, 2) + "\n", "utf8");
32927
+ if (!exists) return "created";
32928
+ return before === after ? "skipped" : "updated";
32929
+ }
32173
32930
  function claudeDesktopConfigPath() {
32174
32931
  const home = homedir6();
32175
32932
  if (process.platform === "darwin") {
@@ -32374,9 +33131,7 @@ async function selectProjectForCurrentDirectory(client, cwd, workspaceId, dryRun
32374
33131
  }
32375
33132
  console.log("\nProject selection (current directory):");
32376
33133
  options.forEach((opt, i) => console.log(` ${i + 1}) ${opt.label}`));
32377
- const choiceRaw = normalizeInput(
32378
- await rl.question(`Choose [1-${options.length}] (default 1): `)
32379
- );
33134
+ const choiceRaw = normalizeInput(await rl.question(`Choose [1-${options.length}] (default 1): `));
32380
33135
  const choiceNum = Number.parseInt(choiceRaw || "1", 10);
32381
33136
  const selected = Number.isFinite(choiceNum) ? options[choiceNum - 1] : options[0];
32382
33137
  if (!selected || selected.kind === "skip") {
@@ -32442,7 +33197,9 @@ ${colors.bright}Updating index for '${projectName}'...${colors.reset}`);
32442
33197
  }
32443
33198
  }
32444
33199
  if (!projectId) {
32445
- console.log(`${colors.yellow}! Could not resolve project ID for ${projectName}${colors.reset}`);
33200
+ console.log(
33201
+ `${colors.yellow}! Could not resolve project ID for ${projectName}${colors.reset}`
33202
+ );
32446
33203
  return;
32447
33204
  }
32448
33205
  } catch (err) {
@@ -32460,7 +33217,9 @@ ${colors.bright}Updating index for '${projectName}'...${colors.reset}`);
32460
33217
  console.log(`${colors.dim}No indexable files found${colors.reset}`);
32461
33218
  return;
32462
33219
  } else {
32463
- console.log(`${colors.dim}Found ${totalFiles.toLocaleString()} files for indexing${colors.reset}`);
33220
+ console.log(
33221
+ `${colors.dim}Found ${totalFiles.toLocaleString()} files for indexing${colors.reset}`
33222
+ );
32464
33223
  }
32465
33224
  } catch {
32466
33225
  console.log(`${colors.dim}Scanning files...${colors.reset}`);
@@ -32480,7 +33239,9 @@ ${colors.bright}Updating index for '${projectName}'...${colors.reset}`);
32480
33239
  const percentage = (progress * 100).toFixed(1);
32481
33240
  const speed = filesPerSec.toFixed(1);
32482
33241
  const size = formatBytes(bytesIndexed);
32483
- process.stdout.write(`\r${colors.cyan}${spinner}${colors.reset} ${progressBar} ${colors.bright}${percentage}%${colors.reset} | ${colors.green}${filesIndexed.toLocaleString()}${colors.reset}/${totalFiles.toLocaleString()} files | ${colors.magenta}${size}${colors.reset} | ${colors.blue}${speed} files/s${colors.reset} `);
33242
+ process.stdout.write(
33243
+ `\r${colors.cyan}${spinner}${colors.reset} ${progressBar} ${colors.bright}${percentage}%${colors.reset} | ${colors.green}${filesIndexed.toLocaleString()}${colors.reset}/${totalFiles.toLocaleString()} files | ${colors.magenta}${size}${colors.reset} | ${colors.blue}${speed} files/s${colors.reset} `
33244
+ );
32484
33245
  };
32485
33246
  const progressInterval = setInterval(updateProgress, 80);
32486
33247
  const ingestWithRetry = async (batch, maxRetries = 3) => {
@@ -32798,7 +33559,7 @@ Code: ${device.user_code}`);
32798
33559
  );
32799
33560
  }
32800
33561
  }
32801
- const NO_HOOKS_EDITORS2 = ["codex", "aider", "antigravity"];
33562
+ const NO_HOOKS_EDITORS2 = ["codex", "opencode", "aider", "antigravity"];
32802
33563
  const getModeForEditor = (editor) => NO_HOOKS_EDITORS2.includes(editor) ? "full" : "bootstrap";
32803
33564
  const detectedPlanName = await client.getPlanName();
32804
33565
  const detectedGraphTier = await client.getGraphTier();
@@ -32809,7 +33570,9 @@ Detected plan: ${planLabel} (graph: ${graphTierLabel})`);
32809
33570
  const contextPackEnabled = !!detectedPlanName && ["pro", "team", "enterprise"].some((p) => detectedPlanName.toLowerCase().includes(p));
32810
33571
  console.log("\nAuto-Update:");
32811
33572
  console.log(" When enabled, ContextStream will automatically update to the latest version");
32812
- console.log(" on new sessions (checks daily). You can disable this if you prefer manual updates.");
33573
+ console.log(
33574
+ " on new sessions (checks daily). You can disable this if you prefer manual updates."
33575
+ );
32813
33576
  const currentAutoUpdate = isAutoUpdateEnabled();
32814
33577
  const autoUpdateChoice = normalizeInput(
32815
33578
  await rl.question(`Enable auto-update? [${currentAutoUpdate ? "Y/n" : "y/N"}]: `)
@@ -32823,6 +33586,7 @@ Detected plan: ${planLabel} (graph: ${graphTierLabel})`);
32823
33586
  }
32824
33587
  const editors = [
32825
33588
  "codex",
33589
+ "opencode",
32826
33590
  "claude",
32827
33591
  "cursor",
32828
33592
  "cline",
@@ -32896,7 +33660,20 @@ Detected plan: ${planLabel} (graph: ${graphTierLabel})`);
32896
33660
  const mcpScope = mcpChoice === "2" && hasCodex && !hasProjectMcpEditors ? "skip" : mcpChoice === "4" ? "skip" : mcpChoice === "1" ? "global" : mcpChoice === "2" ? "project" : "both";
32897
33661
  const mcpServer = buildContextStreamMcpServer({ apiUrl, apiKey, contextPackEnabled });
32898
33662
  const mcpServerClaude = buildContextStreamMcpServer({ apiUrl, apiKey, contextPackEnabled });
33663
+ const mcpServerOpenCode = buildContextStreamOpenCodeLocalServer({
33664
+ apiUrl,
33665
+ apiKey,
33666
+ contextPackEnabled
33667
+ });
32899
33668
  const vsCodeServer = buildContextStreamVsCodeServer({ apiUrl, apiKey, contextPackEnabled });
33669
+ let hasPrintedOpenCodeEnvNote = false;
33670
+ const printOpenCodeEnvNote = () => {
33671
+ if (hasPrintedOpenCodeEnvNote) return;
33672
+ hasPrintedOpenCodeEnvNote = true;
33673
+ console.log(
33674
+ " OpenCode reads CONTEXTSTREAM_API_KEY from your environment. Export it before launching OpenCode."
33675
+ );
33676
+ };
32900
33677
  const needsGlobalMcpConfig = mcpScope === "global" || mcpScope === "both" || mcpScope === "project" && hasCodex;
32901
33678
  if (needsGlobalMcpConfig) {
32902
33679
  console.log("\nInstalling global MCP config...");
@@ -32948,6 +33725,20 @@ Detected plan: ${planLabel} (graph: ${graphTierLabel})`);
32948
33725
  );
32949
33726
  continue;
32950
33727
  }
33728
+ if (editor === "opencode") {
33729
+ const filePath = openCodeConfigPath();
33730
+ if (dryRun) {
33731
+ writeActions.push({ kind: "mcp-config", target: filePath, status: "dry-run" });
33732
+ console.log(`- ${EDITOR_LABELS[editor]}: would update ${filePath}`);
33733
+ printOpenCodeEnvNote();
33734
+ continue;
33735
+ }
33736
+ const status = await upsertOpenCodeMcpConfig(filePath, mcpServerOpenCode);
33737
+ writeActions.push({ kind: "mcp-config", target: filePath, status });
33738
+ console.log(`- ${EDITOR_LABELS[editor]}: ${status} ${filePath}`);
33739
+ printOpenCodeEnvNote();
33740
+ continue;
33741
+ }
32951
33742
  if (editor === "cursor") {
32952
33743
  const filePath = path8.join(homedir6(), ".cursor", "mcp.json");
32953
33744
  if (dryRun) {
@@ -32990,6 +33781,8 @@ Detected plan: ${planLabel} (graph: ${graphTierLabel})`);
32990
33781
  kilo: "kilo",
32991
33782
  codex: null,
32992
33783
  // No hooks API
33784
+ opencode: null,
33785
+ // No hooks API
32993
33786
  aider: null,
32994
33787
  // No hooks API
32995
33788
  antigravity: null
@@ -33030,6 +33823,12 @@ Detected plan: ${planLabel} (graph: ${graphTierLabel})`);
33030
33823
  if (scope === "global" || scope === "both") {
33031
33824
  console.log("\nInstalling global rules...");
33032
33825
  for (const editor of configuredEditors) {
33826
+ if (editor === "opencode") {
33827
+ console.log(
33828
+ `- ${EDITOR_LABELS[editor]}: rules are not auto-generated yet (MCP config only).`
33829
+ );
33830
+ continue;
33831
+ }
33033
33832
  const filePath = globalRulesPathForEditor(editor);
33034
33833
  if (!filePath) {
33035
33834
  console.log(
@@ -33170,6 +33969,21 @@ Applying to ${projects.length} project(s)...`);
33170
33969
  }
33171
33970
  continue;
33172
33971
  }
33972
+ if (editor === "opencode") {
33973
+ const openCodePath = path8.join(projectPath, "opencode.json");
33974
+ if (dryRun) {
33975
+ writeActions.push({
33976
+ kind: "mcp-config",
33977
+ target: openCodePath,
33978
+ status: "dry-run"
33979
+ });
33980
+ } else {
33981
+ const status = await upsertOpenCodeMcpConfig(openCodePath, mcpServerOpenCode);
33982
+ writeActions.push({ kind: "mcp-config", target: openCodePath, status });
33983
+ }
33984
+ printOpenCodeEnvNote();
33985
+ continue;
33986
+ }
33173
33987
  if (editor === "kilo") {
33174
33988
  const kiloPath = path8.join(projectPath, ".kilocode", "mcp.json");
33175
33989
  if (dryRun) {
@@ -33201,6 +34015,7 @@ Applying to ${projects.length} project(s)...`);
33201
34015
  for (const editor of selectedEditors) {
33202
34016
  if (scope !== "project" && scope !== "both") continue;
33203
34017
  if (!configuredEditors.includes(editor)) continue;
34018
+ if (editor === "opencode") continue;
33204
34019
  const rule = generateRuleContent(editor, {
33205
34020
  workspaceName,
33206
34021
  workspaceId: workspaceId && workspaceId !== "dry-run" ? workspaceId : void 0,
@@ -33240,11 +34055,15 @@ Applying to ${projects.length} project(s)...`);
33240
34055
  console.log("PROJECT INDEXING");
33241
34056
  console.log("\u2500".repeat(60));
33242
34057
  if (filesIndexed === 0) {
33243
- console.log("Indexing enables semantic code search and AI-powered graph knowledge for rich AI context.");
34058
+ console.log(
34059
+ "Indexing enables semantic code search and AI-powered graph knowledge for rich AI context."
34060
+ );
33244
34061
  } else {
33245
34062
  console.log("Your project index is stale and could use a refresh.");
33246
34063
  }
33247
- console.log("Powered by our blazing-fast Rust engine, indexing typically takes under a minute,");
34064
+ console.log(
34065
+ "Powered by our blazing-fast Rust engine, indexing typically takes under a minute,"
34066
+ );
33248
34067
  console.log("though larger projects may take a bit longer.\n");
33249
34068
  console.log("Your code is private and securely stored.\n");
33250
34069
  const indexChoice = normalizeInput(
@@ -33259,7 +34078,9 @@ Applying to ${projects.length} project(s)...`);
33259
34078
  if (indexingEnabled) {
33260
34079
  await indexProjectWithProgress(client, process.cwd(), cwdConfig.workspace_id);
33261
34080
  } else {
33262
- console.log("\nIndexing skipped for now. You can start it later with: contextstream-mcp index <path>");
34081
+ console.log(
34082
+ "\nIndexing skipped for now. You can start it later with: contextstream-mcp index <path>"
34083
+ );
33263
34084
  }
33264
34085
  }
33265
34086
  } catch {
@@ -33269,8 +34090,12 @@ Applying to ${projects.length} project(s)...`);
33269
34090
  console.log("\n" + "\u2500".repeat(60));
33270
34091
  console.log("PROJECT INDEXING");
33271
34092
  console.log("\u2500".repeat(60));
33272
- console.log("Indexing enables semantic code search and AI-powered graph knowledge for rich AI context.");
33273
- console.log("Powered by our blazing-fast Rust engine, indexing typically takes under a minute,");
34093
+ console.log(
34094
+ "Indexing enables semantic code search and AI-powered graph knowledge for rich AI context."
34095
+ );
34096
+ console.log(
34097
+ "Powered by our blazing-fast Rust engine, indexing typically takes under a minute,"
34098
+ );
33274
34099
  console.log("though larger projects may take a bit longer.\n");
33275
34100
  console.log("Your code is private and securely stored.\n");
33276
34101
  const indexChoice = normalizeInput(
@@ -33292,7 +34117,9 @@ Applying to ${projects.length} project(s)...`);
33292
34117
  await indexProjectWithProgress(client, projectPath, workspaceId);
33293
34118
  }
33294
34119
  } else {
33295
- console.log("\nIndexing skipped for now. You can start it later with: contextstream-mcp index <path>");
34120
+ console.log(
34121
+ "\nIndexing skipped for now. You can start it later with: contextstream-mcp index <path>"
34122
+ );
33296
34123
  }
33297
34124
  }
33298
34125
  console.log("\nDone.");
@@ -33313,7 +34140,9 @@ Applying to ${projects.length} project(s)...`);
33313
34140
  "- For UI-based MCP setup (Cline/Kilo/Roo global), see https://contextstream.io/docs/mcp"
33314
34141
  );
33315
34142
  console.log("");
33316
- console.log("You're all set! ContextStream gives your AI persistent memory, semantic code search, and cross-session context.");
34143
+ console.log(
34144
+ "You're all set! ContextStream gives your AI persistent memory, semantic code search, and cross-session context."
34145
+ );
33317
34146
  console.log("More at: https://contextstream.io/docs/mcp");
33318
34147
  } finally {
33319
34148
  rl.close();