@spilno/herald-mcp 1.36.0 → 1.36.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js DELETED
@@ -1,2822 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Herald MCP - AI-native interface to CEDA ecosystem
4
- *
5
- * Dual-mode:
6
- * - CLI mode (TTY): Natural commands for humans
7
- * - MCP mode (piped): JSON-RPC for AI agents
8
- */
9
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
10
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
11
- import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
12
- import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "fs";
13
- import { homedir, userInfo } from "os";
14
- import { join, dirname } from "path";
15
- import { createHash } from "crypto";
16
- import * as readline from "readline";
17
- import { runInit } from "./cli/init.js";
18
- import { runLogin } from "./cli/login.js";
19
- import { runLogout } from "./cli/logout.js";
20
- import { runConfig } from "./cli/config.js";
21
- import { runUpgrade } from "./cli/upgrade.js";
22
- import { sanitize, previewSanitization } from "./sanitization.js";
23
- // Configuration - all sensitive values from environment only
24
- // CEDA_URL is primary, HERALD_API_URL for backwards compat, default to cloud
25
- const CEDA_API_URL = process.env.CEDA_URL || process.env.HERALD_API_URL || "https://getceda.com";
26
- // CEDA_TOKEN is the primary auth (from app.getceda.com OAuth)
27
- // HERALD_API_TOKEN kept for backwards compatibility
28
- const CEDA_API_TOKEN = process.env.CEDA_TOKEN || process.env.HERALD_API_TOKEN;
29
- const CEDA_API_USER = process.env.HERALD_API_USER;
30
- const CEDA_API_PASS = process.env.HERALD_API_PASS;
31
- // CEDA-70: Zero-config context - everything auto-derived, nothing required
32
- // User is ALWAYS known (whoami). Company/project inferred from path as tags.
33
- function deriveUser() {
34
- // Priority: git user > env var > OS user
35
- // Git user is trusted (immutable identity from git config)
36
- const gitUser = getGitUser();
37
- if (gitUser)
38
- return gitUser;
39
- try {
40
- return userInfo().username;
41
- }
42
- catch {
43
- return "unknown";
44
- }
45
- }
46
- function deriveTags() {
47
- // Derive tags from cwd path - last 2 meaningful segments
48
- // /Users/john/projects/acme/backend → ["acme", "backend"]
49
- try {
50
- const cwd = process.cwd();
51
- const parts = cwd.split("/").filter(p => p && !["Users", "home", "Documents", "projects", "repos", "GitHub"].includes(p));
52
- return parts.slice(-2); // Last 2 segments as tags
53
- }
54
- catch {
55
- return [];
56
- }
57
- }
58
- function findGitRoot(startPath) {
59
- let current = startPath;
60
- while (current !== '/') {
61
- if (existsSync(join(current, '.git'))) {
62
- return current;
63
- }
64
- current = dirname(current);
65
- }
66
- return null;
67
- }
68
- function getGitRemote() {
69
- try {
70
- const gitRoot = findGitRoot(process.cwd());
71
- if (!gitRoot)
72
- return { remote: null, org: null, repo: null };
73
- const configPath = join(gitRoot, '.git', 'config');
74
- if (!existsSync(configPath))
75
- return { remote: null, org: null, repo: null };
76
- const config = readFileSync(configPath, 'utf-8');
77
- // Parse [remote "origin"] url = ...
78
- const remoteMatch = config.match(/\[remote "origin"\][^\[]*url\s*=\s*(.+)/m);
79
- if (!remoteMatch)
80
- return { remote: null, org: null, repo: null };
81
- const remoteUrl = remoteMatch[1].trim();
82
- // Normalize: git@github.com:org/repo.git → github.com/org/repo
83
- // https://github.com/org/repo.git → github.com/org/repo
84
- let normalized = remoteUrl
85
- .replace(/^git@/, '')
86
- .replace(/^https?:\/\//, '')
87
- .replace(/:/, '/')
88
- .replace(/\.git$/, '');
89
- // Extract org and repo
90
- const parts = normalized.split('/');
91
- const repo = parts.pop() || null;
92
- const org = parts.pop() || null;
93
- return { remote: normalized, org, repo };
94
- }
95
- catch {
96
- return { remote: null, org: null, repo: null };
97
- }
98
- }
99
- // Git-based user identity (trusted - derived from git config)
100
- function getGitUser() {
101
- try {
102
- const gitRoot = findGitRoot(process.cwd());
103
- if (!gitRoot)
104
- return null;
105
- const configPath = join(gitRoot, '.git', 'config');
106
- if (!existsSync(configPath))
107
- return null;
108
- const config = readFileSync(configPath, 'utf-8');
109
- // Check local git config first: [user] name = ...
110
- const nameMatch = config.match(/\[user\][^\[]*name\s*=\s*(.+)/m);
111
- if (nameMatch)
112
- return nameMatch[1].trim();
113
- // Fall back to global git config
114
- const globalConfigPath = join(homedir(), '.gitconfig');
115
- if (existsSync(globalConfigPath)) {
116
- const globalConfig = readFileSync(globalConfigPath, 'utf-8');
117
- const globalNameMatch = globalConfig.match(/\[user\][^\[]*name\s*=\s*(.+)/m);
118
- if (globalNameMatch)
119
- return globalNameMatch[1].trim();
120
- }
121
- return null;
122
- }
123
- catch {
124
- return null;
125
- }
126
- }
127
- function hashTag(input) {
128
- // Create short, deterministic hash for tag
129
- // "github.com/Spilno-me/ceda" → "ceda-a7f3b2"
130
- const hash = createHash('sha256').update(input).digest('hex').slice(0, 6);
131
- const name = input.split('/').pop() || 'unknown';
132
- return `${name}-${hash}`;
133
- }
134
- function deriveTagSet() {
135
- // 1. Check env vars (explicit override)
136
- if (process.env.HERALD_COMPANY) {
137
- return {
138
- tags: [process.env.HERALD_COMPANY, process.env.HERALD_PROJECT].filter(Boolean),
139
- trust: 'LOW', // Env vars can be set by anyone
140
- source: 'env',
141
- propagates: false
142
- };
143
- }
144
- // 2. Check Git remote (HIGH trust)
145
- const gitInfo = getGitRemote();
146
- if (gitInfo.remote) {
147
- return {
148
- tags: [
149
- gitInfo.org || 'unknown',
150
- gitInfo.repo || 'unknown',
151
- hashTag(gitInfo.remote) // Unique, unforgeable tag
152
- ],
153
- trust: 'HIGH',
154
- source: 'git',
155
- propagates: true,
156
- gitInfo
157
- };
158
- }
159
- // 3. Fallback to path (LOW trust)
160
- return {
161
- tags: deriveTags(),
162
- trust: 'LOW',
163
- source: 'path',
164
- propagates: false
165
- };
166
- }
167
- function getMcpJsonPath() {
168
- return join(process.cwd(), '.mcp.json');
169
- }
170
- function readMcpJson() {
171
- const mcpPath = getMcpJsonPath();
172
- if (!existsSync(mcpPath))
173
- return null;
174
- try {
175
- return JSON.parse(readFileSync(mcpPath, 'utf-8'));
176
- }
177
- catch {
178
- return null;
179
- }
180
- }
181
- function persistContext(tagSet, user) {
182
- const mcpPath = getMcpJsonPath();
183
- let mcpJson = {};
184
- if (existsSync(mcpPath)) {
185
- try {
186
- mcpJson = JSON.parse(readFileSync(mcpPath, 'utf-8'));
187
- }
188
- catch {
189
- // Corrupted file, preserve structure
190
- mcpJson = {};
191
- }
192
- }
193
- // Add herald context section (preserve existing mcpServers)
194
- // ADR-001: Include trust level and git info
195
- mcpJson.herald = {
196
- ...mcpJson.herald,
197
- context: {
198
- tags: tagSet.tags,
199
- user,
200
- trust: tagSet.trust,
201
- source: tagSet.source,
202
- propagates: tagSet.propagates,
203
- derived: true,
204
- derivedFrom: tagSet.source,
205
- storedAt: new Date().toISOString(),
206
- gitRemote: tagSet.gitInfo?.remote || undefined
207
- }
208
- };
209
- writeFileSync(mcpPath, JSON.stringify(mcpJson, null, 2));
210
- console.error(`[Herald] Context stored: tags=[${tagSet.tags.join(', ')}] trust=${tagSet.trust} source=${tagSet.source}`);
211
- }
212
- function loadOrDeriveContext() {
213
- const user = process.env.HERALD_USER || deriveUser();
214
- // 1. Check env vars (explicit override - highest priority, but LOW trust)
215
- if (process.env.HERALD_COMPANY) {
216
- return {
217
- tags: [process.env.HERALD_COMPANY, process.env.HERALD_PROJECT].filter(Boolean),
218
- user,
219
- trust: 'LOW',
220
- source: 'env',
221
- propagates: false
222
- };
223
- }
224
- // 2. Check .mcp.json for stored context (preserve user's config)
225
- const mcpJson = readMcpJson();
226
- if (mcpJson?.herald?.context?.tags?.length) {
227
- const stored = mcpJson.herald.context;
228
- return {
229
- tags: stored.tags,
230
- user: stored.user || user,
231
- trust: stored.trust || 'LOW',
232
- source: 'stored',
233
- propagates: stored.propagates || false,
234
- gitRemote: stored.gitRemote
235
- };
236
- }
237
- // 3. Derive fresh (ADR-001: use git if available)
238
- const tagSet = deriveTagSet();
239
- // Store for next time (only if no stored context exists)
240
- if (tagSet.tags.length > 0 && !mcpJson?.herald?.context) {
241
- try {
242
- persistContext(tagSet, user);
243
- }
244
- catch {
245
- // Silent fail - don't break startup if we can't write
246
- }
247
- }
248
- return {
249
- tags: tagSet.tags,
250
- user,
251
- trust: tagSet.trust,
252
- source: tagSet.source,
253
- propagates: tagSet.propagates,
254
- gitRemote: tagSet.gitInfo?.remote || undefined
255
- };
256
- }
257
- // Load context once at startup
258
- const LOADED_CONTEXT = loadOrDeriveContext();
259
- // User is always known (can be refreshed via herald_context)
260
- let HERALD_USER = LOADED_CONTEXT.user;
261
- // Tags from context (env > stored > git > path) - can be refreshed
262
- let HERALD_TAGS = LOADED_CONTEXT.tags;
263
- let HERALD_COMPANY = HERALD_TAGS[0] || "";
264
- let HERALD_PROJECT = HERALD_TAGS[1] || HERALD_TAGS[0] || "";
265
- // ADR-001: Trust level determines pattern propagation
266
- // These are mutable - verification with CEDA may upgrade/downgrade trust
267
- let TRUST_LEVEL = LOADED_CONTEXT.trust;
268
- let PROPAGATES = LOADED_CONTEXT.propagates;
269
- let CONTEXT_SOURCE = LOADED_CONTEXT.source;
270
- const GIT_REMOTE = LOADED_CONTEXT.gitRemote;
271
- // Server-verified context (set after /api/auth/verify call)
272
- let VERIFIED_CONTEXT = null;
273
- // Offspring vault context (for Avatar mode)
274
- const HERALD_VAULT = process.env.HERALD_VAULT || "";
275
- const AEGIS_OFFSPRING_PATH = process.env.AEGIS_OFFSPRING_PATH || join(homedir(), "Documents", "aegis_ceda", "_offspring");
276
- // Cloud mode: Use CEDA API for offspring communication instead of local files
277
- const OFFSPRING_CLOUD_MODE = process.env.HERALD_OFFSPRING_CLOUD === "true";
278
- const VERSION = "1.33.0";
279
- // Self-routing description - teaches Claude when to call Herald
280
- const HERALD_DESCRIPTION = `AI-native pattern learning for CEDA.
281
-
282
- AUTHORIZATION GATES (CRITICAL):
283
- Before multi-file operations (>3 files), module scaffolding, or bulk changes:
284
- 1. Present synthesis/plan to user
285
- 2. Ask: "Proceed? [Y/continue/adjust]"
286
- 3. WAIT for explicit approval before writing
287
-
288
- Autonomous (no gate): reading, searching, planning, single-file edits, commits.
289
- Gated: multi-file creation, module scaffolding, bulk operations.
290
-
291
- Why: User steering > execution speed. Long uninterrupted runs block collaboration.
292
-
293
- WHEN TO OFFER REFLECTION:
294
- When you notice FRICTION (multiple iterations, debugging spirals, corrections, confusion)
295
- or FLOW (clean first-pass execution, good instincts, smooth outcomes),
296
- offer: "That was [rough/smooth]. Want me to capture this for pattern learning?"
297
-
298
- IMPORTANT: Always ask user permission before calling herald_reflect.
299
-
300
- Herald learns signal→outcome mappings to help future sessions.`;
301
- // Auto-sync buffer on startup (future: HERALD_AUTO_SYNC=false to disable)
302
- const AUTO_SYNC_ON_STARTUP = process.env.HERALD_AUTO_SYNC !== "false";
303
- // AI API keys for Herald's voice and AI-native simulation
304
- // SECURITY: Never bundle API keys in npm packages
305
- const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY || "";
306
- const OPENAI_API_KEY = process.env.OPENAI_API_KEY || "";
307
- // Session persistence - context-isolated paths
308
- function getHeraldDir() {
309
- return join(homedir(), ".herald", HERALD_COMPANY, HERALD_PROJECT, HERALD_USER);
310
- }
311
- function getSessionFile() {
312
- return join(getHeraldDir(), "session");
313
- }
314
- function getBufferFile() {
315
- return join(getHeraldDir(), "insight_buffer.json");
316
- }
317
- // In-memory session reflections array (clears on restart)
318
- const sessionReflections = [];
319
- function addSessionReflection(reflection) {
320
- const newReflection = {
321
- ...reflection,
322
- id: `sr-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
323
- timestamp: new Date().toISOString(),
324
- };
325
- sessionReflections.push(newReflection);
326
- return newReflection;
327
- }
328
- function getSessionReflectionsSummary() {
329
- const patterns = sessionReflections.filter(r => r.feeling === "success").length;
330
- const antipatterns = sessionReflections.filter(r => r.feeling === "stuck").length;
331
- return {
332
- count: sessionReflections.length,
333
- patterns,
334
- antipatterns,
335
- reflections: sessionReflections,
336
- };
337
- }
338
- function bufferInsight(payload) {
339
- ensureHeraldDir();
340
- const bufferFile = getBufferFile();
341
- let buffer = [];
342
- if (existsSync(bufferFile)) {
343
- try {
344
- buffer = JSON.parse(readFileSync(bufferFile, "utf-8"));
345
- }
346
- catch (error) {
347
- console.error(`[Herald] Buffer parse error in bufferInsight: ${error}`);
348
- console.error(`[Herald] Starting with fresh buffer`);
349
- buffer = [];
350
- }
351
- }
352
- buffer.push({ ...payload, bufferedAt: new Date().toISOString() });
353
- try {
354
- writeFileSync(bufferFile, JSON.stringify(buffer, null, 2));
355
- }
356
- catch (error) {
357
- console.error(`[Herald] Failed to write buffer: ${error}`);
358
- console.error(`[Herald] Insight may be lost - check disk space and permissions`);
359
- }
360
- }
361
- function getBufferedInsights() {
362
- const bufferFile = getBufferFile();
363
- if (existsSync(bufferFile)) {
364
- try {
365
- return JSON.parse(readFileSync(bufferFile, "utf-8"));
366
- }
367
- catch (error) {
368
- console.error(`[Herald] Buffer corrupted: ${error}`);
369
- console.error(`[Herald] Clearing corrupted buffer - insights may be lost`);
370
- try {
371
- unlinkSync(bufferFile);
372
- }
373
- catch {
374
- // Ignore cleanup errors
375
- }
376
- return [];
377
- }
378
- }
379
- return [];
380
- }
381
- function clearBuffer() {
382
- const bufferFile = getBufferFile();
383
- if (existsSync(bufferFile)) {
384
- unlinkSync(bufferFile);
385
- }
386
- }
387
- function saveFailedInsights(failed) {
388
- if (failed.length === 0) {
389
- clearBuffer();
390
- }
391
- else {
392
- ensureHeraldDir();
393
- try {
394
- writeFileSync(getBufferFile(), JSON.stringify(failed, null, 2));
395
- }
396
- catch (error) {
397
- console.error(`[Herald] Failed to save failed insights: ${error}`);
398
- console.error(`[Herald] ${failed.length} insight(s) may be lost`);
399
- }
400
- }
401
- }
402
- function ensureHeraldDir() {
403
- const dir = getHeraldDir();
404
- if (!existsSync(dir)) {
405
- mkdirSync(dir, { recursive: true });
406
- }
407
- }
408
- function saveSession(sessionId) {
409
- ensureHeraldDir();
410
- writeFileSync(getSessionFile(), sessionId, "utf-8");
411
- }
412
- function loadSession() {
413
- const sessionFile = getSessionFile();
414
- if (existsSync(sessionFile)) {
415
- return readFileSync(sessionFile, "utf-8").trim();
416
- }
417
- return null;
418
- }
419
- function clearSession() {
420
- const sessionFile = getSessionFile();
421
- if (existsSync(sessionFile)) {
422
- unlinkSync(sessionFile);
423
- }
424
- }
425
- function getContextString() {
426
- return `${HERALD_COMPANY}:${HERALD_PROJECT}:${HERALD_USER}`;
427
- }
428
- const HERALD_SYSTEM_PROMPT = `You are Herald, the voice of CEDA (Cognitive Event-Driven Architecture).
429
- You help humans design module structures through natural conversation.
430
-
431
- You have access to CEDA's cognitive capabilities:
432
- - Predict: Generate structure predictions from requirements
433
- - Refine: Improve predictions with additional requirements
434
- - Session: Track conversation history
435
-
436
- When users describe what they want, you:
437
- 1. Call CEDA to generate/refine predictions
438
- 2. Explain the results in natural language
439
- 3. Ask clarifying questions when needed
440
-
441
- Keep responses concise and focused. You're a helpful assistant, not verbose.
442
- When showing module structures, summarize the key sections and fields.`;
443
- async function callClaude(systemPrompt, messages) {
444
- if (!ANTHROPIC_API_KEY) {
445
- return "Claude voice unavailable. Set ANTHROPIC_API_KEY environment variable to enable chat mode.";
446
- }
447
- const anthropicMessages = messages
448
- .filter(m => m.role !== "system")
449
- .map(m => ({ role: m.role, content: m.content }));
450
- const systemContent = messages
451
- .filter(m => m.role === "system")
452
- .map(m => m.content)
453
- .join("\n\n");
454
- const response = await fetch("https://api.anthropic.com/v1/messages", {
455
- method: "POST",
456
- headers: {
457
- "Content-Type": "application/json",
458
- "x-api-key": ANTHROPIC_API_KEY,
459
- "anthropic-version": "2023-06-01",
460
- },
461
- body: JSON.stringify({
462
- model: "claude-sonnet-4-20250514",
463
- max_tokens: 1000,
464
- system: systemPrompt + (systemContent ? "\n\n" + systemContent : ""),
465
- messages: anthropicMessages.length > 0 ? anthropicMessages : [{ role: "user", content: "Hello" }],
466
- }),
467
- });
468
- if (!response.ok) {
469
- const error = await response.text();
470
- return `Claude error: ${error}`;
471
- }
472
- const data = await response.json();
473
- return data.content[0]?.text || "No response from Claude";
474
- }
475
- function getAIClient() {
476
- if (ANTHROPIC_API_KEY) {
477
- return { provider: "anthropic", key: ANTHROPIC_API_KEY };
478
- }
479
- if (OPENAI_API_KEY) {
480
- return { provider: "openai", key: OPENAI_API_KEY };
481
- }
482
- return null;
483
- }
484
- function buildReflectionPrompt(session, feeling, insight) {
485
- return `You are a pattern extraction AI analyzing a development session.
486
-
487
- Session context: ${session}
488
- User feeling: ${feeling}
489
- User insight: ${insight}
490
-
491
- Your task: Extract the signal→outcome mapping.
492
-
493
- SIGNAL: The specific action, decision, or behavior that LED to the outcome.
494
- Not what happened, but what CAUSED it. Be specific and actionable.
495
-
496
- OUTCOME: "${feeling === "stuck" ? "antipattern" : "pattern"}" (based on user feeling)
497
-
498
- REINFORCEMENT: If this is a good pattern - what should an AI assistant say to encourage
499
- this behavior when detected in future sessions? Keep it brief, supportive.
500
-
501
- WARNING: If this is an antipattern - what should an AI assistant say to prevent this?
502
- Keep it brief, helpful, not lecturing.
503
-
504
- Respond ONLY with valid JSON (no markdown, no explanation):
505
- {"signal":"...","outcome":"pattern|antipattern","reinforcement":"...","warning":"..."}`;
506
- }
507
- async function callAIForReflection(client, prompt) {
508
- if (client.provider === "anthropic") {
509
- const response = await fetch("https://api.anthropic.com/v1/messages", {
510
- method: "POST",
511
- headers: {
512
- "Content-Type": "application/json",
513
- "x-api-key": client.key,
514
- "anthropic-version": "2023-06-01",
515
- },
516
- body: JSON.stringify({
517
- model: "claude-3-haiku-20240307", // Fast, cheap for reflection
518
- max_tokens: 500,
519
- messages: [{ role: "user", content: prompt }],
520
- }),
521
- });
522
- if (!response.ok) {
523
- throw new Error(`Anthropic API error: ${response.status}`);
524
- }
525
- const data = await response.json();
526
- const text = data.content[0]?.text || "{}";
527
- return JSON.parse(text);
528
- }
529
- if (client.provider === "openai") {
530
- const response = await fetch("https://api.openai.com/v1/chat/completions", {
531
- method: "POST",
532
- headers: {
533
- "Content-Type": "application/json",
534
- "Authorization": `Bearer ${client.key}`,
535
- },
536
- body: JSON.stringify({
537
- model: "gpt-4o-mini", // Fast, cheap for reflection
538
- messages: [{ role: "user", content: prompt }],
539
- response_format: { type: "json_object" },
540
- }),
541
- });
542
- if (!response.ok) {
543
- throw new Error(`OpenAI API error: ${response.status}`);
544
- }
545
- const data = await response.json();
546
- const text = data.choices[0]?.message?.content || "{}";
547
- return JSON.parse(text);
548
- }
549
- throw new Error(`Unknown AI provider: ${client.provider}`);
550
- }
551
- async function translateAndExecute(userInput, conversationHistory) {
552
- const sessionId = loadSession();
553
- const interpretSystemPrompt = `You interpret user requests for CEDA.
554
- Respond with JSON only: {"action": "predict"|"refine"|"info"|"accept"|"reject", "input": "the user's requirement"}
555
- - predict: User wants to create something new
556
- - refine: User wants to modify/add to current design (requires active session)
557
- - info: User is asking a question
558
- - accept: User approves the current design
559
- - reject: User rejects/wants to start over
560
-
561
- Current session: ${sessionId || "none"}`;
562
- const interpretation = await callClaude(interpretSystemPrompt, [
563
- { role: "user", content: userInput }
564
- ]);
565
- let cedaResult = null;
566
- let action = "info";
567
- try {
568
- const parsed = JSON.parse(interpretation);
569
- action = parsed.action;
570
- const input = parsed.input;
571
- if (action === "predict") {
572
- cedaResult = await callCedaAPI("/api/predict", "POST", {
573
- input,
574
- config: { enableAutoFix: true, maxAutoFixAttempts: 3 },
575
- });
576
- if (cedaResult && typeof cedaResult.sessionId === "string") {
577
- saveSession(cedaResult.sessionId);
578
- }
579
- }
580
- else if (action === "refine" && sessionId) {
581
- cedaResult = await callCedaAPI("/api/refine", "POST", {
582
- sessionId,
583
- refinement: input,
584
- });
585
- }
586
- else if (action === "accept" && sessionId) {
587
- cedaResult = await callCedaAPI("/api/feedback", "POST", {
588
- sessionId,
589
- accepted: true,
590
- });
591
- clearSession();
592
- }
593
- else if (action === "reject") {
594
- clearSession();
595
- cedaResult = { success: true, status: "Session cleared" };
596
- }
597
- }
598
- catch {
599
- // Claude didn't return valid JSON, treat as info request
600
- }
601
- let responseContext = "";
602
- if (cedaResult) {
603
- responseContext = `\n\nCEDA ${action} result:\n${JSON.stringify(cedaResult, null, 2)}\n\nSummarize this naturally for the user.`;
604
- }
605
- const responseMessages = [
606
- ...conversationHistory,
607
- { role: "user", content: userInput },
608
- ];
609
- return await callClaude(HERALD_SYSTEM_PROMPT + responseContext, responseMessages);
610
- }
611
- async function runChatMode() {
612
- const contextStr = getContextString();
613
- console.log(`
614
- Herald v${VERSION} - Chat Mode
615
- Context: ${contextStr}
616
- Type your requirements in natural language. Type 'exit' to quit.
617
- ──────────────────────────────────────────────────────────────
618
- `);
619
- const rl = readline.createInterface({
620
- input: process.stdin,
621
- output: process.stdout,
622
- });
623
- const conversationHistory = [];
624
- const currentSession = loadSession();
625
- if (currentSession) {
626
- console.log(`Resuming session: ${currentSession}\n`);
627
- }
628
- const prompt = () => {
629
- rl.question("You: ", async (input) => {
630
- const trimmed = input.trim();
631
- if (trimmed.toLowerCase() === "exit" || trimmed.toLowerCase() === "quit") {
632
- console.log("\nGoodbye!");
633
- rl.close();
634
- return;
635
- }
636
- if (!trimmed) {
637
- prompt();
638
- return;
639
- }
640
- conversationHistory.push({ role: "user", content: trimmed });
641
- const response = await translateAndExecute(trimmed, conversationHistory);
642
- conversationHistory.push({ role: "assistant", content: response });
643
- console.log(`\nHerald: ${response}\n`);
644
- prompt();
645
- });
646
- };
647
- prompt();
648
- }
649
- function getAuthHeader() {
650
- if (CEDA_API_TOKEN) {
651
- return `Bearer ${CEDA_API_TOKEN}`;
652
- }
653
- if (CEDA_API_USER && CEDA_API_PASS) {
654
- const basicAuth = Buffer.from(`${CEDA_API_USER}:${CEDA_API_PASS}`).toString("base64");
655
- return `Basic ${basicAuth}`;
656
- }
657
- return null;
658
- }
659
- async function callCedaAPI(endpoint, method = "GET", body) {
660
- if (!CEDA_API_URL) {
661
- return {
662
- success: false,
663
- error: "HERALD_API_URL not configured. Run: export HERALD_API_URL=https://getceda.com"
664
- };
665
- }
666
- let url = `${CEDA_API_URL}${endpoint}`;
667
- // Only add tenant params to endpoints that need them (patterns, session queries)
668
- // Don't add to simple endpoints like /api/stats, /health
669
- const needsTenantParams = endpoint.startsWith("/api/patterns") ||
670
- endpoint.startsWith("/api/session/") ||
671
- endpoint.startsWith("/api/observations");
672
- if (method === "GET" && needsTenantParams) {
673
- const separator = endpoint.includes("?") ? "&" : "?";
674
- url += `${separator}company=${HERALD_COMPANY}&project=${HERALD_PROJECT}&user=${HERALD_USER}`;
675
- }
676
- const headers = {
677
- "Content-Type": "application/json",
678
- };
679
- const authHeader = getAuthHeader();
680
- if (authHeader) {
681
- headers["Authorization"] = authHeader;
682
- }
683
- let enrichedBody = body;
684
- if (method === "POST" && body && typeof body === "object") {
685
- enrichedBody = {
686
- ...body,
687
- company: HERALD_COMPANY,
688
- project: HERALD_PROJECT,
689
- user: HERALD_USER,
690
- };
691
- }
692
- try {
693
- const response = await fetch(url, {
694
- method,
695
- headers,
696
- body: enrichedBody ? JSON.stringify(enrichedBody) : undefined,
697
- });
698
- if (!response.ok) {
699
- return { success: false, error: `HTTP ${response.status}: ${response.statusText}` };
700
- }
701
- return await response.json();
702
- }
703
- catch (error) {
704
- return { success: false, error: `Connection failed: ${error}` };
705
- }
706
- }
707
- // ============================================
708
- // CLI MODE - Human-friendly commands
709
- // ============================================
710
- function printUsage() {
711
- const currentSession = loadSession();
712
- const contextStr = getContextString();
713
- const sessionDir = getHeraldDir();
714
- console.log(`
715
- Herald MCP v${VERSION} - AI-native interface to CEDA
716
-
717
- Context: ${contextStr}
718
- Session: ${currentSession || "(none)"}
719
- Path: ${sessionDir}
720
-
721
- Usage:
722
- herald-mcp <command> [options]
723
-
724
- Commands:
725
- Setup:
726
- login Authenticate with GitHub (opens browser)
727
- logout Clear stored authentication
728
- init Initialize Herald config in project
729
- config Output MCP JSON for any client
730
-
731
- Account:
732
- upgrade Open billing portal / view usage
733
-
734
- MCP Tools (when running as server):
735
- health Check CEDA system status
736
- stats Get server statistics
737
- patterns View learned patterns
738
-
739
- Legacy CLI:
740
- chat Natural conversation mode
741
- predict "<signal>" Start new prediction
742
- refine "<text>" Refine current session
743
- observe yes|no Record feedback & close session
744
- new Clear session, start fresh
745
-
746
- Examples:
747
- npx @spilno/herald-mcp login # Authenticate
748
- npx @spilno/herald-mcp config # Get MCP config
749
- npx @spilno/herald-mcp init # Setup in project
750
- npx @spilno/herald-mcp upgrade # Manage subscription
751
-
752
- Environment:
753
- CEDA_URL CEDA server URL (default: https://getceda.com)
754
- CEDA_TOKEN Auth token (auto-set after login)
755
-
756
- MCP Mode:
757
- When piped, Herald speaks JSON-RPC for AI agents.
758
- `);
759
- }
760
- function formatOutput(data) {
761
- if (data.error) {
762
- console.error(`Error: ${data.error}`);
763
- process.exit(1);
764
- }
765
- if (data.sessionId) {
766
- console.log(`\nSession: ${data.sessionId}\n`);
767
- }
768
- console.log(JSON.stringify(data, null, 2));
769
- }
770
- async function runCLI(args) {
771
- const command = args[0]?.toLowerCase();
772
- if (!command || command === "help" || command === "--help" || command === "-h") {
773
- printUsage();
774
- return;
775
- }
776
- if (command === "--version" || command === "-v") {
777
- console.log(`herald-mcp v${VERSION}`);
778
- return;
779
- }
780
- switch (command) {
781
- case "init": {
782
- await runInit(args.slice(1));
783
- break;
784
- }
785
- case "login": {
786
- await runLogin(args.slice(1));
787
- break;
788
- }
789
- case "logout": {
790
- await runLogout(args.slice(1));
791
- break;
792
- }
793
- case "config": {
794
- await runConfig(args.slice(1));
795
- break;
796
- }
797
- case "upgrade": {
798
- await runUpgrade(args.slice(1));
799
- break;
800
- }
801
- case "chat": {
802
- await runChatMode();
803
- break;
804
- }
805
- case "health": {
806
- const result = await callCedaAPI("/health");
807
- formatOutput(result);
808
- break;
809
- }
810
- case "stats": {
811
- const result = await callCedaAPI("/api/stats");
812
- formatOutput(result);
813
- break;
814
- }
815
- case "predict": {
816
- const signal = args[1];
817
- if (!signal) {
818
- console.error("Error: Missing signal. Usage: herald-mcp predict \"<signal>\"");
819
- process.exit(1);
820
- }
821
- const result = await callCedaAPI("/api/predict", "POST", {
822
- input: signal,
823
- config: { enableAutoFix: true, maxAutoFixAttempts: 3 },
824
- });
825
- if (result.sessionId && typeof result.sessionId === "string") {
826
- saveSession(result.sessionId);
827
- console.log(`\n✓ Session saved: ${result.sessionId}\n`);
828
- }
829
- formatOutput(result);
830
- break;
831
- }
832
- case "refine": {
833
- const refinement = args[1];
834
- if (!refinement) {
835
- console.error("Error: Missing refinement. Usage: herald-mcp refine \"<refinement>\"");
836
- process.exit(1);
837
- }
838
- const sessionId = loadSession();
839
- if (!sessionId) {
840
- console.error("Error: No active session. Run 'herald-mcp predict \"...\"' first.");
841
- process.exit(1);
842
- }
843
- const result = await callCedaAPI("/api/refine", "POST", {
844
- sessionId,
845
- refinement,
846
- });
847
- formatOutput(result);
848
- break;
849
- }
850
- case "resume":
851
- case "session": {
852
- const sessionId = args[1] || loadSession();
853
- if (!sessionId) {
854
- console.error("Error: No active session. Run 'herald-mcp predict \"...\"' first.");
855
- process.exit(1);
856
- }
857
- const result = await callCedaAPI(`/api/session/${sessionId}`);
858
- formatOutput(result);
859
- break;
860
- }
861
- case "observe": {
862
- const accepted = args[1]?.toLowerCase();
863
- if (!accepted) {
864
- console.error("Error: Missing feedback. Usage: herald-mcp observe yes|no");
865
- process.exit(1);
866
- }
867
- const sessionId = loadSession();
868
- if (!sessionId) {
869
- console.error("Error: No active session. Run 'herald-mcp predict \"...\"' first.");
870
- process.exit(1);
871
- }
872
- const result = await callCedaAPI("/api/feedback", "POST", {
873
- sessionId,
874
- accepted: accepted === "yes" || accepted === "true" || accepted === "accept",
875
- comment: args[2],
876
- });
877
- clearSession();
878
- console.log("\n✓ Session closed.\n");
879
- formatOutput(result);
880
- break;
881
- }
882
- case "new": {
883
- clearSession();
884
- console.log("✓ Session cleared. Ready for new prediction.");
885
- break;
886
- }
887
- default:
888
- console.error(`Unknown command: ${command}`);
889
- printUsage();
890
- process.exit(1);
891
- }
892
- }
893
- // ============================================
894
- // MCP MODE - JSON-RPC for AI agents
895
- // ============================================
896
- const server = new Server({ name: "herald", version: VERSION, description: HERALD_DESCRIPTION }, { capabilities: { tools: {}, resources: {} } });
897
- const tools = [
898
- {
899
- name: "herald_help",
900
- description: "Get started with Herald MCP - shows available tools, quick examples, and links to documentation",
901
- inputSchema: { type: "object", properties: {} },
902
- },
903
- {
904
- name: "herald_health",
905
- description: "Check Herald and CEDA system status",
906
- inputSchema: { type: "object", properties: {} },
907
- },
908
- {
909
- name: "herald_context",
910
- description: `Get or refresh Herald's context (company/project/user).
911
-
912
- Context is derived from git (trusted) or path (fallback).
913
- Use refresh=true after cloning a repo or changing directories to update context.
914
-
915
- Returns: Current context including trust level and source.`,
916
- inputSchema: {
917
- type: "object",
918
- properties: {
919
- refresh: {
920
- type: "boolean",
921
- description: "Re-derive context from current directory's git info"
922
- }
923
- }
924
- },
925
- },
926
- {
927
- name: "herald_stats",
928
- description: "Get CEDA server statistics and loaded patterns info",
929
- inputSchema: { type: "object", properties: {} },
930
- },
931
- {
932
- name: "herald_gate",
933
- description: `Request authorization before large operations.
934
-
935
- WHEN TO USE:
936
- Call this BEFORE multi-file operations (>3 files), module scaffolding, or bulk changes.
937
-
938
- This tool:
939
- 1. Formats a clear authorization request for the user
940
- 2. Returns a gate_id for tracking
941
- 3. Records the operation scope for audit
942
-
943
- After calling this tool, WAIT for explicit user approval before proceeding.
944
- User may respond: Y/yes/proceed (approved), adjust (modify scope), or N/no (denied).
945
-
946
- Example flow:
947
- 1. You complete synthesis/planning
948
- 2. Call herald_gate with operation summary
949
- 3. Tool returns formatted request
950
- 4. STOP and wait for user response
951
- 5. Only proceed if user approves`,
952
- inputSchema: {
953
- type: "object",
954
- properties: {
955
- operation: {
956
- type: "string",
957
- description: "What operation needs authorization (e.g., 'Create bh-incidents module')"
958
- },
959
- scope: {
960
- type: "string",
961
- description: "Scope summary (e.g., '31 files, 6 dictionaries, 4 forms')"
962
- },
963
- template: {
964
- type: "string",
965
- description: "Template/pattern being followed (e.g., 'bh-inspections')"
966
- },
967
- rationale: {
968
- type: "string",
969
- description: "Brief rationale for the operation"
970
- },
971
- },
972
- required: ["operation", "scope"],
973
- },
974
- },
975
- {
976
- name: "herald_predict",
977
- description: "Generate non-deterministic structure prediction from signal. Returns sessionId for multi-turn conversations.",
978
- inputSchema: {
979
- type: "object",
980
- properties: {
981
- signal: { type: "string", description: "Natural language input" },
982
- context: { type: "string", description: "Additional context" },
983
- session_id: { type: "string", description: "Session ID for multi-turn" },
984
- participant: { type: "string", description: "Participant name" },
985
- },
986
- required: ["signal"],
987
- },
988
- },
989
- {
990
- name: "herald_refine",
991
- description: "Refine an existing prediction with additional requirements.",
992
- inputSchema: {
993
- type: "object",
994
- properties: {
995
- session_id: { type: "string", description: "Session ID from previous call" },
996
- refinement: { type: "string", description: "Refinement instruction" },
997
- context: { type: "string", description: "Additional context" },
998
- participant: { type: "string", description: "Participant name" },
999
- },
1000
- required: ["session_id", "refinement"],
1001
- },
1002
- },
1003
- {
1004
- name: "herald_session",
1005
- description: "Get session information including history",
1006
- inputSchema: {
1007
- type: "object",
1008
- properties: {
1009
- session_id: { type: "string", description: "Session ID to retrieve" },
1010
- },
1011
- required: ["session_id"],
1012
- },
1013
- },
1014
- {
1015
- name: "herald_feedback",
1016
- description: "Submit feedback on a prediction (accept/reject)",
1017
- inputSchema: {
1018
- type: "object",
1019
- properties: {
1020
- session_id: { type: "string", description: "Session ID" },
1021
- accepted: { type: "boolean", description: "Whether prediction was accepted" },
1022
- comment: { type: "string", description: "Optional feedback comment" },
1023
- },
1024
- required: ["session_id", "accepted"],
1025
- },
1026
- },
1027
- {
1028
- name: "herald_context_status",
1029
- description: "Read status from Herald contexts across domains (offspring vaults)",
1030
- inputSchema: {
1031
- type: "object",
1032
- properties: {
1033
- vault: { type: "string", description: "Specific vault to query (optional)" },
1034
- },
1035
- },
1036
- },
1037
- {
1038
- name: "herald_share_insight",
1039
- description: "Share a pattern insight with another Herald context. Herald instances communicate through shared insights to propagate learned patterns across domains.",
1040
- inputSchema: {
1041
- type: "object",
1042
- properties: {
1043
- insight: { type: "string", description: "The insight to share" },
1044
- target_vault: { type: "string", description: "Target vault (optional)" },
1045
- topic: { type: "string", description: "Topic category" },
1046
- },
1047
- required: ["insight"],
1048
- },
1049
- },
1050
- {
1051
- name: "herald_sync",
1052
- description: "Flush locally buffered insights to CEDA cloud. Use when insights were recorded in local mode (cloud unavailable) and need to be synced.",
1053
- inputSchema: {
1054
- type: "object",
1055
- properties: {
1056
- dry_run: { type: "boolean", description: "If true, show what would be synced without actually syncing" },
1057
- },
1058
- },
1059
- },
1060
- {
1061
- name: "herald_query_insights",
1062
- description: "Query accumulated insights on a topic",
1063
- inputSchema: {
1064
- type: "object",
1065
- properties: {
1066
- topic: { type: "string", description: "Topic to query" },
1067
- vault: { type: "string", description: "Specific vault to query (optional)" },
1068
- },
1069
- required: ["topic"],
1070
- },
1071
- },
1072
- // CEDA-49: Session Management Tools
1073
- {
1074
- name: "herald_session_list",
1075
- description: "List sessions for a company with optional filters. Returns session summaries including id, status, created/updated timestamps.",
1076
- inputSchema: {
1077
- type: "object",
1078
- properties: {
1079
- company: { type: "string", description: "Filter by company (optional, defaults to HERALD_COMPANY)" },
1080
- project: { type: "string", description: "Filter by project (optional)" },
1081
- user: { type: "string", description: "Filter by user (optional)" },
1082
- status: { type: "string", description: "Filter by status: active, archived, or expired (optional)" },
1083
- limit: { type: "number", description: "Maximum number of sessions to return (optional, default 100)" },
1084
- },
1085
- },
1086
- },
1087
- {
1088
- name: "herald_session_get",
1089
- description: "Get detailed information about a specific session including current prediction state and message history.",
1090
- inputSchema: {
1091
- type: "object",
1092
- properties: {
1093
- session_id: { type: "string", description: "Session ID to retrieve" },
1094
- },
1095
- required: ["session_id"],
1096
- },
1097
- },
1098
- {
1099
- name: "herald_session_history",
1100
- description: "Get version history for a session. Shows all recorded versions with timestamps and change types.",
1101
- inputSchema: {
1102
- type: "object",
1103
- properties: {
1104
- session_id: { type: "string", description: "Session ID to get history for" },
1105
- limit: { type: "number", description: "Maximum number of versions to return (optional)" },
1106
- },
1107
- required: ["session_id"],
1108
- },
1109
- },
1110
- {
1111
- name: "herald_session_rollback",
1112
- description: "Restore a session to a previous version. Creates a new version entry recording the rollback.",
1113
- inputSchema: {
1114
- type: "object",
1115
- properties: {
1116
- session_id: { type: "string", description: "Session ID to rollback" },
1117
- version: { type: "number", description: "Version number to restore to" },
1118
- },
1119
- required: ["session_id", "version"],
1120
- },
1121
- },
1122
- {
1123
- name: "herald_session_archive",
1124
- description: "Archive a session. Archived sessions are preserved but marked as inactive.",
1125
- inputSchema: {
1126
- type: "object",
1127
- properties: {
1128
- session_id: { type: "string", description: "Session ID to archive" },
1129
- },
1130
- required: ["session_id"],
1131
- },
1132
- },
1133
- // Session Mining - Pattern/Antipattern Learning
1134
- {
1135
- name: "herald_reflect",
1136
- description: `Capture a pattern or antipattern from the session.
1137
-
1138
- TRIGGER WORDS: "capture", "log this", "that was smooth/rough", "reflect"
1139
-
1140
- BEFORE CALLING - ASK USER:
1141
- "What specifically worked (or didn't work) here?"
1142
- User's answer goes in the 'insight' parameter.
1143
-
1144
- DO NOT GUESS. The user knows what they valued. Ask them.
1145
-
1146
- ABSTRACTION GUIDANCE:
1147
- Capture the PATTERN, not the SPECIFICS. Good patterns are reusable.
1148
- - BAD: "Fixed bug in /Users/john/project/auth.ts line 47"
1149
- - GOOD: "Early return pattern for auth validation reduces nesting"
1150
- - BAD: "API key sk-proj-xxx was in wrong env file"
1151
- - GOOD: "Secrets in .env.local not .env prevents accidental commits"
1152
-
1153
- Example flow:
1154
- 1. User: "That was smooth, capture it"
1155
- 2. You: "What specifically worked here? (Describe the pattern, not specific files/values)"
1156
- 3. User: "The ASCII visualization approach"
1157
- 4. You call herald_reflect with insight: "ASCII visualization approach"
1158
-
1159
- PRIVACY (CEDA-65):
1160
- - Client-side sanitization runs BEFORE any data leaves your machine
1161
- - API keys, tokens, passwords, file paths with usernames are auto-redacted
1162
- - Private keys and AWS credentials are BLOCKED entirely
1163
- - Use dry_run=true to preview exactly what would be transmitted
1164
-
1165
- DRY RUN MODE:
1166
- Set dry_run=true to preview sanitization without storing.
1167
- Shows what would be redacted and final transmitted text.`,
1168
- inputSchema: {
1169
- type: "object",
1170
- properties: {
1171
- session: {
1172
- type: "string",
1173
- description: "Brief context of what happened"
1174
- },
1175
- feeling: {
1176
- type: "string",
1177
- enum: ["stuck", "success"],
1178
- description: "stuck = friction/antipattern, success = flow/pattern"
1179
- },
1180
- insight: {
1181
- type: "string",
1182
- description: "What specifically worked or didn't - MUST ASK USER, do not guess"
1183
- },
1184
- dry_run: {
1185
- type: "boolean",
1186
- description: "If true, preview what would be captured without storing (CEDA-65)"
1187
- },
1188
- },
1189
- required: ["session", "feeling", "insight"],
1190
- },
1191
- },
1192
- // Query learned patterns - Claude reads this to avoid repeating mistakes
1193
- {
1194
- name: "herald_patterns",
1195
- description: `Query learned patterns and antipatterns for current context.
1196
-
1197
- CALL THIS AT SESSION START to learn from past sessions.
1198
-
1199
- Returns:
1200
- - patterns: Things that worked (reinforce these)
1201
- - antipatterns: Things that failed (avoid these)
1202
- - meta: Which capture method works better
1203
-
1204
- Use this to:
1205
- 1. Avoid repeating past mistakes
1206
- 2. Apply proven approaches
1207
- 3. Learn from other sessions in this project`,
1208
- inputSchema: {
1209
- type: "object",
1210
- properties: {
1211
- context: {
1212
- type: "string",
1213
- description: "Optional context to filter patterns (e.g., 'deployment', 'debugging')"
1214
- },
1215
- },
1216
- },
1217
- },
1218
- // AI-Native Simulation - Deep pattern extraction via AI-to-AI roleplay
1219
- {
1220
- name: "herald_simulate",
1221
- description: `AI-native pattern extraction via AI-to-AI reflection.
1222
-
1223
- Use when you need DEEP analysis - not just capturing, but understanding WHY.
1224
-
1225
- WHEN TO USE herald_simulate vs herald_reflect:
1226
- - herald_reflect: Quick capture, obvious pattern, user knows signal
1227
- - herald_simulate: Complex situation, need AI to discover deeper signal
1228
-
1229
- Requires: ANTHROPIC_API_KEY or OPENAI_API_KEY in env.
1230
-
1231
- BEFORE CALLING - ASK USER:
1232
- "What specifically worked (or didn't)?"
1233
-
1234
- This tool:
1235
- 1. Calls another AI to roleplay as a reflection partner
1236
- 2. AI extracts: signal (what caused it), outcome, reinforcement/warning text
1237
- 3. Sends enriched data to CEDA with method="simulation"
1238
-
1239
- CEDA learns which method works better for which contexts (meta-learning).`,
1240
- inputSchema: {
1241
- type: "object",
1242
- properties: {
1243
- session: {
1244
- type: "string",
1245
- description: "Context of what happened in the session"
1246
- },
1247
- feeling: {
1248
- type: "string",
1249
- enum: ["stuck", "success"],
1250
- description: "stuck = friction/antipattern, success = flow/pattern"
1251
- },
1252
- insight: {
1253
- type: "string",
1254
- description: "User's answer to 'what worked/didn't' - MUST ASK USER"
1255
- },
1256
- },
1257
- required: ["session", "feeling", "insight"],
1258
- },
1259
- },
1260
- // CEDA-64: Herald Command Extensions
1261
- {
1262
- name: "herald_session_reflections",
1263
- description: `Get summary of reflections captured during this MCP session.
1264
-
1265
- Returns count of patterns and antipatterns captured since Herald started.
1266
- This is LOCAL tracking - clears when Herald restarts.
1267
-
1268
- Use this to:
1269
- 1. Review what's been captured in the current session
1270
- 2. Verify reflections were recorded
1271
- 3. Get a quick summary before ending a session`,
1272
- inputSchema: {
1273
- type: "object",
1274
- properties: {},
1275
- },
1276
- },
1277
- {
1278
- name: "herald_pattern_feedback",
1279
- description: `Provide feedback on whether a learned pattern/antipattern helped.
1280
-
1281
- Call this after applying a pattern from herald_patterns to track effectiveness.
1282
- This feeds the meta-learning loop - CEDA learns which patterns actually help.
1283
-
1284
- Parameters:
1285
- - pattern_id: ID of the pattern/reflection (from herald_patterns output)
1286
- - pattern_text: Alternative to ID - the pattern text to match
1287
- - outcome: "helped" or "didnt_help"
1288
-
1289
- Example: After applying an antipattern warning and it prevented a mistake,
1290
- call with outcome="helped" to reinforce that pattern.`,
1291
- inputSchema: {
1292
- type: "object",
1293
- properties: {
1294
- pattern_id: {
1295
- type: "string",
1296
- description: "ID of the pattern/reflection to provide feedback on"
1297
- },
1298
- pattern_text: {
1299
- type: "string",
1300
- description: "Alternative: pattern text to match (if ID not available)"
1301
- },
1302
- outcome: {
1303
- type: "string",
1304
- enum: ["helped", "didnt_help"],
1305
- description: "Whether applying this pattern helped or not"
1306
- },
1307
- },
1308
- required: ["outcome"],
1309
- },
1310
- },
1311
- {
1312
- name: "herald_share_scoped",
1313
- description: `Share an insight with other Herald contexts using scope control.
1314
-
1315
- Scopes:
1316
- - "parent": Share with parent project/company (escalate learning)
1317
- - "siblings": Share with sibling projects in same company
1318
- - "all": Share globally across all contexts
1319
-
1320
- Use this to propagate valuable patterns beyond the current context.
1321
- Example: A debugging pattern that worked well could be shared with siblings.`,
1322
- inputSchema: {
1323
- type: "object",
1324
- properties: {
1325
- insight: {
1326
- type: "string",
1327
- description: "The insight/pattern to share"
1328
- },
1329
- scope: {
1330
- type: "string",
1331
- enum: ["parent", "siblings", "all"],
1332
- description: "Who to share with: parent, siblings, or all"
1333
- },
1334
- topic: {
1335
- type: "string",
1336
- description: "Optional topic category for the insight"
1337
- },
1338
- },
1339
- required: ["insight", "scope"],
1340
- },
1341
- },
1342
- // CEDA-65: GDPR Compliance Tools
1343
- {
1344
- name: "herald_forget",
1345
- description: `GDPR Article 17 - Right to Erasure ("Right to be Forgotten").
1346
-
1347
- Delete learned patterns and reflections from CEDA storage.
1348
-
1349
- Use this when:
1350
- - User requests deletion of their data
1351
- - Compliance requires data removal
1352
- - Cleaning up test/invalid patterns
1353
-
1354
- Parameters:
1355
- - pattern_id: Delete a specific pattern by ID
1356
- - session_id: Delete all patterns from a session
1357
- - all: Delete ALL patterns for current context (company/project/user)
1358
-
1359
- WARNING: This action is irreversible. Data will be permanently deleted.`,
1360
- inputSchema: {
1361
- type: "object",
1362
- properties: {
1363
- pattern_id: {
1364
- type: "string",
1365
- description: "Specific pattern ID to delete"
1366
- },
1367
- session_id: {
1368
- type: "string",
1369
- description: "Delete all patterns from this session"
1370
- },
1371
- all: {
1372
- type: "boolean",
1373
- description: "Delete ALL patterns for current context (use with caution)"
1374
- },
1375
- },
1376
- },
1377
- },
1378
- {
1379
- name: "herald_export",
1380
- description: `GDPR Article 20 - Right to Data Portability.
1381
-
1382
- Export all learned patterns and reflections in a portable format.
1383
-
1384
- Use this when:
1385
- - User requests a copy of their data
1386
- - Migrating data between systems
1387
- - Compliance audit requires data export
1388
-
1389
- Returns all patterns for the current context (company/project/user) in the specified format.`,
1390
- inputSchema: {
1391
- type: "object",
1392
- properties: {
1393
- format: {
1394
- type: "string",
1395
- enum: ["json", "csv"],
1396
- description: "Export format: json (default) or csv"
1397
- },
1398
- },
1399
- },
1400
- },
1401
- ];
1402
- server.setRequestHandler(ListToolsRequestSchema, async () => {
1403
- return { tools };
1404
- });
1405
- // ============================================
1406
- // MCP RESOURCES - Auto-readable by Claude Code
1407
- // ============================================
1408
- // Helper to fetch patterns with cascade (reused from herald_patterns tool)
1409
- async function fetchPatternsWithCascade() {
1410
- const seenInsights = new Set();
1411
- const patterns = [];
1412
- const antipatterns = [];
1413
- const queries = [
1414
- { scope: "user", url: `/api/herald/reflections?company=${HERALD_COMPANY}&project=${HERALD_PROJECT}&user=${HERALD_USER}&limit=100` },
1415
- { scope: "project", url: `/api/herald/reflections?company=${HERALD_COMPANY}&project=${HERALD_PROJECT}&limit=100` },
1416
- { scope: "company", url: `/api/herald/reflections?company=${HERALD_COMPANY}&limit=100` },
1417
- ];
1418
- for (const { scope, url } of queries) {
1419
- try {
1420
- const result = await callCedaAPI(url);
1421
- const scopePatterns = result.patterns || [];
1422
- const scopeAntipatterns = result.antipatterns || [];
1423
- for (const p of scopePatterns) {
1424
- const key = p.insight.toLowerCase().trim();
1425
- if (!seenInsights.has(key)) {
1426
- seenInsights.add(key);
1427
- patterns.push(`${p.insight} [${scope}]`);
1428
- }
1429
- }
1430
- for (const ap of scopeAntipatterns) {
1431
- const key = ap.insight.toLowerCase().trim();
1432
- if (!seenInsights.has(key)) {
1433
- seenInsights.add(key);
1434
- antipatterns.push(`${ap.insight} [${scope}]`);
1435
- }
1436
- }
1437
- }
1438
- catch {
1439
- // Continue if a level fails
1440
- }
1441
- }
1442
- return { patterns, antipatterns, context: `${HERALD_USER}→${HERALD_PROJECT}→${HERALD_COMPANY}` };
1443
- }
1444
- server.setRequestHandler(ListResourcesRequestSchema, async () => {
1445
- const resources = [
1446
- {
1447
- uri: "herald://patterns",
1448
- name: "Herald Learned Patterns",
1449
- description: `Patterns and antipatterns learned from past sessions for ${HERALD_USER}→${HERALD_PROJECT}→${HERALD_COMPANY}. READ THIS AT SESSION START.`,
1450
- mimeType: "text/plain",
1451
- },
1452
- {
1453
- uri: "herald://context",
1454
- name: "Herald Context",
1455
- description: "Current Herald context configuration (company/project/user)",
1456
- mimeType: "application/json",
1457
- },
1458
- ];
1459
- return { resources };
1460
- });
1461
- server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
1462
- const { uri } = request.params;
1463
- if (uri === "herald://patterns") {
1464
- try {
1465
- const { patterns, antipatterns, context } = await fetchPatternsWithCascade();
1466
- let content = `# Herald Patterns for ${context}\n\n`;
1467
- content += `**READ THIS FIRST** - These are learned patterns from past sessions.\n\n`;
1468
- if (antipatterns.length > 0) {
1469
- content += `## ⚠️ ANTIPATTERNS - AVOID THESE\n`;
1470
- antipatterns.forEach((ap, i) => {
1471
- content += `${i + 1}. ${ap}\n`;
1472
- });
1473
- content += `\n`;
1474
- }
1475
- if (patterns.length > 0) {
1476
- content += `## ✓ PATTERNS - DO THESE\n`;
1477
- patterns.forEach((p, i) => {
1478
- content += `${i + 1}. ${p}\n`;
1479
- });
1480
- content += `\n`;
1481
- }
1482
- if (patterns.length === 0 && antipatterns.length === 0) {
1483
- content += `No patterns learned yet. Capture patterns with "herald reflect" when you notice friction or flow.\n`;
1484
- }
1485
- content += `\n---\n*Auto-loaded from CEDA. Call herald_pattern_feedback() when a pattern helps.*\n`;
1486
- return {
1487
- contents: [{
1488
- uri,
1489
- mimeType: "text/plain",
1490
- text: content,
1491
- }],
1492
- };
1493
- }
1494
- catch (error) {
1495
- return {
1496
- contents: [{
1497
- uri,
1498
- mimeType: "text/plain",
1499
- text: `Failed to load patterns: ${error}\n\nCEDA may be unavailable.`,
1500
- }],
1501
- };
1502
- }
1503
- }
1504
- if (uri === "herald://context") {
1505
- const context = {
1506
- company: HERALD_COMPANY,
1507
- project: HERALD_PROJECT,
1508
- user: HERALD_USER,
1509
- vault: HERALD_VAULT || null,
1510
- tags: HERALD_TAGS,
1511
- trust: TRUST_LEVEL,
1512
- source: CONTEXT_SOURCE,
1513
- propagates: PROPAGATES,
1514
- gitRemote: GIT_REMOTE,
1515
- cedaUrl: CEDA_API_URL,
1516
- };
1517
- return {
1518
- contents: [{
1519
- uri,
1520
- mimeType: "application/json",
1521
- text: JSON.stringify(context, null, 2),
1522
- }],
1523
- };
1524
- }
1525
- throw new Error(`Unknown resource: ${uri}`);
1526
- });
1527
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
1528
- const { name, arguments: args } = request.params;
1529
- try {
1530
- switch (name) {
1531
- case "herald_help": {
1532
- const contextStr = getContextString();
1533
- const helpText = `# Herald MCP v${VERSION}
1534
-
1535
- Welcome to Herald - your AI-native interface to CEDA (Cognitive Event-Driven Architecture).
1536
-
1537
- ## Current Context
1538
- - Company: ${HERALD_COMPANY}
1539
- - Project: ${HERALD_PROJECT}
1540
- - User: ${HERALD_USER}
1541
-
1542
- ## Available Tools
1543
-
1544
- **Getting Started:**
1545
- - \`herald_help\` - This guide
1546
- - \`herald_health\` - Check CEDA connection
1547
- - \`herald_stats\` - View patterns and sessions
1548
-
1549
- **Core Workflow:**
1550
- 1. \`herald_predict\` - Generate structure predictions from natural language
1551
- Example: "create a safety incident module"
1552
- 2. \`herald_refine\` - Improve predictions iteratively
1553
- 3. \`herald_feedback\` - Accept or reject predictions (feeds learning loop)
1554
-
1555
- **Sessions:**
1556
- - \`herald_session\` - View session history (legacy)
1557
-
1558
- **Session Management (CEDA-49):**
1559
- - \`herald_session_list\` - List sessions with filters (company, project, user, status)
1560
- - \`herald_session_get\` - Get detailed session info including prediction state
1561
- - \`herald_session_history\` - View version history for a session
1562
- - \`herald_session_rollback\` - Restore a session to a previous version
1563
- - \`herald_session_archive\` - Archive a session (mark as inactive)
1564
-
1565
- **Context Sync:**
1566
- - \`herald_context_status\` - See other Herald instances
1567
- - \`herald_share_insight\` - Share patterns across projects
1568
- - \`herald_query_insights\` - Get accumulated insights
1569
-
1570
- ## Quick Example
1571
-
1572
- Ask me to create something:
1573
- > "Create a module for tracking safety incidents with forms for reporting and investigation"
1574
-
1575
- Herald will:
1576
- 1. Generate a structure prediction based on learned patterns
1577
- 2. Let you refine it ("add OSHA compliance fields")
1578
- 3. Learn from your feedback to improve future predictions
1579
-
1580
- ## Resources
1581
- - Setup Guide: https://getceda.com/docs/herald-setup-guide.md
1582
- - CEDA Backend: ${CEDA_API_URL || "not configured"}
1583
-
1584
- ## Tips
1585
- - Be specific in your requests - Herald learns from patterns
1586
- - Use refine to iterate on predictions
1587
- - Your feedback (accept/reject) improves CEDA for everyone
1588
- `;
1589
- return {
1590
- content: [{ type: "text", text: helpText }],
1591
- };
1592
- }
1593
- case "herald_health": {
1594
- const cedaHealth = await callCedaAPI("/health");
1595
- const buffer = getBufferedInsights();
1596
- const cloudAvailable = !cedaHealth.error;
1597
- const config = {
1598
- cedaUrl: CEDA_API_URL,
1599
- company: HERALD_COMPANY,
1600
- project: HERALD_PROJECT,
1601
- user: HERALD_USER,
1602
- vault: HERALD_VAULT || "(not set)",
1603
- tags: HERALD_TAGS,
1604
- trust: TRUST_LEVEL,
1605
- source: CONTEXT_SOURCE,
1606
- propagates: PROPAGATES,
1607
- gitRemote: GIT_REMOTE,
1608
- };
1609
- const warnings = [];
1610
- if (CONTEXT_SOURCE === 'path') {
1611
- warnings.push(`Context derived from folder path (LOW trust)`);
1612
- warnings.push("Add git remote for HIGH trust context");
1613
- }
1614
- if (!process.env.CEDA_URL && !process.env.HERALD_API_URL) {
1615
- warnings.push("Using default CEDA_URL (getceda.com) - set CEDA_URL for custom endpoint");
1616
- }
1617
- return {
1618
- content: [{
1619
- type: "text",
1620
- text: JSON.stringify({
1621
- herald: {
1622
- version: VERSION,
1623
- config,
1624
- warnings: warnings.length > 0 ? warnings : undefined,
1625
- },
1626
- ceda: cedaHealth,
1627
- buffer: {
1628
- size: buffer.length,
1629
- mode: cloudAvailable ? "cloud" : "local",
1630
- hint: buffer.length > 0 ? "Use herald_sync to flush buffered insights" : undefined,
1631
- },
1632
- }, null, 2)
1633
- }],
1634
- };
1635
- }
1636
- case "herald_context": {
1637
- const refresh = args?.refresh;
1638
- if (refresh) {
1639
- // Re-derive context from current directory
1640
- const newContext = loadOrDeriveContext();
1641
- // Update module-level variables directly
1642
- HERALD_USER = newContext.user;
1643
- HERALD_TAGS = newContext.tags;
1644
- HERALD_COMPANY = newContext.tags[0] || "";
1645
- HERALD_PROJECT = newContext.tags[1] || newContext.tags[0] || "";
1646
- TRUST_LEVEL = newContext.trust;
1647
- CONTEXT_SOURCE = newContext.source;
1648
- PROPAGATES = newContext.propagates;
1649
- return {
1650
- content: [{
1651
- type: "text",
1652
- text: JSON.stringify({
1653
- refreshed: true,
1654
- context: {
1655
- company: HERALD_COMPANY,
1656
- project: HERALD_PROJECT,
1657
- user: HERALD_USER,
1658
- tags: HERALD_TAGS,
1659
- trust: TRUST_LEVEL,
1660
- source: CONTEXT_SOURCE,
1661
- propagates: PROPAGATES,
1662
- gitRemote: newContext.gitRemote,
1663
- },
1664
- message: TRUST_LEVEL === 'HIGH'
1665
- ? `Context refreshed from git: ${newContext.gitRemote}`
1666
- : `Context refreshed from ${CONTEXT_SOURCE} (LOW trust)`
1667
- }, null, 2)
1668
- }],
1669
- };
1670
- }
1671
- // Just return current context
1672
- return {
1673
- content: [{
1674
- type: "text",
1675
- text: JSON.stringify({
1676
- context: {
1677
- company: HERALD_COMPANY,
1678
- project: HERALD_PROJECT,
1679
- user: HERALD_USER,
1680
- tags: HERALD_TAGS,
1681
- trust: TRUST_LEVEL,
1682
- source: CONTEXT_SOURCE,
1683
- propagates: PROPAGATES,
1684
- gitRemote: GIT_REMOTE,
1685
- },
1686
- hint: TRUST_LEVEL === 'LOW'
1687
- ? "Use herald_context(refresh=true) in a git repo for HIGH trust"
1688
- : undefined
1689
- }, null, 2)
1690
- }],
1691
- };
1692
- }
1693
- case "herald_stats": {
1694
- const result = await callCedaAPI("/api/stats");
1695
- return {
1696
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1697
- };
1698
- }
1699
- case "herald_gate": {
1700
- const operation = args?.operation;
1701
- const scope = args?.scope;
1702
- const template = args?.template;
1703
- const rationale = args?.rationale;
1704
- // Generate gate ID for tracking
1705
- const gateId = `gate-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
1706
- // Format the authorization request
1707
- let gateRequest = `\n## Authorization Required\n\n`;
1708
- gateRequest += `**Operation:** ${operation}\n`;
1709
- gateRequest += `**Scope:** ${scope}\n`;
1710
- if (template) {
1711
- gateRequest += `**Template:** Following \`${template}\` patterns\n`;
1712
- }
1713
- if (rationale) {
1714
- gateRequest += `**Rationale:** ${rationale}\n`;
1715
- }
1716
- gateRequest += `\n---\n`;
1717
- gateRequest += `**Proceed?** [Y/yes/proceed] [adjust] [N/no]\n`;
1718
- gateRequest += `\n_Gate ID: ${gateId}_\n`;
1719
- return {
1720
- content: [{
1721
- type: "text",
1722
- text: JSON.stringify({
1723
- success: true,
1724
- gate_id: gateId,
1725
- status: "awaiting_authorization",
1726
- message: gateRequest,
1727
- operation,
1728
- scope,
1729
- template: template || null,
1730
- instruction: "STOP HERE. Wait for user response before proceeding with any file operations.",
1731
- }, null, 2)
1732
- }],
1733
- };
1734
- }
1735
- case "herald_predict": {
1736
- const signal = args?.signal;
1737
- const contextStr = args?.context;
1738
- const sessionId = args?.session_id;
1739
- const participant = args?.participant;
1740
- // Convert string context to CEDA's expected array format
1741
- const context = contextStr
1742
- ? [{ type: "user_context", value: contextStr, source: "herald" }]
1743
- : undefined;
1744
- const result = await callCedaAPI("/api/predict", "POST", {
1745
- input: signal,
1746
- context,
1747
- sessionId,
1748
- participant,
1749
- config: { enableAutoFix: true, maxAutoFixAttempts: 3 },
1750
- });
1751
- return {
1752
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1753
- };
1754
- }
1755
- case "herald_refine": {
1756
- const sessionId = args?.session_id;
1757
- const refinement = args?.refinement;
1758
- const contextStr = args?.context;
1759
- const participant = args?.participant;
1760
- // Convert string context to CEDA's expected array format
1761
- const context = contextStr
1762
- ? [{ type: "user_context", value: contextStr, source: "herald" }]
1763
- : undefined;
1764
- const result = await callCedaAPI("/api/refine", "POST", {
1765
- sessionId,
1766
- refinement,
1767
- context,
1768
- participant,
1769
- });
1770
- return {
1771
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1772
- };
1773
- }
1774
- case "herald_session": {
1775
- const sessionId = args?.session_id;
1776
- const result = await callCedaAPI(`/api/session/${sessionId}`);
1777
- return {
1778
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1779
- };
1780
- }
1781
- case "herald_feedback": {
1782
- const sessionId = args?.session_id;
1783
- const accepted = args?.accepted;
1784
- const comment = args?.comment;
1785
- const result = await callCedaAPI("/api/feedback", "POST", {
1786
- sessionId,
1787
- accepted,
1788
- comment,
1789
- });
1790
- return {
1791
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1792
- };
1793
- }
1794
- case "herald_context_status": {
1795
- const vault = args?.vault;
1796
- if (OFFSPRING_CLOUD_MODE) {
1797
- const endpoint = vault ? `/api/herald/contexts?vault=${vault}` : "/api/herald/contexts";
1798
- const result = await callCedaAPI(endpoint);
1799
- return {
1800
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1801
- };
1802
- }
1803
- // Local mode - read from files
1804
- const vaults = vault ? [vault] : ["spilno", "goprint", "disrupt"];
1805
- const statuses = {};
1806
- for (const v of vaults) {
1807
- const statusPath = join(AEGIS_OFFSPRING_PATH, v, "_status.md");
1808
- if (existsSync(statusPath)) {
1809
- statuses[v] = readFileSync(statusPath, "utf-8");
1810
- }
1811
- }
1812
- return {
1813
- content: [{ type: "text", text: JSON.stringify(statuses, null, 2) }],
1814
- };
1815
- }
1816
- case "herald_share_insight": {
1817
- const insight = args?.insight;
1818
- const targetVault = args?.target_vault;
1819
- const topic = args?.topic;
1820
- const payload = {
1821
- insight,
1822
- topic,
1823
- targetVault,
1824
- sourceVault: HERALD_VAULT || undefined,
1825
- company: HERALD_COMPANY,
1826
- project: HERALD_PROJECT,
1827
- user: HERALD_USER,
1828
- };
1829
- // Cloud-first: try to POST to CEDA, buffer locally on failure
1830
- // Map Herald's vault terminology to CEDA's context terminology
1831
- // Default toContext to "all" for guest mode / when no target specified
1832
- try {
1833
- const result = await callCedaAPI("/api/herald/insight", "POST", {
1834
- insight,
1835
- toContext: targetVault || "all", // Required by CEDA, default to broadcast
1836
- topic,
1837
- fromContext: HERALD_VAULT || `${HERALD_COMPANY}/${HERALD_PROJECT}`,
1838
- });
1839
- // Check if API returned an error
1840
- if (result.error) {
1841
- bufferInsight(payload);
1842
- return {
1843
- content: [{
1844
- type: "text",
1845
- text: JSON.stringify({
1846
- success: true,
1847
- mode: "local",
1848
- message: "Insight buffered locally (cloud returned error)",
1849
- error: result.error,
1850
- bufferSize: getBufferedInsights().length,
1851
- hint: "Use herald_sync to flush buffer when cloud recovers",
1852
- }, null, 2)
1853
- }],
1854
- };
1855
- }
1856
- return {
1857
- content: [{
1858
- type: "text",
1859
- text: JSON.stringify({
1860
- ...result,
1861
- mode: "cloud",
1862
- }, null, 2)
1863
- }],
1864
- };
1865
- }
1866
- catch (error) {
1867
- // Cloud unavailable - buffer locally
1868
- bufferInsight(payload);
1869
- return {
1870
- content: [{
1871
- type: "text",
1872
- text: JSON.stringify({
1873
- success: true,
1874
- mode: "local",
1875
- message: "Insight buffered locally (cloud unavailable)",
1876
- bufferSize: getBufferedInsights().length,
1877
- hint: "Use herald_sync to flush buffer when cloud recovers",
1878
- }, null, 2)
1879
- }],
1880
- };
1881
- }
1882
- }
1883
- case "herald_sync": {
1884
- const dryRun = args?.dry_run;
1885
- const buffer = getBufferedInsights();
1886
- if (buffer.length === 0) {
1887
- return {
1888
- content: [{
1889
- type: "text",
1890
- text: JSON.stringify({
1891
- success: true,
1892
- message: "Buffer empty, nothing to sync",
1893
- synced: 0,
1894
- }, null, 2)
1895
- }],
1896
- };
1897
- }
1898
- if (dryRun) {
1899
- return {
1900
- content: [{
1901
- type: "text",
1902
- text: JSON.stringify({
1903
- dryRun: true,
1904
- wouldSync: buffer.length,
1905
- insights: buffer.map(b => ({
1906
- topic: b.topic,
1907
- insight: b.insight.substring(0, 100) + (b.insight.length > 100 ? "..." : ""),
1908
- bufferedAt: b.bufferedAt,
1909
- })),
1910
- }, null, 2)
1911
- }],
1912
- };
1913
- }
1914
- const synced = [];
1915
- const failed = [];
1916
- for (const item of buffer) {
1917
- try {
1918
- const result = await callCedaAPI("/api/herald/insight", "POST", {
1919
- insight: item.insight,
1920
- topic: item.topic,
1921
- toContext: item.targetVault || "all", // CEDA expects toContext, default to "all"
1922
- fromContext: item.sourceVault, // CEDA expects fromContext
1923
- });
1924
- if (result.error) {
1925
- failed.push(item);
1926
- }
1927
- else {
1928
- synced.push(item);
1929
- }
1930
- }
1931
- catch {
1932
- failed.push(item);
1933
- }
1934
- }
1935
- // Save only failed items back to buffer
1936
- saveFailedInsights(failed);
1937
- return {
1938
- content: [{
1939
- type: "text",
1940
- text: JSON.stringify({
1941
- success: true,
1942
- message: failed.length === 0 ? "All insights synced to CEDA" : "Partial sync completed",
1943
- synced: synced.length,
1944
- failed: failed.length,
1945
- remainingBuffer: failed.length,
1946
- }, null, 2)
1947
- }],
1948
- };
1949
- }
1950
- case "herald_query_insights": {
1951
- const topic = args?.topic;
1952
- const vault = args?.vault;
1953
- if (OFFSPRING_CLOUD_MODE) {
1954
- const endpoint = vault
1955
- ? `/api/herald/insights?topic=${encodeURIComponent(topic)}&vault=${vault}`
1956
- : `/api/herald/insights?topic=${encodeURIComponent(topic)}`;
1957
- const result = await callCedaAPI(endpoint);
1958
- return {
1959
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1960
- };
1961
- }
1962
- return {
1963
- content: [{ type: "text", text: JSON.stringify({ insights: [], message: "Local mode - no shared insights" }, null, 2) }],
1964
- };
1965
- }
1966
- // CEDA-49: Session Management Tools
1967
- case "herald_session_list": {
1968
- const company = args?.company;
1969
- const project = args?.project;
1970
- const user = args?.user;
1971
- const status = args?.status;
1972
- const limit = args?.limit;
1973
- const params = new URLSearchParams();
1974
- params.set("company", company || HERALD_COMPANY);
1975
- if (project)
1976
- params.set("project", project);
1977
- if (user)
1978
- params.set("user", user);
1979
- if (status)
1980
- params.set("status", status);
1981
- if (limit)
1982
- params.set("limit", String(limit));
1983
- const result = await callCedaAPI(`/api/sessions?${params.toString()}`);
1984
- return {
1985
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1986
- };
1987
- }
1988
- case "herald_session_get": {
1989
- const sessionId = args?.session_id;
1990
- const result = await callCedaAPI(`/api/session/${sessionId}`);
1991
- return {
1992
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1993
- };
1994
- }
1995
- case "herald_session_history": {
1996
- const sessionId = args?.session_id;
1997
- const limit = args?.limit;
1998
- let endpoint = `/api/session/${sessionId}/history`;
1999
- if (limit) {
2000
- endpoint += `?limit=${limit}`;
2001
- }
2002
- const result = await callCedaAPI(endpoint);
2003
- return {
2004
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2005
- };
2006
- }
2007
- case "herald_session_rollback": {
2008
- const sessionId = args?.session_id;
2009
- const version = args?.version;
2010
- const result = await callCedaAPI(`/api/session/${sessionId}/rollback?version=${version}`, "POST");
2011
- return {
2012
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2013
- };
2014
- }
2015
- case "herald_session_archive": {
2016
- const sessionId = args?.session_id;
2017
- const result = await callCedaAPI(`/api/session/${sessionId}`, "PUT", {
2018
- status: "archived",
2019
- });
2020
- return {
2021
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2022
- };
2023
- }
2024
- case "herald_reflect": {
2025
- const session = args?.session;
2026
- const feeling = args?.feeling;
2027
- const insight = args?.insight;
2028
- const dryRun = args?.dry_run;
2029
- // CEDA-65: Client-side sanitization preview (no network required)
2030
- const sessionPreview = previewSanitization(session);
2031
- const insightPreview = previewSanitization(insight);
2032
- // Check if content would be blocked
2033
- if (sessionPreview.wouldBlock || insightPreview.wouldBlock) {
2034
- return {
2035
- content: [{
2036
- type: "text",
2037
- text: JSON.stringify({
2038
- success: false,
2039
- mode: "blocked",
2040
- error: "Content contains restricted data that cannot be transmitted",
2041
- blockReason: sessionPreview.blockReason || insightPreview.blockReason,
2042
- detectedTypes: [...sessionPreview.detectedTypes, ...insightPreview.detectedTypes],
2043
- hint: "Remove private keys, AWS credentials, or other restricted data before capturing.",
2044
- }, null, 2)
2045
- }],
2046
- isError: true,
2047
- };
2048
- }
2049
- // Dry-run mode - show sanitization preview without storing
2050
- if (dryRun) {
2051
- return {
2052
- content: [{
2053
- type: "text",
2054
- text: JSON.stringify({
2055
- success: true,
2056
- mode: "dry-run",
2057
- message: "Preview of what would be captured (no data stored or transmitted)",
2058
- feeling,
2059
- sanitization: {
2060
- session: {
2061
- original: session,
2062
- sanitized: sessionPreview.sanitized,
2063
- wouldSanitize: sessionPreview.wouldSanitize,
2064
- detectedTypes: sessionPreview.detectedTypes,
2065
- classification: sessionPreview.classification,
2066
- },
2067
- insight: {
2068
- original: insight,
2069
- sanitized: insightPreview.sanitized,
2070
- wouldSanitize: insightPreview.wouldSanitize,
2071
- detectedTypes: insightPreview.detectedTypes,
2072
- classification: insightPreview.classification,
2073
- },
2074
- },
2075
- hint: insightPreview.wouldSanitize || sessionPreview.wouldSanitize
2076
- ? "Some content will be redacted. Consider using more abstract descriptions."
2077
- : "Content looks clean. Safe to capture.",
2078
- }, null, 2)
2079
- }],
2080
- };
2081
- }
2082
- // Sanitize before transmission
2083
- const sanitizedSession = sessionPreview.sanitized;
2084
- const sanitizedInsight = insightPreview.sanitized;
2085
- // CEDA-64: Track reflection locally for session summary
2086
- addSessionReflection({
2087
- session,
2088
- feeling,
2089
- insight,
2090
- method: "direct",
2091
- });
2092
- // Call CEDA's reflect endpoint with SANITIZED insight
2093
- try {
2094
- const result = await callCedaAPI("/api/herald/reflect", "POST", {
2095
- session: sanitizedSession,
2096
- feeling,
2097
- insight: sanitizedInsight, // Sanitized - no PII/secrets transmitted
2098
- method: "direct", // Track capture method for meta-learning
2099
- company: HERALD_COMPANY,
2100
- project: HERALD_PROJECT,
2101
- user: HERALD_USER,
2102
- vault: HERALD_VAULT || undefined,
2103
- });
2104
- if (result.error) {
2105
- // If cloud fails, store locally for later processing (also sanitized)
2106
- bufferInsight({
2107
- insight: `[REFLECT:${feeling}] ${sanitizedInsight} | Context: ${sanitizedSession}`,
2108
- topic: feeling === "stuck" ? "antipattern" : "pattern",
2109
- company: HERALD_COMPANY,
2110
- project: HERALD_PROJECT,
2111
- user: HERALD_USER,
2112
- });
2113
- return {
2114
- content: [{
2115
- type: "text",
2116
- text: JSON.stringify({
2117
- success: true,
2118
- mode: "local",
2119
- message: "Reflection buffered locally (cloud unavailable)",
2120
- feeling,
2121
- insight,
2122
- hint: "CEDA will process this when synced. Use herald_sync to flush buffer.",
2123
- buffered: true,
2124
- }, null, 2)
2125
- }],
2126
- };
2127
- }
2128
- // Cloud processed successfully
2129
- return {
2130
- content: [{
2131
- type: "text",
2132
- text: JSON.stringify({
2133
- success: true,
2134
- mode: "cloud",
2135
- feeling,
2136
- insight,
2137
- message: feeling === "stuck"
2138
- ? `Antipattern captured: "${insight}"`
2139
- : `Pattern captured: "${insight}"`,
2140
- context: {
2141
- company: HERALD_COMPANY,
2142
- project: HERALD_PROJECT,
2143
- tags: HERALD_TAGS,
2144
- trust: TRUST_LEVEL,
2145
- propagates: PROPAGATES,
2146
- },
2147
- ...result,
2148
- }, null, 2)
2149
- }],
2150
- };
2151
- }
2152
- catch (error) {
2153
- // Network error - buffer locally (sanitized)
2154
- bufferInsight({
2155
- insight: `[REFLECT:${feeling}] ${sanitizedInsight} | Context: ${sanitizedSession}`,
2156
- topic: feeling === "stuck" ? "antipattern" : "pattern",
2157
- company: HERALD_COMPANY,
2158
- project: HERALD_PROJECT,
2159
- user: HERALD_USER,
2160
- });
2161
- return {
2162
- content: [{
2163
- type: "text",
2164
- text: JSON.stringify({
2165
- success: true,
2166
- mode: "local",
2167
- message: "Reflection buffered locally (cloud unreachable)",
2168
- feeling,
2169
- insight: sanitizedInsight,
2170
- hint: "Use herald_sync when cloud recovers.",
2171
- buffered: true,
2172
- sanitized: sessionPreview.wouldSanitize || insightPreview.wouldSanitize,
2173
- }, null, 2)
2174
- }],
2175
- };
2176
- }
2177
- }
2178
- case "herald_patterns": {
2179
- // Query learned patterns with inheritance: user → project → company
2180
- // More specific patterns take precedence over broader ones
2181
- try {
2182
- // Helper to dedupe patterns by insight text (first occurrence wins)
2183
- const seenInsights = new Set();
2184
- const dedupePatterns = (items, scope) => {
2185
- return items.filter(item => {
2186
- const key = item.insight.toLowerCase().trim();
2187
- if (seenInsights.has(key))
2188
- return false;
2189
- seenInsights.add(key);
2190
- return true;
2191
- }).map(item => ({ ...item, scope }));
2192
- };
2193
- // Cascade queries: user (most specific) → project → company (broadest)
2194
- const queries = [
2195
- { scope: "user", url: `/api/herald/reflections?company=${HERALD_COMPANY}&project=${HERALD_PROJECT}&user=${HERALD_USER}&limit=100` },
2196
- { scope: "project", url: `/api/herald/reflections?company=${HERALD_COMPANY}&project=${HERALD_PROJECT}&limit=100` },
2197
- { scope: "company", url: `/api/herald/reflections?company=${HERALD_COMPANY}&limit=100` },
2198
- ];
2199
- const patterns = [];
2200
- const antipatterns = [];
2201
- // Query each level, dedupe as we go (user patterns win over project, project over company)
2202
- for (const { scope, url } of queries) {
2203
- try {
2204
- const result = await callCedaAPI(url);
2205
- const scopePatterns = result.patterns || [];
2206
- const scopeAntipatterns = result.antipatterns || [];
2207
- patterns.push(...dedupePatterns(scopePatterns, scope));
2208
- antipatterns.push(...dedupePatterns(scopeAntipatterns, scope));
2209
- }
2210
- catch {
2211
- // Continue if a level fails (e.g., user not set)
2212
- }
2213
- }
2214
- const metaResult = await callCedaAPI("/api/herald/meta-patterns");
2215
- const metaPatterns = metaResult.metaPatterns || [];
2216
- // Build readable summary with scope indicators
2217
- let summary = `## Learned Patterns for ${HERALD_USER}→${HERALD_PROJECT}→${HERALD_COMPANY}\n\n`;
2218
- if (antipatterns.length > 0) {
2219
- summary += `### ⚠️ Antipatterns (avoid these)\n`;
2220
- antipatterns.forEach((ap, i) => {
2221
- const scopeTag = ap.scope ? ` [${ap.scope}]` : "";
2222
- summary += `${i + 1}. ${ap.insight}${scopeTag}`;
2223
- if (ap.warning)
2224
- summary += `\n → ${ap.warning}`;
2225
- summary += `\n`;
2226
- });
2227
- summary += `\n`;
2228
- }
2229
- if (patterns.length > 0) {
2230
- summary += `### ✓ Patterns (do these)\n`;
2231
- patterns.forEach((p, i) => {
2232
- const scopeTag = p.scope ? ` [${p.scope}]` : "";
2233
- summary += `${i + 1}. ${p.insight}${scopeTag}`;
2234
- if (p.reinforcement)
2235
- summary += `\n → ${p.reinforcement}`;
2236
- summary += `\n`;
2237
- });
2238
- summary += `\n`;
2239
- }
2240
- if (metaPatterns.length > 0) {
2241
- const meta = metaPatterns[0];
2242
- summary += `### Meta-learning\n`;
2243
- summary += `Recommended capture method: ${meta.recommendedMethod} (${(meta.confidence * 100).toFixed(0)}% confidence)\n`;
2244
- }
2245
- if (patterns.length === 0 && antipatterns.length === 0) {
2246
- summary = `No patterns learned yet for ${HERALD_USER}→${HERALD_PROJECT}→${HERALD_COMPANY}.\n\nCapture patterns with "herald reflect" or "herald simulate" when you notice friction or flow.`;
2247
- }
2248
- return {
2249
- content: [{
2250
- type: "text",
2251
- text: summary,
2252
- }],
2253
- };
2254
- }
2255
- catch (error) {
2256
- return {
2257
- content: [{
2258
- type: "text",
2259
- text: `Failed to query patterns: ${error}\n\nCEDA may be unavailable.`,
2260
- }],
2261
- };
2262
- }
2263
- }
2264
- case "herald_simulate": {
2265
- const session = args?.session;
2266
- const feeling = args?.feeling;
2267
- const insight = args?.insight;
2268
- // CEDA-65: Client-side sanitization
2269
- const simSessionPreview = previewSanitization(session);
2270
- const simInsightPreview = previewSanitization(insight);
2271
- // Block restricted content
2272
- if (simSessionPreview.wouldBlock || simInsightPreview.wouldBlock) {
2273
- return {
2274
- content: [{
2275
- type: "text",
2276
- text: JSON.stringify({
2277
- success: false,
2278
- mode: "blocked",
2279
- error: "Content contains restricted data that cannot be transmitted",
2280
- blockReason: simSessionPreview.blockReason || simInsightPreview.blockReason,
2281
- hint: "Remove private keys, AWS credentials, or other restricted data before capturing.",
2282
- }, null, 2)
2283
- }],
2284
- isError: true,
2285
- };
2286
- }
2287
- const simSanitizedSession = simSessionPreview.sanitized;
2288
- const simSanitizedInsight = simInsightPreview.sanitized;
2289
- // CEDA-64: Track reflection locally for session summary
2290
- addSessionReflection({
2291
- session,
2292
- feeling,
2293
- insight,
2294
- method: "simulation",
2295
- });
2296
- // Check for AI API key
2297
- const aiClient = getAIClient();
2298
- if (!aiClient) {
2299
- return {
2300
- content: [{
2301
- type: "text",
2302
- text: JSON.stringify({
2303
- success: false,
2304
- error: "No AI key configured",
2305
- hint: "Add ANTHROPIC_API_KEY or OPENAI_API_KEY to env in .claude/settings.local.json",
2306
- fallback: "Use herald_reflect for direct capture instead",
2307
- }, null, 2)
2308
- }],
2309
- };
2310
- }
2311
- try {
2312
- // Build prompt and call AI for reflection (use sanitized input)
2313
- const prompt = buildReflectionPrompt(simSanitizedSession, feeling, simSanitizedInsight);
2314
- const extracted = await callAIForReflection(aiClient, prompt);
2315
- // Sanitize AI-extracted fields too
2316
- const sanitizedSignal = sanitize(extracted.signal || "").sanitizedText;
2317
- const sanitizedReinforcement = extracted.reinforcement ? sanitize(extracted.reinforcement).sanitizedText : undefined;
2318
- const sanitizedWarning = extracted.warning ? sanitize(extracted.warning).sanitizedText : undefined;
2319
- // Send enriched data to CEDA (all sanitized)
2320
- const result = await callCedaAPI("/api/herald/reflect", "POST", {
2321
- session: simSanitizedSession,
2322
- feeling,
2323
- insight: simSanitizedInsight,
2324
- method: "simulation", // Track capture method
2325
- // AI-extracted fields (sanitized)
2326
- signal: sanitizedSignal,
2327
- outcome: extracted.outcome,
2328
- reinforcement: sanitizedReinforcement,
2329
- warning: sanitizedWarning,
2330
- company: HERALD_COMPANY,
2331
- project: HERALD_PROJECT,
2332
- user: HERALD_USER,
2333
- vault: HERALD_VAULT || undefined,
2334
- });
2335
- if (result.error) {
2336
- // Cloud failed but we have AI extraction - buffer with enriched data (sanitized)
2337
- bufferInsight({
2338
- insight: `[SIMULATE:${feeling}] Signal: ${sanitizedSignal} | Insight: ${simSanitizedInsight} | ${extracted.outcome === "pattern" ? `Reinforce: ${sanitizedReinforcement}` : `Warn: ${sanitizedWarning}`}`,
2339
- topic: extracted.outcome,
2340
- company: HERALD_COMPANY,
2341
- project: HERALD_PROJECT,
2342
- user: HERALD_USER,
2343
- });
2344
- return {
2345
- content: [{
2346
- type: "text",
2347
- text: JSON.stringify({
2348
- success: true,
2349
- mode: "local",
2350
- method: "simulation",
2351
- message: "AI reflection complete, buffered locally (cloud unavailable)",
2352
- extracted: {
2353
- signal: extracted.signal,
2354
- outcome: extracted.outcome,
2355
- reinforcement: extracted.reinforcement,
2356
- warning: extracted.warning,
2357
- },
2358
- hint: "Use herald_sync to flush to CEDA when cloud recovers",
2359
- }, null, 2)
2360
- }],
2361
- };
2362
- }
2363
- // Success - AI reflection sent to CEDA
2364
- return {
2365
- content: [{
2366
- type: "text",
2367
- text: JSON.stringify({
2368
- success: true,
2369
- mode: "cloud",
2370
- method: "simulation",
2371
- provider: aiClient.provider,
2372
- message: extracted.outcome === "pattern"
2373
- ? `Pattern extracted via AI reflection`
2374
- : `Antipattern extracted via AI reflection`,
2375
- extracted: {
2376
- signal: extracted.signal,
2377
- outcome: extracted.outcome,
2378
- reinforcement: extracted.reinforcement,
2379
- warning: extracted.warning,
2380
- },
2381
- insight,
2382
- ...result,
2383
- }, null, 2)
2384
- }],
2385
- };
2386
- }
2387
- catch (error) {
2388
- // AI call failed
2389
- return {
2390
- content: [{
2391
- type: "text",
2392
- text: JSON.stringify({
2393
- success: false,
2394
- error: `AI reflection failed: ${error}`,
2395
- provider: aiClient.provider,
2396
- hint: "Check API key validity. Use herald_reflect for direct capture as fallback.",
2397
- }, null, 2)
2398
- }],
2399
- };
2400
- }
2401
- }
2402
- // CEDA-64: Herald Command Extensions - Handlers
2403
- case "herald_session_reflections": {
2404
- const summary = getSessionReflectionsSummary();
2405
- let message = `## Session Reflections Summary\n\n`;
2406
- message += `**Total captured:** ${summary.count}\n`;
2407
- message += `- Patterns (success): ${summary.patterns}\n`;
2408
- message += `- Antipatterns (stuck): ${summary.antipatterns}\n\n`;
2409
- if (summary.reflections.length > 0) {
2410
- message += `### Captured This Session:\n`;
2411
- summary.reflections.forEach((r, i) => {
2412
- const icon = r.feeling === "success" ? "+" : "-";
2413
- message += `${i + 1}. [${icon}] ${r.insight} (${r.method}, ${r.timestamp})\n`;
2414
- });
2415
- }
2416
- else {
2417
- message += `No reflections captured yet. Use herald_reflect or herald_simulate to capture patterns.`;
2418
- }
2419
- return {
2420
- content: [{
2421
- type: "text",
2422
- text: message,
2423
- }],
2424
- };
2425
- }
2426
- case "herald_pattern_feedback": {
2427
- const patternId = args?.pattern_id;
2428
- const patternText = args?.pattern_text;
2429
- const outcome = args?.outcome;
2430
- if (!patternId && !patternText) {
2431
- return {
2432
- content: [{
2433
- type: "text",
2434
- text: JSON.stringify({
2435
- success: false,
2436
- error: "Either pattern_id or pattern_text is required",
2437
- }, null, 2)
2438
- }],
2439
- isError: true,
2440
- };
2441
- }
2442
- try {
2443
- const result = await callCedaAPI("/api/herald/feedback", "POST", {
2444
- patternId,
2445
- patternText,
2446
- outcome,
2447
- helped: outcome === "helped",
2448
- company: HERALD_COMPANY,
2449
- project: HERALD_PROJECT,
2450
- user: HERALD_USER,
2451
- });
2452
- if (result.error) {
2453
- return {
2454
- content: [{
2455
- type: "text",
2456
- text: JSON.stringify({
2457
- success: false,
2458
- error: result.error,
2459
- hint: "Pattern feedback could not be recorded. The pattern may not exist.",
2460
- }, null, 2)
2461
- }],
2462
- };
2463
- }
2464
- return {
2465
- content: [{
2466
- type: "text",
2467
- text: JSON.stringify({
2468
- success: true,
2469
- message: outcome === "helped"
2470
- ? "Feedback recorded: pattern helped! This reinforces the pattern."
2471
- : "Feedback recorded: pattern didn't help. This will be factored into future recommendations.",
2472
- ...result,
2473
- }, null, 2)
2474
- }],
2475
- };
2476
- }
2477
- catch (error) {
2478
- return {
2479
- content: [{
2480
- type: "text",
2481
- text: JSON.stringify({
2482
- success: false,
2483
- error: `Failed to record feedback: ${error}`,
2484
- hint: "CEDA may be unavailable. Try again later.",
2485
- }, null, 2)
2486
- }],
2487
- };
2488
- }
2489
- }
2490
- case "herald_share_scoped": {
2491
- const insight = args?.insight;
2492
- const scope = args?.scope;
2493
- const topic = args?.topic;
2494
- try {
2495
- const result = await callCedaAPI("/api/herald/share", "POST", {
2496
- insight,
2497
- scope,
2498
- topic,
2499
- sourceCompany: HERALD_COMPANY,
2500
- sourceProject: HERALD_PROJECT,
2501
- sourceUser: HERALD_USER,
2502
- sourceVault: HERALD_VAULT || undefined,
2503
- });
2504
- if (result.error) {
2505
- return {
2506
- content: [{
2507
- type: "text",
2508
- text: JSON.stringify({
2509
- success: false,
2510
- error: result.error,
2511
- hint: "Insight could not be shared. Check scope and try again.",
2512
- }, null, 2)
2513
- }],
2514
- };
2515
- }
2516
- const scopeDescription = {
2517
- parent: "parent project/company",
2518
- siblings: "sibling projects",
2519
- all: "all contexts globally",
2520
- };
2521
- return {
2522
- content: [{
2523
- type: "text",
2524
- text: JSON.stringify({
2525
- success: true,
2526
- message: `Insight shared with ${scopeDescription[scope]}`,
2527
- scope,
2528
- topic: topic || "general",
2529
- ...result,
2530
- }, null, 2)
2531
- }],
2532
- };
2533
- }
2534
- catch (error) {
2535
- return {
2536
- content: [{
2537
- type: "text",
2538
- text: JSON.stringify({
2539
- success: false,
2540
- error: `Failed to share insight: ${error}`,
2541
- hint: "CEDA may be unavailable. Try again later.",
2542
- }, null, 2)
2543
- }],
2544
- };
2545
- }
2546
- }
2547
- // CEDA-65: GDPR Compliance Tools - Handlers
2548
- case "herald_forget": {
2549
- const patternId = args?.pattern_id;
2550
- const sessionId = args?.session_id;
2551
- const deleteAll = args?.all;
2552
- if (!patternId && !sessionId && !deleteAll) {
2553
- return {
2554
- content: [{
2555
- type: "text",
2556
- text: JSON.stringify({
2557
- success: false,
2558
- error: "At least one parameter required: pattern_id, session_id, or all",
2559
- hint: "Specify what data to delete",
2560
- }, null, 2)
2561
- }],
2562
- isError: true,
2563
- };
2564
- }
2565
- try {
2566
- const result = await callCedaAPI("/api/herald/forget", "DELETE", {
2567
- patternId,
2568
- sessionId,
2569
- all: deleteAll,
2570
- company: HERALD_COMPANY,
2571
- project: HERALD_PROJECT,
2572
- user: HERALD_USER,
2573
- });
2574
- if (result.error) {
2575
- return {
2576
- content: [{
2577
- type: "text",
2578
- text: JSON.stringify({
2579
- success: false,
2580
- error: result.error,
2581
- hint: "Data deletion failed. Check parameters and try again.",
2582
- }, null, 2)
2583
- }],
2584
- };
2585
- }
2586
- let message = "Data deleted successfully (GDPR Art. 17)";
2587
- if (patternId) {
2588
- message = `Pattern ${patternId} deleted`;
2589
- }
2590
- else if (sessionId) {
2591
- message = `All patterns from session ${sessionId} deleted`;
2592
- }
2593
- else if (deleteAll) {
2594
- message = `All patterns for ${HERALD_COMPANY}/${HERALD_PROJECT}/${HERALD_USER} deleted`;
2595
- }
2596
- return {
2597
- content: [{
2598
- type: "text",
2599
- text: JSON.stringify({
2600
- success: true,
2601
- message,
2602
- gdprArticle: "Article 17 - Right to Erasure",
2603
- ...result,
2604
- }, null, 2)
2605
- }],
2606
- };
2607
- }
2608
- catch (error) {
2609
- return {
2610
- content: [{
2611
- type: "text",
2612
- text: JSON.stringify({
2613
- success: false,
2614
- error: `Failed to delete data: ${error}`,
2615
- hint: "CEDA may be unavailable. Try again later.",
2616
- }, null, 2)
2617
- }],
2618
- };
2619
- }
2620
- }
2621
- case "herald_export": {
2622
- const format = args?.format || "json";
2623
- try {
2624
- const result = await callCedaAPI(`/api/herald/export?company=${HERALD_COMPANY}&project=${HERALD_PROJECT}&user=${HERALD_USER}&format=${format}`);
2625
- if (result.error) {
2626
- return {
2627
- content: [{
2628
- type: "text",
2629
- text: JSON.stringify({
2630
- success: false,
2631
- error: result.error,
2632
- hint: "Data export failed. Try again later.",
2633
- }, null, 2)
2634
- }],
2635
- };
2636
- }
2637
- return {
2638
- content: [{
2639
- type: "text",
2640
- text: JSON.stringify({
2641
- success: true,
2642
- message: `Data exported in ${format.toUpperCase()} format (GDPR Art. 20)`,
2643
- gdprArticle: "Article 20 - Right to Data Portability",
2644
- format,
2645
- context: `${HERALD_COMPANY}/${HERALD_PROJECT}/${HERALD_USER}`,
2646
- ...result,
2647
- }, null, 2)
2648
- }],
2649
- };
2650
- }
2651
- catch (error) {
2652
- return {
2653
- content: [{
2654
- type: "text",
2655
- text: JSON.stringify({
2656
- success: false,
2657
- error: `Failed to export data: ${error}`,
2658
- hint: "CEDA may be unavailable. Try again later.",
2659
- }, null, 2)
2660
- }],
2661
- };
2662
- }
2663
- }
2664
- default:
2665
- return {
2666
- content: [{ type: "text", text: `Unknown tool: ${name}` }],
2667
- isError: true,
2668
- };
2669
- }
2670
- }
2671
- catch (error) {
2672
- return {
2673
- content: [{ type: "text", text: `Error: ${error}` }],
2674
- isError: true,
2675
- };
2676
- }
2677
- });
2678
- async function autoSyncBuffer() {
2679
- if (!AUTO_SYNC_ON_STARTUP)
2680
- return;
2681
- const buffer = getBufferedInsights();
2682
- if (buffer.length === 0)
2683
- return;
2684
- console.error(`[Herald] Auto-syncing ${buffer.length} buffered insight(s)...`);
2685
- const synced = [];
2686
- const failed = [];
2687
- for (const item of buffer) {
2688
- try {
2689
- const result = await callCedaAPI("/api/herald/insight", "POST", {
2690
- insight: item.insight,
2691
- topic: item.topic,
2692
- toContext: item.targetVault || "all", // CEDA expects toContext
2693
- fromContext: item.sourceVault, // CEDA expects fromContext
2694
- });
2695
- if (result.error) {
2696
- failed.push(item);
2697
- }
2698
- else {
2699
- synced.push(item);
2700
- }
2701
- }
2702
- catch {
2703
- failed.push(item);
2704
- }
2705
- }
2706
- saveFailedInsights(failed);
2707
- if (synced.length > 0) {
2708
- console.error(`[Herald] Synced ${synced.length} insight(s) to cloud`);
2709
- }
2710
- if (failed.length > 0) {
2711
- console.error(`[Herald] ${failed.length} insight(s) failed - will retry on next startup`);
2712
- }
2713
- }
2714
- /**
2715
- * CEDA-82: Verify trust with CEDA server
2716
- * If user registered via GitHub OAuth and has access to current repo,
2717
- * CEDA returns verified context with HIGH trust.
2718
- * Otherwise, trust remains as locally detected.
2719
- */
2720
- async function verifyWithCeda() {
2721
- // Only verify if we have a git remote (potential HIGH trust)
2722
- if (!GIT_REMOTE) {
2723
- console.error(`[Herald] No git remote - skipping verification (trust: ${TRUST_LEVEL})`);
2724
- return;
2725
- }
2726
- try {
2727
- const result = await callCedaAPI("/api/auth/verify", "POST", {
2728
- gitRemote: GIT_REMOTE,
2729
- user: HERALD_USER,
2730
- });
2731
- if (result.verified === true && result.context) {
2732
- // Server verified - user has access to this repo
2733
- const ctx = result.context;
2734
- VERIFIED_CONTEXT = {
2735
- verified: true,
2736
- company: ctx.company,
2737
- project: ctx.project,
2738
- trust: ctx.trust,
2739
- tags: ctx.tags,
2740
- };
2741
- // Upgrade trust to server-verified
2742
- TRUST_LEVEL = VERIFIED_CONTEXT.trust || 'HIGH';
2743
- PROPAGATES = true;
2744
- CONTEXT_SOURCE = 'verified';
2745
- console.error(`[Herald] Verified with CEDA: ${VERIFIED_CONTEXT.company}/${VERIFIED_CONTEXT.project} (trust: HIGH)`);
2746
- }
2747
- else {
2748
- // Not verified - user not registered or no access to this repo
2749
- // Keep local trust level (which may be HIGH from git, but unverified)
2750
- const reason = result.error || 'User not registered for this repository';
2751
- console.error(`[Herald] Not verified: ${reason} (trust: ${TRUST_LEVEL}, unverified)`);
2752
- // Optionally downgrade to LOW if strict mode
2753
- if (process.env.HERALD_STRICT_TRUST === 'true') {
2754
- TRUST_LEVEL = 'LOW';
2755
- PROPAGATES = false;
2756
- console.error(`[Herald] Strict mode: downgraded to LOW trust`);
2757
- }
2758
- }
2759
- }
2760
- catch (error) {
2761
- // CEDA unreachable - keep local trust
2762
- console.error(`[Herald] Verification failed (CEDA unreachable): ${error}`);
2763
- console.error(`[Herald] Using local trust: ${TRUST_LEVEL}`);
2764
- }
2765
- }
2766
- async function sendStartupHeartbeat() {
2767
- // Fire-and-forget heartbeat - don't block startup
2768
- try {
2769
- await callCedaAPI("/api/herald/heartbeat", "POST", {
2770
- event: "startup",
2771
- version: VERSION,
2772
- user: HERALD_USER,
2773
- tags: HERALD_TAGS,
2774
- trust: TRUST_LEVEL,
2775
- propagates: PROPAGATES,
2776
- contextSource: CONTEXT_SOURCE,
2777
- gitRemote: GIT_REMOTE,
2778
- platform: process.platform,
2779
- nodeVersion: process.version,
2780
- });
2781
- }
2782
- catch {
2783
- // Silent fail - don't block MCP startup
2784
- }
2785
- }
2786
- async function runMCP() {
2787
- const transport = new StdioServerTransport();
2788
- await server.connect(transport);
2789
- // CEDA-82: Verify trust with CEDA server before announcing
2790
- // This may upgrade trust if user is registered
2791
- await verifyWithCeda();
2792
- console.error(`Herald MCP v${VERSION} running`);
2793
- console.error(`User: ${HERALD_USER} | Tags: [${HERALD_TAGS.join(", ")}]`);
2794
- console.error(`Trust: ${TRUST_LEVEL} (${CONTEXT_SOURCE})${PROPAGATES ? " | Propagates: YES" : ""}`);
2795
- if (VERIFIED_CONTEXT?.verified) {
2796
- console.error(`Context: ${VERIFIED_CONTEXT.company}/${VERIFIED_CONTEXT.project} (server-verified)`);
2797
- }
2798
- // Send startup heartbeat for visibility (non-blocking)
2799
- sendStartupHeartbeat();
2800
- // Auto-sync buffered insights on startup
2801
- await autoSyncBuffer();
2802
- }
2803
- // ============================================
2804
- // ENTRY POINT - Detect mode
2805
- // ============================================
2806
- async function main() {
2807
- const args = process.argv.slice(2);
2808
- // If we have CLI arguments, run CLI mode
2809
- if (args.length > 0) {
2810
- await runCLI(args);
2811
- return;
2812
- }
2813
- // If stdin is a TTY (human at terminal), show help
2814
- if (process.stdin.isTTY) {
2815
- printUsage();
2816
- return;
2817
- }
2818
- // Otherwise, run MCP server (AI agent calling via pipe)
2819
- await runMCP();
2820
- }
2821
- main().catch(console.error);
2822
- //# sourceMappingURL=index.js.map