import { GraphCache } from './cache.js'; import type { WikiGraph } from './graph.js'; export interface ToolResult { content: Array<{ type: 'text'; text: string }>; isError?: boolean; } export interface ToolDef { name: string; description: string; inputSchema: { type: 'object'; properties: Record; required: string[]; }; handler: (args: Record) => ToolResult; } function text(s: string, isError = false): ToolResult { return { content: [{ type: 'text', text: s }], isError }; } const CORPUS_PROP = { corpus: { type: 'string', description: 'Absolute path to the `.wiki/` corpus directory.', }, }; /** * Build the 5 v1 wiki-graph tools over a shared mtime cache. * Each tool takes `corpus` (abs path to a `.wiki/`); the engine parses * server-side, so the corpus never enters the LLM context. */ export function makeWikiGraphTools(cache: GraphCache): ToolDef[] { function load(args: Record): WikiGraph | ToolResult { const corpus = args.corpus; if (typeof corpus !== 'string' || corpus.length === 0) { return text('Missing required "corpus" (absolute path to a .wiki/ directory).', true); } try { return cache.load(corpus); } catch (e) { return text(`Failed to read corpus "${corpus}": ${(e as Error).message}`, true); } } function str(args: Record, key: string): string | null { const v = args[key]; return typeof v === 'string' && v.length > 0 ? v : null; } return [ { name: 'path', description: 'Shortest chain of pages between two wiki pages (undirected). The main relational query — "what connects X and Y".', inputSchema: { type: 'object', properties: { ...CORPUS_PROP, from: { type: 'string', description: 'Start page slug.' }, to: { type: 'string', description: 'Target page slug.' }, }, required: ['corpus', 'from', 'to'], }, handler: (args) => { const g = load(args); if ('content' in g) return g; const from = str(args, 'from'); const to = str(args, 'to'); if (!from || !to) return text('path requires "from" and "to" slugs.', true); const chain = g.path(from, to); if (chain.length === 0) return text(`No path between "${from}" and "${to}".`); return text(`Path (${chain.length} nodes): ${chain.join(' → ')}`); }, }, { name: 'neighbors', description: 'Pages reachable via outgoing links within N hops (direction respected).', inputSchema: { type: 'object', properties: { ...CORPUS_PROP, node: { type: 'string', description: 'Page slug.' }, depth: { type: 'number', description: 'Hop radius (default 1).' }, }, required: ['corpus', 'node'], }, handler: (args) => { const g = load(args); if ('content' in g) return g; const node = str(args, 'node'); if (!node) return text('neighbors requires a "node" slug.', true); const depthRaw = args.depth; const depth = typeof depthRaw === 'number' && Number.isInteger(depthRaw) && depthRaw >= 1 ? depthRaw : 1; const list = g.neighbors(node, depth); if (list.length === 0) { return text(`"${node}" has no outgoing links within depth ${depth}.`); } return text(`Neighbors of "${node}" (depth ${depth}): ${list.join(', ')}`); }, }, { name: 'backlinks', description: 'Pages that link TO this page (incoming references).', inputSchema: { type: 'object', properties: { ...CORPUS_PROP, node: { type: 'string', description: 'Page slug.' }, }, required: ['corpus', 'node'], }, handler: (args) => { const g = load(args); if ('content' in g) return g; const node = str(args, 'node'); if (!node) return text('backlinks requires a "node" slug.', true); const list = g.backlinks(node); if (list.length === 0) return text(`Nothing links to "${node}".`); return text(`Backlinks of "${node}": ${list.join(', ')}`); }, }, { name: 'orphans', description: 'Wiki health: pages with no links at all, plus dangling (unresolved) link targets.', inputSchema: { type: 'object', properties: { ...CORPUS_PROP }, required: ['corpus'], }, handler: (args) => { const g = load(args); if ('content' in g) return g; const o = g.orphans(); return text( `Orphan pages (${o.pages.length}): ${o.pages.join(', ') || '—'}\n` + `Dangling links (${o.dangling.length}): ${o.dangling.join(', ') || '—'}`, ); }, }, { name: 'stats', description: 'Corpus counters: nodes, edges, connected components, dangling links, basename collisions.', inputSchema: { type: 'object', properties: { ...CORPUS_PROP }, required: ['corpus'], }, handler: (args) => { const g = load(args); if ('content' in g) return g; const s = g.stats(); return text( `nodes=${s.nodes} edges=${s.edges} components=${s.components} ` + `dangling=${s.dangling} collisions=${s.collisions}`, ); }, }, ]; }