negotium 0.1.7 → 0.1.9
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/README.md +26 -2
- package/dist/canonical-mcp-bridge.js +14 -0
- package/dist/canonical-mcp-bridge.js.map +9 -0
- package/dist/chunk-10ct8814.js +429 -0
- package/dist/chunk-10ct8814.js.map +11 -0
- package/dist/chunk-18fzftjf.js +214 -0
- package/dist/chunk-18fzftjf.js.map +10 -0
- package/dist/chunk-1k7t7bnx.js +97 -0
- package/dist/chunk-1k7t7bnx.js.map +10 -0
- package/dist/chunk-1msje2zf.js +23 -0
- package/dist/chunk-1msje2zf.js.map +10 -0
- package/dist/chunk-2dgkah1z.js +706 -0
- package/dist/chunk-2dgkah1z.js.map +14 -0
- package/dist/chunk-3t4dtm26.js +61 -0
- package/dist/chunk-3t4dtm26.js.map +10 -0
- package/dist/chunk-3vn2vnj1.js +51 -0
- package/dist/chunk-3vn2vnj1.js.map +10 -0
- package/dist/chunk-6hknphtc.js +2054 -0
- package/dist/chunk-6hknphtc.js.map +29 -0
- package/dist/chunk-6tfzpcvk.js +2149 -0
- package/dist/chunk-6tfzpcvk.js.map +15 -0
- package/dist/chunk-8g0c4as0.js +23 -0
- package/dist/chunk-8g0c4as0.js.map +10 -0
- package/dist/chunk-9spx3h9n.js +279 -0
- package/dist/chunk-9spx3h9n.js.map +10 -0
- package/dist/chunk-c500xe9z.js +33 -0
- package/dist/chunk-c500xe9z.js.map +10 -0
- package/dist/chunk-e9b2xpff.js +147 -0
- package/dist/chunk-e9b2xpff.js.map +10 -0
- package/dist/chunk-edb07sjp.js +567 -0
- package/dist/chunk-edb07sjp.js.map +14 -0
- package/dist/chunk-eexqsee7.js +368 -0
- package/dist/chunk-eexqsee7.js.map +11 -0
- package/dist/chunk-efyxk97v.js +500 -0
- package/dist/chunk-efyxk97v.js.map +13 -0
- package/dist/chunk-epqxwkvh.js +3534 -0
- package/dist/chunk-epqxwkvh.js.map +20 -0
- package/dist/chunk-f1pzh3xh.js +115 -0
- package/dist/chunk-f1pzh3xh.js.map +10 -0
- package/dist/chunk-f5xgprg1.js +59 -0
- package/dist/chunk-f5xgprg1.js.map +10 -0
- package/dist/chunk-f97qybwk.js +584 -0
- package/dist/chunk-f97qybwk.js.map +11 -0
- package/dist/chunk-fvc7ncxd.js +252 -0
- package/dist/chunk-fvc7ncxd.js.map +10 -0
- package/dist/chunk-g7pvfj5e.js +574 -0
- package/dist/chunk-g7pvfj5e.js.map +14 -0
- package/dist/chunk-h8vmb9gf.js +315 -0
- package/dist/chunk-h8vmb9gf.js.map +10 -0
- package/dist/chunk-hgw7n8cm.js +43 -0
- package/dist/chunk-hgw7n8cm.js.map +10 -0
- package/dist/chunk-hsq7j9jk.js +126 -0
- package/dist/chunk-hsq7j9jk.js.map +10 -0
- package/dist/chunk-hwy3n31w.js +21 -0
- package/dist/chunk-hwy3n31w.js.map +9 -0
- package/dist/chunk-j0kjscb7.js +53 -0
- package/dist/chunk-j0kjscb7.js.map +10 -0
- package/dist/chunk-jzz91jwh.js +70 -0
- package/dist/chunk-jzz91jwh.js.map +10 -0
- package/dist/chunk-k9z47mab.js +247 -0
- package/dist/chunk-k9z47mab.js.map +12 -0
- package/dist/chunk-kbm35t7w.js +1331 -0
- package/dist/chunk-kbm35t7w.js.map +16 -0
- package/dist/chunk-m4veth0c.js +1649 -0
- package/dist/chunk-m4veth0c.js.map +17 -0
- package/dist/chunk-mhty3nna.js +150 -0
- package/dist/chunk-mhty3nna.js.map +11 -0
- package/dist/chunk-mmgh670x.js +248 -0
- package/dist/chunk-mmgh670x.js.map +11 -0
- package/dist/chunk-n0kk5xbq.js +2358 -0
- package/dist/chunk-n0kk5xbq.js.map +26 -0
- package/dist/chunk-nmv41qx9.js +2847 -0
- package/dist/chunk-nmv41qx9.js.map +23 -0
- package/dist/chunk-nqdp52aj.js +113 -0
- package/dist/chunk-nqdp52aj.js.map +12 -0
- package/dist/chunk-ns2m25dz.js +139 -0
- package/dist/chunk-ns2m25dz.js.map +10 -0
- package/dist/chunk-pkdmypdd.js +147 -0
- package/dist/chunk-pkdmypdd.js.map +11 -0
- package/dist/chunk-rmyfmrrr.js +1634 -0
- package/dist/chunk-rmyfmrrr.js.map +15 -0
- package/dist/chunk-sa874j5c.js +9 -0
- package/dist/chunk-sa874j5c.js.map +10 -0
- package/dist/chunk-sgsz2fpq.js +289 -0
- package/dist/chunk-sgsz2fpq.js.map +10 -0
- package/dist/chunk-sn51wnf1.js +3020 -0
- package/dist/chunk-sn51wnf1.js.map +32 -0
- package/dist/chunk-sqhr1a7e.js +587 -0
- package/dist/chunk-sqhr1a7e.js.map +11 -0
- package/dist/chunk-tmj5r2zv.js +2933 -0
- package/dist/chunk-tmj5r2zv.js.map +20 -0
- package/dist/chunk-vefj3880.js +56 -0
- package/dist/chunk-vefj3880.js.map +10 -0
- package/dist/chunk-vp2db42d.js +121 -0
- package/dist/chunk-vp2db42d.js.map +10 -0
- package/dist/chunk-wry79q55.js +108 -0
- package/dist/chunk-wry79q55.js.map +10 -0
- package/dist/chunk-x1arr7ze.js +63 -0
- package/dist/chunk-x1arr7ze.js.map +10 -0
- package/dist/chunk-z2xy5n2q.js +103 -0
- package/dist/chunk-z2xy5n2q.js.map +10 -0
- package/dist/hosted-agent.js +53 -0
- package/dist/hosted-agent.js.map +10 -0
- package/dist/main.js +132 -20
- package/dist/main.js.map +4 -4
- package/dist/runtime/scripts/faster-whisper-wrapper.py +87 -0
- package/dist/runtime/scripts/mcp-patchright-http.mjs +349 -0
- package/dist/runtime/src/agents/api-topic-agent-switch.ts +178 -0
- package/dist/runtime/src/agents/archiver.ts +396 -0
- package/dist/runtime/src/agents/auth-check.ts +64 -0
- package/dist/runtime/src/agents/claude-provider.ts +737 -0
- package/dist/runtime/src/agents/claude-registry.ts +115 -0
- package/dist/runtime/src/agents/codex-native-multi-agent.ts +177 -0
- package/dist/runtime/src/agents/codex-provider.ts +785 -0
- package/dist/runtime/src/agents/codex-registry.ts +115 -0
- package/dist/runtime/src/agents/codex-tree-kill.ts +365 -0
- package/dist/runtime/src/agents/contracts.ts +69 -0
- package/dist/runtime/src/agents/deep-map.ts +13 -0
- package/dist/runtime/src/agents/execution-host.ts +91 -0
- package/dist/runtime/src/agents/fixtures/claude-attachments.jsonl +2 -0
- package/dist/runtime/src/agents/fixtures/codex-shell.jsonl +5 -0
- package/dist/runtime/src/agents/fork.ts +65 -0
- package/dist/runtime/src/agents/hosted-agent.ts +49 -0
- package/dist/runtime/src/agents/idle-archiver.ts +128 -0
- package/dist/runtime/src/agents/index.ts +205 -0
- package/dist/runtime/src/agents/maestro-provider.ts +206 -0
- package/dist/runtime/src/agents/maestro-registry.ts +31 -0
- package/dist/runtime/src/agents/mcp-tools/ask-user.ts +280 -0
- package/dist/runtime/src/agents/mcp-tools/common.ts +21 -0
- package/dist/runtime/src/agents/mcp-tools/self-config.ts +220 -0
- package/dist/runtime/src/agents/mcp-tools/spawn-subagent.ts +525 -0
- package/dist/runtime/src/agents/mcp-tools/visuals.ts +88 -0
- package/dist/runtime/src/agents/model-catalog.ts +151 -0
- package/dist/runtime/src/agents/registry.ts +24 -0
- package/dist/runtime/src/agents/rollout/claude.ts +331 -0
- package/dist/runtime/src/agents/rollout/codex.ts +569 -0
- package/dist/runtime/src/agents/rollout/shared.ts +205 -0
- package/dist/runtime/src/agents/self-config-core.ts +415 -0
- package/dist/runtime/src/agents/task-events.ts +44 -0
- package/dist/runtime/src/agents/tool-format.ts +352 -0
- package/dist/runtime/src/agents/topic-agent-switch.ts +193 -0
- package/dist/runtime/src/agents/topic-cleanup.ts +231 -0
- package/dist/runtime/src/agents/vault-tool-policy.ts +33 -0
- package/dist/runtime/src/application/execute-external-user-turn.ts +23 -0
- package/dist/runtime/src/application/submit-user-message.ts +50 -0
- package/dist/runtime/src/application/switch-topic-effort.ts +48 -0
- package/dist/runtime/src/application/switch-topic-model.ts +79 -0
- package/dist/runtime/src/application/topic-service.ts +139 -0
- package/dist/runtime/src/application/vault-command.ts +170 -0
- package/dist/runtime/src/bus.ts +319 -0
- package/dist/runtime/src/index.ts +333 -0
- package/dist/runtime/src/mcp/agent-health-server.ts +309 -0
- package/dist/runtime/src/mcp/background-bash-server.ts +346 -0
- package/dist/runtime/src/mcp/canonical-bridge-config.ts +84 -0
- package/dist/runtime/src/mcp/canonical-proxy-server.ts +171 -0
- package/dist/runtime/src/mcp/dm-ipc.ts +60 -0
- package/dist/runtime/src/mcp/mcp-helpers.ts +40 -0
- package/dist/runtime/src/mcp/paddleocr-server.ts +105 -0
- package/dist/runtime/src/mcp/peer-bridge.ts +114 -0
- package/dist/runtime/src/mcp/runtime-spec.ts +137 -0
- package/dist/runtime/src/mcp/session-comm/bridge-ipc-config.ts +27 -0
- package/dist/runtime/src/mcp/session-comm/peer-forward.ts +165 -0
- package/dist/runtime/src/mcp/session-comm/runtime.ts +115 -0
- package/dist/runtime/src/mcp/session-comm/server.ts +649 -0
- package/dist/runtime/src/mcp/session-comm/topic-config.ts +131 -0
- package/dist/runtime/src/mcp/session-comm/topics.ts +229 -0
- package/dist/runtime/src/mcp/stdio-protect.ts +84 -0
- package/dist/runtime/src/mcp/system-health-server.ts +130 -0
- package/dist/runtime/src/mcp/task-server.ts +173 -0
- package/dist/runtime/src/mcp/token-stats-server.ts +98 -0
- package/dist/runtime/src/mcp/vault-http.ts +219 -0
- package/dist/runtime/src/mcp/vault-run.ts +153 -0
- package/dist/runtime/src/mcp/vault-server.ts +104 -0
- package/dist/runtime/src/mcp/wiki-server.ts +755 -0
- package/dist/runtime/src/media/file-events.ts +33 -0
- package/dist/runtime/src/media/text-extractor.ts +312 -0
- package/dist/runtime/src/media/video.ts +92 -0
- package/dist/runtime/src/outbox/file-ops.ts +210 -0
- package/dist/runtime/src/outbox/index.ts +8 -0
- package/dist/runtime/src/outbox/utils.ts +51 -0
- package/dist/runtime/src/platform/background-bash/manager.ts +279 -0
- package/dist/runtime/src/platform/config.ts +371 -0
- package/dist/runtime/src/platform/constants.ts +18 -0
- package/dist/runtime/src/platform/debug-users.ts +14 -0
- package/dist/runtime/src/platform/delay.ts +1 -0
- package/dist/runtime/src/platform/error.ts +9 -0
- package/dist/runtime/src/platform/file-utils.ts +16 -0
- package/dist/runtime/src/platform/jsonl.ts +202 -0
- package/dist/runtime/src/platform/lifecycle.ts +174 -0
- package/dist/runtime/src/platform/logger.ts +31 -0
- package/dist/runtime/src/platform/maestro-bootstrap-env.ts +3 -0
- package/dist/runtime/src/platform/mcp-config.ts +878 -0
- package/dist/runtime/src/platform/modules.ts +145 -0
- package/dist/runtime/src/platform/node-plugins.ts +46 -0
- package/dist/runtime/src/platform/playwright/manager.ts +1233 -0
- package/dist/runtime/src/prompts/agents/wiki-archiver.md +261 -0
- package/dist/runtime/src/prompts/builders.ts +368 -0
- package/dist/runtime/src/prompts/sessions/channel-system.md +48 -0
- package/dist/runtime/src/prompts/sessions/manager-system.md +37 -0
- package/dist/runtime/src/prompts/sessions/topic-system.md +47 -0
- package/dist/runtime/src/prompts/sessions/visual-design.md +86 -0
- package/dist/runtime/src/query/active-rooms.ts +534 -0
- package/dist/runtime/src/query/control.ts +313 -0
- package/dist/runtime/src/query/index.ts +24 -0
- package/dist/runtime/src/query/session-inbox-cleanup.ts +44 -0
- package/dist/runtime/src/query/session-inbox-path.ts +42 -0
- package/dist/runtime/src/query/state.ts +94 -0
- package/dist/runtime/src/query/types.ts +83 -0
- package/dist/runtime/src/runtime/ask-callbacks.ts +153 -0
- package/dist/runtime/src/runtime/attachments.ts +151 -0
- package/dist/runtime/src/runtime/background-sessions.ts +83 -0
- package/dist/runtime/src/runtime/channel-context.ts +107 -0
- package/dist/runtime/src/runtime/errors.ts +69 -0
- package/dist/runtime/src/runtime/event-heartbeat.ts +77 -0
- package/dist/runtime/src/runtime/file-hooks.ts +81 -0
- package/dist/runtime/src/runtime/footer.ts +23 -0
- package/dist/runtime/src/runtime/inbox.ts +946 -0
- package/dist/runtime/src/runtime/tasks.ts +56 -0
- package/dist/runtime/src/runtime/topic-config.ts +29 -0
- package/dist/runtime/src/runtime/turn-runner.ts +2298 -0
- package/dist/runtime/src/runtime/usage-alert.ts +84 -0
- package/dist/runtime/src/runtime/visual-html.ts +109 -0
- package/dist/runtime/src/runtime/visual-store.ts +430 -0
- package/dist/runtime/src/runtime/visuals.ts +171 -0
- package/dist/runtime/src/security/sanitize.ts +27 -0
- package/dist/runtime/src/security/sensitive-path.ts +34 -0
- package/dist/runtime/src/storage/activity-log.ts +152 -0
- package/dist/runtime/src/storage/api-messages.ts +617 -0
- package/dist/runtime/src/storage/api-topic-brief.ts +127 -0
- package/dist/runtime/src/storage/api-topic-config.ts +192 -0
- package/dist/runtime/src/storage/api-topics.ts +844 -0
- package/dist/runtime/src/storage/app-settings.ts +44 -0
- package/dist/runtime/src/storage/browser-profiles.ts +133 -0
- package/dist/runtime/src/storage/conversations.ts +268 -0
- package/dist/runtime/src/storage/forum/agent-settings.ts +20 -0
- package/dist/runtime/src/storage/forum/index.ts +4 -0
- package/dist/runtime/src/storage/forum/repository.ts +283 -0
- package/dist/runtime/src/storage/forum/schema.ts +300 -0
- package/dist/runtime/src/storage/forum-db.ts +23 -0
- package/dist/runtime/src/storage/runtime-events.ts +130 -0
- package/dist/runtime/src/storage/runtime-leases.ts +189 -0
- package/dist/runtime/src/storage/runtime-process-leases.ts +216 -0
- package/dist/runtime/src/storage/runtime-topic-state.ts +148 -0
- package/dist/runtime/src/storage/runtime-turn-requests.ts +301 -0
- package/dist/runtime/src/storage/self-schedules.ts +298 -0
- package/dist/runtime/src/storage/session-asks.ts +347 -0
- package/dist/runtime/src/storage/sqlite.ts +129 -0
- package/dist/runtime/src/storage/tasks.ts +179 -0
- package/dist/runtime/src/storage/token-stats.ts +144 -0
- package/dist/runtime/src/storage/topic-archive-state.ts +59 -0
- package/dist/runtime/src/storage/topic-archive.ts +74 -0
- package/dist/runtime/src/storage/topic-settings.ts +67 -0
- package/dist/runtime/src/storage/topic-transcript.ts +98 -0
- package/dist/runtime/src/storage/vault-crypto.ts +65 -0
- package/dist/runtime/src/storage/vault.ts +225 -0
- package/dist/runtime/src/storage/wiki-summary-names.ts +33 -0
- package/dist/runtime/src/storage/wiki.ts +85 -0
- package/dist/runtime/src/topics/create.ts +123 -0
- package/dist/runtime/src/topics/derive.ts +398 -0
- package/dist/runtime/src/topics/lifecycle.ts +311 -0
- package/dist/runtime/src/topics/links.ts +7 -0
- package/dist/runtime/src/topics/personal-general.ts +65 -0
- package/dist/runtime/src/topics/session.ts +381 -0
- package/dist/runtime/src/types/api.ts +497 -0
- package/dist/runtime/src/types.ts +257 -0
- package/dist/runtime/src/version.ts +1 -0
- package/dist/runtime/tsconfig.json +13 -0
- package/package.json +28 -3
- package/dist/main.d.ts +0 -3
- package/dist/main.d.ts.map +0 -1
|
@@ -0,0 +1,755 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
5
|
+
import { argv, exit } from "node:process";
|
|
6
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
7
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
|
+
import {
|
|
9
|
+
CallToolRequestSchema,
|
|
10
|
+
type CallToolResult,
|
|
11
|
+
ListToolsRequestSchema,
|
|
12
|
+
type ListToolsResult,
|
|
13
|
+
type Tool,
|
|
14
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
15
|
+
import { SHARED_WIKI_DIR } from "#platform/config";
|
|
16
|
+
import {
|
|
17
|
+
wikiBriefStorageKey,
|
|
18
|
+
wikiSummaryFilename,
|
|
19
|
+
wikiSummarySlug,
|
|
20
|
+
} from "#storage/wiki-summary-names";
|
|
21
|
+
|
|
22
|
+
// --- Configuration --------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
// Otium has one shared filesystem wiki for the workspace. userId/topicId still
|
|
25
|
+
// influence DB keys and filenames, but never the wiki root.
|
|
26
|
+
const WIKI_ROOT = SHARED_WIKI_DIR;
|
|
27
|
+
|
|
28
|
+
// --- CLI parsing -----------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
type WikiSurface = "all" | "wiki" | "skills";
|
|
31
|
+
|
|
32
|
+
function parseArgv(): { userId: string; topicId?: string; surface: WikiSurface } {
|
|
33
|
+
let userId = "default";
|
|
34
|
+
let topicId: string | undefined;
|
|
35
|
+
let surface: WikiSurface = "all";
|
|
36
|
+
|
|
37
|
+
for (let i = 2; i < argv.length; i++) {
|
|
38
|
+
const a = argv[i];
|
|
39
|
+
if (a.startsWith("--user-id=")) userId = a.slice("--user-id=".length);
|
|
40
|
+
else if (a.startsWith("--topic-id=")) topicId = a.slice("--topic-id=".length);
|
|
41
|
+
else if (a === "--surface=wiki") surface = "wiki";
|
|
42
|
+
else if (a === "--surface=skills") surface = "skills";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return { userId, topicId, surface };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const { topicId, surface } = parseArgv();
|
|
49
|
+
|
|
50
|
+
const WIKI_DIR = WIKI_ROOT;
|
|
51
|
+
|
|
52
|
+
const SKILLS_DIR = resolve(WIKI_DIR, "skills");
|
|
53
|
+
const TOPICS_DIR = resolve(WIKI_DIR, "topic");
|
|
54
|
+
const SUMMARIES_DIR = resolve(WIKI_DIR, "summaries");
|
|
55
|
+
const ARTICLES_DIR = resolve(WIKI_DIR, "articles");
|
|
56
|
+
const ARCHIVE_DIR = resolve(WIKI_DIR, "archive");
|
|
57
|
+
|
|
58
|
+
// --- DB bridge (topicId mode only) ----------------------------------------
|
|
59
|
+
// Imported lazily so the DB connection is only opened when needed AND the
|
|
60
|
+
// import uses the shared @/ path alias that works under both bun and node.
|
|
61
|
+
|
|
62
|
+
let getTopicBrief:
|
|
63
|
+
| ((id: string) => {
|
|
64
|
+
briefMd: string;
|
|
65
|
+
latestSummaryMd?: string;
|
|
66
|
+
summaryDate?: string;
|
|
67
|
+
updatedAt: string;
|
|
68
|
+
} | null)
|
|
69
|
+
| undefined;
|
|
70
|
+
let setTopicBrief:
|
|
71
|
+
| ((id: string, f: { briefMd?: string; latestSummaryMd?: string; summaryDate?: string }) => void)
|
|
72
|
+
| undefined;
|
|
73
|
+
|
|
74
|
+
async function ensureDbBridge() {
|
|
75
|
+
if (!topicId) return;
|
|
76
|
+
if (getTopicBrief) return; // already loaded
|
|
77
|
+
try {
|
|
78
|
+
const mod = await import("#storage/api-topic-brief");
|
|
79
|
+
getTopicBrief = mod.getTopicBrief;
|
|
80
|
+
setTopicBrief = mod.setTopicBrief;
|
|
81
|
+
} catch {
|
|
82
|
+
// DB not available — degrade gracefully (file-only).
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// --- Helpers ---------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
function ensureDir(dir: string): void {
|
|
89
|
+
mkdirSync(dir, { recursive: true });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function safeExt<T, K extends string = string>(
|
|
93
|
+
obj: Record<string, unknown> | undefined | null,
|
|
94
|
+
keys: readonly K[],
|
|
95
|
+
fallback: T,
|
|
96
|
+
): T {
|
|
97
|
+
if (!obj || typeof obj !== "object") return fallback;
|
|
98
|
+
for (const key of keys) {
|
|
99
|
+
const value = obj[key];
|
|
100
|
+
if (value !== undefined && value !== null) return value as T;
|
|
101
|
+
}
|
|
102
|
+
return fallback;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function topicNameFrom(topicIdStr: string): string {
|
|
106
|
+
return topicIdStr;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function slugify(topic: string): string {
|
|
110
|
+
return wikiSummarySlug(topic);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// --- Tool handlers (file-based) -------------------------------------------
|
|
114
|
+
|
|
115
|
+
function wikiQuery(args: Record<string, unknown>): CallToolResult {
|
|
116
|
+
const question = safeExt(args, ["question", "query", "q", "text"], "");
|
|
117
|
+
const query = question.toLowerCase();
|
|
118
|
+
|
|
119
|
+
const results: string[] = [];
|
|
120
|
+
const scored: { score: number; path: string; text: string }[] = [];
|
|
121
|
+
|
|
122
|
+
function scan(dir: string, label: string): void {
|
|
123
|
+
ensureDir(dir);
|
|
124
|
+
try {
|
|
125
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
126
|
+
if (entry.isDirectory()) {
|
|
127
|
+
const sub = join(dir, entry.name);
|
|
128
|
+
// Limit depth: only go one level deep for articles/skills
|
|
129
|
+
if (label === "articles" || label === "skills") {
|
|
130
|
+
try {
|
|
131
|
+
for (const f of readdirSync(sub, { withFileTypes: true })) {
|
|
132
|
+
if (!f.isFile() || !f.name.endsWith(".md")) continue;
|
|
133
|
+
const fp = join(sub, f.name);
|
|
134
|
+
try {
|
|
135
|
+
const text = readFileSync(fp, "utf-8");
|
|
136
|
+
scored.push({
|
|
137
|
+
score: scoreMatch(text),
|
|
138
|
+
path: `${label}/${entry.name}/${f.name}`,
|
|
139
|
+
text,
|
|
140
|
+
});
|
|
141
|
+
} catch {
|
|
142
|
+
/* skip unreadable */
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
} catch {
|
|
146
|
+
/* skip unreadable dir */
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
150
|
+
const fp = join(dir, entry.name);
|
|
151
|
+
try {
|
|
152
|
+
const text = readFileSync(fp, "utf-8");
|
|
153
|
+
scored.push({ score: scoreMatch(text), path: `${label}/${entry.name}`, text });
|
|
154
|
+
} catch {
|
|
155
|
+
/* skip unreadable */
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
} catch {
|
|
160
|
+
/* dir may not exist */
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function scoreMatch(text: string): number {
|
|
165
|
+
const lower = text.toLowerCase();
|
|
166
|
+
let score = 0;
|
|
167
|
+
const words = query.split(/\s+/).filter(Boolean);
|
|
168
|
+
for (const w of words) {
|
|
169
|
+
if (lower.includes(w)) {
|
|
170
|
+
score += w.length >= 3 ? 3 : 1;
|
|
171
|
+
// Bonus for title match (first line starts with #)
|
|
172
|
+
const firstLine = lower.split("\n")[0];
|
|
173
|
+
if (firstLine.startsWith("#") && firstLine.includes(w)) score += 5;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return score;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
scan(ARTICLES_DIR, "articles");
|
|
180
|
+
// Skills are node-local runtime knowledge, not canonical workspace memory.
|
|
181
|
+
// Keep the legacy all-in-one server compatible while ensuring the explicit
|
|
182
|
+
// wiki surface cannot read a node's skill library.
|
|
183
|
+
if (surface === "all") scan(SKILLS_DIR, "skills");
|
|
184
|
+
scan(TOPICS_DIR, "topic");
|
|
185
|
+
scan(SUMMARIES_DIR, "summaries");
|
|
186
|
+
|
|
187
|
+
scored.sort((a, b) => b.score - a.score);
|
|
188
|
+
const top = scored.slice(0, 8);
|
|
189
|
+
|
|
190
|
+
if (top.length === 0) {
|
|
191
|
+
results.push("No matching wiki articles found.");
|
|
192
|
+
} else {
|
|
193
|
+
results.push(`Found ${top.length} matching result(s):\n`);
|
|
194
|
+
for (const { path, text } of top) {
|
|
195
|
+
// Extract title from first heading
|
|
196
|
+
const titleLine = text.split("\n").find((l) => l.startsWith("#"));
|
|
197
|
+
const title = titleLine ? titleLine.replace(/^#+\s*/, "") : basename(path, ".md");
|
|
198
|
+
// Truncate body to 400 chars
|
|
199
|
+
const body = text
|
|
200
|
+
.replace(/^---[\s\S]*?---\n?/, "")
|
|
201
|
+
.replace(/^#.*$/m, "")
|
|
202
|
+
.trim()
|
|
203
|
+
.slice(0, 400);
|
|
204
|
+
results.push(
|
|
205
|
+
`## ${title}\nPath: ${path}\n\n${body}${text.length > 400 ? "\n...(truncated)" : ""}\n`,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return { content: [{ type: "text", text: results.join("\n") }] };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function wikiTopicBrief(args: Record<string, unknown>): CallToolResult {
|
|
214
|
+
const rawTopic = safeExt<string | undefined>(
|
|
215
|
+
args,
|
|
216
|
+
["topic", "topicName", "topicId"],
|
|
217
|
+
topicId ?? undefined,
|
|
218
|
+
);
|
|
219
|
+
if (!rawTopic) {
|
|
220
|
+
return {
|
|
221
|
+
content: [{ type: "text", text: "No topic specified (provide --topic-id or topic arg)." }],
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (topicId && getTopicBrief) {
|
|
226
|
+
try {
|
|
227
|
+
const brief = getTopicBrief(rawTopic);
|
|
228
|
+
if (!brief) {
|
|
229
|
+
return { content: [{ type: "text", text: `No brief found for topic: ${rawTopic}` }] };
|
|
230
|
+
}
|
|
231
|
+
const lines = [
|
|
232
|
+
`# Topic Brief: ${topicNameFrom(rawTopic)}`,
|
|
233
|
+
"",
|
|
234
|
+
brief.briefMd || "(empty brief)",
|
|
235
|
+
];
|
|
236
|
+
if (brief.latestSummaryMd) {
|
|
237
|
+
lines.push("", "## Latest Summary", "", brief.latestSummaryMd);
|
|
238
|
+
}
|
|
239
|
+
if (brief.summaryDate) {
|
|
240
|
+
lines.push("", `Summary date: ${brief.summaryDate}`);
|
|
241
|
+
}
|
|
242
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
243
|
+
} catch {
|
|
244
|
+
return { content: [{ type: "text", text: `Could not read topic brief for: ${rawTopic}` }] };
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return { content: [{ type: "text", text: `No topic brief found for: ${rawTopic}` }] };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function wikiLastConversation(args: Record<string, unknown>): CallToolResult {
|
|
252
|
+
const rawTopic = safeExt<string | undefined>(
|
|
253
|
+
args,
|
|
254
|
+
["topic", "topicName", "topicId"],
|
|
255
|
+
topicId ?? undefined,
|
|
256
|
+
);
|
|
257
|
+
const turns = safeExt(args, ["turns", "limit", "n"], 5);
|
|
258
|
+
const maxTurns = Math.min(Math.max(1, Number(turns) || 5), 10);
|
|
259
|
+
|
|
260
|
+
if (!rawTopic) {
|
|
261
|
+
return { content: [{ type: "text", text: "No topic specified." }] };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const name = slugify(topicNameFrom(rawTopic));
|
|
265
|
+
|
|
266
|
+
// Try per-topic archive dir first, then flat file
|
|
267
|
+
const archiveDir = resolve(ARCHIVE_DIR, name);
|
|
268
|
+
ensureDir(archiveDir);
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
// List archive files sorted by name (which should be date-sorted)
|
|
272
|
+
let files = readdirSync(archiveDir)
|
|
273
|
+
.filter((f) => f.endsWith(".jsonl"))
|
|
274
|
+
.sort()
|
|
275
|
+
.reverse();
|
|
276
|
+
if (files.length === 0) {
|
|
277
|
+
// Try flat archive
|
|
278
|
+
ensureDir(ARCHIVE_DIR);
|
|
279
|
+
files = readdirSync(ARCHIVE_DIR)
|
|
280
|
+
.filter((f) => f.startsWith(name) && f.endsWith(".jsonl"))
|
|
281
|
+
.sort()
|
|
282
|
+
.reverse();
|
|
283
|
+
}
|
|
284
|
+
if (files.length === 0) {
|
|
285
|
+
return { content: [{ type: "text", text: `No archive found for topic: ${rawTopic}` }] };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Read most recent archive file
|
|
289
|
+
const actualPath = files[0].includes("/")
|
|
290
|
+
? files[0]
|
|
291
|
+
: resolve(readdirSync(archiveDir).includes(files[0]) ? archiveDir : ARCHIVE_DIR, files[0]);
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
const raw = readFileSync(actualPath, "utf-8");
|
|
295
|
+
const lines = raw.trim().split("\n").filter(Boolean);
|
|
296
|
+
|
|
297
|
+
// Reconstruct turns from UnifiedEvent JSONL: pair user_message with following text/result
|
|
298
|
+
const turns: string[] = [];
|
|
299
|
+
let currentUser = "";
|
|
300
|
+
let currentAssistant = "";
|
|
301
|
+
|
|
302
|
+
const truncate = (s: string, max: number) =>
|
|
303
|
+
s.length > max ? `${s.slice(0, max)}\n...(truncated)` : s;
|
|
304
|
+
|
|
305
|
+
const flush = () => {
|
|
306
|
+
if (currentUser || currentAssistant) {
|
|
307
|
+
turns.push(
|
|
308
|
+
`**User**: ${truncate(currentUser || "(no prompt)", 500)}\n\n**Assistant**: ${truncate(currentAssistant || "(no response)", 1000)}`,
|
|
309
|
+
);
|
|
310
|
+
currentUser = "";
|
|
311
|
+
currentAssistant = "";
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
for (const line of lines) {
|
|
316
|
+
try {
|
|
317
|
+
// Archives are written by archiveTopicMessages → TopicArchiveTranscriptRecord:
|
|
318
|
+
// { type: "message", role: "user"|"assistant"|"system", text: "..." }
|
|
319
|
+
const record = JSON.parse(line) as { type: string; role?: string; text?: string };
|
|
320
|
+
if (record.type === "message" && record.role === "user") {
|
|
321
|
+
flush();
|
|
322
|
+
currentUser = record.text ?? "";
|
|
323
|
+
} else if (record.type === "message" && record.role === "assistant") {
|
|
324
|
+
currentAssistant += record.text ?? "";
|
|
325
|
+
}
|
|
326
|
+
} catch {
|
|
327
|
+
// skip malformed lines
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
flush();
|
|
331
|
+
|
|
332
|
+
if (turns.length === 0) {
|
|
333
|
+
return { content: [{ type: "text", text: `Archive file found but no valid entries.` }] };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const recent = turns.slice(-maxTurns);
|
|
337
|
+
return {
|
|
338
|
+
content: [
|
|
339
|
+
{
|
|
340
|
+
type: "text",
|
|
341
|
+
text: `## Last ${recent.length} turns from "${rawTopic}" (${files[0]})\n\n${recent.join("\n\n---\n\n")}`,
|
|
342
|
+
},
|
|
343
|
+
],
|
|
344
|
+
};
|
|
345
|
+
} catch {
|
|
346
|
+
return { content: [{ type: "text", text: `Could not read archive for: ${rawTopic}` }] };
|
|
347
|
+
}
|
|
348
|
+
} catch {
|
|
349
|
+
return { content: [{ type: "text", text: `No archive found for: ${rawTopic}` }] };
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function skillQuery(args: Record<string, unknown>): CallToolResult {
|
|
354
|
+
const question = safeExt(args, ["question", "query", "q"], "");
|
|
355
|
+
const query = question.toLowerCase();
|
|
356
|
+
|
|
357
|
+
ensureDir(SKILLS_DIR);
|
|
358
|
+
|
|
359
|
+
const matches: { name: string; path: string; desc: string }[] = [];
|
|
360
|
+
|
|
361
|
+
try {
|
|
362
|
+
for (const entry of readdirSync(SKILLS_DIR, { withFileTypes: true })) {
|
|
363
|
+
if (!entry.isDirectory()) continue;
|
|
364
|
+
const skillFile = resolve(SKILLS_DIR, entry.name, "skill.md");
|
|
365
|
+
try {
|
|
366
|
+
const text = readFileSync(skillFile, "utf-8");
|
|
367
|
+
const lower = text.toLowerCase();
|
|
368
|
+
let score = 0;
|
|
369
|
+
for (const w of query.split(/\s+/).filter(Boolean)) {
|
|
370
|
+
if (lower.includes(w)) score += w.length >= 3 ? 3 : 1;
|
|
371
|
+
}
|
|
372
|
+
if (score > 0) {
|
|
373
|
+
// Extract name/description from frontmatter
|
|
374
|
+
const fmMatch = text.match(/^---\n([\s\S]*?)\n---/);
|
|
375
|
+
let name = entry.name;
|
|
376
|
+
let desc = "";
|
|
377
|
+
if (fmMatch) {
|
|
378
|
+
const fm = fmMatch[1];
|
|
379
|
+
const nMatch = fm.match(/^name:\s*(.+)$/m);
|
|
380
|
+
const dMatch = fm.match(/^description:\s*(.+)$/m);
|
|
381
|
+
if (nMatch) name = nMatch[1].trim();
|
|
382
|
+
if (dMatch) desc = dMatch[1].trim();
|
|
383
|
+
}
|
|
384
|
+
matches.push({ name, path: `skills/${entry.name}/skill.md`, desc });
|
|
385
|
+
}
|
|
386
|
+
} catch {
|
|
387
|
+
/* skip */
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
} catch {
|
|
391
|
+
/* dir may not exist */
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (matches.length === 0) {
|
|
395
|
+
return { content: [{ type: "text", text: "No matching skills found." }] };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const lines = matches.map((m) => `- **${m.name}** (${m.path})${m.desc ? ` — ${m.desc}` : ""}`);
|
|
399
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function skillSave(args: Record<string, unknown>): CallToolResult {
|
|
403
|
+
const name = safeExt(args, ["name", "skill_name"], "");
|
|
404
|
+
const content: string = safeExt(args, ["content", "text", "body"], "");
|
|
405
|
+
|
|
406
|
+
if (!name) return { content: [{ type: "text", text: "Missing skill name." }] };
|
|
407
|
+
if (!content) return { content: [{ type: "text", text: "Missing skill content." }] };
|
|
408
|
+
|
|
409
|
+
function extractGotchas(md: string): string[] {
|
|
410
|
+
// No `m` flag: `$` anchors to end-of-string so lazy [\s\S]*? isn't fooled
|
|
411
|
+
// by mid-section newlines the way it would be in multiline mode.
|
|
412
|
+
const m = md.match(/(?:^|\n)## Gotchas\s*\n([\s\S]*?)(?=\n## |$)/);
|
|
413
|
+
if (!m) return [];
|
|
414
|
+
return m[1].split("\n").filter((l) => l.trim().startsWith("-"));
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function mergeGotchas(target: string, extra: string[]): string {
|
|
418
|
+
const existing = new Set(extractGotchas(target).map((l) => l.toLowerCase()));
|
|
419
|
+
const fresh = extra.filter((l) => !existing.has(l.toLowerCase()));
|
|
420
|
+
if (fresh.length === 0) return target;
|
|
421
|
+
const sectionMatch = target.match(/(?:^|\n)(## Gotchas\s*\n[\s\S]*?)(?=\n## |$)/);
|
|
422
|
+
if (sectionMatch) {
|
|
423
|
+
const block = sectionMatch[1];
|
|
424
|
+
const insert = `${block.trimEnd()}\n${fresh.join("\n")}`;
|
|
425
|
+
return target.replace(block, insert);
|
|
426
|
+
}
|
|
427
|
+
return `${target.trimEnd()}\n\n## Gotchas\n${fresh.join("\n")}\n`;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const skillDir = resolve(SKILLS_DIR, name);
|
|
431
|
+
ensureDir(skillDir);
|
|
432
|
+
const skillPath = resolve(skillDir, "skill.md");
|
|
433
|
+
|
|
434
|
+
let finalContent = content;
|
|
435
|
+
try {
|
|
436
|
+
const existing = readFileSync(skillPath, "utf-8");
|
|
437
|
+
finalContent = mergeGotchas(content, extractGotchas(existing));
|
|
438
|
+
} catch {
|
|
439
|
+
// new skill — no existing file to merge from
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
writeFileSync(skillPath, finalContent, "utf-8");
|
|
443
|
+
|
|
444
|
+
return { content: [{ type: "text", text: `Skill "${name}" saved at skills/${name}/skill.md` }] };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function saveWikiEntry(args: Record<string, unknown>): CallToolResult {
|
|
448
|
+
const rawTopic = safeExt(args, ["topic", "topicName"], "");
|
|
449
|
+
const content: string = safeExt(args, ["content", "text", "body"], "");
|
|
450
|
+
|
|
451
|
+
if (!rawTopic) return { content: [{ type: "text", text: "Missing topic." }] };
|
|
452
|
+
if (!content) return { content: [{ type: "text", text: "Missing content." }] };
|
|
453
|
+
|
|
454
|
+
const now = new Date();
|
|
455
|
+
const dateStr = now.toISOString().slice(0, 10);
|
|
456
|
+
const summaryName = wikiSummaryFilename(dateStr, rawTopic, topicId);
|
|
457
|
+
const storageKey = wikiBriefStorageKey(rawTopic, topicId);
|
|
458
|
+
const name = slugify(storageKey);
|
|
459
|
+
|
|
460
|
+
// Save to summaries directory
|
|
461
|
+
ensureDir(SUMMARIES_DIR);
|
|
462
|
+
const summaryFile = resolve(SUMMARIES_DIR, summaryName);
|
|
463
|
+
writeFileSync(summaryFile, content, "utf-8");
|
|
464
|
+
|
|
465
|
+
// Also update SQLite brief if in topicId mode
|
|
466
|
+
if (topicId && setTopicBrief) {
|
|
467
|
+
try {
|
|
468
|
+
// Extract a brief from the first paragraph (non-heading)
|
|
469
|
+
const bodyStart = content.indexOf("\n\n");
|
|
470
|
+
const briefParagraph =
|
|
471
|
+
bodyStart > 0
|
|
472
|
+
? (content.slice(bodyStart).trim().split("\n\n")[0]?.slice(0, 600) ?? "")
|
|
473
|
+
: content.slice(0, 600);
|
|
474
|
+
setTopicBrief(storageKey, {
|
|
475
|
+
latestSummaryMd: content,
|
|
476
|
+
summaryDate: dateStr,
|
|
477
|
+
briefMd: briefParagraph,
|
|
478
|
+
});
|
|
479
|
+
} catch {
|
|
480
|
+
// DB update failed — file save succeeded, not critical.
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Also update topic brief file for backward-compat
|
|
485
|
+
ensureDir(TOPICS_DIR);
|
|
486
|
+
const briefPath = resolve(TOPICS_DIR, `${name}.md`);
|
|
487
|
+
try {
|
|
488
|
+
writeFileSync(briefPath, content, "utf-8");
|
|
489
|
+
} catch {
|
|
490
|
+
/* best effort */
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return {
|
|
494
|
+
content: [
|
|
495
|
+
{
|
|
496
|
+
type: "text",
|
|
497
|
+
text:
|
|
498
|
+
`Saved summary: summaries/${summaryName}` +
|
|
499
|
+
(topicId ? "\nSQLite brief also updated." : ""),
|
|
500
|
+
},
|
|
501
|
+
],
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function indexUpsert(args: Record<string, unknown>): CallToolResult {
|
|
506
|
+
const slug = safeExt(args, ["slug", "id"], "");
|
|
507
|
+
const desc = safeExt(args, ["description", "desc", "text"], "");
|
|
508
|
+
const kind: string = safeExt(args, ["kind", "type"], "article");
|
|
509
|
+
const section = safeExt<string | undefined>(args, ["section", "category"], undefined);
|
|
510
|
+
const date = safeExt<string | undefined>(args, ["date", "created"], undefined);
|
|
511
|
+
|
|
512
|
+
if (!slug) return { content: [{ type: "text", text: "Missing slug." }] };
|
|
513
|
+
|
|
514
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
515
|
+
const dateStr = date ?? today;
|
|
516
|
+
|
|
517
|
+
const indexPath = (() => {
|
|
518
|
+
if (kind === "topic") return resolve(WIKI_DIR, "topic-index.md");
|
|
519
|
+
if (kind === "skill") return resolve(WIKI_DIR, "skill-index.md");
|
|
520
|
+
return resolve(WIKI_DIR, "article-index.md");
|
|
521
|
+
})();
|
|
522
|
+
|
|
523
|
+
ensureDir(dirname(indexPath));
|
|
524
|
+
|
|
525
|
+
let index: string;
|
|
526
|
+
try {
|
|
527
|
+
index = readFileSync(indexPath, "utf-8");
|
|
528
|
+
} catch {
|
|
529
|
+
index = "";
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const lines = index.split("\n");
|
|
533
|
+
const link = (() => {
|
|
534
|
+
switch (kind) {
|
|
535
|
+
case "summary":
|
|
536
|
+
return `- [[summaries/${slug}]] ${desc} (${dateStr})`;
|
|
537
|
+
case "topic":
|
|
538
|
+
return `- [[topic/${slug}]] ${desc} (${dateStr})`;
|
|
539
|
+
case "skill":
|
|
540
|
+
return `- [[skills/${slug}]] ${desc} (${dateStr})`;
|
|
541
|
+
default:
|
|
542
|
+
return `- [[articles/${slug}]] ${desc} (${dateStr})`;
|
|
543
|
+
}
|
|
544
|
+
})();
|
|
545
|
+
|
|
546
|
+
// Find existing entry for this slug and replace, or append
|
|
547
|
+
const slugPattern = `[[${slug}]]`; // loose match on wikilink target
|
|
548
|
+
let replaced = false;
|
|
549
|
+
for (let i = 0; i < lines.length; i++) {
|
|
550
|
+
if (lines[i].includes(slugPattern)) {
|
|
551
|
+
lines[i] = link;
|
|
552
|
+
replaced = true;
|
|
553
|
+
break;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (!replaced) {
|
|
558
|
+
// Append to appropriate section
|
|
559
|
+
if (section) {
|
|
560
|
+
let sectionIdx = -1;
|
|
561
|
+
for (let i = 0; i < lines.length; i++) {
|
|
562
|
+
if (lines[i].startsWith("## ") && lines[i].includes(section)) {
|
|
563
|
+
sectionIdx = i;
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
if (sectionIdx >= 0) {
|
|
568
|
+
// Insert after section heading, before next section
|
|
569
|
+
let insertAt = sectionIdx + 1;
|
|
570
|
+
while (
|
|
571
|
+
insertAt < lines.length &&
|
|
572
|
+
lines[insertAt].trim() !== "" &&
|
|
573
|
+
!lines[insertAt].startsWith("## ")
|
|
574
|
+
) {
|
|
575
|
+
insertAt++;
|
|
576
|
+
}
|
|
577
|
+
lines.splice(insertAt, 0, link);
|
|
578
|
+
} else {
|
|
579
|
+
lines.push(`\n## ${section}`, link);
|
|
580
|
+
}
|
|
581
|
+
} else {
|
|
582
|
+
lines.push(link);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
writeFileSync(indexPath, lines.join("\n"), "utf-8");
|
|
587
|
+
|
|
588
|
+
return { content: [{ type: "text", text: `Index updated: ${link}` }] };
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// --- MCP Tool definitions -------------------------------------------------
|
|
592
|
+
|
|
593
|
+
const WIKI_TOOLS: Tool[] = [
|
|
594
|
+
{
|
|
595
|
+
name: "wiki_query",
|
|
596
|
+
description: "Search the wiki knowledge base and return relevant articles.",
|
|
597
|
+
inputSchema: {
|
|
598
|
+
type: "object",
|
|
599
|
+
properties: {
|
|
600
|
+
question: { type: "string", description: "The question or topic to search for" },
|
|
601
|
+
topic: { type: "string", description: "Optional topic name to narrow the search" },
|
|
602
|
+
},
|
|
603
|
+
required: ["question"],
|
|
604
|
+
},
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
name: "wiki_topic_brief",
|
|
608
|
+
description: "Get the lightweight topic brief for a specific topic.",
|
|
609
|
+
inputSchema: {
|
|
610
|
+
type: "object",
|
|
611
|
+
properties: {
|
|
612
|
+
topic: { type: "string", description: "Topic name (e.g. 'dev', 'trading', 'research')" },
|
|
613
|
+
},
|
|
614
|
+
required: ["topic"],
|
|
615
|
+
},
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
name: "wiki_last_conversation",
|
|
619
|
+
description: "Read the last N turns from the most recent archived session log for a topic.",
|
|
620
|
+
inputSchema: {
|
|
621
|
+
type: "object",
|
|
622
|
+
properties: {
|
|
623
|
+
topic: { type: "string", description: "Topic name (e.g. 'dev', 'trading')" },
|
|
624
|
+
turns: {
|
|
625
|
+
type: "number",
|
|
626
|
+
description: "Number of recent turns (max 10), default 5",
|
|
627
|
+
minimum: 1,
|
|
628
|
+
maximum: 10,
|
|
629
|
+
},
|
|
630
|
+
},
|
|
631
|
+
required: ["topic"],
|
|
632
|
+
},
|
|
633
|
+
},
|
|
634
|
+
{
|
|
635
|
+
name: "save_wiki_entry",
|
|
636
|
+
description: "Save a session summary directly to wiki/summaries/.",
|
|
637
|
+
inputSchema: {
|
|
638
|
+
type: "object",
|
|
639
|
+
properties: {
|
|
640
|
+
topic: { type: "string", description: "Topic name of the session" },
|
|
641
|
+
content: { type: "string", description: "Session summary in markdown" },
|
|
642
|
+
},
|
|
643
|
+
required: ["topic", "content"],
|
|
644
|
+
},
|
|
645
|
+
},
|
|
646
|
+
{
|
|
647
|
+
name: "index_upsert",
|
|
648
|
+
description: "Upsert an entry in wiki/article-index.md, topic-index.md, or skill-index.md.",
|
|
649
|
+
inputSchema: {
|
|
650
|
+
type: "object",
|
|
651
|
+
properties: {
|
|
652
|
+
slug: { type: "string", description: "Article/summary slug or topic name" },
|
|
653
|
+
description: { type: "string", description: "Single-line description for the index row" },
|
|
654
|
+
kind: {
|
|
655
|
+
type: "string",
|
|
656
|
+
enum: ["article", "summary", "topic", "skill"],
|
|
657
|
+
description: "Which index to update",
|
|
658
|
+
},
|
|
659
|
+
section: { type: "string", description: "For kind='article': H2 section title" },
|
|
660
|
+
date: { type: "string", description: "Override entry date YYYY-MM-DD (default: today)" },
|
|
661
|
+
},
|
|
662
|
+
required: ["slug", "description", "kind"],
|
|
663
|
+
},
|
|
664
|
+
},
|
|
665
|
+
];
|
|
666
|
+
|
|
667
|
+
const SKILL_TOOLS: Tool[] = [
|
|
668
|
+
{
|
|
669
|
+
name: "skill_query",
|
|
670
|
+
description: "Search this node's local skill library and return matching definitions.",
|
|
671
|
+
inputSchema: {
|
|
672
|
+
type: "object",
|
|
673
|
+
properties: {
|
|
674
|
+
question: {
|
|
675
|
+
type: "string",
|
|
676
|
+
description: "The skill name or description of what you want to do",
|
|
677
|
+
},
|
|
678
|
+
},
|
|
679
|
+
required: ["question"],
|
|
680
|
+
},
|
|
681
|
+
},
|
|
682
|
+
{
|
|
683
|
+
name: "skill_save",
|
|
684
|
+
description: "Create or update a skill on this node.",
|
|
685
|
+
inputSchema: {
|
|
686
|
+
type: "object",
|
|
687
|
+
properties: {
|
|
688
|
+
name: { type: "string", description: "Skill folder name in kebab-case" },
|
|
689
|
+
content: {
|
|
690
|
+
type: "string",
|
|
691
|
+
description: "Skill definition in markdown (with frontmatter name+description)",
|
|
692
|
+
},
|
|
693
|
+
},
|
|
694
|
+
required: ["name", "content"],
|
|
695
|
+
},
|
|
696
|
+
},
|
|
697
|
+
];
|
|
698
|
+
|
|
699
|
+
const TOOLS =
|
|
700
|
+
surface === "wiki"
|
|
701
|
+
? WIKI_TOOLS
|
|
702
|
+
: surface === "skills"
|
|
703
|
+
? SKILL_TOOLS
|
|
704
|
+
: [...WIKI_TOOLS, ...SKILL_TOOLS];
|
|
705
|
+
|
|
706
|
+
// --- Server ----------------------------------------------------------------
|
|
707
|
+
|
|
708
|
+
const server = new Server(
|
|
709
|
+
{ name: "wiki-server", version: "2.0.0" },
|
|
710
|
+
{ capabilities: { tools: {} } },
|
|
711
|
+
);
|
|
712
|
+
|
|
713
|
+
server.setRequestHandler(ListToolsRequestSchema, async (): Promise<ListToolsResult> => {
|
|
714
|
+
return { tools: TOOLS };
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
server.setRequestHandler(CallToolRequestSchema, async (request): Promise<CallToolResult> => {
|
|
718
|
+
const { name, arguments: args } = request.params;
|
|
719
|
+
const a = (args ?? {}) as Record<string, unknown>;
|
|
720
|
+
|
|
721
|
+
{
|
|
722
|
+
const fn: Record<string, (a: Record<string, unknown>) => CallToolResult> = {
|
|
723
|
+
wiki_query: wikiQuery,
|
|
724
|
+
wiki_topic_brief: wikiTopicBrief,
|
|
725
|
+
wiki_last_conversation: wikiLastConversation,
|
|
726
|
+
skill_query: skillQuery,
|
|
727
|
+
skill_save: skillSave,
|
|
728
|
+
save_wiki_entry: saveWikiEntry,
|
|
729
|
+
index_upsert: indexUpsert,
|
|
730
|
+
};
|
|
731
|
+
const handler = TOOLS.some((tool) => tool.name === name) ? fn[name] : undefined;
|
|
732
|
+
if (handler) return handler(a);
|
|
733
|
+
return { content: [{ type: "text", text: `Unknown tool: ${name}` }] };
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
// --- Entrypoint ------------------------------------------------------------
|
|
738
|
+
|
|
739
|
+
async function main() {
|
|
740
|
+
// Initialize DB bridge before accepting connections (topicId mode only)
|
|
741
|
+
await ensureDbBridge();
|
|
742
|
+
|
|
743
|
+
// Ensure directories exist
|
|
744
|
+
for (const d of [SKILLS_DIR, TOPICS_DIR, SUMMARIES_DIR, ARTICLES_DIR, ARCHIVE_DIR]) {
|
|
745
|
+
ensureDir(d);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const transport = new StdioServerTransport();
|
|
749
|
+
await server.connect(transport);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
main().catch((err) => {
|
|
753
|
+
console.error("wiki-server fatal:", err);
|
|
754
|
+
exit(1);
|
|
755
|
+
});
|