melchizedek-agents 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/AGENT_SETUP.md +182 -0
  2. package/DOCUMENTATION.md +614 -0
  3. package/LICENSE +21 -0
  4. package/QUICKSTART.md +164 -0
  5. package/README.md +151 -0
  6. package/config/agents/README.md +27 -0
  7. package/config/agents/examples/ares.yaml +74 -0
  8. package/config/agents/examples/cartographers.yaml +115 -0
  9. package/config/agents/examples/claude.yaml +17 -0
  10. package/config/agents/examples/council.yaml +65 -0
  11. package/config/agents/examples/critic.yaml +74 -0
  12. package/config/agents/examples/delegation.yaml +29 -0
  13. package/config/agents/examples/hierarchical.yaml +26 -0
  14. package/config/agents/examples/image_production.yaml +112 -0
  15. package/config/agents/examples/librarian.yaml +75 -0
  16. package/config/agents/examples/model_zoo.yaml +81 -0
  17. package/config/agents/examples/patient_advocate.yaml +107 -0
  18. package/config/agents/examples/scriptorium.yaml +96 -0
  19. package/config/agents/examples/style_council.yaml +83 -0
  20. package/config/agents/examples/syndicate.yaml +34 -0
  21. package/config/agents/examples/tutor.yaml +66 -0
  22. package/config/agents/syndicateSchema.yaml +291 -0
  23. package/dist/lib/config.d.ts +108 -0
  24. package/dist/lib/config.js +110 -0
  25. package/dist/lib/dispatch.d.ts +133 -0
  26. package/dist/lib/dispatch.js +182 -0
  27. package/dist/lib/grounding.d.ts +28 -0
  28. package/dist/lib/grounding.js +61 -0
  29. package/dist/lib/index.d.ts +14 -0
  30. package/dist/lib/index.js +13 -0
  31. package/dist/lib/loadEnv.d.ts +21 -0
  32. package/dist/lib/loadEnv.js +58 -0
  33. package/dist/lib/loadSyndicate.d.ts +177 -0
  34. package/dist/lib/loadSyndicate.js +264 -0
  35. package/dist/lib/memory/supabaseMemoryService.d.ts +118 -0
  36. package/dist/lib/memory/supabaseMemoryService.js +499 -0
  37. package/dist/lib/models/claudeLlm.d.ts +93 -0
  38. package/dist/lib/models/claudeLlm.js +332 -0
  39. package/dist/lib/models/gptLlm.d.ts +110 -0
  40. package/dist/lib/models/gptLlm.js +497 -0
  41. package/dist/lib/models/grokLlm.d.ts +64 -0
  42. package/dist/lib/models/grokLlm.js +86 -0
  43. package/dist/lib/models/ollamaLlm.d.ts +105 -0
  44. package/dist/lib/models/ollamaLlm.js +134 -0
  45. package/dist/lib/models/openAiCompatibleLlm.d.ts +125 -0
  46. package/dist/lib/models/openAiCompatibleLlm.js +331 -0
  47. package/dist/lib/models/providerMap.d.ts +28 -0
  48. package/dist/lib/models/providerMap.js +45 -0
  49. package/dist/lib/models/registry.d.ts +71 -0
  50. package/dist/lib/models/registry.js +159 -0
  51. package/dist/lib/models/schemaNormalize.d.ts +22 -0
  52. package/dist/lib/models/schemaNormalize.js +58 -0
  53. package/dist/lib/observability/supabaseSpanExporter.d.ts +35 -0
  54. package/dist/lib/observability/supabaseSpanExporter.js +115 -0
  55. package/dist/lib/observability/tracer.d.ts +44 -0
  56. package/dist/lib/observability/tracer.js +242 -0
  57. package/dist/lib/persistence/supabaseProvider.d.ts +44 -0
  58. package/dist/lib/persistence/supabaseProvider.js +88 -0
  59. package/dist/lib/session/supabaseSessionService.d.ts +26 -0
  60. package/dist/lib/session/supabaseSessionService.js +193 -0
  61. package/dist/lib/session/transcript.d.ts +193 -0
  62. package/dist/lib/session/transcript.js +309 -0
  63. package/dist/lib/toolRegistry.d.ts +15 -0
  64. package/dist/lib/toolRegistry.js +58 -0
  65. package/dist/lib/tools/collectionsSearchTool.d.ts +55 -0
  66. package/dist/lib/tools/collectionsSearchTool.js +86 -0
  67. package/dist/lib/tools/generateImageTool.d.ts +16 -0
  68. package/dist/lib/tools/generateImageTool.js +119 -0
  69. package/dist/lib/tools/inspectImageTool.d.ts +21 -0
  70. package/dist/lib/tools/inspectImageTool.js +97 -0
  71. package/dist/lib/tools/mcpToolFactory.d.ts +2 -0
  72. package/dist/lib/tools/mcpToolFactory.js +116 -0
  73. package/dist/lib/tools/toolContract.d.ts +78 -0
  74. package/dist/lib/tools/toolContract.js +129 -0
  75. package/dist/lib/tools/webExtractTool.d.ts +70 -0
  76. package/dist/lib/tools/webExtractTool.js +461 -0
  77. package/dist/lib/tools/webSearchTool.d.ts +53 -0
  78. package/dist/lib/tools/webSearchTool.js +126 -0
  79. package/dist/lib/tools/wikiTools.d.ts +112 -0
  80. package/dist/lib/tools/wikiTools.js +697 -0
  81. package/dist/lib/tools/xSearchTool.d.ts +61 -0
  82. package/dist/lib/tools/xSearchTool.js +125 -0
  83. package/dist/lib/wiki/agentRun.d.ts +39 -0
  84. package/dist/lib/wiki/agentRun.js +79 -0
  85. package/dist/lib/wiki/builder.d.ts +71 -0
  86. package/dist/lib/wiki/builder.js +165 -0
  87. package/dist/lib/wiki/dive.d.ts +56 -0
  88. package/dist/lib/wiki/dive.js +208 -0
  89. package/dist/lib/wiki/entities.d.ts +220 -0
  90. package/dist/lib/wiki/entities.js +661 -0
  91. package/dist/lib/wiki/extract.d.ts +97 -0
  92. package/dist/lib/wiki/extract.js +305 -0
  93. package/dist/lib/wiki/fill.d.ts +37 -0
  94. package/dist/lib/wiki/fill.js +175 -0
  95. package/dist/lib/wiki/format.d.ts +132 -0
  96. package/dist/lib/wiki/format.js +177 -0
  97. package/dist/lib/wiki/graph.d.ts +80 -0
  98. package/dist/lib/wiki/graph.js +147 -0
  99. package/dist/lib/wiki/lint.d.ts +58 -0
  100. package/dist/lib/wiki/lint.js +267 -0
  101. package/dist/lib/wiki/markdown.d.ts +127 -0
  102. package/dist/lib/wiki/markdown.js +336 -0
  103. package/dist/lib/wiki/pipeline.d.ts +69 -0
  104. package/dist/lib/wiki/pipeline.js +119 -0
  105. package/dist/lib/wiki/vault.d.ts +84 -0
  106. package/dist/lib/wiki/vault.js +194 -0
  107. package/dist/scripts/a2a_server.d.ts +2 -0
  108. package/dist/scripts/a2a_server.js +997 -0
  109. package/dist/scripts/syndicate_chat.d.ts +2 -0
  110. package/dist/scripts/syndicate_chat.js +485 -0
  111. package/package.json +96 -0
@@ -0,0 +1,61 @@
1
+ /**
2
+ * lib/tools/xSearchTool.ts — xAI X (Twitter) search, sentinel-style.
3
+ *
4
+ * WHY this file exists:
5
+ * xAI's Agent Tools API exposes `x_search` — server-side live search over
6
+ * X posts — as a first-class tool alongside `web_search` (verified live
7
+ * against api.x.ai/v1/responses, 2026-07-24). It is the one search
8
+ * capability no other provider offers, and the reason to route a
9
+ * news/social agent to a grok-* model at all.
10
+ *
11
+ * HOW IT WORKS (mirrors webSearchTool.ts exactly):
12
+ * Declaring `x_search` in an agent's YAML tools list self-registers a
13
+ * sentinel in llmRequest.toolsDict. _getDeclaration() returns undefined so
14
+ * it is never sent as a client-side function tool; the GptLlm/GrokLlm
15
+ * request builder calls wantsXSearch(llmRequest) and appends
16
+ * { type: 'x_search' } to the provider tools.
17
+ *
18
+ * SCOPE: xAI-only. Declare it ONLY on grok-* agents — other Responses-API
19
+ * providers (OpenAI) reject the unknown tool type with a 400, and Gemini/
20
+ * Anthropic adapters ignore the sentinel entirely (the tool degrades to a
21
+ * no-op there rather than throwing, same non-throwing routing contract as
22
+ * web_search).
23
+ *
24
+ * WHICH CONSTRAINTS: xAI accepts server-side search constraints on the tool
25
+ * object itself (verified against docs.x.ai/developers/tools/x-search,
26
+ * 2026-08-02): from_date/to_date (YYYY-MM-DD) and allowed_x_handles/
27
+ * excluded_x_handles (≤20 each, mutually exclusive). They come from the
28
+ * environment, not YAML — XAI_X_SEARCH_FROM_DATE, XAI_X_SEARCH_TO_DATE,
29
+ * XAI_X_SEARCH_ALLOWED_HANDLES, XAI_X_SEARCH_EXCLUDED_HANDLES (handle
30
+ * lists comma-separated, leading @ tolerated) — the XAI_COLLECTION_IDS
31
+ * doctrine: constraints are deployment configuration, the YAML stays
32
+ * shareable. Misconfiguration degrades with a warning, never fatally:
33
+ * malformed dates are dropped, oversize handle lists truncate, and when
34
+ * both lists are set the allowlist wins.
35
+ */
36
+ import { BaseTool } from '@google/adk';
37
+ import type { LlmRequest } from '@google/adk';
38
+ export declare const X_SEARCH_TOOL_NAME = "x_search";
39
+ export declare class XSearchTool extends BaseTool {
40
+ constructor();
41
+ /** Never a client-side function tool — xAI runs the search server-side. */
42
+ _getDeclaration(): undefined;
43
+ /** Server-side tool: nothing to execute locally. */
44
+ runAsync(): Promise<unknown>;
45
+ processLlmRequest({ llmRequest }: {
46
+ llmRequest: LlmRequest;
47
+ }): Promise<void>;
48
+ }
49
+ /** Shared instance, mirroring WEB_SEARCH. */
50
+ export declare const X_SEARCH: XSearchTool;
51
+ /** True when the agent requested X search (xAI sentinel path). */
52
+ export declare function wantsXSearch(llmRequest: LlmRequest): boolean;
53
+ /** Sentinel test used by the request builder to skip function-tool emission. */
54
+ export declare function isXSearchSentinel(tool: unknown): boolean;
55
+ /**
56
+ * Optional server-side constraints for the x_search tool, read from the
57
+ * environment at request-build time (see WHICH CONSTRAINTS above). With
58
+ * nothing configured this returns {} and the tool ships bare, exactly as
59
+ * it always has.
60
+ */
61
+ export declare function xSearchParamsFromEnv(): Record<string, unknown>;
@@ -0,0 +1,125 @@
1
+ /**
2
+ * lib/tools/xSearchTool.ts — xAI X (Twitter) search, sentinel-style.
3
+ *
4
+ * WHY this file exists:
5
+ * xAI's Agent Tools API exposes `x_search` — server-side live search over
6
+ * X posts — as a first-class tool alongside `web_search` (verified live
7
+ * against api.x.ai/v1/responses, 2026-07-24). It is the one search
8
+ * capability no other provider offers, and the reason to route a
9
+ * news/social agent to a grok-* model at all.
10
+ *
11
+ * HOW IT WORKS (mirrors webSearchTool.ts exactly):
12
+ * Declaring `x_search` in an agent's YAML tools list self-registers a
13
+ * sentinel in llmRequest.toolsDict. _getDeclaration() returns undefined so
14
+ * it is never sent as a client-side function tool; the GptLlm/GrokLlm
15
+ * request builder calls wantsXSearch(llmRequest) and appends
16
+ * { type: 'x_search' } to the provider tools.
17
+ *
18
+ * SCOPE: xAI-only. Declare it ONLY on grok-* agents — other Responses-API
19
+ * providers (OpenAI) reject the unknown tool type with a 400, and Gemini/
20
+ * Anthropic adapters ignore the sentinel entirely (the tool degrades to a
21
+ * no-op there rather than throwing, same non-throwing routing contract as
22
+ * web_search).
23
+ *
24
+ * WHICH CONSTRAINTS: xAI accepts server-side search constraints on the tool
25
+ * object itself (verified against docs.x.ai/developers/tools/x-search,
26
+ * 2026-08-02): from_date/to_date (YYYY-MM-DD) and allowed_x_handles/
27
+ * excluded_x_handles (≤20 each, mutually exclusive). They come from the
28
+ * environment, not YAML — XAI_X_SEARCH_FROM_DATE, XAI_X_SEARCH_TO_DATE,
29
+ * XAI_X_SEARCH_ALLOWED_HANDLES, XAI_X_SEARCH_EXCLUDED_HANDLES (handle
30
+ * lists comma-separated, leading @ tolerated) — the XAI_COLLECTION_IDS
31
+ * doctrine: constraints are deployment configuration, the YAML stays
32
+ * shareable. Misconfiguration degrades with a warning, never fatally:
33
+ * malformed dates are dropped, oversize handle lists truncate, and when
34
+ * both lists are set the allowlist wins.
35
+ */
36
+ import { BaseTool } from '@google/adk';
37
+ import { providerForModel } from "../models/providerMap.js";
38
+ export const X_SEARCH_TOOL_NAME = 'x_search';
39
+ export class XSearchTool extends BaseTool {
40
+ constructor() {
41
+ super({
42
+ name: X_SEARCH_TOOL_NAME,
43
+ description: 'Live search over X (Twitter) posts via xAI Agent Tools — ' +
44
+ 'grok-* models only; a no-op sentinel on every other provider.',
45
+ });
46
+ }
47
+ /** Never a client-side function tool — xAI runs the search server-side. */
48
+ _getDeclaration() {
49
+ return undefined;
50
+ }
51
+ /** Server-side tool: nothing to execute locally. */
52
+ async runAsync() {
53
+ return Promise.resolve();
54
+ }
55
+ async processLlmRequest({ llmRequest }) {
56
+ if (!llmRequest.model)
57
+ return;
58
+ if (providerForModel(llmRequest.model) !== 'xai')
59
+ return; // no-op elsewhere
60
+ llmRequest.toolsDict[X_SEARCH_TOOL_NAME] = this;
61
+ }
62
+ }
63
+ /** Shared instance, mirroring WEB_SEARCH. */
64
+ export const X_SEARCH = new XSearchTool();
65
+ /** True when the agent requested X search (xAI sentinel path). */
66
+ export function wantsXSearch(llmRequest) {
67
+ return llmRequest.toolsDict?.[X_SEARCH_TOOL_NAME] instanceof XSearchTool;
68
+ }
69
+ /** Sentinel test used by the request builder to skip function-tool emission. */
70
+ export function isXSearchSentinel(tool) {
71
+ return tool instanceof XSearchTool;
72
+ }
73
+ /** xAI's documented ceiling on allowed_x_handles / excluded_x_handles. */
74
+ const X_SEARCH_MAX_HANDLES = 20;
75
+ /** ISO date (YYYY-MM-DD) from an env var; malformed values warn and drop. */
76
+ function isoDateFromEnv(name) {
77
+ const raw = process.env[name]?.trim();
78
+ if (!raw)
79
+ return undefined;
80
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(raw)) {
81
+ console.warn(`[x_search] ${name}="${raw}" is not YYYY-MM-DD — ignored.`);
82
+ return undefined;
83
+ }
84
+ return raw;
85
+ }
86
+ /** Comma-separated handles from an env var; trimmed, @-stripped, capped. */
87
+ function handlesFromEnv(name) {
88
+ const handles = (process.env[name] ?? '')
89
+ .split(',')
90
+ .map((h) => h.trim().replace(/^@/, ''))
91
+ .filter(Boolean);
92
+ if (handles.length > X_SEARCH_MAX_HANDLES) {
93
+ console.warn(`[x_search] ${name} lists ${handles.length} handles — xAI caps the ` +
94
+ `list at ${X_SEARCH_MAX_HANDLES}; keeping the first ${X_SEARCH_MAX_HANDLES}.`);
95
+ return handles.slice(0, X_SEARCH_MAX_HANDLES);
96
+ }
97
+ return handles;
98
+ }
99
+ /**
100
+ * Optional server-side constraints for the x_search tool, read from the
101
+ * environment at request-build time (see WHICH CONSTRAINTS above). With
102
+ * nothing configured this returns {} and the tool ships bare, exactly as
103
+ * it always has.
104
+ */
105
+ export function xSearchParamsFromEnv() {
106
+ const params = {};
107
+ const from = isoDateFromEnv('XAI_X_SEARCH_FROM_DATE');
108
+ const to = isoDateFromEnv('XAI_X_SEARCH_TO_DATE');
109
+ if (from)
110
+ params.from_date = from;
111
+ if (to)
112
+ params.to_date = to;
113
+ const allowed = handlesFromEnv('XAI_X_SEARCH_ALLOWED_HANDLES');
114
+ const excluded = handlesFromEnv('XAI_X_SEARCH_EXCLUDED_HANDLES');
115
+ if (allowed.length > 0 && excluded.length > 0) {
116
+ console.warn('[x_search] XAI_X_SEARCH_ALLOWED_HANDLES and _EXCLUDED_HANDLES are ' +
117
+ 'mutually exclusive (xAI rejects both together) — keeping the ' +
118
+ 'allowlist, dropping the exclusions.');
119
+ }
120
+ if (allowed.length > 0)
121
+ params.allowed_x_handles = allowed;
122
+ else if (excluded.length > 0)
123
+ params.excluded_x_handles = excluded;
124
+ return params;
125
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * lib/wiki/agentRun.ts — one-shot agent execution for wiki operations.
3
+ *
4
+ * WHY this file exists:
5
+ * Three wiki operations need a model in the loop — gap-fill (write the
6
+ * prose a structural build can't), wiki_query (answer a question over the
7
+ * bundle), wiki_garden (author or revise a document). Each is a single
8
+ * agent run: build an ADK LlmAgent, hand it the wiki FunctionTools it
9
+ * needs, stream one exchange, return the text. This helper is that idiom
10
+ * (the same one scripts/demo_model_optionality.ts uses), shared so the
11
+ * three call sites can't drift.
12
+ *
13
+ * Model routing goes through lib/models/registry.ts, so the YAML-style
14
+ * model string ('gemini-3.7-flash', 'claude-*', 'ollama/*'…) picks the
15
+ * provider — wiki operations inherit the framework's full model
16
+ * optionality. A missing provider key degrades to a clear error string;
17
+ * nothing here throws for want of credentials.
18
+ */
19
+ import type { FunctionTool } from '@google/adk';
20
+ export interface WikiAgentRun {
21
+ name: string;
22
+ description: string;
23
+ model: string;
24
+ instruction: string;
25
+ userText: string;
26
+ tools?: FunctionTool[];
27
+ temperature?: number;
28
+ maxOutputTokens?: number;
29
+ }
30
+ export interface WikiAgentResult {
31
+ text: string;
32
+ error?: string;
33
+ }
34
+ /** True when the model's provider has credentials (Ollama needs none). */
35
+ export declare function modelAvailable(model: string): {
36
+ ok: boolean;
37
+ reason?: string;
38
+ };
39
+ export declare function runWikiAgent(run: WikiAgentRun): Promise<WikiAgentResult>;
@@ -0,0 +1,79 @@
1
+ /**
2
+ * lib/wiki/agentRun.ts — one-shot agent execution for wiki operations.
3
+ *
4
+ * WHY this file exists:
5
+ * Three wiki operations need a model in the loop — gap-fill (write the
6
+ * prose a structural build can't), wiki_query (answer a question over the
7
+ * bundle), wiki_garden (author or revise a document). Each is a single
8
+ * agent run: build an ADK LlmAgent, hand it the wiki FunctionTools it
9
+ * needs, stream one exchange, return the text. This helper is that idiom
10
+ * (the same one scripts/demo_model_optionality.ts uses), shared so the
11
+ * three call sites can't drift.
12
+ *
13
+ * Model routing goes through lib/models/registry.ts, so the YAML-style
14
+ * model string ('gemini-3.7-flash', 'claude-*', 'ollama/*'…) picks the
15
+ * provider — wiki operations inherit the framework's full model
16
+ * optionality. A missing provider key degrades to a clear error string;
17
+ * nothing here throws for want of credentials.
18
+ */
19
+ import { randomUUID } from 'node:crypto';
20
+ import { InMemorySessionService, LlmAgent, Runner } from '@google/adk';
21
+ import { providerForModel, providerKeyPresent, registerAvailableProviders, } from "../models/registry.js";
22
+ /** True when the model's provider has credentials (Ollama needs none). */
23
+ export function modelAvailable(model) {
24
+ const provider = providerForModel(model);
25
+ if (provider === 'ollama' || providerKeyPresent(provider))
26
+ return { ok: true };
27
+ return { ok: false, reason: `provider "${provider}" has no API key configured` };
28
+ }
29
+ export async function runWikiAgent(run) {
30
+ const availability = modelAvailable(run.model);
31
+ if (!availability.ok) {
32
+ return { text: '', error: availability.reason };
33
+ }
34
+ registerAvailableProviders();
35
+ const agent = new LlmAgent({
36
+ name: run.name,
37
+ description: run.description,
38
+ model: run.model,
39
+ instruction: run.instruction,
40
+ generateContentConfig: {
41
+ temperature: run.temperature ?? 0.3,
42
+ maxOutputTokens: run.maxOutputTokens ?? 4096,
43
+ },
44
+ ...(run.tools && run.tools.length > 0 ? { tools: run.tools } : {}),
45
+ });
46
+ const appName = 'melchizedek-wiki';
47
+ const userId = 'wiki';
48
+ const sessionId = randomUUID();
49
+ const sessionService = new InMemorySessionService();
50
+ const runner = new Runner({ agent, appName, sessionService });
51
+ await sessionService.createSession({ appName, userId, sessionId, state: {} });
52
+ let outputText = '';
53
+ let errorText = '';
54
+ try {
55
+ const stream = runner.runAsync({
56
+ userId,
57
+ sessionId,
58
+ newMessage: { role: 'user', parts: [{ text: run.userText }] },
59
+ });
60
+ for await (const event of stream) {
61
+ const evAny = event;
62
+ if ((evAny.errorCode || evAny.errorMessage) && evAny.errorCode !== 'STOP') {
63
+ errorText = `[${evAny.errorCode ?? 'ERROR'}] ${evAny.errorMessage ?? ''}`;
64
+ }
65
+ for (const part of event.content?.parts ?? []) {
66
+ const p = part;
67
+ if (!p.thought && p.text)
68
+ outputText += p.text;
69
+ }
70
+ }
71
+ }
72
+ catch (err) {
73
+ errorText = err instanceof Error ? err.message : String(err);
74
+ }
75
+ return {
76
+ text: outputText.trim(),
77
+ ...(errorText ? { error: errorText } : {}),
78
+ };
79
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * lib/wiki/builder.ts — docs assembled from typed parts, refreshed in place.
3
+ *
4
+ * WHY this file exists:
5
+ * "Build the docs structurally" means a document's skeleton — frontmatter,
6
+ * generated sections, prose slots — is code output, not typing. A DocSpec
7
+ * declares the parts; this engine renders a new file or REFRESHES an
8
+ * existing one, and the refresh contract is the whole point:
9
+ *
10
+ * - machine-owned regions (wiki:generated markers) are rewritten from
11
+ * the current spec — repo truth can never drift from the doc;
12
+ * - prose (everything outside markers, and filled wiki:fill slots) is
13
+ * preserved verbatim — a rebuild never destroys writing, human or LLM;
14
+ * - `generated {by, at}` is bumped only when content actually changed,
15
+ * so rebuilds are idempotent and diffs stay honest.
16
+ *
17
+ * The engine is bundle-agnostic: what to generate FROM (agent YAMLs, tool
18
+ * contracts, SQL) is the caller's business (scripts/wiki/build.ts here);
19
+ * this module only guarantees the mechanics.
20
+ */
21
+ import { type LogOp } from './format.ts';
22
+ export type BodyPart = {
23
+ kind: 'prose';
24
+ markdown: string;
25
+ } | {
26
+ kind: 'generated';
27
+ id: string;
28
+ source: string;
29
+ markdown: string;
30
+ } | {
31
+ kind: 'fill';
32
+ id: string;
33
+ hint: string;
34
+ };
35
+ export interface DocSpec {
36
+ bundlePath: string;
37
+ /** Spec-owned frontmatter. Omit entirely for reserved files (indexes). */
38
+ fm?: Record<string, unknown>;
39
+ body: BodyPart[];
40
+ }
41
+ export interface BuildContext {
42
+ /** Actor recorded in `generated.by` for structural output. */
43
+ actor: string;
44
+ /** ISO date stamped into `generated.at` when content changes. */
45
+ date: string;
46
+ }
47
+ export type UpsertAction = 'created' | 'updated' | 'unchanged';
48
+ export declare function renderDoc(spec: DocSpec, ctx: BuildContext): string;
49
+ export declare function refreshDoc(existingRaw: string, spec: DocSpec, ctx: BuildContext): {
50
+ raw: string;
51
+ changed: boolean;
52
+ };
53
+ export declare function upsertDoc(root: string, spec: DocSpec, ctx: BuildContext): UpsertAction;
54
+ export interface IndexEntry {
55
+ bundlePath: string;
56
+ title: string;
57
+ description: string;
58
+ }
59
+ /**
60
+ * A directory's index.md: optional hand prose survives around one generated
61
+ * listing. Root index carries okf_version frontmatter; others none (reserved
62
+ * files are exempt from the `type` requirement).
63
+ */
64
+ export declare function indexSpec(dirBundlePath: string, heading: string, entries: IndexEntry[], subdirs?: IndexEntry[], rootFm?: Record<string, unknown>): DocSpec;
65
+ /**
66
+ * Append one entry to a bundle log. `logPath` defaults to the root log; a
67
+ * caller whose entry would NAME private knowledge must pass the private
68
+ * annex's own log instead ('/private/log.md'), because the root log is a
69
+ * published file and a summary is text like any other (wiki/decisions/0003).
70
+ */
71
+ export declare function appendLog(root: string, date: string, op: LogOp, summary: string, detail?: string, logPath?: string): void;
@@ -0,0 +1,165 @@
1
+ /**
2
+ * lib/wiki/builder.ts — docs assembled from typed parts, refreshed in place.
3
+ *
4
+ * WHY this file exists:
5
+ * "Build the docs structurally" means a document's skeleton — frontmatter,
6
+ * generated sections, prose slots — is code output, not typing. A DocSpec
7
+ * declares the parts; this engine renders a new file or REFRESHES an
8
+ * existing one, and the refresh contract is the whole point:
9
+ *
10
+ * - machine-owned regions (wiki:generated markers) are rewritten from
11
+ * the current spec — repo truth can never drift from the doc;
12
+ * - prose (everything outside markers, and filled wiki:fill slots) is
13
+ * preserved verbatim — a rebuild never destroys writing, human or LLM;
14
+ * - `generated {by, at}` is bumped only when content actually changed,
15
+ * so rebuilds are idempotent and diffs stay honest.
16
+ *
17
+ * The engine is bundle-agnostic: what to generate FROM (agent YAMLs, tool
18
+ * contracts, SQL) is the caller's business (scripts/wiki/build.ts here);
19
+ * this module only guarantees the mechanics.
20
+ */
21
+ import { existsSync, readFileSync } from 'node:fs';
22
+ import { posix } from 'node:path';
23
+ import { formatLogEntry } from "./format.js";
24
+ import { emitDoc, emitFrontmatter, fillSlot, generatedBlock, insertLogEntry, mdLink, parseDoc, setGeneratedContent, setSlotContent, slotIsUnfilled, } from "./markdown.js";
25
+ import { jailedAbsPath, writeDocFile } from "./vault.js";
26
+ // ── Rendering (fresh documents) ──────────────────────────────────────────────
27
+ function renderPart(part) {
28
+ switch (part.kind) {
29
+ case 'prose':
30
+ return part.markdown.trim();
31
+ case 'generated':
32
+ return generatedBlock(part.id, part.source, part.markdown);
33
+ case 'fill':
34
+ return fillSlot(part.id, undefined, part.hint);
35
+ }
36
+ }
37
+ export function renderDoc(spec, ctx) {
38
+ const body = spec.body.map(renderPart).join('\n\n');
39
+ if (!spec.fm)
40
+ return `${body.trim()}\n`;
41
+ const fm = {
42
+ ...spec.fm,
43
+ generated: { by: ctx.actor, at: ctx.date },
44
+ };
45
+ return emitDoc(fm, body);
46
+ }
47
+ // ── Refreshing (existing documents) ──────────────────────────────────────────
48
+ function replaceFrontmatterBlock(raw, fm) {
49
+ const parsed = parseDoc(raw);
50
+ const lines = raw.split('\n');
51
+ const body = lines.slice(parsed.bodyStartLine - 1).join('\n');
52
+ return `${emitFrontmatter(fm)}\n${body.replace(/^\n+/, '')}`;
53
+ }
54
+ /** Raw text with the volatile `generated:` stamp neutralised, for comparison. */
55
+ function comparable(raw) {
56
+ const parsed = parseDoc(raw);
57
+ if (parsed.frontmatter === null)
58
+ return raw;
59
+ const { generated: _drop, ...rest } = parsed.frontmatter;
60
+ return replaceFrontmatterBlock(raw, rest);
61
+ }
62
+ export function refreshDoc(existingRaw, spec, ctx) {
63
+ const existing = parseDoc(existingRaw);
64
+ // Harvest prose already written into fill slots, keyed by slot id.
65
+ const harvested = new Map();
66
+ for (const slot of existing.slots) {
67
+ if (!slotIsUnfilled(slot))
68
+ harvested.set(slot.id, slot.content);
69
+ }
70
+ let raw = existingRaw;
71
+ // Frontmatter: spec keys win; human-added keys (status, verified, tags…)
72
+ // survive because the merge starts from what is on disk.
73
+ if (spec.fm) {
74
+ const merged = { ...(existing.frontmatter ?? {}), ...spec.fm };
75
+ raw = replaceFrontmatterBlock(raw, merged);
76
+ }
77
+ // Machine-owned regions: rewrite interiors; append blocks the doc lacks.
78
+ for (const part of spec.body) {
79
+ if (part.kind === 'generated') {
80
+ const next = setGeneratedContent(raw, part.id, part.markdown);
81
+ raw = next ?? `${raw.trimEnd()}\n\n${generatedBlock(part.id, part.source, part.markdown)}\n`;
82
+ }
83
+ if (part.kind === 'fill') {
84
+ const present = parseDoc(raw).slots.some((s) => s.id === part.id);
85
+ if (!present) {
86
+ raw = `${raw.trimEnd()}\n\n${fillSlot(part.id, undefined, part.hint)}\n`;
87
+ }
88
+ }
89
+ }
90
+ // Re-inject harvested prose into any slot the regeneration re-emitted.
91
+ for (const [id, content] of harvested) {
92
+ const current = parseDoc(raw).slots.find((s) => s.id === id);
93
+ if (current && slotIsUnfilled(current)) {
94
+ raw = setSlotContent(raw, id, content) ?? raw;
95
+ }
96
+ }
97
+ const changed = comparable(raw) !== comparable(existingRaw);
98
+ if (changed && spec.fm) {
99
+ const merged = {
100
+ ...(parseDoc(raw).frontmatter ?? {}),
101
+ generated: { by: ctx.actor, at: ctx.date },
102
+ };
103
+ raw = replaceFrontmatterBlock(raw, merged);
104
+ }
105
+ return { raw, changed };
106
+ }
107
+ // ── Upsert ───────────────────────────────────────────────────────────────────
108
+ export function upsertDoc(root, spec, ctx) {
109
+ const abs = jailedAbsPath(root, spec.bundlePath);
110
+ if (!existsSync(abs)) {
111
+ writeDocFile(root, spec.bundlePath, renderDoc(spec, ctx));
112
+ return 'created';
113
+ }
114
+ const existingRaw = readFileSync(abs, 'utf-8');
115
+ const { raw, changed } = refreshDoc(existingRaw, spec, ctx);
116
+ if (!changed)
117
+ return 'unchanged';
118
+ writeDocFile(root, spec.bundlePath, raw);
119
+ return 'updated';
120
+ }
121
+ /**
122
+ * A directory's index.md: optional hand prose survives around one generated
123
+ * listing. Root index carries okf_version frontmatter; others none (reserved
124
+ * files are exempt from the `type` requirement).
125
+ */
126
+ export function indexSpec(dirBundlePath, heading, entries, subdirs = [], rootFm) {
127
+ const dir = dirBundlePath.endsWith('/') ? dirBundlePath : `${dirBundlePath}/`;
128
+ const listing = [];
129
+ for (const sub of subdirs) {
130
+ listing.push(`- ${mdLink(sub.title, sub.bundlePath)} — ${sub.description}`);
131
+ }
132
+ for (const entry of entries) {
133
+ listing.push(`- ${mdLink(entry.title, entry.bundlePath)} — ${entry.description}`);
134
+ }
135
+ return {
136
+ bundlePath: posix.join(dir, 'index.md'),
137
+ ...(rootFm ? { fm: rootFm } : {}),
138
+ body: [
139
+ { kind: 'prose', markdown: `# ${heading}` },
140
+ {
141
+ kind: 'generated',
142
+ id: 'listing',
143
+ source: 'directory contents',
144
+ markdown: listing.join('\n'),
145
+ },
146
+ ],
147
+ };
148
+ }
149
+ // ── Log (reserved log.md, §9) ────────────────────────────────────────────────
150
+ const LOG_PREAMBLE = `# Log
151
+
152
+ Chronological record of changes to this bundle, newest first. Entries are
153
+ machine-parseable: \`## [YYYY-MM-DD] <op> | <summary>\`.`;
154
+ /**
155
+ * Append one entry to a bundle log. `logPath` defaults to the root log; a
156
+ * caller whose entry would NAME private knowledge must pass the private
157
+ * annex's own log instead ('/private/log.md'), because the root log is a
158
+ * published file and a summary is text like any other (wiki/decisions/0003).
159
+ */
160
+ export function appendLog(root, date, op, summary, detail, logPath = '/log.md') {
161
+ const abs = jailedAbsPath(root, logPath);
162
+ const entry = formatLogEntry(date, op, summary, detail);
163
+ const raw = existsSync(abs) ? readFileSync(abs, 'utf-8') : `${LOG_PREAMBLE}\n`;
164
+ writeDocFile(root, logPath, insertLogEntry(raw, entry));
165
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * lib/wiki/dive.ts — search scoring and "repo dive" reading paths.
3
+ *
4
+ * WHY this file exists:
5
+ * The two navigation questions an agent asks a wiki are "which documents
6
+ * mention X?" (search) and "I have a TASK — what should I read, in what
7
+ * order, within a budget?" (dive). Both are answered here with plain
8
+ * lexical scoring plus graph propagation — deliberately NO model call and
9
+ * NO embeddings: navigation must be instant, deterministic, and free, so
10
+ * an agent can call it dozens of times while working. Semantic answering
11
+ * belongs to wiki_query (an agent using these primitives), not here.
12
+ *
13
+ * Dive = scored seeds + link expansion. Seeds are the best lexical
14
+ * matches; the knowledge graph then pulls in what the seeds are wired to
15
+ * (score decaying with link distance), because in a well-gardened bundle
16
+ * PROXIMITY IN THE GRAPH IS RELEVANCE. The result is an ordered reading
17
+ * plan — orientation first (indexes), then matched concepts, then linked
18
+ * context — trimmed to a word budget.
19
+ */
20
+ import { type WikiGraph } from './graph.ts';
21
+ import { type Vault, type WikiDoc } from './vault.ts';
22
+ export declare function tokenize(text: string): string[];
23
+ export interface ScoredDoc {
24
+ doc: WikiDoc;
25
+ score: number;
26
+ /** Which query tokens hit, and where — the explanation for the ranking. */
27
+ matched: string[];
28
+ snippet: string;
29
+ }
30
+ export declare function scoreDocs(vault: Vault, query: string): ScoredDoc[];
31
+ export interface DiveStop {
32
+ order: number;
33
+ path: string;
34
+ title: string;
35
+ wordCount: number;
36
+ /** Why this document is on the path. */
37
+ reason: string;
38
+ /** 'orient' (indexes) | 'core' (matched) | 'context' (graph-linked). */
39
+ role: 'orient' | 'core' | 'context';
40
+ }
41
+ export interface DivePlan {
42
+ task: string;
43
+ stops: DiveStop[];
44
+ totalWords: number;
45
+ budgetWords: number;
46
+ /** Documents that scored but did not fit the budget. */
47
+ overflow: Array<{
48
+ path: string;
49
+ title: string;
50
+ }>;
51
+ }
52
+ /**
53
+ * Assemble an ordered reading plan for a task. Deterministic and
54
+ * model-free; the caller (an agent on a knowledge task) does the reading.
55
+ */
56
+ export declare function repoDive(vault: Vault, task: string, budgetWords?: number, graph?: WikiGraph): DivePlan;