@spilno/herald-mcp 1.28.2 → 1.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -71
- package/dist/cli/init.d.ts +5 -0
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +115 -53
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/templates/claude-md.d.ts +3 -1
- package/dist/cli/templates/claude-md.d.ts.map +1 -1
- package/dist/cli/templates/claude-md.js +42 -11
- package/dist/cli/templates/claude-md.js.map +1 -1
- package/dist/cli/templates/hookify-rules.d.ts +22 -0
- package/dist/cli/templates/hookify-rules.d.ts.map +1 -0
- package/dist/cli/templates/hookify-rules.js +57 -0
- package/dist/cli/templates/hookify-rules.js.map +1 -0
- package/dist/index.js +556 -150
- package/dist/index.js.map +1 -1
- package/dist/sanitization.d.ts +68 -0
- package/dist/sanitization.d.ts.map +1 -0
- package/dist/sanitization.js +236 -0
- package/dist/sanitization.js.map +1 -0
- package/dist/sanitization.spec.d.ts +2 -0
- package/dist/sanitization.spec.d.ts.map +1 -0
- package/dist/sanitization.spec.js +90 -0
- package/dist/sanitization.spec.js.map +1 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -8,44 +8,144 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
10
10
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
11
|
-
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
11
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
12
12
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "fs";
|
|
13
|
-
import { homedir } from "os";
|
|
13
|
+
import { homedir, userInfo } from "os";
|
|
14
14
|
import { join } from "path";
|
|
15
15
|
import * as readline from "readline";
|
|
16
16
|
import { runInit } from "./cli/init.js";
|
|
17
|
+
import { sanitize, previewSanitization } from "./sanitization.js";
|
|
17
18
|
// Configuration - all sensitive values from environment only
|
|
18
19
|
// CEDA_URL is primary, HERALD_API_URL for backwards compat, default to cloud
|
|
19
20
|
const CEDA_API_URL = process.env.CEDA_URL || process.env.HERALD_API_URL || "https://getceda.com";
|
|
20
21
|
const CEDA_API_TOKEN = process.env.HERALD_API_TOKEN;
|
|
21
22
|
const CEDA_API_USER = process.env.HERALD_API_USER;
|
|
22
23
|
const CEDA_API_PASS = process.env.HERALD_API_PASS;
|
|
23
|
-
// CEDA-
|
|
24
|
-
// User
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
//
|
|
36
|
-
|
|
24
|
+
// CEDA-70: Zero-config context - everything auto-derived, nothing required
|
|
25
|
+
// User is ALWAYS known (whoami). Company/project inferred from path as tags.
|
|
26
|
+
function deriveUser() {
|
|
27
|
+
try {
|
|
28
|
+
return userInfo().username;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return "unknown";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function deriveTags() {
|
|
35
|
+
// Derive tags from cwd path - last 2 meaningful segments
|
|
36
|
+
// /Users/john/projects/acme/backend → ["acme", "backend"]
|
|
37
|
+
try {
|
|
38
|
+
const cwd = process.cwd();
|
|
39
|
+
const parts = cwd.split("/").filter(p => p && !["Users", "home", "Documents", "projects", "repos", "GitHub"].includes(p));
|
|
40
|
+
return parts.slice(-2); // Last 2 segments as tags
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function getMcpJsonPath() {
|
|
47
|
+
return join(process.cwd(), '.mcp.json');
|
|
37
48
|
}
|
|
38
|
-
|
|
39
|
-
const
|
|
49
|
+
function readMcpJson() {
|
|
50
|
+
const mcpPath = getMcpJsonPath();
|
|
51
|
+
if (!existsSync(mcpPath))
|
|
52
|
+
return null;
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(readFileSync(mcpPath, 'utf-8'));
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function persistContext(context) {
|
|
61
|
+
const mcpPath = getMcpJsonPath();
|
|
62
|
+
let mcpJson = {};
|
|
63
|
+
if (existsSync(mcpPath)) {
|
|
64
|
+
try {
|
|
65
|
+
mcpJson = JSON.parse(readFileSync(mcpPath, 'utf-8'));
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Corrupted file, preserve structure
|
|
69
|
+
mcpJson = {};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Add herald context section (preserve existing mcpServers)
|
|
73
|
+
mcpJson.herald = {
|
|
74
|
+
...mcpJson.herald,
|
|
75
|
+
context: {
|
|
76
|
+
tags: context.tags,
|
|
77
|
+
user: context.user,
|
|
78
|
+
derived: true,
|
|
79
|
+
derivedFrom: 'path',
|
|
80
|
+
storedAt: new Date().toISOString()
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
writeFileSync(mcpPath, JSON.stringify(mcpJson, null, 2));
|
|
84
|
+
console.error(`[Herald] Context stored in .mcp.json: tags=[${context.tags.join(', ')}]`);
|
|
85
|
+
}
|
|
86
|
+
function loadOrDeriveContext() {
|
|
87
|
+
// 1. Check env vars (explicit override - highest priority)
|
|
88
|
+
if (process.env.HERALD_COMPANY) {
|
|
89
|
+
return {
|
|
90
|
+
tags: [process.env.HERALD_COMPANY, process.env.HERALD_PROJECT].filter(Boolean),
|
|
91
|
+
user: process.env.HERALD_USER || deriveUser(),
|
|
92
|
+
source: 'env'
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
// 2. Check .mcp.json for stored context
|
|
96
|
+
const mcpJson = readMcpJson();
|
|
97
|
+
if (mcpJson?.herald?.context?.tags?.length) {
|
|
98
|
+
return {
|
|
99
|
+
tags: mcpJson.herald.context.tags,
|
|
100
|
+
user: mcpJson.herald.context.user || deriveUser(),
|
|
101
|
+
source: 'stored'
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
// 3. Derive from path
|
|
105
|
+
const tags = deriveTags();
|
|
106
|
+
const user = deriveUser();
|
|
107
|
+
// Store for next time (if we have tags and .mcp.json exists or we can create it)
|
|
108
|
+
if (tags.length > 0) {
|
|
109
|
+
try {
|
|
110
|
+
persistContext({ tags, user });
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// Silent fail - don't break startup if we can't write
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return { tags, user, source: 'derived' };
|
|
117
|
+
}
|
|
118
|
+
// Load context once at startup
|
|
119
|
+
const LOADED_CONTEXT = loadOrDeriveContext();
|
|
120
|
+
// User is always known
|
|
121
|
+
const HERALD_USER = LOADED_CONTEXT.user;
|
|
122
|
+
// Tags from context (env > stored > derived)
|
|
123
|
+
const HERALD_TAGS = LOADED_CONTEXT.tags;
|
|
124
|
+
const HERALD_COMPANY = HERALD_TAGS[0] || "";
|
|
125
|
+
const HERALD_PROJECT = HERALD_TAGS[1] || HERALD_TAGS[0] || "";
|
|
126
|
+
// Track context source for telemetry
|
|
127
|
+
const CONTEXT_SOURCE = LOADED_CONTEXT.source;
|
|
128
|
+
const CONTEXT_AUTO_DERIVED = CONTEXT_SOURCE === 'derived';
|
|
40
129
|
// Offspring vault context (for Avatar mode)
|
|
41
130
|
const HERALD_VAULT = process.env.HERALD_VAULT || "";
|
|
42
131
|
const AEGIS_OFFSPRING_PATH = process.env.AEGIS_OFFSPRING_PATH || join(homedir(), "Documents", "aegis_ceda", "_offspring");
|
|
43
132
|
// Cloud mode: Use CEDA API for offspring communication instead of local files
|
|
44
133
|
const OFFSPRING_CLOUD_MODE = process.env.HERALD_OFFSPRING_CLOUD === "true";
|
|
45
|
-
const VERSION = "1.
|
|
134
|
+
const VERSION = "1.30.0";
|
|
46
135
|
// Self-routing description - teaches Claude when to call Herald
|
|
47
136
|
const HERALD_DESCRIPTION = `AI-native pattern learning for CEDA.
|
|
48
137
|
|
|
138
|
+
AUTHORIZATION GATES (CRITICAL):
|
|
139
|
+
Before multi-file operations (>3 files), module scaffolding, or bulk changes:
|
|
140
|
+
1. Present synthesis/plan to user
|
|
141
|
+
2. Ask: "Proceed? [Y/continue/adjust]"
|
|
142
|
+
3. WAIT for explicit approval before writing
|
|
143
|
+
|
|
144
|
+
Autonomous (no gate): reading, searching, planning, single-file edits, commits.
|
|
145
|
+
Gated: multi-file creation, module scaffolding, bulk operations.
|
|
146
|
+
|
|
147
|
+
Why: User steering > execution speed. Long uninterrupted runs block collaboration.
|
|
148
|
+
|
|
49
149
|
WHEN TO OFFER REFLECTION:
|
|
50
150
|
When you notice FRICTION (multiple iterations, debugging spirals, corrections, confusion)
|
|
51
151
|
or FLOW (clean first-pass execution, good instincts, smooth outcomes),
|
|
@@ -60,9 +160,9 @@ const AUTO_SYNC_ON_STARTUP = process.env.HERALD_AUTO_SYNC !== "false";
|
|
|
60
160
|
// SECURITY: Never bundle API keys in npm packages
|
|
61
161
|
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY || "";
|
|
62
162
|
const OPENAI_API_KEY = process.env.OPENAI_API_KEY || "";
|
|
63
|
-
// Session persistence -
|
|
163
|
+
// Session persistence - context-isolated paths
|
|
64
164
|
function getHeraldDir() {
|
|
65
|
-
return join(homedir(), ".herald", HERALD_USER);
|
|
165
|
+
return join(homedir(), ".herald", HERALD_COMPANY, HERALD_PROJECT, HERALD_USER);
|
|
66
166
|
}
|
|
67
167
|
function getSessionFile() {
|
|
68
168
|
return join(getHeraldDir(), "session");
|
|
@@ -179,7 +279,7 @@ function clearSession() {
|
|
|
179
279
|
}
|
|
180
280
|
}
|
|
181
281
|
function getContextString() {
|
|
182
|
-
return `${
|
|
282
|
+
return `${HERALD_COMPANY}:${HERALD_PROJECT}:${HERALD_USER}`;
|
|
183
283
|
}
|
|
184
284
|
const HERALD_SYSTEM_PROMPT = `You are Herald, the voice of CEDA (Cognitive Event-Driven Architecture).
|
|
185
285
|
You help humans design module structures through natural conversation.
|
|
@@ -420,13 +520,14 @@ async function callCedaAPI(endpoint, method = "GET", body) {
|
|
|
420
520
|
};
|
|
421
521
|
}
|
|
422
522
|
let url = `${CEDA_API_URL}${endpoint}`;
|
|
423
|
-
//
|
|
424
|
-
|
|
523
|
+
// Only add tenant params to endpoints that need them (patterns, session queries)
|
|
524
|
+
// Don't add to simple endpoints like /api/stats, /health
|
|
525
|
+
const needsTenantParams = endpoint.startsWith("/api/patterns") ||
|
|
425
526
|
endpoint.startsWith("/api/session/") ||
|
|
426
527
|
endpoint.startsWith("/api/observations");
|
|
427
|
-
if (method === "GET" &&
|
|
528
|
+
if (method === "GET" && needsTenantParams) {
|
|
428
529
|
const separator = endpoint.includes("?") ? "&" : "?";
|
|
429
|
-
url += `${separator}
|
|
530
|
+
url += `${separator}company=${HERALD_COMPANY}&project=${HERALD_PROJECT}&user=${HERALD_USER}`;
|
|
430
531
|
}
|
|
431
532
|
const headers = {
|
|
432
533
|
"Content-Type": "application/json",
|
|
@@ -437,10 +538,10 @@ async function callCedaAPI(endpoint, method = "GET", body) {
|
|
|
437
538
|
}
|
|
438
539
|
let enrichedBody = body;
|
|
439
540
|
if (method === "POST" && body && typeof body === "object") {
|
|
440
|
-
// CEDA-67: Use tags instead of company/project hierarchy
|
|
441
541
|
enrichedBody = {
|
|
442
542
|
...body,
|
|
443
|
-
|
|
543
|
+
company: HERALD_COMPANY,
|
|
544
|
+
project: HERALD_PROJECT,
|
|
444
545
|
user: HERALD_USER,
|
|
445
546
|
};
|
|
446
547
|
}
|
|
@@ -495,13 +596,13 @@ Examples:
|
|
|
495
596
|
herald-mcp observe yes
|
|
496
597
|
|
|
497
598
|
Environment:
|
|
498
|
-
|
|
499
|
-
|
|
599
|
+
HERALD_API_URL CEDA server URL (required for API calls)
|
|
600
|
+
HERALD_COMPANY Company context (default: default)
|
|
601
|
+
HERALD_PROJECT Project context (default: default)
|
|
602
|
+
HERALD_USER User context (default: default)
|
|
500
603
|
ANTHROPIC_API_KEY Claude API key (required for chat mode)
|
|
501
604
|
HERALD_API_TOKEN Bearer token (optional)
|
|
502
605
|
|
|
503
|
-
Context tags are derived automatically from your working directory.
|
|
504
|
-
|
|
505
606
|
MCP Mode:
|
|
506
607
|
When piped, Herald speaks JSON-RPC for AI agents.
|
|
507
608
|
`);
|
|
@@ -626,7 +727,7 @@ async function runCLI(args) {
|
|
|
626
727
|
// ============================================
|
|
627
728
|
// MCP MODE - JSON-RPC for AI agents
|
|
628
729
|
// ============================================
|
|
629
|
-
const server = new Server({ name: "herald", version: VERSION, description: HERALD_DESCRIPTION }, { capabilities: { tools: {} } });
|
|
730
|
+
const server = new Server({ name: "herald", version: VERSION, description: HERALD_DESCRIPTION }, { capabilities: { tools: {}, resources: {} } });
|
|
630
731
|
const tools = [
|
|
631
732
|
{
|
|
632
733
|
name: "herald_help",
|
|
@@ -643,6 +744,50 @@ const tools = [
|
|
|
643
744
|
description: "Get CEDA server statistics and loaded patterns info",
|
|
644
745
|
inputSchema: { type: "object", properties: {} },
|
|
645
746
|
},
|
|
747
|
+
{
|
|
748
|
+
name: "herald_gate",
|
|
749
|
+
description: `Request authorization before large operations.
|
|
750
|
+
|
|
751
|
+
WHEN TO USE:
|
|
752
|
+
Call this BEFORE multi-file operations (>3 files), module scaffolding, or bulk changes.
|
|
753
|
+
|
|
754
|
+
This tool:
|
|
755
|
+
1. Formats a clear authorization request for the user
|
|
756
|
+
2. Returns a gate_id for tracking
|
|
757
|
+
3. Records the operation scope for audit
|
|
758
|
+
|
|
759
|
+
After calling this tool, WAIT for explicit user approval before proceeding.
|
|
760
|
+
User may respond: Y/yes/proceed (approved), adjust (modify scope), or N/no (denied).
|
|
761
|
+
|
|
762
|
+
Example flow:
|
|
763
|
+
1. You complete synthesis/planning
|
|
764
|
+
2. Call herald_gate with operation summary
|
|
765
|
+
3. Tool returns formatted request
|
|
766
|
+
4. STOP and wait for user response
|
|
767
|
+
5. Only proceed if user approves`,
|
|
768
|
+
inputSchema: {
|
|
769
|
+
type: "object",
|
|
770
|
+
properties: {
|
|
771
|
+
operation: {
|
|
772
|
+
type: "string",
|
|
773
|
+
description: "What operation needs authorization (e.g., 'Create bh-incidents module')"
|
|
774
|
+
},
|
|
775
|
+
scope: {
|
|
776
|
+
type: "string",
|
|
777
|
+
description: "Scope summary (e.g., '31 files, 6 dictionaries, 4 forms')"
|
|
778
|
+
},
|
|
779
|
+
template: {
|
|
780
|
+
type: "string",
|
|
781
|
+
description: "Template/pattern being followed (e.g., 'bh-inspections')"
|
|
782
|
+
},
|
|
783
|
+
rationale: {
|
|
784
|
+
type: "string",
|
|
785
|
+
description: "Brief rationale for the operation"
|
|
786
|
+
},
|
|
787
|
+
},
|
|
788
|
+
required: ["operation", "scope"],
|
|
789
|
+
},
|
|
790
|
+
},
|
|
646
791
|
{
|
|
647
792
|
name: "herald_predict",
|
|
648
793
|
description: "Generate non-deterministic structure prediction from signal. Returns sessionId for multi-turn conversations.",
|
|
@@ -747,7 +892,7 @@ const tools = [
|
|
|
747
892
|
inputSchema: {
|
|
748
893
|
type: "object",
|
|
749
894
|
properties: {
|
|
750
|
-
company: { type: "string", description: "Filter by company (optional,
|
|
895
|
+
company: { type: "string", description: "Filter by company (optional, defaults to HERALD_COMPANY)" },
|
|
751
896
|
project: { type: "string", description: "Filter by project (optional)" },
|
|
752
897
|
user: { type: "string", description: "Filter by user (optional)" },
|
|
753
898
|
status: { type: "string", description: "Filter by status: active, archived, or expired (optional)" },
|
|
@@ -814,15 +959,28 @@ User's answer goes in the 'insight' parameter.
|
|
|
814
959
|
|
|
815
960
|
DO NOT GUESS. The user knows what they valued. Ask them.
|
|
816
961
|
|
|
962
|
+
ABSTRACTION GUIDANCE:
|
|
963
|
+
Capture the PATTERN, not the SPECIFICS. Good patterns are reusable.
|
|
964
|
+
- BAD: "Fixed bug in /Users/john/project/auth.ts line 47"
|
|
965
|
+
- GOOD: "Early return pattern for auth validation reduces nesting"
|
|
966
|
+
- BAD: "API key sk-proj-xxx was in wrong env file"
|
|
967
|
+
- GOOD: "Secrets in .env.local not .env prevents accidental commits"
|
|
968
|
+
|
|
817
969
|
Example flow:
|
|
818
970
|
1. User: "That was smooth, capture it"
|
|
819
|
-
2. You: "What specifically worked here?"
|
|
971
|
+
2. You: "What specifically worked here? (Describe the pattern, not specific files/values)"
|
|
820
972
|
3. User: "The ASCII visualization approach"
|
|
821
973
|
4. You call herald_reflect with insight: "ASCII visualization approach"
|
|
822
974
|
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
975
|
+
PRIVACY (CEDA-65):
|
|
976
|
+
- Client-side sanitization runs BEFORE any data leaves your machine
|
|
977
|
+
- API keys, tokens, passwords, file paths with usernames are auto-redacted
|
|
978
|
+
- Private keys and AWS credentials are BLOCKED entirely
|
|
979
|
+
- Use dry_run=true to preview exactly what would be transmitted
|
|
980
|
+
|
|
981
|
+
DRY RUN MODE:
|
|
982
|
+
Set dry_run=true to preview sanitization without storing.
|
|
983
|
+
Shows what would be redacted and final transmitted text.`,
|
|
826
984
|
inputSchema: {
|
|
827
985
|
type: "object",
|
|
828
986
|
properties: {
|
|
@@ -1060,6 +1218,124 @@ Returns all patterns for the current context (company/project/user) in the speci
|
|
|
1060
1218
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
1061
1219
|
return { tools };
|
|
1062
1220
|
});
|
|
1221
|
+
// ============================================
|
|
1222
|
+
// MCP RESOURCES - Auto-readable by Claude Code
|
|
1223
|
+
// ============================================
|
|
1224
|
+
// Helper to fetch patterns with cascade (reused from herald_patterns tool)
|
|
1225
|
+
async function fetchPatternsWithCascade() {
|
|
1226
|
+
const seenInsights = new Set();
|
|
1227
|
+
const patterns = [];
|
|
1228
|
+
const antipatterns = [];
|
|
1229
|
+
const queries = [
|
|
1230
|
+
{ scope: "user", url: `/api/herald/reflections?company=${HERALD_COMPANY}&project=${HERALD_PROJECT}&user=${HERALD_USER}&limit=10` },
|
|
1231
|
+
{ scope: "project", url: `/api/herald/reflections?company=${HERALD_COMPANY}&project=${HERALD_PROJECT}&limit=10` },
|
|
1232
|
+
{ scope: "company", url: `/api/herald/reflections?company=${HERALD_COMPANY}&limit=10` },
|
|
1233
|
+
];
|
|
1234
|
+
for (const { scope, url } of queries) {
|
|
1235
|
+
try {
|
|
1236
|
+
const result = await callCedaAPI(url);
|
|
1237
|
+
const scopePatterns = result.patterns || [];
|
|
1238
|
+
const scopeAntipatterns = result.antipatterns || [];
|
|
1239
|
+
for (const p of scopePatterns) {
|
|
1240
|
+
const key = p.insight.toLowerCase().trim();
|
|
1241
|
+
if (!seenInsights.has(key)) {
|
|
1242
|
+
seenInsights.add(key);
|
|
1243
|
+
patterns.push(`${p.insight} [${scope}]`);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
for (const ap of scopeAntipatterns) {
|
|
1247
|
+
const key = ap.insight.toLowerCase().trim();
|
|
1248
|
+
if (!seenInsights.has(key)) {
|
|
1249
|
+
seenInsights.add(key);
|
|
1250
|
+
antipatterns.push(`${ap.insight} [${scope}]`);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
catch {
|
|
1255
|
+
// Continue if a level fails
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
return { patterns, antipatterns, context: `${HERALD_USER}→${HERALD_PROJECT}→${HERALD_COMPANY}` };
|
|
1259
|
+
}
|
|
1260
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
1261
|
+
const resources = [
|
|
1262
|
+
{
|
|
1263
|
+
uri: "herald://patterns",
|
|
1264
|
+
name: "Herald Learned Patterns",
|
|
1265
|
+
description: `Patterns and antipatterns learned from past sessions for ${HERALD_USER}→${HERALD_PROJECT}→${HERALD_COMPANY}. READ THIS AT SESSION START.`,
|
|
1266
|
+
mimeType: "text/plain",
|
|
1267
|
+
},
|
|
1268
|
+
{
|
|
1269
|
+
uri: "herald://context",
|
|
1270
|
+
name: "Herald Context",
|
|
1271
|
+
description: "Current Herald context configuration (company/project/user)",
|
|
1272
|
+
mimeType: "application/json",
|
|
1273
|
+
},
|
|
1274
|
+
];
|
|
1275
|
+
return { resources };
|
|
1276
|
+
});
|
|
1277
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
1278
|
+
const { uri } = request.params;
|
|
1279
|
+
if (uri === "herald://patterns") {
|
|
1280
|
+
try {
|
|
1281
|
+
const { patterns, antipatterns, context } = await fetchPatternsWithCascade();
|
|
1282
|
+
let content = `# Herald Patterns for ${context}\n\n`;
|
|
1283
|
+
content += `**READ THIS FIRST** - These are learned patterns from past sessions.\n\n`;
|
|
1284
|
+
if (antipatterns.length > 0) {
|
|
1285
|
+
content += `## ⚠️ ANTIPATTERNS - AVOID THESE\n`;
|
|
1286
|
+
antipatterns.forEach((ap, i) => {
|
|
1287
|
+
content += `${i + 1}. ${ap}\n`;
|
|
1288
|
+
});
|
|
1289
|
+
content += `\n`;
|
|
1290
|
+
}
|
|
1291
|
+
if (patterns.length > 0) {
|
|
1292
|
+
content += `## ✓ PATTERNS - DO THESE\n`;
|
|
1293
|
+
patterns.forEach((p, i) => {
|
|
1294
|
+
content += `${i + 1}. ${p}\n`;
|
|
1295
|
+
});
|
|
1296
|
+
content += `\n`;
|
|
1297
|
+
}
|
|
1298
|
+
if (patterns.length === 0 && antipatterns.length === 0) {
|
|
1299
|
+
content += `No patterns learned yet. Capture patterns with "herald reflect" when you notice friction or flow.\n`;
|
|
1300
|
+
}
|
|
1301
|
+
content += `\n---\n*Auto-loaded from CEDA. Call herald_pattern_feedback() when a pattern helps.*\n`;
|
|
1302
|
+
return {
|
|
1303
|
+
contents: [{
|
|
1304
|
+
uri,
|
|
1305
|
+
mimeType: "text/plain",
|
|
1306
|
+
text: content,
|
|
1307
|
+
}],
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
catch (error) {
|
|
1311
|
+
return {
|
|
1312
|
+
contents: [{
|
|
1313
|
+
uri,
|
|
1314
|
+
mimeType: "text/plain",
|
|
1315
|
+
text: `Failed to load patterns: ${error}\n\nCEDA may be unavailable.`,
|
|
1316
|
+
}],
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
if (uri === "herald://context") {
|
|
1321
|
+
const context = {
|
|
1322
|
+
company: HERALD_COMPANY,
|
|
1323
|
+
project: HERALD_PROJECT,
|
|
1324
|
+
user: HERALD_USER,
|
|
1325
|
+
vault: HERALD_VAULT || null,
|
|
1326
|
+
autoDerived: CONTEXT_AUTO_DERIVED,
|
|
1327
|
+
cedaUrl: CEDA_API_URL,
|
|
1328
|
+
};
|
|
1329
|
+
return {
|
|
1330
|
+
contents: [{
|
|
1331
|
+
uri,
|
|
1332
|
+
mimeType: "application/json",
|
|
1333
|
+
text: JSON.stringify(context, null, 2),
|
|
1334
|
+
}],
|
|
1335
|
+
};
|
|
1336
|
+
}
|
|
1337
|
+
throw new Error(`Unknown resource: ${uri}`);
|
|
1338
|
+
});
|
|
1063
1339
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1064
1340
|
const { name, arguments: args } = request.params;
|
|
1065
1341
|
try {
|
|
@@ -1071,7 +1347,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1071
1347
|
Welcome to Herald - your AI-native interface to CEDA (Cognitive Event-Driven Architecture).
|
|
1072
1348
|
|
|
1073
1349
|
## Current Context
|
|
1074
|
-
-
|
|
1350
|
+
- Company: ${HERALD_COMPANY}
|
|
1351
|
+
- Project: ${HERALD_PROJECT}
|
|
1075
1352
|
- User: ${HERALD_USER}
|
|
1076
1353
|
|
|
1077
1354
|
## Available Tools
|
|
@@ -1129,17 +1406,19 @@ Herald will:
|
|
|
1129
1406
|
const cedaHealth = await callCedaAPI("/health");
|
|
1130
1407
|
const buffer = getBufferedInsights();
|
|
1131
1408
|
const cloudAvailable = !cedaHealth.error;
|
|
1132
|
-
// CEDA-67: AI-native context - tags derived from cwd
|
|
1133
1409
|
const config = {
|
|
1134
1410
|
cedaUrl: CEDA_API_URL,
|
|
1135
|
-
|
|
1411
|
+
company: HERALD_COMPANY,
|
|
1412
|
+
project: HERALD_PROJECT,
|
|
1136
1413
|
user: HERALD_USER,
|
|
1137
|
-
cwd: process.cwd(),
|
|
1138
1414
|
vault: HERALD_VAULT || "(not set)",
|
|
1415
|
+
autoDerived: CONTEXT_AUTO_DERIVED,
|
|
1139
1416
|
};
|
|
1140
1417
|
const warnings = [];
|
|
1141
|
-
if (
|
|
1142
|
-
warnings.push(
|
|
1418
|
+
if (CONTEXT_AUTO_DERIVED) {
|
|
1419
|
+
warnings.push(`Context auto-derived from folder: ${HERALD_COMPANY}/${HERALD_PROJECT}`);
|
|
1420
|
+
warnings.push("Run 'npx @spilno/herald-mcp init' for persistent config");
|
|
1421
|
+
}
|
|
1143
1422
|
if (!process.env.CEDA_URL && !process.env.HERALD_API_URL) {
|
|
1144
1423
|
warnings.push("Using default CEDA_URL (getceda.com) - set CEDA_URL for custom endpoint");
|
|
1145
1424
|
}
|
|
@@ -1168,6 +1447,42 @@ Herald will:
|
|
|
1168
1447
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
1169
1448
|
};
|
|
1170
1449
|
}
|
|
1450
|
+
case "herald_gate": {
|
|
1451
|
+
const operation = args?.operation;
|
|
1452
|
+
const scope = args?.scope;
|
|
1453
|
+
const template = args?.template;
|
|
1454
|
+
const rationale = args?.rationale;
|
|
1455
|
+
// Generate gate ID for tracking
|
|
1456
|
+
const gateId = `gate-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
1457
|
+
// Format the authorization request
|
|
1458
|
+
let gateRequest = `\n## Authorization Required\n\n`;
|
|
1459
|
+
gateRequest += `**Operation:** ${operation}\n`;
|
|
1460
|
+
gateRequest += `**Scope:** ${scope}\n`;
|
|
1461
|
+
if (template) {
|
|
1462
|
+
gateRequest += `**Template:** Following \`${template}\` patterns\n`;
|
|
1463
|
+
}
|
|
1464
|
+
if (rationale) {
|
|
1465
|
+
gateRequest += `**Rationale:** ${rationale}\n`;
|
|
1466
|
+
}
|
|
1467
|
+
gateRequest += `\n---\n`;
|
|
1468
|
+
gateRequest += `**Proceed?** [Y/yes/proceed] [adjust] [N/no]\n`;
|
|
1469
|
+
gateRequest += `\n_Gate ID: ${gateId}_\n`;
|
|
1470
|
+
return {
|
|
1471
|
+
content: [{
|
|
1472
|
+
type: "text",
|
|
1473
|
+
text: JSON.stringify({
|
|
1474
|
+
success: true,
|
|
1475
|
+
gate_id: gateId,
|
|
1476
|
+
status: "awaiting_authorization",
|
|
1477
|
+
message: gateRequest,
|
|
1478
|
+
operation,
|
|
1479
|
+
scope,
|
|
1480
|
+
template: template || null,
|
|
1481
|
+
instruction: "STOP HERE. Wait for user response before proceeding with any file operations.",
|
|
1482
|
+
}, null, 2)
|
|
1483
|
+
}],
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1171
1486
|
case "herald_predict": {
|
|
1172
1487
|
const signal = args?.signal;
|
|
1173
1488
|
const contextStr = args?.context;
|
|
@@ -1253,23 +1568,24 @@ Herald will:
|
|
|
1253
1568
|
const insight = args?.insight;
|
|
1254
1569
|
const targetVault = args?.target_vault;
|
|
1255
1570
|
const topic = args?.topic;
|
|
1256
|
-
// CEDA-67: Use tags instead of company/project
|
|
1257
1571
|
const payload = {
|
|
1258
1572
|
insight,
|
|
1259
1573
|
topic,
|
|
1260
1574
|
targetVault,
|
|
1261
1575
|
sourceVault: HERALD_VAULT || undefined,
|
|
1262
|
-
|
|
1576
|
+
company: HERALD_COMPANY,
|
|
1577
|
+
project: HERALD_PROJECT,
|
|
1263
1578
|
user: HERALD_USER,
|
|
1264
1579
|
};
|
|
1265
1580
|
// Cloud-first: try to POST to CEDA, buffer locally on failure
|
|
1266
1581
|
// Map Herald's vault terminology to CEDA's context terminology
|
|
1582
|
+
// Default toContext to "all" for guest mode / when no target specified
|
|
1267
1583
|
try {
|
|
1268
1584
|
const result = await callCedaAPI("/api/herald/insight", "POST", {
|
|
1269
1585
|
insight,
|
|
1270
|
-
toContext: targetVault,
|
|
1586
|
+
toContext: targetVault || "all", // Required by CEDA, default to broadcast
|
|
1271
1587
|
topic,
|
|
1272
|
-
fromContext: HERALD_VAULT
|
|
1588
|
+
fromContext: HERALD_VAULT || `${HERALD_COMPANY}/${HERALD_PROJECT}`,
|
|
1273
1589
|
});
|
|
1274
1590
|
// Check if API returned an error
|
|
1275
1591
|
if (result.error) {
|
|
@@ -1353,8 +1669,8 @@ Herald will:
|
|
|
1353
1669
|
const result = await callCedaAPI("/api/herald/insight", "POST", {
|
|
1354
1670
|
insight: item.insight,
|
|
1355
1671
|
topic: item.topic,
|
|
1356
|
-
|
|
1357
|
-
|
|
1672
|
+
toContext: item.targetVault || "all", // CEDA expects toContext, default to "all"
|
|
1673
|
+
fromContext: item.sourceVault, // CEDA expects fromContext
|
|
1358
1674
|
});
|
|
1359
1675
|
if (result.error) {
|
|
1360
1676
|
failed.push(item);
|
|
@@ -1400,23 +1716,15 @@ Herald will:
|
|
|
1400
1716
|
}
|
|
1401
1717
|
// CEDA-49: Session Management Tools
|
|
1402
1718
|
case "herald_session_list": {
|
|
1403
|
-
// CEDA-67: Use tags for filtering, company/project for backwards compat
|
|
1404
1719
|
const company = args?.company;
|
|
1405
1720
|
const project = args?.project;
|
|
1406
1721
|
const user = args?.user;
|
|
1407
1722
|
const status = args?.status;
|
|
1408
1723
|
const limit = args?.limit;
|
|
1409
1724
|
const params = new URLSearchParams();
|
|
1410
|
-
|
|
1411
|
-
if (
|
|
1412
|
-
|
|
1413
|
-
params.set("company", company);
|
|
1414
|
-
if (project)
|
|
1415
|
-
params.set("project", project);
|
|
1416
|
-
}
|
|
1417
|
-
else {
|
|
1418
|
-
params.set("tags", CONTEXT_TAGS.join(","));
|
|
1419
|
-
}
|
|
1725
|
+
params.set("company", company || HERALD_COMPANY);
|
|
1726
|
+
if (project)
|
|
1727
|
+
params.set("project", project);
|
|
1420
1728
|
if (user)
|
|
1421
1729
|
params.set("user", user);
|
|
1422
1730
|
if (status)
|
|
@@ -1469,44 +1777,62 @@ Herald will:
|
|
|
1469
1777
|
const feeling = args?.feeling;
|
|
1470
1778
|
const insight = args?.insight;
|
|
1471
1779
|
const dryRun = args?.dry_run;
|
|
1472
|
-
// CEDA-65:
|
|
1780
|
+
// CEDA-65: Client-side sanitization preview (no network required)
|
|
1781
|
+
const sessionPreview = previewSanitization(session);
|
|
1782
|
+
const insightPreview = previewSanitization(insight);
|
|
1783
|
+
// Check if content would be blocked
|
|
1784
|
+
if (sessionPreview.wouldBlock || insightPreview.wouldBlock) {
|
|
1785
|
+
return {
|
|
1786
|
+
content: [{
|
|
1787
|
+
type: "text",
|
|
1788
|
+
text: JSON.stringify({
|
|
1789
|
+
success: false,
|
|
1790
|
+
mode: "blocked",
|
|
1791
|
+
error: "Content contains restricted data that cannot be transmitted",
|
|
1792
|
+
blockReason: sessionPreview.blockReason || insightPreview.blockReason,
|
|
1793
|
+
detectedTypes: [...sessionPreview.detectedTypes, ...insightPreview.detectedTypes],
|
|
1794
|
+
hint: "Remove private keys, AWS credentials, or other restricted data before capturing.",
|
|
1795
|
+
}, null, 2)
|
|
1796
|
+
}],
|
|
1797
|
+
isError: true,
|
|
1798
|
+
};
|
|
1799
|
+
}
|
|
1800
|
+
// Dry-run mode - show sanitization preview without storing
|
|
1473
1801
|
if (dryRun) {
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
insight
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
hint: "CEDA may be unavailable. Try again later.",
|
|
1505
|
-
}, null, 2)
|
|
1506
|
-
}],
|
|
1507
|
-
};
|
|
1508
|
-
}
|
|
1802
|
+
return {
|
|
1803
|
+
content: [{
|
|
1804
|
+
type: "text",
|
|
1805
|
+
text: JSON.stringify({
|
|
1806
|
+
success: true,
|
|
1807
|
+
mode: "dry-run",
|
|
1808
|
+
message: "Preview of what would be captured (no data stored or transmitted)",
|
|
1809
|
+
feeling,
|
|
1810
|
+
sanitization: {
|
|
1811
|
+
session: {
|
|
1812
|
+
original: session,
|
|
1813
|
+
sanitized: sessionPreview.sanitized,
|
|
1814
|
+
wouldSanitize: sessionPreview.wouldSanitize,
|
|
1815
|
+
detectedTypes: sessionPreview.detectedTypes,
|
|
1816
|
+
classification: sessionPreview.classification,
|
|
1817
|
+
},
|
|
1818
|
+
insight: {
|
|
1819
|
+
original: insight,
|
|
1820
|
+
sanitized: insightPreview.sanitized,
|
|
1821
|
+
wouldSanitize: insightPreview.wouldSanitize,
|
|
1822
|
+
detectedTypes: insightPreview.detectedTypes,
|
|
1823
|
+
classification: insightPreview.classification,
|
|
1824
|
+
},
|
|
1825
|
+
},
|
|
1826
|
+
hint: insightPreview.wouldSanitize || sessionPreview.wouldSanitize
|
|
1827
|
+
? "Some content will be redacted. Consider using more abstract descriptions."
|
|
1828
|
+
: "Content looks clean. Safe to capture.",
|
|
1829
|
+
}, null, 2)
|
|
1830
|
+
}],
|
|
1831
|
+
};
|
|
1509
1832
|
}
|
|
1833
|
+
// Sanitize before transmission
|
|
1834
|
+
const sanitizedSession = sessionPreview.sanitized;
|
|
1835
|
+
const sanitizedInsight = insightPreview.sanitized;
|
|
1510
1836
|
// CEDA-64: Track reflection locally for session summary
|
|
1511
1837
|
addSessionReflection({
|
|
1512
1838
|
session,
|
|
@@ -1514,31 +1840,25 @@ Herald will:
|
|
|
1514
1840
|
insight,
|
|
1515
1841
|
method: "direct",
|
|
1516
1842
|
});
|
|
1517
|
-
// Call CEDA's reflect endpoint with
|
|
1843
|
+
// Call CEDA's reflect endpoint with SANITIZED insight
|
|
1518
1844
|
try {
|
|
1519
1845
|
const result = await callCedaAPI("/api/herald/reflect", "POST", {
|
|
1520
|
-
session,
|
|
1846
|
+
session: sanitizedSession,
|
|
1521
1847
|
feeling,
|
|
1522
|
-
insight, //
|
|
1848
|
+
insight: sanitizedInsight, // Sanitized - no PII/secrets transmitted
|
|
1523
1849
|
method: "direct", // Track capture method for meta-learning
|
|
1524
|
-
|
|
1850
|
+
company: HERALD_COMPANY,
|
|
1851
|
+
project: HERALD_PROJECT,
|
|
1525
1852
|
user: HERALD_USER,
|
|
1526
1853
|
vault: HERALD_VAULT || undefined,
|
|
1527
1854
|
});
|
|
1528
1855
|
if (result.error) {
|
|
1529
|
-
// If cloud fails, store locally for later processing
|
|
1530
|
-
const localRecord = {
|
|
1531
|
-
session,
|
|
1532
|
-
feeling,
|
|
1533
|
-
tags: CONTEXT_TAGS,
|
|
1534
|
-
user: HERALD_USER,
|
|
1535
|
-
timestamp: new Date().toISOString(),
|
|
1536
|
-
};
|
|
1537
|
-
// Buffer as insight for later sync
|
|
1856
|
+
// If cloud fails, store locally for later processing (also sanitized)
|
|
1538
1857
|
bufferInsight({
|
|
1539
|
-
insight: `[REFLECT:${feeling}] ${
|
|
1858
|
+
insight: `[REFLECT:${feeling}] ${sanitizedInsight} | Context: ${sanitizedSession}`,
|
|
1540
1859
|
topic: feeling === "stuck" ? "antipattern" : "pattern",
|
|
1541
|
-
|
|
1860
|
+
company: HERALD_COMPANY,
|
|
1861
|
+
project: HERALD_PROJECT,
|
|
1542
1862
|
user: HERALD_USER,
|
|
1543
1863
|
});
|
|
1544
1864
|
return {
|
|
@@ -1568,17 +1888,23 @@ Herald will:
|
|
|
1568
1888
|
message: feeling === "stuck"
|
|
1569
1889
|
? `Antipattern captured: "${insight}"`
|
|
1570
1890
|
: `Pattern captured: "${insight}"`,
|
|
1891
|
+
context: {
|
|
1892
|
+
company: HERALD_COMPANY,
|
|
1893
|
+
project: HERALD_PROJECT,
|
|
1894
|
+
autoDerived: CONTEXT_AUTO_DERIVED,
|
|
1895
|
+
},
|
|
1571
1896
|
...result,
|
|
1572
1897
|
}, null, 2)
|
|
1573
1898
|
}],
|
|
1574
1899
|
};
|
|
1575
1900
|
}
|
|
1576
1901
|
catch (error) {
|
|
1577
|
-
// Network error - buffer locally
|
|
1902
|
+
// Network error - buffer locally (sanitized)
|
|
1578
1903
|
bufferInsight({
|
|
1579
|
-
insight: `[REFLECT:${feeling}] ${
|
|
1904
|
+
insight: `[REFLECT:${feeling}] ${sanitizedInsight} | Context: ${sanitizedSession}`,
|
|
1580
1905
|
topic: feeling === "stuck" ? "antipattern" : "pattern",
|
|
1581
|
-
|
|
1906
|
+
company: HERALD_COMPANY,
|
|
1907
|
+
project: HERALD_PROJECT,
|
|
1582
1908
|
user: HERALD_USER,
|
|
1583
1909
|
});
|
|
1584
1910
|
return {
|
|
@@ -1589,29 +1915,60 @@ Herald will:
|
|
|
1589
1915
|
mode: "local",
|
|
1590
1916
|
message: "Reflection buffered locally (cloud unreachable)",
|
|
1591
1917
|
feeling,
|
|
1592
|
-
insight,
|
|
1918
|
+
insight: sanitizedInsight,
|
|
1593
1919
|
hint: "Use herald_sync when cloud recovers.",
|
|
1594
1920
|
buffered: true,
|
|
1921
|
+
sanitized: sessionPreview.wouldSanitize || insightPreview.wouldSanitize,
|
|
1595
1922
|
}, null, 2)
|
|
1596
1923
|
}],
|
|
1597
1924
|
};
|
|
1598
1925
|
}
|
|
1599
1926
|
}
|
|
1600
1927
|
case "herald_patterns": {
|
|
1601
|
-
// Query learned patterns
|
|
1928
|
+
// Query learned patterns with inheritance: user → project → company
|
|
1929
|
+
// More specific patterns take precedence over broader ones
|
|
1602
1930
|
try {
|
|
1603
|
-
|
|
1931
|
+
// Helper to dedupe patterns by insight text (first occurrence wins)
|
|
1932
|
+
const seenInsights = new Set();
|
|
1933
|
+
const dedupePatterns = (items, scope) => {
|
|
1934
|
+
return items.filter(item => {
|
|
1935
|
+
const key = item.insight.toLowerCase().trim();
|
|
1936
|
+
if (seenInsights.has(key))
|
|
1937
|
+
return false;
|
|
1938
|
+
seenInsights.add(key);
|
|
1939
|
+
return true;
|
|
1940
|
+
}).map(item => ({ ...item, scope }));
|
|
1941
|
+
};
|
|
1942
|
+
// Cascade queries: user (most specific) → project → company (broadest)
|
|
1943
|
+
const queries = [
|
|
1944
|
+
{ scope: "user", url: `/api/herald/reflections?company=${HERALD_COMPANY}&project=${HERALD_PROJECT}&user=${HERALD_USER}&limit=10` },
|
|
1945
|
+
{ scope: "project", url: `/api/herald/reflections?company=${HERALD_COMPANY}&project=${HERALD_PROJECT}&limit=10` },
|
|
1946
|
+
{ scope: "company", url: `/api/herald/reflections?company=${HERALD_COMPANY}&limit=10` },
|
|
1947
|
+
];
|
|
1948
|
+
const patterns = [];
|
|
1949
|
+
const antipatterns = [];
|
|
1950
|
+
// Query each level, dedupe as we go (user patterns win over project, project over company)
|
|
1951
|
+
for (const { scope, url } of queries) {
|
|
1952
|
+
try {
|
|
1953
|
+
const result = await callCedaAPI(url);
|
|
1954
|
+
const scopePatterns = result.patterns || [];
|
|
1955
|
+
const scopeAntipatterns = result.antipatterns || [];
|
|
1956
|
+
patterns.push(...dedupePatterns(scopePatterns, scope));
|
|
1957
|
+
antipatterns.push(...dedupePatterns(scopeAntipatterns, scope));
|
|
1958
|
+
}
|
|
1959
|
+
catch {
|
|
1960
|
+
// Continue if a level fails (e.g., user not set)
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1604
1963
|
const metaResult = await callCedaAPI("/api/herald/meta-patterns");
|
|
1605
|
-
// Format for Claude consumption
|
|
1606
|
-
const patterns = reflectionsResult.patterns || [];
|
|
1607
|
-
const antipatterns = reflectionsResult.antipatterns || [];
|
|
1608
1964
|
const metaPatterns = metaResult.metaPatterns || [];
|
|
1609
|
-
// Build readable summary
|
|
1610
|
-
let summary = `## Learned Patterns for ${HERALD_USER}→${
|
|
1965
|
+
// Build readable summary with scope indicators
|
|
1966
|
+
let summary = `## Learned Patterns for ${HERALD_USER}→${HERALD_PROJECT}→${HERALD_COMPANY}\n\n`;
|
|
1611
1967
|
if (antipatterns.length > 0) {
|
|
1612
1968
|
summary += `### ⚠️ Antipatterns (avoid these)\n`;
|
|
1613
1969
|
antipatterns.slice(0, 5).forEach((ap, i) => {
|
|
1614
|
-
|
|
1970
|
+
const scopeTag = ap.scope ? ` [${ap.scope}]` : "";
|
|
1971
|
+
summary += `${i + 1}. ${ap.insight}${scopeTag}`;
|
|
1615
1972
|
if (ap.warning)
|
|
1616
1973
|
summary += `\n → ${ap.warning}`;
|
|
1617
1974
|
summary += `\n`;
|
|
@@ -1621,7 +1978,8 @@ Herald will:
|
|
|
1621
1978
|
if (patterns.length > 0) {
|
|
1622
1979
|
summary += `### ✓ Patterns (do these)\n`;
|
|
1623
1980
|
patterns.slice(0, 5).forEach((p, i) => {
|
|
1624
|
-
|
|
1981
|
+
const scopeTag = p.scope ? ` [${p.scope}]` : "";
|
|
1982
|
+
summary += `${i + 1}. ${p.insight}${scopeTag}`;
|
|
1625
1983
|
if (p.reinforcement)
|
|
1626
1984
|
summary += `\n → ${p.reinforcement}`;
|
|
1627
1985
|
summary += `\n`;
|
|
@@ -1634,7 +1992,7 @@ Herald will:
|
|
|
1634
1992
|
summary += `Recommended capture method: ${meta.recommendedMethod} (${(meta.confidence * 100).toFixed(0)}% confidence)\n`;
|
|
1635
1993
|
}
|
|
1636
1994
|
if (patterns.length === 0 && antipatterns.length === 0) {
|
|
1637
|
-
summary = `No patterns learned yet for ${HERALD_USER}→${
|
|
1995
|
+
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.`;
|
|
1638
1996
|
}
|
|
1639
1997
|
return {
|
|
1640
1998
|
content: [{
|
|
@@ -1656,6 +2014,27 @@ Herald will:
|
|
|
1656
2014
|
const session = args?.session;
|
|
1657
2015
|
const feeling = args?.feeling;
|
|
1658
2016
|
const insight = args?.insight;
|
|
2017
|
+
// CEDA-65: Client-side sanitization
|
|
2018
|
+
const simSessionPreview = previewSanitization(session);
|
|
2019
|
+
const simInsightPreview = previewSanitization(insight);
|
|
2020
|
+
// Block restricted content
|
|
2021
|
+
if (simSessionPreview.wouldBlock || simInsightPreview.wouldBlock) {
|
|
2022
|
+
return {
|
|
2023
|
+
content: [{
|
|
2024
|
+
type: "text",
|
|
2025
|
+
text: JSON.stringify({
|
|
2026
|
+
success: false,
|
|
2027
|
+
mode: "blocked",
|
|
2028
|
+
error: "Content contains restricted data that cannot be transmitted",
|
|
2029
|
+
blockReason: simSessionPreview.blockReason || simInsightPreview.blockReason,
|
|
2030
|
+
hint: "Remove private keys, AWS credentials, or other restricted data before capturing.",
|
|
2031
|
+
}, null, 2)
|
|
2032
|
+
}],
|
|
2033
|
+
isError: true,
|
|
2034
|
+
};
|
|
2035
|
+
}
|
|
2036
|
+
const simSanitizedSession = simSessionPreview.sanitized;
|
|
2037
|
+
const simSanitizedInsight = simInsightPreview.sanitized;
|
|
1659
2038
|
// CEDA-64: Track reflection locally for session summary
|
|
1660
2039
|
addSessionReflection({
|
|
1661
2040
|
session,
|
|
@@ -1679,30 +2058,36 @@ Herald will:
|
|
|
1679
2058
|
};
|
|
1680
2059
|
}
|
|
1681
2060
|
try {
|
|
1682
|
-
// Build prompt and call AI for reflection
|
|
1683
|
-
const prompt = buildReflectionPrompt(
|
|
2061
|
+
// Build prompt and call AI for reflection (use sanitized input)
|
|
2062
|
+
const prompt = buildReflectionPrompt(simSanitizedSession, feeling, simSanitizedInsight);
|
|
1684
2063
|
const extracted = await callAIForReflection(aiClient, prompt);
|
|
1685
|
-
//
|
|
2064
|
+
// Sanitize AI-extracted fields too
|
|
2065
|
+
const sanitizedSignal = sanitize(extracted.signal || "").sanitizedText;
|
|
2066
|
+
const sanitizedReinforcement = extracted.reinforcement ? sanitize(extracted.reinforcement).sanitizedText : undefined;
|
|
2067
|
+
const sanitizedWarning = extracted.warning ? sanitize(extracted.warning).sanitizedText : undefined;
|
|
2068
|
+
// Send enriched data to CEDA (all sanitized)
|
|
1686
2069
|
const result = await callCedaAPI("/api/herald/reflect", "POST", {
|
|
1687
|
-
session,
|
|
2070
|
+
session: simSanitizedSession,
|
|
1688
2071
|
feeling,
|
|
1689
|
-
insight,
|
|
2072
|
+
insight: simSanitizedInsight,
|
|
1690
2073
|
method: "simulation", // Track capture method
|
|
1691
|
-
// AI-extracted fields
|
|
1692
|
-
signal:
|
|
2074
|
+
// AI-extracted fields (sanitized)
|
|
2075
|
+
signal: sanitizedSignal,
|
|
1693
2076
|
outcome: extracted.outcome,
|
|
1694
|
-
reinforcement:
|
|
1695
|
-
warning:
|
|
1696
|
-
|
|
2077
|
+
reinforcement: sanitizedReinforcement,
|
|
2078
|
+
warning: sanitizedWarning,
|
|
2079
|
+
company: HERALD_COMPANY,
|
|
2080
|
+
project: HERALD_PROJECT,
|
|
1697
2081
|
user: HERALD_USER,
|
|
1698
2082
|
vault: HERALD_VAULT || undefined,
|
|
1699
2083
|
});
|
|
1700
2084
|
if (result.error) {
|
|
1701
|
-
// Cloud failed but we have AI extraction - buffer with enriched data
|
|
2085
|
+
// Cloud failed but we have AI extraction - buffer with enriched data (sanitized)
|
|
1702
2086
|
bufferInsight({
|
|
1703
|
-
insight: `[SIMULATE:${feeling}] Signal: ${
|
|
2087
|
+
insight: `[SIMULATE:${feeling}] Signal: ${sanitizedSignal} | Insight: ${simSanitizedInsight} | ${extracted.outcome === "pattern" ? `Reinforce: ${sanitizedReinforcement}` : `Warn: ${sanitizedWarning}`}`,
|
|
1704
2088
|
topic: extracted.outcome,
|
|
1705
|
-
|
|
2089
|
+
company: HERALD_COMPANY,
|
|
2090
|
+
project: HERALD_PROJECT,
|
|
1706
2091
|
user: HERALD_USER,
|
|
1707
2092
|
});
|
|
1708
2093
|
return {
|
|
@@ -1809,7 +2194,8 @@ Herald will:
|
|
|
1809
2194
|
patternText,
|
|
1810
2195
|
outcome,
|
|
1811
2196
|
helped: outcome === "helped",
|
|
1812
|
-
|
|
2197
|
+
company: HERALD_COMPANY,
|
|
2198
|
+
project: HERALD_PROJECT,
|
|
1813
2199
|
user: HERALD_USER,
|
|
1814
2200
|
});
|
|
1815
2201
|
if (result.error) {
|
|
@@ -1859,7 +2245,8 @@ Herald will:
|
|
|
1859
2245
|
insight,
|
|
1860
2246
|
scope,
|
|
1861
2247
|
topic,
|
|
1862
|
-
|
|
2248
|
+
sourceCompany: HERALD_COMPANY,
|
|
2249
|
+
sourceProject: HERALD_PROJECT,
|
|
1863
2250
|
sourceUser: HERALD_USER,
|
|
1864
2251
|
sourceVault: HERALD_VAULT || undefined,
|
|
1865
2252
|
});
|
|
@@ -1929,7 +2316,8 @@ Herald will:
|
|
|
1929
2316
|
patternId,
|
|
1930
2317
|
sessionId,
|
|
1931
2318
|
all: deleteAll,
|
|
1932
|
-
|
|
2319
|
+
company: HERALD_COMPANY,
|
|
2320
|
+
project: HERALD_PROJECT,
|
|
1933
2321
|
user: HERALD_USER,
|
|
1934
2322
|
});
|
|
1935
2323
|
if (result.error) {
|
|
@@ -1952,7 +2340,7 @@ Herald will:
|
|
|
1952
2340
|
message = `All patterns from session ${sessionId} deleted`;
|
|
1953
2341
|
}
|
|
1954
2342
|
else if (deleteAll) {
|
|
1955
|
-
message = `All patterns for ${
|
|
2343
|
+
message = `All patterns for ${HERALD_COMPANY}/${HERALD_PROJECT}/${HERALD_USER} deleted`;
|
|
1956
2344
|
}
|
|
1957
2345
|
return {
|
|
1958
2346
|
content: [{
|
|
@@ -1982,7 +2370,7 @@ Herald will:
|
|
|
1982
2370
|
case "herald_export": {
|
|
1983
2371
|
const format = args?.format || "json";
|
|
1984
2372
|
try {
|
|
1985
|
-
const result = await callCedaAPI(`/api/herald/export?
|
|
2373
|
+
const result = await callCedaAPI(`/api/herald/export?company=${HERALD_COMPANY}&project=${HERALD_PROJECT}&user=${HERALD_USER}&format=${format}`);
|
|
1986
2374
|
if (result.error) {
|
|
1987
2375
|
return {
|
|
1988
2376
|
content: [{
|
|
@@ -2003,7 +2391,7 @@ Herald will:
|
|
|
2003
2391
|
message: `Data exported in ${format.toUpperCase()} format (GDPR Art. 20)`,
|
|
2004
2392
|
gdprArticle: "Article 20 - Right to Data Portability",
|
|
2005
2393
|
format,
|
|
2006
|
-
context: `${
|
|
2394
|
+
context: `${HERALD_COMPANY}/${HERALD_PROJECT}/${HERALD_USER}`,
|
|
2007
2395
|
...result,
|
|
2008
2396
|
}, null, 2)
|
|
2009
2397
|
}],
|
|
@@ -2050,8 +2438,8 @@ async function autoSyncBuffer() {
|
|
|
2050
2438
|
const result = await callCedaAPI("/api/herald/insight", "POST", {
|
|
2051
2439
|
insight: item.insight,
|
|
2052
2440
|
topic: item.topic,
|
|
2053
|
-
|
|
2054
|
-
|
|
2441
|
+
toContext: item.targetVault || "all", // CEDA expects toContext
|
|
2442
|
+
fromContext: item.sourceVault, // CEDA expects fromContext
|
|
2055
2443
|
});
|
|
2056
2444
|
if (result.error) {
|
|
2057
2445
|
failed.push(item);
|
|
@@ -2072,12 +2460,30 @@ async function autoSyncBuffer() {
|
|
|
2072
2460
|
console.error(`[Herald] ${failed.length} insight(s) failed - will retry on next startup`);
|
|
2073
2461
|
}
|
|
2074
2462
|
}
|
|
2463
|
+
async function sendStartupHeartbeat() {
|
|
2464
|
+
// Fire-and-forget heartbeat - don't block startup
|
|
2465
|
+
try {
|
|
2466
|
+
await callCedaAPI("/api/herald/heartbeat", "POST", {
|
|
2467
|
+
event: "startup",
|
|
2468
|
+
version: VERSION,
|
|
2469
|
+
user: HERALD_USER,
|
|
2470
|
+
tags: HERALD_TAGS,
|
|
2471
|
+
contextSource: CONTEXT_SOURCE,
|
|
2472
|
+
platform: process.platform,
|
|
2473
|
+
nodeVersion: process.version,
|
|
2474
|
+
});
|
|
2475
|
+
}
|
|
2476
|
+
catch {
|
|
2477
|
+
// Silent fail - don't block MCP startup
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2075
2480
|
async function runMCP() {
|
|
2076
2481
|
const transport = new StdioServerTransport();
|
|
2077
2482
|
await server.connect(transport);
|
|
2078
|
-
console.error(
|
|
2079
|
-
console.error(`
|
|
2080
|
-
|
|
2483
|
+
console.error(`Herald MCP v${VERSION} running`);
|
|
2484
|
+
console.error(`User: ${HERALD_USER} | Tags: [${HERALD_TAGS.join(", ")}] (${CONTEXT_SOURCE})`);
|
|
2485
|
+
// Send startup heartbeat for visibility (non-blocking)
|
|
2486
|
+
sendStartupHeartbeat();
|
|
2081
2487
|
// Auto-sync buffered insights on startup
|
|
2082
2488
|
await autoSyncBuffer();
|
|
2083
2489
|
}
|