negotium 0.2.17 → 0.2.18

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.
Files changed (34) hide show
  1. package/dist/agent-helpers.js +41 -32
  2. package/dist/agent-helpers.js.map +9 -9
  3. package/dist/{chunk-5cpdbbeh.js → chunk-fc58cqry.js} +3 -1
  4. package/dist/{chunk-5cpdbbeh.js.map → chunk-fc58cqry.js.map} +3 -3
  5. package/dist/hosted-agent.js +4 -2
  6. package/dist/hosted-agent.js.map +4 -4
  7. package/dist/main.js +662 -93
  8. package/dist/main.js.map +11 -11
  9. package/dist/mcp-factories.js +659 -91
  10. package/dist/mcp-factories.js.map +10 -10
  11. package/dist/prompts.js +8 -8
  12. package/dist/prompts.js.map +3 -3
  13. package/dist/registry.js +3 -3
  14. package/dist/registry.js.map +2 -2
  15. package/dist/rollout.js +1 -1
  16. package/dist/runtime/src/agents/idle-archiver.ts +3 -2
  17. package/dist/runtime/src/mcp/canonical-proxy-server.ts +18 -1
  18. package/dist/runtime/src/mcp/wiki-server.ts +801 -59
  19. package/dist/runtime/src/platform/mcp-config.ts +1 -0
  20. package/dist/runtime/src/prompts/agents/wiki-archiver.md +16 -6
  21. package/dist/runtime/src/prompts/builders.ts +17 -34
  22. package/dist/runtime/src/runtime/turn-runner.ts +14 -17
  23. package/dist/runtime/src/storage/api-topics.ts +13 -4
  24. package/dist/runtime/src/topics/lifecycle.ts +3 -2
  25. package/dist/runtime/src/types/api.ts +6 -0
  26. package/dist/runtime/src/version.ts +1 -1
  27. package/dist/storage.js +12 -6
  28. package/dist/storage.js.map +3 -3
  29. package/dist/types/packages/core/src/mcp/wiki-server.d.ts +4 -0
  30. package/dist/types/packages/core/src/prompts/builders.d.ts +2 -4
  31. package/dist/types/packages/core/src/storage/api-topics.d.ts +1 -0
  32. package/dist/types/packages/core/src/types/api.d.ts +6 -0
  33. package/dist/types/packages/core/src/version.d.ts +1 -1
  34. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -1636,7 +1636,7 @@ var exports_version = {};
1636
1636
  __export(exports_version, {
1637
1637
  NEGOTIUM_VERSION: () => NEGOTIUM_VERSION
1638
1638
  });
1639
- var NEGOTIUM_VERSION = "0.2.17";
1639
+ var NEGOTIUM_VERSION = "0.2.18";
1640
1640
 
1641
1641
  // ../../packages/core/src/agents/codex-native-multi-agent.ts
1642
1642
  import { spawn } from "child_process";
@@ -2943,6 +2943,8 @@ var init_mcp_config = __esm(() => {
2943
2943
  return env ? buildStdioMcpServer(agent, CANONICAL_MCP_PROXY_SERVER, ["--surface=wiki"], env) : null;
2944
2944
  }
2945
2945
  const args = [`--user-id=${userId}`];
2946
+ if (topicId)
2947
+ args.push(`--current-topic-id=${topicId}`);
2946
2948
  const resolvedWikiTopicId = wikiTopicId ?? topicId ?? (session !== "dm" ? session : undefined);
2947
2949
  if (resolvedWikiTopicId)
2948
2950
  args.push(`--topic-id=${resolvedWikiTopicId}`);
@@ -7632,6 +7634,7 @@ function initializeApiTopicsSchema() {
7632
7634
  last_message_at TEXT,
7633
7635
  parent_topic_id TEXT,
7634
7636
  memory_topic_id TEXT,
7637
+ memory_key TEXT,
7635
7638
  is_fork INTEGER NOT NULL DEFAULT 0 CHECK (is_fork IN (0,1)),
7636
7639
  is_subagent INTEGER NOT NULL DEFAULT 0 CHECK (is_subagent IN (0,1)),
7637
7640
  visibility TEXT NOT NULL DEFAULT 'visible' CHECK (visibility IN ('visible','hidden')),
@@ -7681,8 +7684,8 @@ function initializeApiTopicsSchema() {
7681
7684
  const legacyBaseEffort = row.base_effort ?? row.default_effort;
7682
7685
  db.query(`INSERT INTO api_topics_next
7683
7686
  (id,title,kind,description,agent,base_model,base_effort,response_policy,
7684
- created_at,last_message_at,parent_topic_id,memory_topic_id,is_fork,is_subagent,visibility,access_mode,session_id)
7685
- VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(String(row.id), String(row.title), normalized.kind, typeof row.description === "string" ? row.description : null, normalized.agent ?? null, typeof legacyBaseModel === "string" ? legacyBaseModel : null, typeof legacyBaseEffort === "string" ? legacyBaseEffort : null, normalized.aiMode, String(row.created_at), typeof row.last_message_at === "string" ? row.last_message_at : null, typeof row.parent_topic_id === "string" ? row.parent_topic_id : null, typeof row.memory_topic_id === "string" ? row.memory_topic_id : null, Number(row.is_fork ?? 0) !== 0 ? 1 : 0, Number(row.is_subagent ?? 0) !== 0 ? 1 : 0, row.visibility === "hidden" ? "hidden" : "visible", row.access_mode === "shared" ? "shared" : "private", typeof row.session_id === "string" ? row.session_id : null);
7687
+ created_at,last_message_at,parent_topic_id,memory_topic_id,memory_key,is_fork,is_subagent,visibility,access_mode,session_id)
7688
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(String(row.id), String(row.title), normalized.kind, typeof row.description === "string" ? row.description : null, normalized.agent ?? null, typeof legacyBaseModel === "string" ? legacyBaseModel : null, typeof legacyBaseEffort === "string" ? legacyBaseEffort : null, normalized.aiMode, String(row.created_at), typeof row.last_message_at === "string" ? row.last_message_at : null, typeof row.parent_topic_id === "string" ? row.parent_topic_id : null, typeof row.memory_topic_id === "string" ? row.memory_topic_id : null, typeof row.memory_key === "string" ? row.memory_key : null, Number(row.is_fork ?? 0) !== 0 ? 1 : 0, Number(row.is_subagent ?? 0) !== 0 ? 1 : 0, row.visibility === "hidden" ? "hidden" : "visible", row.access_mode === "shared" ? "shared" : "private", typeof row.session_id === "string" ? row.session_id : null);
7686
7689
  }
7687
7690
  db.exec("DROP TABLE IF EXISTS topic_members");
7688
7691
  db.exec("DROP TABLE api_topics");
@@ -7751,6 +7754,9 @@ function initializeApiTopicsSchema() {
7751
7754
  if (!tableColumns2("api_topics").has("memory_topic_id")) {
7752
7755
  db.exec("ALTER TABLE api_topics ADD COLUMN memory_topic_id TEXT");
7753
7756
  }
7757
+ if (!tableColumns2("api_topics").has("memory_key")) {
7758
+ db.exec("ALTER TABLE api_topics ADD COLUMN memory_key TEXT");
7759
+ }
7754
7760
  db.exec(`
7755
7761
  UPDATE api_topics
7756
7762
  SET browser_profile_owner = (
@@ -7811,6 +7817,7 @@ function rowToDto2(r, participants = getTopicParticipants(r.id), tellTargets) {
7811
7817
  lastMessageAt: r.last_message_at ?? new Date().toISOString(),
7812
7818
  parentTopicId: r.parent_topic_id ?? undefined,
7813
7819
  memoryTopicId: r.memory_topic_id ?? undefined,
7820
+ memoryKey: r.memory_key ?? undefined,
7814
7821
  isFork: r.is_fork !== 0,
7815
7822
  ...r.is_subagent !== 0 ? { isSubagent: true } : {},
7816
7823
  ...r.is_subagent !== 0 ? {
@@ -7905,9 +7912,9 @@ function upsertTopic(t) {
7905
7912
  db.transaction(() => {
7906
7913
  db.query(`INSERT INTO api_topics
7907
7914
  (id,title,kind,description,agent,base_model,base_effort,response_policy,
7908
- created_at,last_message_at,parent_topic_id,memory_topic_id,is_fork,is_subagent,visibility,access_mode,
7915
+ created_at,last_message_at,parent_topic_id,memory_topic_id,memory_key,is_fork,is_subagent,visibility,access_mode,
7909
7916
  subagent_report_mode)
7910
- VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
7917
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
7911
7918
  ON CONFLICT(id) DO UPDATE SET
7912
7919
  title = excluded.title,
7913
7920
  kind = excluded.kind,
@@ -7920,11 +7927,12 @@ function upsertTopic(t) {
7920
7927
  last_message_at = excluded.last_message_at,
7921
7928
  parent_topic_id = excluded.parent_topic_id,
7922
7929
  memory_topic_id = excluded.memory_topic_id,
7930
+ memory_key = excluded.memory_key,
7923
7931
  is_fork = excluded.is_fork,
7924
7932
  is_subagent = excluded.is_subagent,
7925
7933
  visibility = excluded.visibility,
7926
7934
  access_mode = excluded.access_mode,
7927
- subagent_report_mode = excluded.subagent_report_mode`).run(t.id, t.title, normalized.kind, t.description ?? null, normalized.agent ?? null, t.defaultModel ?? null, t.defaultEffort ?? null, normalized.aiMode, t.createdAt, t.lastMessageAt ?? null, t.parentTopicId ?? null, t.memoryTopicId ?? null, t.isFork ? 1 : 0, t.isSubagent ? 1 : 0, normalizeTopicVisibility(t.visibility), normalizeTopicAccessMode(t.accessMode), t.subagentReportMode ?? "auto");
7935
+ subagent_report_mode = excluded.subagent_report_mode`).run(t.id, t.title, normalized.kind, t.description ?? null, normalized.agent ?? null, t.defaultModel ?? null, t.defaultEffort ?? null, normalized.aiMode, t.createdAt, t.lastMessageAt ?? null, t.parentTopicId ?? null, t.memoryTopicId ?? null, t.memoryKey ?? null, t.isFork ? 1 : 0, t.isSubagent ? 1 : 0, normalizeTopicVisibility(t.visibility), normalizeTopicAccessMode(t.accessMode), t.subagentReportMode ?? "auto");
7928
7936
  db.query("DELETE FROM topic_members WHERE topic_id = ?").run(t.id);
7929
7937
  for (const participant of t.participants) {
7930
7938
  db.query("INSERT INTO topic_members (topic_id,user_id,role) VALUES (?,?,?)").run(t.id, participant.userId, participant.role);
@@ -11708,14 +11716,14 @@ function buildMemoryPromptSection(opts) {
11708
11716
  const parts = [`
11709
11717
 
11710
11718
  ## Memory`];
11711
- parts.push(`Memory directory: ${opts.wikiDir}/topic`);
11712
- parts.push(`Files: ${opts.hasFiles ? opts.briefFile : "(none)"}`);
11713
- if (opts.latestSummaryFile) {
11714
- parts.push(`Latest summary: ${opts.latestSummaryFile}`);
11715
- }
11716
- parts.push("", opts.isManager ? opts.hasFiles ? "Above is this workspace's memory-hub brief (accumulated across all archived topics). For past context or other topics, consult it first, then use the `wiki_query` MCP tool for deeper recall." : "If you need past context while handling delegated work, use the `wiki_query` MCP tool." : opts.hasFiles ? "The file above is this topic's wiki brief. Read it first for past context; use the `wiki_query` MCP tool for deeper recall." : opts.latestSummaryFile ? "There is no wiki brief file yet. For past context, read the Latest summary first; use the `wiki_query` MCP tool for deeper recall." : "There is no wiki brief file yet. Use the `wiki_query` MCP tool for past context.");
11717
- parts.push("", opts.isManager ? "When answering about past workspace decisions or other topics, weave in memory naturally, but confirm with `wiki_query` when unsure." : opts.hasFiles || opts.latestSummaryFile ? "On your first reply in the topic, read the wiki brief" + (opts.latestSummaryFile ? " and Latest summary" : "") + " and mention a one-line context recap so the user can quickly confirm it is on track." : "On your first reply in the topic, check prior conversation context if needed and mention it in one natural line.");
11718
- parts.push("", opts.hasFiles ? "If the user says your memory is wrong, find the file in the Memory directory with Read and fix it directly with Edit." : "If the user says your memory is wrong, check the relevant wiki brief and fix it directly when possible, or use `wiki_query` to find the entry and reconcile.");
11719
+ if (opts.isManager) {
11720
+ parts.push("Use `wiki_query` for past workspace decisions or cross-topic context, then `wiki_read` only for the relevant result.");
11721
+ } else if (opts.memoryKey) {
11722
+ parts.push(`This topic uses the canonical memory key \`${opts.memoryKey}\`. At the start of a new session, read it with \`wiki_read(kind: "topic", key: "${opts.memoryKey}")\`.`);
11723
+ } else {
11724
+ parts.push(`At the start of a new session, search topic memories with \`wiki_query(question: "${opts.topicTitle}", kind: "topic", limit: 5)\` and read the most relevant candidate with \`wiki_read\`.`, "If a candidate is clearly the same continuing topic/persona, read it with `adopt: true`; otherwise keep this topic's own name as its memory key. Do not merge weak or ambiguous matches.");
11725
+ }
11726
+ parts.push("Keep the lookup quiet. Mention prior context naturally in one short line only when it helps the user.", 'Use `wiki_query(kind: "article")` for reusable knowledge and `wiki_query(kind: "summary")` for historical session details.');
11719
11727
  if (opts.hasArchive) {
11720
11728
  parts.push("", "If you need the actual conversation from an earlier session, use the `wiki_last_conversation` MCP tool.");
11721
11729
  }
@@ -12802,11 +12810,12 @@ function archiveActiveTopicForMemory(topicId, userId, options) {
12802
12810
  }, "topic-memory-archiver: raw snapshot preserved below exchange threshold");
12803
12811
  return "below-threshold";
12804
12812
  }
12805
- const memoryTopic = getTopicMemoryOrigin(topicId) ?? topic;
12813
+ const memoryTopic = topic.memoryKey ? topic : getTopicMemoryOrigin(topicId) ?? topic;
12814
+ const memoryTitle = memoryTopic.memoryKey?.trim() || memoryTopic.title;
12806
12815
  const launched = (options.launchArchiver ?? runArchiverTurn)({
12807
12816
  userId,
12808
12817
  topicId: memoryTopic.id,
12809
- topicTitle: memoryTopic.title,
12818
+ topicTitle: memoryTitle,
12810
12819
  archivePath: job.archivePath,
12811
12820
  ...options.rawArchivePaths ? { rawArchivePaths: options.rawArchivePaths } : {},
12812
12821
  messageCount: job.messageCount,
@@ -16182,11 +16191,12 @@ async function deleteTopicCascadeImpl(topic, userId, options, deletingAncestorId
16182
16191
  }
16183
16192
  if (archived && meetsMemoryArchiveExchangeThreshold(archived.exchangeCount, MIN_MEMORY_ARCHIVE_EXCHANGES, topic)) {
16184
16193
  try {
16185
- const memoryTopic = getTopicMemoryOrigin(topicId) ?? topic;
16194
+ const memoryTopic = topic.memoryKey ? topic : getTopicMemoryOrigin(topicId) ?? topic;
16195
+ const memoryTitle = memoryTopic.memoryKey?.trim() || memoryTopic.title;
16186
16196
  runArchiverTurn({
16187
16197
  userId,
16188
16198
  ...memoryTopic.id !== topicId && !deletingTopicIds.has(memoryTopic.id) ? { topicId: memoryTopic.id } : {},
16189
- topicTitle: memoryTopic.title,
16199
+ topicTitle: memoryTitle,
16190
16200
  archivePath: archived.path,
16191
16201
  ...rawArchives.length > 0 ? { rawArchivePaths: rawArchives } : {},
16192
16202
  messageCount: archived.messageCount
@@ -18780,25 +18790,24 @@ function startAiTurn(params) {
18780
18790
  const isManager = topicRecord?.kind === "manager";
18781
18791
  const isMentionOnlyChannel = topic.aiMode === "mention" && !isManager;
18782
18792
  let systemPrompt = isManager ? buildManagerSystemPrompt(systemPromptOpts) : isMentionOnlyChannel ? buildChannelSystemPrompt(systemPromptOpts) : buildTopicSystemPrompt(systemPromptOpts);
18783
- const memoryTopic = isMentionOnlyChannel || isManager ? topic : getTopicMemoryOrigin(topicId) ?? topic;
18793
+ const memoryTopic = isMentionOnlyChannel || isManager || topic.memoryKey ? topic : getTopicMemoryOrigin(topicId) ?? topic;
18794
+ const memoryTitle = memoryTopic.memoryKey?.trim() || memoryTopic.title;
18784
18795
  if (!isMentionOnlyChannel) {
18796
+ let hasMemoryArchive = false;
18785
18797
  try {
18786
- const resolvedBrief = resolveTopicBrief(memoryTopic.id, memoryTopic.title);
18798
+ const resolvedBrief = resolveTopicBrief(memoryTopic.id, memoryTitle);
18787
18799
  const wikiDir = getSharedWikiDir();
18788
- const { briefFile, hasBriefFile, latestSummaryFile } = resolveWikiMemoryMirror(wikiDir, memoryTopic.id, memoryTopic.title);
18789
- if (resolvedBrief || hasBriefFile || latestSummaryFile) {
18790
- systemPrompt += buildMemoryPromptSection({
18791
- briefFile,
18792
- wikiDir,
18793
- hasFiles: hasBriefFile,
18794
- latestSummaryFile,
18795
- hasArchive: Boolean(resolvedBrief?.brief.latestSummaryMd || latestSummaryFile),
18796
- isManager
18797
- });
18798
- }
18800
+ const { latestSummaryFile } = resolveWikiMemoryMirror(wikiDir, memoryTopic.id, memoryTitle);
18801
+ hasMemoryArchive = Boolean(resolvedBrief?.brief.latestSummaryMd || latestSummaryFile);
18799
18802
  } catch (err2) {
18800
18803
  logger.warn({ topicId, err: err2 }, "ai: failed to inject topic brief");
18801
18804
  }
18805
+ systemPrompt += buildMemoryPromptSection({
18806
+ topicTitle: topic.title,
18807
+ ...memoryTopic.memoryKey ? { memoryKey: memoryTopic.memoryKey } : {},
18808
+ hasArchive: hasMemoryArchive,
18809
+ isManager
18810
+ });
18802
18811
  }
18803
18812
  const activeVisual = visualTools ? getActiveVisualForPrompt(topicId, userId) : null;
18804
18813
  if (activeVisual) {
@@ -24931,7 +24940,7 @@ import {
24931
24940
  unlinkSync as unlinkSync18,
24932
24941
  writeFileSync as writeFileSync17
24933
24942
  } from "fs";
24934
- import { basename as basename7, dirname as dirname16, join as join36, resolve as resolve17 } from "path";
24943
+ import { basename as basename7, dirname as dirname16, join as join36, relative as relative2, resolve as resolve17 } from "path";
24935
24944
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
24936
24945
  import { StdioServerTransport as StdioServerTransport2 } from "@modelcontextprotocol/sdk/server/stdio.js";
24937
24946
  import {
@@ -25070,9 +25079,243 @@ function topicNameFrom(topicIdStr) {
25070
25079
  function slugify(topic) {
25071
25080
  return wikiSummarySlug(topic);
25072
25081
  }
25082
+ function normalizeSearchText(value) {
25083
+ return value.toLowerCase().replaceAll(/[^a-z0-9\uAC00-\uD7A3]+/g, " ").trim().replaceAll(/\s+/g, " ");
25084
+ }
25085
+ function compactSearchText(value) {
25086
+ return normalizeSearchText(value).replaceAll(" ", "");
25087
+ }
25088
+ function trigrams(value) {
25089
+ const compact = compactSearchText(value);
25090
+ if (compact.length < 3)
25091
+ return new Set(compact ? [compact] : []);
25092
+ const result = new Set;
25093
+ for (let i = 0;i <= compact.length - 3; i += 1)
25094
+ result.add(compact.slice(i, i + 3));
25095
+ return result;
25096
+ }
25097
+ function fuzzySearchScore(query2, candidate, maximum) {
25098
+ const compactQuery = compactSearchText(query2);
25099
+ const compactCandidate = compactSearchText(candidate);
25100
+ if (compactQuery.length < 3 || compactCandidate.length < 3)
25101
+ return 0;
25102
+ const similarity = trigramSimilarity(compactQuery, compactCandidate);
25103
+ const minimumSimilarity = compactQuery.length <= 4 ? 0.6 : 0.45;
25104
+ return similarity >= minimumSimilarity ? similarity * maximum : 0;
25105
+ }
25106
+ function meaningfulSearchTokens(value) {
25107
+ return normalizeSearchText(value).split(" ").filter((token) => (token.length >= 2 || /^\d+$/.test(token)) && !SEARCH_STOP_WORDS.has(token));
25108
+ }
25109
+ function canonicalSearchToken(token) {
25110
+ return token.length > 4 && token.endsWith("s") ? token.slice(0, -1) : token;
25111
+ }
25112
+ function canonicalSearchTokens(value) {
25113
+ return meaningfulSearchTokens(value).map(canonicalSearchToken);
25114
+ }
25115
+ function countToken(tokens, token) {
25116
+ let count = 0;
25117
+ for (const candidate of tokens)
25118
+ if (candidate === token)
25119
+ count += 1;
25120
+ return count;
25121
+ }
25122
+ function countPhrase(text2, phrase) {
25123
+ if (!phrase)
25124
+ return 0;
25125
+ let count = 0;
25126
+ let offset = 0;
25127
+ let match = text2.indexOf(phrase, offset);
25128
+ while (match >= 0) {
25129
+ count += 1;
25130
+ offset = match + phrase.length;
25131
+ match = text2.indexOf(phrase, offset);
25132
+ }
25133
+ return count;
25134
+ }
25135
+ function summaryTopicIdentity(key) {
25136
+ return normalizeSearchText(key.replace(/^\d{4}-\d{2}-\d{2}-/, ""));
25137
+ }
25138
+ function scoreSummaryTime(query2, key, title) {
25139
+ const date = key.match(/^(\d{4})-(\d{2})-(\d{2})/);
25140
+ if (!date)
25141
+ return 0;
25142
+ const tokens = meaningfulSearchTokens(query2);
25143
+ const documentDate = `${date[1]}-${date[2]}-${date[3]}`;
25144
+ const requestedDateMatch = query2.match(/\b(\d{4}) (\d{2}) (\d{2})\b/);
25145
+ const requestedDate = requestedDateMatch ? `${requestedDateMatch[1]}-${requestedDateMatch[2]}-${requestedDateMatch[3]}` : undefined;
25146
+ const beforeDate = /\bbefore\b/.test(query2);
25147
+ const afterDate = /\bafter\b/.test(query2);
25148
+ if (requestedDate && beforeDate && documentDate >= requestedDate)
25149
+ return null;
25150
+ if (requestedDate && afterDate && documentDate <= requestedDate)
25151
+ return null;
25152
+ const queryYear = tokens.find((token) => /^\d{4}(?:\uB144)?$/.test(token))?.replace(/\uB144$/, "");
25153
+ const queryMonth = tokens.map((token) => SEARCH_MONTHS.get(token)).find(Boolean);
25154
+ const hasRecencyTerm = tokens.some((token) => SEARCH_RECENCY_TERMS.has(token));
25155
+ const yearMatches = !queryYear || queryYear === date[1];
25156
+ const monthMatches = !queryMonth || queryMonth === date[2];
25157
+ if (!queryYear && !queryMonth && !hasRecencyTerm || !yearMatches || !monthMatches)
25158
+ return 0;
25159
+ const normalizedMetadata = `${normalizeSearchText(key)} ${normalizeSearchText(title)}`;
25160
+ const contentTokens = tokens.filter((token) => !/^\d{4}(?:\uB144)?$/.test(token) && !SEARCH_MONTHS.has(token) && !SEARCH_RECENCY_TERMS.has(token));
25161
+ const contentMatches = contentTokens.filter((token) => normalizedMetadata.includes(token)).length;
25162
+ const relationScore = requestedDate && (beforeDate || afterDate) ? 60 : 0;
25163
+ const exactDateScore = requestedDate === documentDate ? 90 : 0;
25164
+ return 30 + relationScore + exactDateScore + contentMatches * 15;
25165
+ }
25166
+ function trigramSimilarity(left, right) {
25167
+ const a = trigrams(left);
25168
+ const b = trigrams(right);
25169
+ if (a.size === 0 || b.size === 0)
25170
+ return 0;
25171
+ let overlap = 0;
25172
+ for (const gram of a)
25173
+ if (b.has(gram))
25174
+ overlap += 1;
25175
+ return 2 * overlap / (a.size + b.size);
25176
+ }
25177
+ function parseWikiIndex(path) {
25178
+ let text2;
25179
+ try {
25180
+ text2 = readFileSync22(path, "utf-8");
25181
+ } catch {
25182
+ return [];
25183
+ }
25184
+ const candidates = [];
25185
+ const entryPattern = /^\s*-\s*\[\[(topic|articles|summaries)\/([^\]]+)\]\]\s*(.*)$/;
25186
+ for (const line of text2.split(`
25187
+ `)) {
25188
+ const match = line.match(entryPattern);
25189
+ if (!match)
25190
+ continue;
25191
+ const namespace = match[1];
25192
+ const key = match[2].replace(/\.md$/i, "");
25193
+ const tail = match[3].trim();
25194
+ const dateMatch = tail.match(/\((\d{4}-\d{2}-\d{2})(?:[^)]*)\)\s*$/);
25195
+ const description = dateMatch ? tail.slice(0, dateMatch.index).trim() : tail;
25196
+ candidates.push({
25197
+ kind: namespace === "topic" ? "topic" : namespace === "articles" ? "article" : "summary",
25198
+ key,
25199
+ description,
25200
+ ...dateMatch ? { date: dateMatch[1] } : {}
25201
+ });
25202
+ }
25203
+ return candidates;
25204
+ }
25205
+ function indexCandidateExists(candidate) {
25206
+ if (candidate.kind === "topic")
25207
+ return true;
25208
+ const root = candidate.kind === "article" ? runtime().articlesDir : runtime().summariesDir;
25209
+ const filePath = resolve17(root, `${candidate.key}.md`);
25210
+ const relativePath = relative2(root, filePath);
25211
+ if (relativePath.startsWith("..") || relativePath.startsWith("/"))
25212
+ return false;
25213
+ try {
25214
+ return statSync15(filePath).isFile();
25215
+ } catch {
25216
+ return false;
25217
+ }
25218
+ }
25219
+ function scoreIndexKey(query2, key) {
25220
+ const normalizedQuery = normalizeSearchText(query2);
25221
+ const compactQuery = compactSearchText(normalizedQuery);
25222
+ const normalizedKey = normalizeSearchText(key);
25223
+ const compactKey = compactSearchText(normalizedKey);
25224
+ const tokens = meaningfulSearchTokens(normalizedQuery);
25225
+ let score = 0;
25226
+ if (normalizedKey === normalizedQuery)
25227
+ score += 120;
25228
+ else if (compactKey === compactQuery)
25229
+ score += 110;
25230
+ else {
25231
+ if (compactQuery.length >= 2 && (compactKey.startsWith(compactQuery) || compactQuery.startsWith(compactKey))) {
25232
+ score += 65;
25233
+ }
25234
+ if (tokens.length > 0 && compactQuery.length >= 3 && compactKey.includes(compactQuery)) {
25235
+ score += 45;
25236
+ }
25237
+ score += fuzzySearchScore(compactQuery, compactKey, 55);
25238
+ }
25239
+ for (const token of tokens) {
25240
+ if (normalizedKey.includes(token))
25241
+ score += 10;
25242
+ }
25243
+ return score;
25244
+ }
25245
+ function scoreIndexCandidate(query2, candidate) {
25246
+ const normalizedQuery = normalizeSearchText(query2);
25247
+ if (!normalizedQuery)
25248
+ return 0;
25249
+ const normalizedDescription = normalizeSearchText(candidate.description);
25250
+ const tokens = canonicalSearchTokens(normalizedQuery);
25251
+ const descriptionTokens = new Set(canonicalSearchTokens(normalizedDescription));
25252
+ let score = scoreIndexKey(normalizedQuery, candidate.key);
25253
+ if (tokens.length > 0 && normalizedDescription.includes(normalizedQuery))
25254
+ score += 30;
25255
+ let descriptionMatches = 0;
25256
+ for (const token of tokens) {
25257
+ if (descriptionTokens.has(token)) {
25258
+ score += 5;
25259
+ descriptionMatches += 1;
25260
+ }
25261
+ }
25262
+ if (tokens.length > 1 && descriptionMatches === tokens.length)
25263
+ score += 25;
25264
+ if (candidate.kind === "topic") {
25265
+ const requestsCurrent = /\b(?:current|active|ongoing|open)\b|\uD604\uC7AC|\uC9C4\uD589 \uC911/.test(normalizedQuery);
25266
+ const archivedCandidate = /\b(?:archive|archived|historical|closed)\b|\uBCF4\uAD00|\uC885\uB8CC/.test(`${normalizeSearchText(candidate.key)} ${normalizedDescription}`);
25267
+ if (requestsCurrent && archivedCandidate)
25268
+ score -= 40;
25269
+ }
25270
+ return score;
25271
+ }
25073
25272
  function wikiQuery(args) {
25074
- const question = safeExt(args, ["question", "query", "q", "text"], "");
25075
- const query2 = question.toLowerCase();
25273
+ const question = String(safeExt(args, ["question", "query", "q", "text"], "")).trim();
25274
+ const requestedKind = String(safeExt(args, ["kind", "type"], "all"));
25275
+ const kind = requestedKind === "topic" || requestedKind === "article" || requestedKind === "summary" ? requestedKind : "all";
25276
+ const requestedLimit = Number(safeExt(args, ["limit", "maxResults"], 8));
25277
+ const limit = Math.min(Math.max(Number.isFinite(requestedLimit) ? requestedLimit : 8, 1), 20);
25278
+ if (!question) {
25279
+ return {
25280
+ content: [{ type: "text", text: "A non-empty wiki query is required." }],
25281
+ isError: true
25282
+ };
25283
+ }
25284
+ const normalizedQuery = normalizeSearchText(question);
25285
+ if (!normalizedQuery) {
25286
+ return { content: [{ type: "text", text: "No matching wiki articles found." }] };
25287
+ }
25288
+ const canReadTopicMemory = runtime().host.canReadTopicMemory;
25289
+ const temporalDirection = /\b(?:latest|newest|recent)\b|\uCD5C\uADFC|\uCD5C\uC2E0/.test(normalizedQuery) ? "newest" : /\b(?:earliest|oldest)\b|\uAC00\uC7A5 \uC624\uB798\uB41C|\uCD5C\uCD08/.test(normalizedQuery) ? "oldest" : undefined;
25290
+ const visibleIndexCandidates = [
25291
+ ...parseWikiIndex(resolve17(runtime().wikiDir, "topic-index.md")),
25292
+ ...parseWikiIndex(resolve17(runtime().wikiDir, "article-index.md"))
25293
+ ].filter((candidate) => kind === "all" || candidate.kind === kind).filter((candidate) => candidate.kind !== "topic" || !canReadTopicMemory || canReadTopicMemory(candidate.key, runtime().userId));
25294
+ const hasStaleExactIndexMatch = visibleIndexCandidates.some((candidate) => !indexCandidateExists(candidate) && scoreIndexKey(question, candidate.key) >= 100);
25295
+ const indexCandidates = visibleIndexCandidates.filter(indexCandidateExists).map((candidate) => ({
25296
+ candidate,
25297
+ keyScore: scoreIndexKey(question, candidate.key),
25298
+ score: scoreIndexCandidate(question, candidate)
25299
+ })).filter(({ keyScore, score }) => keyScore >= 25 || score >= 40).sort((left, right) => right.score - left.score || left.candidate.key.localeCompare(right.candidate.key));
25300
+ const strongestIndex = indexCandidates[0];
25301
+ if (kind === "topic" && strongestIndex && (strongestIndex.keyScore >= 25 || strongestIndex.score >= 40)) {
25302
+ const runnerUp = indexCandidates[1];
25303
+ const ambiguous = strongestIndex.keyScore < 100 && runnerUp !== undefined && runnerUp.score >= strongestIndex.score * 0.9;
25304
+ if (ambiguous) {
25305
+ const lines2 = ["Found 2 ambiguous wiki candidates:", ""];
25306
+ for (const { candidate, score } of [strongestIndex, runnerUp]) {
25307
+ lines2.push(`- kind: ${candidate.kind}`, ` key: ${candidate.key}`, ` score: ${score.toFixed(2)}`, ` description: ${candidate.description || "(none)"}`, ...candidate.date ? [` date: ${candidate.date}`] : []);
25308
+ }
25309
+ return { content: [{ type: "text", text: lines2.join(`
25310
+ `) }] };
25311
+ }
25312
+ const lines = ["Found 1 wiki candidate:", ""];
25313
+ for (const { candidate, score } of [strongestIndex]) {
25314
+ lines.push(`- kind: ${candidate.kind}`, ` key: ${candidate.key}`, ` score: ${score.toFixed(2)}`, ` description: ${candidate.description || "(none)"}`, ...candidate.date ? [` date: ${candidate.date}`] : []);
25315
+ }
25316
+ return { content: [{ type: "text", text: lines.join(`
25317
+ `) }] };
25318
+ }
25076
25319
  const results = [];
25077
25320
  const scored = [];
25078
25321
  function scan(dir, label) {
@@ -25089,67 +25332,234 @@ function wikiQuery(args) {
25089
25332
  const fp = join36(sub, f.name);
25090
25333
  try {
25091
25334
  const text2 = readFileSync22(fp, "utf-8");
25335
+ const key = `${entry.name}/${f.name.replace(/\.md$/i, "")}`;
25092
25336
  scored.push({
25093
- score: scoreMatch(text2),
25337
+ kind: label === "articles" ? "article" : "skill",
25338
+ key,
25339
+ keyScore: scoreIndexKey(question, key),
25340
+ score: scoreMatch(text2, key, label === "articles" ? "article" : "skill"),
25094
25341
  path: `${label}/${entry.name}/${f.name}`,
25095
- text: text2
25342
+ text: text2,
25343
+ title: titleFromDocument(text2, key)
25096
25344
  });
25097
25345
  } catch {}
25098
25346
  }
25099
25347
  } catch {}
25100
25348
  }
25101
25349
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
25350
+ if (label === "topic" && canReadTopicMemory && !canReadTopicMemory(entry.name.replace(/\.md$/i, ""), runtime().userId)) {
25351
+ continue;
25352
+ }
25102
25353
  const fp = join36(dir, entry.name);
25103
25354
  try {
25104
25355
  const text2 = readFileSync22(fp, "utf-8");
25356
+ const key = entry.name.replace(/\.md$/i, "");
25357
+ const resultKind = label === "articles" ? "article" : label === "topic" ? "topic" : label === "summaries" ? "summary" : "skill";
25105
25358
  scored.push({
25106
- score: scoreMatch(text2),
25359
+ kind: resultKind,
25360
+ key,
25361
+ keyScore: scoreIndexKey(question, key),
25362
+ score: scoreMatch(text2, key, resultKind),
25107
25363
  path: `${label}/${entry.name}`,
25108
- text: text2
25364
+ text: text2,
25365
+ title: titleFromDocument(text2, key),
25366
+ ...label === "summaries" && /^\d{4}-\d{2}-\d{2}/.test(key) ? { date: key.slice(0, 10) } : {}
25109
25367
  });
25110
25368
  } catch {}
25111
25369
  }
25112
25370
  }
25113
25371
  } catch {}
25114
25372
  }
25115
- function scoreMatch(text2) {
25116
- const lower = text2.toLowerCase();
25373
+ function titleFromDocument(text2, key) {
25374
+ const titleLine = text2.split(`
25375
+ `).find((line) => /^#+\s*/.test(line));
25376
+ return titleLine ? titleLine.replace(/^#+\s*/, "") : basename7(key);
25377
+ }
25378
+ function scoreMatch(text2, key, resultKind) {
25379
+ const titleLine = text2.split(`
25380
+ `).find((line) => /^#+\s*/.test(line)) ?? "";
25381
+ const title = titleLine.replace(/^#+\s*/, "");
25382
+ const normalizedKey = normalizeSearchText(key);
25383
+ const normalizedTitle2 = normalizeSearchText(title);
25384
+ const normalizedBody = normalizeSearchText(text2.replace(titleLine, ""));
25385
+ const compactQuery = compactSearchText(normalizedQuery);
25386
+ const compactKey = compactSearchText(normalizedKey);
25387
+ const compactTitle = compactSearchText(normalizedTitle2);
25388
+ const tokens = canonicalSearchTokens(normalizedQuery);
25389
+ const keyTokens = new Set(canonicalSearchTokens(normalizedKey));
25390
+ const titleTokens = new Set(canonicalSearchTokens(normalizedTitle2));
25391
+ const bodyTokens = canonicalSearchTokens(normalizedBody);
25117
25392
  let score = 0;
25118
- const words = query2.split(/\s+/).filter(Boolean);
25119
- for (const w of words) {
25120
- if (lower.includes(w)) {
25121
- score += w.length >= 3 ? 3 : 1;
25122
- const firstLine = lower.split(`
25123
- `)[0];
25124
- if (firstLine.startsWith("#") && firstLine.includes(w))
25125
- score += 5;
25126
- }
25127
- }
25393
+ let strongMetadataEvidence = false;
25394
+ if (normalizedKey === normalizedQuery) {
25395
+ score += 120;
25396
+ strongMetadataEvidence = true;
25397
+ } else if (compactKey === compactQuery) {
25398
+ score += 110;
25399
+ strongMetadataEvidence = true;
25400
+ } else if (compactQuery.length >= 2 && (compactKey.startsWith(compactQuery) || compactQuery.startsWith(compactKey))) {
25401
+ score += 65;
25402
+ strongMetadataEvidence = true;
25403
+ }
25404
+ if (tokens.length > 0 && compactQuery.length >= 3 && compactKey.includes(compactQuery)) {
25405
+ score += 45;
25406
+ strongMetadataEvidence = true;
25407
+ }
25408
+ const fuzzyKeyScore = fuzzySearchScore(compactQuery, compactKey, 55);
25409
+ score += fuzzyKeyScore;
25410
+ if (fuzzyKeyScore > 0)
25411
+ strongMetadataEvidence = true;
25412
+ if (normalizedTitle2 === normalizedQuery) {
25413
+ score += 115;
25414
+ strongMetadataEvidence = true;
25415
+ } else if (compactTitle === compactQuery) {
25416
+ score += 105;
25417
+ strongMetadataEvidence = true;
25418
+ } else if (compactQuery.length >= 2 && compactTitle.startsWith(compactQuery)) {
25419
+ score += 60;
25420
+ strongMetadataEvidence = true;
25421
+ }
25422
+ const fuzzyTitleScore = fuzzySearchScore(compactQuery, compactTitle, 50);
25423
+ score += fuzzyTitleScore;
25424
+ if (fuzzyTitleScore > 0)
25425
+ strongMetadataEvidence = true;
25426
+ if (resultKind === "summary") {
25427
+ const temporalScore = scoreSummaryTime(normalizedQuery, key, title);
25428
+ if (temporalScore === null)
25429
+ return 0;
25430
+ score += temporalScore;
25431
+ if (temporalScore > 0)
25432
+ strongMetadataEvidence = true;
25433
+ }
25434
+ const isMultiToken = tokens.length > 1;
25435
+ if (tokens.length > 0 && normalizedTitle2.includes(normalizedQuery)) {
25436
+ score += 35;
25437
+ strongMetadataEvidence = true;
25438
+ }
25439
+ const phraseMatches = isMultiToken ? countPhrase(normalizedBody, normalizedQuery) : 0;
25440
+ if (phraseMatches > 0)
25441
+ score += phraseMatches * 12;
25442
+ else if (tokens.length === 1 && tokens[0].length >= 5 && normalizedBody.includes(tokens[0])) {
25443
+ score += 16;
25444
+ }
25445
+ let bodyMatches = 0;
25446
+ let minimumBodyFrequency = Number.POSITIVE_INFINITY;
25447
+ for (const token of tokens) {
25448
+ if (keyTokens.has(token))
25449
+ score += 10;
25450
+ if (titleTokens.has(token))
25451
+ score += 12;
25452
+ const bodyFrequency = countToken(bodyTokens, token);
25453
+ if (bodyFrequency > 0) {
25454
+ score += Math.min(bodyFrequency, 3) * 4;
25455
+ bodyMatches += 1;
25456
+ minimumBodyFrequency = Math.min(minimumBodyFrequency, bodyFrequency);
25457
+ }
25458
+ }
25459
+ if (isMultiToken && bodyMatches === tokens.length) {
25460
+ score += 8 * Math.min(minimumBodyFrequency, 3);
25461
+ }
25462
+ const minimumBodyTokens = hasStaleExactIndexMatch ? 3 : 2;
25463
+ const partialArticleEvidence = resultKind === "article" && bodyMatches >= 3 && bodyMatches / tokens.length >= 0.6;
25464
+ const strongBodyEvidence = tokens.length >= minimumBodyTokens && (bodyMatches === tokens.length && (phraseMatches > 0 || minimumBodyFrequency >= 1) || partialArticleEvidence);
25465
+ if (!strongMetadataEvidence && !strongBodyEvidence)
25466
+ return 0;
25128
25467
  return score;
25129
25468
  }
25130
- scan(runtime().articlesDir, "articles");
25131
- if (runtime().surface === "all")
25469
+ if (kind === "all" || kind === "article")
25470
+ scan(runtime().articlesDir, "articles");
25471
+ if (kind === "all" && runtime().surface === "all")
25132
25472
  scan(runtime().skillsDir, "skills");
25133
- scan(runtime().topicsDir, "topic");
25134
- scan(runtime().summariesDir, "summaries");
25135
- scored.sort((a, b) => b.score - a.score);
25136
- const top = scored.slice(0, 8);
25137
- if (top.length === 0) {
25473
+ if (kind === "all" || kind === "topic")
25474
+ scan(runtime().topicsDir, "topic");
25475
+ if (kind === "all" || kind === "summary")
25476
+ scan(runtime().summariesDir, "summaries");
25477
+ const merged = new Map;
25478
+ for (const { candidate, keyScore, score } of indexCandidates) {
25479
+ merged.set(`${candidate.kind}:${candidate.key}`, {
25480
+ kind: candidate.kind,
25481
+ key: candidate.key,
25482
+ keyScore,
25483
+ score,
25484
+ description: candidate.description,
25485
+ date: candidate.date
25486
+ });
25487
+ }
25488
+ const documentThreshold = kind === "topic" ? 24 : 16;
25489
+ for (const document of scored) {
25490
+ if (document.score < documentThreshold)
25491
+ continue;
25492
+ const id = `${document.kind}:${document.key}`;
25493
+ const indexed = merged.get(id);
25494
+ merged.set(id, indexed ? { ...indexed, ...document, score: Math.max(indexed.score, document.score) } : document);
25495
+ }
25496
+ let ranked = [...merged.values()].sort((left, right) => right.score - left.score || (left.kind === "summary" && right.kind === "summary" ? temporalDirection === "oldest" ? (left.date ?? "").localeCompare(right.date ?? "") : (right.date ?? "").localeCompare(left.date ?? "") : 0) || left.key.localeCompare(right.key));
25497
+ if (kind === "summary") {
25498
+ const sameTopic = ranked.filter((result) => {
25499
+ const identity = summaryTopicIdentity(result.key);
25500
+ return identity.length >= 3 && normalizedQuery.includes(identity);
25501
+ });
25502
+ if (sameTopic.length > 0)
25503
+ ranked = sameTopic;
25504
+ const requestedDateMatch = normalizedQuery.match(/\b(\d{4}) (\d{2}) (\d{2})\b/);
25505
+ const requestedDate = requestedDateMatch ? `${requestedDateMatch[1]}-${requestedDateMatch[2]}-${requestedDateMatch[3]}` : undefined;
25506
+ const beforeDate = /\bbefore\b/.test(normalizedQuery);
25507
+ const afterDate = /\bafter\b/.test(normalizedQuery);
25508
+ if (requestedDate && beforeDate) {
25509
+ ranked = ranked.filter((result) => !result.date || result.date < requestedDate);
25510
+ } else if (requestedDate && afterDate) {
25511
+ ranked = ranked.filter((result) => !result.date || result.date > requestedDate);
25512
+ }
25513
+ ranked.sort((left, right) => {
25514
+ if (beforeDate) {
25515
+ return (right.date ?? "").localeCompare(left.date ?? "") || right.score - left.score;
25516
+ }
25517
+ if (afterDate) {
25518
+ return (left.date ?? "").localeCompare(right.date ?? "") || right.score - left.score;
25519
+ }
25520
+ if (requestedDate) {
25521
+ const dateDifference = Number(right.date === requestedDate) - Number(left.date === requestedDate);
25522
+ if (dateDifference !== 0)
25523
+ return dateDifference;
25524
+ }
25525
+ if (temporalDirection) {
25526
+ const dateDifference = temporalDirection === "oldest" ? (left.date ?? "").localeCompare(right.date ?? "") : (right.date ?? "").localeCompare(left.date ?? "");
25527
+ if (dateDifference !== 0)
25528
+ return dateDifference;
25529
+ }
25530
+ return right.score - left.score || (right.date ?? "").localeCompare(left.date ?? "") || left.key.localeCompare(right.key);
25531
+ });
25532
+ }
25533
+ if (kind === "topic" && ranked[0]) {
25534
+ const runnerUp = ranked[1];
25535
+ const ambiguous = ranked[0].keyScore < 100 && runnerUp !== undefined && runnerUp.score >= ranked[0].score * 0.9;
25536
+ if (ambiguous) {
25537
+ ranked = ranked.slice(0, 2);
25538
+ } else {
25539
+ ranked = ranked.slice(0, 1);
25540
+ }
25541
+ } else {
25542
+ ranked = ranked.slice(0, limit);
25543
+ }
25544
+ if (ranked.length === 0) {
25138
25545
  results.push("No matching wiki articles found.");
25139
25546
  } else {
25140
- results.push(`Found ${top.length} matching result(s):
25547
+ results.push(`Found ${ranked.length} matching result(s):
25141
25548
  `);
25142
- for (const { path, text: text2 } of top) {
25143
- const titleLine = text2.split(`
25144
- `).find((l) => l.startsWith("#"));
25145
- const title = titleLine ? titleLine.replace(/^#+\s*/, "") : basename7(path, ".md");
25146
- const body = text2.replace(/^---[\s\S]*?---\n?/, "").replace(/^#.*$/m, "").trim().slice(0, 400);
25147
- results.push(`## ${title}
25148
- Path: ${path}
25149
-
25150
- ${body}${text2.length > 400 ? `
25549
+ for (const result of ranked) {
25550
+ if (result.kind === "skill") {
25551
+ const body = (result.text ?? "").trim().slice(0, 400);
25552
+ results.push(`## ${result.title ?? result.key}
25553
+ Path: ${result.path}
25554
+
25555
+ ${body}${(result.text?.length ?? 0) > 400 ? `
25151
25556
  ...(truncated)` : ""}
25152
25557
  `);
25558
+ continue;
25559
+ }
25560
+ results.push(`- kind: ${result.kind}`, ` key: ${result.key}`, ` score: ${result.score.toFixed(2)}`, ...result.title ? [` title: ${result.title}`] : [], ` description: ${result.description || "(none)"}`, ...result.date ? [` date: ${result.date}`] : [], ...result.path ? [` path: ${result.path}`, ` Path: ${result.path}`] : [], ...result.text ? [
25561
+ ` excerpt: ${result.text.replace(/^---[\s\S]*?---\n?/, "").replace(/^#.*$/m, "").trim().slice(0, 400)}`
25562
+ ] : []);
25153
25563
  }
25154
25564
  }
25155
25565
  return { content: [{ type: "text", text: results.join(`
@@ -25168,36 +25578,94 @@ function wikiTopicBrief(args) {
25168
25578
  };
25169
25579
  }
25170
25580
  const { getTopicBrief: getTopicBrief2, resolveTopicBrief: resolveTopicBrief2 } = runtime().host;
25171
- if (runtime().topicId && (getTopicBrief2 || resolveTopicBrief2)) {
25581
+ const hasAuthorizedBridge = Boolean(runtime().topicId && (getTopicBrief2 || resolveTopicBrief2));
25582
+ if (hasAuthorizedBridge) {
25172
25583
  try {
25173
25584
  const brief = resolveTopicBrief2 ? resolveTopicBrief2(rawTopic, runtime().userId) : getTopicBrief2(rawTopic);
25174
- if (!brief) {
25175
- return {
25176
- content: [{ type: "text", text: `No brief found for topic: ${rawTopic}` }]
25177
- };
25178
- }
25179
- const lines = [
25180
- `# Topic Brief: ${topicNameFrom(rawTopic)}`,
25181
- "",
25182
- brief.briefMd || "(empty brief)"
25183
- ];
25184
- if (brief.latestSummaryMd) {
25185
- lines.push("", "## Latest Summary", "", brief.latestSummaryMd);
25186
- }
25187
- if (brief.summaryDate) {
25188
- lines.push("", `Summary date: ${brief.summaryDate}`);
25189
- }
25190
- return { content: [{ type: "text", text: lines.join(`
25585
+ if (brief) {
25586
+ const lines = [
25587
+ `# Topic Brief: ${topicNameFrom(rawTopic)}`,
25588
+ "",
25589
+ brief.briefMd || "(empty brief)"
25590
+ ];
25591
+ if (brief.latestSummaryMd) {
25592
+ lines.push("", "## Latest Summary", "", brief.latestSummaryMd);
25593
+ }
25594
+ if (brief.summaryDate) {
25595
+ lines.push("", `Summary date: ${brief.summaryDate}`);
25596
+ }
25597
+ return { content: [{ type: "text", text: lines.join(`
25191
25598
  `) }] };
25192
- } catch {
25599
+ }
25600
+ } catch {}
25601
+ return {
25602
+ content: [{ type: "text", text: `No brief found for topic: ${rawTopic}` }]
25603
+ };
25604
+ }
25605
+ const topicKey = wikiSummarySlug(rawTopic.replace(/^topic\//, "").replace(/\.md$/i, ""));
25606
+ const filePath = resolve17(runtime().topicsDir, `${topicKey}.md`);
25607
+ try {
25608
+ const content = readFileSync22(filePath, "utf-8");
25609
+ return {
25610
+ content: [{ type: "text", text: `# Topic Brief: ${topicKey}
25611
+
25612
+ ${content}` }]
25613
+ };
25614
+ } catch {}
25615
+ return {
25616
+ content: [{ type: "text", text: `No brief found for topic: ${rawTopic}` }]
25617
+ };
25618
+ }
25619
+ function wikiRead(args) {
25620
+ const rawKind = String(safeExt(args, ["kind", "type"], "article"));
25621
+ const kind = rawKind === "topic" || rawKind === "summary" ? rawKind : "article";
25622
+ const rawKey = String(safeExt(args, ["key", "slug", "topic"], "")).trim();
25623
+ if (!rawKey) {
25624
+ return { content: [{ type: "text", text: "A wiki document key is required." }], isError: true };
25625
+ }
25626
+ if (kind === "topic") {
25627
+ const result = wikiTopicBrief({ topic: rawKey });
25628
+ const resultText = result.content.map((entry) => entry.type === "text" ? entry.text : "").join(`
25629
+ `);
25630
+ const adopt = safeExt(args, ["adopt", "use", "select"], false);
25631
+ if (!adopt || resultText.startsWith("No brief found for topic"))
25632
+ return result;
25633
+ const currentTopicId = runtime().currentTopicId;
25634
+ const memoryKey = wikiSummarySlug(rawKey.replace(/^topic\//, "").replace(/\.md$/i, ""));
25635
+ if (!currentTopicId || !runtime().host.adoptTopicMemory?.(currentTopicId, runtime().userId, memoryKey)) {
25193
25636
  return {
25194
- content: [{ type: "text", text: `Could not read topic brief for: ${rawTopic}` }]
25637
+ content: [
25638
+ ...result.content,
25639
+ { type: "text", text: `
25640
+ Memory was read but could not be adopted by this topic.` }
25641
+ ]
25195
25642
  };
25196
25643
  }
25644
+ return {
25645
+ content: [...result.content, { type: "text", text: `
25646
+ Adopted topic memory: ${memoryKey}` }]
25647
+ };
25648
+ }
25649
+ if (rawKey.includes("\\") || rawKey.split("/").some((part) => part === "..") || rawKey.startsWith("/")) {
25650
+ return { content: [{ type: "text", text: "Invalid wiki document key." }], isError: true };
25651
+ }
25652
+ const root = kind === "summary" ? runtime().summariesDir : runtime().articlesDir;
25653
+ const key = rawKey.replace(new RegExp(`^(?:${kind === "summary" ? "summaries" : "articles"})/`), "").replace(/\.md$/i, "");
25654
+ const filePath = resolve17(root, `${key}.md`);
25655
+ const relativePath = relative2(root, filePath);
25656
+ if (relativePath.startsWith("..") || relativePath.startsWith("/")) {
25657
+ return { content: [{ type: "text", text: "Invalid wiki document key." }], isError: true };
25658
+ }
25659
+ try {
25660
+ const content = readFileSync22(filePath, "utf-8");
25661
+ return {
25662
+ content: [{ type: "text", text: `# Wiki ${kind}: ${key}
25663
+
25664
+ ${content}` }]
25665
+ };
25666
+ } catch {
25667
+ return { content: [{ type: "text", text: `No ${kind} found for: ${rawKey}` }] };
25197
25668
  }
25198
- return {
25199
- content: [{ type: "text", text: `No topic brief found for: ${rawTopic}` }]
25200
- };
25201
25669
  }
25202
25670
  function wikiLastConversation(args) {
25203
25671
  const rawTopic = safeExt(args, ["topic", "topicName", "topicId"], runtime().topicId);
@@ -25566,6 +26034,7 @@ function createWikiMcpServer(context, host2) {
25566
26034
  const wikiDir = resolve17(host2.wikiRoot);
25567
26035
  const current3 = {
25568
26036
  userId: context.userId,
26037
+ currentTopicId: context.currentTopicId ?? context.topicId,
25569
26038
  topicId: context.topicId,
25570
26039
  surface,
25571
26040
  host: host2,
@@ -25595,6 +26064,7 @@ function createWikiMcpServer(context, host2) {
25595
26064
  const { name, arguments: args } = request.params;
25596
26065
  const handlers = {
25597
26066
  wiki_query: wikiQuery,
26067
+ wiki_read: wikiRead,
25598
26068
  wiki_topic_brief: wikiTopicBrief,
25599
26069
  wiki_last_conversation: wikiLastConversation,
25600
26070
  skill_query: skillQuery,
@@ -25609,16 +26079,87 @@ function createWikiMcpServer(context, host2) {
25609
26079
  });
25610
26080
  return server;
25611
26081
  }
25612
- var wikiRuntime, FILE_LOCK_WAIT_MS = 10, FILE_LOCK_TIMEOUT_MS = 5000, FILE_LOCK_STALE_MS = 30000, lockWaitArray, WIKI_TOOLS, SKILL_TOOLS;
26082
+ var wikiRuntime, FILE_LOCK_WAIT_MS = 10, FILE_LOCK_TIMEOUT_MS = 5000, FILE_LOCK_STALE_MS = 30000, lockWaitArray, SEARCH_STOP_WORDS, SEARCH_MONTHS, SEARCH_RECENCY_TERMS, WIKI_TOOLS, SKILL_TOOLS;
25613
26083
  var init_wiki_server = __esm(() => {
25614
26084
  init_config();
25615
26085
  init_wiki_summary_names();
25616
26086
  wikiRuntime = new AsyncLocalStorage3;
25617
26087
  lockWaitArray = new Int32Array(new SharedArrayBuffer(4));
26088
+ SEARCH_STOP_WORDS = new Set([
26089
+ "a",
26090
+ "an",
26091
+ "and",
26092
+ "are",
26093
+ "as",
26094
+ "at",
26095
+ "be",
26096
+ "by",
26097
+ "for",
26098
+ "from",
26099
+ "in",
26100
+ "is",
26101
+ "it",
26102
+ "of",
26103
+ "on",
26104
+ "or",
26105
+ "that",
26106
+ "the",
26107
+ "to",
26108
+ "with"
26109
+ ]);
26110
+ SEARCH_MONTHS = new Map([
26111
+ ["january", "01"],
26112
+ ["jan", "01"],
26113
+ ["1\uC6D4", "01"],
26114
+ ["february", "02"],
26115
+ ["feb", "02"],
26116
+ ["2\uC6D4", "02"],
26117
+ ["march", "03"],
26118
+ ["mar", "03"],
26119
+ ["3\uC6D4", "03"],
26120
+ ["april", "04"],
26121
+ ["apr", "04"],
26122
+ ["4\uC6D4", "04"],
26123
+ ["may", "05"],
26124
+ ["5\uC6D4", "05"],
26125
+ ["june", "06"],
26126
+ ["jun", "06"],
26127
+ ["6\uC6D4", "06"],
26128
+ ["july", "07"],
26129
+ ["jul", "07"],
26130
+ ["7\uC6D4", "07"],
26131
+ ["august", "08"],
26132
+ ["aug", "08"],
26133
+ ["8\uC6D4", "08"],
26134
+ ["september", "09"],
26135
+ ["sep", "09"],
26136
+ ["sept", "09"],
26137
+ ["9\uC6D4", "09"],
26138
+ ["october", "10"],
26139
+ ["oct", "10"],
26140
+ ["10\uC6D4", "10"],
26141
+ ["november", "11"],
26142
+ ["nov", "11"],
26143
+ ["11\uC6D4", "11"],
26144
+ ["december", "12"],
26145
+ ["dec", "12"],
26146
+ ["12\uC6D4", "12"]
26147
+ ]);
26148
+ SEARCH_RECENCY_TERMS = new Set([
26149
+ "latest",
26150
+ "newest",
26151
+ "recent",
26152
+ "earliest",
26153
+ "oldest",
26154
+ "\uCD5C\uADFC",
26155
+ "\uCD5C\uC2E0",
26156
+ "\uCD5C\uCD08",
26157
+ "\uC624\uB798\uB41C"
26158
+ ]);
25618
26159
  WIKI_TOOLS = [
25619
26160
  {
25620
26161
  name: "wiki_query",
25621
- description: "Search the wiki knowledge base and return relevant articles.",
26162
+ description: "Search topic, article, or summary indexes and return relevant candidates.",
25622
26163
  inputSchema: {
25623
26164
  type: "object",
25624
26165
  properties: {
@@ -25629,11 +26170,38 @@ var init_wiki_server = __esm(() => {
25629
26170
  topic: {
25630
26171
  type: "string",
25631
26172
  description: "Optional topic name to narrow the search"
26173
+ },
26174
+ kind: {
26175
+ type: "string",
26176
+ enum: ["all", "topic", "article", "summary"],
26177
+ description: "Document kind to search; defaults to all"
26178
+ },
26179
+ limit: {
26180
+ type: "number",
26181
+ minimum: 1,
26182
+ maximum: 20,
26183
+ description: "Maximum number of candidates; defaults to 8"
25632
26184
  }
25633
26185
  },
25634
26186
  required: ["question"]
25635
26187
  }
25636
26188
  },
26189
+ {
26190
+ name: "wiki_read",
26191
+ description: "Read one topic, article, or summary returned by wiki_query. For a clearly matching continuing persona, set adopt=true when reading a topic.",
26192
+ inputSchema: {
26193
+ type: "object",
26194
+ properties: {
26195
+ kind: { type: "string", enum: ["topic", "article", "summary"] },
26196
+ key: { type: "string", description: "Canonical key returned by wiki_query" },
26197
+ adopt: {
26198
+ type: "boolean",
26199
+ description: "For kind=topic, persist this canonical memory key for the current room"
26200
+ }
26201
+ },
26202
+ required: ["kind", "key"]
26203
+ }
26204
+ },
25637
26205
  {
25638
26206
  name: "wiki_topic_brief",
25639
26207
  description: "Get the lightweight topic brief for a specific topic.",
@@ -40957,6 +41525,7 @@ var init_canonical_mcp_bridge = __esm(async () => {
40957
41525
  task: new Set(["task_create", "task_update", "task_list", "task_get", "task_delete"]),
40958
41526
  wiki: new Set([
40959
41527
  "wiki_query",
41528
+ "wiki_read",
40960
41529
  "wiki_topic_brief",
40961
41530
  "wiki_last_conversation",
40962
41531
  "save_wiki_entry",
@@ -45823,4 +46392,4 @@ switch (command) {
45823
46392
  }
45824
46393
  }
45825
46394
 
45826
- //# debugId=C4FD6B649361AE3464756E2164756E21
46395
+ //# debugId=9061330203CC009264756E2164756E21