@spilno/herald-mcp 1.1.3 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,62 +2,121 @@
2
2
 
3
3
  Herald MCP - AI-native interface to [CEDA](https://getceda.com) (Cognitive Event-Driven Architecture).
4
4
 
5
- ## Installation
5
+ **Dual-mode**: Natural chat for humans, MCP for AI agents.
6
6
 
7
- ```bash
8
- # Run directly (no install)
9
- npx @spilno/herald-mcp
7
+ ## Quick Start
10
8
 
11
- # Or install globally
9
+ ```bash
12
10
  npm install -g @spilno/herald-mcp
13
- herald-mcp
11
+
12
+ export HERALD_API_URL=https://getceda.com
13
+
14
+ herald-mcp chat
15
+ ```
16
+
17
+ That's it. Chat mode works out of the box.
18
+
19
+ ## Chat Mode (Natural Conversation)
20
+
21
+ ```bash
22
+ herald-mcp chat
23
+ ```
24
+
25
+ ```
26
+ You: I need a safety assessment module for construction sites
27
+ Herald: I've designed a Safety Assessment module with 4 sections...
28
+
29
+ You: Add OSHA compliance fields
30
+ Herald: Done. Added compliance checklist to Risk Evaluation...
31
+
32
+ You: Looks good, let's use it
33
+ Herald: Great! Module accepted and saved.
14
34
  ```
15
35
 
16
- ## Configuration
36
+ ## Command Mode (Structured)
37
+
38
+ ```bash
39
+ herald-mcp predict "create safety assessment"
40
+ herald-mcp refine "add OSHA compliance"
41
+ herald-mcp resume
42
+ herald-mcp observe yes
43
+ herald-mcp new
44
+ herald-mcp health
45
+ herald-mcp stats
46
+ ```
17
47
 
18
- Set environment variables:
48
+ ## Session Persistence
49
+
50
+ Sessions saved to `~/.herald/session` - resume anytime:
51
+
52
+ ```bash
53
+ # Start today
54
+ herald-mcp predict "create incident report module"
55
+
56
+ # Continue tomorrow
57
+ herald-mcp resume
58
+ herald-mcp refine "add witness statements section"
59
+ ```
60
+
61
+ ## Environment Variables
19
62
 
20
63
  | Variable | Required | Description |
21
64
  |----------|----------|-------------|
22
- | `HERALD_API_URL` | Yes | CEDA server URL (e.g., `https://getceda.com`) |
23
- | `HERALD_API_TOKEN` | No | Bearer token for authentication |
24
- | `HERALD_API_USER` | No | Basic auth username (alternative to token) |
25
- | `HERALD_API_PASS` | No | Basic auth password (alternative to token) |
65
+ | `HERALD_API_URL` | Yes | CEDA server URL |
66
+ | `HERALD_API_TOKEN` | No | Bearer token for auth |
67
+ | `HERALD_COMPANY` | No | Multi-tenant company context |
68
+ | `HERALD_PROJECT` | No | Multi-tenant project context |
69
+ | `HERALD_VAULT` | No | Offspring vault ID (spilno, goprint, disrupt) |
70
+ | `HERALD_OFFSPRING_CLOUD` | No | Set to "true" for cloud mode |
71
+ | `AEGIS_OFFSPRING_PATH` | No | Local path to offspring status files |
26
72
 
27
- ## Tools
73
+ ## Herald Context Sync (v1.3.0+)
28
74
 
29
- Herald exposes 6 MCP tools:
75
+ Herald instances can communicate across contexts, sharing insights and synchronizing status:
30
76
 
31
- | Tool | Description |
32
- |------|-------------|
33
- | `herald_health` | Check CEDA system status |
34
- | `herald_stats` | Get server statistics and loaded patterns |
35
- | `herald_predict` | Generate prediction from natural language input |
36
- | `herald_refine` | Refine existing prediction with additional requirements |
37
- | `herald_session` | Get session history and state |
38
- | `herald_observe` | Record prediction feedback for learning |
77
+ ### Tools
39
78
 
40
- ## Usage with Claude Code
79
+ | Tool | Purpose |
80
+ |------|---------|
81
+ | `herald_context_status` | Read status from Herald contexts across domains |
82
+ | `herald_share_insight` | Share a pattern insight with another context |
83
+ | `herald_query_insights` | Query accumulated insights on a topic |
41
84
 
42
- Add to `~/.claude.json`:
85
+ ### Local Mode (Default)
43
86
 
44
- ```json
45
- {
46
- "mcpServers": {
47
- "herald": {
48
- "command": "npx",
49
- "args": ["@spilno/herald-mcp"],
50
- "env": {
51
- "HERALD_API_URL": "https://getceda.com"
52
- }
53
- }
54
- }
55
- }
87
+ ```bash
88
+ # Context files in local directory
89
+ export AEGIS_OFFSPRING_PATH=~/Documents/aegis_ceda/_offspring
90
+ export HERALD_VAULT=spilno # Which context this Herald serves
91
+
92
+ herald-mcp # MCP mode uses local files
93
+ ```
94
+
95
+ ### Cloud Mode
96
+
97
+ ```bash
98
+ # Use CEDA API for context synchronization
99
+ export HERALD_API_URL=https://getceda.com
100
+ export HERALD_OFFSPRING_CLOUD=true
101
+ export HERALD_VAULT=spilno
102
+
103
+ herald-mcp # MCP mode calls CEDA API
104
+ ```
105
+
106
+ ### Herald-to-Herald Protocol
107
+
108
+ ```
109
+ POST /api/herald/heartbeat # Herald reports its context status
110
+ GET /api/herald/contexts # Herald discovers sibling contexts
111
+ POST /api/herald/insight # Herald shares an insight
112
+ GET /api/herald/insights # Herald queries shared insights
56
113
  ```
57
114
 
58
- ## Usage with Claude Desktop
115
+ ## MCP Mode (for AI Agents)
59
116
 
60
- Add to `claude_desktop_config.json`:
117
+ When piped, Herald speaks JSON-RPC for Claude, Devin, and other AI agents.
118
+
119
+ ### Claude Code (~/.claude.json)
61
120
 
62
121
  ```json
63
122
  {
@@ -73,34 +132,17 @@ Add to `claude_desktop_config.json`:
73
132
  }
74
133
  ```
75
134
 
76
- ## Usage with Devin
135
+ ### Devin MCP Marketplace
77
136
 
78
- In Devin MCP marketplace:
79
137
  - **Server Name**: Herald-CEDA
80
- - **Transport**: STDIO
81
- - **Command**: `npx`
82
- - **Arguments**: `@spilno/herald-mcp`
138
+ - **Command**: `npx @spilno/herald-mcp`
83
139
  - **Environment**: `HERALD_API_URL=https://getceda.com`
84
140
 
85
- ## Multi-turn Conversations
86
-
87
- Herald supports session-based conversations for iterative refinement:
141
+ ## Architecture
88
142
 
89
143
  ```
90
- 1. herald_predict("create assessment module")
91
- Returns sessionId + initial prediction
92
-
93
- 2. herald_refine(sessionId, "add OSHA compliance fields")
94
- → Returns refined prediction
95
-
96
- 3. herald_refine(sessionId, "include corrective actions workflow")
97
- → Returns further refined prediction
98
-
99
- 4. herald_session(sessionId)
100
- → Returns full history of all turns
101
-
102
- 5. herald_observe(sessionId, accepted=true)
103
- → Records feedback for learning
144
+ Human ←→ Herald (Claude voice) ←→ CEDA (cognition)
145
+ Agent ←→ Herald (JSON-RPC) ←→ CEDA
104
146
  ```
105
147
 
106
148
  ## License
package/dist/index.d.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Herald MCP - Diplomatic interface to CEDA ecosystem
3
+ * Herald MCP - AI-native interface to CEDA ecosystem
4
4
  *
5
- * Herald speaks CEDA. Cognition stays sovereign.
5
+ * Dual-mode:
6
+ * - CLI mode (TTY): Natural commands for humans
7
+ * - MCP mode (piped): JSON-RPC for AI agents
6
8
  */
7
9
  export {};
package/dist/index.js CHANGED
@@ -1,18 +1,219 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Herald MCP - Diplomatic interface to CEDA ecosystem
3
+ * Herald MCP - AI-native interface to CEDA ecosystem
4
4
  *
5
- * Herald speaks CEDA. Cognition stays sovereign.
5
+ * Dual-mode:
6
+ * - CLI mode (TTY): Natural commands for humans
7
+ * - MCP mode (piped): JSON-RPC for AI agents
6
8
  */
7
9
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
8
10
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9
11
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
12
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "fs";
13
+ import { homedir } from "os";
14
+ import { join } from "path";
10
15
  // Configuration - all sensitive values from environment only
11
16
  const CEDA_API_URL = process.env.HERALD_API_URL;
12
- const CEDA_API_TOKEN = process.env.HERALD_API_TOKEN; // Bearer token (optional for demo)
13
- const CEDA_API_USER = process.env.HERALD_API_USER; // Basic auth user (optional)
14
- const CEDA_API_PASS = process.env.HERALD_API_PASS; // Basic auth pass (optional)
15
- const DEFAULT_CONTEXT_ID = process.env.HERALD_CONTEXT_ID || "ctx_default";
17
+ const CEDA_API_TOKEN = process.env.HERALD_API_TOKEN;
18
+ const CEDA_API_USER = process.env.HERALD_API_USER;
19
+ const CEDA_API_PASS = process.env.HERALD_API_PASS;
20
+ // Multi-tenant context
21
+ const HERALD_COMPANY = process.env.HERALD_COMPANY || "default";
22
+ const HERALD_PROJECT = process.env.HERALD_PROJECT || "default";
23
+ const HERALD_USER = process.env.HERALD_USER || "default";
24
+ // Offspring vault context (for Avatar mode)
25
+ const HERALD_VAULT = process.env.HERALD_VAULT || ""; // e.g., "spilno", "goprint", "disrupt"
26
+ const AEGIS_OFFSPRING_PATH = process.env.AEGIS_OFFSPRING_PATH || join(homedir(), "Documents", "aegis_ceda", "_offspring");
27
+ // Cloud mode: Use CEDA API for offspring communication instead of local files
28
+ const OFFSPRING_CLOUD_MODE = process.env.HERALD_OFFSPRING_CLOUD === "true";
29
+ const VERSION = "1.3.0";
30
+ // Claude for Herald's voice - bundled key with limits, users can override
31
+ const HERALD_VOICE_KEY = "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";
32
+ const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY || HERALD_VOICE_KEY;
33
+ // Session persistence - context-isolated paths
34
+ // ~/.herald/{company}/{project}/{user}/session
35
+ function getHeraldDir() {
36
+ return join(homedir(), ".herald", HERALD_COMPANY, HERALD_PROJECT, HERALD_USER);
37
+ }
38
+ function getSessionFile() {
39
+ return join(getHeraldDir(), "session");
40
+ }
41
+ function ensureHeraldDir() {
42
+ const dir = getHeraldDir();
43
+ if (!existsSync(dir)) {
44
+ mkdirSync(dir, { recursive: true });
45
+ }
46
+ }
47
+ function saveSession(sessionId) {
48
+ ensureHeraldDir();
49
+ writeFileSync(getSessionFile(), sessionId, "utf-8");
50
+ }
51
+ function loadSession() {
52
+ const sessionFile = getSessionFile();
53
+ if (existsSync(sessionFile)) {
54
+ return readFileSync(sessionFile, "utf-8").trim();
55
+ }
56
+ return null;
57
+ }
58
+ function clearSession() {
59
+ const sessionFile = getSessionFile();
60
+ if (existsSync(sessionFile)) {
61
+ unlinkSync(sessionFile);
62
+ }
63
+ }
64
+ // Context info for display
65
+ function getContextString() {
66
+ return `${HERALD_COMPANY}:${HERALD_PROJECT}:${HERALD_USER}`;
67
+ }
68
+ const HERALD_SYSTEM_PROMPT = `You are Herald, the voice of CEDA (Cognitive Event-Driven Architecture).
69
+ You help humans design module structures through natural conversation.
70
+
71
+ You have access to CEDA's cognitive capabilities:
72
+ - Predict: Generate structure predictions from requirements
73
+ - Refine: Improve predictions with additional requirements
74
+ - Session: Track conversation history
75
+
76
+ When users describe what they want, you:
77
+ 1. Call CEDA to generate/refine predictions
78
+ 2. Explain the results in natural language
79
+ 3. Ask clarifying questions when needed
80
+
81
+ Keep responses concise and focused. You're a helpful assistant, not verbose.
82
+ When showing module structures, summarize the key sections and fields.`;
83
+ async function callClaude(systemPrompt, messages) {
84
+ // Convert to Anthropic format (separate system from messages)
85
+ const anthropicMessages = messages
86
+ .filter(m => m.role !== "system")
87
+ .map(m => ({ role: m.role, content: m.content }));
88
+ // Combine all system messages
89
+ const systemContent = messages
90
+ .filter(m => m.role === "system")
91
+ .map(m => m.content)
92
+ .join("\n\n");
93
+ const response = await fetch("https://api.anthropic.com/v1/messages", {
94
+ method: "POST",
95
+ headers: {
96
+ "Content-Type": "application/json",
97
+ "x-api-key": ANTHROPIC_API_KEY,
98
+ "anthropic-version": "2023-06-01",
99
+ },
100
+ body: JSON.stringify({
101
+ model: "claude-sonnet-4-20250514",
102
+ max_tokens: 1000,
103
+ system: systemPrompt + (systemContent ? "\n\n" + systemContent : ""),
104
+ messages: anthropicMessages.length > 0 ? anthropicMessages : [{ role: "user", content: "Hello" }],
105
+ }),
106
+ });
107
+ if (!response.ok) {
108
+ const error = await response.text();
109
+ return `Claude error: ${error}`;
110
+ }
111
+ const data = await response.json();
112
+ return data.content[0]?.text || "No response from Claude";
113
+ }
114
+ async function translateAndExecute(userInput, conversationHistory) {
115
+ const sessionId = loadSession();
116
+ // First, ask Claude to interpret the user's intent
117
+ const interpretSystemPrompt = `You interpret user requests for CEDA.
118
+ Respond with JSON only: {"action": "predict"|"refine"|"info"|"accept"|"reject", "input": "the user's requirement"}
119
+ - predict: User wants to create something new
120
+ - refine: User wants to modify/add to current design (requires active session)
121
+ - info: User is asking a question
122
+ - accept: User approves the current design
123
+ - reject: User rejects/wants to start over
124
+
125
+ Current session: ${sessionId || "none"}`;
126
+ const interpretation = await callClaude(interpretSystemPrompt, [
127
+ { role: "user", content: userInput }
128
+ ]);
129
+ let cedaResult = null;
130
+ let action = "info";
131
+ try {
132
+ const parsed = JSON.parse(interpretation);
133
+ action = parsed.action;
134
+ const input = parsed.input;
135
+ if (action === "predict") {
136
+ cedaResult = await callCedaAPI("/api/predict", "POST", {
137
+ input,
138
+ config: { enableAutoFix: true, maxAutoFixAttempts: 3 },
139
+ });
140
+ if (cedaResult.sessionId) {
141
+ saveSession(cedaResult.sessionId);
142
+ }
143
+ }
144
+ else if (action === "refine" && sessionId) {
145
+ cedaResult = await callCedaAPI("/api/refine", "POST", {
146
+ sessionId,
147
+ refinement: input,
148
+ });
149
+ }
150
+ else if (action === "accept" && sessionId) {
151
+ cedaResult = await callCedaAPI("/api/feedback", "POST", {
152
+ sessionId,
153
+ accepted: true,
154
+ });
155
+ clearSession();
156
+ }
157
+ else if (action === "reject") {
158
+ clearSession();
159
+ cedaResult = { success: true, status: "Session cleared" };
160
+ }
161
+ }
162
+ catch {
163
+ // Claude didn't return valid JSON, treat as info request
164
+ }
165
+ // Build response context
166
+ let responseContext = "";
167
+ if (cedaResult) {
168
+ responseContext = `\n\nCEDA ${action} result:\n${JSON.stringify(cedaResult, null, 2)}\n\nSummarize this naturally for the user.`;
169
+ }
170
+ // Ask Claude to formulate a natural response
171
+ const responseMessages = [
172
+ ...conversationHistory,
173
+ { role: "user", content: userInput },
174
+ ];
175
+ return await callClaude(HERALD_SYSTEM_PROMPT + responseContext, responseMessages);
176
+ }
177
+ import * as readline from "readline";
178
+ async function runChatMode() {
179
+ // Key is bundled - chat works out of the box
180
+ const contextStr = getContextString();
181
+ console.log(`
182
+ Herald v${VERSION} - Chat Mode
183
+ Context: ${contextStr}
184
+ Type your requirements in natural language. Type 'exit' to quit.
185
+ ──────────────────────────────────────────────────────────────
186
+ `);
187
+ const rl = readline.createInterface({
188
+ input: process.stdin,
189
+ output: process.stdout,
190
+ });
191
+ const conversationHistory = [];
192
+ const currentSession = loadSession();
193
+ if (currentSession) {
194
+ console.log(`Resuming session: ${currentSession}\n`);
195
+ }
196
+ const prompt = () => {
197
+ rl.question("You: ", async (input) => {
198
+ const trimmed = input.trim();
199
+ if (trimmed.toLowerCase() === "exit" || trimmed.toLowerCase() === "quit") {
200
+ console.log("\nGoodbye!");
201
+ rl.close();
202
+ return;
203
+ }
204
+ if (!trimmed) {
205
+ prompt();
206
+ return;
207
+ }
208
+ conversationHistory.push({ role: "user", content: trimmed });
209
+ const response = await translateAndExecute(trimmed, conversationHistory);
210
+ conversationHistory.push({ role: "assistant", content: response });
211
+ console.log(`\nHerald: ${response}\n`);
212
+ prompt();
213
+ });
214
+ };
215
+ prompt();
216
+ }
16
217
  // Auth strategy: prefer Bearer token, fallback to Basic auth, allow none for demo
17
218
  function getAuthHeader() {
18
219
  if (CEDA_API_TOKEN) {
@@ -22,30 +223,43 @@ function getAuthHeader() {
22
223
  const basicAuth = Buffer.from(`${CEDA_API_USER}:${CEDA_API_PASS}`).toString("base64");
23
224
  return `Basic ${basicAuth}`;
24
225
  }
25
- return null; // No auth - acceptable for local demo
226
+ return null;
26
227
  }
27
228
  async function callCedaAPI(endpoint, method = "GET", body) {
28
- // Validate configuration
29
229
  if (!CEDA_API_URL) {
30
230
  return {
31
231
  success: false,
32
- error: "HERALD_API_URL not configured. Set environment variable."
232
+ error: "HERALD_API_URL not configured. Run: export HERALD_API_URL=https://getceda.com"
33
233
  };
34
234
  }
35
- const url = `${CEDA_API_URL}${endpoint}`;
235
+ // Add context params to URL for GET requests (except /health)
236
+ let url = `${CEDA_API_URL}${endpoint}`;
237
+ if (method === "GET" && endpoint.startsWith("/api/")) {
238
+ const separator = endpoint.includes("?") ? "&" : "?";
239
+ url += `${separator}company=${HERALD_COMPANY}&project=${HERALD_PROJECT}&user=${HERALD_USER}`;
240
+ }
36
241
  const headers = {
37
242
  "Content-Type": "application/json",
38
243
  };
39
- // Add auth header if configured (optional for demo)
40
244
  const authHeader = getAuthHeader();
41
245
  if (authHeader) {
42
246
  headers["Authorization"] = authHeader;
43
247
  }
248
+ // Add context to POST body
249
+ let enrichedBody = body;
250
+ if (method === "POST" && body && typeof body === "object") {
251
+ enrichedBody = {
252
+ ...body,
253
+ company: HERALD_COMPANY,
254
+ project: HERALD_PROJECT,
255
+ user: HERALD_USER,
256
+ };
257
+ }
44
258
  try {
45
259
  const response = await fetch(url, {
46
260
  method,
47
261
  headers,
48
- body: body ? JSON.stringify(body) : undefined,
262
+ body: enrichedBody ? JSON.stringify(enrichedBody) : undefined,
49
263
  });
50
264
  if (!response.ok) {
51
265
  return { success: false, error: `HTTP ${response.status}: ${response.statusText}` };
@@ -56,32 +270,181 @@ async function callCedaAPI(endpoint, method = "GET", body) {
56
270
  return { success: false, error: `Connection failed: ${error}` };
57
271
  }
58
272
  }
59
- // Create MCP server
60
- const server = new Server({
61
- name: "herald",
62
- version: "1.0.0",
63
- }, {
64
- capabilities: {
65
- tools: {},
66
- },
67
- });
68
- // Tool definitions - aligned with actual CEDA server endpoints
273
+ // ============================================
274
+ // CLI MODE - Human-friendly commands
275
+ // ============================================
276
+ function printUsage() {
277
+ const currentSession = loadSession();
278
+ const contextStr = getContextString();
279
+ const sessionDir = getHeraldDir();
280
+ console.log(`
281
+ Herald MCP v${VERSION} - AI-native interface to CEDA
282
+
283
+ Context: ${contextStr}
284
+ Session: ${currentSession || "(none)"}
285
+ Path: ${sessionDir}
286
+
287
+ Usage:
288
+ herald-mcp <command> [options]
289
+
290
+ Commands:
291
+ chat Natural conversation mode (Claude voice)
292
+ predict "<signal>" Start new prediction (saves session)
293
+ refine "<text>" Refine current session
294
+ resume Show current session state
295
+ observe yes|no Record feedback & close session
296
+ new Clear session, start fresh
297
+ health Check CEDA system status
298
+ stats Get server statistics
299
+
300
+ Examples:
301
+ herald-mcp chat # Natural conversation
302
+ herald-mcp predict "create safety assessment" # Command mode
303
+ herald-mcp refine "add OSHA compliance"
304
+ herald-mcp observe yes
305
+
306
+ Environment:
307
+ HERALD_API_URL CEDA server URL (required)
308
+ HERALD_COMPANY Company context (default: default)
309
+ HERALD_PROJECT Project context (default: default)
310
+ HERALD_USER User context (default: default)
311
+ ANTHROPIC_API_KEY Claude key override (optional, bundled key available)
312
+ HERALD_API_TOKEN Bearer token (optional)
313
+
314
+ MCP Mode:
315
+ When piped, Herald speaks JSON-RPC for AI agents.
316
+ `);
317
+ }
318
+ function formatOutput(data) {
319
+ if (data.error) {
320
+ console.error(`Error: ${data.error}`);
321
+ process.exit(1);
322
+ }
323
+ // Pretty print with session ID highlighted if present
324
+ if (data.sessionId) {
325
+ console.log(`\nSession: ${data.sessionId}\n`);
326
+ }
327
+ console.log(JSON.stringify(data, null, 2));
328
+ }
329
+ async function runCLI(args) {
330
+ const command = args[0]?.toLowerCase();
331
+ if (!command || command === "help" || command === "--help" || command === "-h") {
332
+ printUsage();
333
+ return;
334
+ }
335
+ if (command === "--version" || command === "-v") {
336
+ console.log(`herald-mcp v${VERSION}`);
337
+ return;
338
+ }
339
+ switch (command) {
340
+ case "chat": {
341
+ await runChatMode();
342
+ break;
343
+ }
344
+ case "health": {
345
+ const result = await callCedaAPI("/health");
346
+ formatOutput(result);
347
+ break;
348
+ }
349
+ case "stats": {
350
+ const result = await callCedaAPI("/api/stats");
351
+ formatOutput(result);
352
+ break;
353
+ }
354
+ case "predict": {
355
+ const signal = args[1];
356
+ if (!signal) {
357
+ console.error("Error: Missing signal. Usage: herald-mcp predict \"<signal>\"");
358
+ process.exit(1);
359
+ }
360
+ const result = await callCedaAPI("/api/predict", "POST", {
361
+ input: signal,
362
+ config: { enableAutoFix: true, maxAutoFixAttempts: 3 },
363
+ });
364
+ // Save session for future commands
365
+ if (result.sessionId) {
366
+ saveSession(result.sessionId);
367
+ console.log(`\n✓ Session saved: ${result.sessionId}\n`);
368
+ }
369
+ formatOutput(result);
370
+ break;
371
+ }
372
+ case "refine": {
373
+ const refinement = args[1];
374
+ if (!refinement) {
375
+ console.error("Error: Missing refinement. Usage: herald-mcp refine \"<refinement>\"");
376
+ process.exit(1);
377
+ }
378
+ const sessionId = loadSession();
379
+ if (!sessionId) {
380
+ console.error("Error: No active session. Run 'herald-mcp predict \"...\"' first.");
381
+ process.exit(1);
382
+ }
383
+ const result = await callCedaAPI("/api/refine", "POST", {
384
+ sessionId,
385
+ refinement,
386
+ });
387
+ formatOutput(result);
388
+ break;
389
+ }
390
+ case "resume":
391
+ case "session": {
392
+ const sessionId = args[1] || loadSession();
393
+ if (!sessionId) {
394
+ console.error("Error: No active session. Run 'herald-mcp predict \"...\"' first.");
395
+ process.exit(1);
396
+ }
397
+ const result = await callCedaAPI(`/api/session/${sessionId}`);
398
+ formatOutput(result);
399
+ break;
400
+ }
401
+ case "observe": {
402
+ const accepted = args[1]?.toLowerCase();
403
+ if (!accepted) {
404
+ console.error("Error: Missing feedback. Usage: herald-mcp observe yes|no");
405
+ process.exit(1);
406
+ }
407
+ const sessionId = loadSession();
408
+ if (!sessionId) {
409
+ console.error("Error: No active session. Run 'herald-mcp predict \"...\"' first.");
410
+ process.exit(1);
411
+ }
412
+ const result = await callCedaAPI("/api/feedback", "POST", {
413
+ sessionId,
414
+ accepted: accepted === "yes" || accepted === "true" || accepted === "accept",
415
+ comment: args[2],
416
+ });
417
+ // Clear session after feedback
418
+ clearSession();
419
+ console.log("\n✓ Session closed.\n");
420
+ formatOutput(result);
421
+ break;
422
+ }
423
+ case "new": {
424
+ clearSession();
425
+ console.log("✓ Session cleared. Ready for new prediction.");
426
+ break;
427
+ }
428
+ default:
429
+ console.error(`Unknown command: ${command}`);
430
+ printUsage();
431
+ process.exit(1);
432
+ }
433
+ }
434
+ // ============================================
435
+ // MCP MODE - JSON-RPC for AI agents
436
+ // ============================================
437
+ const server = new Server({ name: "herald", version: VERSION }, { capabilities: { tools: {} } });
69
438
  const tools = [
70
439
  {
71
440
  name: "herald_health",
72
441
  description: "Check Herald and CEDA system status",
73
- inputSchema: {
74
- type: "object",
75
- properties: {},
76
- },
442
+ inputSchema: { type: "object", properties: {} },
77
443
  },
78
444
  {
79
445
  name: "herald_stats",
80
446
  description: "Get CEDA server statistics and loaded patterns info",
81
- inputSchema: {
82
- type: "object",
83
- properties: {},
84
- },
447
+ inputSchema: { type: "object", properties: {} },
85
448
  },
86
449
  {
87
450
  name: "herald_predict",
@@ -89,62 +452,35 @@ const tools = [
89
452
  inputSchema: {
90
453
  type: "object",
91
454
  properties: {
92
- signal: {
93
- type: "string",
94
- description: "Natural language input (e.g., 'create safety assessment module')",
95
- },
96
- context: {
97
- type: "string",
98
- description: "Additional context for better prediction",
99
- },
100
- session_id: {
101
- type: "string",
102
- description: "Session ID for multi-turn conversations (returned from previous predict/refine)",
103
- },
104
- participant: {
105
- type: "string",
106
- description: "Participant name (e.g., 'user', 'architect', 'compliance')",
107
- },
455
+ signal: { type: "string", description: "Natural language input" },
456
+ context: { type: "string", description: "Additional context" },
457
+ session_id: { type: "string", description: "Session ID for multi-turn" },
458
+ participant: { type: "string", description: "Participant name" },
108
459
  },
109
460
  required: ["signal"],
110
461
  },
111
462
  },
112
463
  {
113
464
  name: "herald_refine",
114
- description: "Refine an existing prediction with additional requirements. Use session_id from previous predict call.",
465
+ description: "Refine an existing prediction with additional requirements.",
115
466
  inputSchema: {
116
467
  type: "object",
117
468
  properties: {
118
- session_id: {
119
- type: "string",
120
- description: "Session ID from previous predict or refine call",
121
- },
122
- refinement: {
123
- type: "string",
124
- description: "Refinement instruction (e.g., 'make it OSHA compliant', 'add corrective actions section')",
125
- },
126
- context: {
127
- type: "string",
128
- description: "Additional context for refinement",
129
- },
130
- participant: {
131
- type: "string",
132
- description: "Participant name (e.g., 'user', 'architect', 'compliance')",
133
- },
469
+ session_id: { type: "string", description: "Session ID from previous call" },
470
+ refinement: { type: "string", description: "Refinement instruction" },
471
+ context: { type: "string", description: "Additional context" },
472
+ participant: { type: "string", description: "Participant name" },
134
473
  },
135
474
  required: ["session_id", "refinement"],
136
475
  },
137
476
  },
138
477
  {
139
478
  name: "herald_session",
140
- description: "Get session information including history of all predictions and refinements",
479
+ description: "Get session information including history",
141
480
  inputSchema: {
142
481
  type: "object",
143
482
  properties: {
144
- session_id: {
145
- type: "string",
146
- description: "Session ID to retrieve",
147
- },
483
+ session_id: { type: "string", description: "Session ID to retrieve" },
148
484
  },
149
485
  required: ["session_id"],
150
486
  },
@@ -155,149 +491,290 @@ const tools = [
155
491
  inputSchema: {
156
492
  type: "object",
157
493
  properties: {
158
- session_id: {
159
- type: "string",
160
- description: "Session ID from prediction",
161
- },
162
- accepted: {
163
- type: "boolean",
164
- description: "Was prediction accepted by user?",
165
- },
166
- feedback: {
167
- type: "string",
168
- description: "Optional user feedback/comment",
169
- },
494
+ session_id: { type: "string", description: "Session ID" },
495
+ accepted: { type: "boolean", description: "Was prediction accepted?" },
496
+ feedback: { type: "string", description: "Optional feedback" },
170
497
  },
171
498
  required: ["session_id", "accepted"],
172
499
  },
173
500
  },
501
+ // === HERALD CONTEXT SYNC TOOLS ===
502
+ {
503
+ name: "herald_context_status",
504
+ description: "Get status from Herald contexts across domains. Returns session counts, active threads, blockers, and pending items for each context.",
505
+ inputSchema: {
506
+ type: "object",
507
+ properties: {
508
+ context: { type: "string", description: "Optional: specific context to query. Omit for all known contexts." },
509
+ },
510
+ },
511
+ },
512
+ {
513
+ name: "herald_share_insight",
514
+ description: "Share a pattern insight with another Herald context. Herald instances communicate through shared insights to propagate learned patterns across domains.",
515
+ inputSchema: {
516
+ type: "object",
517
+ properties: {
518
+ context: { type: "string", description: "Target context to share insight with" },
519
+ insight: { type: "string", description: "The insight to share" },
520
+ topic: { type: "string", description: "Optional topic/category for the insight" },
521
+ },
522
+ required: ["context", "insight"],
523
+ },
524
+ },
525
+ {
526
+ name: "herald_query_insights",
527
+ description: "Query Herald's accumulated insights on a topic. Herald draws from pattern knowledge shared across contexts. Use this when you need guidance on implementation decisions or domain patterns.",
528
+ inputSchema: {
529
+ type: "object",
530
+ properties: {
531
+ question: { type: "string", description: "What you need insight on" },
532
+ domain: { type: "string", description: "Optional domain context" },
533
+ },
534
+ required: ["question"],
535
+ },
536
+ },
174
537
  ];
175
- // Handle list tools
176
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
177
- tools,
178
- }));
179
- // Handle tool calls
538
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
180
539
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
181
540
  const { name, arguments: args } = request.params;
541
+ let result;
182
542
  switch (name) {
183
- case "herald_health": {
184
- // Maps to: GET /health
185
- const result = await callCedaAPI("/health");
186
- return {
187
- content: [
188
- {
189
- type: "text",
190
- text: JSON.stringify(result, null, 2),
191
- },
192
- ],
193
- };
194
- }
195
- case "herald_stats": {
196
- // Maps to: GET /api/stats
197
- const result = await callCedaAPI("/api/stats");
198
- return {
199
- content: [
200
- {
201
- type: "text",
202
- text: JSON.stringify(result, null, 2),
203
- },
204
- ],
205
- };
206
- }
543
+ case "herald_health":
544
+ result = await callCedaAPI("/health");
545
+ break;
546
+ case "herald_stats":
547
+ result = await callCedaAPI("/api/stats");
548
+ break;
207
549
  case "herald_predict": {
208
- // Maps to: POST /api/predict
209
550
  const params = args;
210
- // Build context array if additional context provided
211
- const context = params.context ? [
212
- {
213
- type: "user_context",
214
- value: params.context,
215
- source: "herald_mcp",
216
- }
217
- ] : [];
218
- const result = await callCedaAPI("/api/predict", "POST", {
551
+ const context = params.context ? [{ type: "user_context", value: params.context, source: "herald_mcp" }] : [];
552
+ result = await callCedaAPI("/api/predict", "POST", {
219
553
  input: params.signal,
220
554
  context,
221
555
  sessionId: params.session_id,
222
556
  participant: params.participant,
223
- config: {
224
- enableAutoFix: true,
225
- maxAutoFixAttempts: 3,
226
- },
557
+ config: { enableAutoFix: true, maxAutoFixAttempts: 3 },
227
558
  });
228
- return {
229
- content: [
230
- {
231
- type: "text",
232
- text: JSON.stringify(result, null, 2),
233
- },
234
- ],
235
- };
559
+ break;
236
560
  }
237
561
  case "herald_refine": {
238
- // Maps to: POST /api/refine
239
562
  const params = args;
240
- // Build context array if additional context provided
241
- const context = params.context ? [
242
- {
243
- type: "refinement_context",
244
- value: params.context,
245
- source: "herald_mcp",
246
- }
247
- ] : [];
248
- const result = await callCedaAPI("/api/refine", "POST", {
563
+ const context = params.context ? [{ type: "refinement_context", value: params.context, source: "herald_mcp" }] : [];
564
+ result = await callCedaAPI("/api/refine", "POST", {
249
565
  sessionId: params.session_id,
250
566
  refinement: params.refinement,
251
567
  context,
252
568
  participant: params.participant,
253
569
  });
254
- return {
255
- content: [
256
- {
257
- type: "text",
258
- text: JSON.stringify(result, null, 2),
259
- },
260
- ],
261
- };
570
+ break;
262
571
  }
263
572
  case "herald_session": {
264
- // Maps to: GET /api/session/:id
265
573
  const params = args;
266
- const result = await callCedaAPI(`/api/session/${params.session_id}`);
267
- return {
268
- content: [
269
- {
270
- type: "text",
271
- text: JSON.stringify(result, null, 2),
272
- },
273
- ],
274
- };
574
+ result = await callCedaAPI(`/api/session/${params.session_id}`);
575
+ break;
275
576
  }
276
577
  case "herald_observe": {
277
- // Maps to: POST /api/feedback
278
578
  const params = args;
279
- const result = await callCedaAPI("/api/feedback", "POST", {
280
- sessionId: params.session_id, // CEDA uses camelCase
579
+ result = await callCedaAPI("/api/feedback", "POST", {
580
+ sessionId: params.session_id,
281
581
  accepted: params.accepted,
282
582
  comment: params.feedback,
283
583
  });
284
- return {
285
- content: [
286
- {
287
- type: "text",
288
- text: JSON.stringify(result, null, 2),
289
- },
290
- ],
584
+ break;
585
+ }
586
+ // === HERALD CONTEXT SYNC HANDLERS ===
587
+ case "herald_context_status": {
588
+ const params = args;
589
+ const knownContexts = ["goprint", "disrupt", "spilno"];
590
+ const contextsToQuery = params.context ? [params.context] : knownContexts;
591
+ // Cloud mode: Call CEDA API (Herald-to-Herald protocol)
592
+ if (OFFSPRING_CLOUD_MODE && CEDA_API_URL) {
593
+ const contextParam = params.context ? `?context=${params.context}` : "";
594
+ result = await callCedaAPI(`/api/herald/contexts${contextParam}`);
595
+ break;
596
+ }
597
+ const statuses = [];
598
+ for (const ctx of contextsToQuery) {
599
+ const statusPath = join(AEGIS_OFFSPRING_PATH, `${ctx}.md`);
600
+ if (!existsSync(statusPath)) {
601
+ statuses.push({
602
+ context: ctx,
603
+ status: "not_found",
604
+ sessionCount: 0,
605
+ lastOutcome: null,
606
+ activeThreads: [],
607
+ blockers: [],
608
+ pendingItems: [],
609
+ readyForSync: false,
610
+ });
611
+ continue;
612
+ }
613
+ const content = readFileSync(statusPath, "utf-8");
614
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
615
+ if (!frontmatterMatch) {
616
+ statuses.push({
617
+ context: ctx,
618
+ status: "invalid_format",
619
+ sessionCount: 0,
620
+ lastOutcome: null,
621
+ activeThreads: [],
622
+ blockers: [],
623
+ pendingItems: [],
624
+ readyForSync: false,
625
+ });
626
+ continue;
627
+ }
628
+ const yaml = frontmatterMatch[1];
629
+ const parseYamlArray = (key) => {
630
+ const match = yaml.match(new RegExp(`${key}:\\s*\\n((?:\\s+-[^\\n]+\\n?)+)`, "m"));
631
+ if (!match)
632
+ return [];
633
+ return match[1]
634
+ .split("\n")
635
+ .filter((line) => line.trim().startsWith("-"))
636
+ .map((line) => line.replace(/^\s*-\s*/, "").trim());
637
+ };
638
+ const parseYamlValue = (key) => {
639
+ const match = yaml.match(new RegExp(`^\\s*${key}:\\s*(.+)$`, "m"));
640
+ return match ? match[1].trim() : null;
641
+ };
642
+ statuses.push({
643
+ context: ctx,
644
+ status: "active",
645
+ sessionCount: parseInt(parseYamlValue("session_count") || "0", 10),
646
+ lastOutcome: parseYamlValue("outcome"),
647
+ activeThreads: parseYamlArray("active_threads"),
648
+ blockers: parseYamlArray("blockers"),
649
+ pendingItems: parseYamlArray("awaiting_aegis"), // Read from file but expose as pendingItems
650
+ readyForSync: parseYamlValue("ready_for_handoff") === "true",
651
+ });
652
+ }
653
+ const totalSessions = statuses.reduce((sum, s) => sum + s.sessionCount, 0);
654
+ const pendingCount = statuses.reduce((sum, s) => sum + s.pendingItems.length, 0);
655
+ result = {
656
+ success: true,
657
+ mode: "local",
658
+ statuses,
659
+ summary: {
660
+ totalSessions,
661
+ pendingItemsCount: pendingCount,
662
+ contextsQueried: contextsToQuery.length,
663
+ },
664
+ };
665
+ break;
666
+ }
667
+ case "herald_share_insight": {
668
+ const params = args;
669
+ const validContexts = ["goprint", "disrupt", "spilno"];
670
+ if (!validContexts.includes(params.context)) {
671
+ result = { success: false, error: `Unknown context: ${params.context}. Valid: ${validContexts.join(", ")}` };
672
+ break;
673
+ }
674
+ // Cloud mode: Call CEDA API (Herald-to-Herald protocol)
675
+ if (OFFSPRING_CLOUD_MODE && CEDA_API_URL) {
676
+ result = await callCedaAPI("/api/herald/insight", "POST", {
677
+ targetContext: params.context,
678
+ insight: params.insight,
679
+ topic: params.topic,
680
+ });
681
+ break;
682
+ }
683
+ // Local mode: Write to filesystem
684
+ const insightPath = join(AEGIS_OFFSPRING_PATH, `${params.context}_insights.md`);
685
+ const entry = `---
686
+ timestamp: ${new Date().toISOString()}
687
+ from: herald
688
+ topic: ${params.topic || "general"}
689
+ ---
690
+
691
+ ${params.insight}
692
+
693
+ ---
694
+ `;
695
+ let existingContent = "";
696
+ if (existsSync(insightPath)) {
697
+ existingContent = readFileSync(insightPath, "utf-8");
698
+ }
699
+ writeFileSync(insightPath, existingContent + entry);
700
+ result = {
701
+ success: true,
702
+ mode: "local",
703
+ context: params.context,
704
+ message: `Insight shared with ${params.context}`,
705
+ path: insightPath,
706
+ };
707
+ break;
708
+ }
709
+ case "herald_query_insights": {
710
+ const params = args;
711
+ // Determine which context Herald is serving
712
+ const ctx = HERALD_VAULT || "default";
713
+ // Cloud mode: Call CEDA API (Herald-to-Herald protocol)
714
+ if (OFFSPRING_CLOUD_MODE && CEDA_API_URL) {
715
+ result = await callCedaAPI(`/api/herald/insights?context=${ctx}&query=${encodeURIComponent(params.question)}`);
716
+ break;
717
+ }
718
+ // Local mode: Read from filesystem
719
+ const insightsPath = join(AEGIS_OFFSPRING_PATH, `${ctx}_insights.md`);
720
+ let relevantInsights = "";
721
+ if (existsSync(insightsPath)) {
722
+ const content = readFileSync(insightsPath, "utf-8");
723
+ const entries = content.split(/^---$/m).filter((e) => e.trim());
724
+ const keywords = params.question.toLowerCase().split(/\s+/);
725
+ for (const entry of entries) {
726
+ const entryLower = entry.toLowerCase();
727
+ if (keywords.some((kw) => kw.length > 3 && entryLower.includes(kw))) {
728
+ relevantInsights += entry.trim() + "\n\n";
729
+ }
730
+ }
731
+ }
732
+ let response = `**Herald's Insight**\n\nOn your query: "${params.question}"\n\n`;
733
+ if (relevantInsights) {
734
+ response += `From accumulated pattern knowledge:\n\n${relevantInsights}`;
735
+ }
736
+ else {
737
+ response += `I don't have specific insights on this yet. Consider:\n`;
738
+ response += `1. Proceeding with your best judgment\n`;
739
+ response += `2. Recording your approach with herald_observe so I can learn\n`;
740
+ response += `3. The pattern will emerge through practice`;
741
+ }
742
+ result = {
743
+ success: true,
744
+ mode: "local",
745
+ context: ctx,
746
+ question: params.question,
747
+ response,
748
+ hasInsights: !!relevantInsights,
291
749
  };
750
+ break;
292
751
  }
293
752
  default:
294
753
  throw new Error(`Unknown tool: ${name}`);
295
754
  }
755
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
296
756
  });
297
- // Start server
298
- async function main() {
757
+ async function runMCP() {
299
758
  const transport = new StdioServerTransport();
300
759
  await server.connect(transport);
301
760
  console.error("Herald MCP server running on stdio");
302
761
  }
762
+ // ============================================
763
+ // ENTRY POINT - Detect mode
764
+ // ============================================
765
+ async function main() {
766
+ const args = process.argv.slice(2);
767
+ // If we have CLI arguments, run CLI mode
768
+ if (args.length > 0) {
769
+ await runCLI(args);
770
+ return;
771
+ }
772
+ // If stdin is a TTY (human at terminal), show help
773
+ if (process.stdin.isTTY) {
774
+ printUsage();
775
+ return;
776
+ }
777
+ // Otherwise, run MCP server (AI agent calling via pipe)
778
+ await runMCP();
779
+ }
303
780
  main().catch(console.error);
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@spilno/herald-mcp",
3
- "version": "1.1.3",
3
+ "version": "1.3.0",
4
4
  "description": "Herald MCP - AI-native interface to CEDA (Cognitive Event-Driven Architecture)",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
7
7
  "bin": {
8
- "herald-mcp": "./dist/index.js"
8
+ "herald-mcp": "dist/index.js"
9
9
  },
10
10
  "scripts": {
11
- "build": "tsc",
11
+ "build": "tsc && chmod +x dist/index.js",
12
12
  "start": "node dist/index.js",
13
13
  "dev": "tsx src/index.ts",
14
14
  "prepublishOnly": "npm run build"