pi-acp 0.0.21 → 0.0.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -8
- package/dist/index.js +133 -54
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,16 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
ACP ([Agent Client Protocol](https://agentclientprotocol.com/overview/introduction)) adapter for [`pi`](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent) coding agent (fka shitty coding agent).
|
|
4
4
|
|
|
5
|
-
`pi-acp` communicates **ACP JSON-RPC 2.0 over stdio** to an ACP client (e.g.
|
|
5
|
+
`pi-acp` communicates **ACP JSON-RPC 2.0 over stdio** to an ACP client (e.g. Zed editor) and spawns `pi --mode rpc`, bridging requests/events between the two.
|
|
6
6
|
|
|
7
7
|
## Status
|
|
8
8
|
|
|
9
9
|
This is an MVP-style adapter intended to be useful today and easy to iterate on. Some ACP features may be not implemented or are not supported (see [Limitations](#limitations)). Development is centered around [Zed](https://zed.dev) editor support, other clients may have varying levels of compatibility.
|
|
10
10
|
|
|
11
|
+
Expect some minor breaking changes.
|
|
12
|
+
|
|
11
13
|
## Features
|
|
12
14
|
|
|
13
15
|
- Streams assistant output as ACP `agent_message_chunk`
|
|
14
16
|
- Maps pi tool execution to ACP `tool_call` / `tool_call_update`
|
|
17
|
+
- Tool call locations are surfaced when available for ACP clients that support opening the referenced file/context
|
|
18
|
+
- Relative file paths from pi are resolved against the session cwd before being emitted as ACP tool locations, which enables follow-along features in clients like Zed
|
|
19
|
+
- For `edit`, `pi-acp` attempts to infer a 1-based line number from a unique `oldText` match in the pre-edit file snapshot and includes it in the emitted tool location when possible
|
|
15
20
|
- For `edit`, `pi-acp` snapshots the file before the tool runs and emits an ACP **structured diff** (`oldText`/`newText`) on completion when possible
|
|
16
21
|
- Session persistence
|
|
17
22
|
- pi stores its own sessions in `~/.pi/agent/sessions/...`
|
|
@@ -21,7 +26,7 @@ This is an MVP-style adapter intended to be useful today and easy to iterate on.
|
|
|
21
26
|
- Adds a small set of built-in commands for headless/editor usage
|
|
22
27
|
- Supports skill commands (if enabled in pi settings, they appear as `/skill:skill-name` in the ACP client)
|
|
23
28
|
- Skills are loaded by pi directly and are available in ACP sessions
|
|
24
|
-
- (Zed) `pi-acp` emits
|
|
29
|
+
- (Zed) `pi-acp` emits “startup info” block into the session (pi version, context, skills, prompts, extensions - similar to `pi` in the terminal). You can disable it by setting `quietStartup: true` in pi settings (`~/.pi/agent/settings.json` or `<project>/.pi/settings.json`). When `quietStartup` is enabled, `pi-acp` will still emit a 'New version available' message if the installed pi version is outdated.
|
|
25
30
|
- (Zed) Session history is supported in Zed starting with [`v0.225.0`](https://zed.dev/releases/preview/0.225.0). Session loading / history maps to pi's session files. Sessions can be resumed both in `pi` and in the ACP client.
|
|
26
31
|
|
|
27
32
|
## Prerequisites
|
|
@@ -40,10 +45,22 @@ npm install -g @mariozechner/pi-coding-agent
|
|
|
40
45
|
|
|
41
46
|
### Add pi-acp to your ACP client, e.g. [Zed](https://zed.dev/docs/agents/external-agents/)
|
|
42
47
|
|
|
43
|
-
|
|
48
|
+
#### Using ACP Registry in Zed or other clients that support it:
|
|
49
|
+
|
|
50
|
+
In Zed launch the registry with `zed: acp registry` command and select `pi ACP` adapter from the list. This will automatically add the agent server configuration to your `settings.json` and keep it up to date:
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
"agent_servers": {
|
|
54
|
+
"pi-acp": {
|
|
55
|
+
"type": "registry",
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
```
|
|
44
59
|
|
|
45
60
|
#### Using with `npx` (no global install needed, always loads the latest version):
|
|
46
61
|
|
|
62
|
+
Add the following to your Zed `settings.json`:
|
|
63
|
+
|
|
47
64
|
```json
|
|
48
65
|
"agent_servers": {
|
|
49
66
|
"pi": {
|
|
@@ -96,14 +113,12 @@ Point your ACP client to the built `dist/index.js`:
|
|
|
96
113
|
|
|
97
114
|
`pi-acp` supports slash commands:
|
|
98
115
|
|
|
99
|
-
#### 1) File-based commands (
|
|
116
|
+
#### 1) File-based commands (aka prompts)
|
|
100
117
|
|
|
101
118
|
Loaded from:
|
|
102
119
|
|
|
103
|
-
- User commands: `~/.pi/agent/
|
|
104
|
-
- Project commands: `<cwd>/.pi/
|
|
105
|
-
|
|
106
|
-
These are expanded adapter-side (pi RPC mode doesn’t expand them).
|
|
120
|
+
- User commands: `~/.pi/agent/prompts/**/*.md`
|
|
121
|
+
- Project commands: `<cwd>/.pi/prompts/**/*.md`
|
|
107
122
|
|
|
108
123
|
#### 2) Built-in commands
|
|
109
124
|
|
package/dist/index.js
CHANGED
|
@@ -84,6 +84,7 @@ import { isAbsolute, resolve as resolvePath } from "path";
|
|
|
84
84
|
// src/pi-rpc/process.ts
|
|
85
85
|
import { spawn } from "child_process";
|
|
86
86
|
import * as readline from "readline";
|
|
87
|
+
import { platform } from "os";
|
|
87
88
|
var PiRpcSpawnError = class extends Error {
|
|
88
89
|
/** Underlying spawn error code, e.g. ENOENT, EACCES */
|
|
89
90
|
code;
|
|
@@ -145,8 +146,9 @@ var PiRpcProcess = class _PiRpcProcess {
|
|
|
145
146
|
});
|
|
146
147
|
}
|
|
147
148
|
static async spawn(params) {
|
|
148
|
-
const
|
|
149
|
-
const
|
|
149
|
+
const isWindows = platform() === "win32";
|
|
150
|
+
const cmd = params.piCommand ?? (isWindows ? "pi.cmd" : "pi");
|
|
151
|
+
const args = ["--mode", "rpc", "--no-themes"];
|
|
150
152
|
if (params.sessionPath) args.push("--session", params.sessionPath);
|
|
151
153
|
const child = spawn(cmd, args, {
|
|
152
154
|
cwd: params.cwd,
|
|
@@ -524,9 +526,31 @@ function expandSlashCommand(text, fileCommands) {
|
|
|
524
526
|
}
|
|
525
527
|
|
|
526
528
|
// src/acp/session.ts
|
|
529
|
+
function findUniqueLineNumber(text, needle) {
|
|
530
|
+
if (!needle) return void 0;
|
|
531
|
+
const first = text.indexOf(needle);
|
|
532
|
+
if (first < 0) return void 0;
|
|
533
|
+
const second = text.indexOf(needle, first + needle.length);
|
|
534
|
+
if (second >= 0) return void 0;
|
|
535
|
+
let line = 1;
|
|
536
|
+
for (let i = 0; i < first; i += 1) {
|
|
537
|
+
if (text.charCodeAt(i) === 10) line += 1;
|
|
538
|
+
}
|
|
539
|
+
return line;
|
|
540
|
+
}
|
|
541
|
+
function toToolCallLocations(args, cwd, line) {
|
|
542
|
+
const path = typeof args?.path === "string" ? args.path : void 0;
|
|
543
|
+
if (!path) return void 0;
|
|
544
|
+
const resolvedPath = isAbsolute(path) ? path : resolvePath(cwd, path);
|
|
545
|
+
return [{ path: resolvedPath, ...typeof line === "number" ? { line } : {} }];
|
|
546
|
+
}
|
|
527
547
|
var SessionManager = class {
|
|
528
548
|
sessions = /* @__PURE__ */ new Map();
|
|
529
549
|
store = new SessionStore();
|
|
550
|
+
/** Dispose all sessions and their underlying pi subprocesses. */
|
|
551
|
+
disposeAll() {
|
|
552
|
+
for (const [id] of this.sessions) this.close(id);
|
|
553
|
+
}
|
|
530
554
|
/** Get a registered session if it exists (no throw). */
|
|
531
555
|
maybeGet(sessionId) {
|
|
532
556
|
return this.sessions.get(sessionId);
|
|
@@ -544,6 +568,13 @@ var SessionManager = class {
|
|
|
544
568
|
}
|
|
545
569
|
this.sessions.delete(sessionId);
|
|
546
570
|
}
|
|
571
|
+
/** Close all sessions except the one with `keepSessionId`. */
|
|
572
|
+
closeAllExcept(keepSessionId) {
|
|
573
|
+
for (const [id] of this.sessions) {
|
|
574
|
+
if (id === keepSessionId) continue;
|
|
575
|
+
this.close(id);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
547
578
|
async create(params) {
|
|
548
579
|
let proc;
|
|
549
580
|
try {
|
|
@@ -658,13 +689,6 @@ var PiAcpSession = class {
|
|
|
658
689
|
});
|
|
659
690
|
}
|
|
660
691
|
async prompt(message, images = []) {
|
|
661
|
-
if (!this.startupInfoSent && this.startupInfo) {
|
|
662
|
-
this.startupInfoSent = true;
|
|
663
|
-
this.emit({
|
|
664
|
-
sessionUpdate: "agent_message_chunk",
|
|
665
|
-
content: { type: "text", text: this.startupInfo }
|
|
666
|
-
});
|
|
667
|
-
}
|
|
668
692
|
const expandedMessage = expandSlashCommand(message, this.fileCommands);
|
|
669
693
|
const turnPromise = new Promise((resolve3, reject) => {
|
|
670
694
|
const queued = { message: expandedMessage, images, resolve: resolve3, reject };
|
|
@@ -757,6 +781,13 @@ var PiAcpSession = class {
|
|
|
757
781
|
});
|
|
758
782
|
break;
|
|
759
783
|
}
|
|
784
|
+
if (ame?.type === "thinking_delta" && typeof ame.delta === "string") {
|
|
785
|
+
this.emit({
|
|
786
|
+
sessionUpdate: "agent_thought_chunk",
|
|
787
|
+
content: { type: "text", text: ame.delta }
|
|
788
|
+
});
|
|
789
|
+
break;
|
|
790
|
+
}
|
|
760
791
|
if (ame?.type === "toolcall_start" || ame?.type === "toolcall_delta" || ame?.type === "toolcall_end") {
|
|
761
792
|
const toolCall = (
|
|
762
793
|
// pi sometimes includes the tool call directly on the event
|
|
@@ -775,6 +806,7 @@ var PiAcpSession = class {
|
|
|
775
806
|
return { partialArgs: s };
|
|
776
807
|
}
|
|
777
808
|
})();
|
|
809
|
+
const locations = toToolCallLocations(rawInput, this.cwd);
|
|
778
810
|
const existingStatus = this.currentToolCalls.get(toolCallId);
|
|
779
811
|
const status = existingStatus ?? "pending";
|
|
780
812
|
if (!existingStatus) {
|
|
@@ -785,6 +817,7 @@ var PiAcpSession = class {
|
|
|
785
817
|
title: toolName,
|
|
786
818
|
kind: toToolKind(toolName),
|
|
787
819
|
status,
|
|
820
|
+
locations,
|
|
788
821
|
rawInput
|
|
789
822
|
});
|
|
790
823
|
} else {
|
|
@@ -792,6 +825,7 @@ var PiAcpSession = class {
|
|
|
792
825
|
sessionUpdate: "tool_call_update",
|
|
793
826
|
toolCallId,
|
|
794
827
|
status,
|
|
828
|
+
locations,
|
|
795
829
|
rawInput
|
|
796
830
|
});
|
|
797
831
|
}
|
|
@@ -804,6 +838,7 @@ var PiAcpSession = class {
|
|
|
804
838
|
const toolCallId = String(ev.toolCallId ?? crypto.randomUUID());
|
|
805
839
|
const toolName = String(ev.toolName ?? "tool");
|
|
806
840
|
const args = ev.args;
|
|
841
|
+
let line;
|
|
807
842
|
if (toolName === "edit") {
|
|
808
843
|
const p = typeof args?.path === "string" ? args.path : void 0;
|
|
809
844
|
if (p) {
|
|
@@ -811,10 +846,13 @@ var PiAcpSession = class {
|
|
|
811
846
|
const abs = isAbsolute(p) ? p : resolvePath(this.cwd, p);
|
|
812
847
|
const oldText = readFileSync3(abs, "utf8");
|
|
813
848
|
this.editSnapshots.set(toolCallId, { path: p, oldText });
|
|
849
|
+
const needle = typeof args?.oldText === "string" ? args.oldText : "";
|
|
850
|
+
line = findUniqueLineNumber(oldText, needle);
|
|
814
851
|
} catch {
|
|
815
852
|
}
|
|
816
853
|
}
|
|
817
854
|
}
|
|
855
|
+
const locations = toToolCallLocations(args, this.cwd, line);
|
|
818
856
|
if (!this.currentToolCalls.has(toolCallId)) {
|
|
819
857
|
this.currentToolCalls.set(toolCallId, "in_progress");
|
|
820
858
|
this.emit({
|
|
@@ -823,6 +861,7 @@ var PiAcpSession = class {
|
|
|
823
861
|
title: toolName,
|
|
824
862
|
kind: toToolKind(toolName),
|
|
825
863
|
status: "in_progress",
|
|
864
|
+
locations,
|
|
826
865
|
rawInput: args
|
|
827
866
|
});
|
|
828
867
|
} else {
|
|
@@ -831,6 +870,7 @@ var PiAcpSession = class {
|
|
|
831
870
|
sessionUpdate: "tool_call_update",
|
|
832
871
|
toolCallId,
|
|
833
872
|
status: "in_progress",
|
|
873
|
+
locations,
|
|
834
874
|
rawInput: args
|
|
835
875
|
});
|
|
836
876
|
}
|
|
@@ -1457,6 +1497,9 @@ var PiAcpAgent = class {
|
|
|
1457
1497
|
conn;
|
|
1458
1498
|
sessions = new SessionManager();
|
|
1459
1499
|
store = new SessionStore();
|
|
1500
|
+
dispose() {
|
|
1501
|
+
this.sessions.disposeAll();
|
|
1502
|
+
}
|
|
1460
1503
|
// Remember recent session cwd and use it as the default filter.
|
|
1461
1504
|
lastSessionCwd = null;
|
|
1462
1505
|
constructor(conn, _config) {
|
|
@@ -1514,12 +1557,21 @@ var PiAcpAgent = class {
|
|
|
1514
1557
|
fileCommands,
|
|
1515
1558
|
piCommand: process.env.PI_ACP_PI_COMMAND
|
|
1516
1559
|
});
|
|
1517
|
-
let
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1560
|
+
let state = null;
|
|
1561
|
+
let availableModels = null;
|
|
1562
|
+
await Promise.all([
|
|
1563
|
+
session.proc.getState().then((s) => {
|
|
1564
|
+
state = s;
|
|
1565
|
+
}).catch(() => {
|
|
1566
|
+
state = null;
|
|
1567
|
+
}),
|
|
1568
|
+
session.proc.getAvailableModels().then((m) => {
|
|
1569
|
+
availableModels = m;
|
|
1570
|
+
}).catch(() => {
|
|
1571
|
+
availableModels = null;
|
|
1572
|
+
})
|
|
1573
|
+
]);
|
|
1574
|
+
const rawModelsCount = Array.isArray(availableModels?.models) ? availableModels.models.length : 0;
|
|
1523
1575
|
if (rawModelsCount === 0) {
|
|
1524
1576
|
try {
|
|
1525
1577
|
session.proc.dispose?.();
|
|
@@ -1530,8 +1582,8 @@ var PiAcpAgent = class {
|
|
|
1530
1582
|
"Configure an API key or log in with an OAuth provider."
|
|
1531
1583
|
);
|
|
1532
1584
|
}
|
|
1533
|
-
const models = await getModelState(session.proc);
|
|
1534
|
-
const thinking = await getThinkingState(session.proc);
|
|
1585
|
+
const models = await getModelState(session.proc, { state, availableModels });
|
|
1586
|
+
const thinking = await getThinkingState(session.proc, { state });
|
|
1535
1587
|
const quietStartup = getQuietStartup(params.cwd);
|
|
1536
1588
|
const updateNotice = buildUpdateNotice();
|
|
1537
1589
|
const preludeText = quietStartup ? updateNotice ? updateNotice + "\n" : "" : buildStartupInfo({
|
|
@@ -1539,7 +1591,9 @@ var PiAcpAgent = class {
|
|
|
1539
1591
|
fileCommands,
|
|
1540
1592
|
updateNotice
|
|
1541
1593
|
});
|
|
1542
|
-
if (preludeText)
|
|
1594
|
+
if (preludeText)
|
|
1595
|
+
session.setStartupInfo(preludeText);
|
|
1596
|
+
this.sessions.closeAllExcept?.(session.sessionId);
|
|
1543
1597
|
const response = {
|
|
1544
1598
|
sessionId: session.sessionId,
|
|
1545
1599
|
models,
|
|
@@ -2010,6 +2064,7 @@ ${JSON.stringify(stats, null, 2)}`;
|
|
|
2010
2064
|
proc,
|
|
2011
2065
|
fileCommands
|
|
2012
2066
|
});
|
|
2067
|
+
this.sessions.closeAllExcept?.(session.sessionId);
|
|
2013
2068
|
this.store.upsert({
|
|
2014
2069
|
sessionId: params.sessionId,
|
|
2015
2070
|
cwd: params.cwd,
|
|
@@ -2157,14 +2212,17 @@ ${JSON.stringify(stats, null, 2)}`;
|
|
|
2157
2212
|
function isThinkingLevel(x) {
|
|
2158
2213
|
return x === "off" || x === "minimal" || x === "low" || x === "medium" || x === "high" || x === "xhigh";
|
|
2159
2214
|
}
|
|
2160
|
-
async function getThinkingState(proc) {
|
|
2215
|
+
async function getThinkingState(proc, pre) {
|
|
2161
2216
|
let current = "medium";
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2217
|
+
const state = pre?.state ?? await (async () => {
|
|
2218
|
+
try {
|
|
2219
|
+
return await proc.getState();
|
|
2220
|
+
} catch {
|
|
2221
|
+
return null;
|
|
2222
|
+
}
|
|
2223
|
+
})();
|
|
2224
|
+
const tl = typeof state?.thinkingLevel === "string" ? state.thinkingLevel : null;
|
|
2225
|
+
if (tl && isThinkingLevel(tl)) current = tl;
|
|
2168
2226
|
const available = ["off", "minimal", "low", "medium", "high", "xhigh"];
|
|
2169
2227
|
return {
|
|
2170
2228
|
currentModeId: current,
|
|
@@ -2175,34 +2233,40 @@ async function getThinkingState(proc) {
|
|
|
2175
2233
|
}))
|
|
2176
2234
|
};
|
|
2177
2235
|
}
|
|
2178
|
-
async function getModelState(proc) {
|
|
2236
|
+
async function getModelState(proc, pre) {
|
|
2179
2237
|
let availableModels = [];
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2238
|
+
const data = pre?.availableModels ?? await (async () => {
|
|
2239
|
+
try {
|
|
2240
|
+
return await proc.getAvailableModels();
|
|
2241
|
+
} catch {
|
|
2242
|
+
return null;
|
|
2243
|
+
}
|
|
2244
|
+
})();
|
|
2245
|
+
const models = Array.isArray(data?.models) ? data.models : [];
|
|
2246
|
+
availableModels = models.map((m) => {
|
|
2247
|
+
const provider = String(m?.provider ?? "").trim();
|
|
2248
|
+
const id = String(m?.id ?? "").trim();
|
|
2249
|
+
if (!provider || !id) return null;
|
|
2250
|
+
const name = String(m?.name ?? id);
|
|
2251
|
+
return {
|
|
2252
|
+
modelId: `${provider}/${id}`,
|
|
2253
|
+
name: `${provider}/${name}`,
|
|
2254
|
+
description: null
|
|
2255
|
+
};
|
|
2256
|
+
}).filter(Boolean);
|
|
2196
2257
|
let currentModelId = null;
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
const id = String(model.id ?? "").trim();
|
|
2203
|
-
if (provider && id) currentModelId = `${provider}/${id}`;
|
|
2258
|
+
const state = pre?.state ?? await (async () => {
|
|
2259
|
+
try {
|
|
2260
|
+
return await proc.getState();
|
|
2261
|
+
} catch {
|
|
2262
|
+
return null;
|
|
2204
2263
|
}
|
|
2205
|
-
}
|
|
2264
|
+
})();
|
|
2265
|
+
const model = state?.model;
|
|
2266
|
+
if (model && typeof model === "object") {
|
|
2267
|
+
const provider = String(model.provider ?? "").trim();
|
|
2268
|
+
const id = String(model.id ?? "").trim();
|
|
2269
|
+
if (provider && id) currentModelId = `${provider}/${id}`;
|
|
2206
2270
|
}
|
|
2207
2271
|
if (!availableModels.length && !currentModelId) return null;
|
|
2208
2272
|
if (!currentModelId) currentModelId = availableModels[0]?.modelId ?? "default";
|
|
@@ -2369,9 +2433,11 @@ function readNearestPackageJson(metaUrl) {
|
|
|
2369
2433
|
}
|
|
2370
2434
|
|
|
2371
2435
|
// src/index.ts
|
|
2436
|
+
import { platform as platform2 } from "os";
|
|
2372
2437
|
if (process.argv.includes("--terminal-login")) {
|
|
2373
2438
|
const { spawnSync: spawnSync2 } = await import("child_process");
|
|
2374
|
-
const
|
|
2439
|
+
const isWindows = platform2() === "win32";
|
|
2440
|
+
const cmd = process.env.PI_ACP_PI_COMMAND ?? (isWindows ? "pi.cmd" : "pi");
|
|
2375
2441
|
const res = spawnSync2(cmd, [], { stdio: "inherit", env: process.env });
|
|
2376
2442
|
if (res.error && res.error.code === "ENOENT") {
|
|
2377
2443
|
process.stderr.write(
|
|
@@ -2405,10 +2471,23 @@ var output = new ReadableStream({
|
|
|
2405
2471
|
}
|
|
2406
2472
|
});
|
|
2407
2473
|
var stream = ndJsonStream(input, output);
|
|
2408
|
-
new AgentSideConnection((conn) => new PiAcpAgent(conn), stream);
|
|
2474
|
+
var agent = new AgentSideConnection((conn) => new PiAcpAgent(conn), stream);
|
|
2475
|
+
function shutdown() {
|
|
2476
|
+
try {
|
|
2477
|
+
;
|
|
2478
|
+
agent?.agent?.dispose?.();
|
|
2479
|
+
} catch {
|
|
2480
|
+
}
|
|
2481
|
+
try {
|
|
2482
|
+
process.exit(0);
|
|
2483
|
+
} catch {
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
process.stdin.on("end", shutdown);
|
|
2487
|
+
process.stdin.on("close", shutdown);
|
|
2409
2488
|
process.stdin.resume();
|
|
2410
|
-
process.on("SIGINT",
|
|
2411
|
-
process.on("SIGTERM",
|
|
2489
|
+
process.on("SIGINT", shutdown);
|
|
2490
|
+
process.on("SIGTERM", shutdown);
|
|
2412
2491
|
process.stdout.on("error", () => {
|
|
2413
2492
|
try {
|
|
2414
2493
|
process.exit(0);
|