@holon-run/agentinbox 0.1.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/LICENSE +178 -0
- package/README.md +168 -0
- package/dist/src/adapters.js +111 -0
- package/dist/src/backend.js +175 -0
- package/dist/src/cli.js +620 -0
- package/dist/src/client.js +279 -0
- package/dist/src/control_server.js +93 -0
- package/dist/src/daemon.js +246 -0
- package/dist/src/filter.js +167 -0
- package/dist/src/http.js +408 -0
- package/dist/src/matcher.js +47 -0
- package/dist/src/model.js +2 -0
- package/dist/src/paths.js +141 -0
- package/dist/src/service.js +1338 -0
- package/dist/src/source_schema.js +150 -0
- package/dist/src/sources/feishu.js +567 -0
- package/dist/src/sources/github.js +485 -0
- package/dist/src/sources/github_ci.js +372 -0
- package/dist/src/store.js +1271 -0
- package/dist/src/terminal.js +301 -0
- package/dist/src/util.js +36 -0
- package/package.json +52 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.AgentInboxClient = void 0;
|
|
7
|
+
const node_http_1 = __importDefault(require("node:http"));
|
|
8
|
+
const node_https_1 = __importDefault(require("node:https"));
|
|
9
|
+
class AgentInboxClient {
|
|
10
|
+
transport;
|
|
11
|
+
constructor(transport) {
|
|
12
|
+
this.transport = transport;
|
|
13
|
+
}
|
|
14
|
+
async request(endpoint, body, method = "POST") {
|
|
15
|
+
if (this.transport.kind === "socket") {
|
|
16
|
+
return requestViaSocket(this.transport.socketPath, endpoint, body, method);
|
|
17
|
+
}
|
|
18
|
+
return requestViaUrl(this.transport.baseUrl, endpoint, body, method);
|
|
19
|
+
}
|
|
20
|
+
watchInbox(agentId, options = {}) {
|
|
21
|
+
const query = new URLSearchParams();
|
|
22
|
+
if (options.afterItemId) {
|
|
23
|
+
query.set("after_item_id", options.afterItemId);
|
|
24
|
+
}
|
|
25
|
+
if (options.includeAcked) {
|
|
26
|
+
query.set("include_acked", "true");
|
|
27
|
+
}
|
|
28
|
+
if (options.heartbeatMs) {
|
|
29
|
+
query.set("heartbeat_ms", String(options.heartbeatMs));
|
|
30
|
+
}
|
|
31
|
+
const endpoint = `/agents/${encodeURIComponent(agentId)}/inbox/watch${query.size > 0 ? `?${query.toString()}` : ""}`;
|
|
32
|
+
if (this.transport.kind === "socket") {
|
|
33
|
+
return watchViaSocket(this.transport.socketPath, endpoint);
|
|
34
|
+
}
|
|
35
|
+
return watchViaUrl(this.transport.baseUrl, endpoint);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
exports.AgentInboxClient = AgentInboxClient;
|
|
39
|
+
function requestViaSocket(socketPath, endpoint, body, method) {
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
const payload = body ? JSON.stringify(body) : undefined;
|
|
42
|
+
const req = node_http_1.default.request({
|
|
43
|
+
socketPath,
|
|
44
|
+
path: normalizeEndpoint(endpoint),
|
|
45
|
+
method,
|
|
46
|
+
headers: payload
|
|
47
|
+
? {
|
|
48
|
+
"content-type": "application/json",
|
|
49
|
+
"content-length": Buffer.byteLength(payload),
|
|
50
|
+
}
|
|
51
|
+
: undefined,
|
|
52
|
+
}, (res) => {
|
|
53
|
+
void collectResponse(res).then(resolve, reject);
|
|
54
|
+
});
|
|
55
|
+
req.on("error", reject);
|
|
56
|
+
if (payload) {
|
|
57
|
+
req.write(payload);
|
|
58
|
+
}
|
|
59
|
+
req.end();
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function requestViaUrl(baseUrl, endpoint, body, method) {
|
|
63
|
+
return new Promise((resolve, reject) => {
|
|
64
|
+
const url = new URL(normalizeEndpoint(endpoint), baseUrl);
|
|
65
|
+
const payload = body ? JSON.stringify(body) : undefined;
|
|
66
|
+
const transport = url.protocol === "https:" ? node_https_1.default : node_http_1.default;
|
|
67
|
+
const req = transport.request({
|
|
68
|
+
protocol: url.protocol,
|
|
69
|
+
hostname: url.hostname,
|
|
70
|
+
port: url.port,
|
|
71
|
+
path: `${url.pathname}${url.search}`,
|
|
72
|
+
method,
|
|
73
|
+
headers: payload
|
|
74
|
+
? {
|
|
75
|
+
"content-type": "application/json",
|
|
76
|
+
"content-length": Buffer.byteLength(payload),
|
|
77
|
+
}
|
|
78
|
+
: undefined,
|
|
79
|
+
}, (res) => {
|
|
80
|
+
void collectResponse(res).then(resolve, reject);
|
|
81
|
+
});
|
|
82
|
+
req.on("error", reject);
|
|
83
|
+
if (payload) {
|
|
84
|
+
req.write(payload);
|
|
85
|
+
}
|
|
86
|
+
req.end();
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
function watchViaSocket(socketPath, endpoint) {
|
|
90
|
+
return createSseStream((handleResponse) => {
|
|
91
|
+
const req = node_http_1.default.request({
|
|
92
|
+
socketPath,
|
|
93
|
+
path: normalizeEndpoint(endpoint),
|
|
94
|
+
method: "GET",
|
|
95
|
+
headers: {
|
|
96
|
+
accept: "text/event-stream",
|
|
97
|
+
},
|
|
98
|
+
}, handleResponse);
|
|
99
|
+
req.end();
|
|
100
|
+
return req;
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
function watchViaUrl(baseUrl, endpoint) {
|
|
104
|
+
return createSseStream((handleResponse) => {
|
|
105
|
+
const url = new URL(normalizeEndpoint(endpoint), baseUrl);
|
|
106
|
+
const transport = url.protocol === "https:" ? node_https_1.default : node_http_1.default;
|
|
107
|
+
const req = transport.request({
|
|
108
|
+
protocol: url.protocol,
|
|
109
|
+
hostname: url.hostname,
|
|
110
|
+
port: url.port,
|
|
111
|
+
path: `${url.pathname}${url.search}`,
|
|
112
|
+
method: "GET",
|
|
113
|
+
headers: {
|
|
114
|
+
accept: "text/event-stream",
|
|
115
|
+
},
|
|
116
|
+
}, handleResponse);
|
|
117
|
+
req.end();
|
|
118
|
+
return req;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
function collectResponse(res) {
|
|
122
|
+
return new Promise((resolve, reject) => {
|
|
123
|
+
const chunks = [];
|
|
124
|
+
res.on("data", (chunk) => {
|
|
125
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
126
|
+
});
|
|
127
|
+
res.on("end", () => {
|
|
128
|
+
try {
|
|
129
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
130
|
+
const parsed = raw ? JSON.parse(raw) : {};
|
|
131
|
+
resolve({
|
|
132
|
+
statusCode: res.statusCode ?? 500,
|
|
133
|
+
data: parsed,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
reject(error);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
res.on("error", reject);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
function normalizeEndpoint(endpoint) {
|
|
144
|
+
return endpoint.startsWith("/") ? endpoint : `/${endpoint}`;
|
|
145
|
+
}
|
|
146
|
+
function createSseStream(openRequest) {
|
|
147
|
+
const queue = [];
|
|
148
|
+
const waiters = [];
|
|
149
|
+
let done = false;
|
|
150
|
+
let failure = null;
|
|
151
|
+
let request = null;
|
|
152
|
+
let response = null;
|
|
153
|
+
const push = (event) => {
|
|
154
|
+
if (done) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const waiter = waiters.shift();
|
|
158
|
+
if (waiter) {
|
|
159
|
+
waiter({ value: event, done: false });
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
queue.push(event);
|
|
163
|
+
};
|
|
164
|
+
const finish = (error) => {
|
|
165
|
+
if (done) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
done = true;
|
|
169
|
+
failure = error ?? null;
|
|
170
|
+
request?.destroy();
|
|
171
|
+
response?.destroy();
|
|
172
|
+
while (waiters.length > 0) {
|
|
173
|
+
const waiter = waiters.shift();
|
|
174
|
+
if (!waiter) {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
waiter({ value: undefined, done: true });
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
request = openRequest((res) => {
|
|
181
|
+
response = res;
|
|
182
|
+
if ((res.statusCode ?? 500) < 200 || (res.statusCode ?? 500) >= 300) {
|
|
183
|
+
const chunks = [];
|
|
184
|
+
res.on("data", (chunk) => {
|
|
185
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
186
|
+
});
|
|
187
|
+
res.on("end", () => {
|
|
188
|
+
const body = Buffer.concat(chunks).toString("utf8");
|
|
189
|
+
finish(new Error(body || `watch failed with status ${res.statusCode ?? 500}`));
|
|
190
|
+
});
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
let buffer = "";
|
|
194
|
+
res.setEncoding("utf8");
|
|
195
|
+
res.on("data", (chunk) => {
|
|
196
|
+
try {
|
|
197
|
+
buffer += chunk;
|
|
198
|
+
let separatorIndex = buffer.indexOf("\n\n");
|
|
199
|
+
while (separatorIndex >= 0) {
|
|
200
|
+
const frame = buffer.slice(0, separatorIndex);
|
|
201
|
+
buffer = buffer.slice(separatorIndex + 2);
|
|
202
|
+
const event = parseSseFrame(frame);
|
|
203
|
+
if (event) {
|
|
204
|
+
push(event);
|
|
205
|
+
}
|
|
206
|
+
separatorIndex = buffer.indexOf("\n\n");
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
finish(error instanceof Error ? error : new Error(String(error)));
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
res.on("end", () => {
|
|
214
|
+
finish();
|
|
215
|
+
});
|
|
216
|
+
res.on("error", (error) => {
|
|
217
|
+
finish(error instanceof Error ? error : new Error(String(error)));
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
request.on("error", (error) => {
|
|
221
|
+
finish(error instanceof Error ? error : new Error(String(error)));
|
|
222
|
+
});
|
|
223
|
+
return {
|
|
224
|
+
[Symbol.asyncIterator]() {
|
|
225
|
+
return {
|
|
226
|
+
next() {
|
|
227
|
+
if (queue.length > 0) {
|
|
228
|
+
return Promise.resolve({ value: queue.shift(), done: false });
|
|
229
|
+
}
|
|
230
|
+
if (done) {
|
|
231
|
+
if (failure) {
|
|
232
|
+
return Promise.reject(failure);
|
|
233
|
+
}
|
|
234
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
235
|
+
}
|
|
236
|
+
return new Promise((resolve) => {
|
|
237
|
+
waiters.push(resolve);
|
|
238
|
+
}).then((result) => {
|
|
239
|
+
if (result.done && failure) {
|
|
240
|
+
throw failure;
|
|
241
|
+
}
|
|
242
|
+
return result;
|
|
243
|
+
});
|
|
244
|
+
},
|
|
245
|
+
return() {
|
|
246
|
+
finish();
|
|
247
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
function parseSseFrame(frame) {
|
|
254
|
+
const lines = frame
|
|
255
|
+
.split(/\r?\n/)
|
|
256
|
+
.filter((line) => line.length > 0 && !line.startsWith(":"));
|
|
257
|
+
if (lines.length === 0) {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
let eventName = "message";
|
|
261
|
+
const dataLines = [];
|
|
262
|
+
for (const line of lines) {
|
|
263
|
+
if (line.startsWith("event:")) {
|
|
264
|
+
eventName = line.slice("event:".length).trim();
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (line.startsWith("data:")) {
|
|
268
|
+
dataLines.push(line.slice("data:".length).trim());
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (dataLines.length === 0) {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
const payload = JSON.parse(dataLines.join("\n"));
|
|
275
|
+
if (eventName !== payload.event) {
|
|
276
|
+
throw new Error(`unexpected SSE event name ${eventName} for payload event ${payload.event}`);
|
|
277
|
+
}
|
|
278
|
+
return payload;
|
|
279
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.startControlServer = startControlServer;
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_net_1 = __importDefault(require("node:net"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
async function startControlServer(server, transport) {
|
|
11
|
+
if (transport.kind === "socket") {
|
|
12
|
+
await prepareSocketPath(transport.socketPath);
|
|
13
|
+
await new Promise((resolve, reject) => {
|
|
14
|
+
server.once("error", reject);
|
|
15
|
+
server.listen(transport.socketPath, () => {
|
|
16
|
+
server.removeListener("error", reject);
|
|
17
|
+
resolve();
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
return {
|
|
21
|
+
info: {
|
|
22
|
+
socketPath: transport.socketPath,
|
|
23
|
+
},
|
|
24
|
+
async close() {
|
|
25
|
+
await closeServer(server);
|
|
26
|
+
cleanupSocketPath(transport.socketPath);
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
await new Promise((resolve, reject) => {
|
|
31
|
+
server.once("error", reject);
|
|
32
|
+
server.listen(transport.port, transport.host, () => {
|
|
33
|
+
server.removeListener("error", reject);
|
|
34
|
+
resolve();
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
return {
|
|
38
|
+
info: {
|
|
39
|
+
url: transport.baseUrl,
|
|
40
|
+
},
|
|
41
|
+
async close() {
|
|
42
|
+
await closeServer(server);
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
async function prepareSocketPath(socketPath) {
|
|
47
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(socketPath), { recursive: true });
|
|
48
|
+
if (!node_fs_1.default.existsSync(socketPath)) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (await canConnectToSocket(socketPath)) {
|
|
52
|
+
throw new Error(`AgentInbox daemon already running at ${socketPath}`);
|
|
53
|
+
}
|
|
54
|
+
cleanupSocketPath(socketPath);
|
|
55
|
+
}
|
|
56
|
+
function cleanupSocketPath(socketPath) {
|
|
57
|
+
try {
|
|
58
|
+
if (node_fs_1.default.existsSync(socketPath)) {
|
|
59
|
+
node_fs_1.default.unlinkSync(socketPath);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// Best effort cleanup for stale sockets.
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function canConnectToSocket(socketPath) {
|
|
67
|
+
return new Promise((resolve) => {
|
|
68
|
+
const socket = node_net_1.default.createConnection(socketPath);
|
|
69
|
+
let settled = false;
|
|
70
|
+
const finish = (connected) => {
|
|
71
|
+
if (settled) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
settled = true;
|
|
75
|
+
socket.destroy();
|
|
76
|
+
resolve(connected);
|
|
77
|
+
};
|
|
78
|
+
socket.once("connect", () => finish(true));
|
|
79
|
+
socket.once("error", () => finish(false));
|
|
80
|
+
socket.setTimeout(200, () => finish(false));
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
function closeServer(server) {
|
|
84
|
+
return new Promise((resolve, reject) => {
|
|
85
|
+
server.close((error) => {
|
|
86
|
+
if (error) {
|
|
87
|
+
reject(error);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
resolve();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.ensureDaemonForClient = ensureDaemonForClient;
|
|
7
|
+
exports.startDaemon = startDaemon;
|
|
8
|
+
exports.stopDaemon = stopDaemon;
|
|
9
|
+
exports.daemonStatus = daemonStatus;
|
|
10
|
+
exports.writePidFile = writePidFile;
|
|
11
|
+
exports.removePidFile = removePidFile;
|
|
12
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
13
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
14
|
+
const node_child_process_1 = require("node:child_process");
|
|
15
|
+
const client_1 = require("./client");
|
|
16
|
+
const paths_1 = require("./paths");
|
|
17
|
+
const START_TIMEOUT_MS = 15_000;
|
|
18
|
+
async function ensureDaemonForClient(options = {}) {
|
|
19
|
+
const transport = resolveDaemonClientTransport(options);
|
|
20
|
+
if (options.noAutoStart || transport.kind !== "socket") {
|
|
21
|
+
return transport;
|
|
22
|
+
}
|
|
23
|
+
if (await canReachHealthz(transport)) {
|
|
24
|
+
return transport;
|
|
25
|
+
}
|
|
26
|
+
await startDaemon(options);
|
|
27
|
+
return transport;
|
|
28
|
+
}
|
|
29
|
+
async function startDaemon(options = {}) {
|
|
30
|
+
const env = options.env ?? process.env;
|
|
31
|
+
const transport = requireSocketTransport(resolveDaemonClientTransport(options), "daemon start");
|
|
32
|
+
const homeDir = (0, paths_1.resolveAgentInboxHome)(env, options.homeDirOverride);
|
|
33
|
+
const { pidPath, logPath } = (0, paths_1.resolveDaemonPaths)(env, options.homeDirOverride);
|
|
34
|
+
node_fs_1.default.mkdirSync(homeDir, { recursive: true });
|
|
35
|
+
cleanupStalePidFile(pidPath);
|
|
36
|
+
if (await canReachHealthz(transport)) {
|
|
37
|
+
const pid = readPidFile(pidPath);
|
|
38
|
+
return {
|
|
39
|
+
started: false,
|
|
40
|
+
pid: pid ?? -1,
|
|
41
|
+
pidPath,
|
|
42
|
+
logPath,
|
|
43
|
+
transport,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const logFd = openLogFile(logPath);
|
|
47
|
+
let child;
|
|
48
|
+
try {
|
|
49
|
+
child = (0, node_child_process_1.spawn)(process.execPath, daemonChildArgs(), {
|
|
50
|
+
cwd: process.cwd(),
|
|
51
|
+
env: {
|
|
52
|
+
...env,
|
|
53
|
+
AGENTINBOX_HOME: homeDir,
|
|
54
|
+
AGENTINBOX_SOCKET: transport.socketPath,
|
|
55
|
+
AGENTINBOX_URL: "",
|
|
56
|
+
},
|
|
57
|
+
detached: true,
|
|
58
|
+
stdio: ["ignore", logFd, logFd],
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
node_fs_1.default.closeSync(logFd);
|
|
63
|
+
}
|
|
64
|
+
child.unref();
|
|
65
|
+
await waitForHealthz(transport, START_TIMEOUT_MS);
|
|
66
|
+
return {
|
|
67
|
+
started: true,
|
|
68
|
+
pid: child.pid ?? -1,
|
|
69
|
+
pidPath,
|
|
70
|
+
logPath,
|
|
71
|
+
transport,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
async function stopDaemon(options = {}) {
|
|
75
|
+
const env = options.env ?? process.env;
|
|
76
|
+
const transport = requireSocketTransport(resolveDaemonClientTransport(options), "daemon stop");
|
|
77
|
+
const { pidPath, logPath } = (0, paths_1.resolveDaemonPaths)(env, options.homeDirOverride);
|
|
78
|
+
const pid = readPidFile(pidPath);
|
|
79
|
+
if (pid != null && isProcessAlive(pid)) {
|
|
80
|
+
process.kill(pid, "SIGTERM");
|
|
81
|
+
await waitForProcessExit(pid, 3_000);
|
|
82
|
+
}
|
|
83
|
+
cleanupStalePidFile(pidPath, true);
|
|
84
|
+
cleanupFile(transport.socketPath);
|
|
85
|
+
return daemonStatus(options);
|
|
86
|
+
}
|
|
87
|
+
async function daemonStatus(options = {}) {
|
|
88
|
+
const env = options.env ?? process.env;
|
|
89
|
+
const transport = requireSocketTransport(resolveDaemonClientTransport(options), "daemon status");
|
|
90
|
+
const { pidPath, logPath } = (0, paths_1.resolveDaemonPaths)(env, options.homeDirOverride);
|
|
91
|
+
const pid = readPidFile(pidPath);
|
|
92
|
+
if (pid != null && !isProcessAlive(pid)) {
|
|
93
|
+
cleanupStalePidFile(pidPath, true);
|
|
94
|
+
cleanupFile(transport.socketPath);
|
|
95
|
+
return {
|
|
96
|
+
running: false,
|
|
97
|
+
pid: null,
|
|
98
|
+
pidPath,
|
|
99
|
+
logPath,
|
|
100
|
+
transport,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
running: await canReachHealthz(transport),
|
|
105
|
+
pid,
|
|
106
|
+
pidPath,
|
|
107
|
+
logPath,
|
|
108
|
+
transport,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function writePidFile(pidPath, pid) {
|
|
112
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(pidPath), { recursive: true });
|
|
113
|
+
node_fs_1.default.writeFileSync(pidPath, `${pid}\n`, "utf8");
|
|
114
|
+
}
|
|
115
|
+
function removePidFile(pidPath) {
|
|
116
|
+
cleanupFile(pidPath);
|
|
117
|
+
}
|
|
118
|
+
function daemonChildArgs() {
|
|
119
|
+
const execArgv = [...process.execArgv];
|
|
120
|
+
return [...execArgv, process.argv[1], "serve"];
|
|
121
|
+
}
|
|
122
|
+
function requireSocketTransport(transport, commandName) {
|
|
123
|
+
if (transport.kind !== "socket") {
|
|
124
|
+
throw new Error(`${commandName} requires a local socket transport`);
|
|
125
|
+
}
|
|
126
|
+
return transport;
|
|
127
|
+
}
|
|
128
|
+
function resolveDaemonClientTransport(options) {
|
|
129
|
+
const env = options.env ?? process.env;
|
|
130
|
+
const homeDir = (0, paths_1.resolveAgentInboxHome)(env, options.homeDirOverride);
|
|
131
|
+
if (options.socketPathOverride && options.baseUrlOverride) {
|
|
132
|
+
throw new Error("client accepts either --socket or --url, not both");
|
|
133
|
+
}
|
|
134
|
+
if (options.socketPathOverride) {
|
|
135
|
+
return {
|
|
136
|
+
kind: "socket",
|
|
137
|
+
socketPath: node_path_1.default.resolve(options.socketPathOverride),
|
|
138
|
+
source: "flag",
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
if (options.baseUrlOverride) {
|
|
142
|
+
return {
|
|
143
|
+
kind: "url",
|
|
144
|
+
baseUrl: options.baseUrlOverride,
|
|
145
|
+
source: "flag",
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
if (env.AGENTINBOX_SOCKET) {
|
|
149
|
+
return {
|
|
150
|
+
kind: "socket",
|
|
151
|
+
socketPath: node_path_1.default.resolve(env.AGENTINBOX_SOCKET),
|
|
152
|
+
source: "env",
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
if (env.AGENTINBOX_URL) {
|
|
156
|
+
return {
|
|
157
|
+
kind: "url",
|
|
158
|
+
baseUrl: env.AGENTINBOX_URL,
|
|
159
|
+
source: "env",
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
kind: "socket",
|
|
164
|
+
socketPath: node_path_1.default.join(homeDir, "agentinbox.sock"),
|
|
165
|
+
source: "default",
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
function openLogFile(logPath) {
|
|
169
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(logPath), { recursive: true });
|
|
170
|
+
return node_fs_1.default.openSync(logPath, "a");
|
|
171
|
+
}
|
|
172
|
+
async function canReachHealthz(transport) {
|
|
173
|
+
try {
|
|
174
|
+
const client = new client_1.AgentInboxClient(transport);
|
|
175
|
+
const response = await client.request("/healthz", undefined, "GET");
|
|
176
|
+
return response.statusCode === 200 && response.data.ok === true;
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
async function waitForHealthz(transport, timeoutMs) {
|
|
183
|
+
const startedAt = Date.now();
|
|
184
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
185
|
+
if (await canReachHealthz(transport)) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
await sleep(100);
|
|
189
|
+
}
|
|
190
|
+
throw new Error("timed out waiting for AgentInbox daemon to become ready");
|
|
191
|
+
}
|
|
192
|
+
async function waitForProcessExit(pid, timeoutMs) {
|
|
193
|
+
const startedAt = Date.now();
|
|
194
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
195
|
+
if (!isProcessAlive(pid)) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
await sleep(100);
|
|
199
|
+
}
|
|
200
|
+
throw new Error(`timed out waiting for process ${pid} to exit`);
|
|
201
|
+
}
|
|
202
|
+
function cleanupStalePidFile(pidPath, force = false) {
|
|
203
|
+
const pid = readPidFile(pidPath);
|
|
204
|
+
if (pid == null) {
|
|
205
|
+
cleanupFile(pidPath);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (force || !isProcessAlive(pid)) {
|
|
209
|
+
cleanupFile(pidPath);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function readPidFile(pidPath) {
|
|
213
|
+
try {
|
|
214
|
+
const raw = node_fs_1.default.readFileSync(pidPath, "utf8").trim();
|
|
215
|
+
if (!raw) {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
const pid = Number.parseInt(raw, 10);
|
|
219
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function isProcessAlive(pid) {
|
|
226
|
+
try {
|
|
227
|
+
process.kill(pid, 0);
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function cleanupFile(filePath) {
|
|
235
|
+
try {
|
|
236
|
+
if (node_fs_1.default.existsSync(filePath)) {
|
|
237
|
+
node_fs_1.default.unlinkSync(filePath);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
// Best effort cleanup.
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function sleep(ms) {
|
|
245
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
246
|
+
}
|