acpx 0.1.7 → 0.1.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/LICENSE +1 -1
- package/README.md +9 -3
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +129 -31
- package/package.json +3 -3
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -6,10 +6,13 @@
|
|
|
6
6
|
|
|
7
7
|
[](https://www.npmjs.com/package/acpx)
|
|
8
8
|
[](https://www.npmjs.com/package/acpx)
|
|
9
|
-
[](https://github.com/openclaw/acpx/actions/workflows/ci.yml)
|
|
10
10
|
[](https://opensource.org/licenses/MIT)
|
|
11
11
|
[](https://nodejs.org)
|
|
12
12
|
|
|
13
|
+
> Alpha notice: `acpx` is in alpha and the CLI/runtime interfaces are likely to change.
|
|
14
|
+
> ACP coverage status: see [ACP Spec Coverage Roadmap](docs/2026-02-19-acp-coverage-roadmap.md).
|
|
15
|
+
|
|
13
16
|
Your agents love acpx! 🤖❤️ They hate having to scrape characters from a PTY session 😤
|
|
14
17
|
|
|
15
18
|
`acpx` is a headless CLI client for the [Agent Client Protocol (ACP)](https://agentclientprotocol.com), so AI agents and orchestrators can talk to coding agents over a structured protocol instead of PTY scraping.
|
|
@@ -84,10 +87,10 @@ I want you to use acpx to run coding agents over the Agent Client Protocol
|
|
|
84
87
|
|
|
85
88
|
3. Read the acpx skill reference so you know every command, flag, and
|
|
86
89
|
workflow pattern:
|
|
87
|
-
https://raw.githubusercontent.com/
|
|
90
|
+
https://raw.githubusercontent.com/openclaw/acpx/main/skills/acpx/SKILL.md
|
|
88
91
|
|
|
89
92
|
4. If you need the full CLI reference with all options and examples:
|
|
90
|
-
https://raw.githubusercontent.com/
|
|
93
|
+
https://raw.githubusercontent.com/openclaw/acpx/main/docs/CLI.md
|
|
91
94
|
|
|
92
95
|
From now on, when you need to delegate work to a coding agent, use acpx
|
|
93
96
|
instead of spawning raw terminal sessions. For example:
|
|
@@ -257,6 +260,9 @@ JSON events include a stable envelope for correlation:
|
|
|
257
260
|
}
|
|
258
261
|
```
|
|
259
262
|
|
|
263
|
+
Session-control JSON payloads (`sessions new|ensure`, `status`) may also include
|
|
264
|
+
`runtimeSessionId` when the adapter exposes a provider-native session ID.
|
|
265
|
+
|
|
260
266
|
## Built-in agents and custom servers
|
|
261
267
|
|
|
262
268
|
Built-ins:
|
package/dist/cli.d.ts
CHANGED
package/dist/cli.js
CHANGED
|
@@ -1715,6 +1715,35 @@ function classifyPermissionDecision(params, response) {
|
|
|
1715
1715
|
return "denied";
|
|
1716
1716
|
}
|
|
1717
1717
|
|
|
1718
|
+
// src/agent-session-id.ts
|
|
1719
|
+
var AGENT_SESSION_ID_META_KEYS = ["agentSessionId", "sessionId"];
|
|
1720
|
+
function normalizeAgentSessionId(value) {
|
|
1721
|
+
if (typeof value !== "string") {
|
|
1722
|
+
return void 0;
|
|
1723
|
+
}
|
|
1724
|
+
const trimmed = value.trim();
|
|
1725
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
1726
|
+
}
|
|
1727
|
+
function asMetaRecord(meta) {
|
|
1728
|
+
if (!meta || typeof meta !== "object" || Array.isArray(meta)) {
|
|
1729
|
+
return void 0;
|
|
1730
|
+
}
|
|
1731
|
+
return meta;
|
|
1732
|
+
}
|
|
1733
|
+
function extractAgentSessionId(meta) {
|
|
1734
|
+
const record = asMetaRecord(meta);
|
|
1735
|
+
if (!record) {
|
|
1736
|
+
return void 0;
|
|
1737
|
+
}
|
|
1738
|
+
for (const key of AGENT_SESSION_ID_META_KEYS) {
|
|
1739
|
+
const normalized = normalizeAgentSessionId(record[key]);
|
|
1740
|
+
if (normalized) {
|
|
1741
|
+
return normalized;
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
return void 0;
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1718
1747
|
// src/terminal.ts
|
|
1719
1748
|
import { spawn } from "child_process";
|
|
1720
1749
|
import { randomUUID } from "crypto";
|
|
@@ -2373,18 +2402,22 @@ var AcpClient = class {
|
|
|
2373
2402
|
cwd: asAbsoluteCwd(cwd),
|
|
2374
2403
|
mcpServers: []
|
|
2375
2404
|
});
|
|
2376
|
-
return
|
|
2405
|
+
return {
|
|
2406
|
+
sessionId: result.sessionId,
|
|
2407
|
+
agentSessionId: extractAgentSessionId(result._meta)
|
|
2408
|
+
};
|
|
2377
2409
|
}
|
|
2378
2410
|
async loadSession(sessionId, cwd = this.options.cwd) {
|
|
2379
2411
|
this.getConnection();
|
|
2380
|
-
await this.loadSessionWithOptions(sessionId, cwd, {});
|
|
2412
|
+
return await this.loadSessionWithOptions(sessionId, cwd, {});
|
|
2381
2413
|
}
|
|
2382
2414
|
async loadSessionWithOptions(sessionId, cwd = this.options.cwd, options = {}) {
|
|
2383
2415
|
const connection = this.getConnection();
|
|
2384
2416
|
const previousSuppression = this.suppressSessionUpdates;
|
|
2385
2417
|
this.suppressSessionUpdates = previousSuppression || Boolean(options.suppressReplayUpdates);
|
|
2418
|
+
let response;
|
|
2386
2419
|
try {
|
|
2387
|
-
await connection.loadSession({
|
|
2420
|
+
response = await connection.loadSession({
|
|
2388
2421
|
sessionId,
|
|
2389
2422
|
cwd: asAbsoluteCwd(cwd),
|
|
2390
2423
|
mcpServers: []
|
|
@@ -2396,6 +2429,9 @@ var AcpClient = class {
|
|
|
2396
2429
|
} finally {
|
|
2397
2430
|
this.suppressSessionUpdates = previousSuppression;
|
|
2398
2431
|
}
|
|
2432
|
+
return {
|
|
2433
|
+
agentSessionId: extractAgentSessionId(response?._meta)
|
|
2434
|
+
};
|
|
2399
2435
|
}
|
|
2400
2436
|
async prompt(sessionId, text) {
|
|
2401
2437
|
const connection = this.getConnection();
|
|
@@ -4203,6 +4239,7 @@ function parseSessionRecord(raw) {
|
|
|
4203
4239
|
return null;
|
|
4204
4240
|
}
|
|
4205
4241
|
const record = raw;
|
|
4242
|
+
const agentSessionId = normalizeAgentSessionId(record.agentSessionId);
|
|
4206
4243
|
const name = record.name == null ? void 0 : typeof record.name === "string" && record.name.trim().length > 0 ? record.name.trim() : null;
|
|
4207
4244
|
const pid = record.pid == null ? void 0 : Number.isInteger(record.pid) && record.pid > 0 ? record.pid : null;
|
|
4208
4245
|
const closed = record.closed == null ? false : typeof record.closed === "boolean" ? record.closed : null;
|
|
@@ -4225,6 +4262,7 @@ function parseSessionRecord(raw) {
|
|
|
4225
4262
|
...record,
|
|
4226
4263
|
id: record.id,
|
|
4227
4264
|
sessionId: record.sessionId,
|
|
4265
|
+
agentSessionId,
|
|
4228
4266
|
agentCommand: record.agentCommand,
|
|
4229
4267
|
cwd: record.cwd,
|
|
4230
4268
|
name,
|
|
@@ -4617,6 +4655,13 @@ function applyLifecycleSnapshotToRecord(record, snapshot) {
|
|
|
4617
4655
|
record.lastAgentExitAt = void 0;
|
|
4618
4656
|
record.lastAgentDisconnectReason = void 0;
|
|
4619
4657
|
}
|
|
4658
|
+
function reconcileAgentSessionId(record, agentSessionId) {
|
|
4659
|
+
const normalized = normalizeAgentSessionId(agentSessionId);
|
|
4660
|
+
if (!normalized) {
|
|
4661
|
+
return;
|
|
4662
|
+
}
|
|
4663
|
+
record.agentSessionId = normalized;
|
|
4664
|
+
}
|
|
4620
4665
|
function shouldFallbackToNewSession(error) {
|
|
4621
4666
|
if (error instanceof TimeoutError || error instanceof InterruptedError) {
|
|
4622
4667
|
return false;
|
|
@@ -4653,31 +4698,40 @@ async function connectAndLoadSession(options) {
|
|
|
4653
4698
|
let sessionId = record.sessionId;
|
|
4654
4699
|
if (client.supportsLoadSession()) {
|
|
4655
4700
|
try {
|
|
4656
|
-
await withTimeout(
|
|
4701
|
+
const loadResult = await withTimeout(
|
|
4657
4702
|
client.loadSessionWithOptions(record.sessionId, record.cwd, {
|
|
4658
4703
|
suppressReplayUpdates: true
|
|
4659
4704
|
}),
|
|
4660
4705
|
options.timeoutMs
|
|
4661
4706
|
);
|
|
4707
|
+
reconcileAgentSessionId(record, loadResult.agentSessionId);
|
|
4662
4708
|
resumed = true;
|
|
4663
4709
|
} catch (error) {
|
|
4664
4710
|
loadError = formatErrorMessage(error);
|
|
4665
4711
|
if (!shouldFallbackToNewSession(error)) {
|
|
4666
4712
|
throw error;
|
|
4667
4713
|
}
|
|
4668
|
-
|
|
4714
|
+
const createdSession = await withTimeout(
|
|
4669
4715
|
client.createSession(record.cwd),
|
|
4670
4716
|
options.timeoutMs
|
|
4671
4717
|
);
|
|
4718
|
+
sessionId = createdSession.sessionId;
|
|
4672
4719
|
record.sessionId = sessionId;
|
|
4720
|
+
reconcileAgentSessionId(record, createdSession.agentSessionId);
|
|
4673
4721
|
}
|
|
4674
4722
|
} else {
|
|
4675
|
-
|
|
4723
|
+
const createdSession = await withTimeout(
|
|
4724
|
+
client.createSession(record.cwd),
|
|
4725
|
+
options.timeoutMs
|
|
4726
|
+
);
|
|
4727
|
+
sessionId = createdSession.sessionId;
|
|
4676
4728
|
record.sessionId = sessionId;
|
|
4729
|
+
reconcileAgentSessionId(record, createdSession.agentSessionId);
|
|
4677
4730
|
}
|
|
4678
4731
|
options.onSessionIdResolved?.(sessionId);
|
|
4679
4732
|
return {
|
|
4680
4733
|
sessionId,
|
|
4734
|
+
agentSessionId: record.agentSessionId,
|
|
4681
4735
|
resumed,
|
|
4682
4736
|
loadError
|
|
4683
4737
|
};
|
|
@@ -5030,10 +5084,11 @@ async function runOnce(options) {
|
|
|
5030
5084
|
return await withInterrupt(
|
|
5031
5085
|
async () => {
|
|
5032
5086
|
await withTimeout(client.start(), options.timeoutMs);
|
|
5033
|
-
const
|
|
5087
|
+
const createdSession = await withTimeout(
|
|
5034
5088
|
client.createSession(absolutePath(options.cwd)),
|
|
5035
5089
|
options.timeoutMs
|
|
5036
5090
|
);
|
|
5091
|
+
const sessionId = createdSession.sessionId;
|
|
5037
5092
|
output.setContext({
|
|
5038
5093
|
sessionId,
|
|
5039
5094
|
stream: "prompt"
|
|
@@ -5069,15 +5124,17 @@ async function createSession(options) {
|
|
|
5069
5124
|
return await withInterrupt(
|
|
5070
5125
|
async () => {
|
|
5071
5126
|
await withTimeout(client.start(), options.timeoutMs);
|
|
5072
|
-
const
|
|
5127
|
+
const createdSession = await withTimeout(
|
|
5073
5128
|
client.createSession(absolutePath(options.cwd)),
|
|
5074
5129
|
options.timeoutMs
|
|
5075
5130
|
);
|
|
5131
|
+
const sessionId = createdSession.sessionId;
|
|
5076
5132
|
const lifecycle = client.getAgentLifecycleSnapshot();
|
|
5077
5133
|
const now = isoNow2();
|
|
5078
5134
|
const record = {
|
|
5079
5135
|
id: sessionId,
|
|
5080
5136
|
sessionId,
|
|
5137
|
+
agentSessionId: createdSession.agentSessionId,
|
|
5081
5138
|
agentCommand: options.agentCommand,
|
|
5082
5139
|
cwd: absolutePath(options.cwd),
|
|
5083
5140
|
name: normalizeName(options.name),
|
|
@@ -5655,8 +5712,22 @@ function resolveAgentInvocation(explicitAgentName, globalFlags, config) {
|
|
|
5655
5712
|
}
|
|
5656
5713
|
function printSessionsByFormat(sessions, format) {
|
|
5657
5714
|
if (format === "json") {
|
|
5658
|
-
process.stdout.write(
|
|
5659
|
-
|
|
5715
|
+
process.stdout.write(
|
|
5716
|
+
`${JSON.stringify(
|
|
5717
|
+
sessions.map((session) => {
|
|
5718
|
+
const { id, sessionId, agentSessionId, ...rest } = session;
|
|
5719
|
+
return {
|
|
5720
|
+
...rest,
|
|
5721
|
+
...canonicalSessionIdentity({
|
|
5722
|
+
id,
|
|
5723
|
+
sessionId,
|
|
5724
|
+
agentSessionId
|
|
5725
|
+
})
|
|
5726
|
+
};
|
|
5727
|
+
})
|
|
5728
|
+
)}
|
|
5729
|
+
`
|
|
5730
|
+
);
|
|
5660
5731
|
return;
|
|
5661
5732
|
}
|
|
5662
5733
|
if (format === "quiet") {
|
|
@@ -5684,8 +5755,7 @@ function printClosedSessionByFormat(record, format) {
|
|
|
5684
5755
|
process.stdout.write(
|
|
5685
5756
|
`${JSON.stringify({
|
|
5686
5757
|
type: "session_closed",
|
|
5687
|
-
|
|
5688
|
-
sessionId: record.sessionId,
|
|
5758
|
+
...canonicalSessionIdentity(record),
|
|
5689
5759
|
name: record.name
|
|
5690
5760
|
})}
|
|
5691
5761
|
`
|
|
@@ -5698,15 +5768,22 @@ function printClosedSessionByFormat(record, format) {
|
|
|
5698
5768
|
process.stdout.write(`${record.id}
|
|
5699
5769
|
`);
|
|
5700
5770
|
}
|
|
5771
|
+
function canonicalSessionIdentity(record) {
|
|
5772
|
+
const normalizedAgentSessionId = normalizeAgentSessionId(record.agentSessionId);
|
|
5773
|
+
return {
|
|
5774
|
+
acpxRecordId: record.id,
|
|
5775
|
+
acpxSessionId: record.sessionId,
|
|
5776
|
+
...normalizedAgentSessionId ? { agentSessionId: normalizedAgentSessionId } : {}
|
|
5777
|
+
};
|
|
5778
|
+
}
|
|
5701
5779
|
function printNewSessionByFormat(record, replaced, format) {
|
|
5702
5780
|
if (format === "json") {
|
|
5703
5781
|
process.stdout.write(
|
|
5704
5782
|
`${JSON.stringify({
|
|
5705
5783
|
type: "session_created",
|
|
5706
|
-
|
|
5707
|
-
sessionId: record.sessionId,
|
|
5784
|
+
...canonicalSessionIdentity(record),
|
|
5708
5785
|
name: record.name,
|
|
5709
|
-
|
|
5786
|
+
...replaced ? { replacedAcpxRecordId: replaced.id } : {}
|
|
5710
5787
|
})}
|
|
5711
5788
|
`
|
|
5712
5789
|
);
|
|
@@ -5730,8 +5807,7 @@ function printEnsuredSessionByFormat(record, created, format) {
|
|
|
5730
5807
|
process.stdout.write(
|
|
5731
5808
|
`${JSON.stringify({
|
|
5732
5809
|
type: "session_ensured",
|
|
5733
|
-
|
|
5734
|
-
sessionId: record.sessionId,
|
|
5810
|
+
...canonicalSessionIdentity(record),
|
|
5735
5811
|
name: record.name,
|
|
5736
5812
|
created
|
|
5737
5813
|
})}
|
|
@@ -5753,7 +5829,7 @@ function printQueuedPromptByFormat(result, format) {
|
|
|
5753
5829
|
process.stdout.write(
|
|
5754
5830
|
`${JSON.stringify({
|
|
5755
5831
|
type: "queued",
|
|
5756
|
-
|
|
5832
|
+
acpxRecordId: result.sessionId,
|
|
5757
5833
|
requestId: result.requestId
|
|
5758
5834
|
})}
|
|
5759
5835
|
`
|
|
@@ -5910,8 +5986,13 @@ async function handleExec(explicitAgentName, promptParts, flags, command, config
|
|
|
5910
5986
|
}
|
|
5911
5987
|
function printCancelResultByFormat(result, format) {
|
|
5912
5988
|
if (format === "json") {
|
|
5913
|
-
process.stdout.write(
|
|
5914
|
-
|
|
5989
|
+
process.stdout.write(
|
|
5990
|
+
`${JSON.stringify({
|
|
5991
|
+
acpxRecordId: result.sessionId || null,
|
|
5992
|
+
cancelled: result.cancelled
|
|
5993
|
+
})}
|
|
5994
|
+
`
|
|
5995
|
+
);
|
|
5915
5996
|
return;
|
|
5916
5997
|
}
|
|
5917
5998
|
if (result.cancelled) {
|
|
@@ -5924,7 +6005,7 @@ function printSetModeResultByFormat(modeId, result, format) {
|
|
|
5924
6005
|
if (format === "json") {
|
|
5925
6006
|
process.stdout.write(
|
|
5926
6007
|
`${JSON.stringify({
|
|
5927
|
-
|
|
6008
|
+
...canonicalSessionIdentity(result.record),
|
|
5928
6009
|
modeId,
|
|
5929
6010
|
resumed: result.resumed
|
|
5930
6011
|
})}
|
|
@@ -5944,7 +6025,7 @@ function printSetConfigOptionResultByFormat(configId, value, result, format) {
|
|
|
5944
6025
|
if (format === "json") {
|
|
5945
6026
|
process.stdout.write(
|
|
5946
6027
|
`${JSON.stringify({
|
|
5947
|
-
|
|
6028
|
+
...canonicalSessionIdentity(result.record),
|
|
5948
6029
|
configId,
|
|
5949
6030
|
value,
|
|
5950
6031
|
resumed: result.resumed,
|
|
@@ -6136,8 +6217,18 @@ async function handleSessionsEnsure(explicitAgentName, flags, command, config) {
|
|
|
6136
6217
|
}
|
|
6137
6218
|
function printSessionDetailsByFormat(record, format) {
|
|
6138
6219
|
if (format === "json") {
|
|
6139
|
-
|
|
6140
|
-
|
|
6220
|
+
const { id, sessionId, agentSessionId, ...rest } = record;
|
|
6221
|
+
process.stdout.write(
|
|
6222
|
+
`${JSON.stringify({
|
|
6223
|
+
...rest,
|
|
6224
|
+
...canonicalSessionIdentity({
|
|
6225
|
+
id,
|
|
6226
|
+
sessionId,
|
|
6227
|
+
agentSessionId
|
|
6228
|
+
})
|
|
6229
|
+
})}
|
|
6230
|
+
`
|
|
6231
|
+
);
|
|
6141
6232
|
return;
|
|
6142
6233
|
}
|
|
6143
6234
|
if (format === "quiet") {
|
|
@@ -6145,9 +6236,11 @@ function printSessionDetailsByFormat(record, format) {
|
|
|
6145
6236
|
`);
|
|
6146
6237
|
return;
|
|
6147
6238
|
}
|
|
6148
|
-
process.stdout.write(`
|
|
6239
|
+
process.stdout.write(`acpxRecordId: ${record.id}
|
|
6240
|
+
`);
|
|
6241
|
+
process.stdout.write(`acpxSessionId: ${record.sessionId}
|
|
6149
6242
|
`);
|
|
6150
|
-
process.stdout.write(`
|
|
6243
|
+
process.stdout.write(`agentSessionId: ${record.agentSessionId ?? "-"}
|
|
6151
6244
|
`);
|
|
6152
6245
|
process.stdout.write(`agent: ${record.agentCommand}
|
|
6153
6246
|
`);
|
|
@@ -6188,8 +6281,7 @@ function printSessionHistoryByFormat(record, limit, format) {
|
|
|
6188
6281
|
if (format === "json") {
|
|
6189
6282
|
process.stdout.write(
|
|
6190
6283
|
`${JSON.stringify({
|
|
6191
|
-
|
|
6192
|
-
sessionId: record.sessionId,
|
|
6284
|
+
...canonicalSessionIdentity(record),
|
|
6193
6285
|
limit,
|
|
6194
6286
|
count: visible.length,
|
|
6195
6287
|
entries: visible
|
|
@@ -6275,7 +6367,9 @@ async function handleStatus(explicitAgentName, flags, command, config) {
|
|
|
6275
6367
|
});
|
|
6276
6368
|
if (!record) {
|
|
6277
6369
|
const payload2 = {
|
|
6278
|
-
|
|
6370
|
+
acpxRecordId: null,
|
|
6371
|
+
acpxSessionId: null,
|
|
6372
|
+
agentSessionId: null,
|
|
6279
6373
|
agentCommand: agent.agentCommand,
|
|
6280
6374
|
pid: null,
|
|
6281
6375
|
status: "no-session",
|
|
@@ -6309,7 +6403,7 @@ async function handleStatus(explicitAgentName, flags, command, config) {
|
|
|
6309
6403
|
}
|
|
6310
6404
|
const running = isProcessAlive(record.pid);
|
|
6311
6405
|
const payload = {
|
|
6312
|
-
|
|
6406
|
+
...canonicalSessionIdentity(record),
|
|
6313
6407
|
agentCommand: record.agentCommand,
|
|
6314
6408
|
pid: record.pid ?? null,
|
|
6315
6409
|
status: running ? "running" : "dead",
|
|
@@ -6328,7 +6422,11 @@ async function handleStatus(explicitAgentName, flags, command, config) {
|
|
|
6328
6422
|
`);
|
|
6329
6423
|
return;
|
|
6330
6424
|
}
|
|
6331
|
-
process.stdout.write(`
|
|
6425
|
+
process.stdout.write(`acpxRecordId: ${payload.acpxRecordId}
|
|
6426
|
+
`);
|
|
6427
|
+
process.stdout.write(`acpxSessionId: ${payload.acpxSessionId}
|
|
6428
|
+
`);
|
|
6429
|
+
process.stdout.write(`agentSessionId: ${payload.agentSessionId ?? "-"}
|
|
6332
6430
|
`);
|
|
6333
6431
|
process.stdout.write(`agent: ${payload.agentCommand}
|
|
6334
6432
|
`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "acpx",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "Headless CLI client for the Agent Client Protocol (ACP) — talk to coding agents from the command line",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -36,11 +36,11 @@
|
|
|
36
36
|
"coding-agent",
|
|
37
37
|
"ai"
|
|
38
38
|
],
|
|
39
|
-
"author": "
|
|
39
|
+
"author": "",
|
|
40
40
|
"license": "MIT",
|
|
41
41
|
"repository": {
|
|
42
42
|
"type": "git",
|
|
43
|
-
"url": "git+https://github.com/
|
|
43
|
+
"url": "git+https://github.com/openclaw/acpx.git"
|
|
44
44
|
},
|
|
45
45
|
"engines": {
|
|
46
46
|
"node": ">=18"
|