@ema.co/mcp-toolkit 2026.2.27 → 2026.2.28

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.

Potentially problematic release.


This version of @ema.co/mcp-toolkit might be problematic. Click here for more details.

Files changed (58) hide show
  1. package/.context/public/guides/ema-user-guide.md +7 -6
  2. package/.context/public/guides/mcp-tools-guide.md +46 -23
  3. package/dist/config/index.js +11 -0
  4. package/dist/config/workflow-patterns.js +361 -0
  5. package/dist/mcp/autobuilder.js +2 -2
  6. package/dist/mcp/domain/generation-schema.js +15 -9
  7. package/dist/mcp/domain/structural-rules.js +3 -3
  8. package/dist/mcp/domain/validation-rules.js +20 -27
  9. package/dist/mcp/domain/workflow-generator.js +3 -3
  10. package/dist/mcp/domain/workflow-graph.js +1 -1
  11. package/dist/mcp/guidance.js +60 -1
  12. package/dist/mcp/handlers/conversation/adapter.js +13 -0
  13. package/dist/mcp/handlers/conversation/create.js +19 -0
  14. package/dist/mcp/handlers/conversation/delete.js +18 -0
  15. package/dist/mcp/handlers/conversation/formatters.js +62 -0
  16. package/dist/mcp/handlers/conversation/history.js +15 -0
  17. package/dist/mcp/handlers/conversation/index.js +43 -0
  18. package/dist/mcp/handlers/conversation/list.js +40 -0
  19. package/dist/mcp/handlers/conversation/messages.js +13 -0
  20. package/dist/mcp/handlers/conversation/rename.js +16 -0
  21. package/dist/mcp/handlers/conversation/send.js +90 -0
  22. package/dist/mcp/handlers/data/index.js +169 -3
  23. package/dist/mcp/handlers/feedback/client-id.js +49 -0
  24. package/dist/mcp/handlers/feedback/coalesce.js +167 -0
  25. package/dist/mcp/handlers/feedback/index.js +42 -1
  26. package/dist/mcp/handlers/feedback/outbox.js +301 -0
  27. package/dist/mcp/handlers/feedback/probes.js +127 -0
  28. package/dist/mcp/handlers/feedback/remote-store.js +59 -0
  29. package/dist/mcp/handlers/feedback/store.js +13 -1
  30. package/dist/mcp/handlers/persona/delete.js +7 -28
  31. package/dist/mcp/handlers/persona/update.js +7 -26
  32. package/dist/mcp/handlers/persona/version.js +30 -15
  33. package/dist/mcp/handlers/template/adapter.js +23 -0
  34. package/dist/mcp/handlers/template/crud.js +174 -0
  35. package/dist/mcp/handlers/template/index.js +6 -7
  36. package/dist/mcp/handlers/workflow/adapter.js +30 -46
  37. package/dist/mcp/handlers/workflow/index.js +2 -2
  38. package/dist/mcp/handlers/workflow/validation.js +2 -2
  39. package/dist/mcp/knowledge-guidance-topics.js +90 -53
  40. package/dist/mcp/knowledge.js +7 -357
  41. package/dist/mcp/prompts.js +5 -5
  42. package/dist/mcp/resources-dynamic.js +46 -38
  43. package/dist/mcp/resources-validation.js +5 -5
  44. package/dist/mcp/server.js +38 -5
  45. package/dist/mcp/tools.js +340 -8
  46. package/dist/sdk/client-adapter.js +90 -2
  47. package/dist/sdk/client.js +7 -0
  48. package/dist/sdk/ema-client.js +242 -27
  49. package/dist/sdk/generated/agent-catalog.js +96 -39
  50. package/dist/sdk/generated/deprecated-actions.js +1 -1
  51. package/dist/sdk/grpc-client.js +67 -5
  52. package/dist/sync/central-factory.js +86 -0
  53. package/dist/sync/central-version-storage.js +387 -0
  54. package/dist/sync/dis-port.js +75 -0
  55. package/dist/sync/version-policy.js +29 -31
  56. package/dist/sync/version-storage-interface.js +11 -0
  57. package/dist/sync/version-storage.js +22 -22
  58. package/package.json +2 -1
@@ -974,19 +974,17 @@ Chat workflows that process \`chat_conversation\` through search, extraction, or
974
974
 
975
975
  ## Correct Patterns
976
976
 
977
- ### Pattern 1: Search + Respond (most common)
977
+ ### Pattern 1: Search + Respond (most common — 89% of production)
978
978
  \`\`\`
979
979
  chat_trigger.chat_conversation → conversation_to_search_query.conversation
980
980
  conversation_to_search_query.summarized_conversation → search.query
981
- search.search_results → respond_for_external_actions.external_action_result
982
- chat_trigger.user_query → respond_for_external_actions.query
983
- chat_trigger.chat_conversation → respond_for_external_actions.conversation
984
- respond_for_external_actions.response → WORKFLOW_OUTPUT
981
+ search.search_results → call_llm.named_inputs_Search_Results
982
+ chat_trigger.user_query → call_llm.query
983
+ chat_trigger.chat_conversation → call_llm.named_inputs_Conversation
984
+ call_llm.response_with_sources → WORKFLOW_OUTPUT
985
985
  \`\`\`
986
986
 
987
- > **Note**: \`respond_with_sources/v0\` is DEPRECATED. Use \`respond_for_external_actions\` for all new workflows (search results, tool results, or any context-aware response).
988
-
989
- ### Pattern 2: Tool/Action + Respond
987
+ ### Pattern 2: Tool/Action + Respond (uses respond_for_external_actions)
990
988
  \`\`\`
991
989
  chat_trigger.chat_conversation → external_action_caller.conversation
992
990
  external_action_caller.tool_execution_result → respond_for_external_actions.external_action_result
@@ -995,12 +993,14 @@ chat_trigger.chat_conversation → respond_for_external_actions.conversation
995
993
  respond_for_external_actions.response → WORKFLOW_OUTPUT
996
994
  \`\`\`
997
995
 
998
- ### Pattern 3: call_llm with Conversation Context (advanced)
999
- If you must use \`call_llm\` as the terminal responder, you **must** wire \`chat_conversation\` into its \`named_inputs\`:
996
+ > **Note**: \`respond_for_external_actions\` is ONLY for explaining \`external_action_caller\` results (Salesforce, ServiceNow, etc). For KB Q&A after search, use \`call_llm\` with \`named_inputs\`.
997
+
998
+ ### Pattern 3: Search + Respond with Citations
1000
999
  \`\`\`
1001
- chat_trigger.chat_conversationcall_llm.named_inputs (key: "conversation_history")
1000
+ search.search_resultsrespond_with_sources.search_results
1001
+ chat_trigger.user_query → respond_with_sources.query
1002
+ respond_with_sources.response → WORKFLOW_OUTPUT
1002
1003
  \`\`\`
1003
- This ensures the LLM sees prior turns.
1004
1004
 
1005
1005
  ## Anti-Patterns
1006
1006
 
@@ -1009,6 +1009,13 @@ This ensures the LLM sees prior turns.
1009
1009
  chat_trigger → search → call_llm (no conversation) → WORKFLOW_OUTPUT
1010
1010
  \`\`\`
1011
1011
  **Problem**: call_llm only sees search results, not prior conversation. Repeats questions.
1012
+ **Fix**: Wire \`chat_conversation\` into \`call_llm.named_inputs_Conversation\`.
1013
+
1014
+ ### ❌ respond_for_external_actions for KB Q&A
1015
+ \`\`\`
1016
+ chat_trigger → search → respond_for_external_actions → WORKFLOW_OUTPUT
1017
+ \`\`\`
1018
+ **Problem**: \`respond_for_external_actions\` is designed for \`external_action_caller\` tool results, not search results. Use \`call_llm\` with \`named_inputs_Search_Results\` instead.
1012
1019
 
1013
1020
  ### ❌ Entity Extraction → call_llm (no history)
1014
1021
  \`\`\`
@@ -1016,19 +1023,14 @@ chat_trigger → entity_extraction → call_llm (stateless) → WORKFLOW_OUTPUT
1016
1023
  \`\`\`
1017
1024
  **Problem**: LLM extracts entities but response node doesn't know what was already discussed.
1018
1025
 
1019
- ## Key Insight
1020
-
1021
- \`respond_for_external_actions\` is **conversation-aware by design** — it incorporates conversation context via its \`conversation\` input. Generic \`call_llm\` does not — you must manually wire \`chat_conversation\` via \`named_inputs\`.
1022
-
1023
- > **Deprecated**: \`respond_with_sources/v0\` — use \`respond_for_external_actions\` for all new workflows.
1024
-
1025
1026
  ## When to Use Each Response Node
1026
1027
 
1027
1028
  | Scenario | Response Node | Why |
1028
1029
  |----------|---------------|-----|
1029
- | KB/document Q&A | \`respond_for_external_actions\` | Handles search results, conversation-aware |
1030
- | Tool/API results | \`respond_for_external_actions\` | Explains tool results in conversation context |
1031
- | Complex multi-step reasoning | \`call_llm/v2\` + named_inputs with chat_conversation | Full control but requires manual history wiring |
1030
+ | KB/document Q&A | \`call_llm\` with \`named_inputs_Search_Results\` | Most common (89%), full control via named_inputs |
1031
+ | KB Q&A with citations | \`respond_with_sources\` | Automatic citation grounding |
1032
+ | External tool results | \`respond_for_external_actions\` | ONLY for \`external_action_caller\` outputs |
1033
+ | Complex multi-step reasoning | \`custom_agent\` | Role context + multi-step reasoning |
1032
1034
  | Static/template response | \`fixed_response/v1\` | No LLM needed, just template + variables |
1033
1035
  `;
1034
1036
  },
@@ -1239,10 +1241,10 @@ When you already have a clean extracted value and just need TEXT_WITH_SOURCES wr
1239
1241
  md += `- **Outputs**: \`response_with_sources\` (TEXT_WITH_SOURCES)\n`;
1240
1242
  md += `- **Critical**: Must wire to WORKFLOW_OUTPUT or response is lost. \`call_llm/v0\` is deprecated.\n\n`;
1241
1243
  md += `### respond_for_external_actions\n`;
1242
- md += `Generate conversation-aware response from search results or tool outputs.\n`;
1243
- md += `- **Inputs**: \`query\` (TEXT_WITH_SOURCES), \`conversation\` (CHAT_CONVERSATION), \`external_action_result\` (TEXT_WITH_SOURCES/SEARCH_RESULT)\n`;
1244
+ md += `Explain results from external_action_caller (Salesforce, ServiceNow, etc) in conversation context.\n`;
1245
+ md += `- **Inputs**: \`query\` (TEXT_WITH_SOURCES), \`conversation\` (CHAT_CONVERSATION), \`external_action_result\` (from external_action_caller)\n`;
1244
1246
  md += `- **Outputs**: \`response\` (TEXT_WITH_SOURCES)\n`;
1245
- md += `- **Critical**: Replaces deprecated \`respond_with_sources/v0\`. Has built-in citation handling and conversation awareness.\n\n`;
1247
+ md += `- **Critical**: ONLY use after \`external_action_caller\`. For KB Q&A after search, use \`call_llm\` with \`named_inputs_Search_Results\`.\n\n`;
1246
1248
  md += `### fixed_response/v1\n`;
1247
1249
  md += `Return a static predefined message.\n`;
1248
1250
  md += `- **Inputs**: \`trigger_when\` (ENUM), \`named_inputs_*\` for template variables\n`;
@@ -1280,12 +1282,12 @@ When you already have a clean extracted value and just need TEXT_WITH_SOURCES wr
1280
1282
  md += `- **Inputs**: \`query\` (TEXT_WITH_SOURCES)\n`;
1281
1283
  md += `- **Outputs**: \`web_search_results\` (SEARCH_RESULT)\n`;
1282
1284
  md += `- **Critical**: No data upload needed (searches the web).\n\n`;
1283
- md += `### combine_search_results\n`;
1284
- md += `Merge results from multiple search sources with deduplication.\n`;
1285
- md += `- **Inputs**: \`search_results_1\` (SEARCH_RESULT), \`search_results_2\` (SEARCH_RESULT)\n`;
1286
- md += `- **Outputs**: \`combined_results\` (SEARCH_RESULT)\n`;
1287
- md += `- **Pairs with**: search/v2 + live_web_search, or any two search sources\n`;
1288
- md += `- **Note**: \`combine_search_results/v0\` is deprecated. For new workflows, prefer \`call_llm\` with multiple \`named_inputs\` to combine search results.\n\n`;
1285
+ md += `### combine_and_rerank_search_results\n`;
1286
+ md += `Merge results from multiple search sources with relevance-based reranking.\n`;
1287
+ md += `- **Inputs**: Multiple search result inputs (SEARCH_RESULT)\n`;
1288
+ md += `- **Outputs**: Reranked combined results (SEARCH_RESULT)\n`;
1289
+ md += `- **When**: Need weighted/prioritized results from multiple sources, or need to ensure certain results rank higher\n`;
1290
+ md += `- **Alternative**: \`call_llm\` with multiple \`named_inputs\` gives prompt-level control over how results are synthesized.\n\n`;
1289
1291
  md += `### response_validator\n`;
1290
1292
  md += `Validate LLM output against quality/compliance criteria.\n`;
1291
1293
  md += `- **Inputs**: \`reference_query\` (TEXT_WITH_SOURCES), \`response_to_validate\` (TEXT_WITH_SOURCES)\n`;
@@ -1420,15 +1422,16 @@ When you already have a clean extracted value and just need TEXT_WITH_SOURCES wr
1420
1422
  { name: "vadSettings", type: 42, purpose: "Voice activity detection, timeouts" },
1421
1423
  ],
1422
1424
  common_patterns: {
1423
- simple_qa: "chat_trigger → search → respond_for_external_actions → WORKFLOW_OUTPUT",
1425
+ simple_qa: "chat_trigger → search → call_llm (via named_inputs_Search_Results) → WORKFLOW_OUTPUT",
1424
1426
  with_routing: "chat_trigger → chat_categorizer → [branch per intent] → respond → WORKFLOW_OUTPUT",
1425
1427
  with_tools: "chat_trigger → categorizer → external_action_caller → respond_for_external_actions → WORKFLOW_OUTPUT",
1426
1428
  },
1427
1429
  best_practices: [
1428
1430
  "Use search/v2 (NOT v0) with datastore_configs",
1429
1431
  "Include Fallback category in every categorizer",
1430
- "Use respond_for_external_actions (NOT deprecated respond_with_sources)",
1431
- "For approval workflows: enable HITL flag on send_email_agent or entity_extraction_with_documents (only nodes that support HITL). general_hitl is NOT deployable.",
1432
+ "For KB Q&A after search: use call_llm with named_inputs_Search_Results (89% of production)",
1433
+ "respond_for_external_actions is ONLY for external_action_caller results (Salesforce, etc)",
1434
+ "For approval workflows: use general_hitl (Human Collaboration Agent) or HITL flags on send_email_agent/entity_extraction_with_documents",
1432
1435
  ],
1433
1436
  _next_step: "Call workflow(mode='get', persona_id='...') to get full schema, then generate workflow_def",
1434
1437
  }, null, 2);
@@ -1549,7 +1552,7 @@ Wire any custom widget to a search node: \`search/v2\` → \`datastore_configs\`
1549
1552
  | Deprecated | Use Instead |
1550
1553
  |------------|-------------|
1551
1554
  | search/v0 | search/v2 (requires datastore_configs) |
1552
- | respond_with_sources | respond_for_external_actions |
1555
+ | respond_with_sources/v0 | call_llm with named_inputs (for KB Q&A) or respond_with_sources current version (for citations) |
1553
1556
  | call_llm/v0 | call_llm/v2 |
1554
1557
 
1555
1558
  ## Anti-Patterns
@@ -1791,15 +1794,20 @@ interface ResultMapping {
1791
1794
  },
1792
1795
  {
1793
1796
  "id": "respond",
1794
- "actionType": "respond_for_external_actions",
1797
+ "actionType": "call_llm",
1795
1798
  "displayName": "Generate Response",
1796
1799
  "inputs": {
1797
- "search_results": {
1800
+ "query": {
1801
+ "type": "action_output",
1802
+ "actionName": "trigger",
1803
+ "output": "user_query"
1804
+ },
1805
+ "named_inputs_Search_Results": {
1798
1806
  "type": "action_output",
1799
1807
  "actionName": "search",
1800
1808
  "output": "search_results"
1801
1809
  },
1802
- "conversation": {
1810
+ "named_inputs_Conversation": {
1803
1811
  "type": "action_output",
1804
1812
  "actionName": "trigger",
1805
1813
  "output": "chat_conversation"
@@ -1808,7 +1816,7 @@ interface ResultMapping {
1808
1816
  }
1809
1817
  ],
1810
1818
  "resultMappings": [
1811
- { "nodeId": "respond", "output": "response" }
1819
+ { "nodeId": "respond", "output": "response_with_sources" }
1812
1820
  ]
1813
1821
  }
1814
1822
  \`\`\`
@@ -59,9 +59,9 @@ These rules are extracted from the Go validator's static validation logic. Follo
59
59
  **Good Pattern**:
60
60
  \`\`\`json
61
61
  {
62
- "path": ["trigger", "categorizer", "billing_branch", "search", "respond_for_external_actions"],
62
+ "path": ["trigger", "categorizer", "billing_branch", "search", "respond"],
63
63
  "named_result": "response",
64
- "produces": ["response"] // ✅ Produced
64
+ "produces": ["response"] // ✅ Produced (respond is call_llm with named_inputs)
65
65
  }
66
66
  \`\`\`
67
67
 
@@ -75,8 +75,8 @@ These rules are extracted from the Go validator's static validation logic. Follo
75
75
  3. Connect the node's output to WORKFLOW_OUTPUT
76
76
 
77
77
  **Common Fixes**:
78
- - Add \`respond_for_external_actions\` node after search
79
- - Add \`call_llm\` node for text generation
78
+ - Add \`call_llm\` node with \`named_inputs_Search_Results\` after search (most common)
79
+ - Add \`respond_with_sources\` for citation-grounded responses
80
80
  - Add \`fixed_response\` for static responses
81
81
 
82
82
  **Prevention** (During Generation):
@@ -183,7 +183,7 @@ These rules are extracted from the Go validator's static validation logic. Follo
183
183
  **Good Pattern**:
184
184
  \`\`\`json
185
185
  {
186
- "path": ["trigger", "categorizer", "billing", "search", "respond_for_external_actions"],
186
+ "path": ["trigger", "categorizer", "billing", "search", "respond"],
187
187
  "has_response": true // ✅ Has response
188
188
  }
189
189
  \`\`\`
@@ -29,25 +29,28 @@ import { generateTools } from "./tools.js";
29
29
  // Handler imports (simple 1-2 line handlers stay inline)
30
30
  import { handleEnv } from "./handlers/env/index.js";
31
31
  import { handleAction } from "./handlers/action/index.js";
32
- import { handleTemplate } from "./handlers/template/index.js";
33
32
  import { handleReference } from "./handlers/reference/index.js";
34
33
  import { handleCatalog } from "./handlers/catalog/index.js";
35
34
  import { handleFeedback } from "./handlers/feedback/index.js";
36
35
  import { recordTelemetry } from "./handlers/feedback/store.js";
36
+ import { flushOutbox, startFlushTimer, stopFlushTimer } from "./handlers/feedback/outbox.js";
37
+ import { maybeInjectProbe } from "./handlers/feedback/probes.js";
37
38
  // V2 adapters (extracted from server.ts — each owns its routing/transformation logic)
38
39
  import { handlePersonaAdapter } from "./handlers/persona/adapter.js";
39
40
  import { handleWorkflowAdapter } from "./handlers/workflow/adapter.js";
40
41
  import { handleSyncAdapter } from "./handlers/sync/adapter.js";
41
42
  import { handleDemoAdapter } from "./handlers/demo/adapter.js";
42
43
  import { handleDebugAdapter } from "./handlers/debug/adapter.js";
44
+ import { handleConversationAdapter } from "./handlers/conversation/adapter.js";
45
+ import { handleTemplateAdapter } from "./handlers/template/adapter.js";
43
46
  // Start token initialization in background (non-blocking)
44
47
  void initializeApiKeyTokens();
45
48
  // ─────────────────────────────────────────────────────────────────────────────
46
49
  // Tool Definitions
47
50
  // ─────────────────────────────────────────────────────────────────────────────
48
51
  //
49
- // V2 TOOLS (7 tools) - LLM-optimized minimal interface
50
- // - env, persona, catalog, workflow, sync, toolkit_feedback, debug
52
+ // V2 TOOLS (9 tools) - LLM-optimized minimal interface
53
+ // - env, persona, catalog, workflow, sync, toolkit_feedback, debug, conversation, template
51
54
  // - Defined in: ./tools.ts
52
55
  //
53
56
  // NAMING CONVENTION:
@@ -58,7 +61,7 @@ void initializeApiKeyTokens();
58
61
  /**
59
62
  * Generate all available tools
60
63
  *
61
- * V2: 7 tools (persona, catalog, workflow, sync, env, toolkit_feedback, debug) - LLM-optimized
64
+ * V2: 9 tools (persona, catalog, workflow, sync, env, toolkit_feedback, debug, conversation, template) - LLM-optimized
62
65
  *
63
66
  * Why V2:
64
67
  * - Minimal tool count optimizes LLM tool selection
@@ -73,6 +76,8 @@ function generateAllTools() {
73
76
  }
74
77
  // Generate tools (called once at module load)
75
78
  const TOOLS = generateAllTools();
79
+ // Track whether the first-session opt-out notice has been shown
80
+ let firstNoticeShown = false;
76
81
  const toolHandlers = {
77
82
  // ═══════════════════════════════════════════════════════════════════════════
78
83
  // V2 TOOLS — LLM-optimized interface
@@ -91,7 +96,7 @@ const toolHandlers = {
91
96
  return handleAction(args, client);
92
97
  },
93
98
  template: async (args) => {
94
- return handleTemplate(args);
99
+ return handleTemplateAdapter(args, createClient, getDefaultEnvName);
95
100
  },
96
101
  data: async (args) => {
97
102
  const client = createClient(args.env);
@@ -138,6 +143,9 @@ const toolHandlers = {
138
143
  debug: async (args) => {
139
144
  return handleDebugAdapter(args, createClient, getDefaultEnvName);
140
145
  },
146
+ conversation: async (args) => {
147
+ return handleConversationAdapter(args, createClient, getDefaultEnvName);
148
+ },
141
149
  };
142
150
  // ─────────────────────────────────────────────────────────────────────────────
143
151
  // Helpers
@@ -172,6 +180,12 @@ function determineOperation(toolName, args) {
172
180
  if (toolName === "debug") {
173
181
  return args.method ? String(args.method) : "conversations";
174
182
  }
183
+ if (toolName === "conversation") {
184
+ return args.method ? String(args.method) : "create";
185
+ }
186
+ if (toolName === "template") {
187
+ return args.method ? String(args.method) : "list";
188
+ }
175
189
  return toolName;
176
190
  }
177
191
  // ─────────────────────────────────────────────────────────────────────────────
@@ -238,6 +252,15 @@ export async function startMcpServer() {
238
252
  if (toolGuidance?.nextSteps?.[operation]) {
239
253
  response._next_step = toolGuidance.nextSteps[operation];
240
254
  }
255
+ // Inject targeted probe question if conditions are met
256
+ maybeInjectProbe(response, name, operation);
257
+ // First-session opt-out notice (shown once)
258
+ if (!firstNoticeShown && name !== "toolkit_feedback") {
259
+ response._notice =
260
+ "This toolkit collects anonymous usage telemetry to improve quality. " +
261
+ "Set EMA_FEEDBACK_DISABLED=1 to opt out. See ~/.ema-mcp/outbox/sent-log.jsonl for transparency.";
262
+ firstNoticeShown = true;
263
+ }
241
264
  return {
242
265
  content: [{ type: "text", text: JSON.stringify(response, null, 2) }],
243
266
  };
@@ -333,6 +356,16 @@ export async function startMcpServer() {
333
356
  : TOOLKIT_VERSION;
334
357
  const toolCount = TOOLS.length;
335
358
  console.error(`Ema MCP Server started: ${TOOLKIT_NAME}@${buildInfo} | ${toolCount} tools`);
359
+ // Flush any pending outbox entries from previous sessions (catches npx ephemeral runs)
360
+ flushOutbox(TOOLKIT_VERSION).catch(() => { });
361
+ startFlushTimer(TOOLKIT_VERSION);
362
+ // Graceful shutdown — flush remaining outbox entries before exit
363
+ const gracefulShutdown = () => {
364
+ stopFlushTimer(TOOLKIT_VERSION).catch(() => { });
365
+ };
366
+ process.on("SIGTERM", gracefulShutdown);
367
+ process.on("SIGINT", gracefulShutdown);
368
+ process.on("beforeExit", gracefulShutdown);
336
369
  }
337
370
  // CLI support: allow --help to exit (used by build:verify)
338
371
  const argv = process.argv.slice(2);