negotium 0.2.17 → 0.2.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/agent-helpers.js +41 -32
  2. package/dist/agent-helpers.js.map +9 -9
  3. package/dist/{chunk-5cpdbbeh.js → chunk-fc58cqry.js} +3 -1
  4. package/dist/{chunk-5cpdbbeh.js.map → chunk-fc58cqry.js.map} +3 -3
  5. package/dist/hosted-agent.js +4 -2
  6. package/dist/hosted-agent.js.map +4 -4
  7. package/dist/main.js +662 -93
  8. package/dist/main.js.map +11 -11
  9. package/dist/mcp-factories.js +659 -91
  10. package/dist/mcp-factories.js.map +10 -10
  11. package/dist/prompts.js +8 -8
  12. package/dist/prompts.js.map +3 -3
  13. package/dist/registry.js +3 -3
  14. package/dist/registry.js.map +2 -2
  15. package/dist/rollout.js +1 -1
  16. package/dist/runtime/src/agents/idle-archiver.ts +3 -2
  17. package/dist/runtime/src/mcp/canonical-proxy-server.ts +18 -1
  18. package/dist/runtime/src/mcp/wiki-server.ts +801 -59
  19. package/dist/runtime/src/platform/mcp-config.ts +1 -0
  20. package/dist/runtime/src/prompts/agents/wiki-archiver.md +16 -6
  21. package/dist/runtime/src/prompts/builders.ts +17 -34
  22. package/dist/runtime/src/runtime/turn-runner.ts +14 -17
  23. package/dist/runtime/src/storage/api-topics.ts +13 -4
  24. package/dist/runtime/src/topics/lifecycle.ts +3 -2
  25. package/dist/runtime/src/types/api.ts +6 -0
  26. package/dist/runtime/src/version.ts +1 -1
  27. package/dist/storage.js +12 -6
  28. package/dist/storage.js.map +3 -3
  29. package/dist/types/packages/core/src/mcp/wiki-server.d.ts +4 -0
  30. package/dist/types/packages/core/src/prompts/builders.d.ts +2 -4
  31. package/dist/types/packages/core/src/storage/api-topics.d.ts +1 -0
  32. package/dist/types/packages/core/src/types/api.d.ts +6 -0
  33. package/dist/types/packages/core/src/version.d.ts +1 -1
  34. package/package.json +1 -1
@@ -952,7 +952,7 @@ var init_claude_registry = __esm(() => {
952
952
  });
953
953
 
954
954
  // ../../packages/core/src/version.ts
955
- var NEGOTIUM_VERSION = "0.2.17";
955
+ var NEGOTIUM_VERSION = "0.2.18";
956
956
 
957
957
  // ../../packages/core/src/agents/codex-native-multi-agent.ts
958
958
  import { spawn as spawn2 } from "child_process";
@@ -2128,6 +2128,8 @@ var init_mcp_config = __esm(() => {
2128
2128
  return env ? buildStdioMcpServer(agent, CANONICAL_MCP_PROXY_SERVER, ["--surface=wiki"], env) : null;
2129
2129
  }
2130
2130
  const args = [`--user-id=${userId}`];
2131
+ if (topicId)
2132
+ args.push(`--current-topic-id=${topicId}`);
2131
2133
  const resolvedWikiTopicId = wikiTopicId ?? topicId ?? (session !== "dm" ? session : undefined);
2132
2134
  if (resolvedWikiTopicId)
2133
2135
  args.push(`--topic-id=${resolvedWikiTopicId}`);
@@ -4140,6 +4142,7 @@ function initializeApiTopicsSchema() {
4140
4142
  last_message_at TEXT,
4141
4143
  parent_topic_id TEXT,
4142
4144
  memory_topic_id TEXT,
4145
+ memory_key TEXT,
4143
4146
  is_fork INTEGER NOT NULL DEFAULT 0 CHECK (is_fork IN (0,1)),
4144
4147
  is_subagent INTEGER NOT NULL DEFAULT 0 CHECK (is_subagent IN (0,1)),
4145
4148
  visibility TEXT NOT NULL DEFAULT 'visible' CHECK (visibility IN ('visible','hidden')),
@@ -4189,8 +4192,8 @@ function initializeApiTopicsSchema() {
4189
4192
  const legacyBaseEffort = row.base_effort ?? row.default_effort;
4190
4193
  db.query(`INSERT INTO api_topics_next
4191
4194
  (id,title,kind,description,agent,base_model,base_effort,response_policy,
4192
- created_at,last_message_at,parent_topic_id,memory_topic_id,is_fork,is_subagent,visibility,access_mode,session_id)
4193
- 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);
4195
+ created_at,last_message_at,parent_topic_id,memory_topic_id,memory_key,is_fork,is_subagent,visibility,access_mode,session_id)
4196
+ 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);
4194
4197
  }
4195
4198
  db.exec("DROP TABLE IF EXISTS topic_members");
4196
4199
  db.exec("DROP TABLE api_topics");
@@ -4259,6 +4262,9 @@ function initializeApiTopicsSchema() {
4259
4262
  if (!tableColumns("api_topics").has("memory_topic_id")) {
4260
4263
  db.exec("ALTER TABLE api_topics ADD COLUMN memory_topic_id TEXT");
4261
4264
  }
4265
+ if (!tableColumns("api_topics").has("memory_key")) {
4266
+ db.exec("ALTER TABLE api_topics ADD COLUMN memory_key TEXT");
4267
+ }
4262
4268
  db.exec(`
4263
4269
  UPDATE api_topics
4264
4270
  SET browser_profile_owner = (
@@ -4319,6 +4325,7 @@ function rowToDto2(r, participants = getTopicParticipants(r.id), tellTargets) {
4319
4325
  lastMessageAt: r.last_message_at ?? new Date().toISOString(),
4320
4326
  parentTopicId: r.parent_topic_id ?? undefined,
4321
4327
  memoryTopicId: r.memory_topic_id ?? undefined,
4328
+ memoryKey: r.memory_key ?? undefined,
4322
4329
  isFork: r.is_fork !== 0,
4323
4330
  ...r.is_subagent !== 0 ? { isSubagent: true } : {},
4324
4331
  ...r.is_subagent !== 0 ? {
@@ -4390,9 +4397,9 @@ function upsertTopic(t) {
4390
4397
  db.transaction(() => {
4391
4398
  db.query(`INSERT INTO api_topics
4392
4399
  (id,title,kind,description,agent,base_model,base_effort,response_policy,
4393
- created_at,last_message_at,parent_topic_id,memory_topic_id,is_fork,is_subagent,visibility,access_mode,
4400
+ created_at,last_message_at,parent_topic_id,memory_topic_id,memory_key,is_fork,is_subagent,visibility,access_mode,
4394
4401
  subagent_report_mode)
4395
- VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
4402
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
4396
4403
  ON CONFLICT(id) DO UPDATE SET
4397
4404
  title = excluded.title,
4398
4405
  kind = excluded.kind,
@@ -4405,11 +4412,12 @@ function upsertTopic(t) {
4405
4412
  last_message_at = excluded.last_message_at,
4406
4413
  parent_topic_id = excluded.parent_topic_id,
4407
4414
  memory_topic_id = excluded.memory_topic_id,
4415
+ memory_key = excluded.memory_key,
4408
4416
  is_fork = excluded.is_fork,
4409
4417
  is_subagent = excluded.is_subagent,
4410
4418
  visibility = excluded.visibility,
4411
4419
  access_mode = excluded.access_mode,
4412
- 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");
4420
+ 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");
4413
4421
  db.query("DELETE FROM topic_members WHERE topic_id = ?").run(t.id);
4414
4422
  for (const participant of t.participants) {
4415
4423
  db.query("INSERT INTO topic_members (topic_id,user_id,role) VALUES (?,?,?)").run(t.id, participant.userId, participant.role);
@@ -9006,7 +9014,7 @@ __export(exports_codex_provider, {
9006
9014
  import { execFileSync as execFileSync6 } from "child_process";
9007
9015
  import { existsSync as existsSync15, readFileSync as readFileSync14, realpathSync as realpathSync4, statSync as statSync6 } from "fs";
9008
9016
  import { homedir as homedir6 } from "os";
9009
- import { dirname as dirname11, isAbsolute as isAbsolute3, join as join16, relative, resolve as resolve12 } from "path";
9017
+ import { dirname as dirname11, isAbsolute as isAbsolute3, join as join16, relative as relative2, resolve as resolve12 } from "path";
9010
9018
  import { Codex } from "@openai/codex-sdk";
9011
9019
  function sameCodexUsage(usage, total) {
9012
9020
  return usage.input_tokens === total.inputTokens && usage.output_tokens === total.outputTokens && (usage.cached_input_tokens ?? 0) === total.cachedInputTokens && (usage.cache_write_input_tokens ?? 0) === total.cacheWriteInputTokens;
@@ -9190,7 +9198,7 @@ class CodexFilePreviewTracker {
9190
9198
  if (!this.#root || !this.#baselineAvailable || !succeeded)
9191
9199
  return {};
9192
9200
  const absolute = canonicalPath(isAbsolute3(path) ? path : resolve12(this.#cwd, path));
9193
- const relativePath = relative(this.#root, absolute);
9201
+ const relativePath = relative2(this.#root, absolute);
9194
9202
  if (!relativePath || relativePath.startsWith("..") || isAbsolute3(relativePath))
9195
9203
  return {};
9196
9204
  const before = this.#baseline.has(relativePath) ? this.#baseline.get(relativePath) : this.#headFile(relativePath);
@@ -10282,14 +10290,14 @@ function buildMemoryPromptSection(opts) {
10282
10290
  const parts = [`
10283
10291
 
10284
10292
  ## Memory`];
10285
- parts.push(`Memory directory: ${opts.wikiDir}/topic`);
10286
- parts.push(`Files: ${opts.hasFiles ? opts.briefFile : "(none)"}`);
10287
- if (opts.latestSummaryFile) {
10288
- parts.push(`Latest summary: ${opts.latestSummaryFile}`);
10289
- }
10290
- 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.");
10291
- 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.");
10292
- 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.");
10293
+ if (opts.isManager) {
10294
+ parts.push("Use `wiki_query` for past workspace decisions or cross-topic context, then `wiki_read` only for the relevant result.");
10295
+ } else if (opts.memoryKey) {
10296
+ 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}")\`.`);
10297
+ } else {
10298
+ 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.");
10299
+ }
10300
+ 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.');
10293
10301
  if (opts.hasArchive) {
10294
10302
  parts.push("", "If you need the actual conversation from an earlier session, use the `wiki_last_conversation` MCP tool.");
10295
10303
  }
@@ -11245,11 +11253,12 @@ function archiveActiveTopicForMemory(topicId, userId, options) {
11245
11253
  }, "topic-memory-archiver: raw snapshot preserved below exchange threshold");
11246
11254
  return "below-threshold";
11247
11255
  }
11248
- const memoryTopic = getTopicMemoryOrigin(topicId) ?? topic;
11256
+ const memoryTopic = topic.memoryKey ? topic : getTopicMemoryOrigin(topicId) ?? topic;
11257
+ const memoryTitle = memoryTopic.memoryKey?.trim() || memoryTopic.title;
11249
11258
  const launched = (options.launchArchiver ?? runArchiverTurn)({
11250
11259
  userId,
11251
11260
  topicId: memoryTopic.id,
11252
- topicTitle: memoryTopic.title,
11261
+ topicTitle: memoryTitle,
11253
11262
  archivePath: job.archivePath,
11254
11263
  ...options.rawArchivePaths ? { rawArchivePaths: options.rawArchivePaths } : {},
11255
11264
  messageCount: job.messageCount,
@@ -13357,11 +13366,12 @@ async function deleteTopicCascadeImpl(topic, userId, options, deletingAncestorId
13357
13366
  }
13358
13367
  if (archived && meetsMemoryArchiveExchangeThreshold(archived.exchangeCount, MIN_MEMORY_ARCHIVE_EXCHANGES, topic)) {
13359
13368
  try {
13360
- const memoryTopic = getTopicMemoryOrigin(topicId) ?? topic;
13369
+ const memoryTopic = topic.memoryKey ? topic : getTopicMemoryOrigin(topicId) ?? topic;
13370
+ const memoryTitle = memoryTopic.memoryKey?.trim() || memoryTopic.title;
13361
13371
  runArchiverTurn({
13362
13372
  userId,
13363
13373
  ...memoryTopic.id !== topicId && !deletingTopicIds.has(memoryTopic.id) ? { topicId: memoryTopic.id } : {},
13364
- topicTitle: memoryTopic.title,
13374
+ topicTitle: memoryTitle,
13365
13375
  archivePath: archived.path,
13366
13376
  ...rawArchives.length > 0 ? { rawArchivePaths: rawArchives } : {},
13367
13377
  messageCount: archived.messageCount
@@ -16439,25 +16449,24 @@ function startAiTurn(params) {
16439
16449
  const isManager = topicRecord?.kind === "manager";
16440
16450
  const isMentionOnlyChannel = topic.aiMode === "mention" && !isManager;
16441
16451
  let systemPrompt = isManager ? buildManagerSystemPrompt(systemPromptOpts) : isMentionOnlyChannel ? buildChannelSystemPrompt(systemPromptOpts) : buildTopicSystemPrompt(systemPromptOpts);
16442
- const memoryTopic = isMentionOnlyChannel || isManager ? topic : getTopicMemoryOrigin(topicId) ?? topic;
16452
+ const memoryTopic = isMentionOnlyChannel || isManager || topic.memoryKey ? topic : getTopicMemoryOrigin(topicId) ?? topic;
16453
+ const memoryTitle = memoryTopic.memoryKey?.trim() || memoryTopic.title;
16443
16454
  if (!isMentionOnlyChannel) {
16455
+ let hasMemoryArchive = false;
16444
16456
  try {
16445
- const resolvedBrief = resolveTopicBrief(memoryTopic.id, memoryTopic.title);
16457
+ const resolvedBrief = resolveTopicBrief(memoryTopic.id, memoryTitle);
16446
16458
  const wikiDir = getSharedWikiDir();
16447
- const { briefFile, hasBriefFile, latestSummaryFile } = resolveWikiMemoryMirror(wikiDir, memoryTopic.id, memoryTopic.title);
16448
- if (resolvedBrief || hasBriefFile || latestSummaryFile) {
16449
- systemPrompt += buildMemoryPromptSection({
16450
- briefFile,
16451
- wikiDir,
16452
- hasFiles: hasBriefFile,
16453
- latestSummaryFile,
16454
- hasArchive: Boolean(resolvedBrief?.brief.latestSummaryMd || latestSummaryFile),
16455
- isManager
16456
- });
16457
- }
16459
+ const { latestSummaryFile } = resolveWikiMemoryMirror(wikiDir, memoryTopic.id, memoryTitle);
16460
+ hasMemoryArchive = Boolean(resolvedBrief?.brief.latestSummaryMd || latestSummaryFile);
16458
16461
  } catch (err) {
16459
16462
  logger.warn({ topicId, err }, "ai: failed to inject topic brief");
16460
16463
  }
16464
+ systemPrompt += buildMemoryPromptSection({
16465
+ topicTitle: topic.title,
16466
+ ...memoryTopic.memoryKey ? { memoryKey: memoryTopic.memoryKey } : {},
16467
+ hasArchive: hasMemoryArchive,
16468
+ isManager
16469
+ });
16461
16470
  }
16462
16471
  const activeVisual = visualTools ? getActiveVisualForPrompt(topicId, userId) : null;
16463
16472
  if (activeVisual) {
@@ -18197,7 +18206,7 @@ import {
18197
18206
  unlinkSync,
18198
18207
  writeFileSync as writeFileSync2
18199
18208
  } from "fs";
18200
- import { basename, dirname as dirname2, join as join2, resolve as resolve2 } from "path";
18209
+ import { basename, dirname as dirname2, join as join2, relative, resolve as resolve2 } from "path";
18201
18210
  init_config();
18202
18211
  init_wiki_summary_names();
18203
18212
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
@@ -18331,9 +18340,314 @@ function topicNameFrom(topicIdStr) {
18331
18340
  function slugify(topic) {
18332
18341
  return wikiSummarySlug(topic);
18333
18342
  }
18343
+ function normalizeSearchText(value2) {
18344
+ return value2.toLowerCase().replaceAll(/[^a-z0-9\uAC00-\uD7A3]+/g, " ").trim().replaceAll(/\s+/g, " ");
18345
+ }
18346
+ function compactSearchText(value2) {
18347
+ return normalizeSearchText(value2).replaceAll(" ", "");
18348
+ }
18349
+ function trigrams(value2) {
18350
+ const compact = compactSearchText(value2);
18351
+ if (compact.length < 3)
18352
+ return new Set(compact ? [compact] : []);
18353
+ const result = new Set;
18354
+ for (let i = 0;i <= compact.length - 3; i += 1)
18355
+ result.add(compact.slice(i, i + 3));
18356
+ return result;
18357
+ }
18358
+ function fuzzySearchScore(query, candidate, maximum) {
18359
+ const compactQuery = compactSearchText(query);
18360
+ const compactCandidate = compactSearchText(candidate);
18361
+ if (compactQuery.length < 3 || compactCandidate.length < 3)
18362
+ return 0;
18363
+ const similarity = trigramSimilarity(compactQuery, compactCandidate);
18364
+ const minimumSimilarity = compactQuery.length <= 4 ? 0.6 : 0.45;
18365
+ return similarity >= minimumSimilarity ? similarity * maximum : 0;
18366
+ }
18367
+ var SEARCH_STOP_WORDS = new Set([
18368
+ "a",
18369
+ "an",
18370
+ "and",
18371
+ "are",
18372
+ "as",
18373
+ "at",
18374
+ "be",
18375
+ "by",
18376
+ "for",
18377
+ "from",
18378
+ "in",
18379
+ "is",
18380
+ "it",
18381
+ "of",
18382
+ "on",
18383
+ "or",
18384
+ "that",
18385
+ "the",
18386
+ "to",
18387
+ "with"
18388
+ ]);
18389
+ var SEARCH_MONTHS = new Map([
18390
+ ["january", "01"],
18391
+ ["jan", "01"],
18392
+ ["1\uC6D4", "01"],
18393
+ ["february", "02"],
18394
+ ["feb", "02"],
18395
+ ["2\uC6D4", "02"],
18396
+ ["march", "03"],
18397
+ ["mar", "03"],
18398
+ ["3\uC6D4", "03"],
18399
+ ["april", "04"],
18400
+ ["apr", "04"],
18401
+ ["4\uC6D4", "04"],
18402
+ ["may", "05"],
18403
+ ["5\uC6D4", "05"],
18404
+ ["june", "06"],
18405
+ ["jun", "06"],
18406
+ ["6\uC6D4", "06"],
18407
+ ["july", "07"],
18408
+ ["jul", "07"],
18409
+ ["7\uC6D4", "07"],
18410
+ ["august", "08"],
18411
+ ["aug", "08"],
18412
+ ["8\uC6D4", "08"],
18413
+ ["september", "09"],
18414
+ ["sep", "09"],
18415
+ ["sept", "09"],
18416
+ ["9\uC6D4", "09"],
18417
+ ["october", "10"],
18418
+ ["oct", "10"],
18419
+ ["10\uC6D4", "10"],
18420
+ ["november", "11"],
18421
+ ["nov", "11"],
18422
+ ["11\uC6D4", "11"],
18423
+ ["december", "12"],
18424
+ ["dec", "12"],
18425
+ ["12\uC6D4", "12"]
18426
+ ]);
18427
+ var SEARCH_RECENCY_TERMS = new Set([
18428
+ "latest",
18429
+ "newest",
18430
+ "recent",
18431
+ "earliest",
18432
+ "oldest",
18433
+ "\uCD5C\uADFC",
18434
+ "\uCD5C\uC2E0",
18435
+ "\uCD5C\uCD08",
18436
+ "\uC624\uB798\uB41C"
18437
+ ]);
18438
+ function meaningfulSearchTokens(value2) {
18439
+ return normalizeSearchText(value2).split(" ").filter((token) => (token.length >= 2 || /^\d+$/.test(token)) && !SEARCH_STOP_WORDS.has(token));
18440
+ }
18441
+ function canonicalSearchToken(token) {
18442
+ return token.length > 4 && token.endsWith("s") ? token.slice(0, -1) : token;
18443
+ }
18444
+ function canonicalSearchTokens(value2) {
18445
+ return meaningfulSearchTokens(value2).map(canonicalSearchToken);
18446
+ }
18447
+ function countToken(tokens, token) {
18448
+ let count = 0;
18449
+ for (const candidate of tokens)
18450
+ if (candidate === token)
18451
+ count += 1;
18452
+ return count;
18453
+ }
18454
+ function countPhrase(text, phrase) {
18455
+ if (!phrase)
18456
+ return 0;
18457
+ let count = 0;
18458
+ let offset = 0;
18459
+ let match = text.indexOf(phrase, offset);
18460
+ while (match >= 0) {
18461
+ count += 1;
18462
+ offset = match + phrase.length;
18463
+ match = text.indexOf(phrase, offset);
18464
+ }
18465
+ return count;
18466
+ }
18467
+ function summaryTopicIdentity(key) {
18468
+ return normalizeSearchText(key.replace(/^\d{4}-\d{2}-\d{2}-/, ""));
18469
+ }
18470
+ function scoreSummaryTime(query, key, title) {
18471
+ const date = key.match(/^(\d{4})-(\d{2})-(\d{2})/);
18472
+ if (!date)
18473
+ return 0;
18474
+ const tokens = meaningfulSearchTokens(query);
18475
+ const documentDate = `${date[1]}-${date[2]}-${date[3]}`;
18476
+ const requestedDateMatch = query.match(/\b(\d{4}) (\d{2}) (\d{2})\b/);
18477
+ const requestedDate = requestedDateMatch ? `${requestedDateMatch[1]}-${requestedDateMatch[2]}-${requestedDateMatch[3]}` : undefined;
18478
+ const beforeDate = /\bbefore\b/.test(query);
18479
+ const afterDate = /\bafter\b/.test(query);
18480
+ if (requestedDate && beforeDate && documentDate >= requestedDate)
18481
+ return null;
18482
+ if (requestedDate && afterDate && documentDate <= requestedDate)
18483
+ return null;
18484
+ const queryYear = tokens.find((token) => /^\d{4}(?:\uB144)?$/.test(token))?.replace(/\uB144$/, "");
18485
+ const queryMonth = tokens.map((token) => SEARCH_MONTHS.get(token)).find(Boolean);
18486
+ const hasRecencyTerm = tokens.some((token) => SEARCH_RECENCY_TERMS.has(token));
18487
+ const yearMatches = !queryYear || queryYear === date[1];
18488
+ const monthMatches = !queryMonth || queryMonth === date[2];
18489
+ if (!queryYear && !queryMonth && !hasRecencyTerm || !yearMatches || !monthMatches)
18490
+ return 0;
18491
+ const normalizedMetadata = `${normalizeSearchText(key)} ${normalizeSearchText(title)}`;
18492
+ const contentTokens = tokens.filter((token) => !/^\d{4}(?:\uB144)?$/.test(token) && !SEARCH_MONTHS.has(token) && !SEARCH_RECENCY_TERMS.has(token));
18493
+ const contentMatches = contentTokens.filter((token) => normalizedMetadata.includes(token)).length;
18494
+ const relationScore = requestedDate && (beforeDate || afterDate) ? 60 : 0;
18495
+ const exactDateScore = requestedDate === documentDate ? 90 : 0;
18496
+ return 30 + relationScore + exactDateScore + contentMatches * 15;
18497
+ }
18498
+ function trigramSimilarity(left, right) {
18499
+ const a = trigrams(left);
18500
+ const b = trigrams(right);
18501
+ if (a.size === 0 || b.size === 0)
18502
+ return 0;
18503
+ let overlap = 0;
18504
+ for (const gram of a)
18505
+ if (b.has(gram))
18506
+ overlap += 1;
18507
+ return 2 * overlap / (a.size + b.size);
18508
+ }
18509
+ function parseWikiIndex(path) {
18510
+ let text;
18511
+ try {
18512
+ text = readFileSync2(path, "utf-8");
18513
+ } catch {
18514
+ return [];
18515
+ }
18516
+ const candidates = [];
18517
+ const entryPattern = /^\s*-\s*\[\[(topic|articles|summaries)\/([^\]]+)\]\]\s*(.*)$/;
18518
+ for (const line of text.split(`
18519
+ `)) {
18520
+ const match = line.match(entryPattern);
18521
+ if (!match)
18522
+ continue;
18523
+ const namespace = match[1];
18524
+ const key = match[2].replace(/\.md$/i, "");
18525
+ const tail = match[3].trim();
18526
+ const dateMatch = tail.match(/\((\d{4}-\d{2}-\d{2})(?:[^)]*)\)\s*$/);
18527
+ const description = dateMatch ? tail.slice(0, dateMatch.index).trim() : tail;
18528
+ candidates.push({
18529
+ kind: namespace === "topic" ? "topic" : namespace === "articles" ? "article" : "summary",
18530
+ key,
18531
+ description,
18532
+ ...dateMatch ? { date: dateMatch[1] } : {}
18533
+ });
18534
+ }
18535
+ return candidates;
18536
+ }
18537
+ function indexCandidateExists(candidate) {
18538
+ if (candidate.kind === "topic")
18539
+ return true;
18540
+ const root = candidate.kind === "article" ? runtime().articlesDir : runtime().summariesDir;
18541
+ const filePath = resolve2(root, `${candidate.key}.md`);
18542
+ const relativePath = relative(root, filePath);
18543
+ if (relativePath.startsWith("..") || relativePath.startsWith("/"))
18544
+ return false;
18545
+ try {
18546
+ return statSync(filePath).isFile();
18547
+ } catch {
18548
+ return false;
18549
+ }
18550
+ }
18551
+ function scoreIndexKey(query, key) {
18552
+ const normalizedQuery = normalizeSearchText(query);
18553
+ const compactQuery = compactSearchText(normalizedQuery);
18554
+ const normalizedKey = normalizeSearchText(key);
18555
+ const compactKey = compactSearchText(normalizedKey);
18556
+ const tokens = meaningfulSearchTokens(normalizedQuery);
18557
+ let score = 0;
18558
+ if (normalizedKey === normalizedQuery)
18559
+ score += 120;
18560
+ else if (compactKey === compactQuery)
18561
+ score += 110;
18562
+ else {
18563
+ if (compactQuery.length >= 2 && (compactKey.startsWith(compactQuery) || compactQuery.startsWith(compactKey))) {
18564
+ score += 65;
18565
+ }
18566
+ if (tokens.length > 0 && compactQuery.length >= 3 && compactKey.includes(compactQuery)) {
18567
+ score += 45;
18568
+ }
18569
+ score += fuzzySearchScore(compactQuery, compactKey, 55);
18570
+ }
18571
+ for (const token of tokens) {
18572
+ if (normalizedKey.includes(token))
18573
+ score += 10;
18574
+ }
18575
+ return score;
18576
+ }
18577
+ function scoreIndexCandidate(query, candidate) {
18578
+ const normalizedQuery = normalizeSearchText(query);
18579
+ if (!normalizedQuery)
18580
+ return 0;
18581
+ const normalizedDescription = normalizeSearchText(candidate.description);
18582
+ const tokens = canonicalSearchTokens(normalizedQuery);
18583
+ const descriptionTokens = new Set(canonicalSearchTokens(normalizedDescription));
18584
+ let score = scoreIndexKey(normalizedQuery, candidate.key);
18585
+ if (tokens.length > 0 && normalizedDescription.includes(normalizedQuery))
18586
+ score += 30;
18587
+ let descriptionMatches = 0;
18588
+ for (const token of tokens) {
18589
+ if (descriptionTokens.has(token)) {
18590
+ score += 5;
18591
+ descriptionMatches += 1;
18592
+ }
18593
+ }
18594
+ if (tokens.length > 1 && descriptionMatches === tokens.length)
18595
+ score += 25;
18596
+ if (candidate.kind === "topic") {
18597
+ const requestsCurrent = /\b(?:current|active|ongoing|open)\b|\uD604\uC7AC|\uC9C4\uD589 \uC911/.test(normalizedQuery);
18598
+ const archivedCandidate = /\b(?:archive|archived|historical|closed)\b|\uBCF4\uAD00|\uC885\uB8CC/.test(`${normalizeSearchText(candidate.key)} ${normalizedDescription}`);
18599
+ if (requestsCurrent && archivedCandidate)
18600
+ score -= 40;
18601
+ }
18602
+ return score;
18603
+ }
18334
18604
  function wikiQuery(args) {
18335
- const question = safeExt(args, ["question", "query", "q", "text"], "");
18336
- const query = question.toLowerCase();
18605
+ const question = String(safeExt(args, ["question", "query", "q", "text"], "")).trim();
18606
+ const requestedKind = String(safeExt(args, ["kind", "type"], "all"));
18607
+ const kind = requestedKind === "topic" || requestedKind === "article" || requestedKind === "summary" ? requestedKind : "all";
18608
+ const requestedLimit = Number(safeExt(args, ["limit", "maxResults"], 8));
18609
+ const limit = Math.min(Math.max(Number.isFinite(requestedLimit) ? requestedLimit : 8, 1), 20);
18610
+ if (!question) {
18611
+ return {
18612
+ content: [{ type: "text", text: "A non-empty wiki query is required." }],
18613
+ isError: true
18614
+ };
18615
+ }
18616
+ const normalizedQuery = normalizeSearchText(question);
18617
+ if (!normalizedQuery) {
18618
+ return { content: [{ type: "text", text: "No matching wiki articles found." }] };
18619
+ }
18620
+ const canReadTopicMemory = runtime().host.canReadTopicMemory;
18621
+ 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;
18622
+ const visibleIndexCandidates = [
18623
+ ...parseWikiIndex(resolve2(runtime().wikiDir, "topic-index.md")),
18624
+ ...parseWikiIndex(resolve2(runtime().wikiDir, "article-index.md"))
18625
+ ].filter((candidate) => kind === "all" || candidate.kind === kind).filter((candidate) => candidate.kind !== "topic" || !canReadTopicMemory || canReadTopicMemory(candidate.key, runtime().userId));
18626
+ const hasStaleExactIndexMatch = visibleIndexCandidates.some((candidate) => !indexCandidateExists(candidate) && scoreIndexKey(question, candidate.key) >= 100);
18627
+ const indexCandidates = visibleIndexCandidates.filter(indexCandidateExists).map((candidate) => ({
18628
+ candidate,
18629
+ keyScore: scoreIndexKey(question, candidate.key),
18630
+ score: scoreIndexCandidate(question, candidate)
18631
+ })).filter(({ keyScore, score }) => keyScore >= 25 || score >= 40).sort((left, right) => right.score - left.score || left.candidate.key.localeCompare(right.candidate.key));
18632
+ const strongestIndex = indexCandidates[0];
18633
+ if (kind === "topic" && strongestIndex && (strongestIndex.keyScore >= 25 || strongestIndex.score >= 40)) {
18634
+ const runnerUp = indexCandidates[1];
18635
+ const ambiguous = strongestIndex.keyScore < 100 && runnerUp !== undefined && runnerUp.score >= strongestIndex.score * 0.9;
18636
+ if (ambiguous) {
18637
+ const lines2 = ["Found 2 ambiguous wiki candidates:", ""];
18638
+ for (const { candidate, score } of [strongestIndex, runnerUp]) {
18639
+ lines2.push(`- kind: ${candidate.kind}`, ` key: ${candidate.key}`, ` score: ${score.toFixed(2)}`, ` description: ${candidate.description || "(none)"}`, ...candidate.date ? [` date: ${candidate.date}`] : []);
18640
+ }
18641
+ return { content: [{ type: "text", text: lines2.join(`
18642
+ `) }] };
18643
+ }
18644
+ const lines = ["Found 1 wiki candidate:", ""];
18645
+ for (const { candidate, score } of [strongestIndex]) {
18646
+ lines.push(`- kind: ${candidate.kind}`, ` key: ${candidate.key}`, ` score: ${score.toFixed(2)}`, ` description: ${candidate.description || "(none)"}`, ...candidate.date ? [` date: ${candidate.date}`] : []);
18647
+ }
18648
+ return { content: [{ type: "text", text: lines.join(`
18649
+ `) }] };
18650
+ }
18337
18651
  const results = [];
18338
18652
  const scored = [];
18339
18653
  function scan(dir, label) {
@@ -18350,67 +18664,234 @@ function wikiQuery(args) {
18350
18664
  const fp = join2(sub, f.name);
18351
18665
  try {
18352
18666
  const text = readFileSync2(fp, "utf-8");
18667
+ const key = `${entry.name}/${f.name.replace(/\.md$/i, "")}`;
18353
18668
  scored.push({
18354
- score: scoreMatch(text),
18669
+ kind: label === "articles" ? "article" : "skill",
18670
+ key,
18671
+ keyScore: scoreIndexKey(question, key),
18672
+ score: scoreMatch(text, key, label === "articles" ? "article" : "skill"),
18355
18673
  path: `${label}/${entry.name}/${f.name}`,
18356
- text
18674
+ text,
18675
+ title: titleFromDocument(text, key)
18357
18676
  });
18358
18677
  } catch {}
18359
18678
  }
18360
18679
  } catch {}
18361
18680
  }
18362
18681
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
18682
+ if (label === "topic" && canReadTopicMemory && !canReadTopicMemory(entry.name.replace(/\.md$/i, ""), runtime().userId)) {
18683
+ continue;
18684
+ }
18363
18685
  const fp = join2(dir, entry.name);
18364
18686
  try {
18365
18687
  const text = readFileSync2(fp, "utf-8");
18688
+ const key = entry.name.replace(/\.md$/i, "");
18689
+ const resultKind = label === "articles" ? "article" : label === "topic" ? "topic" : label === "summaries" ? "summary" : "skill";
18366
18690
  scored.push({
18367
- score: scoreMatch(text),
18691
+ kind: resultKind,
18692
+ key,
18693
+ keyScore: scoreIndexKey(question, key),
18694
+ score: scoreMatch(text, key, resultKind),
18368
18695
  path: `${label}/${entry.name}`,
18369
- text
18696
+ text,
18697
+ title: titleFromDocument(text, key),
18698
+ ...label === "summaries" && /^\d{4}-\d{2}-\d{2}/.test(key) ? { date: key.slice(0, 10) } : {}
18370
18699
  });
18371
18700
  } catch {}
18372
18701
  }
18373
18702
  }
18374
18703
  } catch {}
18375
18704
  }
18376
- function scoreMatch(text) {
18377
- const lower = text.toLowerCase();
18705
+ function titleFromDocument(text, key) {
18706
+ const titleLine = text.split(`
18707
+ `).find((line) => /^#+\s*/.test(line));
18708
+ return titleLine ? titleLine.replace(/^#+\s*/, "") : basename(key);
18709
+ }
18710
+ function scoreMatch(text, key, resultKind) {
18711
+ const titleLine = text.split(`
18712
+ `).find((line) => /^#+\s*/.test(line)) ?? "";
18713
+ const title = titleLine.replace(/^#+\s*/, "");
18714
+ const normalizedKey = normalizeSearchText(key);
18715
+ const normalizedTitle = normalizeSearchText(title);
18716
+ const normalizedBody = normalizeSearchText(text.replace(titleLine, ""));
18717
+ const compactQuery = compactSearchText(normalizedQuery);
18718
+ const compactKey = compactSearchText(normalizedKey);
18719
+ const compactTitle = compactSearchText(normalizedTitle);
18720
+ const tokens = canonicalSearchTokens(normalizedQuery);
18721
+ const keyTokens = new Set(canonicalSearchTokens(normalizedKey));
18722
+ const titleTokens = new Set(canonicalSearchTokens(normalizedTitle));
18723
+ const bodyTokens = canonicalSearchTokens(normalizedBody);
18378
18724
  let score = 0;
18379
- const words = query.split(/\s+/).filter(Boolean);
18380
- for (const w of words) {
18381
- if (lower.includes(w)) {
18382
- score += w.length >= 3 ? 3 : 1;
18383
- const firstLine = lower.split(`
18384
- `)[0];
18385
- if (firstLine.startsWith("#") && firstLine.includes(w))
18386
- score += 5;
18725
+ let strongMetadataEvidence = false;
18726
+ if (normalizedKey === normalizedQuery) {
18727
+ score += 120;
18728
+ strongMetadataEvidence = true;
18729
+ } else if (compactKey === compactQuery) {
18730
+ score += 110;
18731
+ strongMetadataEvidence = true;
18732
+ } else if (compactQuery.length >= 2 && (compactKey.startsWith(compactQuery) || compactQuery.startsWith(compactKey))) {
18733
+ score += 65;
18734
+ strongMetadataEvidence = true;
18735
+ }
18736
+ if (tokens.length > 0 && compactQuery.length >= 3 && compactKey.includes(compactQuery)) {
18737
+ score += 45;
18738
+ strongMetadataEvidence = true;
18739
+ }
18740
+ const fuzzyKeyScore = fuzzySearchScore(compactQuery, compactKey, 55);
18741
+ score += fuzzyKeyScore;
18742
+ if (fuzzyKeyScore > 0)
18743
+ strongMetadataEvidence = true;
18744
+ if (normalizedTitle === normalizedQuery) {
18745
+ score += 115;
18746
+ strongMetadataEvidence = true;
18747
+ } else if (compactTitle === compactQuery) {
18748
+ score += 105;
18749
+ strongMetadataEvidence = true;
18750
+ } else if (compactQuery.length >= 2 && compactTitle.startsWith(compactQuery)) {
18751
+ score += 60;
18752
+ strongMetadataEvidence = true;
18753
+ }
18754
+ const fuzzyTitleScore = fuzzySearchScore(compactQuery, compactTitle, 50);
18755
+ score += fuzzyTitleScore;
18756
+ if (fuzzyTitleScore > 0)
18757
+ strongMetadataEvidence = true;
18758
+ if (resultKind === "summary") {
18759
+ const temporalScore = scoreSummaryTime(normalizedQuery, key, title);
18760
+ if (temporalScore === null)
18761
+ return 0;
18762
+ score += temporalScore;
18763
+ if (temporalScore > 0)
18764
+ strongMetadataEvidence = true;
18765
+ }
18766
+ const isMultiToken = tokens.length > 1;
18767
+ if (tokens.length > 0 && normalizedTitle.includes(normalizedQuery)) {
18768
+ score += 35;
18769
+ strongMetadataEvidence = true;
18770
+ }
18771
+ const phraseMatches = isMultiToken ? countPhrase(normalizedBody, normalizedQuery) : 0;
18772
+ if (phraseMatches > 0)
18773
+ score += phraseMatches * 12;
18774
+ else if (tokens.length === 1 && tokens[0].length >= 5 && normalizedBody.includes(tokens[0])) {
18775
+ score += 16;
18776
+ }
18777
+ let bodyMatches = 0;
18778
+ let minimumBodyFrequency = Number.POSITIVE_INFINITY;
18779
+ for (const token of tokens) {
18780
+ if (keyTokens.has(token))
18781
+ score += 10;
18782
+ if (titleTokens.has(token))
18783
+ score += 12;
18784
+ const bodyFrequency = countToken(bodyTokens, token);
18785
+ if (bodyFrequency > 0) {
18786
+ score += Math.min(bodyFrequency, 3) * 4;
18787
+ bodyMatches += 1;
18788
+ minimumBodyFrequency = Math.min(minimumBodyFrequency, bodyFrequency);
18387
18789
  }
18388
18790
  }
18791
+ if (isMultiToken && bodyMatches === tokens.length) {
18792
+ score += 8 * Math.min(minimumBodyFrequency, 3);
18793
+ }
18794
+ const minimumBodyTokens = hasStaleExactIndexMatch ? 3 : 2;
18795
+ const partialArticleEvidence = resultKind === "article" && bodyMatches >= 3 && bodyMatches / tokens.length >= 0.6;
18796
+ const strongBodyEvidence = tokens.length >= minimumBodyTokens && (bodyMatches === tokens.length && (phraseMatches > 0 || minimumBodyFrequency >= 1) || partialArticleEvidence);
18797
+ if (!strongMetadataEvidence && !strongBodyEvidence)
18798
+ return 0;
18389
18799
  return score;
18390
18800
  }
18391
- scan(runtime().articlesDir, "articles");
18392
- if (runtime().surface === "all")
18801
+ if (kind === "all" || kind === "article")
18802
+ scan(runtime().articlesDir, "articles");
18803
+ if (kind === "all" && runtime().surface === "all")
18393
18804
  scan(runtime().skillsDir, "skills");
18394
- scan(runtime().topicsDir, "topic");
18395
- scan(runtime().summariesDir, "summaries");
18396
- scored.sort((a, b) => b.score - a.score);
18397
- const top = scored.slice(0, 8);
18398
- if (top.length === 0) {
18805
+ if (kind === "all" || kind === "topic")
18806
+ scan(runtime().topicsDir, "topic");
18807
+ if (kind === "all" || kind === "summary")
18808
+ scan(runtime().summariesDir, "summaries");
18809
+ const merged = new Map;
18810
+ for (const { candidate, keyScore, score } of indexCandidates) {
18811
+ merged.set(`${candidate.kind}:${candidate.key}`, {
18812
+ kind: candidate.kind,
18813
+ key: candidate.key,
18814
+ keyScore,
18815
+ score,
18816
+ description: candidate.description,
18817
+ date: candidate.date
18818
+ });
18819
+ }
18820
+ const documentThreshold = kind === "topic" ? 24 : 16;
18821
+ for (const document of scored) {
18822
+ if (document.score < documentThreshold)
18823
+ continue;
18824
+ const id = `${document.kind}:${document.key}`;
18825
+ const indexed = merged.get(id);
18826
+ merged.set(id, indexed ? { ...indexed, ...document, score: Math.max(indexed.score, document.score) } : document);
18827
+ }
18828
+ 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));
18829
+ if (kind === "summary") {
18830
+ const sameTopic = ranked.filter((result) => {
18831
+ const identity = summaryTopicIdentity(result.key);
18832
+ return identity.length >= 3 && normalizedQuery.includes(identity);
18833
+ });
18834
+ if (sameTopic.length > 0)
18835
+ ranked = sameTopic;
18836
+ const requestedDateMatch = normalizedQuery.match(/\b(\d{4}) (\d{2}) (\d{2})\b/);
18837
+ const requestedDate = requestedDateMatch ? `${requestedDateMatch[1]}-${requestedDateMatch[2]}-${requestedDateMatch[3]}` : undefined;
18838
+ const beforeDate = /\bbefore\b/.test(normalizedQuery);
18839
+ const afterDate = /\bafter\b/.test(normalizedQuery);
18840
+ if (requestedDate && beforeDate) {
18841
+ ranked = ranked.filter((result) => !result.date || result.date < requestedDate);
18842
+ } else if (requestedDate && afterDate) {
18843
+ ranked = ranked.filter((result) => !result.date || result.date > requestedDate);
18844
+ }
18845
+ ranked.sort((left, right) => {
18846
+ if (beforeDate) {
18847
+ return (right.date ?? "").localeCompare(left.date ?? "") || right.score - left.score;
18848
+ }
18849
+ if (afterDate) {
18850
+ return (left.date ?? "").localeCompare(right.date ?? "") || right.score - left.score;
18851
+ }
18852
+ if (requestedDate) {
18853
+ const dateDifference = Number(right.date === requestedDate) - Number(left.date === requestedDate);
18854
+ if (dateDifference !== 0)
18855
+ return dateDifference;
18856
+ }
18857
+ if (temporalDirection) {
18858
+ const dateDifference = temporalDirection === "oldest" ? (left.date ?? "").localeCompare(right.date ?? "") : (right.date ?? "").localeCompare(left.date ?? "");
18859
+ if (dateDifference !== 0)
18860
+ return dateDifference;
18861
+ }
18862
+ return right.score - left.score || (right.date ?? "").localeCompare(left.date ?? "") || left.key.localeCompare(right.key);
18863
+ });
18864
+ }
18865
+ if (kind === "topic" && ranked[0]) {
18866
+ const runnerUp = ranked[1];
18867
+ const ambiguous = ranked[0].keyScore < 100 && runnerUp !== undefined && runnerUp.score >= ranked[0].score * 0.9;
18868
+ if (ambiguous) {
18869
+ ranked = ranked.slice(0, 2);
18870
+ } else {
18871
+ ranked = ranked.slice(0, 1);
18872
+ }
18873
+ } else {
18874
+ ranked = ranked.slice(0, limit);
18875
+ }
18876
+ if (ranked.length === 0) {
18399
18877
  results.push("No matching wiki articles found.");
18400
18878
  } else {
18401
- results.push(`Found ${top.length} matching result(s):
18879
+ results.push(`Found ${ranked.length} matching result(s):
18402
18880
  `);
18403
- for (const { path, text } of top) {
18404
- const titleLine = text.split(`
18405
- `).find((l) => l.startsWith("#"));
18406
- const title = titleLine ? titleLine.replace(/^#+\s*/, "") : basename(path, ".md");
18407
- const body = text.replace(/^---[\s\S]*?---\n?/, "").replace(/^#.*$/m, "").trim().slice(0, 400);
18408
- results.push(`## ${title}
18409
- Path: ${path}
18881
+ for (const result of ranked) {
18882
+ if (result.kind === "skill") {
18883
+ const body = (result.text ?? "").trim().slice(0, 400);
18884
+ results.push(`## ${result.title ?? result.key}
18885
+ Path: ${result.path}
18410
18886
 
18411
- ${body}${text.length > 400 ? `
18887
+ ${body}${(result.text?.length ?? 0) > 400 ? `
18412
18888
  ...(truncated)` : ""}
18413
18889
  `);
18890
+ continue;
18891
+ }
18892
+ 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 ? [
18893
+ ` excerpt: ${result.text.replace(/^---[\s\S]*?---\n?/, "").replace(/^#.*$/m, "").trim().slice(0, 400)}`
18894
+ ] : []);
18414
18895
  }
18415
18896
  }
18416
18897
  return { content: [{ type: "text", text: results.join(`
@@ -18429,36 +18910,94 @@ function wikiTopicBrief(args) {
18429
18910
  };
18430
18911
  }
18431
18912
  const { getTopicBrief, resolveTopicBrief } = runtime().host;
18432
- if (runtime().topicId && (getTopicBrief || resolveTopicBrief)) {
18913
+ const hasAuthorizedBridge = Boolean(runtime().topicId && (getTopicBrief || resolveTopicBrief));
18914
+ if (hasAuthorizedBridge) {
18433
18915
  try {
18434
18916
  const brief = resolveTopicBrief ? resolveTopicBrief(rawTopic, runtime().userId) : getTopicBrief(rawTopic);
18435
- if (!brief) {
18436
- return {
18437
- content: [{ type: "text", text: `No brief found for topic: ${rawTopic}` }]
18438
- };
18439
- }
18440
- const lines = [
18441
- `# Topic Brief: ${topicNameFrom(rawTopic)}`,
18442
- "",
18443
- brief.briefMd || "(empty brief)"
18444
- ];
18445
- if (brief.latestSummaryMd) {
18446
- lines.push("", "## Latest Summary", "", brief.latestSummaryMd);
18447
- }
18448
- if (brief.summaryDate) {
18449
- lines.push("", `Summary date: ${brief.summaryDate}`);
18450
- }
18451
- return { content: [{ type: "text", text: lines.join(`
18917
+ if (brief) {
18918
+ const lines = [
18919
+ `# Topic Brief: ${topicNameFrom(rawTopic)}`,
18920
+ "",
18921
+ brief.briefMd || "(empty brief)"
18922
+ ];
18923
+ if (brief.latestSummaryMd) {
18924
+ lines.push("", "## Latest Summary", "", brief.latestSummaryMd);
18925
+ }
18926
+ if (brief.summaryDate) {
18927
+ lines.push("", `Summary date: ${brief.summaryDate}`);
18928
+ }
18929
+ return { content: [{ type: "text", text: lines.join(`
18452
18930
  `) }] };
18453
- } catch {
18931
+ }
18932
+ } catch {}
18933
+ return {
18934
+ content: [{ type: "text", text: `No brief found for topic: ${rawTopic}` }]
18935
+ };
18936
+ }
18937
+ const topicKey = wikiSummarySlug(rawTopic.replace(/^topic\//, "").replace(/\.md$/i, ""));
18938
+ const filePath = resolve2(runtime().topicsDir, `${topicKey}.md`);
18939
+ try {
18940
+ const content = readFileSync2(filePath, "utf-8");
18941
+ return {
18942
+ content: [{ type: "text", text: `# Topic Brief: ${topicKey}
18943
+
18944
+ ${content}` }]
18945
+ };
18946
+ } catch {}
18947
+ return {
18948
+ content: [{ type: "text", text: `No brief found for topic: ${rawTopic}` }]
18949
+ };
18950
+ }
18951
+ function wikiRead(args) {
18952
+ const rawKind = String(safeExt(args, ["kind", "type"], "article"));
18953
+ const kind = rawKind === "topic" || rawKind === "summary" ? rawKind : "article";
18954
+ const rawKey = String(safeExt(args, ["key", "slug", "topic"], "")).trim();
18955
+ if (!rawKey) {
18956
+ return { content: [{ type: "text", text: "A wiki document key is required." }], isError: true };
18957
+ }
18958
+ if (kind === "topic") {
18959
+ const result = wikiTopicBrief({ topic: rawKey });
18960
+ const resultText = result.content.map((entry) => entry.type === "text" ? entry.text : "").join(`
18961
+ `);
18962
+ const adopt = safeExt(args, ["adopt", "use", "select"], false);
18963
+ if (!adopt || resultText.startsWith("No brief found for topic"))
18964
+ return result;
18965
+ const currentTopicId = runtime().currentTopicId;
18966
+ const memoryKey = wikiSummarySlug(rawKey.replace(/^topic\//, "").replace(/\.md$/i, ""));
18967
+ if (!currentTopicId || !runtime().host.adoptTopicMemory?.(currentTopicId, runtime().userId, memoryKey)) {
18454
18968
  return {
18455
- content: [{ type: "text", text: `Could not read topic brief for: ${rawTopic}` }]
18969
+ content: [
18970
+ ...result.content,
18971
+ { type: "text", text: `
18972
+ Memory was read but could not be adopted by this topic.` }
18973
+ ]
18456
18974
  };
18457
18975
  }
18976
+ return {
18977
+ content: [...result.content, { type: "text", text: `
18978
+ Adopted topic memory: ${memoryKey}` }]
18979
+ };
18980
+ }
18981
+ if (rawKey.includes("\\") || rawKey.split("/").some((part) => part === "..") || rawKey.startsWith("/")) {
18982
+ return { content: [{ type: "text", text: "Invalid wiki document key." }], isError: true };
18983
+ }
18984
+ const root = kind === "summary" ? runtime().summariesDir : runtime().articlesDir;
18985
+ const key = rawKey.replace(new RegExp(`^(?:${kind === "summary" ? "summaries" : "articles"})/`), "").replace(/\.md$/i, "");
18986
+ const filePath = resolve2(root, `${key}.md`);
18987
+ const relativePath = relative(root, filePath);
18988
+ if (relativePath.startsWith("..") || relativePath.startsWith("/")) {
18989
+ return { content: [{ type: "text", text: "Invalid wiki document key." }], isError: true };
18990
+ }
18991
+ try {
18992
+ const content = readFileSync2(filePath, "utf-8");
18993
+ return {
18994
+ content: [{ type: "text", text: `# Wiki ${kind}: ${key}
18995
+
18996
+ ${content}` }]
18997
+ };
18998
+ } catch {
18999
+ return { content: [{ type: "text", text: `No ${kind} found for: ${rawKey}` }] };
18458
19000
  }
18459
- return {
18460
- content: [{ type: "text", text: `No topic brief found for: ${rawTopic}` }]
18461
- };
18462
19001
  }
18463
19002
  function wikiLastConversation(args) {
18464
19003
  const rawTopic = safeExt(args, ["topic", "topicName", "topicId"], runtime().topicId);
@@ -18825,7 +19364,7 @@ function indexUpsert(args) {
18825
19364
  var WIKI_TOOLS = [
18826
19365
  {
18827
19366
  name: "wiki_query",
18828
- description: "Search the wiki knowledge base and return relevant articles.",
19367
+ description: "Search topic, article, or summary indexes and return relevant candidates.",
18829
19368
  inputSchema: {
18830
19369
  type: "object",
18831
19370
  properties: {
@@ -18836,11 +19375,38 @@ var WIKI_TOOLS = [
18836
19375
  topic: {
18837
19376
  type: "string",
18838
19377
  description: "Optional topic name to narrow the search"
19378
+ },
19379
+ kind: {
19380
+ type: "string",
19381
+ enum: ["all", "topic", "article", "summary"],
19382
+ description: "Document kind to search; defaults to all"
19383
+ },
19384
+ limit: {
19385
+ type: "number",
19386
+ minimum: 1,
19387
+ maximum: 20,
19388
+ description: "Maximum number of candidates; defaults to 8"
18839
19389
  }
18840
19390
  },
18841
19391
  required: ["question"]
18842
19392
  }
18843
19393
  },
19394
+ {
19395
+ name: "wiki_read",
19396
+ description: "Read one topic, article, or summary returned by wiki_query. For a clearly matching continuing persona, set adopt=true when reading a topic.",
19397
+ inputSchema: {
19398
+ type: "object",
19399
+ properties: {
19400
+ kind: { type: "string", enum: ["topic", "article", "summary"] },
19401
+ key: { type: "string", description: "Canonical key returned by wiki_query" },
19402
+ adopt: {
19403
+ type: "boolean",
19404
+ description: "For kind=topic, persist this canonical memory key for the current room"
19405
+ }
19406
+ },
19407
+ required: ["kind", "key"]
19408
+ }
19409
+ },
18844
19410
  {
18845
19411
  name: "wiki_topic_brief",
18846
19412
  description: "Get the lightweight topic brief for a specific topic.",
@@ -18973,6 +19539,7 @@ function createWikiMcpServer(context, host) {
18973
19539
  const wikiDir = resolve2(host.wikiRoot);
18974
19540
  const current = {
18975
19541
  userId: context.userId,
19542
+ currentTopicId: context.currentTopicId ?? context.topicId,
18976
19543
  topicId: context.topicId,
18977
19544
  surface,
18978
19545
  host,
@@ -19002,6 +19569,7 @@ function createWikiMcpServer(context, host) {
19002
19569
  const { name, arguments: args } = request.params;
19003
19570
  const handlers = {
19004
19571
  wiki_query: wikiQuery,
19572
+ wiki_read: wikiRead,
19005
19573
  wiki_topic_brief: wikiTopicBrief,
19006
19574
  wiki_last_conversation: wikiLastConversation,
19007
19575
  skill_query: skillQuery,
@@ -19943,4 +20511,4 @@ export {
19943
20511
  createAgentHealthMcpServer
19944
20512
  };
19945
20513
 
19946
- //# debugId=AD1DC675E382EEA164756E2164756E21
20514
+ //# debugId=AE60D4AF576C043E64756E2164756E21