@contextstream/mcp-server 0.4.64 → 0.4.65

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,15 @@ 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
+ }
12916
12922
  function pickString(value) {
12917
12923
  if (typeof value !== "string") return null;
12918
12924
  const trimmed = value.trim();
@@ -13448,7 +13454,12 @@ var ContextStreamClient = class _ContextStreamClient {
13448
13454
  if (!body.content || body.content.trim().length === 0) {
13449
13455
  throw new Error("content is required and cannot be empty");
13450
13456
  }
13451
- return request(this.config, "/memory/events", { body: withDefaults });
13457
+ const normalizedTags = normalizeTags(body.tags);
13458
+ const apiBody = {
13459
+ ...withDefaults,
13460
+ ...normalizedTags ? { tags: normalizedTags } : {}
13461
+ };
13462
+ return request(this.config, "/memory/events", { body: apiBody });
13452
13463
  }
13453
13464
  bulkIngestEvents(body) {
13454
13465
  return request(this.config, "/memory/events/ingest", { body: this.withDefaults(body) });
@@ -14863,7 +14874,7 @@ var ContextStreamClient = class _ContextStreamClient {
14863
14874
  async captureContext(params) {
14864
14875
  const withDefaults = this.withDefaults(params);
14865
14876
  let apiEventType = "manual_note";
14866
- const tags = params.tags || [];
14877
+ const tags = [...params.tags || []];
14867
14878
  switch (params.event_type) {
14868
14879
  case "conversation":
14869
14880
  apiEventType = "chat";
@@ -14906,18 +14917,20 @@ var ContextStreamClient = class _ContextStreamClient {
14906
14917
  apiEventType = "manual_note";
14907
14918
  tags.push(params.event_type);
14908
14919
  }
14920
+ const normalizedTags = normalizeTags(tags);
14909
14921
  return this.createMemoryEvent({
14910
14922
  workspace_id: withDefaults.workspace_id,
14911
14923
  project_id: withDefaults.project_id,
14912
14924
  event_type: apiEventType,
14913
14925
  title: params.title,
14914
14926
  content: params.content,
14927
+ tags: normalizedTags,
14915
14928
  provenance: params.provenance,
14916
14929
  code_refs: params.code_refs,
14917
14930
  metadata: {
14918
14931
  original_type: params.event_type,
14919
14932
  session_id: params.session_id,
14920
- tags,
14933
+ ...normalizedTags ? { tags: normalizedTags } : {},
14921
14934
  importance: params.importance || "medium",
14922
14935
  captured_at: (/* @__PURE__ */ new Date()).toISOString(),
14923
14936
  source: "mcp_auto_capture"
@@ -14933,8 +14946,9 @@ var ContextStreamClient = class _ContextStreamClient {
14933
14946
  const metadata = {
14934
14947
  ...params.metadata || {}
14935
14948
  };
14936
- if (params.tags && params.tags.length > 0) {
14937
- metadata.tags = params.tags;
14949
+ const normalizedTags = normalizeTags(params.tags);
14950
+ if (normalizedTags) {
14951
+ metadata.tags = normalizedTags;
14938
14952
  }
14939
14953
  if (!metadata.captured_at) {
14940
14954
  metadata.captured_at = (/* @__PURE__ */ new Date()).toISOString();
@@ -14948,6 +14962,7 @@ var ContextStreamClient = class _ContextStreamClient {
14948
14962
  event_type: params.event_type,
14949
14963
  title: params.title,
14950
14964
  content: params.content,
14965
+ tags: normalizedTags,
14951
14966
  provenance: params.provenance,
14952
14967
  code_refs: params.code_refs,
14953
14968
  metadata
@@ -14967,6 +14982,7 @@ var ContextStreamClient = class _ContextStreamClient {
14967
14982
  */
14968
14983
  async sessionRemember(params) {
14969
14984
  const withDefaults = this.withDefaults(params);
14985
+ const normalizedTags = normalizeTags(params.tags);
14970
14986
  if (!withDefaults.workspace_id) {
14971
14987
  throw new Error(
14972
14988
  "workspace_id is required for session_remember. Set defaultWorkspaceId in config or provide workspace_id."
@@ -14978,7 +14994,8 @@ var ContextStreamClient = class _ContextStreamClient {
14978
14994
  workspace_id: withDefaults.workspace_id,
14979
14995
  project_id: withDefaults.project_id,
14980
14996
  importance: params.importance,
14981
- await_indexing: params.await_indexing
14997
+ await_indexing: params.await_indexing,
14998
+ ...normalizedTags ? { tags: normalizedTags } : {}
14982
14999
  }
14983
15000
  });
14984
15001
  }
@@ -17045,6 +17062,7 @@ ${context}`;
17045
17062
  if (withDefaults.project_id) query.set("project_id", withDefaults.project_id);
17046
17063
  if (params?.doc_type) query.set("doc_type", params.doc_type);
17047
17064
  if (params?.is_personal !== void 0) query.set("is_personal", String(params.is_personal));
17065
+ if (params?.query) query.set("query", params.query);
17048
17066
  if (params?.page) query.set("page", String(params.page));
17049
17067
  if (params?.per_page) query.set("per_page", String(params.per_page));
17050
17068
  const suffix = query.toString() ? `?${query.toString()}` : "";
@@ -17694,6 +17712,56 @@ function classifyIndexConfidence(indexed, apiIndexed, locallyIndexed, freshness)
17694
17712
  reason: "Index state is inferred but lacks corroborating API/local metadata."
17695
17713
  };
17696
17714
  }
17715
+ function classifyGraphIngestIndexState(input) {
17716
+ const { statusResult, locallyIndexed } = input;
17717
+ const candidates = candidateObjects(statusResult);
17718
+ const projectIndexState = readString(candidates, "project_index_state")?.toLowerCase();
17719
+ const indexInProgress = apiResultIsIndexing(statusResult);
17720
+ const indexed = apiResultReportsIndexed(statusResult) || locallyIndexed;
17721
+ const indexedAt = extractIndexTimestamp(statusResult);
17722
+ const ageHours = indexedAt !== void 0 ? Math.floor((Date.now() - indexedAt.getTime()) / (1e3 * 60 * 60)) : void 0;
17723
+ const freshness = classifyIndexFreshness(indexed, ageHours);
17724
+ if (indexInProgress) {
17725
+ return {
17726
+ state: "indexing",
17727
+ freshness,
17728
+ indexInProgress,
17729
+ indexed,
17730
+ projectIndexState,
17731
+ ageHours
17732
+ };
17733
+ }
17734
+ const explicitlyMissing = projectIndexState === "missing" || projectIndexState === "not_indexed" || projectIndexState === "unindexed";
17735
+ if (!indexed || explicitlyMissing) {
17736
+ return {
17737
+ state: "missing",
17738
+ freshness,
17739
+ indexInProgress,
17740
+ indexed,
17741
+ projectIndexState,
17742
+ ageHours
17743
+ };
17744
+ }
17745
+ const explicitlyStale = projectIndexState === "stale";
17746
+ if (freshness === "stale" || explicitlyStale) {
17747
+ return {
17748
+ state: "stale",
17749
+ freshness,
17750
+ indexInProgress,
17751
+ indexed,
17752
+ projectIndexState,
17753
+ ageHours
17754
+ };
17755
+ }
17756
+ return {
17757
+ state: "ready",
17758
+ freshness,
17759
+ indexInProgress,
17760
+ indexed,
17761
+ projectIndexState,
17762
+ ageHours
17763
+ };
17764
+ }
17697
17765
 
17698
17766
  // src/todo-utils.ts
17699
17767
  function normalizeTodoStatus(status) {
@@ -17942,6 +18010,32 @@ After updating, restart the AI tool to use the new version.
17942
18010
  \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
18011
  `.trim();
17944
18012
  }
18013
+ function generateSuggestedRulesNotice(result) {
18014
+ const suggestedRules = result.suggested_rules;
18015
+ if (!suggestedRules || suggestedRules.length === 0) {
18016
+ return "";
18017
+ }
18018
+ const ruleLines = suggestedRules.slice(0, 3).map((rule, i) => {
18019
+ const cat = rule.category || "general";
18020
+ const confidence = rule.confidence ? `${Math.round(rule.confidence * 100)}%` : "?";
18021
+ const count = rule.occurrence_count || 0;
18022
+ const keywords = (rule.keywords || []).join(", ");
18023
+ return `${i + 1}. [${cat}] ${rule.instruction || ""} (confidence: ${confidence}, seen ${count}x)
18024
+ Keywords: ${keywords}
18025
+ Rule ID: ${rule.id}`;
18026
+ });
18027
+ return `
18028
+
18029
+ \u{1F4A1} [SUGGESTED_RULES] ContextStream detected recurring patterns and generated rule suggestions.
18030
+ \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
18031
+ Present these to the user. They can accept or reject each one.
18032
+
18033
+ ${ruleLines.join("\n")}
18034
+
18035
+ To accept: session(action="suggested_rule_action", rule_id="<id>", rule_action="accept")
18036
+ To reject: session(action="suggested_rule_action", rule_id="<id>", rule_action="reject")
18037
+ \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`;
18038
+ }
17945
18039
  var DEFAULT_PARAM_DESCRIPTIONS = {
17946
18040
  api_key: "ContextStream API key.",
17947
18041
  apiKey: "ContextStream API key.",
@@ -20231,6 +20325,9 @@ Hint: Run session_init(folder_path="<your_project_path>") first to establish a s
20231
20325
  if ("data" in value) return extractCollectionArray(value.data);
20232
20326
  return void 0;
20233
20327
  }
20328
+ function normalizeDocLookupText(value) {
20329
+ return value.toLowerCase().replace(/[^a-z0-9\s]/g, " ").replace(/\s+/g, " ").trim();
20330
+ }
20234
20331
  function tokenizeForDocMatch(query) {
20235
20332
  const stopWords = /* @__PURE__ */ new Set([
20236
20333
  "the",
@@ -20248,23 +20345,99 @@ Hint: Run session_init(folder_path="<your_project_path>") first to establish a s
20248
20345
  "find",
20249
20346
  "plan",
20250
20347
  "phase",
20251
- "phases"
20348
+ "phases",
20349
+ "get",
20350
+ "open",
20351
+ "show",
20352
+ "read"
20252
20353
  ]);
20253
- return query.split(/[^A-Za-z0-9]+/).map((term) => term.trim().toLowerCase()).filter((term) => term.length >= 3 && !stopWords.has(term));
20354
+ return normalizeDocLookupText(query).split(/\s+/).map((term) => term.trim()).filter((term) => term.length >= 3 && !stopWords.has(term));
20254
20355
  }
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);
20356
+ function scoreDocMatch(doc, query, terms) {
20357
+ const rawQuery = query.trim();
20358
+ const normalizedQuery = normalizeDocLookupText(rawQuery);
20359
+ const titleRaw = String(doc?.title ?? doc?.name ?? doc?.summary ?? "").trim();
20360
+ const title = normalizeDocLookupText(titleRaw);
20361
+ const docId = String(doc?.id ?? "").trim();
20362
+ if (docId && rawQuery && docId.toLowerCase() === rawQuery.toLowerCase()) {
20363
+ return { doc, score: 100, exact: true, source: "doc_id" };
20364
+ }
20365
+ if (title && normalizedQuery && title === normalizedQuery) {
20366
+ return { doc, score: 95, exact: true, source: "exact_title" };
20367
+ }
20368
+ if (title && normalizedQuery && title.includes(normalizedQuery) && normalizedQuery.length >= 8) {
20369
+ return { doc, score: 80, exact: false, source: "title_contains_query" };
20370
+ }
20371
+ if (terms.length === 0) {
20372
+ return { doc, score: 0, exact: false, source: "term_overlap" };
20373
+ }
20374
+ const matchedTerms = terms.filter((term) => title.includes(term)).length;
20375
+ if (matchedTerms === 0) {
20376
+ return { doc, score: 0, exact: false, source: "term_overlap" };
20377
+ }
20378
+ if (matchedTerms === terms.length) {
20379
+ return {
20380
+ doc,
20381
+ score: 60 + matchedTerms * 5,
20382
+ exact: false,
20383
+ source: "all_terms"
20384
+ };
20385
+ }
20386
+ return {
20387
+ doc,
20388
+ score: matchedTerms * 10,
20389
+ exact: false,
20390
+ source: "term_overlap"
20391
+ };
20260
20392
  }
20261
- function rankDocsForQuery(docs, query, limit) {
20393
+ function rankDocsForQueryMatches(docs, query, limit) {
20262
20394
  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) }));
20395
+ const scored = docs.map((doc, idx) => ({ idx, ...scoreDocMatch(doc, query, terms) }));
20265
20396
  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);
20397
+ return scored.filter((entry) => entry.score > 0).slice(0, limit);
20398
+ }
20399
+ function rankDocsForQuery(docs, query, limit) {
20400
+ const rankedMatches = rankDocsForQueryMatches(docs, query, limit);
20401
+ return rankedMatches.length > 0 ? rankedMatches.map((entry) => entry.doc) : docs.slice(0, limit);
20402
+ }
20403
+ function selectResolvedDocMatch(matches) {
20404
+ if (matches.length === 0) return void 0;
20405
+ if (matches[0].exact) return matches[0];
20406
+ const top = matches[0];
20407
+ const second = matches[1];
20408
+ if (top.score >= 80 && (!second || second.score <= 40)) {
20409
+ return top;
20410
+ }
20411
+ return void 0;
20412
+ }
20413
+ function extractMemorySearchResults(response) {
20414
+ const result = response?.data ?? response;
20415
+ if (Array.isArray(result?.results)) return result.results;
20416
+ if (Array.isArray(result?.items)) return result.items;
20417
+ return [];
20418
+ }
20419
+ function buildHybridMemoryDocResults(memoryResults, docMatches, limit) {
20420
+ const docs = docMatches.map((match) => ({
20421
+ entity_type: "doc",
20422
+ id: match.doc?.id ?? "unknown",
20423
+ title: match.doc?.title ?? match.doc?.name ?? "Untitled",
20424
+ preview: String(match.doc?.content ?? "").slice(0, 150),
20425
+ score: match.score,
20426
+ match_source: match.source,
20427
+ doc_type: match.doc?.doc_type ?? "general"
20428
+ }));
20429
+ const memory = memoryResults.map((item) => ({
20430
+ entity_type: "memory",
20431
+ id: item?.id ?? item?.node_id ?? "unknown",
20432
+ title: item?.title ?? item?.summary ?? item?.name ?? "Untitled",
20433
+ preview: String(item?.content ?? item?.details ?? "").slice(0, 150),
20434
+ score: Math.round(Number(item?.score ?? 0) * 100),
20435
+ match_source: "memory_search",
20436
+ node_type: item?.node_type ?? item?.event_type ?? item?.type ?? "unknown"
20437
+ }));
20438
+ const combined = [...docs, ...memory];
20439
+ combined.sort((a, b) => Number(b.score || 0) - Number(a.score || 0));
20440
+ return combined.slice(0, limit);
20268
20441
  }
20269
20442
  async function findDocsFallback(workspaceId, candidateProjectIds, query, limit) {
20270
20443
  const uniqueCandidates = [];
@@ -20398,6 +20571,137 @@ Hint: Run session_init(folder_path="<your_project_path>") first to establish a s
20398
20571
  }
20399
20572
  })();
20400
20573
  }
20574
+ async function runGraphIngestWithPreflight(projectId, wait) {
20575
+ const folderPath = resolveFolderPath(void 0, sessionManager);
20576
+ const localIndexProjectId = folderPath ? await indexedProjectIdForFolder(folderPath) : void 0;
20577
+ const locallyIndexed = localIndexProjectId === projectId;
20578
+ const suggestedPath = folderPath || "<your_project_path>";
20579
+ let statusResult = {};
20580
+ try {
20581
+ statusResult = await client.projectIndexStatus(projectId);
20582
+ } catch (error) {
20583
+ if (!isNotFoundError(error)) throw error;
20584
+ }
20585
+ const preflight = classifyGraphIngestIndexState({
20586
+ statusResult,
20587
+ locallyIndexed
20588
+ });
20589
+ const preflightData = {
20590
+ index_state: preflight.state,
20591
+ index_freshness: preflight.freshness,
20592
+ index_age_hours: preflight.ageHours ?? null,
20593
+ index_in_progress: preflight.indexInProgress,
20594
+ project_index_state: preflight.projectIndexState ?? null,
20595
+ locally_indexed: locallyIndexed
20596
+ };
20597
+ if (preflight.state === "indexing") {
20598
+ const deferred = {
20599
+ status: "deferred",
20600
+ wait,
20601
+ reason: "Project index refresh is in progress. Graph ingest is deferred until index refresh completes.",
20602
+ preflight: {
20603
+ ...preflightData,
20604
+ auto_refresh_started: false,
20605
+ graph_ingest_executed: false
20606
+ },
20607
+ next_step: `Run project(action="index_status") and retry graph(action="ingest") when freshness is not stale.`
20608
+ };
20609
+ return {
20610
+ content: [
20611
+ {
20612
+ type: "text",
20613
+ text: `Project indexing is currently in progress; graph ingest was deferred to avoid rebuilding edges from stale embeddings.
20614
+
20615
+ ${formatContent(deferred)}`
20616
+ }
20617
+ ],
20618
+ structuredContent: deferred
20619
+ };
20620
+ }
20621
+ if (preflight.state === "stale" || preflight.state === "missing") {
20622
+ const validPath = folderPath ? await validateReadableDirectory(folderPath) : void 0;
20623
+ if (!validPath?.ok) {
20624
+ return errorResult(
20625
+ `Graph ingest is blocked because the project index is ${preflight.state} and no readable project folder is available for auto-refresh.
20626
+ Run project(action="ingest_local", path="${suggestedPath}") first, then retry graph(action="ingest").`
20627
+ );
20628
+ }
20629
+ if (!preflight.indexInProgress) {
20630
+ startBackgroundIngest(projectId, validPath.resolvedPath, { force: true }, { preflight: true });
20631
+ }
20632
+ const deferred = {
20633
+ status: "deferred",
20634
+ wait,
20635
+ reason: `Project index is ${preflight.state}. Started index refresh before graph ingest to avoid destructive edge rebuilds from stale embeddings.`,
20636
+ preflight: {
20637
+ ...preflightData,
20638
+ auto_refresh_started: !preflight.indexInProgress,
20639
+ auto_refresh_path: validPath.resolvedPath,
20640
+ auto_refresh_force: true,
20641
+ graph_ingest_executed: false
20642
+ },
20643
+ next_step: `Monitor with project(action="index_status"), then rerun graph(action="ingest").`
20644
+ };
20645
+ return {
20646
+ content: [
20647
+ {
20648
+ type: "text",
20649
+ text: `Project index is ${preflight.state}; started index refresh and deferred graph ingest to prevent stale-edge rebuild.
20650
+
20651
+ ${formatContent(deferred)}`
20652
+ }
20653
+ ],
20654
+ structuredContent: deferred
20655
+ };
20656
+ }
20657
+ let estimate = null;
20658
+ try {
20659
+ const stats = await client.projectStatistics(projectId);
20660
+ estimate = estimateGraphIngestMinutes(stats);
20661
+ } catch (error) {
20662
+ logDebug(`Failed to fetch project statistics for graph estimate: ${error}`);
20663
+ }
20664
+ const result = await client.graphIngest({ project_id: projectId, wait });
20665
+ const estimateText = estimate ? `Estimated time: ${estimate.min}-${estimate.max} min${estimate.basis ? ` (based on ${estimate.basis})` : ""}.` : "Estimated time varies with repo size.";
20666
+ const note = `Graph ingestion is running ${wait ? "synchronously" : "asynchronously"} and can take a few minutes. ${estimateText}`;
20667
+ const structured = toStructured(result);
20668
+ const structuredContent = structured && typeof structured === "object" ? {
20669
+ ...structured,
20670
+ wait,
20671
+ note,
20672
+ preflight: {
20673
+ ...preflightData,
20674
+ auto_refresh_started: false,
20675
+ graph_ingest_executed: true
20676
+ },
20677
+ ...estimate ? {
20678
+ estimate_minutes: { min: estimate.min, max: estimate.max },
20679
+ estimate_basis: estimate.basis
20680
+ } : {}
20681
+ } : {
20682
+ wait,
20683
+ note,
20684
+ preflight: {
20685
+ ...preflightData,
20686
+ auto_refresh_started: false,
20687
+ graph_ingest_executed: true
20688
+ },
20689
+ ...estimate ? {
20690
+ estimate_minutes: { min: estimate.min, max: estimate.max },
20691
+ estimate_basis: estimate.basis
20692
+ } : {}
20693
+ };
20694
+ return {
20695
+ content: [
20696
+ {
20697
+ type: "text",
20698
+ text: `${note}
20699
+ ${formatContent(result)}`
20700
+ }
20701
+ ],
20702
+ structuredContent
20703
+ };
20704
+ }
20401
20705
  function summarizeVideoTextExtraction(metadata) {
20402
20706
  if (!metadata || typeof metadata !== "object") return void 0;
20403
20707
  const extraction = metadata.video_text_extraction;
@@ -21753,43 +22057,7 @@ Access: Free`,
21753
22057
  );
21754
22058
  }
21755
22059
  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
- };
22060
+ return runGraphIngestWithPreflight(projectId, wait);
21793
22061
  }
21794
22062
  );
21795
22063
  registerTool(
@@ -24406,6 +24674,7 @@ Action: ${cp.suggested_action === "prepare_save" ? "Consider saving important de
24406
24674
  const instructionsLine = result.instructions ? `
24407
24675
 
24408
24676
  [INSTRUCTIONS] ${result.instructions}` : "";
24677
+ const suggestedRulesLine = generateSuggestedRulesNotice(result);
24409
24678
  const contextRulesLine = `
24410
24679
 
24411
24680
  ${CONTEXT_CALL_REMINDER}`;
@@ -24418,6 +24687,7 @@ ${rulesWarningLine}` : "",
24418
24687
  versionWarningLine ? `
24419
24688
 
24420
24689
  ${versionWarningLine}` : "",
24690
+ suggestedRulesLine,
24421
24691
  contextPressureWarning,
24422
24692
  semanticHints,
24423
24693
  instructionsLine,
@@ -26150,7 +26420,8 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
26150
26420
  workspace_id: workspaceId,
26151
26421
  project_id: projectId,
26152
26422
  content: input.content,
26153
- importance
26423
+ importance,
26424
+ tags: input.tags
26154
26425
  });
26155
26426
  const rememberHint = getCaptureHint("preference");
26156
26427
  const resultWithHint = { ...result, hint: rememberHint };
@@ -26787,7 +27058,7 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
26787
27058
  order: external_exports.number().optional().describe("Task order within plan"),
26788
27059
  task_ids: external_exports.array(external_exports.string().uuid()).optional().describe("Task IDs for reorder_tasks"),
26789
27060
  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"),
27061
+ tags: external_exports.array(external_exports.string()).optional().describe("Tags for event or task categorization"),
26791
27062
  // Batch import params
26792
27063
  events: external_exports.array(
26793
27064
  external_exports.object({
@@ -26824,7 +27095,7 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
26824
27095
  diagram_id: external_exports.string().uuid().optional().describe("Diagram ID for get_diagram/update_diagram/delete_diagram"),
26825
27096
  diagram_type: external_exports.enum(["flowchart", "sequence", "class", "er", "gantt", "mindmap", "pie", "other"]).optional().describe("Mermaid diagram type"),
26826
27097
  // Doc params
26827
- doc_id: external_exports.string().uuid().optional().describe("Doc ID for get_doc/update_doc/delete_doc"),
27098
+ 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
27099
  doc_type: external_exports.enum(["roadmap", "spec", "general"]).optional().describe("Document type"),
26829
27100
  milestones: external_exports.array(
26830
27101
  external_exports.object({
@@ -26859,6 +27130,7 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
26859
27130
  event_type: input.event_type,
26860
27131
  title: input.title,
26861
27132
  content: input.content,
27133
+ tags: input.tags,
26862
27134
  metadata: input.metadata,
26863
27135
  provenance: input.provenance,
26864
27136
  code_refs: input.code_refs
@@ -27020,13 +27292,42 @@ ${formatContent(result)}`
27020
27292
  if (!input.query) {
27021
27293
  return errorResult("search requires: query");
27022
27294
  }
27023
- const result = await client.memorySearch({
27024
- workspace_id: workspaceId,
27025
- project_id: projectId,
27295
+ const [memoryResult, docsResult] = await Promise.all([
27296
+ client.memorySearch({
27297
+ workspace_id: workspaceId,
27298
+ project_id: projectId,
27299
+ query: input.query,
27300
+ limit: input.limit
27301
+ }),
27302
+ workspaceId ? client.docsList({
27303
+ workspace_id: workspaceId,
27304
+ project_id: projectId,
27305
+ per_page: Math.max(10, Math.min(input.limit ?? 10, 25))
27306
+ }) : Promise.resolve(void 0)
27307
+ ]);
27308
+ const memoryItems = extractMemorySearchResults(memoryResult);
27309
+ const docsItems = extractCollectionArray(docsResult ?? {}) ?? [];
27310
+ const docMatches = rankDocsForQueryMatches(
27311
+ docsItems,
27312
+ input.query,
27313
+ Math.max(1, Math.min(input.limit ?? 10, 25))
27314
+ );
27315
+ const hybridResults = buildHybridMemoryDocResults(
27316
+ memoryItems,
27317
+ docMatches,
27318
+ Math.max(1, Math.min(input.limit ?? 10, 25))
27319
+ );
27320
+ const outputText = formatContent({
27026
27321
  query: input.query,
27027
- limit: input.limit
27322
+ results: hybridResults,
27323
+ memory_results: memoryItems,
27324
+ doc_matches: docMatches.map((entry) => ({
27325
+ ...entry.doc,
27326
+ match_score: entry.score,
27327
+ exact_match: entry.exact,
27328
+ match_source: entry.source
27329
+ }))
27028
27330
  });
27029
- const outputText = formatContent(result);
27030
27331
  trackToolTokenSavings(client, "memory_search", outputText, {
27031
27332
  workspace_id: workspaceId,
27032
27333
  project_id: projectId
@@ -27402,17 +27703,80 @@ ${formatContent(result)}`
27402
27703
  if (!input.doc_id) {
27403
27704
  return errorResult("get_doc requires: doc_id");
27404
27705
  }
27405
- const getDocResult = await client.docsGet({ doc_id: input.doc_id });
27706
+ const directDocId = normalizeUuid(input.doc_id);
27707
+ if (directDocId) {
27708
+ const getDocResult = await client.docsGet({ doc_id: directDocId });
27709
+ return {
27710
+ content: [{ type: "text", text: formatContent(getDocResult) }]
27711
+ };
27712
+ }
27713
+ if (!workspaceId) {
27714
+ return errorResult(
27715
+ "get_doc title/query lookups require workspace_id. Call session_init first or pass a doc UUID."
27716
+ );
27717
+ }
27718
+ const candidateProjectIds = [projectId, explicitProjectId, void 0];
27719
+ const fallback = await findDocsFallback(
27720
+ workspaceId,
27721
+ candidateProjectIds,
27722
+ input.doc_id,
27723
+ Math.max(10, Math.min(input.limit ?? 20, 30))
27724
+ );
27725
+ const rankedMatches = rankDocsForQueryMatches(
27726
+ fallback?.docs ?? [],
27727
+ input.doc_id,
27728
+ Math.max(1, Math.min(input.limit ?? 10, 10))
27729
+ );
27730
+ const resolved = selectResolvedDocMatch(rankedMatches);
27731
+ if (resolved) {
27732
+ const resolvedDocId = String(resolved.doc?.id ?? "");
27733
+ if (normalizeUuid(resolvedDocId)) {
27734
+ const getDocResult = await client.docsGet({ doc_id: resolvedDocId });
27735
+ return {
27736
+ content: [
27737
+ {
27738
+ type: "text",
27739
+ text: `Resolved doc query "${input.doc_id}" to doc ID ${resolvedDocId}.
27740
+
27741
+ ${formatContent(
27742
+ getDocResult
27743
+ )}`
27744
+ }
27745
+ ]
27746
+ };
27747
+ }
27748
+ }
27749
+ const topMatches = rankedMatches.slice(0, 5).map((entry) => ({
27750
+ ...entry.doc ?? {},
27751
+ match_score: entry.score,
27752
+ exact_match: entry.exact,
27753
+ match_source: entry.source
27754
+ }));
27755
+ const noMatchMessage = topMatches.length > 0 ? `Could not resolve "${input.doc_id}" to a single doc confidently.` : `No docs found matching "${input.doc_id}".`;
27406
27756
  return {
27407
- content: [{ type: "text", text: formatContent(getDocResult) }]
27757
+ content: [
27758
+ {
27759
+ type: "text",
27760
+ text: `${noMatchMessage}
27761
+
27762
+ ${formatContent({
27763
+ query: input.doc_id,
27764
+ doc_matches: topMatches
27765
+ })}`
27766
+ }
27767
+ ]
27408
27768
  };
27409
27769
  }
27410
27770
  case "update_doc": {
27411
27771
  if (!input.doc_id) {
27412
27772
  return errorResult("update_doc requires: doc_id");
27413
27773
  }
27774
+ const updateDocId = normalizeUuid(input.doc_id);
27775
+ if (!updateDocId) {
27776
+ return errorResult("update_doc requires a valid UUID doc_id.");
27777
+ }
27414
27778
  const updateDocResult = await client.docsUpdate({
27415
- doc_id: input.doc_id,
27779
+ doc_id: updateDocId,
27416
27780
  title: input.title,
27417
27781
  content: input.content,
27418
27782
  doc_type: input.doc_type,
@@ -27426,7 +27790,11 @@ ${formatContent(result)}`
27426
27790
  if (!input.doc_id) {
27427
27791
  return errorResult("delete_doc requires: doc_id");
27428
27792
  }
27429
- const deleteDocResult = await client.docsDelete({ doc_id: input.doc_id });
27793
+ const deleteDocId = normalizeUuid(input.doc_id);
27794
+ if (!deleteDocId) {
27795
+ return errorResult("delete_doc requires a valid UUID doc_id.");
27796
+ }
27797
+ const deleteDocResult = await client.docsDelete({ doc_id: deleteDocId });
27430
27798
  return {
27431
27799
  content: [{ type: "text", text: formatContent(deleteDocResult) }]
27432
27800
  };
@@ -27841,13 +28209,7 @@ ${formatContent(result)}`
27841
28209
  if (!projectId) {
27842
28210
  return errorResult("ingest requires: project_id");
27843
28211
  }
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
- };
28212
+ return runGraphIngestWithPreflight(projectId, input.wait ?? false);
27851
28213
  }
27852
28214
  case "circular_dependencies": {
27853
28215
  if (!projectId) {
@@ -31705,6 +32067,7 @@ init_hooks_config();
31705
32067
  init_files();
31706
32068
  var EDITOR_LABELS = {
31707
32069
  codex: "Codex CLI",
32070
+ opencode: "OpenCode",
31708
32071
  claude: "Claude Code",
31709
32072
  cursor: "Cursor / VS Code",
31710
32073
  cline: "Cline",
@@ -31714,7 +32077,7 @@ var EDITOR_LABELS = {
31714
32077
  antigravity: "Antigravity (Google)"
31715
32078
  };
31716
32079
  function supportsProjectMcpConfig(editor) {
31717
- return editor === "cursor" || editor === "claude" || editor === "kilo" || editor === "roo" || editor === "antigravity";
32080
+ return editor === "opencode" || editor === "cursor" || editor === "claude" || editor === "kilo" || editor === "roo" || editor === "antigravity";
31718
32081
  }
31719
32082
  function normalizeInput(value) {
31720
32083
  return value.trim();
@@ -31960,6 +32323,28 @@ async function isCodexInstalled() {
31960
32323
  ].filter((candidate) => Boolean(candidate));
31961
32324
  return anyPathExists(candidates);
31962
32325
  }
32326
+ function openCodeConfigPath() {
32327
+ const home = homedir6();
32328
+ const xdgConfigHome = process.env.XDG_CONFIG_HOME;
32329
+ if (process.platform === "win32") {
32330
+ const appData = process.env.APPDATA || path8.join(home, "AppData", "Roaming");
32331
+ return path8.join(appData, "opencode", "opencode.json");
32332
+ }
32333
+ const configRoot = xdgConfigHome || path8.join(home, ".config");
32334
+ return path8.join(configRoot, "opencode", "opencode.json");
32335
+ }
32336
+ async function isOpenCodeInstalled() {
32337
+ const configPath = openCodeConfigPath();
32338
+ const configDir = path8.dirname(configPath);
32339
+ const home = homedir6();
32340
+ const candidates = [
32341
+ configDir,
32342
+ configPath,
32343
+ path8.join(home, ".bun", "bin", "opencode"),
32344
+ path8.join(home, ".local", "bin", "opencode")
32345
+ ];
32346
+ return anyPathExists(candidates);
32347
+ }
31963
32348
  async function isClaudeInstalled() {
31964
32349
  const home = homedir6();
31965
32350
  const candidates = [path8.join(home, ".claude"), path8.join(home, ".config", "claude")];
@@ -32031,10 +32416,12 @@ async function isAntigravityInstalled() {
32031
32416
  const localApp = process.env.LOCALAPPDATA;
32032
32417
  const programFiles = process.env.ProgramFiles;
32033
32418
  const programFilesX86 = process.env["ProgramFiles(x86)"];
32034
- if (localApp) candidates.push(path8.join(localApp, "Programs", "Antigravity", "Antigravity.exe"));
32419
+ if (localApp)
32420
+ candidates.push(path8.join(localApp, "Programs", "Antigravity", "Antigravity.exe"));
32035
32421
  if (localApp) candidates.push(path8.join(localApp, "Antigravity", "Antigravity.exe"));
32036
32422
  if (programFiles) candidates.push(path8.join(programFiles, "Antigravity", "Antigravity.exe"));
32037
- if (programFilesX86) candidates.push(path8.join(programFilesX86, "Antigravity", "Antigravity.exe"));
32423
+ if (programFilesX86)
32424
+ candidates.push(path8.join(programFilesX86, "Antigravity", "Antigravity.exe"));
32038
32425
  } else {
32039
32426
  candidates.push("/usr/bin/antigravity");
32040
32427
  candidates.push("/usr/local/bin/antigravity");
@@ -32047,6 +32434,8 @@ async function isEditorInstalled(editor) {
32047
32434
  switch (editor) {
32048
32435
  case "codex":
32049
32436
  return isCodexInstalled();
32437
+ case "opencode":
32438
+ return isOpenCodeInstalled();
32050
32439
  case "claude":
32051
32440
  return isClaudeInstalled();
32052
32441
  case "cursor":
@@ -32066,6 +32455,8 @@ async function isEditorInstalled(editor) {
32066
32455
  }
32067
32456
  }
32068
32457
  var IS_WINDOWS = process.platform === "win32";
32458
+ var DEFAULT_CONTEXTSTREAM_API_URL = "https://api.contextstream.io";
32459
+ var OPENCODE_CONFIG_SCHEMA_URL = "https://opencode.ai/config.json";
32069
32460
  function escapeTomlString(value) {
32070
32461
  return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
32071
32462
  }
@@ -32112,6 +32503,26 @@ function buildContextStreamVsCodeServer(params) {
32112
32503
  env
32113
32504
  };
32114
32505
  }
32506
+ function buildContextStreamOpenCodeEnvironment(params) {
32507
+ const environment = {
32508
+ CONTEXTSTREAM_API_KEY: "{env:CONTEXTSTREAM_API_KEY}"
32509
+ };
32510
+ if (normalizeApiUrl(params.apiUrl) !== DEFAULT_CONTEXTSTREAM_API_URL) {
32511
+ environment.CONTEXTSTREAM_API_URL = params.apiUrl;
32512
+ }
32513
+ if (params.contextPackEnabled === false) {
32514
+ environment.CONTEXTSTREAM_CONTEXT_PACK = "false";
32515
+ }
32516
+ return environment;
32517
+ }
32518
+ function buildContextStreamOpenCodeLocalServer(params) {
32519
+ return {
32520
+ type: "local",
32521
+ command: ["npx", "-y", "contextstream-mcp"],
32522
+ environment: buildContextStreamOpenCodeEnvironment(params),
32523
+ enabled: true
32524
+ };
32525
+ }
32115
32526
  function stripJsonComments(input) {
32116
32527
  return input.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1");
32117
32528
  }
@@ -32170,6 +32581,32 @@ async function upsertJsonVsCodeMcpConfig(filePath, server) {
32170
32581
  if (!exists) return "created";
32171
32582
  return before === after ? "skipped" : "updated";
32172
32583
  }
32584
+ async function upsertOpenCodeMcpConfig(filePath, server) {
32585
+ await fs7.mkdir(path8.dirname(filePath), { recursive: true });
32586
+ const exists = await fileExists(filePath);
32587
+ let root = {};
32588
+ if (exists) {
32589
+ const raw = await fs7.readFile(filePath, "utf8").catch(() => "");
32590
+ const parsed = tryParseJsonLike(raw);
32591
+ if (!parsed.ok) throw new Error(`Invalid JSON in ${filePath}: ${parsed.error}`);
32592
+ root = parsed.value;
32593
+ }
32594
+ if (!root || typeof root !== "object" || Array.isArray(root)) root = {};
32595
+ if (!root.mcp || typeof root.mcp !== "object" || Array.isArray(root.mcp)) root.mcp = {};
32596
+ const before = JSON.stringify({
32597
+ schema: root.$schema ?? null,
32598
+ contextstream: root.mcp.contextstream ?? null
32599
+ });
32600
+ root.$schema = OPENCODE_CONFIG_SCHEMA_URL;
32601
+ root.mcp.contextstream = server;
32602
+ const after = JSON.stringify({
32603
+ schema: root.$schema ?? null,
32604
+ contextstream: root.mcp.contextstream ?? null
32605
+ });
32606
+ await fs7.writeFile(filePath, JSON.stringify(root, null, 2) + "\n", "utf8");
32607
+ if (!exists) return "created";
32608
+ return before === after ? "skipped" : "updated";
32609
+ }
32173
32610
  function claudeDesktopConfigPath() {
32174
32611
  const home = homedir6();
32175
32612
  if (process.platform === "darwin") {
@@ -32374,9 +32811,7 @@ async function selectProjectForCurrentDirectory(client, cwd, workspaceId, dryRun
32374
32811
  }
32375
32812
  console.log("\nProject selection (current directory):");
32376
32813
  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
- );
32814
+ const choiceRaw = normalizeInput(await rl.question(`Choose [1-${options.length}] (default 1): `));
32380
32815
  const choiceNum = Number.parseInt(choiceRaw || "1", 10);
32381
32816
  const selected = Number.isFinite(choiceNum) ? options[choiceNum - 1] : options[0];
32382
32817
  if (!selected || selected.kind === "skip") {
@@ -32442,7 +32877,9 @@ ${colors.bright}Updating index for '${projectName}'...${colors.reset}`);
32442
32877
  }
32443
32878
  }
32444
32879
  if (!projectId) {
32445
- console.log(`${colors.yellow}! Could not resolve project ID for ${projectName}${colors.reset}`);
32880
+ console.log(
32881
+ `${colors.yellow}! Could not resolve project ID for ${projectName}${colors.reset}`
32882
+ );
32446
32883
  return;
32447
32884
  }
32448
32885
  } catch (err) {
@@ -32460,7 +32897,9 @@ ${colors.bright}Updating index for '${projectName}'...${colors.reset}`);
32460
32897
  console.log(`${colors.dim}No indexable files found${colors.reset}`);
32461
32898
  return;
32462
32899
  } else {
32463
- console.log(`${colors.dim}Found ${totalFiles.toLocaleString()} files for indexing${colors.reset}`);
32900
+ console.log(
32901
+ `${colors.dim}Found ${totalFiles.toLocaleString()} files for indexing${colors.reset}`
32902
+ );
32464
32903
  }
32465
32904
  } catch {
32466
32905
  console.log(`${colors.dim}Scanning files...${colors.reset}`);
@@ -32480,7 +32919,9 @@ ${colors.bright}Updating index for '${projectName}'...${colors.reset}`);
32480
32919
  const percentage = (progress * 100).toFixed(1);
32481
32920
  const speed = filesPerSec.toFixed(1);
32482
32921
  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} `);
32922
+ process.stdout.write(
32923
+ `\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} `
32924
+ );
32484
32925
  };
32485
32926
  const progressInterval = setInterval(updateProgress, 80);
32486
32927
  const ingestWithRetry = async (batch, maxRetries = 3) => {
@@ -32798,7 +33239,7 @@ Code: ${device.user_code}`);
32798
33239
  );
32799
33240
  }
32800
33241
  }
32801
- const NO_HOOKS_EDITORS2 = ["codex", "aider", "antigravity"];
33242
+ const NO_HOOKS_EDITORS2 = ["codex", "opencode", "aider", "antigravity"];
32802
33243
  const getModeForEditor = (editor) => NO_HOOKS_EDITORS2.includes(editor) ? "full" : "bootstrap";
32803
33244
  const detectedPlanName = await client.getPlanName();
32804
33245
  const detectedGraphTier = await client.getGraphTier();
@@ -32809,7 +33250,9 @@ Detected plan: ${planLabel} (graph: ${graphTierLabel})`);
32809
33250
  const contextPackEnabled = !!detectedPlanName && ["pro", "team", "enterprise"].some((p) => detectedPlanName.toLowerCase().includes(p));
32810
33251
  console.log("\nAuto-Update:");
32811
33252
  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.");
33253
+ console.log(
33254
+ " on new sessions (checks daily). You can disable this if you prefer manual updates."
33255
+ );
32813
33256
  const currentAutoUpdate = isAutoUpdateEnabled();
32814
33257
  const autoUpdateChoice = normalizeInput(
32815
33258
  await rl.question(`Enable auto-update? [${currentAutoUpdate ? "Y/n" : "y/N"}]: `)
@@ -32823,6 +33266,7 @@ Detected plan: ${planLabel} (graph: ${graphTierLabel})`);
32823
33266
  }
32824
33267
  const editors = [
32825
33268
  "codex",
33269
+ "opencode",
32826
33270
  "claude",
32827
33271
  "cursor",
32828
33272
  "cline",
@@ -32896,7 +33340,20 @@ Detected plan: ${planLabel} (graph: ${graphTierLabel})`);
32896
33340
  const mcpScope = mcpChoice === "2" && hasCodex && !hasProjectMcpEditors ? "skip" : mcpChoice === "4" ? "skip" : mcpChoice === "1" ? "global" : mcpChoice === "2" ? "project" : "both";
32897
33341
  const mcpServer = buildContextStreamMcpServer({ apiUrl, apiKey, contextPackEnabled });
32898
33342
  const mcpServerClaude = buildContextStreamMcpServer({ apiUrl, apiKey, contextPackEnabled });
33343
+ const mcpServerOpenCode = buildContextStreamOpenCodeLocalServer({
33344
+ apiUrl,
33345
+ apiKey,
33346
+ contextPackEnabled
33347
+ });
32899
33348
  const vsCodeServer = buildContextStreamVsCodeServer({ apiUrl, apiKey, contextPackEnabled });
33349
+ let hasPrintedOpenCodeEnvNote = false;
33350
+ const printOpenCodeEnvNote = () => {
33351
+ if (hasPrintedOpenCodeEnvNote) return;
33352
+ hasPrintedOpenCodeEnvNote = true;
33353
+ console.log(
33354
+ " OpenCode reads CONTEXTSTREAM_API_KEY from your environment. Export it before launching OpenCode."
33355
+ );
33356
+ };
32900
33357
  const needsGlobalMcpConfig = mcpScope === "global" || mcpScope === "both" || mcpScope === "project" && hasCodex;
32901
33358
  if (needsGlobalMcpConfig) {
32902
33359
  console.log("\nInstalling global MCP config...");
@@ -32948,6 +33405,20 @@ Detected plan: ${planLabel} (graph: ${graphTierLabel})`);
32948
33405
  );
32949
33406
  continue;
32950
33407
  }
33408
+ if (editor === "opencode") {
33409
+ const filePath = openCodeConfigPath();
33410
+ if (dryRun) {
33411
+ writeActions.push({ kind: "mcp-config", target: filePath, status: "dry-run" });
33412
+ console.log(`- ${EDITOR_LABELS[editor]}: would update ${filePath}`);
33413
+ printOpenCodeEnvNote();
33414
+ continue;
33415
+ }
33416
+ const status = await upsertOpenCodeMcpConfig(filePath, mcpServerOpenCode);
33417
+ writeActions.push({ kind: "mcp-config", target: filePath, status });
33418
+ console.log(`- ${EDITOR_LABELS[editor]}: ${status} ${filePath}`);
33419
+ printOpenCodeEnvNote();
33420
+ continue;
33421
+ }
32951
33422
  if (editor === "cursor") {
32952
33423
  const filePath = path8.join(homedir6(), ".cursor", "mcp.json");
32953
33424
  if (dryRun) {
@@ -32990,6 +33461,8 @@ Detected plan: ${planLabel} (graph: ${graphTierLabel})`);
32990
33461
  kilo: "kilo",
32991
33462
  codex: null,
32992
33463
  // No hooks API
33464
+ opencode: null,
33465
+ // No hooks API
32993
33466
  aider: null,
32994
33467
  // No hooks API
32995
33468
  antigravity: null
@@ -33030,6 +33503,12 @@ Detected plan: ${planLabel} (graph: ${graphTierLabel})`);
33030
33503
  if (scope === "global" || scope === "both") {
33031
33504
  console.log("\nInstalling global rules...");
33032
33505
  for (const editor of configuredEditors) {
33506
+ if (editor === "opencode") {
33507
+ console.log(
33508
+ `- ${EDITOR_LABELS[editor]}: rules are not auto-generated yet (MCP config only).`
33509
+ );
33510
+ continue;
33511
+ }
33033
33512
  const filePath = globalRulesPathForEditor(editor);
33034
33513
  if (!filePath) {
33035
33514
  console.log(
@@ -33170,6 +33649,21 @@ Applying to ${projects.length} project(s)...`);
33170
33649
  }
33171
33650
  continue;
33172
33651
  }
33652
+ if (editor === "opencode") {
33653
+ const openCodePath = path8.join(projectPath, "opencode.json");
33654
+ if (dryRun) {
33655
+ writeActions.push({
33656
+ kind: "mcp-config",
33657
+ target: openCodePath,
33658
+ status: "dry-run"
33659
+ });
33660
+ } else {
33661
+ const status = await upsertOpenCodeMcpConfig(openCodePath, mcpServerOpenCode);
33662
+ writeActions.push({ kind: "mcp-config", target: openCodePath, status });
33663
+ }
33664
+ printOpenCodeEnvNote();
33665
+ continue;
33666
+ }
33173
33667
  if (editor === "kilo") {
33174
33668
  const kiloPath = path8.join(projectPath, ".kilocode", "mcp.json");
33175
33669
  if (dryRun) {
@@ -33201,6 +33695,7 @@ Applying to ${projects.length} project(s)...`);
33201
33695
  for (const editor of selectedEditors) {
33202
33696
  if (scope !== "project" && scope !== "both") continue;
33203
33697
  if (!configuredEditors.includes(editor)) continue;
33698
+ if (editor === "opencode") continue;
33204
33699
  const rule = generateRuleContent(editor, {
33205
33700
  workspaceName,
33206
33701
  workspaceId: workspaceId && workspaceId !== "dry-run" ? workspaceId : void 0,
@@ -33240,11 +33735,15 @@ Applying to ${projects.length} project(s)...`);
33240
33735
  console.log("PROJECT INDEXING");
33241
33736
  console.log("\u2500".repeat(60));
33242
33737
  if (filesIndexed === 0) {
33243
- console.log("Indexing enables semantic code search and AI-powered graph knowledge for rich AI context.");
33738
+ console.log(
33739
+ "Indexing enables semantic code search and AI-powered graph knowledge for rich AI context."
33740
+ );
33244
33741
  } else {
33245
33742
  console.log("Your project index is stale and could use a refresh.");
33246
33743
  }
33247
- console.log("Powered by our blazing-fast Rust engine, indexing typically takes under a minute,");
33744
+ console.log(
33745
+ "Powered by our blazing-fast Rust engine, indexing typically takes under a minute,"
33746
+ );
33248
33747
  console.log("though larger projects may take a bit longer.\n");
33249
33748
  console.log("Your code is private and securely stored.\n");
33250
33749
  const indexChoice = normalizeInput(
@@ -33259,7 +33758,9 @@ Applying to ${projects.length} project(s)...`);
33259
33758
  if (indexingEnabled) {
33260
33759
  await indexProjectWithProgress(client, process.cwd(), cwdConfig.workspace_id);
33261
33760
  } else {
33262
- console.log("\nIndexing skipped for now. You can start it later with: contextstream-mcp index <path>");
33761
+ console.log(
33762
+ "\nIndexing skipped for now. You can start it later with: contextstream-mcp index <path>"
33763
+ );
33263
33764
  }
33264
33765
  }
33265
33766
  } catch {
@@ -33269,8 +33770,12 @@ Applying to ${projects.length} project(s)...`);
33269
33770
  console.log("\n" + "\u2500".repeat(60));
33270
33771
  console.log("PROJECT INDEXING");
33271
33772
  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,");
33773
+ console.log(
33774
+ "Indexing enables semantic code search and AI-powered graph knowledge for rich AI context."
33775
+ );
33776
+ console.log(
33777
+ "Powered by our blazing-fast Rust engine, indexing typically takes under a minute,"
33778
+ );
33274
33779
  console.log("though larger projects may take a bit longer.\n");
33275
33780
  console.log("Your code is private and securely stored.\n");
33276
33781
  const indexChoice = normalizeInput(
@@ -33292,7 +33797,9 @@ Applying to ${projects.length} project(s)...`);
33292
33797
  await indexProjectWithProgress(client, projectPath, workspaceId);
33293
33798
  }
33294
33799
  } else {
33295
- console.log("\nIndexing skipped for now. You can start it later with: contextstream-mcp index <path>");
33800
+ console.log(
33801
+ "\nIndexing skipped for now. You can start it later with: contextstream-mcp index <path>"
33802
+ );
33296
33803
  }
33297
33804
  }
33298
33805
  console.log("\nDone.");
@@ -33313,7 +33820,9 @@ Applying to ${projects.length} project(s)...`);
33313
33820
  "- For UI-based MCP setup (Cline/Kilo/Roo global), see https://contextstream.io/docs/mcp"
33314
33821
  );
33315
33822
  console.log("");
33316
- console.log("You're all set! ContextStream gives your AI persistent memory, semantic code search, and cross-session context.");
33823
+ console.log(
33824
+ "You're all set! ContextStream gives your AI persistent memory, semantic code search, and cross-session context."
33825
+ );
33317
33826
  console.log("More at: https://contextstream.io/docs/mcp");
33318
33827
  } finally {
33319
33828
  rl.close();