@wrongstack/acp 0.319.1 → 1.0.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/agent/wrongstack-acp-agent.d.ts +18 -2
- package/dist/agent.js +65 -12
- package/dist/client.js +1 -0
- package/dist/index.js +66 -12
- package/dist/wrongstack-acp-agent.js +65 -12
- package/package.json +2 -2
|
@@ -77,6 +77,8 @@ export declare class WrongStackACPServer {
|
|
|
77
77
|
private readonly options;
|
|
78
78
|
/** HTTP server when transport mode is HTTP. */
|
|
79
79
|
private httpServer;
|
|
80
|
+
/** Live sockets on `httpServer`; destroyed on `stop()` so `close()` settles. */
|
|
81
|
+
private readonly httpSockets;
|
|
80
82
|
private running;
|
|
81
83
|
constructor(opts?: WrongStackACPServerOptions);
|
|
82
84
|
/**
|
|
@@ -87,8 +89,22 @@ export declare class WrongStackACPServer {
|
|
|
87
89
|
start(): Promise<void>;
|
|
88
90
|
private startStdio;
|
|
89
91
|
private startHttp;
|
|
90
|
-
/**
|
|
91
|
-
|
|
92
|
+
/**
|
|
93
|
+
* `listen()` reports failure through an `'error'` event, not the callback.
|
|
94
|
+
* Without a listener that event is an unhandled exception *and* the start
|
|
95
|
+
* promise never settles — a hang rather than a diagnosable failure.
|
|
96
|
+
*
|
|
97
|
+
* An ephemeral bind (`port === 0`) additionally retries the transient
|
|
98
|
+
* resource-exhaustion codes: under a saturated parallel test run the OS can
|
|
99
|
+
* momentarily have no buffer or port to hand out even though nothing is wrong.
|
|
100
|
+
*/
|
|
101
|
+
private listenWithRetry;
|
|
102
|
+
/**
|
|
103
|
+
* Stop the server. Resolves once the HTTP handle is fully closed, so a
|
|
104
|
+
* caller (a test `afterEach`, a restart) cannot race its next bind against
|
|
105
|
+
* sockets this server still owns.
|
|
106
|
+
*/
|
|
107
|
+
stop(): Promise<void>;
|
|
92
108
|
}
|
|
93
109
|
declare function headerValue(value: string | string[] | undefined): string | undefined;
|
|
94
110
|
declare function requestPath(value: string | undefined): string;
|
package/dist/agent.js
CHANGED
|
@@ -1623,12 +1623,16 @@ import { createServer } from "node:http";
|
|
|
1623
1623
|
import { isIP } from "node:net";
|
|
1624
1624
|
import { fileURLToPath } from "node:url";
|
|
1625
1625
|
import { expandIPv6, writeErr as writeErr2 } from "@wrongstack/core/utils";
|
|
1626
|
+
var LISTEN_RETRY_LIMIT = 5;
|
|
1627
|
+
var LISTEN_RETRY_BASE_MS = 25;
|
|
1626
1628
|
var WrongStackACPServer = class {
|
|
1627
1629
|
transport;
|
|
1628
1630
|
handler;
|
|
1629
1631
|
options;
|
|
1630
1632
|
/** HTTP server when transport mode is HTTP. */
|
|
1631
1633
|
httpServer = null;
|
|
1634
|
+
/** Live sockets on `httpServer`; destroyed on `stop()` so `close()` settles. */
|
|
1635
|
+
httpSockets = /* @__PURE__ */ new Set();
|
|
1632
1636
|
running = false;
|
|
1633
1637
|
constructor(opts = {}) {
|
|
1634
1638
|
this.options = opts;
|
|
@@ -1683,7 +1687,7 @@ var WrongStackACPServer = class {
|
|
|
1683
1687
|
throw new Error("ACP HTTP transport requires authToken for non-loopback hosts");
|
|
1684
1688
|
}
|
|
1685
1689
|
let httpChain = Promise.resolve();
|
|
1686
|
-
|
|
1690
|
+
const httpServer = createServer(async (req, res) => {
|
|
1687
1691
|
if (authToken) {
|
|
1688
1692
|
const url = new URL(requestPath(req.url), `http://${host}:${port}`);
|
|
1689
1693
|
const bearerToken = headerValue(req.headers.authorization)?.replace(/^Bearer\s+/i, "");
|
|
@@ -1780,24 +1784,73 @@ var WrongStackACPServer = class {
|
|
|
1780
1784
|
res.end(JSON.stringify({ error: { code: -32603, message: "Internal error" } }));
|
|
1781
1785
|
}
|
|
1782
1786
|
});
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1787
|
+
this.httpServer = httpServer;
|
|
1788
|
+
const sockets = this.httpSockets;
|
|
1789
|
+
httpServer.on("connection", (socket) => {
|
|
1790
|
+
sockets.add(socket);
|
|
1791
|
+
socket.on("close", () => sockets.delete(socket));
|
|
1792
|
+
});
|
|
1793
|
+
await this.listenWithRetry(port, host);
|
|
1794
|
+
const bound = httpServer.address();
|
|
1795
|
+
const shown = typeof bound === "object" && bound ? bound.port : port;
|
|
1796
|
+
writeErr2(`[wstack-acp] HTTP server listening on http://${host}:${shown}
|
|
1786
1797
|
`);
|
|
1787
|
-
|
|
1798
|
+
this.running = true;
|
|
1799
|
+
}
|
|
1800
|
+
/**
|
|
1801
|
+
* `listen()` reports failure through an `'error'` event, not the callback.
|
|
1802
|
+
* Without a listener that event is an unhandled exception *and* the start
|
|
1803
|
+
* promise never settles — a hang rather than a diagnosable failure.
|
|
1804
|
+
*
|
|
1805
|
+
* An ephemeral bind (`port === 0`) additionally retries the transient
|
|
1806
|
+
* resource-exhaustion codes: under a saturated parallel test run the OS can
|
|
1807
|
+
* momentarily have no buffer or port to hand out even though nothing is wrong.
|
|
1808
|
+
*/
|
|
1809
|
+
listenWithRetry(port, host, attempt = 0) {
|
|
1810
|
+
const server = this.httpServer;
|
|
1811
|
+
if (!server) return Promise.resolve();
|
|
1812
|
+
return new Promise((resolve2, reject) => {
|
|
1813
|
+
const onListening = () => {
|
|
1814
|
+
server.removeListener("error", onError);
|
|
1788
1815
|
resolve2();
|
|
1789
|
-
}
|
|
1816
|
+
};
|
|
1817
|
+
const onError = (err) => {
|
|
1818
|
+
server.removeListener("listening", onListening);
|
|
1819
|
+
const transient = err.code === "ENOBUFS" || err.code === "EADDRINUSE" || err.code === "EADDRNOTAVAIL";
|
|
1820
|
+
if (port === 0 && transient && attempt < LISTEN_RETRY_LIMIT) {
|
|
1821
|
+
const timer = setTimeout(
|
|
1822
|
+
() => {
|
|
1823
|
+
this.listenWithRetry(port, host, attempt + 1).then(resolve2, reject);
|
|
1824
|
+
},
|
|
1825
|
+
LISTEN_RETRY_BASE_MS * (attempt + 1)
|
|
1826
|
+
);
|
|
1827
|
+
timer.unref?.();
|
|
1828
|
+
return;
|
|
1829
|
+
}
|
|
1830
|
+
reject(err);
|
|
1831
|
+
};
|
|
1832
|
+
server.once("error", onError);
|
|
1833
|
+
server.once("listening", onListening);
|
|
1834
|
+
server.listen(port, host);
|
|
1790
1835
|
});
|
|
1791
1836
|
}
|
|
1792
|
-
/**
|
|
1793
|
-
|
|
1837
|
+
/**
|
|
1838
|
+
* Stop the server. Resolves once the HTTP handle is fully closed, so a
|
|
1839
|
+
* caller (a test `afterEach`, a restart) cannot race its next bind against
|
|
1840
|
+
* sockets this server still owns.
|
|
1841
|
+
*/
|
|
1842
|
+
async stop() {
|
|
1794
1843
|
this.running = false;
|
|
1795
1844
|
this.handler.close();
|
|
1796
1845
|
this.transport.close();
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1846
|
+
const server = this.httpServer;
|
|
1847
|
+
this.httpServer = null;
|
|
1848
|
+
if (!server) return;
|
|
1849
|
+
for (const socket of this.httpSockets) socket.destroy();
|
|
1850
|
+
this.httpSockets.clear();
|
|
1851
|
+
await new Promise((resolve2) => {
|
|
1852
|
+
server.close(() => resolve2());
|
|
1853
|
+
});
|
|
1801
1854
|
}
|
|
1802
1855
|
};
|
|
1803
1856
|
var defaultEchoRunTurn = async (_input, _emit) => {
|
package/dist/client.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -1298,12 +1298,16 @@ import { createServer } from "node:http";
|
|
|
1298
1298
|
import { isIP } from "node:net";
|
|
1299
1299
|
import { fileURLToPath } from "node:url";
|
|
1300
1300
|
import { expandIPv6, writeErr as writeErr2 } from "@wrongstack/core/utils";
|
|
1301
|
+
var LISTEN_RETRY_LIMIT = 5;
|
|
1302
|
+
var LISTEN_RETRY_BASE_MS = 25;
|
|
1301
1303
|
var WrongStackACPServer = class {
|
|
1302
1304
|
transport;
|
|
1303
1305
|
handler;
|
|
1304
1306
|
options;
|
|
1305
1307
|
/** HTTP server when transport mode is HTTP. */
|
|
1306
1308
|
httpServer = null;
|
|
1309
|
+
/** Live sockets on `httpServer`; destroyed on `stop()` so `close()` settles. */
|
|
1310
|
+
httpSockets = /* @__PURE__ */ new Set();
|
|
1307
1311
|
running = false;
|
|
1308
1312
|
constructor(opts = {}) {
|
|
1309
1313
|
this.options = opts;
|
|
@@ -1358,7 +1362,7 @@ var WrongStackACPServer = class {
|
|
|
1358
1362
|
throw new Error("ACP HTTP transport requires authToken for non-loopback hosts");
|
|
1359
1363
|
}
|
|
1360
1364
|
let httpChain = Promise.resolve();
|
|
1361
|
-
|
|
1365
|
+
const httpServer = createServer(async (req, res) => {
|
|
1362
1366
|
if (authToken) {
|
|
1363
1367
|
const url = new URL(requestPath(req.url), `http://${host}:${port}`);
|
|
1364
1368
|
const bearerToken = headerValue(req.headers.authorization)?.replace(/^Bearer\s+/i, "");
|
|
@@ -1455,24 +1459,73 @@ var WrongStackACPServer = class {
|
|
|
1455
1459
|
res.end(JSON.stringify({ error: { code: -32603, message: "Internal error" } }));
|
|
1456
1460
|
}
|
|
1457
1461
|
});
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1462
|
+
this.httpServer = httpServer;
|
|
1463
|
+
const sockets = this.httpSockets;
|
|
1464
|
+
httpServer.on("connection", (socket) => {
|
|
1465
|
+
sockets.add(socket);
|
|
1466
|
+
socket.on("close", () => sockets.delete(socket));
|
|
1467
|
+
});
|
|
1468
|
+
await this.listenWithRetry(port, host);
|
|
1469
|
+
const bound = httpServer.address();
|
|
1470
|
+
const shown = typeof bound === "object" && bound ? bound.port : port;
|
|
1471
|
+
writeErr2(`[wstack-acp] HTTP server listening on http://${host}:${shown}
|
|
1461
1472
|
`);
|
|
1462
|
-
|
|
1473
|
+
this.running = true;
|
|
1474
|
+
}
|
|
1475
|
+
/**
|
|
1476
|
+
* `listen()` reports failure through an `'error'` event, not the callback.
|
|
1477
|
+
* Without a listener that event is an unhandled exception *and* the start
|
|
1478
|
+
* promise never settles — a hang rather than a diagnosable failure.
|
|
1479
|
+
*
|
|
1480
|
+
* An ephemeral bind (`port === 0`) additionally retries the transient
|
|
1481
|
+
* resource-exhaustion codes: under a saturated parallel test run the OS can
|
|
1482
|
+
* momentarily have no buffer or port to hand out even though nothing is wrong.
|
|
1483
|
+
*/
|
|
1484
|
+
listenWithRetry(port, host, attempt = 0) {
|
|
1485
|
+
const server = this.httpServer;
|
|
1486
|
+
if (!server) return Promise.resolve();
|
|
1487
|
+
return new Promise((resolve4, reject) => {
|
|
1488
|
+
const onListening = () => {
|
|
1489
|
+
server.removeListener("error", onError);
|
|
1463
1490
|
resolve4();
|
|
1464
|
-
}
|
|
1491
|
+
};
|
|
1492
|
+
const onError = (err) => {
|
|
1493
|
+
server.removeListener("listening", onListening);
|
|
1494
|
+
const transient = err.code === "ENOBUFS" || err.code === "EADDRINUSE" || err.code === "EADDRNOTAVAIL";
|
|
1495
|
+
if (port === 0 && transient && attempt < LISTEN_RETRY_LIMIT) {
|
|
1496
|
+
const timer = setTimeout(
|
|
1497
|
+
() => {
|
|
1498
|
+
this.listenWithRetry(port, host, attempt + 1).then(resolve4, reject);
|
|
1499
|
+
},
|
|
1500
|
+
LISTEN_RETRY_BASE_MS * (attempt + 1)
|
|
1501
|
+
);
|
|
1502
|
+
timer.unref?.();
|
|
1503
|
+
return;
|
|
1504
|
+
}
|
|
1505
|
+
reject(err);
|
|
1506
|
+
};
|
|
1507
|
+
server.once("error", onError);
|
|
1508
|
+
server.once("listening", onListening);
|
|
1509
|
+
server.listen(port, host);
|
|
1465
1510
|
});
|
|
1466
1511
|
}
|
|
1467
|
-
/**
|
|
1468
|
-
|
|
1512
|
+
/**
|
|
1513
|
+
* Stop the server. Resolves once the HTTP handle is fully closed, so a
|
|
1514
|
+
* caller (a test `afterEach`, a restart) cannot race its next bind against
|
|
1515
|
+
* sockets this server still owns.
|
|
1516
|
+
*/
|
|
1517
|
+
async stop() {
|
|
1469
1518
|
this.running = false;
|
|
1470
1519
|
this.handler.close();
|
|
1471
1520
|
this.transport.close();
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1521
|
+
const server = this.httpServer;
|
|
1522
|
+
this.httpServer = null;
|
|
1523
|
+
if (!server) return;
|
|
1524
|
+
for (const socket of this.httpSockets) socket.destroy();
|
|
1525
|
+
this.httpSockets.clear();
|
|
1526
|
+
await new Promise((resolve4) => {
|
|
1527
|
+
server.close(() => resolve4());
|
|
1528
|
+
});
|
|
1476
1529
|
}
|
|
1477
1530
|
};
|
|
1478
1531
|
var defaultEchoRunTurn = async (_input, _emit) => {
|
|
@@ -3474,6 +3527,7 @@ var ToolTranslator = class {
|
|
|
3474
3527
|
cancelAll() {
|
|
3475
3528
|
for (const [, p] of this.pending) {
|
|
3476
3529
|
clearTimeout(p.timeout);
|
|
3530
|
+
p.reject(new Error("Call cancelled by client"));
|
|
3477
3531
|
}
|
|
3478
3532
|
this.pending.clear();
|
|
3479
3533
|
}
|
|
@@ -942,12 +942,16 @@ var StdioTransport = class {
|
|
|
942
942
|
};
|
|
943
943
|
|
|
944
944
|
// src/agent/wrongstack-acp-agent.ts
|
|
945
|
+
var LISTEN_RETRY_LIMIT = 5;
|
|
946
|
+
var LISTEN_RETRY_BASE_MS = 25;
|
|
945
947
|
var WrongStackACPServer = class {
|
|
946
948
|
transport;
|
|
947
949
|
handler;
|
|
948
950
|
options;
|
|
949
951
|
/** HTTP server when transport mode is HTTP. */
|
|
950
952
|
httpServer = null;
|
|
953
|
+
/** Live sockets on `httpServer`; destroyed on `stop()` so `close()` settles. */
|
|
954
|
+
httpSockets = /* @__PURE__ */ new Set();
|
|
951
955
|
running = false;
|
|
952
956
|
constructor(opts = {}) {
|
|
953
957
|
this.options = opts;
|
|
@@ -1002,7 +1006,7 @@ var WrongStackACPServer = class {
|
|
|
1002
1006
|
throw new Error("ACP HTTP transport requires authToken for non-loopback hosts");
|
|
1003
1007
|
}
|
|
1004
1008
|
let httpChain = Promise.resolve();
|
|
1005
|
-
|
|
1009
|
+
const httpServer = createServer(async (req, res) => {
|
|
1006
1010
|
if (authToken) {
|
|
1007
1011
|
const url = new URL(requestPath(req.url), `http://${host}:${port}`);
|
|
1008
1012
|
const bearerToken = headerValue(req.headers.authorization)?.replace(/^Bearer\s+/i, "");
|
|
@@ -1099,24 +1103,73 @@ var WrongStackACPServer = class {
|
|
|
1099
1103
|
res.end(JSON.stringify({ error: { code: -32603, message: "Internal error" } }));
|
|
1100
1104
|
}
|
|
1101
1105
|
});
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1106
|
+
this.httpServer = httpServer;
|
|
1107
|
+
const sockets = this.httpSockets;
|
|
1108
|
+
httpServer.on("connection", (socket) => {
|
|
1109
|
+
sockets.add(socket);
|
|
1110
|
+
socket.on("close", () => sockets.delete(socket));
|
|
1111
|
+
});
|
|
1112
|
+
await this.listenWithRetry(port, host);
|
|
1113
|
+
const bound = httpServer.address();
|
|
1114
|
+
const shown = typeof bound === "object" && bound ? bound.port : port;
|
|
1115
|
+
writeErr2(`[wstack-acp] HTTP server listening on http://${host}:${shown}
|
|
1105
1116
|
`);
|
|
1106
|
-
|
|
1117
|
+
this.running = true;
|
|
1118
|
+
}
|
|
1119
|
+
/**
|
|
1120
|
+
* `listen()` reports failure through an `'error'` event, not the callback.
|
|
1121
|
+
* Without a listener that event is an unhandled exception *and* the start
|
|
1122
|
+
* promise never settles — a hang rather than a diagnosable failure.
|
|
1123
|
+
*
|
|
1124
|
+
* An ephemeral bind (`port === 0`) additionally retries the transient
|
|
1125
|
+
* resource-exhaustion codes: under a saturated parallel test run the OS can
|
|
1126
|
+
* momentarily have no buffer or port to hand out even though nothing is wrong.
|
|
1127
|
+
*/
|
|
1128
|
+
listenWithRetry(port, host, attempt = 0) {
|
|
1129
|
+
const server = this.httpServer;
|
|
1130
|
+
if (!server) return Promise.resolve();
|
|
1131
|
+
return new Promise((resolve2, reject) => {
|
|
1132
|
+
const onListening = () => {
|
|
1133
|
+
server.removeListener("error", onError);
|
|
1107
1134
|
resolve2();
|
|
1108
|
-
}
|
|
1135
|
+
};
|
|
1136
|
+
const onError = (err) => {
|
|
1137
|
+
server.removeListener("listening", onListening);
|
|
1138
|
+
const transient = err.code === "ENOBUFS" || err.code === "EADDRINUSE" || err.code === "EADDRNOTAVAIL";
|
|
1139
|
+
if (port === 0 && transient && attempt < LISTEN_RETRY_LIMIT) {
|
|
1140
|
+
const timer = setTimeout(
|
|
1141
|
+
() => {
|
|
1142
|
+
this.listenWithRetry(port, host, attempt + 1).then(resolve2, reject);
|
|
1143
|
+
},
|
|
1144
|
+
LISTEN_RETRY_BASE_MS * (attempt + 1)
|
|
1145
|
+
);
|
|
1146
|
+
timer.unref?.();
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
reject(err);
|
|
1150
|
+
};
|
|
1151
|
+
server.once("error", onError);
|
|
1152
|
+
server.once("listening", onListening);
|
|
1153
|
+
server.listen(port, host);
|
|
1109
1154
|
});
|
|
1110
1155
|
}
|
|
1111
|
-
/**
|
|
1112
|
-
|
|
1156
|
+
/**
|
|
1157
|
+
* Stop the server. Resolves once the HTTP handle is fully closed, so a
|
|
1158
|
+
* caller (a test `afterEach`, a restart) cannot race its next bind against
|
|
1159
|
+
* sockets this server still owns.
|
|
1160
|
+
*/
|
|
1161
|
+
async stop() {
|
|
1113
1162
|
this.running = false;
|
|
1114
1163
|
this.handler.close();
|
|
1115
1164
|
this.transport.close();
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1165
|
+
const server = this.httpServer;
|
|
1166
|
+
this.httpServer = null;
|
|
1167
|
+
if (!server) return;
|
|
1168
|
+
for (const socket of this.httpSockets) socket.destroy();
|
|
1169
|
+
this.httpSockets.clear();
|
|
1170
|
+
await new Promise((resolve2) => {
|
|
1171
|
+
server.close(() => resolve2());
|
|
1172
|
+
});
|
|
1120
1173
|
}
|
|
1121
1174
|
};
|
|
1122
1175
|
var defaultEchoRunTurn = async (_input, _emit) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wrongstack/acp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "ACP (Agent Client Protocol) integration for WrongStack — client + agent support",
|
|
6
6
|
"keywords": [
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
],
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"@agentclientprotocol/sdk": "^1.4.0",
|
|
55
|
-
"@wrongstack/core": "0.
|
|
55
|
+
"@wrongstack/core": "1.0.0"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@types/node": "^26.2.0",
|