arisa 4.0.10 → 4.0.14
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 +1 -1
- package/package.json +8 -7
- package/src/index.js +126 -17
- package/src/runtime/bootstrap.js +1 -1
package/README.md
CHANGED
|
@@ -162,7 +162,7 @@ Notes:
|
|
|
162
162
|
|
|
163
163
|
- interactive bootstrap remains unchanged when no CLI overrides are provided
|
|
164
164
|
- `--bootstrap` can be combined with overrides to regenerate config non-interactively
|
|
165
|
-
-
|
|
165
|
+
- Arisa's HTTP server listens on `ARISA_HTTP_PORT` (default `11970`); bootstrap OAuth pages and tool-provided web routes use this server
|
|
166
166
|
- unknown `--pi.provider` or `--pi.model` values are ignored and replaced by safe defaults
|
|
167
167
|
|
|
168
168
|
Telegram bot tokens can be created with:
|
package/package.json
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arisa",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.14",
|
|
4
4
|
"description": "Telegram + Pi Agent modular assistant",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"bin": {
|
|
8
8
|
"arisa": "bin/arisa.js"
|
|
9
9
|
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node src/index.js",
|
|
12
|
+
"bootstrap": "node src/index.js --bootstrap",
|
|
13
|
+
"test": "node --test"
|
|
14
|
+
},
|
|
10
15
|
"keywords": [
|
|
11
16
|
"telegram",
|
|
12
17
|
"pi-agent",
|
|
@@ -36,14 +41,10 @@
|
|
|
36
41
|
"url": "https://github.com/clasen/Arisa/issues"
|
|
37
42
|
},
|
|
38
43
|
"homepage": "https://github.com/clasen/Arisa#readme",
|
|
44
|
+
"packageManager": "pnpm@11.3.0+sha512.2c403d6594527287672b1f7056343a1f7c3634036a67ffabfcc2b3d7595d843768f8787148d1b57cf7956c90606bbd192857c363af19e96d2d0ec9ec5741d215",
|
|
39
45
|
"dependencies": {
|
|
40
46
|
"@earendil-works/pi-coding-agent": "^0.79.9",
|
|
41
47
|
"@sinclair/typebox": "^0.34.41",
|
|
42
48
|
"grammy": "^1.42.0"
|
|
43
|
-
},
|
|
44
|
-
"scripts": {
|
|
45
|
-
"start": "node src/index.js",
|
|
46
|
-
"bootstrap": "node src/index.js --bootstrap",
|
|
47
|
-
"test": "node --test"
|
|
48
49
|
}
|
|
49
|
-
}
|
|
50
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { createServer } from "node:http";
|
|
4
|
+
import { execFile } from "node:child_process";
|
|
4
5
|
import { bootstrapIfNeeded } from "./runtime/bootstrap.js";
|
|
5
6
|
import { createApp } from "./runtime/create-app.js";
|
|
6
7
|
import { createLogger } from "./runtime/logger.js";
|
|
@@ -22,20 +23,126 @@ const logger = createLogger({ verbose });
|
|
|
22
23
|
let activeApp = null;
|
|
23
24
|
let shuttingDown = false;
|
|
24
25
|
|
|
25
|
-
const
|
|
26
|
+
const defaultHttpPort = 11970;
|
|
27
|
+
const httpPort = Number(process.env.ARISA_HTTP_PORT || defaultHttpPort);
|
|
28
|
+
const shouldStartHttpServer = Boolean(httpPort && !["stop", "status", "flush"].includes(command));
|
|
26
29
|
let httpRequestHandler = null;
|
|
30
|
+
let httpServerListening = false;
|
|
31
|
+
let httpServer = null;
|
|
27
32
|
|
|
28
33
|
function setHttpRequestHandler(handler) {
|
|
29
34
|
httpRequestHandler = handler;
|
|
30
35
|
}
|
|
31
36
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
const httpServerReady = shouldStartHttpServer
|
|
38
|
+
? startHttpServer()
|
|
39
|
+
: Promise.resolve(false);
|
|
40
|
+
|
|
41
|
+
async function runCommand(commandName, args = []) {
|
|
42
|
+
return new Promise((resolve) => {
|
|
43
|
+
execFile(commandName, args, (error, stdout = "") => {
|
|
44
|
+
resolve(error ? "" : stdout.trim());
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function getListeningPids(port) {
|
|
50
|
+
const lsofOutput = await runCommand("lsof", ["-tiTCP", `-sTCP:LISTEN`, `-iTCP:${port}`]);
|
|
51
|
+
if (lsofOutput) {
|
|
52
|
+
return lsofOutput.split(/\s+/).map(Number).filter(Number.isFinite);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const fuserOutput = await runCommand("fuser", ["-n", "tcp", String(port)]);
|
|
56
|
+
return fuserOutput.split(/\s+/).map(Number).filter(Number.isFinite);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function getProcessCommand(pid) {
|
|
60
|
+
return runCommand("ps", ["-p", String(pid), "-o", "command="]);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function looksLikeArisaProcess(commandText) {
|
|
64
|
+
return /\barisa\b/.test(commandText) || /\/arisa\/src\/index\.js\b/i.test(commandText);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isProcessRunning(pid) {
|
|
68
|
+
try {
|
|
69
|
+
process.kill(pid, 0);
|
|
70
|
+
return true;
|
|
71
|
+
} catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function waitForProcessExit(pid, timeoutMs = 3000) {
|
|
77
|
+
const deadline = Date.now() + timeoutMs;
|
|
78
|
+
while (Date.now() < deadline) {
|
|
79
|
+
if (!isProcessRunning(pid)) return true;
|
|
80
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
81
|
+
}
|
|
82
|
+
return !isProcessRunning(pid);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function stopStaleArisaListeners(port) {
|
|
86
|
+
const pids = await getListeningPids(port);
|
|
87
|
+
for (const pid of pids) {
|
|
88
|
+
if (pid === process.pid) continue;
|
|
89
|
+
const commandText = await getProcessCommand(pid);
|
|
90
|
+
if (!looksLikeArisaProcess(commandText)) {
|
|
91
|
+
logger.error("http", `port ${port} is already used by pid ${pid}; not killing non-Arisa process`);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
logger.log("http", `stopping stale Arisa listener on port ${port} (pid ${pid})`);
|
|
96
|
+
try {
|
|
97
|
+
process.kill(pid, "SIGTERM");
|
|
98
|
+
if (!await waitForProcessExit(pid)) {
|
|
99
|
+
process.kill(pid, "SIGKILL");
|
|
100
|
+
await waitForProcessExit(pid, 1000);
|
|
101
|
+
}
|
|
102
|
+
} catch (error) {
|
|
103
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
104
|
+
logger.error("http", `failed to stop stale Arisa listener pid ${pid}: ${message}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function startHttpServer() {
|
|
110
|
+
await stopStaleArisaListeners(httpPort);
|
|
111
|
+
return new Promise((resolve) => {
|
|
112
|
+
const server = createServer((req, res) => {
|
|
113
|
+
if (httpRequestHandler) return httpRequestHandler(req, res);
|
|
114
|
+
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
115
|
+
res.end("ok");
|
|
116
|
+
return undefined;
|
|
117
|
+
});
|
|
118
|
+
httpServer = server;
|
|
119
|
+
server.on("error", (error) => {
|
|
120
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
121
|
+
logger.error("http", `server failed on port ${httpPort}: ${message}`);
|
|
122
|
+
resolve(false);
|
|
123
|
+
});
|
|
124
|
+
server.listen(httpPort)
|
|
125
|
+
.on("listening", () => {
|
|
126
|
+
httpServerListening = true;
|
|
127
|
+
logger.log("http", `health server on port ${httpPort}`);
|
|
128
|
+
resolve(true);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function getHttpOptions() {
|
|
134
|
+
await httpServerReady;
|
|
135
|
+
return httpServerListening ? { httpPort, setHttpRequestHandler } : {};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function stopHttpServer() {
|
|
139
|
+
if (!httpServerListening || !httpServer) return;
|
|
140
|
+
await new Promise((resolve) => {
|
|
141
|
+
httpServer.close(() => resolve());
|
|
142
|
+
});
|
|
143
|
+
httpServer = null;
|
|
144
|
+
httpServerListening = false;
|
|
145
|
+
httpRequestHandler = null;
|
|
39
146
|
}
|
|
40
147
|
|
|
41
148
|
function parseCliArgs(rawArgs) {
|
|
@@ -97,15 +204,14 @@ function toServiceRunnerArgs(nestedFlags) {
|
|
|
97
204
|
return args;
|
|
98
205
|
}
|
|
99
206
|
|
|
100
|
-
const bootstrapHttpOptions = httpPort ? { httpPort, setHttpRequestHandler } : {};
|
|
101
207
|
const webhookUrl = bootstrapOverrides.webhook?.url || "";
|
|
102
|
-
const appHttpOptions = httpPort ? { webhookUrl, setHttpRequestHandler } : {};
|
|
103
208
|
|
|
104
209
|
async function shutdown(exitCode = 0) {
|
|
105
210
|
if (shuttingDown) return;
|
|
106
211
|
shuttingDown = true;
|
|
107
212
|
try {
|
|
108
213
|
await activeApp?.stop?.();
|
|
214
|
+
await stopHttpServer();
|
|
109
215
|
} catch (error) {
|
|
110
216
|
logger.error("app", `shutdown failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
111
217
|
exitCode = exitCode || 1;
|
|
@@ -124,8 +230,8 @@ process.once("SIGINT", () => {
|
|
|
124
230
|
shutdown(0);
|
|
125
231
|
});
|
|
126
232
|
|
|
127
|
-
async function startRuntimeApp() {
|
|
128
|
-
const app = await createApp({ logger, runtimeOverrides, ...
|
|
233
|
+
async function startRuntimeApp(httpOptions = {}) {
|
|
234
|
+
const app = await createApp({ logger, runtimeOverrides, webhookUrl, ...httpOptions });
|
|
129
235
|
activeApp = app;
|
|
130
236
|
await app.start();
|
|
131
237
|
}
|
|
@@ -137,9 +243,10 @@ async function runForeground() {
|
|
|
137
243
|
|| runtimeOverrides?.pi?.apiKey
|
|
138
244
|
);
|
|
139
245
|
logger.log("app", `starting${verbose ? " in verbose mode" : ""}`);
|
|
140
|
-
|
|
246
|
+
const httpOptions = await getHttpOptions();
|
|
247
|
+
await bootstrapIfNeeded({ force: forceBootstrap, cliConfigOverrides: bootstrapOverrides, ...httpOptions });
|
|
141
248
|
try {
|
|
142
|
-
await startRuntimeApp();
|
|
249
|
+
await startRuntimeApp(httpOptions);
|
|
143
250
|
} catch (error) {
|
|
144
251
|
const message = error instanceof Error ? error.message : String(error);
|
|
145
252
|
if (message.includes("No auth found")) {
|
|
@@ -152,8 +259,8 @@ async function runForeground() {
|
|
|
152
259
|
throw error;
|
|
153
260
|
}
|
|
154
261
|
console.log("Reopening bootstrap so you can provide a Pi API key or switch to a provider you already authenticated with.\n");
|
|
155
|
-
await bootstrapIfNeeded({ force: true, cliConfigOverrides: bootstrapOverrides, ...
|
|
156
|
-
await startRuntimeApp();
|
|
262
|
+
await bootstrapIfNeeded({ force: true, cliConfigOverrides: bootstrapOverrides, ...httpOptions });
|
|
263
|
+
await startRuntimeApp(httpOptions);
|
|
157
264
|
return;
|
|
158
265
|
}
|
|
159
266
|
throw error;
|
|
@@ -168,7 +275,9 @@ async function main() {
|
|
|
168
275
|
}
|
|
169
276
|
|
|
170
277
|
if (command === "start") {
|
|
171
|
-
|
|
278
|
+
const httpOptions = await getHttpOptions();
|
|
279
|
+
await bootstrapIfNeeded({ force: forceBootstrap, cliConfigOverrides: bootstrapOverrides, ...httpOptions });
|
|
280
|
+
await stopHttpServer();
|
|
172
281
|
const result = await startService({ verbose, cliArgs: toServiceRunnerArgs(cli.nestedFlags) });
|
|
173
282
|
if (!result.ok) {
|
|
174
283
|
console.log(`Arisa is already running in background (pid ${result.pid}).`);
|
package/src/runtime/bootstrap.js
CHANGED
|
@@ -254,7 +254,7 @@ export async function bootstrapIfNeeded({ force = false, cliConfigOverrides = {}
|
|
|
254
254
|
}
|
|
255
255
|
if (!httpPort || !setHttpRequestHandler) {
|
|
256
256
|
throw new Error(
|
|
257
|
-
`No auth found for ${resolvedPi.provider}.
|
|
257
|
+
`No auth found for ${resolvedPi.provider}. Arisa's HTTP server is unavailable; set ARISA_HTTP_PORT or free the configured port.`
|
|
258
258
|
);
|
|
259
259
|
}
|
|
260
260
|
const authRelay = installAuthRelay(httpPort, setHttpRequestHandler);
|