@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/README.md +4 -4
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js +5 -8
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/init.d.ts +1 -1
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +39 -15
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/login.d.ts +1 -0
- package/dist/cli/login.d.ts.map +1 -1
- package/dist/cli/login.js +15 -2
- package/dist/cli/login.js.map +1 -1
- package/dist/cli/templates/claude-md.d.ts +3 -3
- package/dist/cli/templates/claude-md.d.ts.map +1 -1
- package/dist/cli/templates/claude-md.js +9 -9
- package/dist/cli/templates/claude-md.js.map +1 -1
- package/dist/cli.js +345 -30
- package/dist/cli.js.map +1 -1
- package/dist/sanitization.spec.js +1 -1
- package/dist/sanitization.spec.js.map +1 -1
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +74 -2
- package/dist/sdk.js.map +1 -1
- package/dist/session-tools.spec.js +3 -3
- package/dist/session-tools.spec.js.map +1 -1
- package/package.json +3 -3
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -2822
- package/dist/index.js.map +0 -1
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
|