negotium 0.2.17 → 0.2.19

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 (38) hide show
  1. package/dist/agent-helpers.js +41 -32
  2. package/dist/agent-helpers.js.map +10 -10
  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 +1377 -285
  8. package/dist/main.js.map +14 -13
  9. package/dist/mcp-factories.js +1497 -403
  10. package/dist/mcp-factories.js.map +14 -13
  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/archiver.ts +1 -1
  17. package/dist/runtime/src/agents/idle-archiver.ts +3 -2
  18. package/dist/runtime/src/mcp/canonical-proxy-server.ts +31 -15
  19. package/dist/runtime/src/mcp/wiki-search-index.ts +359 -0
  20. package/dist/runtime/src/mcp/wiki-server.ts +1358 -155
  21. package/dist/runtime/src/platform/mcp-config.ts +2 -1
  22. package/dist/runtime/src/prompts/agents/wiki-archiver.md +73 -48
  23. package/dist/runtime/src/prompts/builders.ts +17 -34
  24. package/dist/runtime/src/runtime/turn-runner.ts +14 -17
  25. package/dist/runtime/src/storage/api-topics.ts +13 -4
  26. package/dist/runtime/src/topics/lifecycle.ts +3 -2
  27. package/dist/runtime/src/types/api.ts +6 -0
  28. package/dist/runtime/src/version.ts +1 -1
  29. package/dist/storage.js +12 -6
  30. package/dist/storage.js.map +3 -3
  31. package/dist/types/packages/core/src/agents/archiver.d.ts +1 -1
  32. package/dist/types/packages/core/src/mcp/wiki-search-index.d.ts +59 -0
  33. package/dist/types/packages/core/src/mcp/wiki-server.d.ts +4 -0
  34. package/dist/types/packages/core/src/prompts/builders.d.ts +2 -4
  35. package/dist/types/packages/core/src/storage/api-topics.d.ts +1 -0
  36. package/dist/types/packages/core/src/types/api.d.ts +6 -0
  37. package/dist/types/packages/core/src/version.d.ts +1 -1
  38. 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.19";
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) {
@@ -24911,6 +24920,219 @@ var init_vault2 = __esm(() => {
24911
24920
  init_mcp_helpers();
24912
24921
  });
24913
24922
 
24923
+ // ../../packages/core/src/mcp/wiki-search-index.ts
24924
+ import { chmodSync as chmodSync5, existsSync as existsSync26, readFileSync as readFileSync22, unlinkSync as unlinkSync18 } from "fs";
24925
+ function documentId(source) {
24926
+ return `${source.kind}:${source.key}`;
24927
+ }
24928
+ function ensureSchema(database) {
24929
+ const version = database.query("PRAGMA user_version").get()?.user_version;
24930
+ if (version !== 0 && version !== SEARCH_INDEX_SCHEMA_VERSION) {
24931
+ throw new Error(`Unsupported wiki search index schema: ${version}`);
24932
+ }
24933
+ database.exec(`
24934
+ CREATE TABLE IF NOT EXISTS wiki_search_documents (
24935
+ id TEXT PRIMARY KEY,
24936
+ kind TEXT NOT NULL,
24937
+ key TEXT NOT NULL,
24938
+ path TEXT NOT NULL,
24939
+ mtime_ms REAL NOT NULL,
24940
+ size INTEGER NOT NULL,
24941
+ title TEXT NOT NULL,
24942
+ text TEXT NOT NULL,
24943
+ date TEXT,
24944
+ family TEXT
24945
+ );
24946
+ CREATE UNIQUE INDEX IF NOT EXISTS wiki_search_documents_kind_key
24947
+ ON wiki_search_documents(kind, key);
24948
+ CREATE INDEX IF NOT EXISTS wiki_search_documents_kind_date
24949
+ ON wiki_search_documents(kind, date);
24950
+ CREATE INDEX IF NOT EXISTS wiki_search_documents_kind_family_date
24951
+ ON wiki_search_documents(kind, family, date);
24952
+ CREATE VIRTUAL TABLE IF NOT EXISTS wiki_search_fts USING fts5(
24953
+ document_id UNINDEXED,
24954
+ kind UNINDEXED,
24955
+ tokens,
24956
+ tokenize = 'unicode61'
24957
+ );
24958
+ PRAGMA user_version = ${SEARCH_INDEX_SCHEMA_VERSION};
24959
+ `);
24960
+ }
24961
+
24962
+ class WikiSearchIndex {
24963
+ path;
24964
+ #database;
24965
+ constructor(path) {
24966
+ this.path = path;
24967
+ this.#database = new Database(path, { create: true });
24968
+ try {
24969
+ this.#database.exec("PRAGMA foreign_keys = ON; PRAGMA journal_mode = DELETE;");
24970
+ ensureSchema(this.#database);
24971
+ chmodSync5(path, 384);
24972
+ } catch (error2) {
24973
+ this.#database.close();
24974
+ throw error2;
24975
+ }
24976
+ }
24977
+ close() {
24978
+ this.#database.close();
24979
+ }
24980
+ upsert(source, document) {
24981
+ const id = documentId(source);
24982
+ const apply = this.#database.transaction(() => {
24983
+ this.#database.query("DELETE FROM wiki_search_fts WHERE document_id = ?").run(id);
24984
+ this.#database.query("DELETE FROM wiki_search_documents WHERE id = ?").run(id);
24985
+ this.#database.query(`
24986
+ INSERT INTO wiki_search_documents
24987
+ (id, kind, key, path, mtime_ms, size, title, text, date, family)
24988
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
24989
+ `).run(id, source.kind, source.key, source.path, source.mtimeMs, source.size, document.title, document.text, document.date ?? null, document.family ?? null);
24990
+ this.#database.query("INSERT INTO wiki_search_fts(document_id, kind, tokens) VALUES (?, ?, ?)").run(id, source.kind, document.tokens.join(" "));
24991
+ });
24992
+ apply.immediate();
24993
+ }
24994
+ sync(sources, prepare) {
24995
+ const result = { added: 0, updated: 0, removed: 0 };
24996
+ const existingRows = this.#database.query("SELECT id, path, mtime_ms, size FROM wiki_search_documents ORDER BY id").all();
24997
+ const existing = new Map(existingRows.map((row) => [row.id, row]));
24998
+ const present = new Set;
24999
+ const changed = [];
25000
+ for (const source of sources) {
25001
+ const id = documentId(source);
25002
+ present.add(id);
25003
+ const stored = existing.get(id);
25004
+ if (stored && stored.path === source.path && stored.mtime_ms === source.mtimeMs && stored.size === source.size) {
25005
+ continue;
25006
+ }
25007
+ try {
25008
+ const text2 = readFileSync22(source.path, "utf-8");
25009
+ changed.push({ source, document: prepare(source, text2), added: !stored });
25010
+ } catch {}
25011
+ }
25012
+ const removed = existingRows.filter((row) => !present.has(row.id));
25013
+ const apply = this.#database.transaction(() => {
25014
+ const deleteTerms = this.#database.query("DELETE FROM wiki_search_fts WHERE document_id = ?");
25015
+ const deleteDocument = this.#database.query("DELETE FROM wiki_search_documents WHERE id = ?");
25016
+ const insertDocument = this.#database.query(`
25017
+ INSERT INTO wiki_search_documents
25018
+ (id, kind, key, path, mtime_ms, size, title, text, date, family)
25019
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
25020
+ `);
25021
+ const insertTerms = this.#database.query(`
25022
+ INSERT INTO wiki_search_fts(document_id, kind, tokens)
25023
+ VALUES (?, ?, ?)
25024
+ `);
25025
+ for (const row of removed) {
25026
+ deleteTerms.run(row.id);
25027
+ deleteDocument.run(row.id);
25028
+ result.removed += 1;
25029
+ }
25030
+ for (const { source, document, added } of changed) {
25031
+ const id = documentId(source);
25032
+ deleteTerms.run(id);
25033
+ deleteDocument.run(id);
25034
+ insertDocument.run(id, source.kind, source.key, source.path, source.mtimeMs, source.size, document.title, document.text, document.date ?? null, document.family ?? null);
25035
+ insertTerms.run(id, source.kind, document.tokens.join(" "));
25036
+ if (added)
25037
+ result.added += 1;
25038
+ else
25039
+ result.updated += 1;
25040
+ }
25041
+ });
25042
+ apply.immediate();
25043
+ return result;
25044
+ }
25045
+ metadata(kinds) {
25046
+ if (kinds.length === 0)
25047
+ return [];
25048
+ const placeholders = kinds.map(() => "?").join(", ");
25049
+ const rows = this.#database.query(`SELECT id, kind, key, title, date
25050
+ FROM wiki_search_documents
25051
+ WHERE kind IN (${placeholders})`).all(...kinds);
25052
+ return rows.map((row) => ({
25053
+ id: row.id,
25054
+ kind: row.kind,
25055
+ key: row.key,
25056
+ title: row.title,
25057
+ ...row.date ? { date: row.date } : {}
25058
+ }));
25059
+ }
25060
+ summaryMetadataByDate(query2) {
25061
+ const conditions = ["kind = 'summary'", "date IS NOT NULL"];
25062
+ const parameters = [];
25063
+ if (query2.year && query2.month) {
25064
+ const month = Number(query2.month);
25065
+ const nextYear = month === 12 ? String(Number(query2.year) + 1) : query2.year;
25066
+ const nextMonth = String(month === 12 ? 1 : month + 1).padStart(2, "0");
25067
+ conditions.push("date >= ?", "date < ?");
25068
+ parameters.push(`${query2.year}-${query2.month}-01`, `${nextYear}-${nextMonth}-01`);
25069
+ } else if (query2.year) {
25070
+ conditions.push("date >= ?", "date < ?");
25071
+ parameters.push(`${query2.year}-01-01`, `${Number(query2.year) + 1}-01-01`);
25072
+ } else if (query2.month) {
25073
+ conditions.push("substr(date, 6, 2) = ?");
25074
+ parameters.push(query2.month);
25075
+ }
25076
+ parameters.push(query2.limit);
25077
+ const rows = this.#database.query(`SELECT id, kind, key, title, date
25078
+ FROM wiki_search_documents
25079
+ WHERE ${conditions.join(" AND ")}
25080
+ ORDER BY date ${query2.direction === "oldest" ? "ASC" : "DESC"}, key ASC
25081
+ LIMIT ?`).all(...parameters);
25082
+ return rows.map((row) => ({
25083
+ id: row.id,
25084
+ kind: row.kind,
25085
+ key: row.key,
25086
+ title: row.title,
25087
+ ...row.date ? { date: row.date } : {}
25088
+ }));
25089
+ }
25090
+ matchingDocumentIds(kinds, terms) {
25091
+ if (kinds.length === 0 || terms.length === 0)
25092
+ return new Set;
25093
+ const kindPlaceholders = kinds.map(() => "?").join(", ");
25094
+ const match = terms.map((term) => `"${term.replaceAll('"', '""')}"`).join(" OR ");
25095
+ const rows = this.#database.query(`SELECT DISTINCT document_id
25096
+ FROM wiki_search_fts
25097
+ WHERE wiki_search_fts MATCH ? AND kind IN (${kindPlaceholders})`).all(match, ...kinds);
25098
+ return new Set(rows.map((row) => row.document_id));
25099
+ }
25100
+ documents(ids) {
25101
+ if (ids.size === 0)
25102
+ return [];
25103
+ const values = [...ids];
25104
+ const rows = [];
25105
+ for (let offset = 0;offset < values.length; offset += 500) {
25106
+ const chunk = values.slice(offset, offset + 500);
25107
+ const placeholders = chunk.map(() => "?").join(", ");
25108
+ rows.push(...this.#database.query(`SELECT id, kind, key, path, title, text, date
25109
+ FROM wiki_search_documents
25110
+ WHERE id IN (${placeholders})`).all(...chunk));
25111
+ }
25112
+ return rows.map((row) => ({
25113
+ id: row.id,
25114
+ kind: row.kind,
25115
+ key: row.key,
25116
+ path: row.path,
25117
+ title: row.title,
25118
+ text: row.text,
25119
+ tokens: [],
25120
+ ...row.date ? { date: row.date } : {}
25121
+ }));
25122
+ }
25123
+ }
25124
+ function resetCorruptWikiSearchIndex(path) {
25125
+ for (const candidate of [path, `${path}-journal`, `${path}-wal`, `${path}-shm`]) {
25126
+ if (!existsSync26(candidate))
25127
+ continue;
25128
+ unlinkSync18(candidate);
25129
+ }
25130
+ }
25131
+ var SEARCH_INDEX_SCHEMA_VERSION = 3;
25132
+ var init_wiki_search_index = __esm(async () => {
25133
+ await init_sqlite();
25134
+ });
25135
+
24914
25136
  // ../../packages/core/src/mcp/wiki-server.ts
24915
25137
  var exports_wiki_server = {};
24916
25138
  __export(exports_wiki_server, {
@@ -24921,17 +25143,17 @@ import { AsyncLocalStorage as AsyncLocalStorage3 } from "async_hooks";
24921
25143
  import { randomUUID as randomUUID21 } from "crypto";
24922
25144
  import {
24923
25145
  closeSync as closeSync4,
24924
- existsSync as existsSync26,
25146
+ existsSync as existsSync27,
24925
25147
  mkdirSync as mkdirSync24,
24926
25148
  openSync as openSync4,
24927
25149
  readdirSync as readdirSync10,
24928
- readFileSync as readFileSync22,
25150
+ readFileSync as readFileSync23,
24929
25151
  renameSync as renameSync11,
24930
25152
  statSync as statSync15,
24931
- unlinkSync as unlinkSync18,
25153
+ unlinkSync as unlinkSync19,
24932
25154
  writeFileSync as writeFileSync17
24933
25155
  } from "fs";
24934
- import { basename as basename7, dirname as dirname16, join as join36, resolve as resolve17 } from "path";
25156
+ import { basename as basename7, dirname as dirname16, join as join36, relative as relative2, resolve as resolve17 } from "path";
24935
25157
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
24936
25158
  import { StdioServerTransport as StdioServerTransport2 } from "@modelcontextprotocol/sdk/server/stdio.js";
24937
25159
  import {
@@ -24971,15 +25193,15 @@ function acquireRecoveryGuard(lockPath) {
24971
25193
  } catch (error2) {
24972
25194
  closeSync4(fd);
24973
25195
  try {
24974
- unlinkSync18(guardPath);
25196
+ unlinkSync19(guardPath);
24975
25197
  } catch {}
24976
25198
  throw error2;
24977
25199
  }
24978
25200
  return () => {
24979
25201
  closeSync4(fd);
24980
25202
  try {
24981
- if (readFileSync22(guardPath, "utf-8") === ownerToken)
24982
- unlinkSync18(guardPath);
25203
+ if (readFileSync23(guardPath, "utf-8") === ownerToken)
25204
+ unlinkSync19(guardPath);
24983
25205
  } catch {}
24984
25206
  };
24985
25207
  } catch (error2) {
@@ -25004,7 +25226,7 @@ function acquireFileLock(path) {
25004
25226
  } catch (error2) {
25005
25227
  closeSync4(fd);
25006
25228
  try {
25007
- unlinkSync18(lockPath);
25229
+ unlinkSync19(lockPath);
25008
25230
  } catch {}
25009
25231
  throw error2;
25010
25232
  }
@@ -25012,8 +25234,8 @@ function acquireFileLock(path) {
25012
25234
  closeSync4(fd);
25013
25235
  const releaseGuard = acquireRecoveryGuard(lockPath);
25014
25236
  try {
25015
- if (readFileSync22(lockPath, "utf-8") === ownerToken)
25016
- unlinkSync18(lockPath);
25237
+ if (readFileSync23(lockPath, "utf-8") === ownerToken)
25238
+ unlinkSync19(lockPath);
25017
25239
  } catch {} finally {
25018
25240
  releaseGuard();
25019
25241
  }
@@ -25024,9 +25246,9 @@ function acquireFileLock(path) {
25024
25246
  const releaseGuard = acquireRecoveryGuard(lockPath);
25025
25247
  try {
25026
25248
  try {
25027
- const staleToken = readFileSync22(lockPath, "utf-8");
25028
- if (Date.now() - statSync15(lockPath).mtimeMs > FILE_LOCK_STALE_MS && readFileSync22(lockPath, "utf-8") === staleToken) {
25029
- unlinkSync18(lockPath);
25249
+ const staleToken = readFileSync23(lockPath, "utf-8");
25250
+ if (Date.now() - statSync15(lockPath).mtimeMs > FILE_LOCK_STALE_MS && readFileSync23(lockPath, "utf-8") === staleToken) {
25251
+ unlinkSync19(lockPath);
25030
25252
  continue;
25031
25253
  }
25032
25254
  } catch {
@@ -25049,7 +25271,7 @@ function atomicWriteFile(path, content) {
25049
25271
  renameSync11(temporaryPath, path);
25050
25272
  } catch (error2) {
25051
25273
  try {
25052
- unlinkSync18(temporaryPath);
25274
+ unlinkSync19(temporaryPath);
25053
25275
  } catch {}
25054
25276
  throw error2;
25055
25277
  }
@@ -25070,9 +25292,310 @@ function topicNameFrom(topicIdStr) {
25070
25292
  function slugify(topic) {
25071
25293
  return wikiSummarySlug(topic);
25072
25294
  }
25295
+ function normalizeSearchText(value) {
25296
+ return value.toLowerCase().replaceAll(/[^a-z0-9\uAC00-\uD7A3]+/g, " ").trim().replaceAll(/\s+/g, " ");
25297
+ }
25298
+ function compactSearchText(value) {
25299
+ return normalizeSearchText(value).replaceAll(" ", "");
25300
+ }
25301
+ function trigrams(value) {
25302
+ const compact = compactSearchText(value);
25303
+ if (compact.length < 3)
25304
+ return new Set(compact ? [compact] : []);
25305
+ const result = new Set;
25306
+ for (let i = 0;i <= compact.length - 3; i += 1)
25307
+ result.add(compact.slice(i, i + 3));
25308
+ return result;
25309
+ }
25310
+ function fuzzySearchScore(query2, candidate, maximum) {
25311
+ const compactQuery = compactSearchText(query2);
25312
+ const compactCandidate = compactSearchText(candidate);
25313
+ if (compactQuery.length < 3 || compactCandidate.length < 3)
25314
+ return 0;
25315
+ const similarity = trigramSimilarity(compactQuery, compactCandidate);
25316
+ const minimumSimilarity = compactQuery.length <= 4 ? 0.6 : 0.45;
25317
+ return similarity >= minimumSimilarity ? similarity * maximum : 0;
25318
+ }
25319
+ function meaningfulSearchTokens(value) {
25320
+ return normalizeSearchText(value).split(" ").filter((token) => (token.length >= 2 || /^\d+$/.test(token)) && !SEARCH_STOP_WORDS.has(token));
25321
+ }
25322
+ function canonicalSearchToken(token) {
25323
+ return token.length > 4 && token.endsWith("s") ? token.slice(0, -1) : token;
25324
+ }
25325
+ function canonicalSearchTokens(value) {
25326
+ return meaningfulSearchTokens(value).map(canonicalSearchToken);
25327
+ }
25328
+ function countToken(tokens, token) {
25329
+ let count = 0;
25330
+ for (const candidate of tokens)
25331
+ if (candidate === token)
25332
+ count += 1;
25333
+ return count;
25334
+ }
25335
+ function countPhrase(text2, phrase) {
25336
+ if (!phrase)
25337
+ return 0;
25338
+ let count = 0;
25339
+ let offset = 0;
25340
+ let match = text2.indexOf(phrase, offset);
25341
+ while (match >= 0) {
25342
+ count += 1;
25343
+ offset = match + phrase.length;
25344
+ match = text2.indexOf(phrase, offset);
25345
+ }
25346
+ return count;
25347
+ }
25348
+ function summaryTopicIdentity(key) {
25349
+ return normalizeSearchText(key.replace(/^\d{4}-\d{2}-\d{2}-/, ""));
25350
+ }
25351
+ function scoreSummaryTime(query2, key, title) {
25352
+ const date = key.match(/^(\d{4})-(\d{2})-(\d{2})/);
25353
+ if (!date)
25354
+ return 0;
25355
+ const tokens = meaningfulSearchTokens(query2);
25356
+ const documentDate = `${date[1]}-${date[2]}-${date[3]}`;
25357
+ const requestedDateMatch = query2.match(/\b(\d{4}) (\d{2}) (\d{2})\b/);
25358
+ const requestedDate = requestedDateMatch ? `${requestedDateMatch[1]}-${requestedDateMatch[2]}-${requestedDateMatch[3]}` : undefined;
25359
+ const beforeDate = /\bbefore\b/.test(query2);
25360
+ const afterDate = /\bafter\b/.test(query2);
25361
+ if (requestedDate && beforeDate && documentDate >= requestedDate)
25362
+ return null;
25363
+ if (requestedDate && afterDate && documentDate <= requestedDate)
25364
+ return null;
25365
+ const queryYear = tokens.find((token) => /^\d{4}(?:\uB144)?$/.test(token))?.replace(/\uB144$/, "");
25366
+ const queryMonth = tokens.map((token) => SEARCH_MONTHS.get(token)).find(Boolean);
25367
+ const hasRecencyTerm = tokens.some((token) => SEARCH_RECENCY_TERMS.has(token));
25368
+ const yearMatches = !queryYear || queryYear === date[1];
25369
+ const monthMatches = !queryMonth || queryMonth === date[2];
25370
+ if (!queryYear && !queryMonth && !hasRecencyTerm || !yearMatches || !monthMatches)
25371
+ return 0;
25372
+ const normalizedMetadata = `${normalizeSearchText(key)} ${normalizeSearchText(title)}`;
25373
+ const contentTokens = tokens.filter((token) => !/^\d{4}(?:\uB144)?$/.test(token) && !SEARCH_MONTHS.has(token) && !SEARCH_RECENCY_TERMS.has(token));
25374
+ const contentMatches = contentTokens.filter((token) => normalizedMetadata.includes(token)).length;
25375
+ const relationScore = requestedDate && (beforeDate || afterDate) ? 60 : 0;
25376
+ const exactDateScore = requestedDate === documentDate ? 90 : 0;
25377
+ return 30 + relationScore + exactDateScore + contentMatches * 15;
25378
+ }
25379
+ function trigramSimilarity(left, right) {
25380
+ const a = trigrams(left);
25381
+ const b = trigrams(right);
25382
+ if (a.size === 0 || b.size === 0)
25383
+ return 0;
25384
+ let overlap = 0;
25385
+ for (const gram of a)
25386
+ if (b.has(gram))
25387
+ overlap += 1;
25388
+ return 2 * overlap / (a.size + b.size);
25389
+ }
25390
+ function openWikiSearchIndex() {
25391
+ const path = resolve17(runtime().wikiDir, WIKI_SEARCH_INDEX_FILENAME);
25392
+ try {
25393
+ return new WikiSearchIndex(path);
25394
+ } catch (error2) {
25395
+ if (!/malformed|not a database|schema/i.test(String(error2)))
25396
+ return;
25397
+ try {
25398
+ resetCorruptWikiSearchIndex(path);
25399
+ return new WikiSearchIndex(path);
25400
+ } catch {
25401
+ return;
25402
+ }
25403
+ }
25404
+ }
25405
+ function prepareWikiSearchDocument(kind, key, text2) {
25406
+ const titleLine = text2.split(`
25407
+ `).find((line) => /^#+\s*/.test(line)) ?? "";
25408
+ const title = titleLine ? titleLine.replace(/^#+\s*/, "") : basename7(key);
25409
+ return {
25410
+ title,
25411
+ text: text2,
25412
+ tokens: canonicalSearchTokens(`${key} ${title} ${text2}`),
25413
+ ...kind === "summary" && /^\d{4}-\d{2}-\d{2}/.test(key) ? { date: key.slice(0, 10), family: summaryTopicIdentity(key) } : {}
25414
+ };
25415
+ }
25416
+ function indexWikiSearchDocument(kind, key, text2) {
25417
+ const root = kind === "article" ? runtime().articlesDir : runtime().summariesDir;
25418
+ const path = resolve17(root, `${key}.md`);
25419
+ const index = openWikiSearchIndex();
25420
+ if (!index)
25421
+ return false;
25422
+ try {
25423
+ const stat = statSync15(path);
25424
+ index.upsert({ kind, key, path, mtimeMs: stat.mtimeMs, size: stat.size }, prepareWikiSearchDocument(kind, key, text2));
25425
+ return true;
25426
+ } catch {
25427
+ return false;
25428
+ } finally {
25429
+ index.close();
25430
+ }
25431
+ }
25432
+ function wikiIndexPath(kind) {
25433
+ return resolve17(runtime().wikiDir, WIKI_INDEX_FILENAMES[kind]);
25434
+ }
25435
+ function normalizeIndexDescription(raw) {
25436
+ const description = raw.replaceAll(/\s+/g, " ").trim();
25437
+ if (!description)
25438
+ return { error: "A non-empty one-line description is required." };
25439
+ if (description.length > MAX_INDEX_DESCRIPTION_LENGTH) {
25440
+ return {
25441
+ error: `Description must be at most ${MAX_INDEX_DESCRIPTION_LENGTH} characters (got ${description.length}).`
25442
+ };
25443
+ }
25444
+ return { description };
25445
+ }
25446
+ function normalizeWikiSlug(raw) {
25447
+ const slug = raw.trim().replace(/\.md$/i, "").replace(/^\/+/, "");
25448
+ if (!slug)
25449
+ return { error: "A non-empty slug is required." };
25450
+ if (/[[\]|\\]/.test(slug))
25451
+ return { error: "Slug must not contain [ ] | or backslashes." };
25452
+ if (slug.split("/").some((segment) => segment === "" || segment === "." || segment === "..")) {
25453
+ return { error: "Slug must not contain empty or relative path segments." };
25454
+ }
25455
+ return { slug };
25456
+ }
25457
+ function parseWikiIndex(path) {
25458
+ let text2;
25459
+ try {
25460
+ text2 = readFileSync23(path, "utf-8");
25461
+ } catch {
25462
+ return [];
25463
+ }
25464
+ const candidates = [];
25465
+ const entryPattern = /^\s*-\s*\[\[(topic|articles|summaries)\/([^\]]+)\]\]\s*(.*)$/;
25466
+ for (const line of text2.split(`
25467
+ `)) {
25468
+ const match = line.match(entryPattern);
25469
+ if (!match)
25470
+ continue;
25471
+ const namespace = match[1];
25472
+ const key = match[2].replace(/\.md$/i, "");
25473
+ const tail = match[3].trim();
25474
+ const dateMatch = tail.match(/\((\d{4}-\d{2}-\d{2})(?:[^)]*)\)\s*$/);
25475
+ const description = dateMatch ? tail.slice(0, dateMatch.index).trim() : tail;
25476
+ candidates.push({
25477
+ kind: namespace === "topic" ? "topic" : namespace === "articles" ? "article" : "summary",
25478
+ key,
25479
+ description,
25480
+ ...dateMatch ? { date: dateMatch[1] } : {}
25481
+ });
25482
+ }
25483
+ return candidates;
25484
+ }
25485
+ function indexCandidateExists(candidate) {
25486
+ if (candidate.kind === "topic")
25487
+ return true;
25488
+ const root = candidate.kind === "article" ? runtime().articlesDir : runtime().summariesDir;
25489
+ const filePath = resolve17(root, `${candidate.key}.md`);
25490
+ const relativePath = relative2(root, filePath);
25491
+ if (relativePath.startsWith("..") || relativePath.startsWith("/"))
25492
+ return false;
25493
+ try {
25494
+ return statSync15(filePath).isFile();
25495
+ } catch {
25496
+ return false;
25497
+ }
25498
+ }
25499
+ function scoreIndexKey(query2, key) {
25500
+ const normalizedQuery = normalizeSearchText(query2);
25501
+ const compactQuery = compactSearchText(normalizedQuery);
25502
+ const normalizedKey = normalizeSearchText(key);
25503
+ const compactKey = compactSearchText(normalizedKey);
25504
+ const tokens = meaningfulSearchTokens(normalizedQuery);
25505
+ let score = 0;
25506
+ if (normalizedKey === normalizedQuery)
25507
+ score += 120;
25508
+ else if (compactKey === compactQuery)
25509
+ score += 110;
25510
+ else {
25511
+ if (compactQuery.length >= 2 && (compactKey.startsWith(compactQuery) || compactQuery.startsWith(compactKey))) {
25512
+ score += 65;
25513
+ }
25514
+ if (tokens.length > 0 && compactQuery.length >= 3 && compactKey.includes(compactQuery)) {
25515
+ score += 45;
25516
+ }
25517
+ score += fuzzySearchScore(compactQuery, compactKey, 55);
25518
+ }
25519
+ for (const token of tokens) {
25520
+ if (normalizedKey.includes(token))
25521
+ score += 10;
25522
+ }
25523
+ return score;
25524
+ }
25525
+ function scoreIndexCandidate(query2, candidate) {
25526
+ const normalizedQuery = normalizeSearchText(query2);
25527
+ if (!normalizedQuery)
25528
+ return 0;
25529
+ const normalizedDescription = normalizeSearchText(candidate.description);
25530
+ const tokens = canonicalSearchTokens(normalizedQuery);
25531
+ const descriptionTokens = new Set(canonicalSearchTokens(normalizedDescription));
25532
+ let score = scoreIndexKey(normalizedQuery, candidate.key);
25533
+ if (tokens.length > 0 && normalizedDescription.includes(normalizedQuery))
25534
+ score += 30;
25535
+ let descriptionMatches = 0;
25536
+ for (const token of tokens) {
25537
+ if (descriptionTokens.has(token)) {
25538
+ score += 5;
25539
+ descriptionMatches += 1;
25540
+ }
25541
+ }
25542
+ if (tokens.length > 1 && descriptionMatches === tokens.length)
25543
+ score += 25;
25544
+ if (candidate.kind === "topic") {
25545
+ const requestsCurrent = /\b(?:current|active|ongoing|open)\b|\uD604\uC7AC|\uC9C4\uD589 \uC911/.test(normalizedQuery);
25546
+ const archivedCandidate = /\b(?:archive|archived|historical|closed)\b|\uBCF4\uAD00|\uC885\uB8CC/.test(`${normalizeSearchText(candidate.key)} ${normalizedDescription}`);
25547
+ if (requestsCurrent && archivedCandidate)
25548
+ score -= 40;
25549
+ }
25550
+ return score;
25551
+ }
25073
25552
  function wikiQuery(args) {
25074
- const question = safeExt(args, ["question", "query", "q", "text"], "");
25075
- const query2 = question.toLowerCase();
25553
+ const question = String(safeExt(args, ["question", "query", "q", "text"], "")).trim();
25554
+ const requestedKind = String(safeExt(args, ["kind", "type"], "all"));
25555
+ const kind = requestedKind === "topic" || requestedKind === "article" || requestedKind === "summary" ? requestedKind : "all";
25556
+ const requestedLimit = Number(safeExt(args, ["limit", "maxResults"], 8));
25557
+ const limit = Math.min(Math.max(Number.isFinite(requestedLimit) ? requestedLimit : 8, 1), 20);
25558
+ if (!question) {
25559
+ return {
25560
+ content: [{ type: "text", text: "A non-empty wiki query is required." }],
25561
+ isError: true
25562
+ };
25563
+ }
25564
+ const normalizedQuery = normalizeSearchText(question);
25565
+ if (!normalizedQuery) {
25566
+ return { content: [{ type: "text", text: "No matching wiki articles found." }] };
25567
+ }
25568
+ const canReadTopicMemory = runtime().host.canReadTopicMemory;
25569
+ 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;
25570
+ const visibleIndexCandidates = [
25571
+ ...parseWikiIndex(resolve17(runtime().wikiDir, "topic-index.md")),
25572
+ ...parseWikiIndex(resolve17(runtime().wikiDir, "article-index.md"))
25573
+ ].filter((candidate) => kind === "all" || candidate.kind === kind).filter((candidate) => candidate.kind !== "topic" || !canReadTopicMemory || canReadTopicMemory(candidate.key, runtime().userId));
25574
+ const hasStaleExactIndexMatch = visibleIndexCandidates.some((candidate) => !indexCandidateExists(candidate) && scoreIndexKey(question, candidate.key) >= 100);
25575
+ const indexCandidates = visibleIndexCandidates.filter(indexCandidateExists).map((candidate) => ({
25576
+ candidate,
25577
+ keyScore: scoreIndexKey(question, candidate.key),
25578
+ score: scoreIndexCandidate(question, candidate)
25579
+ })).filter(({ keyScore, score }) => keyScore >= 25 || score >= 40).sort((left, right) => right.score - left.score || left.candidate.key.localeCompare(right.candidate.key));
25580
+ const strongestIndex = indexCandidates[0];
25581
+ if (kind === "topic" && strongestIndex && (strongestIndex.keyScore >= 25 || strongestIndex.score >= 40)) {
25582
+ const runnerUp = indexCandidates[1];
25583
+ const ambiguous = strongestIndex.keyScore < 100 && runnerUp !== undefined && runnerUp.score >= strongestIndex.score * 0.9;
25584
+ if (ambiguous) {
25585
+ const lines2 = ["Found 2 ambiguous wiki candidates:", ""];
25586
+ for (const { candidate, score } of [strongestIndex, runnerUp]) {
25587
+ lines2.push(`- kind: ${candidate.kind}`, ` key: ${candidate.key}`, ` score: ${score.toFixed(2)}`, ` description: ${candidate.description || "(none)"}`, ...candidate.date ? [` date: ${candidate.date}`] : []);
25588
+ }
25589
+ return { content: [{ type: "text", text: lines2.join(`
25590
+ `) }] };
25591
+ }
25592
+ const lines = ["Found 1 wiki candidate:", ""];
25593
+ for (const { candidate, score } of [strongestIndex]) {
25594
+ lines.push(`- kind: ${candidate.kind}`, ` key: ${candidate.key}`, ` score: ${score.toFixed(2)}`, ` description: ${candidate.description || "(none)"}`, ...candidate.date ? [` date: ${candidate.date}`] : []);
25595
+ }
25596
+ return { content: [{ type: "text", text: lines.join(`
25597
+ `) }] };
25598
+ }
25076
25599
  const results = [];
25077
25600
  const scored = [];
25078
25601
  function scan(dir, label) {
@@ -25088,68 +25611,266 @@ function wikiQuery(args) {
25088
25611
  continue;
25089
25612
  const fp = join36(sub, f.name);
25090
25613
  try {
25091
- const text2 = readFileSync22(fp, "utf-8");
25614
+ const text2 = readFileSync23(fp, "utf-8");
25615
+ const key = `${entry.name}/${f.name.replace(/\.md$/i, "")}`;
25092
25616
  scored.push({
25093
- score: scoreMatch(text2),
25617
+ kind: label === "articles" ? "article" : "skill",
25618
+ key,
25619
+ keyScore: scoreIndexKey(question, key),
25620
+ score: scoreMatch(text2, key, label === "articles" ? "article" : "skill"),
25094
25621
  path: `${label}/${entry.name}/${f.name}`,
25095
- text: text2
25622
+ text: text2,
25623
+ title: titleFromDocument(text2, key)
25096
25624
  });
25097
25625
  } catch {}
25098
25626
  }
25099
25627
  } catch {}
25100
25628
  }
25101
25629
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
25630
+ if (label === "topic" && canReadTopicMemory && !canReadTopicMemory(entry.name.replace(/\.md$/i, ""), runtime().userId)) {
25631
+ continue;
25632
+ }
25102
25633
  const fp = join36(dir, entry.name);
25103
25634
  try {
25104
- const text2 = readFileSync22(fp, "utf-8");
25635
+ const text2 = readFileSync23(fp, "utf-8");
25636
+ const key = entry.name.replace(/\.md$/i, "");
25637
+ const resultKind = label === "articles" ? "article" : label === "topic" ? "topic" : label === "summaries" ? "summary" : "skill";
25105
25638
  scored.push({
25106
- score: scoreMatch(text2),
25639
+ kind: resultKind,
25640
+ key,
25641
+ keyScore: scoreIndexKey(question, key),
25642
+ score: scoreMatch(text2, key, resultKind),
25107
25643
  path: `${label}/${entry.name}`,
25108
- text: text2
25644
+ text: text2,
25645
+ title: titleFromDocument(text2, key),
25646
+ ...label === "summaries" && /^\d{4}-\d{2}-\d{2}/.test(key) ? { date: key.slice(0, 10) } : {}
25109
25647
  });
25110
25648
  } catch {}
25111
25649
  }
25112
25650
  }
25113
25651
  } catch {}
25114
25652
  }
25115
- function scoreMatch(text2) {
25116
- const lower = text2.toLowerCase();
25653
+ function titleFromDocument(text2, key) {
25654
+ const titleLine = text2.split(`
25655
+ `).find((line) => /^#+\s*/.test(line));
25656
+ return titleLine ? titleLine.replace(/^#+\s*/, "") : basename7(key);
25657
+ }
25658
+ function scoreMatch(text2, key, resultKind) {
25659
+ const titleLine = text2.split(`
25660
+ `).find((line) => /^#+\s*/.test(line)) ?? "";
25661
+ const title = titleLine.replace(/^#+\s*/, "");
25662
+ const normalizedKey = normalizeSearchText(key);
25663
+ const normalizedTitle2 = normalizeSearchText(title);
25664
+ const normalizedBody = normalizeSearchText(text2.replace(titleLine, ""));
25665
+ const compactQuery = compactSearchText(normalizedQuery);
25666
+ const compactKey = compactSearchText(normalizedKey);
25667
+ const compactTitle = compactSearchText(normalizedTitle2);
25668
+ const tokens = canonicalSearchTokens(normalizedQuery);
25669
+ const keyTokens = new Set(canonicalSearchTokens(normalizedKey));
25670
+ const titleTokens = new Set(canonicalSearchTokens(normalizedTitle2));
25671
+ const bodyTokens = canonicalSearchTokens(normalizedBody);
25117
25672
  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;
25673
+ let strongMetadataEvidence = false;
25674
+ if (normalizedKey === normalizedQuery) {
25675
+ score += 120;
25676
+ strongMetadataEvidence = true;
25677
+ } else if (compactKey === compactQuery) {
25678
+ score += 110;
25679
+ strongMetadataEvidence = true;
25680
+ } else if (compactQuery.length >= 2 && (compactKey.startsWith(compactQuery) || compactQuery.startsWith(compactKey))) {
25681
+ score += 65;
25682
+ strongMetadataEvidence = true;
25683
+ }
25684
+ if (tokens.length > 0 && compactQuery.length >= 3 && compactKey.includes(compactQuery)) {
25685
+ score += 45;
25686
+ strongMetadataEvidence = true;
25687
+ }
25688
+ const fuzzyKeyScore = fuzzySearchScore(compactQuery, compactKey, 55);
25689
+ score += fuzzyKeyScore;
25690
+ if (fuzzyKeyScore > 0)
25691
+ strongMetadataEvidence = true;
25692
+ if (normalizedTitle2 === normalizedQuery) {
25693
+ score += 115;
25694
+ strongMetadataEvidence = true;
25695
+ } else if (compactTitle === compactQuery) {
25696
+ score += 105;
25697
+ strongMetadataEvidence = true;
25698
+ } else if (compactQuery.length >= 2 && compactTitle.startsWith(compactQuery)) {
25699
+ score += 60;
25700
+ strongMetadataEvidence = true;
25701
+ }
25702
+ const fuzzyTitleScore = fuzzySearchScore(compactQuery, compactTitle, 50);
25703
+ score += fuzzyTitleScore;
25704
+ if (fuzzyTitleScore > 0)
25705
+ strongMetadataEvidence = true;
25706
+ if (resultKind === "summary") {
25707
+ const temporalScore = scoreSummaryTime(normalizedQuery, key, title);
25708
+ if (temporalScore === null)
25709
+ return 0;
25710
+ score += temporalScore;
25711
+ if (temporalScore > 0)
25712
+ strongMetadataEvidence = true;
25713
+ }
25714
+ const isMultiToken = tokens.length > 1;
25715
+ if (tokens.length > 0 && normalizedTitle2.includes(normalizedQuery)) {
25716
+ score += 35;
25717
+ strongMetadataEvidence = true;
25718
+ }
25719
+ const phraseMatches = isMultiToken ? countPhrase(normalizedBody, normalizedQuery) : 0;
25720
+ if (phraseMatches > 0)
25721
+ score += phraseMatches * 12;
25722
+ else if (tokens.length === 1 && tokens[0].length >= 5 && normalizedBody.includes(tokens[0])) {
25723
+ score += 16;
25724
+ }
25725
+ let bodyMatches = 0;
25726
+ let minimumBodyFrequency = Number.POSITIVE_INFINITY;
25727
+ for (const token of tokens) {
25728
+ if (keyTokens.has(token))
25729
+ score += 10;
25730
+ if (titleTokens.has(token))
25731
+ score += 12;
25732
+ const bodyFrequency = countToken(bodyTokens, token);
25733
+ if (bodyFrequency > 0) {
25734
+ score += Math.min(bodyFrequency, 3) * 4;
25735
+ bodyMatches += 1;
25736
+ minimumBodyFrequency = Math.min(minimumBodyFrequency, bodyFrequency);
25737
+ }
25738
+ }
25739
+ if (isMultiToken && bodyMatches === tokens.length) {
25740
+ score += 8 * Math.min(minimumBodyFrequency, 3);
25741
+ }
25742
+ const minimumBodyTokens = hasStaleExactIndexMatch ? 3 : 2;
25743
+ const partialArticleEvidence = resultKind === "article" && bodyMatches >= 3 && bodyMatches / tokens.length >= 0.6;
25744
+ const strongBodyEvidence = tokens.length >= minimumBodyTokens && (bodyMatches === tokens.length && (phraseMatches > 0 || minimumBodyFrequency >= 1) || partialArticleEvidence);
25745
+ if (!strongMetadataEvidence && !strongBodyEvidence)
25746
+ return 0;
25747
+ return score;
25748
+ }
25749
+ const bodyKinds = [
25750
+ ...kind === "all" || kind === "article" ? ["article"] : [],
25751
+ ...kind === "all" || kind === "summary" ? ["summary"] : []
25752
+ ];
25753
+ if (bodyKinds.length > 0) {
25754
+ const searchIndex = openWikiSearchIndex();
25755
+ if (searchIndex) {
25756
+ try {
25757
+ const terms = canonicalSearchTokens(normalizedQuery);
25758
+ const ids = searchIndex.matchingDocumentIds(bodyKinds, terms);
25759
+ if (temporalDirection && bodyKinds.includes("summary")) {
25760
+ for (const item of searchIndex.summaryMetadataByDate({
25761
+ direction: temporalDirection === "newest" ? "latest" : "oldest",
25762
+ limit
25763
+ })) {
25764
+ ids.add(item.id);
25765
+ }
25766
+ }
25767
+ for (const document of searchIndex.documents(ids)) {
25768
+ scored.push({
25769
+ kind: document.kind,
25770
+ key: document.key,
25771
+ keyScore: scoreIndexKey(question, document.key),
25772
+ score: scoreMatch(document.text, document.key, document.kind),
25773
+ path: `${document.kind === "article" ? "articles" : "summaries"}/${document.key}.md`,
25774
+ text: document.text,
25775
+ title: document.title,
25776
+ ...document.date ? { date: document.date } : {}
25777
+ });
25778
+ }
25779
+ } catch {} finally {
25780
+ searchIndex.close();
25126
25781
  }
25127
25782
  }
25128
- return score;
25129
25783
  }
25130
- scan(runtime().articlesDir, "articles");
25131
- if (runtime().surface === "all")
25784
+ if (kind === "all" && runtime().surface === "all")
25132
25785
  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) {
25786
+ if (kind === "all" || kind === "topic")
25787
+ scan(runtime().topicsDir, "topic");
25788
+ const merged = new Map;
25789
+ for (const { candidate, keyScore, score } of indexCandidates) {
25790
+ merged.set(`${candidate.kind}:${candidate.key}`, {
25791
+ kind: candidate.kind,
25792
+ key: candidate.key,
25793
+ keyScore,
25794
+ score,
25795
+ description: candidate.description,
25796
+ date: candidate.date
25797
+ });
25798
+ }
25799
+ const documentThreshold = kind === "topic" ? 24 : 16;
25800
+ for (const document of scored) {
25801
+ if (document.score < documentThreshold)
25802
+ continue;
25803
+ const id = `${document.kind}:${document.key}`;
25804
+ const indexed = merged.get(id);
25805
+ merged.set(id, indexed ? { ...indexed, ...document, score: Math.max(indexed.score, document.score) } : document);
25806
+ }
25807
+ 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));
25808
+ if (kind === "summary") {
25809
+ const sameTopic = ranked.filter((result) => {
25810
+ const identity = summaryTopicIdentity(result.key);
25811
+ return identity.length >= 3 && normalizedQuery.includes(identity);
25812
+ });
25813
+ if (sameTopic.length > 0)
25814
+ ranked = sameTopic;
25815
+ const requestedDateMatch = normalizedQuery.match(/\b(\d{4}) (\d{2}) (\d{2})\b/);
25816
+ const requestedDate = requestedDateMatch ? `${requestedDateMatch[1]}-${requestedDateMatch[2]}-${requestedDateMatch[3]}` : undefined;
25817
+ const beforeDate = /\bbefore\b/.test(normalizedQuery);
25818
+ const afterDate = /\bafter\b/.test(normalizedQuery);
25819
+ if (requestedDate && beforeDate) {
25820
+ ranked = ranked.filter((result) => !result.date || result.date < requestedDate);
25821
+ } else if (requestedDate && afterDate) {
25822
+ ranked = ranked.filter((result) => !result.date || result.date > requestedDate);
25823
+ }
25824
+ ranked.sort((left, right) => {
25825
+ if (beforeDate) {
25826
+ return (right.date ?? "").localeCompare(left.date ?? "") || right.score - left.score;
25827
+ }
25828
+ if (afterDate) {
25829
+ return (left.date ?? "").localeCompare(right.date ?? "") || right.score - left.score;
25830
+ }
25831
+ if (requestedDate) {
25832
+ const dateDifference = Number(right.date === requestedDate) - Number(left.date === requestedDate);
25833
+ if (dateDifference !== 0)
25834
+ return dateDifference;
25835
+ }
25836
+ if (temporalDirection) {
25837
+ const dateDifference = temporalDirection === "oldest" ? (left.date ?? "").localeCompare(right.date ?? "") : (right.date ?? "").localeCompare(left.date ?? "");
25838
+ if (dateDifference !== 0)
25839
+ return dateDifference;
25840
+ }
25841
+ return right.score - left.score || (right.date ?? "").localeCompare(left.date ?? "") || left.key.localeCompare(right.key);
25842
+ });
25843
+ }
25844
+ if (kind === "topic" && ranked[0]) {
25845
+ const runnerUp = ranked[1];
25846
+ const ambiguous = ranked[0].keyScore < 100 && runnerUp !== undefined && runnerUp.score >= ranked[0].score * 0.9;
25847
+ if (ambiguous) {
25848
+ ranked = ranked.slice(0, 2);
25849
+ } else {
25850
+ ranked = ranked.slice(0, 1);
25851
+ }
25852
+ } else {
25853
+ ranked = ranked.slice(0, limit);
25854
+ }
25855
+ if (ranked.length === 0) {
25138
25856
  results.push("No matching wiki articles found.");
25139
25857
  } else {
25140
- results.push(`Found ${top.length} matching result(s):
25858
+ results.push(`Found ${ranked.length} matching result(s):
25141
25859
  `);
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 ? `
25860
+ for (const result of ranked) {
25861
+ if (result.kind === "skill") {
25862
+ const body = (result.text ?? "").trim().slice(0, 400);
25863
+ results.push(`## ${result.title ?? result.key}
25864
+ Path: ${result.path}
25865
+
25866
+ ${body}${(result.text?.length ?? 0) > 400 ? `
25151
25867
  ...(truncated)` : ""}
25152
25868
  `);
25869
+ continue;
25870
+ }
25871
+ 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 ? [
25872
+ ` excerpt: ${result.text.replace(/^---[\s\S]*?---\n?/, "").replace(/^#.*$/m, "").trim().slice(0, 400)}`
25873
+ ] : []);
25153
25874
  }
25154
25875
  }
25155
25876
  return { content: [{ type: "text", text: results.join(`
@@ -25168,36 +25889,94 @@ function wikiTopicBrief(args) {
25168
25889
  };
25169
25890
  }
25170
25891
  const { getTopicBrief: getTopicBrief2, resolveTopicBrief: resolveTopicBrief2 } = runtime().host;
25171
- if (runtime().topicId && (getTopicBrief2 || resolveTopicBrief2)) {
25892
+ const hasAuthorizedBridge = Boolean(runtime().topicId && (getTopicBrief2 || resolveTopicBrief2));
25893
+ if (hasAuthorizedBridge) {
25172
25894
  try {
25173
25895
  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(`
25896
+ if (brief) {
25897
+ const lines = [
25898
+ `# Topic Brief: ${topicNameFrom(rawTopic)}`,
25899
+ "",
25900
+ brief.briefMd || "(empty brief)"
25901
+ ];
25902
+ if (brief.latestSummaryMd) {
25903
+ lines.push("", "## Latest Summary", "", brief.latestSummaryMd);
25904
+ }
25905
+ if (brief.summaryDate) {
25906
+ lines.push("", `Summary date: ${brief.summaryDate}`);
25907
+ }
25908
+ return { content: [{ type: "text", text: lines.join(`
25191
25909
  `) }] };
25192
- } catch {
25910
+ }
25911
+ } catch {}
25912
+ return {
25913
+ content: [{ type: "text", text: `No brief found for topic: ${rawTopic}` }]
25914
+ };
25915
+ }
25916
+ const topicKey = wikiSummarySlug(rawTopic.replace(/^topic\//, "").replace(/\.md$/i, ""));
25917
+ const filePath = resolve17(runtime().topicsDir, `${topicKey}.md`);
25918
+ try {
25919
+ const content = readFileSync23(filePath, "utf-8");
25920
+ return {
25921
+ content: [{ type: "text", text: `# Topic Brief: ${topicKey}
25922
+
25923
+ ${content}` }]
25924
+ };
25925
+ } catch {}
25926
+ return {
25927
+ content: [{ type: "text", text: `No brief found for topic: ${rawTopic}` }]
25928
+ };
25929
+ }
25930
+ function wikiRead(args) {
25931
+ const rawKind = String(safeExt(args, ["kind", "type"], "article"));
25932
+ const kind = rawKind === "topic" || rawKind === "summary" ? rawKind : "article";
25933
+ const rawKey = String(safeExt(args, ["key", "slug", "topic"], "")).trim();
25934
+ if (!rawKey) {
25935
+ return { content: [{ type: "text", text: "A wiki document key is required." }], isError: true };
25936
+ }
25937
+ if (kind === "topic") {
25938
+ const result = wikiTopicBrief({ topic: rawKey });
25939
+ const resultText = result.content.map((entry) => entry.type === "text" ? entry.text : "").join(`
25940
+ `);
25941
+ const adopt = safeExt(args, ["adopt", "use", "select"], false);
25942
+ if (!adopt || resultText.startsWith("No brief found for topic"))
25943
+ return result;
25944
+ const currentTopicId = runtime().currentTopicId;
25945
+ const memoryKey = wikiSummarySlug(rawKey.replace(/^topic\//, "").replace(/\.md$/i, ""));
25946
+ if (!currentTopicId || !runtime().host.adoptTopicMemory?.(currentTopicId, runtime().userId, memoryKey)) {
25193
25947
  return {
25194
- content: [{ type: "text", text: `Could not read topic brief for: ${rawTopic}` }]
25948
+ content: [
25949
+ ...result.content,
25950
+ { type: "text", text: `
25951
+ Memory was read but could not be adopted by this topic.` }
25952
+ ]
25195
25953
  };
25196
25954
  }
25955
+ return {
25956
+ content: [...result.content, { type: "text", text: `
25957
+ Adopted topic memory: ${memoryKey}` }]
25958
+ };
25959
+ }
25960
+ if (rawKey.includes("\\") || rawKey.split("/").some((part) => part === "..") || rawKey.startsWith("/")) {
25961
+ return { content: [{ type: "text", text: "Invalid wiki document key." }], isError: true };
25962
+ }
25963
+ const root = kind === "summary" ? runtime().summariesDir : runtime().articlesDir;
25964
+ const key = rawKey.replace(new RegExp(`^(?:${kind === "summary" ? "summaries" : "articles"})/`), "").replace(/\.md$/i, "");
25965
+ const filePath = resolve17(root, `${key}.md`);
25966
+ const relativePath = relative2(root, filePath);
25967
+ if (relativePath.startsWith("..") || relativePath.startsWith("/")) {
25968
+ return { content: [{ type: "text", text: "Invalid wiki document key." }], isError: true };
25969
+ }
25970
+ try {
25971
+ const content = readFileSync23(filePath, "utf-8");
25972
+ return {
25973
+ content: [{ type: "text", text: `# Wiki ${kind}: ${key}
25974
+
25975
+ ${content}` }]
25976
+ };
25977
+ } catch {
25978
+ return { content: [{ type: "text", text: `No ${kind} found for: ${rawKey}` }] };
25197
25979
  }
25198
- return {
25199
- content: [{ type: "text", text: `No topic brief found for: ${rawTopic}` }]
25200
- };
25201
25980
  }
25202
25981
  function wikiLastConversation(args) {
25203
25982
  const rawTopic = safeExt(args, ["topic", "topicName", "topicId"], runtime().topicId);
@@ -25209,18 +25988,18 @@ function wikiLastConversation(args) {
25209
25988
  const name = slugify(topicNameFrom(rawTopic));
25210
25989
  const archiveDir = resolve17(runtime().archiveDir, name);
25211
25990
  try {
25212
- let files = existsSync26(archiveDir) ? readdirSync10(archiveDir).filter((f) => f.endsWith(".jsonl")).sort().reverse() : [];
25991
+ let files = existsSync27(archiveDir) ? readdirSync10(archiveDir).filter((f) => f.endsWith(".jsonl")).sort().reverse() : [];
25213
25992
  if (files.length === 0) {
25214
- files = existsSync26(runtime().archiveDir) ? readdirSync10(runtime().archiveDir).filter((f) => f.startsWith(name) && f.endsWith(".jsonl")).sort().reverse() : [];
25993
+ files = existsSync27(runtime().archiveDir) ? readdirSync10(runtime().archiveDir).filter((f) => f.startsWith(name) && f.endsWith(".jsonl")).sort().reverse() : [];
25215
25994
  }
25216
25995
  if (files.length === 0) {
25217
25996
  return {
25218
25997
  content: [{ type: "text", text: `No archive found for topic: ${rawTopic}` }]
25219
25998
  };
25220
25999
  }
25221
- const actualPath = files[0].includes("/") ? files[0] : resolve17(existsSync26(archiveDir) && readdirSync10(archiveDir).includes(files[0]) ? archiveDir : runtime().archiveDir, files[0]);
26000
+ const actualPath = files[0].includes("/") ? files[0] : resolve17(existsSync27(archiveDir) && readdirSync10(archiveDir).includes(files[0]) ? archiveDir : runtime().archiveDir, files[0]);
25222
26001
  try {
25223
- const raw = readFileSync22(actualPath, "utf-8");
26002
+ const raw = readFileSync23(actualPath, "utf-8");
25224
26003
  const lines = raw.trim().split(`
25225
26004
  `).filter(Boolean);
25226
26005
  const turns2 = [];
@@ -25293,7 +26072,7 @@ function skillQuery(args) {
25293
26072
  continue;
25294
26073
  const skillFile = resolve17(runtime().skillsDir, entry.name, "skill.md");
25295
26074
  try {
25296
- const text2 = readFileSync22(skillFile, "utf-8");
26075
+ const text2 = readFileSync23(skillFile, "utf-8");
25297
26076
  const lower = text2.toLowerCase();
25298
26077
  let score = 0;
25299
26078
  for (const w of query2.split(/\s+/).filter(Boolean)) {
@@ -25364,7 +26143,7 @@ ${fresh.join(`
25364
26143
  const skillPath = resolve17(skillDir, "skill.md");
25365
26144
  let finalContent = content;
25366
26145
  try {
25367
- const existing = readFileSync22(skillPath, "utf-8");
26146
+ const existing = readFileSync23(skillPath, "utf-8");
25368
26147
  finalContent = mergeGotchas(content, extractGotchas(existing));
25369
26148
  } catch {}
25370
26149
  writeFileSync17(skillPath, finalContent, "utf-8");
@@ -25377,15 +26156,7 @@ ${fresh.join(`
25377
26156
  ]
25378
26157
  };
25379
26158
  }
25380
- function saveWikiEntry(args) {
25381
- const rawTopic = safeExt(args, ["topic", "topicName"], "");
25382
- const content = safeExt(args, ["content", "text", "body"], "");
25383
- if (!rawTopic)
25384
- return { content: [{ type: "text", text: "Missing topic." }] };
25385
- if (!content)
25386
- return { content: [{ type: "text", text: "Missing content." }] };
25387
- const now = new Date;
25388
- const dateStr = now.toISOString().slice(0, 10);
26159
+ function writeSummaryDocument(rawTopic, content, dateStr) {
25389
26160
  const topicId = runtime().topicId;
25390
26161
  const fileSlug = wikiBriefStorageKey(rawTopic, topicId);
25391
26162
  ensureDir(runtime().summariesDir);
@@ -25412,7 +26183,7 @@ function saveWikiEntry(args) {
25412
26183
  if (topicId && setTopicBrief2) {
25413
26184
  try {
25414
26185
  const briefPath = resolve17(runtime().topicsDir, `${fileSlug}.md`);
25415
- let briefMd = existsSync26(briefPath) ? readFileSync22(briefPath, "utf-8") : undefined;
26186
+ let briefMd = existsSync27(briefPath) ? readFileSync23(briefPath, "utf-8") : undefined;
25416
26187
  if (briefMd === undefined && getTopicBrief2 && !getTopicBrief2(fileSlug)?.briefMd) {
25417
26188
  const legacy = getTopicBrief2(topicId)?.briefMd;
25418
26189
  if (legacy)
@@ -25426,28 +26197,14 @@ function saveWikiEntry(args) {
25426
26197
  sqliteUpdated = true;
25427
26198
  } catch {}
25428
26199
  }
25429
- return {
25430
- content: [
25431
- {
25432
- type: "text",
25433
- text: `Saved summary: summaries/${summaryName}` + (sqliteUpdated ? `
25434
- SQLite latest-summary also updated.` : "")
25435
- }
25436
- ]
25437
- };
26200
+ return { slug: summaryName.replace(/\.md$/i, ""), sqliteUpdated };
25438
26201
  }
25439
- function saveTopicBrief(args) {
25440
- const rawTopic = safeExt(args, ["topic", "topicName"], "");
25441
- const content = safeExt(args, ["content", "brief", "text", "body"], "");
25442
- if (!rawTopic)
25443
- return { content: [{ type: "text", text: "Missing topic." }] };
25444
- if (!content)
25445
- return { content: [{ type: "text", text: "Missing content." }] };
26202
+ function writeTopicDocument(rawTopic, content) {
25446
26203
  const topicId = runtime().topicId;
25447
26204
  const fileSlug = wikiBriefStorageKey(rawTopic, topicId);
25448
26205
  const briefPath = resolve17(runtime().topicsDir, `${fileSlug}.md`);
25449
26206
  ensureDir(dirname16(briefPath));
25450
- const existed = existsSync26(briefPath);
26207
+ const existed = existsSync27(briefPath);
25451
26208
  writeFileSync17(briefPath, content, "utf-8");
25452
26209
  const setTopicBrief2 = runtime().host.setTopicBrief;
25453
26210
  let sqliteUpdated = false;
@@ -25457,68 +26214,227 @@ function saveTopicBrief(args) {
25457
26214
  sqliteUpdated = true;
25458
26215
  } catch {}
25459
26216
  }
26217
+ return { slug: fileSlug, existed, sqliteUpdated };
26218
+ }
26219
+ function writeArticleDocument(slug, content) {
26220
+ const articlePath = resolve17(runtime().articlesDir, `${slug}.md`);
26221
+ ensureDir(dirname16(articlePath));
26222
+ const existed = existsSync27(articlePath);
26223
+ atomicWriteFile(articlePath, content);
26224
+ return { existed };
26225
+ }
26226
+ function wikiWrite(args) {
26227
+ const rawKind = safeExt(args, ["kind", "type"], "");
26228
+ const content = safeExt(args, ["content", "text", "body", "brief"], "");
26229
+ const rawDescription = safeExt(args, ["description", "desc"], "");
26230
+ const rawTopic = safeExt(args, ["topic", "topicName"], "");
26231
+ const rawSlug = safeExt(args, ["slug", "id"], "");
26232
+ const section = safeExt(args, ["section", "category"], undefined);
26233
+ const rawDate = safeExt(args, ["date", "created"], undefined);
26234
+ if (rawKind !== "summary" && rawKind !== "article" && rawKind !== "topic") {
26235
+ return {
26236
+ content: [{ type: "text", text: 'kind must be one of "summary", "article", "topic".' }],
26237
+ isError: true
26238
+ };
26239
+ }
26240
+ if (!content.trim()) {
26241
+ return { content: [{ type: "text", text: "Missing content." }], isError: true };
26242
+ }
26243
+ const descriptionResult = normalizeIndexDescription(rawDescription);
26244
+ if ("error" in descriptionResult) {
26245
+ return { content: [{ type: "text", text: descriptionResult.error }], isError: true };
26246
+ }
26247
+ const { description } = descriptionResult;
26248
+ const today = new Date().toISOString().slice(0, 10);
26249
+ if (rawKind === "article") {
26250
+ const slugResult = normalizeWikiSlug(rawSlug);
26251
+ if ("error" in slugResult) {
26252
+ return { content: [{ type: "text", text: slugResult.error }], isError: true };
26253
+ }
26254
+ if (!section?.trim()) {
26255
+ return {
26256
+ content: [
26257
+ { type: "text", text: "An article requires a section so the catalog stays navigable." }
26258
+ ],
26259
+ isError: true
26260
+ };
26261
+ }
26262
+ const { existed } = writeArticleDocument(slugResult.slug, content);
26263
+ const link2 = indexRowOrPartialWrite("article", {
26264
+ kind: "article",
26265
+ slug: slugResult.slug,
26266
+ description,
26267
+ date: rawDate ?? today,
26268
+ section: section.trim()
26269
+ });
26270
+ if (typeof link2 !== "string")
26271
+ return link2;
26272
+ const searched = indexWikiSearchDocument("article", slugResult.slug, content);
26273
+ return {
26274
+ content: [
26275
+ {
26276
+ type: "text",
26277
+ text: `Saved article: articles/${slugResult.slug}.md (${existed ? "updated" : "created"})
26278
+ Indexed: ${link2}` + (searched ? "" : `
26279
+ ${SEARCH_CACHE_SKIPPED_NOTE}`)
26280
+ }
26281
+ ]
26282
+ };
26283
+ }
26284
+ const topic = rawTopic.trim();
26285
+ if (!topic) {
26286
+ return { content: [{ type: "text", text: "Missing topic." }], isError: true };
26287
+ }
26288
+ if (rawKind === "summary") {
26289
+ const dateStr = rawDate ?? today;
26290
+ const written2 = writeSummaryDocument(topic, content, dateStr);
26291
+ const link2 = indexRowOrPartialWrite("summary", {
26292
+ kind: "summary",
26293
+ slug: written2.slug,
26294
+ description,
26295
+ date: dateStr
26296
+ });
26297
+ if (typeof link2 !== "string")
26298
+ return link2;
26299
+ const searched = indexWikiSearchDocument("summary", written2.slug, content);
26300
+ return {
26301
+ content: [
26302
+ {
26303
+ type: "text",
26304
+ text: `Saved summary: summaries/${written2.slug}.md
26305
+ Indexed: ${link2}` + (written2.sqliteUpdated ? `
26306
+ SQLite latest-summary also updated.` : "") + (searched ? "" : `
26307
+ ${SEARCH_CACHE_SKIPPED_NOTE}`)
26308
+ }
26309
+ ]
26310
+ };
26311
+ }
26312
+ const written = writeTopicDocument(topic, content);
26313
+ const link = indexRowOrPartialWrite("topic", {
26314
+ kind: "topic",
26315
+ slug: written.slug,
26316
+ description,
26317
+ date: rawDate ?? today
26318
+ });
26319
+ if (typeof link !== "string")
26320
+ return link;
25460
26321
  return {
25461
26322
  content: [
25462
26323
  {
25463
26324
  type: "text",
25464
- text: `Saved brief: topic/${fileSlug}.md (${existed ? "updated" : "created"})` + (sqliteUpdated ? `
26325
+ text: `Saved brief: topic/${written.slug}.md (${written.existed ? "updated" : "created"})
26326
+ Indexed: ${link}` + (written.sqliteUpdated ? `
25465
26327
  SQLite brief also updated.` : "")
25466
26328
  }
25467
26329
  ]
25468
26330
  };
25469
26331
  }
25470
- function indexUpsert(args) {
25471
- const slug = safeExt(args, ["slug", "id"], "");
25472
- const desc = safeExt(args, ["description", "desc", "text"], "");
25473
- const kind = safeExt(args, ["kind", "type"], "article");
25474
- const section = safeExt(args, ["section", "category"], undefined);
25475
- const date = safeExt(args, ["date", "created"], undefined);
25476
- if (!slug)
25477
- return { content: [{ type: "text", text: "Missing slug." }] };
25478
- const today = new Date().toISOString().slice(0, 10);
25479
- const dateStr = date ?? today;
25480
- const indexPath = (() => {
25481
- if (kind === "topic")
25482
- return resolve17(runtime().wikiDir, "topic-index.md");
25483
- if (kind === "skill")
25484
- return resolve17(runtime().wikiDir, "skill-index.md");
25485
- return resolve17(runtime().wikiDir, "article-index.md");
25486
- })();
26332
+ function collectIndexableDocuments() {
26333
+ const sources = [];
26334
+ function add(kind, key, path) {
26335
+ try {
26336
+ const stat = statSync15(path);
26337
+ if (!stat.isFile())
26338
+ return;
26339
+ sources.push({ kind, key, path, mtimeMs: stat.mtimeMs, size: stat.size });
26340
+ } catch {}
26341
+ }
26342
+ for (const [root, kind, allowSubdirectories] of [
26343
+ [runtime().articlesDir, "article", true],
26344
+ [runtime().summariesDir, "summary", false]
26345
+ ]) {
26346
+ let entries;
26347
+ try {
26348
+ entries = readdirSync10(root, { withFileTypes: true });
26349
+ } catch {
26350
+ continue;
26351
+ }
26352
+ for (const entry of entries) {
26353
+ if (entry.isFile() && entry.name.endsWith(".md")) {
26354
+ add(kind, entry.name.replace(/\.md$/i, ""), join36(root, entry.name));
26355
+ continue;
26356
+ }
26357
+ if (!entry.isDirectory() || !allowSubdirectories)
26358
+ continue;
26359
+ let nested;
26360
+ try {
26361
+ nested = readdirSync10(join36(root, entry.name), { withFileTypes: true });
26362
+ } catch {
26363
+ continue;
26364
+ }
26365
+ for (const file of nested) {
26366
+ if (!file.isFile() || !file.name.endsWith(".md"))
26367
+ continue;
26368
+ add(kind, `${entry.name}/${file.name.replace(/\.md$/i, "")}`, join36(root, entry.name, file.name));
26369
+ }
26370
+ }
26371
+ }
26372
+ return sources;
26373
+ }
26374
+ function wikiReindex() {
26375
+ const documents = collectIndexableDocuments();
26376
+ const index = openWikiSearchIndex();
26377
+ if (!index) {
26378
+ return {
26379
+ content: [{ type: "text", text: "Could not open the wiki search index." }],
26380
+ isError: true
26381
+ };
26382
+ }
26383
+ let synced;
26384
+ try {
26385
+ synced = index.sync(documents, (source, text2) => prepareWikiSearchDocument(source.kind, source.key, text2));
26386
+ } catch (error2) {
26387
+ return {
26388
+ content: [{ type: "text", text: `Failed to rebuild the search index: ${String(error2)}` }],
26389
+ isError: true
26390
+ };
26391
+ } finally {
26392
+ index.close();
26393
+ }
26394
+ const lines = [
26395
+ `Search index: ${synced.added} added, ${synced.updated} updated, ${synced.removed} removed (${documents.length} documents).`
26396
+ ];
26397
+ for (const kind of ["article", "summary"]) {
26398
+ const rows = new Set(parseWikiIndex(wikiIndexPath(kind)).filter((candidate) => candidate.kind === kind).map((candidate) => candidate.key));
26399
+ const keys = documents.filter((source) => source.kind === kind).map((source) => source.key);
26400
+ const unindexed = keys.filter((key) => !rows.has(key));
26401
+ const tombstones = [...rows].filter((key) => !keys.includes(key));
26402
+ lines.push(`${WIKI_INDEX_FILENAMES[kind]}: ${rows.size} rows for ${keys.length} documents` + (unindexed.length > 0 ? `; missing rows: ${unindexed.join(", ")}` : "") + (tombstones.length > 0 ? `; tombstones: ${tombstones.join(", ")}` : ""));
26403
+ }
26404
+ return { content: [{ type: "text", text: lines.join(`
26405
+ `) }] };
26406
+ }
26407
+ function indexRowOrPartialWrite(label, options) {
26408
+ try {
26409
+ return upsertIndexRow(options);
26410
+ } catch (error2) {
26411
+ return {
26412
+ content: [
26413
+ {
26414
+ type: "text",
26415
+ text: `Wrote the ${label} document but failed to index it (${String(error2)}). Retry the same wiki_write call: both writes are idempotent.`
26416
+ }
26417
+ ],
26418
+ isError: true
26419
+ };
26420
+ }
26421
+ }
26422
+ function upsertIndexRow(options) {
26423
+ const { kind, slug, description, date, section } = options;
26424
+ const indexPath = wikiIndexPath(kind);
25487
26425
  ensureDir(dirname16(indexPath));
25488
26426
  const releaseLock = acquireFileLock(indexPath);
25489
26427
  try {
25490
26428
  let index;
25491
26429
  try {
25492
- index = readFileSync22(indexPath, "utf-8");
26430
+ index = readFileSync23(indexPath, "utf-8");
25493
26431
  } catch {
25494
26432
  index = "";
25495
26433
  }
25496
26434
  const lines = index.split(`
25497
26435
  `);
25498
- const link = (() => {
25499
- switch (kind) {
25500
- case "summary":
25501
- return `- [[summaries/${slug}]] ${desc} (${dateStr})`;
25502
- case "topic":
25503
- return `- [[topic/${slug}]] ${desc} (${dateStr})`;
25504
- case "skill":
25505
- return `- [[skills/${slug}]] ${desc} (${dateStr})`;
25506
- default:
25507
- return `- [[articles/${slug}]] ${desc} (${dateStr})`;
25508
- }
25509
- })();
25510
- const target = (() => {
25511
- switch (kind) {
25512
- case "summary":
25513
- return `summaries/${slug}`;
25514
- case "topic":
25515
- return `topic/${slug}`;
25516
- case "skill":
25517
- return `skills/${slug}`;
25518
- default:
25519
- return `articles/${slug}`;
25520
- }
25521
- })();
26436
+ const target = `${WIKI_INDEX_NAMESPACES[kind]}/${slug}`;
26437
+ const link = `- [[${target}]] ${description} (${date})`;
25522
26438
  const slugPattern = `[[${target}]]`;
25523
26439
  let replaced = false;
25524
26440
  for (let i = lines.length - 1;i >= 0; i--) {
@@ -25556,16 +26472,69 @@ function indexUpsert(args) {
25556
26472
  }
25557
26473
  atomicWriteFile(indexPath, lines.join(`
25558
26474
  `));
25559
- return { content: [{ type: "text", text: `Index updated: ${link}` }] };
26475
+ return link;
25560
26476
  } finally {
25561
26477
  releaseLock();
25562
26478
  }
25563
26479
  }
26480
+ function wikiDocumentExists(kind, slug) {
26481
+ const root = kind === "article" ? runtime().articlesDir : kind === "summary" ? runtime().summariesDir : kind === "topic" ? runtime().topicsDir : runtime().skillsDir;
26482
+ const filePath = resolve17(root, `${slug}.md`);
26483
+ if (relative2(root, filePath).startsWith(".."))
26484
+ return false;
26485
+ try {
26486
+ return statSync15(filePath).isFile();
26487
+ } catch {
26488
+ return false;
26489
+ }
26490
+ }
26491
+ function indexUpsert(args) {
26492
+ const rawSlug = safeExt(args, ["slug", "id"], "");
26493
+ const rawDescription = safeExt(args, ["description", "desc", "text"], "");
26494
+ const rawKind = safeExt(args, ["kind", "type"], "article");
26495
+ const section = safeExt(args, ["section", "category"], undefined);
26496
+ const date = safeExt(args, ["date", "created"], undefined);
26497
+ if (!(rawKind in WIKI_INDEX_FILENAMES)) {
26498
+ return {
26499
+ content: [{ type: "text", text: `Unsupported kind: ${rawKind}` }],
26500
+ isError: true
26501
+ };
26502
+ }
26503
+ const kind = rawKind;
26504
+ const slugResult = normalizeWikiSlug(rawSlug);
26505
+ if ("error" in slugResult) {
26506
+ return { content: [{ type: "text", text: slugResult.error }], isError: true };
26507
+ }
26508
+ const descriptionResult = normalizeIndexDescription(rawDescription);
26509
+ if ("error" in descriptionResult) {
26510
+ return { content: [{ type: "text", text: descriptionResult.error }], isError: true };
26511
+ }
26512
+ if (kind !== "topic" && !wikiDocumentExists(kind, slugResult.slug)) {
26513
+ return {
26514
+ content: [
26515
+ {
26516
+ type: "text",
26517
+ text: `No ${kind} document named ${slugResult.slug}. Use wiki_write to create the document and its catalog row together.`
26518
+ }
26519
+ ],
26520
+ isError: true
26521
+ };
26522
+ }
26523
+ const link = upsertIndexRow({
26524
+ kind,
26525
+ slug: slugResult.slug,
26526
+ description: descriptionResult.description,
26527
+ date: date ?? new Date().toISOString().slice(0, 10),
26528
+ section
26529
+ });
26530
+ return { content: [{ type: "text", text: `Index updated: ${link}` }] };
26531
+ }
25564
26532
  function createWikiMcpServer(context, host2) {
25565
26533
  const surface = context.surface ?? "all";
25566
26534
  const wikiDir = resolve17(host2.wikiRoot);
25567
26535
  const current3 = {
25568
26536
  userId: context.userId,
26537
+ currentTopicId: context.currentTopicId ?? context.topicId,
25569
26538
  topicId: context.topicId,
25570
26539
  surface,
25571
26540
  host: host2,
@@ -25595,12 +26564,13 @@ function createWikiMcpServer(context, host2) {
25595
26564
  const { name, arguments: args } = request.params;
25596
26565
  const handlers = {
25597
26566
  wiki_query: wikiQuery,
26567
+ wiki_read: wikiRead,
25598
26568
  wiki_topic_brief: wikiTopicBrief,
25599
26569
  wiki_last_conversation: wikiLastConversation,
25600
26570
  skill_query: skillQuery,
25601
26571
  skill_save: skillSave,
25602
- save_wiki_entry: saveWikiEntry,
25603
- save_topic_brief: saveTopicBrief,
26572
+ wiki_write: wikiWrite,
26573
+ wiki_reindex: wikiReindex,
25604
26574
  index_upsert: indexUpsert
25605
26575
  };
25606
26576
  const handler = tools.some((tool) => tool.name === name) ? handlers[name] : undefined;
@@ -25609,16 +26579,100 @@ function createWikiMcpServer(context, host2) {
25609
26579
  });
25610
26580
  return server;
25611
26581
  }
25612
- var wikiRuntime, FILE_LOCK_WAIT_MS = 10, FILE_LOCK_TIMEOUT_MS = 5000, FILE_LOCK_STALE_MS = 30000, lockWaitArray, WIKI_TOOLS, SKILL_TOOLS;
25613
- var init_wiki_server = __esm(() => {
26582
+ 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_SEARCH_INDEX_FILENAME = ".wiki-search-index.sqlite", WIKI_INDEX_FILENAMES, WIKI_INDEX_NAMESPACES, MAX_INDEX_DESCRIPTION_LENGTH = 400, SEARCH_CACHE_SKIPPED_NOTE = "Body-search cache was not updated; catalog retrieval still works. Run wiki_reindex to refill it.", WIKI_TOOLS, SKILL_TOOLS;
26583
+ var init_wiki_server = __esm(async () => {
25614
26584
  init_config();
25615
26585
  init_wiki_summary_names();
26586
+ await init_wiki_search_index();
25616
26587
  wikiRuntime = new AsyncLocalStorage3;
25617
26588
  lockWaitArray = new Int32Array(new SharedArrayBuffer(4));
26589
+ SEARCH_STOP_WORDS = new Set([
26590
+ "a",
26591
+ "an",
26592
+ "and",
26593
+ "are",
26594
+ "as",
26595
+ "at",
26596
+ "be",
26597
+ "by",
26598
+ "for",
26599
+ "from",
26600
+ "in",
26601
+ "is",
26602
+ "it",
26603
+ "of",
26604
+ "on",
26605
+ "or",
26606
+ "that",
26607
+ "the",
26608
+ "to",
26609
+ "with"
26610
+ ]);
26611
+ SEARCH_MONTHS = new Map([
26612
+ ["january", "01"],
26613
+ ["jan", "01"],
26614
+ ["1\uC6D4", "01"],
26615
+ ["february", "02"],
26616
+ ["feb", "02"],
26617
+ ["2\uC6D4", "02"],
26618
+ ["march", "03"],
26619
+ ["mar", "03"],
26620
+ ["3\uC6D4", "03"],
26621
+ ["april", "04"],
26622
+ ["apr", "04"],
26623
+ ["4\uC6D4", "04"],
26624
+ ["may", "05"],
26625
+ ["5\uC6D4", "05"],
26626
+ ["june", "06"],
26627
+ ["jun", "06"],
26628
+ ["6\uC6D4", "06"],
26629
+ ["july", "07"],
26630
+ ["jul", "07"],
26631
+ ["7\uC6D4", "07"],
26632
+ ["august", "08"],
26633
+ ["aug", "08"],
26634
+ ["8\uC6D4", "08"],
26635
+ ["september", "09"],
26636
+ ["sep", "09"],
26637
+ ["sept", "09"],
26638
+ ["9\uC6D4", "09"],
26639
+ ["october", "10"],
26640
+ ["oct", "10"],
26641
+ ["10\uC6D4", "10"],
26642
+ ["november", "11"],
26643
+ ["nov", "11"],
26644
+ ["11\uC6D4", "11"],
26645
+ ["december", "12"],
26646
+ ["dec", "12"],
26647
+ ["12\uC6D4", "12"]
26648
+ ]);
26649
+ SEARCH_RECENCY_TERMS = new Set([
26650
+ "latest",
26651
+ "newest",
26652
+ "recent",
26653
+ "earliest",
26654
+ "oldest",
26655
+ "\uCD5C\uADFC",
26656
+ "\uCD5C\uC2E0",
26657
+ "\uCD5C\uCD08",
26658
+ "\uC624\uB798\uB41C"
26659
+ ]);
26660
+ WIKI_INDEX_FILENAMES = {
26661
+ article: "article-index.md",
26662
+ summary: "summary-index.md",
26663
+ topic: "topic-index.md",
26664
+ skill: "skill-index.md"
26665
+ };
26666
+ WIKI_INDEX_NAMESPACES = {
26667
+ article: "articles",
26668
+ summary: "summaries",
26669
+ topic: "topic",
26670
+ skill: "skills"
26671
+ };
25618
26672
  WIKI_TOOLS = [
25619
26673
  {
25620
26674
  name: "wiki_query",
25621
- description: "Search the wiki knowledge base and return relevant articles.",
26675
+ description: "Search topic, article, or summary indexes and return relevant candidates.",
25622
26676
  inputSchema: {
25623
26677
  type: "object",
25624
26678
  properties: {
@@ -25629,11 +26683,38 @@ var init_wiki_server = __esm(() => {
25629
26683
  topic: {
25630
26684
  type: "string",
25631
26685
  description: "Optional topic name to narrow the search"
26686
+ },
26687
+ kind: {
26688
+ type: "string",
26689
+ enum: ["all", "topic", "article", "summary"],
26690
+ description: "Document kind to search; defaults to all"
26691
+ },
26692
+ limit: {
26693
+ type: "number",
26694
+ minimum: 1,
26695
+ maximum: 20,
26696
+ description: "Maximum number of candidates; defaults to 8"
25632
26697
  }
25633
26698
  },
25634
26699
  required: ["question"]
25635
26700
  }
25636
26701
  },
26702
+ {
26703
+ name: "wiki_read",
26704
+ description: "Read one topic, article, or summary returned by wiki_query. For a clearly matching continuing persona, set adopt=true when reading a topic.",
26705
+ inputSchema: {
26706
+ type: "object",
26707
+ properties: {
26708
+ kind: { type: "string", enum: ["topic", "article", "summary"] },
26709
+ key: { type: "string", description: "Canonical key returned by wiki_query" },
26710
+ adopt: {
26711
+ type: "boolean",
26712
+ description: "For kind=topic, persist this canonical memory key for the current room"
26713
+ }
26714
+ },
26715
+ required: ["kind", "key"]
26716
+ }
26717
+ },
25637
26718
  {
25638
26719
  name: "wiki_topic_brief",
25639
26720
  description: "Get the lightweight topic brief for a specific topic.",
@@ -25669,35 +26750,46 @@ var init_wiki_server = __esm(() => {
25669
26750
  }
25670
26751
  },
25671
26752
  {
25672
- name: "save_wiki_entry",
25673
- description: "Save a session summary directly to wiki/summaries/.",
26753
+ name: "wiki_write",
26754
+ description: "Write a wiki document and its catalog row together. The only way to create a summary, article, or topic brief: summaries go to wiki/summaries/ + summary-index.md, articles to wiki/articles/ + article-index.md, briefs to wiki/topic/ + topic-index.md (also mirrored into SQLite for next-session injection). Never write wiki files by any other means.",
25674
26755
  inputSchema: {
25675
26756
  type: "object",
25676
26757
  properties: {
25677
- topic: { type: "string", description: "Topic name of the session" },
25678
- content: { type: "string", description: "Session summary in markdown" }
26758
+ kind: {
26759
+ type: "string",
26760
+ enum: ["summary", "article", "topic"],
26761
+ description: "Which document to write"
26762
+ },
26763
+ content: { type: "string", description: "Document body in markdown" },
26764
+ description: {
26765
+ type: "string",
26766
+ description: "Single-line catalog description; required so the index stays useful"
26767
+ },
26768
+ topic: {
26769
+ type: "string",
26770
+ description: "Topic name; required for kind=summary and kind=topic"
26771
+ },
26772
+ slug: {
26773
+ type: "string",
26774
+ description: "Article slug in kebab-case; required for kind=article"
26775
+ },
26776
+ section: {
26777
+ type: "string",
26778
+ description: "H2 catalog section; required for kind=article"
26779
+ },
26780
+ date: { type: "string", description: "Override entry date YYYY-MM-DD (default: today)" }
25679
26781
  },
25680
- required: ["topic", "content"]
26782
+ required: ["kind", "content", "description"]
25681
26783
  }
25682
26784
  },
25683
26785
  {
25684
- name: "save_topic_brief",
25685
- description: "Write the accumulated persona/topic brief to wiki/topic/<topic>.md and mirror it into SQLite for next-session injection. Call this AFTER save_wiki_entry (archive \u2192 summary \u2192 brief order).",
25686
- inputSchema: {
25687
- type: "object",
25688
- properties: {
25689
- topic: { type: "string", description: "Topic name of the session" },
25690
- content: {
25691
- type: "string",
25692
- description: "Full accumulated topic brief in markdown (persona layer + recent work)"
25693
- }
25694
- },
25695
- required: ["topic", "content"]
25696
- }
26786
+ name: "wiki_reindex",
26787
+ description: "Rebuild the derived body-search cache from the documents on disk and report catalog gaps. Run this once after adding documents outside wiki_write; retrieval itself never scans the wiki.",
26788
+ inputSchema: { type: "object", properties: {} }
25697
26789
  },
25698
26790
  {
25699
26791
  name: "index_upsert",
25700
- description: "Upsert an entry in wiki/article-index.md, topic-index.md, or skill-index.md.",
26792
+ description: "Correct the description or section of an entry that already exists. Cannot create an entry \u2014 use wiki_write for that.",
25701
26793
  inputSchema: {
25702
26794
  type: "object",
25703
26795
  properties: {
@@ -25774,12 +26866,12 @@ __export(exports_activity_log, {
25774
26866
  readSentFilesForTopic: () => readSentFilesForTopic
25775
26867
  });
25776
26868
  import {
25777
- existsSync as existsSync27,
26869
+ existsSync as existsSync28,
25778
26870
  mkdirSync as mkdirSync25,
25779
26871
  readdirSync as readdirSync11,
25780
26872
  renameSync as renameSync12,
25781
26873
  statSync as statSync16,
25782
- unlinkSync as unlinkSync19,
26874
+ unlinkSync as unlinkSync20,
25783
26875
  writeFileSync as writeFileSync18
25784
26876
  } from "fs";
25785
26877
  import { basename as basename8, join as join37 } from "path";
@@ -25832,7 +26924,7 @@ function rotateOldLogs() {
25832
26924
  if (freed >= toFree)
25833
26925
  break;
25834
26926
  try {
25835
- unlinkSync19(join37(logDir, file.name));
26927
+ unlinkSync20(join37(logDir, file.name));
25836
26928
  freed += file.size;
25837
26929
  logger.info({ file: file.name, sizeKB: (file.size / 1024).toFixed(0) }, "Rotated out old log");
25838
26930
  } catch (e) {
@@ -25858,7 +26950,7 @@ function entryMatchesTopic(e, topicName) {
25858
26950
  }
25859
26951
  function readSentFilesForTopic(userId, topicName) {
25860
26952
  const file = join37(resolveStorageUsersLogDir(), String(userId), "sent-files.jsonl");
25861
- if (!existsSync27(file))
26953
+ if (!existsSync28(file))
25862
26954
  return [];
25863
26955
  try {
25864
26956
  return readJsonlLines(file).map((line) => JSON.parse(line)).filter((e) => entryMatchesTopic(e, topicName));
@@ -25869,7 +26961,7 @@ function readSentFilesForTopic(userId, topicName) {
25869
26961
  }
25870
26962
  function removeSentFilesForTopic(userId, topicName) {
25871
26963
  const file = join37(resolveStorageUsersLogDir(), String(userId), "sent-files.jsonl");
25872
- if (!existsSync27(file))
26964
+ if (!existsSync28(file))
25873
26965
  return;
25874
26966
  try {
25875
26967
  const remaining = readJsonlLines(file).filter((line) => {
@@ -25884,7 +26976,7 @@ function removeSentFilesForTopic(userId, topicName) {
25884
26976
  `)}
25885
26977
  `);
25886
26978
  } else {
25887
- unlinkSync19(file);
26979
+ unlinkSync20(file);
25888
26980
  }
25889
26981
  } catch (e) {
25890
26982
  logger.warn({ err: e, userId, topicName }, "Failed to remove sent-files for topic");
@@ -26682,7 +27774,7 @@ async function buildHostedSurfaceServer(surface, context) {
26682
27774
  case "wiki":
26683
27775
  case "skills": {
26684
27776
  const [{ createWikiMcpServer: createWikiMcpServer2 }, storage, { WORKSPACE_DIR: WORKSPACE_DIR2 }] = await Promise.all([
26685
- Promise.resolve().then(() => (init_wiki_server(), exports_wiki_server)),
27777
+ init_wiki_server().then(() => exports_wiki_server),
26686
27778
  init_storage_public().then(() => exports_storage_public),
26687
27779
  init_mcp_runtime_host().then(() => exports_mcp_runtime_host)
26688
27780
  ]);
@@ -27393,7 +28485,7 @@ var init_spec = __esm(() => {
27393
28485
  });
27394
28486
 
27395
28487
  // ../../packages/mcp-host/src/manifest.ts
27396
- import { existsSync as existsSync28, mkdirSync as mkdirSync26, readFileSync as readFileSync23, renameSync as renameSync13, writeFileSync as writeFileSync19 } from "fs";
28488
+ import { existsSync as existsSync29, mkdirSync as mkdirSync26, readFileSync as readFileSync24, renameSync as renameSync13, writeFileSync as writeFileSync19 } from "fs";
27397
28489
  import { dirname as dirname17 } from "path";
27398
28490
  import { z as z14 } from "zod";
27399
28491
 
@@ -27438,11 +28530,11 @@ class McpManifest {
27438
28530
  return this.entries.get(key)?.enabled ?? false;
27439
28531
  }
27440
28532
  load() {
27441
- if (!existsSync28(this.file))
28533
+ if (!existsSync29(this.file))
27442
28534
  return;
27443
28535
  let parsed;
27444
28536
  try {
27445
- parsed = manifestFileSchema.parse(JSON.parse(readFileSync23(this.file, "utf8")));
28537
+ parsed = manifestFileSchema.parse(JSON.parse(readFileSync24(this.file, "utf8")));
27446
28538
  } catch (e) {
27447
28539
  throw new Error(`Invalid MCP manifest at ${this.file}: ${e instanceof Error ? e.message : String(e)}`);
27448
28540
  }
@@ -27492,9 +28584,9 @@ import { execFileSync as execFileSync8, spawn as spawn8 } from "child_process";
27492
28584
  import {
27493
28585
  mkdirSync as mkdirSync27,
27494
28586
  readdirSync as readdirSync12,
27495
- readFileSync as readFileSync24,
28587
+ readFileSync as readFileSync25,
27496
28588
  renameSync as renameSync14,
27497
- unlinkSync as unlinkSync20,
28589
+ unlinkSync as unlinkSync21,
27498
28590
  writeFileSync as writeFileSync20
27499
28591
  } from "fs";
27500
28592
  import { connect, createServer as createServer3 } from "net";
@@ -27800,7 +28892,7 @@ class McpHost {
27800
28892
  if (file.includes(".tmp-"))
27801
28893
  continue;
27802
28894
  try {
27803
- const port = Number.parseInt(readFileSync24(join38(this.portsDir, file), "utf8").trim(), 10);
28895
+ const port = Number.parseInt(readFileSync25(join38(this.portsDir, file), "utf8").trim(), 10);
27804
28896
  if (!Number.isNaN(port))
27805
28897
  claims.set(file, port);
27806
28898
  } catch {}
@@ -27820,7 +28912,7 @@ class McpHost {
27820
28912
  }
27821
28913
  deletePortFile(fileName) {
27822
28914
  try {
27823
- unlinkSync20(join38(this.portsDir, fileName));
28915
+ unlinkSync21(join38(this.portsDir, fileName));
27824
28916
  } catch {}
27825
28917
  }
27826
28918
  cleanupRunning(rkey, inst) {
@@ -27925,9 +29017,9 @@ var init_src3 = __esm(() => {
27925
29017
  import { randomUUID as randomUUID24 } from "crypto";
27926
29018
  import {
27927
29019
  copyFileSync as copyFileSync4,
27928
- existsSync as existsSync29,
29020
+ existsSync as existsSync30,
27929
29021
  mkdirSync as mkdirSync28,
27930
- readFileSync as readFileSync25,
29022
+ readFileSync as readFileSync26,
27931
29023
  rmSync as rmSync9,
27932
29024
  statSync as statSync18,
27933
29025
  writeFileSync as writeFileSync21
@@ -27963,7 +29055,7 @@ class NodeFileStore {
27963
29055
  if (!FILE_ID_RE.test(fileId))
27964
29056
  return null;
27965
29057
  try {
27966
- const parsed = JSON.parse(readFileSync25(this.#metadataPath(fileId), "utf8"));
29058
+ const parsed = JSON.parse(readFileSync26(this.#metadataPath(fileId), "utf8"));
27967
29059
  if (typeof parsed.filename !== "string" || typeof parsed.mimeType !== "string" || typeof parsed.sizeBytes !== "number" || typeof parsed.savedName !== "string") {
27968
29060
  return null;
27969
29061
  }
@@ -27986,7 +29078,7 @@ class NodeFileStore {
27986
29078
  hooks = {
27987
29079
  resolveAttachmentByFileId: (fileId) => {
27988
29080
  const metadata = this.#metadata(fileId);
27989
- if (!metadata || !existsSync29(join39(this.uploadDir, metadata.savedName)))
29081
+ if (!metadata || !existsSync30(join39(this.uploadDir, metadata.savedName)))
27990
29082
  return null;
27991
29083
  return this.#attachment(fileId, metadata);
27992
29084
  },
@@ -27995,7 +29087,7 @@ class NodeFileStore {
27995
29087
  if (!metadata)
27996
29088
  return null;
27997
29089
  const path = join39(this.uploadDir, metadata.savedName);
27998
- return existsSync29(path) ? path : null;
29090
+ return existsSync30(path) ? path : null;
27999
29091
  },
28000
29092
  storeLocalFileAsUpload: (absPath, access = {}) => this.store(absPath, access),
28001
29093
  deleteFilesForTopic: (topicId) => this.deleteForTopic(topicId)
@@ -28038,7 +29130,7 @@ class NodeFileStore {
28038
29130
  if (!allowed)
28039
29131
  return null;
28040
29132
  const path = join39(this.uploadDir, metadata.savedName);
28041
- if (!existsSync29(path))
29133
+ if (!existsSync30(path))
28042
29134
  return null;
28043
29135
  return new Response(Bun.file(path), {
28044
29136
  headers: {
@@ -28207,12 +29299,12 @@ function createPollingSseStream(req, options) {
28207
29299
  // ../../packages/node/src/control.ts
28208
29300
  import { randomUUID as randomUUID25, timingSafeEqual as timingSafeEqual4 } from "crypto";
28209
29301
  import {
28210
- chmodSync as chmodSync5,
28211
- existsSync as existsSync30,
29302
+ chmodSync as chmodSync6,
29303
+ existsSync as existsSync31,
28212
29304
  mkdirSync as mkdirSync29,
28213
- readFileSync as readFileSync26,
29305
+ readFileSync as readFileSync27,
28214
29306
  renameSync as renameSync15,
28215
- unlinkSync as unlinkSync21,
29307
+ unlinkSync as unlinkSync22,
28216
29308
  writeFileSync as writeFileSync22
28217
29309
  } from "fs";
28218
29310
  import { dirname as dirname18, resolve as resolve21 } from "path";
@@ -28719,16 +29811,16 @@ function writeNodeDaemonInfo(port, startedAt) {
28719
29811
  const temporary = `${NODE_DAEMON_INFO_PATH}.${process.pid}.${randomUUID25()}.tmp`;
28720
29812
  writeFileSync22(temporary, `${JSON.stringify(info, null, 2)}
28721
29813
  `, { mode: 384 });
28722
- chmodSync5(temporary, 384);
29814
+ chmodSync6(temporary, 384);
28723
29815
  renameSync15(temporary, NODE_DAEMON_INFO_PATH);
28724
- chmodSync5(NODE_DAEMON_INFO_PATH, 384);
29816
+ chmodSync6(NODE_DAEMON_INFO_PATH, 384);
28725
29817
  return info;
28726
29818
  }
28727
29819
  function readNodeDaemonInfo() {
28728
- if (!existsSync30(NODE_DAEMON_INFO_PATH))
29820
+ if (!existsSync31(NODE_DAEMON_INFO_PATH))
28729
29821
  return null;
28730
29822
  try {
28731
- const value = JSON.parse(readFileSync26(NODE_DAEMON_INFO_PATH, "utf8"));
29823
+ const value = JSON.parse(readFileSync27(NODE_DAEMON_INFO_PATH, "utf8"));
28732
29824
  if (value?.schemaVersion !== 1 || value.stateDir !== STATE_DIR || !Number.isInteger(value.protocolVersion) || !Number.isInteger(value.pid) || !Number.isInteger(value.port) || value.port <= 0) {
28733
29825
  return null;
28734
29826
  }
@@ -28742,7 +29834,7 @@ function removeNodeDaemonInfo(expected) {
28742
29834
  if (!current3 || current3.pid !== expected.pid || current3.port !== expected.port)
28743
29835
  return;
28744
29836
  try {
28745
- unlinkSync21(NODE_DAEMON_INFO_PATH);
29837
+ unlinkSync22(NODE_DAEMON_INFO_PATH);
28746
29838
  } catch {}
28747
29839
  }
28748
29840
  async function fetchWithTimeout(url, init, timeoutMs) {
@@ -29016,7 +30108,7 @@ var init_schedule = __esm(() => {
29016
30108
 
29017
30109
  // ../../packages/module-cron/src/scripts.ts
29018
30110
  import { spawn as spawn9 } from "child_process";
29019
- import { existsSync as existsSync31, mkdirSync as mkdirSync30, readdirSync as readdirSync13 } from "fs";
30111
+ import { existsSync as existsSync32, mkdirSync as mkdirSync30, readdirSync as readdirSync13 } from "fs";
29020
30112
  import { resolve as resolve22, sep as sep2 } from "path";
29021
30113
  function validateCronScriptName(script) {
29022
30114
  if (!/^[A-Za-z0-9_][A-Za-z0-9_.-]*\.py$/.test(script) || script.includes("..") || script.includes("/") || script.includes("\\")) {
@@ -29039,7 +30131,7 @@ function listCronScripts() {
29039
30131
  }
29040
30132
  function cronScriptExists(script) {
29041
30133
  try {
29042
- return existsSync31(resolveCronScriptPath(script));
30134
+ return existsSync32(resolveCronScriptPath(script));
29043
30135
  } catch {
29044
30136
  return false;
29045
30137
  }
@@ -29071,7 +30163,7 @@ async function runCronPromptScript(options) {
29071
30163
  if (options.signal?.aborted)
29072
30164
  throw new CronScriptError("script run aborted");
29073
30165
  const scriptPath = resolveCronScriptPath(options.script);
29074
- if (!existsSync31(scriptPath))
30166
+ if (!existsSync32(scriptPath))
29075
30167
  throw new CronScriptError(`cron script not found: ${options.script}`);
29076
30168
  const timeoutMs = Math.max(5000, options.timeoutMs ?? DEFAULT_SCRIPT_TIMEOUT_MS);
29077
30169
  const outputLimitBytes = Math.max(1024, options.outputLimitBytes ?? DEFAULT_OUTPUT_LIMIT_BYTES);
@@ -30272,7 +31364,7 @@ var init_scheduler = __esm(async () => {
30272
31364
  });
30273
31365
 
30274
31366
  // ../../packages/module-cron/src/module.ts
30275
- import { existsSync as existsSync32 } from "fs";
31367
+ import { existsSync as existsSync33 } from "fs";
30276
31368
  import { fileURLToPath as fileURLToPath2 } from "url";
30277
31369
  function envMs(name, fallback) {
30278
31370
  const parsed = Number.parseInt(process.env[name] ?? "", 10);
@@ -30303,7 +31395,7 @@ function createCronModule(options = {}) {
30303
31395
  if (event.type === "topic-deleted")
30304
31396
  trackCleanup(resetCronTopicContext(event.topicId));
30305
31397
  });
30306
- const mcpServerFile = options.mcpServerFile ?? (existsSync32(SOURCE_MCP_SERVER_FILE) ? SOURCE_MCP_SERVER_FILE : PACKAGED_MCP_SERVER_FILE);
31398
+ const mcpServerFile = options.mcpServerFile ?? (existsSync33(SOURCE_MCP_SERVER_FILE) ? SOURCE_MCP_SERVER_FILE : PACKAGED_MCP_SERVER_FILE);
30307
31399
  const unregisterMcp = host2.registerRuntimeMcpServer("cron-manager", {
30308
31400
  scopes: ["forum", "manager"],
30309
31401
  forumRequired: true,
@@ -31801,7 +32893,7 @@ var init_context_usage = __esm(async () => {
31801
32893
 
31802
32894
  // ../../adapters/terminal/src/path-suggest.ts
31803
32895
  import { execFile as execFile3 } from "child_process";
31804
- import { existsSync as existsSync33, readdirSync as readdirSync14, statSync as statSync19 } from "fs";
32896
+ import { existsSync as existsSync34, readdirSync as readdirSync14, statSync as statSync19 } from "fs";
31805
32897
  import { homedir as homedir10 } from "os";
31806
32898
  import { basename as basename11, dirname as dirname19, join as join40, sep as sep3 } from "path";
31807
32899
  import { promisify as promisify3 } from "util";
@@ -32050,7 +33142,7 @@ function fragmentToAbsolutePath(frag) {
32050
33142
  }
32051
33143
  function fragmentResolves(frag) {
32052
33144
  try {
32053
- return existsSync33(fragmentToAbsolutePath(frag));
33145
+ return existsSync34(fragmentToAbsolutePath(frag));
32054
33146
  } catch {
32055
33147
  return false;
32056
33148
  }
@@ -38744,7 +39836,7 @@ function renderOutbound(text2, maxLen = 4096) {
38744
39836
  }
38745
39837
 
38746
39838
  // ../../adapters/telegram/src/adapter.ts
38747
- import { existsSync as existsSync34 } from "fs";
39839
+ import { existsSync as existsSync35 } from "fs";
38748
39840
  import { basename as basename12, extname as extname5 } from "path";
38749
39841
  function startTelegramAdapter(opts) {
38750
39842
  const { client, forumChatId } = opts;
@@ -39232,7 +40324,7 @@ function startTelegramAdapter(opts) {
39232
40324
  logger.warn({ path, chatId }, "telegram adapter: blocked sensitive file path");
39233
40325
  return { ref: null, delivered: false, error: "path matches the sensitive-file blacklist" };
39234
40326
  }
39235
- if (!existsSync34(path)) {
40327
+ if (!existsSync35(path)) {
39236
40328
  const sent = await client.sendMessage(chatId, `File: ${path}`, base).catch(() => null);
39237
40329
  const messageId = sentMessageId(sent);
39238
40330
  return {
@@ -40414,19 +41506,19 @@ __export(exports_join, {
40414
41506
  });
40415
41507
  import { createHash as createHash11, randomUUID as randomUUID27 } from "crypto";
40416
41508
  import {
40417
- chmodSync as chmodSync6,
41509
+ chmodSync as chmodSync7,
40418
41510
  closeSync as closeSync5,
40419
- existsSync as existsSync35,
41511
+ existsSync as existsSync36,
40420
41512
  fsyncSync as fsyncSync2,
40421
41513
  linkSync,
40422
41514
  lstatSync,
40423
41515
  mkdirSync as mkdirSync32,
40424
41516
  openSync as openSync5,
40425
- readFileSync as readFileSync27,
41517
+ readFileSync as readFileSync28,
40426
41518
  renameSync as renameSync16,
40427
41519
  rmSync as rmSync10,
40428
41520
  statSync as statSync20,
40429
- unlinkSync as unlinkSync22,
41521
+ unlinkSync as unlinkSync23,
40430
41522
  writeFileSync as writeFileSync23
40431
41523
  } from "fs";
40432
41524
  import { dirname as dirname21, resolve as resolve23 } from "path";
@@ -40526,7 +41618,7 @@ function withJoinCredentialLock(operation) {
40526
41618
  throw error2;
40527
41619
  let current3 = null;
40528
41620
  try {
40529
- current3 = JSON.parse(readFileSync27(ownerPath, "utf8"));
41621
+ current3 = JSON.parse(readFileSync28(ownerPath, "utf8"));
40530
41622
  } catch {}
40531
41623
  let ageMs;
40532
41624
  try {
@@ -40556,7 +41648,7 @@ function withJoinCredentialLock(operation) {
40556
41648
  return operation();
40557
41649
  } finally {
40558
41650
  try {
40559
- const current3 = JSON.parse(readFileSync27(ownerPath, "utf8"));
41651
+ const current3 = JSON.parse(readFileSync28(ownerPath, "utf8"));
40560
41652
  if (current3.pid === owner.pid && current3.token === owner.token) {
40561
41653
  rmSync10(lockPath, { recursive: true, force: true });
40562
41654
  }
@@ -40579,9 +41671,9 @@ function joinCredentialDigest(join42) {
40579
41671
  return createHash11("sha256").update(JSON.stringify(normalizedJoin(join42))).digest("base64url");
40580
41672
  }
40581
41673
  function readPersistedJoin(path = joinFilePath()) {
40582
- if (!existsSync35(path))
41674
+ if (!existsSync36(path))
40583
41675
  return null;
40584
- const parsed = JSON.parse(readFileSync27(path, "utf-8"));
41676
+ const parsed = JSON.parse(readFileSync28(path, "utf-8"));
40585
41677
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
40586
41678
  throw new Error("persisted join credentials are not a JSON object");
40587
41679
  }
@@ -40600,7 +41692,7 @@ function saveJoinWhileLocked(join42, options = {}) {
40600
41692
  const directory = dirname21(path);
40601
41693
  const normalized = normalizedJoin(join42);
40602
41694
  mkdirSync32(directory, { recursive: true });
40603
- if (existsSync35(path)) {
41695
+ if (existsSync36(path)) {
40604
41696
  if (lstatSync(path).isSymbolicLink()) {
40605
41697
  throw new Error(`refusing to replace symlinked Otium join file at ${path}`);
40606
41698
  }
@@ -40613,7 +41705,7 @@ function saveJoinWhileLocked(join42, options = {}) {
40613
41705
  }
40614
41706
  }
40615
41707
  if (existing && joinsEqual(existing, normalized)) {
40616
- chmodSync6(path, 384);
41708
+ chmodSync7(path, 384);
40617
41709
  const fileFd = openSync5(path, "r");
40618
41710
  try {
40619
41711
  fsyncSync2(fileFd);
@@ -40645,9 +41737,9 @@ function saveJoinWhileLocked(join42, options = {}) {
40645
41737
  renameSync16(temporaryPath, path);
40646
41738
  } else {
40647
41739
  linkSync(temporaryPath, path);
40648
- unlinkSync22(temporaryPath);
41740
+ unlinkSync23(temporaryPath);
40649
41741
  }
40650
- chmodSync6(path, 384);
41742
+ chmodSync7(path, 384);
40651
41743
  const directoryFd = openSync5(directory, "r");
40652
41744
  try {
40653
41745
  fsyncSync2(directoryFd);
@@ -40657,8 +41749,8 @@ function saveJoinWhileLocked(join42, options = {}) {
40657
41749
  } catch (error2) {
40658
41750
  if (fd !== undefined)
40659
41751
  closeSync5(fd);
40660
- if (existsSync35(temporaryPath))
40661
- unlinkSync22(temporaryPath);
41752
+ if (existsSync36(temporaryPath))
41753
+ unlinkSync23(temporaryPath);
40662
41754
  throw error2;
40663
41755
  }
40664
41756
  return path;
@@ -40669,12 +41761,12 @@ function saveJoin(join42, options = {}) {
40669
41761
  function removeJoin() {
40670
41762
  return withJoinCredentialLock(() => {
40671
41763
  const path = joinFilePath();
40672
- if (!existsSync35(path))
41764
+ if (!existsSync36(path))
40673
41765
  return false;
40674
41766
  if (lstatSync(path).isSymbolicLink()) {
40675
41767
  throw new Error(`refusing to remove symlinked Otium join file at ${path}`);
40676
41768
  }
40677
- unlinkSync22(path);
41769
+ unlinkSync23(path);
40678
41770
  const directoryFd = openSync5(dirname21(path), "r");
40679
41771
  try {
40680
41772
  fsyncSync2(directoryFd);
@@ -40701,10 +41793,10 @@ function loadJoin() {
40701
41793
  logger.warn("otium: OTIUM_CENTRAL_URL, OTIUM_CELL_ID, OTIUM_CELL_SECRET must be set together \u2014 ignoring partial env");
40702
41794
  }
40703
41795
  const path = joinFilePath();
40704
- if (!existsSync35(path))
41796
+ if (!existsSync36(path))
40705
41797
  return null;
40706
41798
  try {
40707
- const parsed = JSON.parse(readFileSync27(path, "utf-8"));
41799
+ const parsed = JSON.parse(readFileSync28(path, "utf-8"));
40708
41800
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
40709
41801
  return null;
40710
41802
  return normalizeJoin(parsed);
@@ -40728,11 +41820,11 @@ var exports_join_status = {};
40728
41820
  __export(exports_join_status, {
40729
41821
  hasConfiguredOtiumJoin: () => hasConfiguredOtiumJoin
40730
41822
  });
40731
- import { existsSync as existsSync36 } from "fs";
41823
+ import { existsSync as existsSync37 } from "fs";
40732
41824
  import { resolve as resolve24 } from "path";
40733
41825
  function hasConfiguredOtiumJoin() {
40734
41826
  const envJoin = Boolean(process.env.OTIUM_CENTRAL_URL?.trim() && process.env.OTIUM_CELL_ID?.trim() && process.env.OTIUM_CELL_SECRET?.trim());
40735
- return envJoin || existsSync36(resolve24(DATA_DIR, "otium-join.json"));
41827
+ return envJoin || existsSync37(resolve24(DATA_DIR, "otium-join.json"));
40736
41828
  }
40737
41829
  var init_join_status = __esm(() => {
40738
41830
  init_config_public();
@@ -40957,10 +42049,10 @@ var init_canonical_mcp_bridge = __esm(async () => {
40957
42049
  task: new Set(["task_create", "task_update", "task_list", "task_get", "task_delete"]),
40958
42050
  wiki: new Set([
40959
42051
  "wiki_query",
42052
+ "wiki_read",
40960
42053
  "wiki_topic_brief",
40961
42054
  "wiki_last_conversation",
40962
- "save_wiki_entry",
40963
- "save_topic_brief",
42055
+ "wiki_write",
40964
42056
  "index_upsert"
40965
42057
  ])
40966
42058
  };
@@ -43420,16 +44512,16 @@ import {
43420
44512
  randomUUID as randomUUID31
43421
44513
  } from "crypto";
43422
44514
  import {
43423
- chmodSync as chmodSync7,
44515
+ chmodSync as chmodSync8,
43424
44516
  closeSync as closeSync6,
43425
- existsSync as existsSync37,
44517
+ existsSync as existsSync38,
43426
44518
  fsyncSync as fsyncSync3,
43427
44519
  linkSync as linkSync2,
43428
44520
  mkdirSync as mkdirSync34,
43429
44521
  openSync as openSync6,
43430
- readFileSync as readFileSync28,
44522
+ readFileSync as readFileSync29,
43431
44523
  renameSync as renameSync17,
43432
- unlinkSync as unlinkSync23,
44524
+ unlinkSync as unlinkSync24,
43433
44525
  writeFileSync as writeFileSync24
43434
44526
  } from "fs";
43435
44527
  import { dirname as dirname22, resolve as resolve25 } from "path";
@@ -43457,10 +44549,10 @@ function pendingEnrollmentPath() {
43457
44549
  }
43458
44550
  function isEnrollmentPending(invite) {
43459
44551
  const path = pendingEnrollmentPath();
43460
- if (!existsSync37(path))
44552
+ if (!existsSync38(path))
43461
44553
  return false;
43462
44554
  try {
43463
- const saved = JSON.parse(readFileSync28(path, "utf8"));
44555
+ const saved = JSON.parse(readFileSync29(path, "utf8"));
43464
44556
  return saved.central === invite.central && saved.token === invite.token;
43465
44557
  } catch {
43466
44558
  return false;
@@ -43468,8 +44560,8 @@ function isEnrollmentPending(invite) {
43468
44560
  }
43469
44561
  function loadOrCreatePending(invite, nodeName) {
43470
44562
  const path = pendingEnrollmentPath();
43471
- if (existsSync37(path)) {
43472
- const saved = JSON.parse(readFileSync28(path, "utf8"));
44563
+ if (existsSync38(path)) {
44564
+ const saved = JSON.parse(readFileSync29(path, "utf8"));
43473
44565
  if (saved.central === invite.central && saved.token === invite.token)
43474
44566
  return saved;
43475
44567
  throw new Error(`another Otium enrollment is pending at ${path}`);
@@ -43493,8 +44585,8 @@ function loadOrCreatePending(invite, nodeName) {
43493
44585
  closeSync6(fd);
43494
44586
  fd = undefined;
43495
44587
  linkSync2(temporaryPath, path);
43496
- unlinkSync23(temporaryPath);
43497
- chmodSync7(path, 384);
44588
+ unlinkSync24(temporaryPath);
44589
+ chmodSync8(path, 384);
43498
44590
  const directoryFd = openSync6(dirname22(path), "r");
43499
44591
  try {
43500
44592
  fsyncSync3(directoryFd);
@@ -43504,10 +44596,10 @@ function loadOrCreatePending(invite, nodeName) {
43504
44596
  } catch (error2) {
43505
44597
  if (fd !== undefined)
43506
44598
  closeSync6(fd);
43507
- if (existsSync37(temporaryPath))
43508
- unlinkSync23(temporaryPath);
43509
- if (error2.code === "EEXIST" && existsSync37(path)) {
43510
- const saved = JSON.parse(readFileSync28(path, "utf8"));
44599
+ if (existsSync38(temporaryPath))
44600
+ unlinkSync24(temporaryPath);
44601
+ if (error2.code === "EEXIST" && existsSync38(path)) {
44602
+ const saved = JSON.parse(readFileSync29(path, "utf8"));
43511
44603
  if (saved.central === invite.central && saved.token === invite.token)
43512
44604
  return saved;
43513
44605
  }
@@ -43537,14 +44629,14 @@ function replacePendingEnrollment(pending2) {
43537
44629
  } catch (error2) {
43538
44630
  if (fd !== undefined)
43539
44631
  closeSync6(fd);
43540
- if (existsSync37(temporaryPath))
43541
- unlinkSync23(temporaryPath);
44632
+ if (existsSync38(temporaryPath))
44633
+ unlinkSync24(temporaryPath);
43542
44634
  throw error2;
43543
44635
  }
43544
44636
  }
43545
44637
  function recordClaimedCredential(pending2, join43) {
43546
44638
  withJoinCredentialLock(() => {
43547
- const current3 = JSON.parse(readFileSync28(pendingEnrollmentPath(), "utf8"));
44639
+ const current3 = JSON.parse(readFileSync29(pendingEnrollmentPath(), "utf8"));
43548
44640
  if (current3.central !== pending2.central || current3.token !== pending2.token || current3.idempotencyKey !== pending2.idempotencyKey || current3.publicKey !== pending2.publicKey) {
43549
44641
  throw new Error("pending Otium enrollment changed while its claim was in flight");
43550
44642
  }
@@ -43623,7 +44715,7 @@ async function claimEnrollment(invite, nodeName) {
43623
44715
  function commitEnrollment(join43, options = {}) {
43624
44716
  return withJoinCredentialLock(() => {
43625
44717
  const pendingPath = pendingEnrollmentPath();
43626
- const pending2 = existsSync37(pendingPath) ? JSON.parse(readFileSync28(pendingPath, "utf8")) : null;
44718
+ const pending2 = existsSync38(pendingPath) ? JSON.parse(readFileSync29(pendingPath, "utf8")) : null;
43627
44719
  const digest = joinCredentialDigest(join43);
43628
44720
  if (pending2 && (!pending2.claimed || pending2.claimed.digest !== digest || pending2.claimed.cellId !== join43.cellId)) {
43629
44721
  throw new Error(`pending Otium enrollment at ${pendingPath} does not match these credentials`);
@@ -43634,7 +44726,7 @@ function commitEnrollment(join43, options = {}) {
43634
44726
  }
43635
44727
  if (!pending2)
43636
44728
  return path;
43637
- unlinkSync23(pendingPath);
44729
+ unlinkSync24(pendingPath);
43638
44730
  const directoryFd = openSync6(dirname22(pendingPath), "r");
43639
44731
  try {
43640
44732
  fsyncSync3(directoryFd);
@@ -45823,4 +46915,4 @@ switch (command) {
45823
46915
  }
45824
46916
  }
45825
46917
 
45826
- //# debugId=C4FD6B649361AE3464756E2164756E21
46918
+ //# debugId=1F2F931DB2BD421464756E2164756E21