@stackbilt/aegis-core 0.1.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 (148) hide show
  1. package/package.json +96 -0
  2. package/schema.sql +586 -0
  3. package/src/adapters/voice/cloudflare-agent.ts +34 -0
  4. package/src/auth.ts +124 -0
  5. package/src/bluesky.ts +464 -0
  6. package/src/claude-tools/content.ts +188 -0
  7. package/src/claude-tools/email.ts +69 -0
  8. package/src/claude-tools/github.ts +440 -0
  9. package/src/claude-tools/goals.ts +116 -0
  10. package/src/claude-tools/index.ts +353 -0
  11. package/src/claude-tools/web.ts +59 -0
  12. package/src/claude.ts +406 -0
  13. package/src/codebeast.ts +200 -0
  14. package/src/composite.ts +715 -0
  15. package/src/content/column.ts +80 -0
  16. package/src/content/hero-image.ts +47 -0
  17. package/src/content/index.ts +27 -0
  18. package/src/content/journal.ts +91 -0
  19. package/src/content/roundtable.ts +163 -0
  20. package/src/core.ts +309 -0
  21. package/src/dashboard.ts +620 -0
  22. package/src/decision-docs.ts +284 -0
  23. package/src/dispatch.ts +13 -0
  24. package/src/edge-env.ts +58 -0
  25. package/src/email.ts +850 -0
  26. package/src/exports.ts +156 -0
  27. package/src/github-projects.ts +312 -0
  28. package/src/github.ts +670 -0
  29. package/src/groq.ts +247 -0
  30. package/src/health-page.ts +578 -0
  31. package/src/index.ts +89 -0
  32. package/src/kernel/argus-actions.ts +397 -0
  33. package/src/kernel/argus-correlation.ts +639 -0
  34. package/src/kernel/board.ts +91 -0
  35. package/src/kernel/briefing.ts +177 -0
  36. package/src/kernel/classify-memory-topic.ts +166 -0
  37. package/src/kernel/cognition.ts +377 -0
  38. package/src/kernel/court-cards.ts +163 -0
  39. package/src/kernel/dispatch.ts +587 -0
  40. package/src/kernel/domain.ts +50 -0
  41. package/src/kernel/dynamic-tools.ts +322 -0
  42. package/src/kernel/executor-port.ts +45 -0
  43. package/src/kernel/executors/claude.ts +73 -0
  44. package/src/kernel/executors/direct.ts +237 -0
  45. package/src/kernel/executors/groq.ts +18 -0
  46. package/src/kernel/executors/index.ts +87 -0
  47. package/src/kernel/executors/tarotscript.ts +104 -0
  48. package/src/kernel/executors/workers-ai.ts +54 -0
  49. package/src/kernel/insight-cache.ts +76 -0
  50. package/src/kernel/memory/agenda.ts +200 -0
  51. package/src/kernel/memory/blocks.ts +188 -0
  52. package/src/kernel/memory/consolidation.ts +194 -0
  53. package/src/kernel/memory/episodic.ts +241 -0
  54. package/src/kernel/memory/goals.ts +156 -0
  55. package/src/kernel/memory/graph.ts +290 -0
  56. package/src/kernel/memory/index.ts +11 -0
  57. package/src/kernel/memory/insights.ts +316 -0
  58. package/src/kernel/memory/procedural.ts +467 -0
  59. package/src/kernel/memory/pruning.ts +67 -0
  60. package/src/kernel/memory/recall.ts +367 -0
  61. package/src/kernel/memory/semantic.ts +315 -0
  62. package/src/kernel/memory/synthesis.ts +161 -0
  63. package/src/kernel/memory-adapter.ts +369 -0
  64. package/src/kernel/memory-guardrails.ts +76 -0
  65. package/src/kernel/port.ts +23 -0
  66. package/src/kernel/resilience.ts +322 -0
  67. package/src/kernel/router.ts +471 -0
  68. package/src/kernel/scheduled/agent-dispatch.ts +252 -0
  69. package/src/kernel/scheduled/argus-analytics.ts +247 -0
  70. package/src/kernel/scheduled/argus-heartbeat.ts +320 -0
  71. package/src/kernel/scheduled/argus-notify.ts +348 -0
  72. package/src/kernel/scheduled/board-sync.ts +110 -0
  73. package/src/kernel/scheduled/ci-watcher.ts +125 -0
  74. package/src/kernel/scheduled/cognitive-metrics.ts +377 -0
  75. package/src/kernel/scheduled/consolidation.ts +229 -0
  76. package/src/kernel/scheduled/content-drip.ts +47 -0
  77. package/src/kernel/scheduled/content.ts +6 -0
  78. package/src/kernel/scheduled/conversation-facts.ts +204 -0
  79. package/src/kernel/scheduled/cost-report.ts +84 -0
  80. package/src/kernel/scheduled/curiosity.ts +219 -0
  81. package/src/kernel/scheduled/dev-activity.ts +44 -0
  82. package/src/kernel/scheduled/digest.ts +317 -0
  83. package/src/kernel/scheduled/dreaming/agenda-triage.ts +115 -0
  84. package/src/kernel/scheduled/dreaming/facts.ts +239 -0
  85. package/src/kernel/scheduled/dreaming/index.ts +8 -0
  86. package/src/kernel/scheduled/dreaming/llm.ts +33 -0
  87. package/src/kernel/scheduled/dreaming/pattern-synthesis.ts +124 -0
  88. package/src/kernel/scheduled/dreaming/persona.ts +75 -0
  89. package/src/kernel/scheduled/dreaming/symbolic.ts +31 -0
  90. package/src/kernel/scheduled/dreaming/task-proposals.ts +80 -0
  91. package/src/kernel/scheduled/dreaming.ts +66 -0
  92. package/src/kernel/scheduled/entropy.ts +149 -0
  93. package/src/kernel/scheduled/escalation.ts +192 -0
  94. package/src/kernel/scheduled/feed-watcher.ts +206 -0
  95. package/src/kernel/scheduled/goals.ts +214 -0
  96. package/src/kernel/scheduled/governance.ts +41 -0
  97. package/src/kernel/scheduled/heartbeat.ts +220 -0
  98. package/src/kernel/scheduled/inbox-processor.ts +174 -0
  99. package/src/kernel/scheduled/index.ts +245 -0
  100. package/src/kernel/scheduled/issue-proposer.ts +478 -0
  101. package/src/kernel/scheduled/issue-watcher.ts +128 -0
  102. package/src/kernel/scheduled/pr-automerge.ts +213 -0
  103. package/src/kernel/scheduled/product-health.ts +107 -0
  104. package/src/kernel/scheduled/reflection.ts +373 -0
  105. package/src/kernel/scheduled/self-improvement.ts +114 -0
  106. package/src/kernel/scheduled/social-engage.ts +175 -0
  107. package/src/kernel/scheduled/task-audit.ts +60 -0
  108. package/src/kernel/symbolic.ts +156 -0
  109. package/src/kernel/types.ts +145 -0
  110. package/src/landing.ts +1190 -0
  111. package/src/lib/audit-chain/chain.ts +28 -0
  112. package/src/lib/audit-chain/types.ts +12 -0
  113. package/src/lib/observability/errors.ts +55 -0
  114. package/src/markdown.ts +164 -0
  115. package/src/mcp/handlers.ts +647 -0
  116. package/src/mcp/server.ts +184 -0
  117. package/src/mcp/tools.ts +316 -0
  118. package/src/mcp-client.ts +275 -0
  119. package/src/mcp-server.ts +2 -0
  120. package/src/operator/config.example.ts +60 -0
  121. package/src/operator/config.ts +60 -0
  122. package/src/operator/index.ts +46 -0
  123. package/src/operator/persona.example.ts +34 -0
  124. package/src/operator/persona.ts +34 -0
  125. package/src/operator/prompt-builder.ts +190 -0
  126. package/src/operator/types.ts +43 -0
  127. package/src/pulse.ts +1179 -0
  128. package/src/routes/bluesky.ts +116 -0
  129. package/src/routes/cc-tasks.ts +328 -0
  130. package/src/routes/codebeast.ts +1 -0
  131. package/src/routes/content.ts +194 -0
  132. package/src/routes/conversations.ts +25 -0
  133. package/src/routes/dynamic-tools.ts +111 -0
  134. package/src/routes/feedback.ts +192 -0
  135. package/src/routes/health.ts +147 -0
  136. package/src/routes/messages.ts +228 -0
  137. package/src/routes/observability.ts +82 -0
  138. package/src/routes/operator-logs.ts +42 -0
  139. package/src/routes/pages.ts +96 -0
  140. package/src/routes/sessions.ts +54 -0
  141. package/src/sanitize.ts +73 -0
  142. package/src/schema-enums.ts +155 -0
  143. package/src/search.ts +112 -0
  144. package/src/task-intelligence.ts +497 -0
  145. package/src/types.ts +194 -0
  146. package/src/ui.ts +5 -0
  147. package/src/version.ts +3 -0
  148. package/src/workers-ai-chat.ts +333 -0
@@ -0,0 +1,353 @@
1
+ // Claude in-process tool definitions + context builder + delegating dispatcher
2
+ // Core module — re-exports domain handlers, owns memory/agenda/session tools
3
+ // Extracted from claude-tools.ts for LOC governance
4
+
5
+ import { McpClient, McpRegistry } from '../mcp-client.js';
6
+ import { getConversationHistory, getAgendaContext, getRecentHeartbeatContext, addAgendaItem, resolveAgendaItem, budgetConversationHistory } from '../kernel/memory/index.js';
7
+ import { getAllMemoryForContext, recallMemory, recordMemory } from '../kernel/memory-adapter.js';
8
+ import { getCognitiveState, formatCognitiveContext } from '../kernel/cognition.js';
9
+ import { getAttachedBlocks, assembleBlockContext, seedBlocks } from '../kernel/memory/blocks.js';
10
+ import { operatorConfig } from '../operator/index.js';
11
+ import { buildSystemPrompt } from '../operator/prompt-builder.js';
12
+
13
+ import { GITHUB_TOOLS, handleGithubTool } from './github.js';
14
+ import { GOAL_TOOLS, handleGoalTool } from './goals.js';
15
+ import { ROUNDTABLE_TOOLS, DISPATCH_TOOLS, handleContentTool } from './content.js';
16
+ import { WEB_TOOLS, handleWebTool } from './web.js';
17
+ import { SEND_EMAIL_TOOL, handleEmailTool } from './email.js';
18
+ import { getCachedActiveTools, toChatToolDef, createDynamicTool, getDynamicTool, executeDynamicTool, invalidateToolCache, type CreateToolOpts } from '../kernel/dynamic-tools.js';
19
+
20
+ // ─── Types ─────────────────────────────────────────────────────
21
+
22
+ export interface ClaudeConfig {
23
+ apiKey: string;
24
+ model: string;
25
+ baseUrl?: string; // AI Gateway URL or default https://api.anthropic.com
26
+ mcpClient: McpClient;
27
+ mcpRegistry?: McpRegistry;
28
+ db: D1Database;
29
+ channel: string;
30
+ conversationId?: string;
31
+ githubToken?: string;
32
+ githubRepo?: string;
33
+ braveApiKey?: string;
34
+ roundtableDb?: D1Database;
35
+ memoryBinding?: import('../types.js').MemoryServiceBinding;
36
+ resendApiKeys?: { resendApiKey: string; resendApiKeyPersonal: string };
37
+ userQuery?: string;
38
+ edgeEnv?: import('../kernel/dispatch.js').EdgeEnv;
39
+ }
40
+
41
+ // ─── Multi-MCP resolution ────────────────────────────────────
42
+
43
+ export function resolveMcpTool(
44
+ name: string,
45
+ mcpClient: McpClient,
46
+ mcpRegistry?: McpRegistry,
47
+ ): { client: McpClient; mcpName: string } | null {
48
+ if (mcpRegistry) return mcpRegistry.resolveClient(name);
49
+ const mcpName = mcpClient.toMcpToolName(name);
50
+ return mcpName ? { client: mcpClient, mcpName } : null;
51
+ }
52
+
53
+ // ─── Shared types ─────────────────────────────────────────────
54
+
55
+ export interface ContentBlock {
56
+ type: string;
57
+ text?: string;
58
+ id?: string;
59
+ name?: string;
60
+ input?: Record<string, unknown>;
61
+ }
62
+
63
+ export interface Message {
64
+ role: 'user' | 'assistant';
65
+ content: string | ContentBlock[];
66
+ }
67
+
68
+ export interface ApiResponse {
69
+ id: string;
70
+ content: ContentBlock[];
71
+ stop_reason: string;
72
+ usage: { input_tokens: number; output_tokens: number };
73
+ }
74
+
75
+ const SYSTEM_PROMPT = buildSystemPrompt();
76
+
77
+ export function getModelCostRates(model: string): { input: number; output: number } {
78
+ if (model.includes('opus')) return { input: 15, output: 75 };
79
+ return { input: 3, output: 15 }; // Sonnet default
80
+ }
81
+
82
+ // ─── Core in-process tool definitions (memory, agenda, session) ──
83
+
84
+ const MEMORY_TOOL = {
85
+ name: 'record_memory_entry',
86
+ description: `Record a durable fact to long-term semantic memory. Call this whenever you learn something about ${operatorConfig.identity.name}, his businesses, projects, or preferences that should persist across sessions. Be specific — record facts, not summaries.`,
87
+ input_schema: {
88
+ type: 'object' as const,
89
+ properties: {
90
+ topic: { type: 'string', description: `Category (e.g., ${operatorConfig.entities.memoryTopics.map(t => `"${t}"`).join(', ')})` },
91
+ fact: { type: 'string', description: 'The specific durable fact to remember' },
92
+ confidence: { type: 'number', description: 'Confidence 0-1' },
93
+ source: { type: 'string', description: 'Where this came from' },
94
+ },
95
+ required: ['topic', 'fact', 'confidence', 'source'],
96
+ },
97
+ };
98
+
99
+ const ADD_AGENDA_TOOL = {
100
+ name: 'add_agenda_item',
101
+ description: `Add a pending action or follow-up to the persistent agenda. Use when a conversation surfaces something that needs to happen — a decision ${operatorConfig.identity.name} is considering, something you offered to do, or an open question needing follow-up. The agenda surfaces in every future conversation.`,
102
+ input_schema: {
103
+ type: 'object' as const,
104
+ properties: {
105
+ item: { type: 'string', description: 'The action item — concise and actionable' },
106
+ context: { type: 'string', description: 'Brief context: why this was added' },
107
+ priority: { type: 'string', enum: ['low', 'medium', 'high'], description: 'Priority level' },
108
+ },
109
+ required: ['item', 'priority'],
110
+ },
111
+ };
112
+
113
+ const RESOLVE_AGENDA_TOOL = {
114
+ name: 'resolve_agenda_item',
115
+ description: 'Mark an agenda item as done or dismissed. Call when an action has been completed or is no longer relevant.',
116
+ input_schema: {
117
+ type: 'object' as const,
118
+ properties: {
119
+ id: { type: 'number', description: 'The agenda item ID (shown in your context as #N)' },
120
+ status: { type: 'string', enum: ['done', 'dismissed'], description: 'How it resolved' },
121
+ },
122
+ required: ['id', 'status'],
123
+ },
124
+ };
125
+
126
+ const CC_SESSION_TOOL = {
127
+ name: 'lookup_cc_session',
128
+ description: 'Look up Claude Code session digests. Query by session ID or list recent sessions. These contain structured summaries of what was shipped, decided, and discussed in Claude Code sessions.',
129
+ input_schema: {
130
+ type: 'object' as const,
131
+ properties: {
132
+ id: { type: 'string', description: 'Specific session UUID to look up. Omit to list recent sessions.' },
133
+ days: { type: 'number', description: 'Number of days to look back when listing (default 7)' },
134
+ },
135
+ },
136
+ };
137
+
138
+ const CREATE_DYNAMIC_TOOL = {
139
+ name: 'create_dynamic_tool',
140
+ description: 'Create a reusable dynamic tool — a prompt template stored in D1 and executed via LLM. The tool becomes available in future conversations with a dt_ prefix.',
141
+ input_schema: {
142
+ type: 'object' as const,
143
+ properties: {
144
+ name: { type: 'string', description: 'Tool name (snake_case, 2-49 chars, no aegis_/mcp_/bizops_ prefix)' },
145
+ description: { type: 'string', description: 'What the tool does' },
146
+ input_schema: { type: 'string', description: 'JSON Schema for inputs (default: empty object)' },
147
+ prompt_template: { type: 'string', description: 'Prompt template with {{variable}} placeholders' },
148
+ executor: { type: 'string', enum: ['gpt_oss', 'workers_ai', 'groq'], description: 'LLM executor (default: gpt_oss)' },
149
+ ttl_days: { type: 'number', description: 'Auto-expire after N days (optional)' },
150
+ },
151
+ required: ['name', 'description', 'prompt_template'],
152
+ },
153
+ };
154
+
155
+ // ─── Context builder ─────────────────────────────────────────
156
+
157
+ export async function buildContext(config: ClaudeConfig, roundtableDb?: D1Database): Promise<{ systemPrompt: string; tools: unknown[]; conversationHistory: Array<{ role: 'user' | 'assistant'; content: string }> }> {
158
+ const { integrations } = operatorConfig;
159
+
160
+ // MCP tools — registry (multi-server) or single BizOps client
161
+ let mcpTools: unknown[] = [];
162
+ if (config.mcpRegistry) {
163
+ mcpTools = await config.mcpRegistry.listAllTools();
164
+ } else if (integrations.bizops.enabled) {
165
+ await config.mcpClient.listTools();
166
+ mcpTools = config.mcpClient.toAnthropicTools();
167
+ }
168
+
169
+ // Seed blocks on first run (no-op if already seeded)
170
+ await seedBlocks(config.db).catch(err => {
171
+ console.error('[buildContext] seedBlocks failed:', err instanceof Error ? err.message : String(err));
172
+ });
173
+
174
+ // Parallel: blocks + memory + agenda + heartbeat + conversation history + cognitive state — 6 reads
175
+ const [blocks, memoryResult, agendaContext, heartbeatContext, rawHistory, cogState] = await Promise.all([
176
+ getAttachedBlocks(config.db, 'claude'),
177
+ config.memoryBinding ? getAllMemoryForContext(config.memoryBinding, config.userQuery) : Promise.resolve({ text: '', ids: [] as string[] }),
178
+ getAgendaContext(config.db),
179
+ getRecentHeartbeatContext(config.db),
180
+ config.conversationId ? getConversationHistory(config.db, config.conversationId, 10) : Promise.resolve([] as Array<{ role: 'user' | 'assistant'; content: string }>),
181
+ getCognitiveState(config.db),
182
+ ]);
183
+
184
+ // Batch recall: single UPDATE IN (...) instead of N individual queries
185
+ if (memoryResult.ids.length > 0) {
186
+ if (config.memoryBinding) await recallMemory(config.memoryBinding, memoryResult.ids).catch(() => {});
187
+ }
188
+
189
+ // Block context (priority-ordered, always-visible state)
190
+ const blockContext = blocks.length > 0 ? '\n\n' + assembleBlockContext(blocks) : '';
191
+
192
+ // Cognitive context — fallback for narratives/pulse when active_context block hasn't been populated yet.
193
+ // Once consolidation refreshes active_context, blocks are canonical and CogState is redundant.
194
+ const hasActiveContextBlock = blocks.some(b => b.id === 'active_context' && b.version > 1);
195
+ const cognitiveContext = (!hasActiveContextBlock && cogState) ? formatCognitiveContext(cogState) : '';
196
+ const systemPrompt = SYSTEM_PROMPT + blockContext + cognitiveContext + heartbeatContext + agendaContext + memoryResult.text;
197
+
198
+ // Trim history: exclude trailing user message to avoid duplication
199
+ const conversationHistory = rawHistory.length > 0 && rawHistory[rawHistory.length - 1]?.role === 'user'
200
+ ? rawHistory.slice(0, -1)
201
+ : rawHistory;
202
+
203
+ const tools: unknown[] = [
204
+ ...mcpTools,
205
+ MEMORY_TOOL, ADD_AGENDA_TOOL, RESOLVE_AGENDA_TOOL, CC_SESSION_TOOL,
206
+ ];
207
+
208
+ if (integrations.github.enabled && config.githubToken && config.githubRepo) {
209
+ tools.push(...GITHUB_TOOLS);
210
+ }
211
+ if (integrations.brave.enabled && config.braveApiKey) {
212
+ tools.push(...WEB_TOOLS);
213
+ }
214
+ if (integrations.goals.enabled) {
215
+ tools.push(...GOAL_TOOLS);
216
+ }
217
+ if (roundtableDb) {
218
+ tools.push(...ROUNDTABLE_TOOLS);
219
+ tools.push(...DISPATCH_TOOLS);
220
+ }
221
+ if (config.resendApiKeys?.resendApiKey) {
222
+ tools.push(SEND_EMAIL_TOOL);
223
+ }
224
+
225
+ // Dynamic tools: runtime-created prompt templates (dt_ prefix)
226
+ tools.push(CREATE_DYNAMIC_TOOL);
227
+ try {
228
+ const dynamicTools = await getCachedActiveTools(config.db);
229
+ for (const dt of dynamicTools) {
230
+ tools.push(toChatToolDef(dt));
231
+ }
232
+ } catch {
233
+ // Non-fatal — dynamic tools table may not exist yet
234
+ }
235
+
236
+ return { systemPrompt, tools, conversationHistory };
237
+ }
238
+
239
+ // ─── Delegating in-process tool dispatcher ───────────────────
240
+
241
+ export async function handleInProcessTool(
242
+ db: D1Database,
243
+ name: string,
244
+ input: Record<string, unknown>,
245
+ githubToken?: string,
246
+ githubRepo?: string,
247
+ braveApiKey?: string,
248
+ roundtableDb?: D1Database,
249
+ anthropicConfig?: { apiKey: string; model: string; baseUrl: string },
250
+ memoryBinding?: import('../types.js').MemoryServiceBinding,
251
+ resendApiKeys?: { resendApiKey: string; resendApiKeyPersonal: string },
252
+ edgeEnv?: import('../kernel/dispatch.js').EdgeEnv,
253
+ ): Promise<string | null> {
254
+ // Core tools: memory, agenda, session
255
+ if (name === 'record_memory_entry') {
256
+ const i = input as { topic: string; fact: string; confidence: number; source: string };
257
+ if (!memoryBinding) {
258
+ console.warn('[tools] record_memory_entry failed: Memory Worker binding unavailable');
259
+ return 'Error: Memory Worker binding unavailable — cannot record memory';
260
+ }
261
+ try {
262
+ const result = await recordMemory(memoryBinding, i.topic, i.fact, i.confidence, i.source);
263
+ // Surface upsert distinction (#437) — callers relying on the string
264
+ // response need to be able to tell that an existing entry was replaced.
265
+ return result.updated
266
+ ? `Updated: "${i.fact}" → ${i.topic} (new id: ${result.fragment_id}, superseded: ${result.superseded_id})`
267
+ : `Recorded: "${i.fact}" → ${i.topic}`;
268
+ } catch (err) {
269
+ const msg = err instanceof Error ? err.message : String(err);
270
+ console.error('[tools] record_memory_entry write failed:', msg);
271
+ return `Error: Memory write failed — ${msg}`;
272
+ }
273
+ }
274
+ if (name === 'add_agenda_item') {
275
+ const i = input as { item: string; context?: string; priority: 'low' | 'medium' | 'high' };
276
+ const id = await addAgendaItem(db, i.item, i.context, i.priority);
277
+ return `Added agenda item #${id}: "${i.item}" (${i.priority})`;
278
+ }
279
+ if (name === 'resolve_agenda_item') {
280
+ const i = input as { id: number; status: 'done' | 'dismissed' };
281
+ await resolveAgendaItem(db, i.id, i.status);
282
+ return `Resolved agenda item #${i.id} as ${i.status}`;
283
+ }
284
+ if (name === 'lookup_cc_session') {
285
+ const i = input as { id?: string; days?: number };
286
+ if (i.id) {
287
+ const session = await db.prepare('SELECT * FROM cc_sessions WHERE id = ?').bind(i.id).first();
288
+ if (!session) return `No session found with ID ${i.id}`;
289
+ return JSON.stringify(session, null, 2);
290
+ }
291
+ const days = i.days ?? 7;
292
+ const sessions = await db.prepare(
293
+ "SELECT * FROM cc_sessions WHERE created_at > datetime('now', '-' || ? || ' days') ORDER BY created_at DESC LIMIT 20"
294
+ ).bind(days).all();
295
+ if (sessions.results.length === 0) return `No sessions found in the last ${days} days`;
296
+ return JSON.stringify(sessions.results, null, 2);
297
+ }
298
+
299
+ // Dynamic tool creation
300
+ if (name === 'create_dynamic_tool') {
301
+ const opts = input as unknown as CreateToolOpts;
302
+ if (!opts.name || !opts.description || !opts.prompt_template) {
303
+ return 'Error: name, description, and prompt_template are required';
304
+ }
305
+ try {
306
+ const id = await createDynamicTool(db, { ...opts, created_by: 'chat' });
307
+ invalidateToolCache();
308
+ return `Created dynamic tool "${opts.name}" (id: ${id}). It will appear as dt_${opts.name} in future conversations.`;
309
+ } catch (err) {
310
+ return `Error: ${err instanceof Error ? err.message : String(err)}`;
311
+ }
312
+ }
313
+
314
+ // Dynamic tool invocation (dt_* prefix)
315
+ if (name.startsWith('dt_')) {
316
+ const toolName = name.slice(3);
317
+ const tool = await getDynamicTool(db, toolName);
318
+ if (!tool) return `Error: dynamic tool "${toolName}" not found`;
319
+ if (tool.status === 'draft') return 'Error: tool is in draft status — activate it first';
320
+ if (!edgeEnv) return 'Error: edgeEnv not available for dynamic tool execution';
321
+ try {
322
+ const result = await executeDynamicTool(tool, input, edgeEnv);
323
+ return result.text;
324
+ } catch (err) {
325
+ return `Error executing dynamic tool: ${err instanceof Error ? err.message : String(err)}`;
326
+ }
327
+ }
328
+
329
+ // Delegate to domain handlers
330
+ if (githubToken && githubRepo) {
331
+ const result = await handleGithubTool(db, name, input, githubToken, githubRepo);
332
+ if (result !== null) return result;
333
+ }
334
+
335
+ const goalResult = await handleGoalTool(db, name, input);
336
+ if (goalResult !== null) return goalResult;
337
+
338
+ if (braveApiKey) {
339
+ const webResult = await handleWebTool(name, input, braveApiKey);
340
+ if (webResult !== null) return webResult;
341
+ }
342
+
343
+ if (roundtableDb) {
344
+ const contentResult = await handleContentTool(name, input, roundtableDb, db, anthropicConfig, githubToken, githubRepo);
345
+ if (contentResult !== null) return contentResult;
346
+ }
347
+
348
+ if (name === 'send_email' && resendApiKeys) {
349
+ return handleEmailTool(name, input, resendApiKeys);
350
+ }
351
+
352
+ return null;
353
+ }
@@ -0,0 +1,59 @@
1
+ // Web research in-process tool definitions + handlers
2
+ // Extracted from claude-tools.ts for LOC governance
3
+
4
+ import { braveSearch, fetchUrlText } from '../search.js';
5
+
6
+ // ─── Tool definitions ────────────────────────────────────────
7
+
8
+ const WEB_SEARCH_TOOL = {
9
+ name: 'web_search',
10
+ description: 'Search the web using Brave Search. Use for: regulatory lookups, compliance deadlines, state filing requirements, news, competitive research, verifying current facts. Returns title + snippet + URL for each result.',
11
+ input_schema: {
12
+ type: 'object' as const,
13
+ properties: {
14
+ query: { type: 'string', description: 'Search query — be specific for best results' },
15
+ max_results: { type: 'number', description: 'Number of results to return (default: 5, max: 10)' },
16
+ },
17
+ required: ['query'],
18
+ },
19
+ };
20
+
21
+ const FETCH_URL_TOOL = {
22
+ name: 'fetch_url',
23
+ description: 'Fetch a URL and extract its text content. Use for: reading a specific web page, pulling reference data, getting full content from a search result. Strips HTML and returns clean text.',
24
+ input_schema: {
25
+ type: 'object' as const,
26
+ properties: {
27
+ url: { type: 'string', description: 'Full URL to fetch (must start with https://)' },
28
+ },
29
+ required: ['url'],
30
+ },
31
+ };
32
+
33
+ export const WEB_TOOLS = [WEB_SEARCH_TOOL, FETCH_URL_TOOL];
34
+
35
+ // ─── Handler ─────────────────────────────────────────────────
36
+
37
+ export async function handleWebTool(
38
+ name: string,
39
+ input: Record<string, unknown>,
40
+ braveApiKey: string,
41
+ ): Promise<string | null> {
42
+ if (name === 'web_search') {
43
+ const query = input.query as string;
44
+ const maxResults = Math.min((input.max_results as number | undefined) ?? 5, 10);
45
+ const results = await braveSearch(braveApiKey, query, maxResults);
46
+ if (results.length === 0) return `No results found for: "${query}"`;
47
+ return results.map((r, i) =>
48
+ `${i + 1}. **${r.title}**\n ${r.snippet}\n ${r.url}`
49
+ ).join('\n\n');
50
+ }
51
+
52
+ if (name === 'fetch_url') {
53
+ const url = input.url as string;
54
+ const text = await fetchUrlText(url);
55
+ return `Content from ${url}:\n\n${text}`;
56
+ }
57
+
58
+ return null;
59
+ }