negotium 0.2.18 → 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.
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.18";
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";
@@ -24920,6 +24920,219 @@ var init_vault2 = __esm(() => {
24920
24920
  init_mcp_helpers();
24921
24921
  });
24922
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
+
24923
25136
  // ../../packages/core/src/mcp/wiki-server.ts
24924
25137
  var exports_wiki_server = {};
24925
25138
  __export(exports_wiki_server, {
@@ -24930,14 +25143,14 @@ import { AsyncLocalStorage as AsyncLocalStorage3 } from "async_hooks";
24930
25143
  import { randomUUID as randomUUID21 } from "crypto";
24931
25144
  import {
24932
25145
  closeSync as closeSync4,
24933
- existsSync as existsSync26,
25146
+ existsSync as existsSync27,
24934
25147
  mkdirSync as mkdirSync24,
24935
25148
  openSync as openSync4,
24936
25149
  readdirSync as readdirSync10,
24937
- readFileSync as readFileSync22,
25150
+ readFileSync as readFileSync23,
24938
25151
  renameSync as renameSync11,
24939
25152
  statSync as statSync15,
24940
- unlinkSync as unlinkSync18,
25153
+ unlinkSync as unlinkSync19,
24941
25154
  writeFileSync as writeFileSync17
24942
25155
  } from "fs";
24943
25156
  import { basename as basename7, dirname as dirname16, join as join36, relative as relative2, resolve as resolve17 } from "path";
@@ -24980,15 +25193,15 @@ function acquireRecoveryGuard(lockPath) {
24980
25193
  } catch (error2) {
24981
25194
  closeSync4(fd);
24982
25195
  try {
24983
- unlinkSync18(guardPath);
25196
+ unlinkSync19(guardPath);
24984
25197
  } catch {}
24985
25198
  throw error2;
24986
25199
  }
24987
25200
  return () => {
24988
25201
  closeSync4(fd);
24989
25202
  try {
24990
- if (readFileSync22(guardPath, "utf-8") === ownerToken)
24991
- unlinkSync18(guardPath);
25203
+ if (readFileSync23(guardPath, "utf-8") === ownerToken)
25204
+ unlinkSync19(guardPath);
24992
25205
  } catch {}
24993
25206
  };
24994
25207
  } catch (error2) {
@@ -25013,7 +25226,7 @@ function acquireFileLock(path) {
25013
25226
  } catch (error2) {
25014
25227
  closeSync4(fd);
25015
25228
  try {
25016
- unlinkSync18(lockPath);
25229
+ unlinkSync19(lockPath);
25017
25230
  } catch {}
25018
25231
  throw error2;
25019
25232
  }
@@ -25021,8 +25234,8 @@ function acquireFileLock(path) {
25021
25234
  closeSync4(fd);
25022
25235
  const releaseGuard = acquireRecoveryGuard(lockPath);
25023
25236
  try {
25024
- if (readFileSync22(lockPath, "utf-8") === ownerToken)
25025
- unlinkSync18(lockPath);
25237
+ if (readFileSync23(lockPath, "utf-8") === ownerToken)
25238
+ unlinkSync19(lockPath);
25026
25239
  } catch {} finally {
25027
25240
  releaseGuard();
25028
25241
  }
@@ -25033,9 +25246,9 @@ function acquireFileLock(path) {
25033
25246
  const releaseGuard = acquireRecoveryGuard(lockPath);
25034
25247
  try {
25035
25248
  try {
25036
- const staleToken = readFileSync22(lockPath, "utf-8");
25037
- if (Date.now() - statSync15(lockPath).mtimeMs > FILE_LOCK_STALE_MS && readFileSync22(lockPath, "utf-8") === staleToken) {
25038
- 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);
25039
25252
  continue;
25040
25253
  }
25041
25254
  } catch {
@@ -25058,7 +25271,7 @@ function atomicWriteFile(path, content) {
25058
25271
  renameSync11(temporaryPath, path);
25059
25272
  } catch (error2) {
25060
25273
  try {
25061
- unlinkSync18(temporaryPath);
25274
+ unlinkSync19(temporaryPath);
25062
25275
  } catch {}
25063
25276
  throw error2;
25064
25277
  }
@@ -25174,10 +25387,77 @@ function trigramSimilarity(left, right) {
25174
25387
  overlap += 1;
25175
25388
  return 2 * overlap / (a.size + b.size);
25176
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
+ }
25177
25457
  function parseWikiIndex(path) {
25178
25458
  let text2;
25179
25459
  try {
25180
- text2 = readFileSync22(path, "utf-8");
25460
+ text2 = readFileSync23(path, "utf-8");
25181
25461
  } catch {
25182
25462
  return [];
25183
25463
  }
@@ -25331,7 +25611,7 @@ function wikiQuery(args) {
25331
25611
  continue;
25332
25612
  const fp = join36(sub, f.name);
25333
25613
  try {
25334
- const text2 = readFileSync22(fp, "utf-8");
25614
+ const text2 = readFileSync23(fp, "utf-8");
25335
25615
  const key = `${entry.name}/${f.name.replace(/\.md$/i, "")}`;
25336
25616
  scored.push({
25337
25617
  kind: label === "articles" ? "article" : "skill",
@@ -25352,7 +25632,7 @@ function wikiQuery(args) {
25352
25632
  }
25353
25633
  const fp = join36(dir, entry.name);
25354
25634
  try {
25355
- const text2 = readFileSync22(fp, "utf-8");
25635
+ const text2 = readFileSync23(fp, "utf-8");
25356
25636
  const key = entry.name.replace(/\.md$/i, "");
25357
25637
  const resultKind = label === "articles" ? "article" : label === "topic" ? "topic" : label === "summaries" ? "summary" : "skill";
25358
25638
  scored.push({
@@ -25466,14 +25746,45 @@ function wikiQuery(args) {
25466
25746
  return 0;
25467
25747
  return score;
25468
25748
  }
25469
- if (kind === "all" || kind === "article")
25470
- scan(runtime().articlesDir, "articles");
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();
25781
+ }
25782
+ }
25783
+ }
25471
25784
  if (kind === "all" && runtime().surface === "all")
25472
25785
  scan(runtime().skillsDir, "skills");
25473
25786
  if (kind === "all" || kind === "topic")
25474
25787
  scan(runtime().topicsDir, "topic");
25475
- if (kind === "all" || kind === "summary")
25476
- scan(runtime().summariesDir, "summaries");
25477
25788
  const merged = new Map;
25478
25789
  for (const { candidate, keyScore, score } of indexCandidates) {
25479
25790
  merged.set(`${candidate.kind}:${candidate.key}`, {
@@ -25605,7 +25916,7 @@ function wikiTopicBrief(args) {
25605
25916
  const topicKey = wikiSummarySlug(rawTopic.replace(/^topic\//, "").replace(/\.md$/i, ""));
25606
25917
  const filePath = resolve17(runtime().topicsDir, `${topicKey}.md`);
25607
25918
  try {
25608
- const content = readFileSync22(filePath, "utf-8");
25919
+ const content = readFileSync23(filePath, "utf-8");
25609
25920
  return {
25610
25921
  content: [{ type: "text", text: `# Topic Brief: ${topicKey}
25611
25922
 
@@ -25657,7 +25968,7 @@ Adopted topic memory: ${memoryKey}` }]
25657
25968
  return { content: [{ type: "text", text: "Invalid wiki document key." }], isError: true };
25658
25969
  }
25659
25970
  try {
25660
- const content = readFileSync22(filePath, "utf-8");
25971
+ const content = readFileSync23(filePath, "utf-8");
25661
25972
  return {
25662
25973
  content: [{ type: "text", text: `# Wiki ${kind}: ${key}
25663
25974
 
@@ -25677,18 +25988,18 @@ function wikiLastConversation(args) {
25677
25988
  const name = slugify(topicNameFrom(rawTopic));
25678
25989
  const archiveDir = resolve17(runtime().archiveDir, name);
25679
25990
  try {
25680
- 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() : [];
25681
25992
  if (files.length === 0) {
25682
- 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() : [];
25683
25994
  }
25684
25995
  if (files.length === 0) {
25685
25996
  return {
25686
25997
  content: [{ type: "text", text: `No archive found for topic: ${rawTopic}` }]
25687
25998
  };
25688
25999
  }
25689
- 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]);
25690
26001
  try {
25691
- const raw = readFileSync22(actualPath, "utf-8");
26002
+ const raw = readFileSync23(actualPath, "utf-8");
25692
26003
  const lines = raw.trim().split(`
25693
26004
  `).filter(Boolean);
25694
26005
  const turns2 = [];
@@ -25761,7 +26072,7 @@ function skillQuery(args) {
25761
26072
  continue;
25762
26073
  const skillFile = resolve17(runtime().skillsDir, entry.name, "skill.md");
25763
26074
  try {
25764
- const text2 = readFileSync22(skillFile, "utf-8");
26075
+ const text2 = readFileSync23(skillFile, "utf-8");
25765
26076
  const lower = text2.toLowerCase();
25766
26077
  let score = 0;
25767
26078
  for (const w of query2.split(/\s+/).filter(Boolean)) {
@@ -25832,7 +26143,7 @@ ${fresh.join(`
25832
26143
  const skillPath = resolve17(skillDir, "skill.md");
25833
26144
  let finalContent = content;
25834
26145
  try {
25835
- const existing = readFileSync22(skillPath, "utf-8");
26146
+ const existing = readFileSync23(skillPath, "utf-8");
25836
26147
  finalContent = mergeGotchas(content, extractGotchas(existing));
25837
26148
  } catch {}
25838
26149
  writeFileSync17(skillPath, finalContent, "utf-8");
@@ -25845,15 +26156,7 @@ ${fresh.join(`
25845
26156
  ]
25846
26157
  };
25847
26158
  }
25848
- function saveWikiEntry(args) {
25849
- const rawTopic = safeExt(args, ["topic", "topicName"], "");
25850
- const content = safeExt(args, ["content", "text", "body"], "");
25851
- if (!rawTopic)
25852
- return { content: [{ type: "text", text: "Missing topic." }] };
25853
- if (!content)
25854
- return { content: [{ type: "text", text: "Missing content." }] };
25855
- const now = new Date;
25856
- const dateStr = now.toISOString().slice(0, 10);
26159
+ function writeSummaryDocument(rawTopic, content, dateStr) {
25857
26160
  const topicId = runtime().topicId;
25858
26161
  const fileSlug = wikiBriefStorageKey(rawTopic, topicId);
25859
26162
  ensureDir(runtime().summariesDir);
@@ -25880,7 +26183,7 @@ function saveWikiEntry(args) {
25880
26183
  if (topicId && setTopicBrief2) {
25881
26184
  try {
25882
26185
  const briefPath = resolve17(runtime().topicsDir, `${fileSlug}.md`);
25883
- let briefMd = existsSync26(briefPath) ? readFileSync22(briefPath, "utf-8") : undefined;
26186
+ let briefMd = existsSync27(briefPath) ? readFileSync23(briefPath, "utf-8") : undefined;
25884
26187
  if (briefMd === undefined && getTopicBrief2 && !getTopicBrief2(fileSlug)?.briefMd) {
25885
26188
  const legacy = getTopicBrief2(topicId)?.briefMd;
25886
26189
  if (legacy)
@@ -25894,28 +26197,14 @@ function saveWikiEntry(args) {
25894
26197
  sqliteUpdated = true;
25895
26198
  } catch {}
25896
26199
  }
25897
- return {
25898
- content: [
25899
- {
25900
- type: "text",
25901
- text: `Saved summary: summaries/${summaryName}` + (sqliteUpdated ? `
25902
- SQLite latest-summary also updated.` : "")
25903
- }
25904
- ]
25905
- };
26200
+ return { slug: summaryName.replace(/\.md$/i, ""), sqliteUpdated };
25906
26201
  }
25907
- function saveTopicBrief(args) {
25908
- const rawTopic = safeExt(args, ["topic", "topicName"], "");
25909
- const content = safeExt(args, ["content", "brief", "text", "body"], "");
25910
- if (!rawTopic)
25911
- return { content: [{ type: "text", text: "Missing topic." }] };
25912
- if (!content)
25913
- return { content: [{ type: "text", text: "Missing content." }] };
26202
+ function writeTopicDocument(rawTopic, content) {
25914
26203
  const topicId = runtime().topicId;
25915
26204
  const fileSlug = wikiBriefStorageKey(rawTopic, topicId);
25916
26205
  const briefPath = resolve17(runtime().topicsDir, `${fileSlug}.md`);
25917
26206
  ensureDir(dirname16(briefPath));
25918
- const existed = existsSync26(briefPath);
26207
+ const existed = existsSync27(briefPath);
25919
26208
  writeFileSync17(briefPath, content, "utf-8");
25920
26209
  const setTopicBrief2 = runtime().host.setTopicBrief;
25921
26210
  let sqliteUpdated = false;
@@ -25925,68 +26214,227 @@ function saveTopicBrief(args) {
25925
26214
  sqliteUpdated = true;
25926
26215
  } catch {}
25927
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;
25928
26321
  return {
25929
26322
  content: [
25930
26323
  {
25931
26324
  type: "text",
25932
- 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 ? `
25933
26327
  SQLite brief also updated.` : "")
25934
26328
  }
25935
26329
  ]
25936
26330
  };
25937
26331
  }
25938
- function indexUpsert(args) {
25939
- const slug = safeExt(args, ["slug", "id"], "");
25940
- const desc = safeExt(args, ["description", "desc", "text"], "");
25941
- const kind = safeExt(args, ["kind", "type"], "article");
25942
- const section = safeExt(args, ["section", "category"], undefined);
25943
- const date = safeExt(args, ["date", "created"], undefined);
25944
- if (!slug)
25945
- return { content: [{ type: "text", text: "Missing slug." }] };
25946
- const today = new Date().toISOString().slice(0, 10);
25947
- const dateStr = date ?? today;
25948
- const indexPath = (() => {
25949
- if (kind === "topic")
25950
- return resolve17(runtime().wikiDir, "topic-index.md");
25951
- if (kind === "skill")
25952
- return resolve17(runtime().wikiDir, "skill-index.md");
25953
- return resolve17(runtime().wikiDir, "article-index.md");
25954
- })();
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);
25955
26425
  ensureDir(dirname16(indexPath));
25956
26426
  const releaseLock = acquireFileLock(indexPath);
25957
26427
  try {
25958
26428
  let index;
25959
26429
  try {
25960
- index = readFileSync22(indexPath, "utf-8");
26430
+ index = readFileSync23(indexPath, "utf-8");
25961
26431
  } catch {
25962
26432
  index = "";
25963
26433
  }
25964
26434
  const lines = index.split(`
25965
26435
  `);
25966
- const link = (() => {
25967
- switch (kind) {
25968
- case "summary":
25969
- return `- [[summaries/${slug}]] ${desc} (${dateStr})`;
25970
- case "topic":
25971
- return `- [[topic/${slug}]] ${desc} (${dateStr})`;
25972
- case "skill":
25973
- return `- [[skills/${slug}]] ${desc} (${dateStr})`;
25974
- default:
25975
- return `- [[articles/${slug}]] ${desc} (${dateStr})`;
25976
- }
25977
- })();
25978
- const target = (() => {
25979
- switch (kind) {
25980
- case "summary":
25981
- return `summaries/${slug}`;
25982
- case "topic":
25983
- return `topic/${slug}`;
25984
- case "skill":
25985
- return `skills/${slug}`;
25986
- default:
25987
- return `articles/${slug}`;
25988
- }
25989
- })();
26436
+ const target = `${WIKI_INDEX_NAMESPACES[kind]}/${slug}`;
26437
+ const link = `- [[${target}]] ${description} (${date})`;
25990
26438
  const slugPattern = `[[${target}]]`;
25991
26439
  let replaced = false;
25992
26440
  for (let i = lines.length - 1;i >= 0; i--) {
@@ -26024,11 +26472,63 @@ function indexUpsert(args) {
26024
26472
  }
26025
26473
  atomicWriteFile(indexPath, lines.join(`
26026
26474
  `));
26027
- return { content: [{ type: "text", text: `Index updated: ${link}` }] };
26475
+ return link;
26028
26476
  } finally {
26029
26477
  releaseLock();
26030
26478
  }
26031
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
+ }
26032
26532
  function createWikiMcpServer(context, host2) {
26033
26533
  const surface = context.surface ?? "all";
26034
26534
  const wikiDir = resolve17(host2.wikiRoot);
@@ -26069,8 +26569,8 @@ function createWikiMcpServer(context, host2) {
26069
26569
  wiki_last_conversation: wikiLastConversation,
26070
26570
  skill_query: skillQuery,
26071
26571
  skill_save: skillSave,
26072
- save_wiki_entry: saveWikiEntry,
26073
- save_topic_brief: saveTopicBrief,
26572
+ wiki_write: wikiWrite,
26573
+ wiki_reindex: wikiReindex,
26074
26574
  index_upsert: indexUpsert
26075
26575
  };
26076
26576
  const handler = tools.some((tool) => tool.name === name) ? handlers[name] : undefined;
@@ -26079,10 +26579,11 @@ function createWikiMcpServer(context, host2) {
26079
26579
  });
26080
26580
  return server;
26081
26581
  }
26082
- var wikiRuntime, FILE_LOCK_WAIT_MS = 10, FILE_LOCK_TIMEOUT_MS = 5000, FILE_LOCK_STALE_MS = 30000, lockWaitArray, SEARCH_STOP_WORDS, SEARCH_MONTHS, SEARCH_RECENCY_TERMS, WIKI_TOOLS, SKILL_TOOLS;
26083
- 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 () => {
26084
26584
  init_config();
26085
26585
  init_wiki_summary_names();
26586
+ await init_wiki_search_index();
26086
26587
  wikiRuntime = new AsyncLocalStorage3;
26087
26588
  lockWaitArray = new Int32Array(new SharedArrayBuffer(4));
26088
26589
  SEARCH_STOP_WORDS = new Set([
@@ -26156,6 +26657,18 @@ var init_wiki_server = __esm(() => {
26156
26657
  "\uCD5C\uCD08",
26157
26658
  "\uC624\uB798\uB41C"
26158
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
+ };
26159
26672
  WIKI_TOOLS = [
26160
26673
  {
26161
26674
  name: "wiki_query",
@@ -26237,35 +26750,46 @@ var init_wiki_server = __esm(() => {
26237
26750
  }
26238
26751
  },
26239
26752
  {
26240
- name: "save_wiki_entry",
26241
- 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.",
26242
26755
  inputSchema: {
26243
26756
  type: "object",
26244
26757
  properties: {
26245
- topic: { type: "string", description: "Topic name of the session" },
26246
- 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)" }
26247
26781
  },
26248
- required: ["topic", "content"]
26782
+ required: ["kind", "content", "description"]
26249
26783
  }
26250
26784
  },
26251
26785
  {
26252
- name: "save_topic_brief",
26253
- 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).",
26254
- inputSchema: {
26255
- type: "object",
26256
- properties: {
26257
- topic: { type: "string", description: "Topic name of the session" },
26258
- content: {
26259
- type: "string",
26260
- description: "Full accumulated topic brief in markdown (persona layer + recent work)"
26261
- }
26262
- },
26263
- required: ["topic", "content"]
26264
- }
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: {} }
26265
26789
  },
26266
26790
  {
26267
26791
  name: "index_upsert",
26268
- 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.",
26269
26793
  inputSchema: {
26270
26794
  type: "object",
26271
26795
  properties: {
@@ -26342,12 +26866,12 @@ __export(exports_activity_log, {
26342
26866
  readSentFilesForTopic: () => readSentFilesForTopic
26343
26867
  });
26344
26868
  import {
26345
- existsSync as existsSync27,
26869
+ existsSync as existsSync28,
26346
26870
  mkdirSync as mkdirSync25,
26347
26871
  readdirSync as readdirSync11,
26348
26872
  renameSync as renameSync12,
26349
26873
  statSync as statSync16,
26350
- unlinkSync as unlinkSync19,
26874
+ unlinkSync as unlinkSync20,
26351
26875
  writeFileSync as writeFileSync18
26352
26876
  } from "fs";
26353
26877
  import { basename as basename8, join as join37 } from "path";
@@ -26400,7 +26924,7 @@ function rotateOldLogs() {
26400
26924
  if (freed >= toFree)
26401
26925
  break;
26402
26926
  try {
26403
- unlinkSync19(join37(logDir, file.name));
26927
+ unlinkSync20(join37(logDir, file.name));
26404
26928
  freed += file.size;
26405
26929
  logger.info({ file: file.name, sizeKB: (file.size / 1024).toFixed(0) }, "Rotated out old log");
26406
26930
  } catch (e) {
@@ -26426,7 +26950,7 @@ function entryMatchesTopic(e, topicName) {
26426
26950
  }
26427
26951
  function readSentFilesForTopic(userId, topicName) {
26428
26952
  const file = join37(resolveStorageUsersLogDir(), String(userId), "sent-files.jsonl");
26429
- if (!existsSync27(file))
26953
+ if (!existsSync28(file))
26430
26954
  return [];
26431
26955
  try {
26432
26956
  return readJsonlLines(file).map((line) => JSON.parse(line)).filter((e) => entryMatchesTopic(e, topicName));
@@ -26437,7 +26961,7 @@ function readSentFilesForTopic(userId, topicName) {
26437
26961
  }
26438
26962
  function removeSentFilesForTopic(userId, topicName) {
26439
26963
  const file = join37(resolveStorageUsersLogDir(), String(userId), "sent-files.jsonl");
26440
- if (!existsSync27(file))
26964
+ if (!existsSync28(file))
26441
26965
  return;
26442
26966
  try {
26443
26967
  const remaining = readJsonlLines(file).filter((line) => {
@@ -26452,7 +26976,7 @@ function removeSentFilesForTopic(userId, topicName) {
26452
26976
  `)}
26453
26977
  `);
26454
26978
  } else {
26455
- unlinkSync19(file);
26979
+ unlinkSync20(file);
26456
26980
  }
26457
26981
  } catch (e) {
26458
26982
  logger.warn({ err: e, userId, topicName }, "Failed to remove sent-files for topic");
@@ -27250,7 +27774,7 @@ async function buildHostedSurfaceServer(surface, context) {
27250
27774
  case "wiki":
27251
27775
  case "skills": {
27252
27776
  const [{ createWikiMcpServer: createWikiMcpServer2 }, storage, { WORKSPACE_DIR: WORKSPACE_DIR2 }] = await Promise.all([
27253
- Promise.resolve().then(() => (init_wiki_server(), exports_wiki_server)),
27777
+ init_wiki_server().then(() => exports_wiki_server),
27254
27778
  init_storage_public().then(() => exports_storage_public),
27255
27779
  init_mcp_runtime_host().then(() => exports_mcp_runtime_host)
27256
27780
  ]);
@@ -27961,7 +28485,7 @@ var init_spec = __esm(() => {
27961
28485
  });
27962
28486
 
27963
28487
  // ../../packages/mcp-host/src/manifest.ts
27964
- 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";
27965
28489
  import { dirname as dirname17 } from "path";
27966
28490
  import { z as z14 } from "zod";
27967
28491
 
@@ -28006,11 +28530,11 @@ class McpManifest {
28006
28530
  return this.entries.get(key)?.enabled ?? false;
28007
28531
  }
28008
28532
  load() {
28009
- if (!existsSync28(this.file))
28533
+ if (!existsSync29(this.file))
28010
28534
  return;
28011
28535
  let parsed;
28012
28536
  try {
28013
- parsed = manifestFileSchema.parse(JSON.parse(readFileSync23(this.file, "utf8")));
28537
+ parsed = manifestFileSchema.parse(JSON.parse(readFileSync24(this.file, "utf8")));
28014
28538
  } catch (e) {
28015
28539
  throw new Error(`Invalid MCP manifest at ${this.file}: ${e instanceof Error ? e.message : String(e)}`);
28016
28540
  }
@@ -28060,9 +28584,9 @@ import { execFileSync as execFileSync8, spawn as spawn8 } from "child_process";
28060
28584
  import {
28061
28585
  mkdirSync as mkdirSync27,
28062
28586
  readdirSync as readdirSync12,
28063
- readFileSync as readFileSync24,
28587
+ readFileSync as readFileSync25,
28064
28588
  renameSync as renameSync14,
28065
- unlinkSync as unlinkSync20,
28589
+ unlinkSync as unlinkSync21,
28066
28590
  writeFileSync as writeFileSync20
28067
28591
  } from "fs";
28068
28592
  import { connect, createServer as createServer3 } from "net";
@@ -28368,7 +28892,7 @@ class McpHost {
28368
28892
  if (file.includes(".tmp-"))
28369
28893
  continue;
28370
28894
  try {
28371
- 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);
28372
28896
  if (!Number.isNaN(port))
28373
28897
  claims.set(file, port);
28374
28898
  } catch {}
@@ -28388,7 +28912,7 @@ class McpHost {
28388
28912
  }
28389
28913
  deletePortFile(fileName) {
28390
28914
  try {
28391
- unlinkSync20(join38(this.portsDir, fileName));
28915
+ unlinkSync21(join38(this.portsDir, fileName));
28392
28916
  } catch {}
28393
28917
  }
28394
28918
  cleanupRunning(rkey, inst) {
@@ -28493,9 +29017,9 @@ var init_src3 = __esm(() => {
28493
29017
  import { randomUUID as randomUUID24 } from "crypto";
28494
29018
  import {
28495
29019
  copyFileSync as copyFileSync4,
28496
- existsSync as existsSync29,
29020
+ existsSync as existsSync30,
28497
29021
  mkdirSync as mkdirSync28,
28498
- readFileSync as readFileSync25,
29022
+ readFileSync as readFileSync26,
28499
29023
  rmSync as rmSync9,
28500
29024
  statSync as statSync18,
28501
29025
  writeFileSync as writeFileSync21
@@ -28531,7 +29055,7 @@ class NodeFileStore {
28531
29055
  if (!FILE_ID_RE.test(fileId))
28532
29056
  return null;
28533
29057
  try {
28534
- const parsed = JSON.parse(readFileSync25(this.#metadataPath(fileId), "utf8"));
29058
+ const parsed = JSON.parse(readFileSync26(this.#metadataPath(fileId), "utf8"));
28535
29059
  if (typeof parsed.filename !== "string" || typeof parsed.mimeType !== "string" || typeof parsed.sizeBytes !== "number" || typeof parsed.savedName !== "string") {
28536
29060
  return null;
28537
29061
  }
@@ -28554,7 +29078,7 @@ class NodeFileStore {
28554
29078
  hooks = {
28555
29079
  resolveAttachmentByFileId: (fileId) => {
28556
29080
  const metadata = this.#metadata(fileId);
28557
- if (!metadata || !existsSync29(join39(this.uploadDir, metadata.savedName)))
29081
+ if (!metadata || !existsSync30(join39(this.uploadDir, metadata.savedName)))
28558
29082
  return null;
28559
29083
  return this.#attachment(fileId, metadata);
28560
29084
  },
@@ -28563,7 +29087,7 @@ class NodeFileStore {
28563
29087
  if (!metadata)
28564
29088
  return null;
28565
29089
  const path = join39(this.uploadDir, metadata.savedName);
28566
- return existsSync29(path) ? path : null;
29090
+ return existsSync30(path) ? path : null;
28567
29091
  },
28568
29092
  storeLocalFileAsUpload: (absPath, access = {}) => this.store(absPath, access),
28569
29093
  deleteFilesForTopic: (topicId) => this.deleteForTopic(topicId)
@@ -28606,7 +29130,7 @@ class NodeFileStore {
28606
29130
  if (!allowed)
28607
29131
  return null;
28608
29132
  const path = join39(this.uploadDir, metadata.savedName);
28609
- if (!existsSync29(path))
29133
+ if (!existsSync30(path))
28610
29134
  return null;
28611
29135
  return new Response(Bun.file(path), {
28612
29136
  headers: {
@@ -28775,12 +29299,12 @@ function createPollingSseStream(req, options) {
28775
29299
  // ../../packages/node/src/control.ts
28776
29300
  import { randomUUID as randomUUID25, timingSafeEqual as timingSafeEqual4 } from "crypto";
28777
29301
  import {
28778
- chmodSync as chmodSync5,
28779
- existsSync as existsSync30,
29302
+ chmodSync as chmodSync6,
29303
+ existsSync as existsSync31,
28780
29304
  mkdirSync as mkdirSync29,
28781
- readFileSync as readFileSync26,
29305
+ readFileSync as readFileSync27,
28782
29306
  renameSync as renameSync15,
28783
- unlinkSync as unlinkSync21,
29307
+ unlinkSync as unlinkSync22,
28784
29308
  writeFileSync as writeFileSync22
28785
29309
  } from "fs";
28786
29310
  import { dirname as dirname18, resolve as resolve21 } from "path";
@@ -29287,16 +29811,16 @@ function writeNodeDaemonInfo(port, startedAt) {
29287
29811
  const temporary = `${NODE_DAEMON_INFO_PATH}.${process.pid}.${randomUUID25()}.tmp`;
29288
29812
  writeFileSync22(temporary, `${JSON.stringify(info, null, 2)}
29289
29813
  `, { mode: 384 });
29290
- chmodSync5(temporary, 384);
29814
+ chmodSync6(temporary, 384);
29291
29815
  renameSync15(temporary, NODE_DAEMON_INFO_PATH);
29292
- chmodSync5(NODE_DAEMON_INFO_PATH, 384);
29816
+ chmodSync6(NODE_DAEMON_INFO_PATH, 384);
29293
29817
  return info;
29294
29818
  }
29295
29819
  function readNodeDaemonInfo() {
29296
- if (!existsSync30(NODE_DAEMON_INFO_PATH))
29820
+ if (!existsSync31(NODE_DAEMON_INFO_PATH))
29297
29821
  return null;
29298
29822
  try {
29299
- const value = JSON.parse(readFileSync26(NODE_DAEMON_INFO_PATH, "utf8"));
29823
+ const value = JSON.parse(readFileSync27(NODE_DAEMON_INFO_PATH, "utf8"));
29300
29824
  if (value?.schemaVersion !== 1 || value.stateDir !== STATE_DIR || !Number.isInteger(value.protocolVersion) || !Number.isInteger(value.pid) || !Number.isInteger(value.port) || value.port <= 0) {
29301
29825
  return null;
29302
29826
  }
@@ -29310,7 +29834,7 @@ function removeNodeDaemonInfo(expected) {
29310
29834
  if (!current3 || current3.pid !== expected.pid || current3.port !== expected.port)
29311
29835
  return;
29312
29836
  try {
29313
- unlinkSync21(NODE_DAEMON_INFO_PATH);
29837
+ unlinkSync22(NODE_DAEMON_INFO_PATH);
29314
29838
  } catch {}
29315
29839
  }
29316
29840
  async function fetchWithTimeout(url, init, timeoutMs) {
@@ -29584,7 +30108,7 @@ var init_schedule = __esm(() => {
29584
30108
 
29585
30109
  // ../../packages/module-cron/src/scripts.ts
29586
30110
  import { spawn as spawn9 } from "child_process";
29587
- 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";
29588
30112
  import { resolve as resolve22, sep as sep2 } from "path";
29589
30113
  function validateCronScriptName(script) {
29590
30114
  if (!/^[A-Za-z0-9_][A-Za-z0-9_.-]*\.py$/.test(script) || script.includes("..") || script.includes("/") || script.includes("\\")) {
@@ -29607,7 +30131,7 @@ function listCronScripts() {
29607
30131
  }
29608
30132
  function cronScriptExists(script) {
29609
30133
  try {
29610
- return existsSync31(resolveCronScriptPath(script));
30134
+ return existsSync32(resolveCronScriptPath(script));
29611
30135
  } catch {
29612
30136
  return false;
29613
30137
  }
@@ -29639,7 +30163,7 @@ async function runCronPromptScript(options) {
29639
30163
  if (options.signal?.aborted)
29640
30164
  throw new CronScriptError("script run aborted");
29641
30165
  const scriptPath = resolveCronScriptPath(options.script);
29642
- if (!existsSync31(scriptPath))
30166
+ if (!existsSync32(scriptPath))
29643
30167
  throw new CronScriptError(`cron script not found: ${options.script}`);
29644
30168
  const timeoutMs = Math.max(5000, options.timeoutMs ?? DEFAULT_SCRIPT_TIMEOUT_MS);
29645
30169
  const outputLimitBytes = Math.max(1024, options.outputLimitBytes ?? DEFAULT_OUTPUT_LIMIT_BYTES);
@@ -30840,7 +31364,7 @@ var init_scheduler = __esm(async () => {
30840
31364
  });
30841
31365
 
30842
31366
  // ../../packages/module-cron/src/module.ts
30843
- import { existsSync as existsSync32 } from "fs";
31367
+ import { existsSync as existsSync33 } from "fs";
30844
31368
  import { fileURLToPath as fileURLToPath2 } from "url";
30845
31369
  function envMs(name, fallback) {
30846
31370
  const parsed = Number.parseInt(process.env[name] ?? "", 10);
@@ -30871,7 +31395,7 @@ function createCronModule(options = {}) {
30871
31395
  if (event.type === "topic-deleted")
30872
31396
  trackCleanup(resetCronTopicContext(event.topicId));
30873
31397
  });
30874
- 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);
30875
31399
  const unregisterMcp = host2.registerRuntimeMcpServer("cron-manager", {
30876
31400
  scopes: ["forum", "manager"],
30877
31401
  forumRequired: true,
@@ -32369,7 +32893,7 @@ var init_context_usage = __esm(async () => {
32369
32893
 
32370
32894
  // ../../adapters/terminal/src/path-suggest.ts
32371
32895
  import { execFile as execFile3 } from "child_process";
32372
- 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";
32373
32897
  import { homedir as homedir10 } from "os";
32374
32898
  import { basename as basename11, dirname as dirname19, join as join40, sep as sep3 } from "path";
32375
32899
  import { promisify as promisify3 } from "util";
@@ -32618,7 +33142,7 @@ function fragmentToAbsolutePath(frag) {
32618
33142
  }
32619
33143
  function fragmentResolves(frag) {
32620
33144
  try {
32621
- return existsSync33(fragmentToAbsolutePath(frag));
33145
+ return existsSync34(fragmentToAbsolutePath(frag));
32622
33146
  } catch {
32623
33147
  return false;
32624
33148
  }
@@ -39312,7 +39836,7 @@ function renderOutbound(text2, maxLen = 4096) {
39312
39836
  }
39313
39837
 
39314
39838
  // ../../adapters/telegram/src/adapter.ts
39315
- import { existsSync as existsSync34 } from "fs";
39839
+ import { existsSync as existsSync35 } from "fs";
39316
39840
  import { basename as basename12, extname as extname5 } from "path";
39317
39841
  function startTelegramAdapter(opts) {
39318
39842
  const { client, forumChatId } = opts;
@@ -39800,7 +40324,7 @@ function startTelegramAdapter(opts) {
39800
40324
  logger.warn({ path, chatId }, "telegram adapter: blocked sensitive file path");
39801
40325
  return { ref: null, delivered: false, error: "path matches the sensitive-file blacklist" };
39802
40326
  }
39803
- if (!existsSync34(path)) {
40327
+ if (!existsSync35(path)) {
39804
40328
  const sent = await client.sendMessage(chatId, `File: ${path}`, base).catch(() => null);
39805
40329
  const messageId = sentMessageId(sent);
39806
40330
  return {
@@ -40982,19 +41506,19 @@ __export(exports_join, {
40982
41506
  });
40983
41507
  import { createHash as createHash11, randomUUID as randomUUID27 } from "crypto";
40984
41508
  import {
40985
- chmodSync as chmodSync6,
41509
+ chmodSync as chmodSync7,
40986
41510
  closeSync as closeSync5,
40987
- existsSync as existsSync35,
41511
+ existsSync as existsSync36,
40988
41512
  fsyncSync as fsyncSync2,
40989
41513
  linkSync,
40990
41514
  lstatSync,
40991
41515
  mkdirSync as mkdirSync32,
40992
41516
  openSync as openSync5,
40993
- readFileSync as readFileSync27,
41517
+ readFileSync as readFileSync28,
40994
41518
  renameSync as renameSync16,
40995
41519
  rmSync as rmSync10,
40996
41520
  statSync as statSync20,
40997
- unlinkSync as unlinkSync22,
41521
+ unlinkSync as unlinkSync23,
40998
41522
  writeFileSync as writeFileSync23
40999
41523
  } from "fs";
41000
41524
  import { dirname as dirname21, resolve as resolve23 } from "path";
@@ -41094,7 +41618,7 @@ function withJoinCredentialLock(operation) {
41094
41618
  throw error2;
41095
41619
  let current3 = null;
41096
41620
  try {
41097
- current3 = JSON.parse(readFileSync27(ownerPath, "utf8"));
41621
+ current3 = JSON.parse(readFileSync28(ownerPath, "utf8"));
41098
41622
  } catch {}
41099
41623
  let ageMs;
41100
41624
  try {
@@ -41124,7 +41648,7 @@ function withJoinCredentialLock(operation) {
41124
41648
  return operation();
41125
41649
  } finally {
41126
41650
  try {
41127
- const current3 = JSON.parse(readFileSync27(ownerPath, "utf8"));
41651
+ const current3 = JSON.parse(readFileSync28(ownerPath, "utf8"));
41128
41652
  if (current3.pid === owner.pid && current3.token === owner.token) {
41129
41653
  rmSync10(lockPath, { recursive: true, force: true });
41130
41654
  }
@@ -41147,9 +41671,9 @@ function joinCredentialDigest(join42) {
41147
41671
  return createHash11("sha256").update(JSON.stringify(normalizedJoin(join42))).digest("base64url");
41148
41672
  }
41149
41673
  function readPersistedJoin(path = joinFilePath()) {
41150
- if (!existsSync35(path))
41674
+ if (!existsSync36(path))
41151
41675
  return null;
41152
- const parsed = JSON.parse(readFileSync27(path, "utf-8"));
41676
+ const parsed = JSON.parse(readFileSync28(path, "utf-8"));
41153
41677
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
41154
41678
  throw new Error("persisted join credentials are not a JSON object");
41155
41679
  }
@@ -41168,7 +41692,7 @@ function saveJoinWhileLocked(join42, options = {}) {
41168
41692
  const directory = dirname21(path);
41169
41693
  const normalized = normalizedJoin(join42);
41170
41694
  mkdirSync32(directory, { recursive: true });
41171
- if (existsSync35(path)) {
41695
+ if (existsSync36(path)) {
41172
41696
  if (lstatSync(path).isSymbolicLink()) {
41173
41697
  throw new Error(`refusing to replace symlinked Otium join file at ${path}`);
41174
41698
  }
@@ -41181,7 +41705,7 @@ function saveJoinWhileLocked(join42, options = {}) {
41181
41705
  }
41182
41706
  }
41183
41707
  if (existing && joinsEqual(existing, normalized)) {
41184
- chmodSync6(path, 384);
41708
+ chmodSync7(path, 384);
41185
41709
  const fileFd = openSync5(path, "r");
41186
41710
  try {
41187
41711
  fsyncSync2(fileFd);
@@ -41213,9 +41737,9 @@ function saveJoinWhileLocked(join42, options = {}) {
41213
41737
  renameSync16(temporaryPath, path);
41214
41738
  } else {
41215
41739
  linkSync(temporaryPath, path);
41216
- unlinkSync22(temporaryPath);
41740
+ unlinkSync23(temporaryPath);
41217
41741
  }
41218
- chmodSync6(path, 384);
41742
+ chmodSync7(path, 384);
41219
41743
  const directoryFd = openSync5(directory, "r");
41220
41744
  try {
41221
41745
  fsyncSync2(directoryFd);
@@ -41225,8 +41749,8 @@ function saveJoinWhileLocked(join42, options = {}) {
41225
41749
  } catch (error2) {
41226
41750
  if (fd !== undefined)
41227
41751
  closeSync5(fd);
41228
- if (existsSync35(temporaryPath))
41229
- unlinkSync22(temporaryPath);
41752
+ if (existsSync36(temporaryPath))
41753
+ unlinkSync23(temporaryPath);
41230
41754
  throw error2;
41231
41755
  }
41232
41756
  return path;
@@ -41237,12 +41761,12 @@ function saveJoin(join42, options = {}) {
41237
41761
  function removeJoin() {
41238
41762
  return withJoinCredentialLock(() => {
41239
41763
  const path = joinFilePath();
41240
- if (!existsSync35(path))
41764
+ if (!existsSync36(path))
41241
41765
  return false;
41242
41766
  if (lstatSync(path).isSymbolicLink()) {
41243
41767
  throw new Error(`refusing to remove symlinked Otium join file at ${path}`);
41244
41768
  }
41245
- unlinkSync22(path);
41769
+ unlinkSync23(path);
41246
41770
  const directoryFd = openSync5(dirname21(path), "r");
41247
41771
  try {
41248
41772
  fsyncSync2(directoryFd);
@@ -41269,10 +41793,10 @@ function loadJoin() {
41269
41793
  logger.warn("otium: OTIUM_CENTRAL_URL, OTIUM_CELL_ID, OTIUM_CELL_SECRET must be set together \u2014 ignoring partial env");
41270
41794
  }
41271
41795
  const path = joinFilePath();
41272
- if (!existsSync35(path))
41796
+ if (!existsSync36(path))
41273
41797
  return null;
41274
41798
  try {
41275
- const parsed = JSON.parse(readFileSync27(path, "utf-8"));
41799
+ const parsed = JSON.parse(readFileSync28(path, "utf-8"));
41276
41800
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
41277
41801
  return null;
41278
41802
  return normalizeJoin(parsed);
@@ -41296,11 +41820,11 @@ var exports_join_status = {};
41296
41820
  __export(exports_join_status, {
41297
41821
  hasConfiguredOtiumJoin: () => hasConfiguredOtiumJoin
41298
41822
  });
41299
- import { existsSync as existsSync36 } from "fs";
41823
+ import { existsSync as existsSync37 } from "fs";
41300
41824
  import { resolve as resolve24 } from "path";
41301
41825
  function hasConfiguredOtiumJoin() {
41302
41826
  const envJoin = Boolean(process.env.OTIUM_CENTRAL_URL?.trim() && process.env.OTIUM_CELL_ID?.trim() && process.env.OTIUM_CELL_SECRET?.trim());
41303
- return envJoin || existsSync36(resolve24(DATA_DIR, "otium-join.json"));
41827
+ return envJoin || existsSync37(resolve24(DATA_DIR, "otium-join.json"));
41304
41828
  }
41305
41829
  var init_join_status = __esm(() => {
41306
41830
  init_config_public();
@@ -41528,8 +42052,7 @@ var init_canonical_mcp_bridge = __esm(async () => {
41528
42052
  "wiki_read",
41529
42053
  "wiki_topic_brief",
41530
42054
  "wiki_last_conversation",
41531
- "save_wiki_entry",
41532
- "save_topic_brief",
42055
+ "wiki_write",
41533
42056
  "index_upsert"
41534
42057
  ])
41535
42058
  };
@@ -43989,16 +44512,16 @@ import {
43989
44512
  randomUUID as randomUUID31
43990
44513
  } from "crypto";
43991
44514
  import {
43992
- chmodSync as chmodSync7,
44515
+ chmodSync as chmodSync8,
43993
44516
  closeSync as closeSync6,
43994
- existsSync as existsSync37,
44517
+ existsSync as existsSync38,
43995
44518
  fsyncSync as fsyncSync3,
43996
44519
  linkSync as linkSync2,
43997
44520
  mkdirSync as mkdirSync34,
43998
44521
  openSync as openSync6,
43999
- readFileSync as readFileSync28,
44522
+ readFileSync as readFileSync29,
44000
44523
  renameSync as renameSync17,
44001
- unlinkSync as unlinkSync23,
44524
+ unlinkSync as unlinkSync24,
44002
44525
  writeFileSync as writeFileSync24
44003
44526
  } from "fs";
44004
44527
  import { dirname as dirname22, resolve as resolve25 } from "path";
@@ -44026,10 +44549,10 @@ function pendingEnrollmentPath() {
44026
44549
  }
44027
44550
  function isEnrollmentPending(invite) {
44028
44551
  const path = pendingEnrollmentPath();
44029
- if (!existsSync37(path))
44552
+ if (!existsSync38(path))
44030
44553
  return false;
44031
44554
  try {
44032
- const saved = JSON.parse(readFileSync28(path, "utf8"));
44555
+ const saved = JSON.parse(readFileSync29(path, "utf8"));
44033
44556
  return saved.central === invite.central && saved.token === invite.token;
44034
44557
  } catch {
44035
44558
  return false;
@@ -44037,8 +44560,8 @@ function isEnrollmentPending(invite) {
44037
44560
  }
44038
44561
  function loadOrCreatePending(invite, nodeName) {
44039
44562
  const path = pendingEnrollmentPath();
44040
- if (existsSync37(path)) {
44041
- const saved = JSON.parse(readFileSync28(path, "utf8"));
44563
+ if (existsSync38(path)) {
44564
+ const saved = JSON.parse(readFileSync29(path, "utf8"));
44042
44565
  if (saved.central === invite.central && saved.token === invite.token)
44043
44566
  return saved;
44044
44567
  throw new Error(`another Otium enrollment is pending at ${path}`);
@@ -44062,8 +44585,8 @@ function loadOrCreatePending(invite, nodeName) {
44062
44585
  closeSync6(fd);
44063
44586
  fd = undefined;
44064
44587
  linkSync2(temporaryPath, path);
44065
- unlinkSync23(temporaryPath);
44066
- chmodSync7(path, 384);
44588
+ unlinkSync24(temporaryPath);
44589
+ chmodSync8(path, 384);
44067
44590
  const directoryFd = openSync6(dirname22(path), "r");
44068
44591
  try {
44069
44592
  fsyncSync3(directoryFd);
@@ -44073,10 +44596,10 @@ function loadOrCreatePending(invite, nodeName) {
44073
44596
  } catch (error2) {
44074
44597
  if (fd !== undefined)
44075
44598
  closeSync6(fd);
44076
- if (existsSync37(temporaryPath))
44077
- unlinkSync23(temporaryPath);
44078
- if (error2.code === "EEXIST" && existsSync37(path)) {
44079
- 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"));
44080
44603
  if (saved.central === invite.central && saved.token === invite.token)
44081
44604
  return saved;
44082
44605
  }
@@ -44106,14 +44629,14 @@ function replacePendingEnrollment(pending2) {
44106
44629
  } catch (error2) {
44107
44630
  if (fd !== undefined)
44108
44631
  closeSync6(fd);
44109
- if (existsSync37(temporaryPath))
44110
- unlinkSync23(temporaryPath);
44632
+ if (existsSync38(temporaryPath))
44633
+ unlinkSync24(temporaryPath);
44111
44634
  throw error2;
44112
44635
  }
44113
44636
  }
44114
44637
  function recordClaimedCredential(pending2, join43) {
44115
44638
  withJoinCredentialLock(() => {
44116
- const current3 = JSON.parse(readFileSync28(pendingEnrollmentPath(), "utf8"));
44639
+ const current3 = JSON.parse(readFileSync29(pendingEnrollmentPath(), "utf8"));
44117
44640
  if (current3.central !== pending2.central || current3.token !== pending2.token || current3.idempotencyKey !== pending2.idempotencyKey || current3.publicKey !== pending2.publicKey) {
44118
44641
  throw new Error("pending Otium enrollment changed while its claim was in flight");
44119
44642
  }
@@ -44192,7 +44715,7 @@ async function claimEnrollment(invite, nodeName) {
44192
44715
  function commitEnrollment(join43, options = {}) {
44193
44716
  return withJoinCredentialLock(() => {
44194
44717
  const pendingPath = pendingEnrollmentPath();
44195
- const pending2 = existsSync37(pendingPath) ? JSON.parse(readFileSync28(pendingPath, "utf8")) : null;
44718
+ const pending2 = existsSync38(pendingPath) ? JSON.parse(readFileSync29(pendingPath, "utf8")) : null;
44196
44719
  const digest = joinCredentialDigest(join43);
44197
44720
  if (pending2 && (!pending2.claimed || pending2.claimed.digest !== digest || pending2.claimed.cellId !== join43.cellId)) {
44198
44721
  throw new Error(`pending Otium enrollment at ${pendingPath} does not match these credentials`);
@@ -44203,7 +44726,7 @@ function commitEnrollment(join43, options = {}) {
44203
44726
  }
44204
44727
  if (!pending2)
44205
44728
  return path;
44206
- unlinkSync23(pendingPath);
44729
+ unlinkSync24(pendingPath);
44207
44730
  const directoryFd = openSync6(dirname22(pendingPath), "r");
44208
44731
  try {
44209
44732
  fsyncSync3(directoryFd);
@@ -46392,4 +46915,4 @@ switch (command) {
46392
46915
  }
46393
46916
  }
46394
46917
 
46395
- //# debugId=9061330203CC009264756E2164756E21
46918
+ //# debugId=1F2F931DB2BD421464756E2164756E21