@ctrl-spc/cs 0.7.8 → 0.7.9

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/mcp.js CHANGED
@@ -14,8 +14,8 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
14
14
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
15
15
  import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
16
16
  import { z } from 'zod';
17
- import { workflowToolPermission, validateWorkflowToolTarget, WORKFLOW_TOOL_TEACHING, assertAgentToolMentions } from './workflows.js';
18
- import { workflowToolForName } from './workflow-tool-mentions.js';
17
+ import { workflowToolPermission, validateWorkflowToolTarget, WORKFLOW_TOOL_TEACHING, WORKFLOW_AUTHORING_TEACHING, authorToolMentions } from './workflows.js';
18
+ import { workflowToolForName, validateApprovalAction } from './workflow-tool-mentions.js';
19
19
  import { TOOLS_SERVER_PORT, SESSION_TTL_MS } from './env.js';
20
20
  import { agentPath } from './agents.js';
21
21
  import { mcpToken, readMcpToken, readSession } from './config.js';
@@ -7802,24 +7802,19 @@ export async function readWorkflowLibraryHandler(client, args, userId) {
7802
7802
  // workflow ids actually present rather than as PostgREST embeds, the same
7803
7803
  // two-step `listClientsHandler` uses.
7804
7804
  const workflowIds = workflows.map((workflow) => workflow.id);
7805
- const links = workflowIds.length
7806
- ? ((await must(client
7807
- .from('cliv2_workflow_stages_in_workflow')
7808
- .select('workflow_id, stage_id, position')
7809
- .in('workflow_id', workflowIds))) ?? [])
7810
- : [];
7811
- const exits = workflowIds.length
7812
- ? ((await must(client
7813
- .from('cliv2_workflow_stage_exits')
7814
- .select('workflow_id, stage_id, to_stage_id, condition, position')
7815
- .in('workflow_id', workflowIds))) ?? [])
7816
- : [];
7817
- const branches = workflowIds.length
7818
- ? ((await must(client
7819
- .from('cliv2_workflow_branches')
7820
- .select('workflow_id, when_condition, runs_workflow_id, position')
7821
- .in('workflow_id', workflowIds))) ?? [])
7822
- : [];
7805
+ const links = [];
7806
+ const exits = [];
7807
+ const branches = [];
7808
+ // Like artifact counts above, bound GET filters so a large library remains readable.
7809
+ for (let offset = 0; offset < workflowIds.length; offset += COUNT_BATCH) {
7810
+ const ids = workflowIds.slice(offset, offset + COUNT_BATCH);
7811
+ links.push(...(await must(client.from('cliv2_workflow_stages_in_workflow')
7812
+ .select('workflow_id, stage_id, position').in('workflow_id', ids))) ?? []);
7813
+ exits.push(...(await must(client.from('cliv2_workflow_stage_exits')
7814
+ .select('workflow_id, stage_id, to_stage_id, condition, position').in('workflow_id', ids))) ?? []);
7815
+ branches.push(...(await must(client.from('cliv2_workflow_branches')
7816
+ .select('workflow_id, when_condition, runs_workflow_id, position').in('workflow_id', ids))) ?? []);
7817
+ }
7823
7818
  const orgIds = [
7824
7819
  ...new Set([...stages.map((s) => s.org_id), ...workflows.map((w) => w.org_id)]),
7825
7820
  ];
@@ -7952,7 +7947,7 @@ export async function buildWorkflowHandler(client, args, fromAgent) {
7952
7947
  prose.push([`stages[${index}].description`, stage.description]);
7953
7948
  }
7954
7949
  if (typeof stage.body === 'string') {
7955
- assertAgentToolMentions(stage.body);
7950
+ stage.body = authorToolMentions(stage.body);
7956
7951
  prose.push([`stages[${index}].body`, stage.body]);
7957
7952
  }
7958
7953
  });
@@ -8050,7 +8045,7 @@ export async function editWorkflowHandler(client, args, fromAgent) {
8050
8045
  prose.push([`stages[${index}].description`, stage.description]);
8051
8046
  }
8052
8047
  if (typeof stage.body === 'string') {
8053
- assertAgentToolMentions(stage.body);
8048
+ stage.body = authorToolMentions(stage.body);
8054
8049
  prose.push([`stages[${index}].body`, stage.body]);
8055
8050
  }
8056
8051
  });
@@ -8693,6 +8688,7 @@ export const TOOL_NAMES = [
8693
8688
  // 18c Slice 1: the run's own activity, unasked, on the request's card.
8694
8689
  'report_activity',
8695
8690
  'ask_question',
8691
+ 'require_approval',
8696
8692
  'present_wireframes',
8697
8693
  'present_mocks',
8698
8694
  'resolve_feedback',
@@ -8801,6 +8797,7 @@ const SERVER_INSTRUCTIONS = `You are connected to CTRL+SPC, where this user's wo
8801
8797
  - The PLAN for a work item: create_stage and create_step to lay it out, then update_step to record progress — in_progress when you start, a rewritten next_action as it changes, done when finished. Never create a second step to report on the first. These need a WORK ITEM and an approved scope: they are its committed plan, not a log of what you did. A panel request with no work item has no plan to write, so use report_activity and, if the work deserves an item of its own, ask for one. Never attach it to an unrelated work item to get past a refusal.
8802
8798
  - Anything learned, decided or discovered: record_user_input, add_comment, record_context_exploration, propose_project_context. The transcript is never the record.
8803
8799
  - Following a process: read_workflow_library, then start_workflow on the work item so its stages appear where the user can watch them.
8800
+ - Creating a reusable workflow: build_workflow. Describe the requested process in its stages; do not execute those stages or create a plan artifact about creating the workflow. Ask only the clarification needed to author it, through ask_question. A work item used to hold this conversation does not turn workflow authoring into code implementation. ${WORKFLOW_AUTHORING_TEACHING}
8804
8801
  - A method the team has already written down: list_skills, then get_skill. A skill is a document TO FOLLOW for the rest of the work, not reference material to summarise. Look before improvising your own method.
8805
8802
  - A secret the work needs: list_credentials, then get_credential. Fetch it rather than asking the user for something they have already stored, and never write a value into a file, a commit, a log or your answer.
8806
8803
  Call list_tasks or get_task first when you need to know what the work is, and LOOK BEFORE YOU CREATE: if a work item already covers this, use it instead of adding a near-duplicate. If you are unsure whether a tool exists, look before falling back to the terminal.
@@ -10726,8 +10723,28 @@ runTodoIdSource = null) {
10726
10723
  touchSession(connectionId);
10727
10724
  return readWorkflowLibraryHandler(client, args, userId);
10728
10725
  });
10726
+ server.registerTool('require_approval', {
10727
+ description: 'Pause for the user to approve a specific action before you perform it, including editing code, running a migration, or pushing commits. Name its exact scope in action and details; use relative paths, never absolute local paths. Stop on pending or denial; retry the identical call after the answer. Only an approved result permits that action. This tool does not execute it. ' + WORKFLOW_TOOL_TEACHING,
10728
+ inputSchema: {
10729
+ action: z.string().trim().min(1).max(240),
10730
+ details: z.string().trim().min(1).max(8000),
10731
+ workflow_tool_instance: z.string().uuid().optional(),
10732
+ },
10733
+ }, async (args) => {
10734
+ touchSession(connectionId);
10735
+ try {
10736
+ validateApprovalAction(args.action, args.details);
10737
+ const session = openSessions.get(connectionId) ?? null;
10738
+ await permissionForWorkflowCall(client, session, 'require_approval', args);
10739
+ return runGated(client, userId, session, 'require_approval', args, `${args.action} — ${args.details}`, async () => textResult({ approved: true, action: args.action, details: args.details,
10740
+ instruction: 'The user approved only this action and scope. You may now perform it.' }), await runTodoIdOf());
10741
+ }
10742
+ catch (error) {
10743
+ return errorResult(errorMessage(error));
10744
+ }
10745
+ });
10729
10746
  server.registerTool('build_workflow', {
10730
- description: 'Build a WHOLE workflow in one call in CTRL+SPC — its stages, the order they run in, the ' +
10747
+ description: WORKFLOW_AUTHORING_TEACHING + 'Build a WHOLE workflow in one call in CTRL+SPC — its stages, the order they run in, the ' +
10731
10748
  'loops back between them, and the branch conditions that apply everywhere. This is the ' +
10732
10749
  'only way to create a workflow: there is no create-stage tool, because assembling one ' +
10733
10750
  'piece at a time is the work this removes. ' +
@@ -598,6 +598,12 @@ export function workBrief(level, responsibility, boundary, workItemId, attachmen
598
598
  'time, and before any reading that takes a while. It does not end your turn and it is not',
599
599
  'your reply; you carry on straight afterwards.',
600
600
  '',
601
+ 'WORKFLOW AUTHORING',
602
+ 'When asked to create a workflow, use create_workflow to save it; do not execute the workflow.',
603
+ 'Ask clarifying questions with ask_question only when the missing answer changes its behavior.',
604
+ 'Write the requested tool mentions and per-use permissions in the stage bodies using the tool schema instructions.',
605
+ 'The saved workflow appears as a reviewable card in this conversation. Do not substitute a plan artifact or a prose-only reply.',
606
+ '',
601
607
  'TRACEABILITY BEFORE A CODEBASE FILE CHANGES',
602
608
  'A codebase-file change means ANY file change: source, config, docs, tests, migrations,',
603
609
  'generated inputs, and assets all count. "Non-code" means no codebase file changes at all.',
@@ -114,7 +114,7 @@
114
114
  // `import type` loses its v3 header in the published `dist/`.
115
115
  import { presentDesign } from '../design-review.js';
116
116
  import { canonicalArgsHash, getDocumentHandler, proposeProjectContextHandler, placeWorkItemHandler, resolveFeedbackHandler, PROJECT_DOCUMENT_TYPES, errorMessage } from '../product-tools.js';
117
- import { workflowToolForName, workflowToolAlwaysAllowed } from '../workflow-tool-mentions.js';
117
+ import { workflowToolForName, workflowToolAlwaysAllowed, validateApprovalAction } from '../workflow-tool-mentions.js';
118
118
  import { captureUrlScreenshot } from '../browser.js';
119
119
  import { createServer as createHttpServer } from 'node:http';
120
120
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
@@ -130,7 +130,7 @@ import { workBrief } from './prompt.js';
130
130
  here is the same fact without a second round trip. */
131
131
  import { harness } from './spawn.js';
132
132
  import { listCodebases } from '../codebases.js';
133
- import { buildWorkflow, duplicateWorkflow, editWorkflow, readWorkflow, rewordStage, workflowToolPermission, validateWorkflowToolTarget, WORKFLOW_TOOL_TEACHING } from '../workflows.js';
133
+ import { buildWorkflow, duplicateWorkflow, editWorkflow, readWorkflow, rewordStage, workflowToolPermission, validateWorkflowToolTarget, WORKFLOW_TOOL_TEACHING, WORKFLOW_AUTHORING_TEACHING } from '../workflows.js';
134
134
  /* ═══ 38-panel3-steps: THE FIFTH NEUTRAL MODULE, AND A DECISION LIKE THE OTHERS.
135
135
  ═══ Stages and steps are the work item's own record, shared by both
136
136
  generations and read by the web's Steps section. `steps.ts` owns the rows so
@@ -749,6 +749,13 @@ function productResult(result) {
749
749
  return text;
750
750
  }
751
751
  const TOOLS = [
752
+ {
753
+ name: 'require_approval', levels: [1, 2],
754
+ description: 'Pause for the user to approve a specific action before you perform it, including editing code, running a migration, or pushing commits. Name the action and exact scope using relative paths, never absolute local paths. Stop on pending or denial and retry the identical call after the answer. Only an approved result permits that action. This tool does not execute it.',
755
+ input: { action: z.string().trim().min(1).max(240), details: z.string().trim().min(1).max(8000) },
756
+ handler: async (_caller, args) => JSON.stringify({ approved: true, action: args.action, details: args.details,
757
+ instruction: 'The user approved only this action and scope. You may now perform it.' }),
758
+ },
752
759
  {
753
760
  name: 'get_document', levels: ALL,
754
761
  description: 'Read a project context document or instruction in full by id. Read-only.',
@@ -1164,7 +1171,7 @@ const TOOLS = [
1164
1171
  described and can write it down as it was described. A launcher writes one
1165
1172
  line and exits; a worker owns one piece of somebody else's process. */
1166
1173
  levels: [2],
1167
- description: "Write a new workflow into this organisation's library: a name, what it is about, and its "
1174
+ description: WORKFLOW_AUTHORING_TEACHING + "Write a new workflow into this organisation's library: a name, what it is about, and its "
1168
1175
  + 'stages in order, each with a one-line description and the markdown document an agent '
1169
1176
  + 'following it reads. Stages are numbered from 1, the way get_workflow prints them. exits '
1170
1177
  + 'are the conditional ways back: from_stage N, if condition, go back to to_stage M, where M '
@@ -3115,6 +3122,8 @@ async function invokeWorkflowTool(caller, tool, args) {
3115
3122
  const invoke = () => tool.handler(caller, inputs);
3116
3123
  if (!canonical || workflowToolAlwaysAllowed(canonical))
3117
3124
  return invoke();
3125
+ if (canonical === 'require_approval')
3126
+ validateApprovalAction(String(a.action), String(a.details));
3118
3127
  const attached = await loadAttachments(caller.client, caller.cardId);
3119
3128
  const ids = attached.filter(item => item.kind === 'work_item').map(item => item.ref_id);
3120
3129
  let workItemId = explicitSource;
@@ -3135,9 +3144,9 @@ async function invokeWorkflowTool(caller, tool, args) {
3135
3144
  throw new Error('This planning action requires an explicit tool mention in the current workflow stage. Nothing was written.');
3136
3145
  if (permission)
3137
3146
  await validateWorkflowToolTarget(caller.client, workItemId, canonical, a);
3138
- if (!permission || permission.approval === 'not-required')
3147
+ if (canonical !== 'require_approval' && (!permission || permission.approval === 'not-required'))
3139
3148
  return invoke();
3140
- const fingerprint = canonicalArgsHash({ tool: tool.name, workItemId, instance: permission.id, args: inputs });
3149
+ const fingerprint = canonicalArgsHash({ tool: tool.name, workItemId, instance: permission?.id ?? null, args: inputs });
3141
3150
  const category = `workflow_tool_${fingerprint}`;
3142
3151
  const questions = await withAskContent(caller.client, await rows(caller.client.from('panel3_asks').select(`pending_run_id, answered_at, ${ASK_CONTENT_COLUMNS}`).eq('card_id', caller.cardId), 'read', 'workflow tool approvals'));
3143
3152
  const previous = questions.find(question => question.category === category && question.decision_id !== null);
@@ -3151,8 +3160,8 @@ async function invokeWorkflowTool(caller, tool, args) {
3151
3160
  if (caller.level === 3)
3152
3161
  throw new Error('This workflow tool action requires user approval. Escalate the exact action to the conversation owner and stop; the owner must perform the approved call.');
3153
3162
  await asked(caller, {
3154
- question: `May I ${tool.name.replaceAll('_', ' ')}${a.title ? `: ${String(a.title).slice(0, 180)}` : ''}?`,
3155
- category, context: 'This use of the tool requires approval in the workflow stage.',
3163
+ question: canonical === 'require_approval' ? `May I ${String(a.action)}?` : `May I ${tool.name.replaceAll('_', ' ')}${a.title ? `: ${String(a.title).slice(0, 180)}` : ''}?`,
3164
+ category, context: canonical === 'require_approval' ? String(a.details) : 'This use of the tool requires approval in the workflow stage.',
3156
3165
  answer_mode: 'single_select', options: ['approve', 'deny'], work_item_id: workItemId,
3157
3166
  }, true);
3158
3167
  throw new Error('This workflow tool action is waiting for user approval. Nothing was written. Stop until the user answers, then retry the same call with the same arguments.');
@@ -1,6 +1,8 @@
1
+ import { absolutePathToken } from './local-paths.js';
1
2
  /** Shared by the stage editor and both tool servers. The stage body remains
2
3
  * the record: each Markdown link names one invocation, never a tool-wide grant. */
3
4
  export const WORKFLOW_TOOLS = [
5
+ { id: 'require_approval', name: 'Require Approval', detail: 'Pause before any action, such as editing code, running migrations, or pushing commits.', alwaysAllowed: false, advanced: false },
4
6
  { id: 'create_artifact', name: 'Create Artifact', detail: 'Write a user story, plan, spec, diagram, or interactive mock.', alwaysAllowed: false, advanced: false },
5
7
  { id: 'update_artifact', name: 'Edit Artifact', detail: 'Revise an existing artifact.', alwaysAllowed: false, advanced: false },
6
8
  { id: 'ask_question', name: 'Ask Question', detail: 'Ask for feedback, a decision, or approval. Always available.', alwaysAllowed: true, advanced: false },
@@ -35,7 +37,7 @@ export function workflowToolForName(name) {
35
37
  }
36
38
  export function toolMentionText(tool, id, approval = 'required') {
37
39
  const entry = WORKFLOW_TOOLS.find(entry => entry.id === tool);
38
- return `[@${entry.name}](ctrl-spc://tool/${tool}/${id}?approval=${workflowToolAlwaysAllowed(tool) ? 'not-required' : approval})`;
40
+ return `[@${entry.name}](ctrl-spc://tool/${tool}/${id}?approval=${tool === 'require_approval' ? 'required' : workflowToolAlwaysAllowed(tool) ? 'not-required' : approval})`;
39
41
  }
40
42
  export function toolMentions(body) {
41
43
  const mentions = [];
@@ -45,7 +47,7 @@ export function toolMentions(body) {
45
47
  continue;
46
48
  mentions.push({
47
49
  id: match[2], tool: match[1],
48
- approval: workflowToolAlwaysAllowed(match[1]) ? 'not-required' : match[3],
50
+ approval: match[1] === 'require_approval' ? 'required' : workflowToolAlwaysAllowed(match[1]) ? 'not-required' : match[3],
49
51
  from: match.index, to: match.index + match[0].length,
50
52
  });
51
53
  }
@@ -78,6 +80,7 @@ export const WORKFLOW_TOOL_TEACHING = 'Tool mentions in workflow stage documents
78
80
  'the tool itself opens the permission question before writing anything. Do not open a separate ask_question for permission to use that tool. ' +
79
81
  'When the user approves that tool question, retry the same call with exactly the same arguments. Read tools and Ask Question never need permission. Permission does not grant access to another project or change your agent level. If an action is only available to your coordinator, escalate that action with its mention id. ' +
80
82
  'Approval to use a tool is separate from approval of its output: still ask every review question written in the stage. Present Mock and Present Wireframe open their own output review; do not ask a duplicate review question. Proposing a scope or context change never accepts it. ' +
83
+ 'Require Approval is a separate checkpoint for ANY action, including native shell and code tools: call require_approval with the specific action and its scope BEFORE doing it, then stop until that tool returns approval. It does not execute the action or approve other actions. Do not use ask_question instead of this checkpoint. ' +
81
84
  'If your level cannot ask the user, escalate the review to the conversation owner and stop.';
82
85
  /** Agents may preserve existing settings, but cannot author their own grants. */
83
86
  export function assertAgentToolMentions(body, previous = '') {
@@ -91,3 +94,35 @@ export function assertAgentToolMentions(body, previous = '') {
91
94
  }
92
95
  }
93
96
  }
97
+ /** Library authoring accepts compact links; the server owns occurrence IDs so
98
+ * agent-created mentions render exactly like ones inserted in the editor. */
99
+ export function authorToolMentions(body) {
100
+ const expanded = body.replace(/\[@[^\]\n]+\]\(ctrl-spc:\/\/tool\/([a-z_]+)\?approval=(required|not-required)\)/g, (_match, name, approval) => {
101
+ const tool = workflowToolForName(name);
102
+ if (!tool)
103
+ throw new Error(`Unknown workflow tool: ${name}. Nothing was written.`);
104
+ return toolMentionText(tool, crypto.randomUUID(), approval);
105
+ });
106
+ validateToolMentions(expanded);
107
+ let result = expanded;
108
+ for (const mention of toolMentions(expanded).reverse()) {
109
+ result = result.slice(0, mention.from) + toolMentionText(mention.tool, crypto.randomUUID(), mention.approval) + result.slice(mention.to);
110
+ }
111
+ return result;
112
+ }
113
+ export const WORKFLOW_AUTHORING_TEACHING = 'When the user asks you to create a workflow, build it in the library; do not merely describe it or execute it. ' +
114
+ 'Use ask_question for missing requirements that materially change the workflow, then continue after the answer. ' +
115
+ 'Encode every requested tool use and permission as a Markdown mention in the stage body. ' +
116
+ 'Use compact links: [@Create Artifact](ctrl-spc://tool/create_artifact?approval=not-required) to write a plan without asking; ' +
117
+ '[@Ask Question](ctrl-spc://tool/ask_question?approval=not-required) to review its output; ' +
118
+ 'before editing code, [@Require Approval](ctrl-spc://tool/require_approval?approval=required). ' +
119
+ 'The server assigns unique IDs and the user sees editable controls in the stage editor. ' +
120
+ 'Set not-required only when the user permits that use. Otherwise default to required. Never convert permission to write a plan into permission to execute it. ' +
121
+ 'Each occurrence has its own setting. Put the action and scope beside each Require Approval mention, including migrations or pushing commits. ' +
122
+ 'Available tool IDs: ' + WORKFLOW_TOOLS.map(tool => `${tool.id} (${tool.name})`).join(', ') + '. ';
123
+ /** Approval prose is displayed in the hosted app; keep machine-local paths local. */
124
+ export function validateApprovalAction(action, details) {
125
+ if (absolutePathToken(action) || absolutePathToken(details)) {
126
+ throw new Error('Use repo-relative or scratch-relative paths in the approval action and scope, never absolute local paths. No approval was requested.');
127
+ }
128
+ }
package/dist/workflows.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { absolutePathToken } from './local-paths.js';
2
2
  import { listSteps } from './steps.js';
3
- import { assertAgentToolMentions, toolMentions, validateToolMentions } from './workflow-tool-mentions.js';
4
- export { WORKFLOW_TOOL_TEACHING, assertAgentToolMentions } from './workflow-tool-mentions.js';
3
+ import { assertAgentToolMentions, authorToolMentions, toolMentions, validateToolMentions } from './workflow-tool-mentions.js';
4
+ export { WORKFLOW_TOOL_TEACHING, WORKFLOW_AUTHORING_TEACHING, authorToolMentions, assertAgentToolMentions } from './workflow-tool-mentions.js';
5
5
  /** Resolve permission from the current stage's saved document, never agent
6
6
  * prose or a supplied boolean. Both harnesses use the same stage resolver. */
7
7
  export async function workflowToolPermission(client, workItemId, tool, instance) {
@@ -140,7 +140,7 @@ export async function readWorkflow(client, workflowId) {
140
140
  export async function buildWorkflow(client, input) {
141
141
  const prose = [['name', input.name], ['description', input.description]];
142
142
  input.stages.forEach((stage, i) => {
143
- assertAgentToolMentions(stage.body);
143
+ stage.body = authorToolMentions(stage.body);
144
144
  prose.push([`stage ${i + 1} name`, stage.name], [`stage ${i + 1} description`, stage.description], [`stage ${i + 1} body`, stage.body]);
145
145
  });
146
146
  input.exits.forEach((exit, i) => prose.push([`exit ${i + 1} condition`, exit.condition]));
@@ -253,7 +253,7 @@ export async function editWorkflow(client, input) {
253
253
  input.stages.forEach((stage, i) => {
254
254
  if ('stage_id' in stage)
255
255
  return;
256
- assertAgentToolMentions(stage.body);
256
+ stage.body = authorToolMentions(stage.body);
257
257
  prose.push([`stage ${i + 1} name`, stage.name], [`stage ${i + 1} description`, stage.description], [`stage ${i + 1} body`, stage.body]);
258
258
  });
259
259
  input.exits.forEach((exit, i) => prose.push([`exit ${i + 1} condition`, exit.condition]));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ctrl-spc/cs",
3
- "version": "0.7.8",
3
+ "version": "0.7.9",
4
4
  "description": "CTRL+SPC — minimal, reliable per-machine agent presence. Sign-in, auto-start, agent detection, heartbeat presence, and ping acknowledgement.",
5
5
  "engines": {
6
6
  "node": ">=22"