@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/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-67: AI-native context - derived from environment, not configured
24
- // User identity (optional override, defaults to system user)
25
- const HERALD_USER = process.env.HERALD_USER || process.env.USER || "default";
26
- /**
27
- * CEDA-67: Derive context tags from current working directory
28
- * Example: /Users/john/Documents/aegis_ceda/herald-mcp → ["aegis_ceda", "herald-mcp"]
29
- */
30
- function deriveContextTags() {
31
- const cwd = process.cwd();
32
- const parts = cwd.split("/").filter(p => p &&
33
- !p.startsWith(".") &&
34
- !["Users", "Documents", "home", "~", "var", "tmp"].includes(p));
35
- // Take last 3 meaningful path segments as tags
36
- return parts.slice(-3).map(p => p.toLowerCase().replace(/[^a-z0-9-]/g, "-"));
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
- // Derived context (computed once at startup)
39
- const CONTEXT_TAGS = deriveContextTags();
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.28.2";
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 - user-based paths (tags handle context, not folders)
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 `${HERALD_USER}→${CONTEXT_TAGS.join("→")}`;
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
- // CEDA-67: Add tags for context-aware endpoints
424
- const needsContextParams = endpoint.startsWith("/api/patterns") ||
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" && needsContextParams) {
528
+ if (method === "GET" && needsTenantParams) {
428
529
  const separator = endpoint.includes("?") ? "&" : "?";
429
- url += `${separator}tags=${encodeURIComponent(CONTEXT_TAGS.join(","))}&user=${HERALD_USER}`;
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
- tags: CONTEXT_TAGS,
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
- CEDA_URL CEDA server URL (default: https://getceda.com)
499
- HERALD_USER User identity (default: system username)
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, for backwards compatibility)" },
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
- DRY RUN MODE (CEDA-65):
824
- Set dry_run=true to preview what would be captured without storing.
825
- Shows sanitization results and data classification.`,
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
- - Tags: ${CONTEXT_TAGS.join(", ")}
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
- tags: CONTEXT_TAGS,
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 (CONTEXT_TAGS.length === 0)
1142
- warnings.push("No context tags derived - check working directory");
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
- tags: CONTEXT_TAGS,
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
- targetVault: item.targetVault,
1357
- sourceVault: item.sourceVault,
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
- // Use tags by default, fall back to company/project if explicitly provided
1411
- if (company || project) {
1412
- if (company)
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: Dry-run mode - preview what would be captured without storing
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
- try {
1475
- const result = await callCedaAPI("/api/herald/reflect/dry-run", "POST", {
1476
- session,
1477
- feeling,
1478
- insight,
1479
- tags: CONTEXT_TAGS,
1480
- user: HERALD_USER,
1481
- });
1482
- return {
1483
- content: [{
1484
- type: "text",
1485
- text: JSON.stringify({
1486
- success: true,
1487
- mode: "dry-run",
1488
- message: "Preview of what would be captured (no data stored)",
1489
- feeling,
1490
- insight,
1491
- ...result,
1492
- }, null, 2)
1493
- }],
1494
- };
1495
- }
1496
- catch (error) {
1497
- return {
1498
- content: [{
1499
- type: "text",
1500
- text: JSON.stringify({
1501
- success: false,
1502
- mode: "dry-run",
1503
- error: `Dry-run failed: ${error}`,
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 user's insight
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, // User-provided insight - the actual pattern
1848
+ insight: sanitizedInsight, // Sanitized - no PII/secrets transmitted
1523
1849
  method: "direct", // Track capture method for meta-learning
1524
- tags: CONTEXT_TAGS,
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}] ${insight} | Context: ${session}`,
1858
+ insight: `[REFLECT:${feeling}] ${sanitizedInsight} | Context: ${sanitizedSession}`,
1540
1859
  topic: feeling === "stuck" ? "antipattern" : "pattern",
1541
- tags: CONTEXT_TAGS,
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}] ${insight} | Context: ${session}`,
1904
+ insight: `[REFLECT:${feeling}] ${sanitizedInsight} | Context: ${sanitizedSession}`,
1580
1905
  topic: feeling === "stuck" ? "antipattern" : "pattern",
1581
- tags: CONTEXT_TAGS,
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 for current context
1928
+ // Query learned patterns with inheritance: user → project → company
1929
+ // More specific patterns take precedence over broader ones
1602
1930
  try {
1603
- const reflectionsResult = await callCedaAPI(`/api/herald/reflections?tags=${encodeURIComponent(CONTEXT_TAGS.join(","))}&user=${HERALD_USER}&limit=20`);
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}→${CONTEXT_TAGS.join("→")}\n\n`;
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
- summary += `${i + 1}. ${ap.insight}`;
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
- summary += `${i + 1}. ${p.insight}`;
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}→${CONTEXT_TAGS.join("→")}.\n\nCapture patterns with "herald reflect" or "herald simulate" when you notice friction or flow.`;
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(session, feeling, insight);
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
- // Send enriched data to CEDA
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: extracted.signal,
2074
+ // AI-extracted fields (sanitized)
2075
+ signal: sanitizedSignal,
1693
2076
  outcome: extracted.outcome,
1694
- reinforcement: extracted.reinforcement,
1695
- warning: extracted.warning,
1696
- tags: CONTEXT_TAGS,
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: ${extracted.signal} | Insight: ${insight} | ${extracted.outcome === "pattern" ? `Reinforce: ${extracted.reinforcement}` : `Warn: ${extracted.warning}`}`,
2087
+ insight: `[SIMULATE:${feeling}] Signal: ${sanitizedSignal} | Insight: ${simSanitizedInsight} | ${extracted.outcome === "pattern" ? `Reinforce: ${sanitizedReinforcement}` : `Warn: ${sanitizedWarning}`}`,
1704
2088
  topic: extracted.outcome,
1705
- tags: CONTEXT_TAGS,
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
- tags: CONTEXT_TAGS,
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
- sourceTags: CONTEXT_TAGS,
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
- tags: CONTEXT_TAGS,
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 ${HERALD_USER}→${CONTEXT_TAGS.join("→")} deleted`;
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?tags=${encodeURIComponent(CONTEXT_TAGS.join(","))}&user=${HERALD_USER}&format=${format}`);
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: `${HERALD_USER}→${CONTEXT_TAGS.join("→")}`,
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
- targetVault: item.targetVault,
2054
- sourceVault: item.sourceVault,
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("Herald MCP server running on stdio");
2079
- console.error(`Context: ${HERALD_USER}→${CONTEXT_TAGS.join("")}`);
2080
- console.error("Tip: Call herald_patterns() to load learned patterns from past sessions");
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
  }