arisa 4.0.18 → 4.0.20
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 +2 -26
- package/package.json +1 -1
- package/src/index.js +10 -146
- package/src/runtime/bootstrap.js +3 -165
- package/src/runtime/create-app.js +2 -2
- package/src/runtime/service-manager.js +2 -2
- package/src/transport/telegram/bot.js +6 -21
- package/src/runtime/process-ids.js +0 -9
- package/test/process-ids.test.js +0 -14
package/README.md
CHANGED
|
@@ -114,6 +114,7 @@ arisa start # start in background
|
|
|
114
114
|
arisa stop # stop background service
|
|
115
115
|
arisa status # show background service status
|
|
116
116
|
arisa flush # remove ~/.arisa
|
|
117
|
+
arisa --silent # run without verbose logs
|
|
117
118
|
```
|
|
118
119
|
|
|
119
120
|
Runtime model override (current process only):
|
|
@@ -138,32 +139,7 @@ On first run, Arisa will:
|
|
|
138
139
|
6. validate that Pi Agent works
|
|
139
140
|
7. only then start listening to Telegram
|
|
140
141
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
You can skip the interactive questions by providing `--telegram.token` and optional overrides:
|
|
144
|
-
|
|
145
|
-
```bash
|
|
146
|
-
node src/index.js --telegram.token <token>
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
With this mode, Arisa creates `~/.arisa/state/config.json` without prompts and applies these defaults when not provided:
|
|
150
|
-
|
|
151
|
-
- `pi.provider`: `openai-codex` when available, otherwise first provider from the current Pi provider list
|
|
152
|
-
- `pi.model`: first model after bootstrap sorting (currently prioritizes `openai-codex/gpt-5.5`)
|
|
153
|
-
- `telegram.maxChatIds`: `1`
|
|
154
|
-
|
|
155
|
-
Supported overrides:
|
|
156
|
-
|
|
157
|
-
```bash
|
|
158
|
-
node src/index.js --telegram.token <token> --telegram.maxChatIds 3 --pi.provider openai-codex --pi.model gpt-5.5 --pi.apiKey <optional-provider-key>
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
Notes:
|
|
162
|
-
|
|
163
|
-
- interactive bootstrap remains unchanged when no CLI overrides are provided
|
|
164
|
-
- `--bootstrap` can be combined with overrides to regenerate config non-interactively
|
|
165
|
-
- when `--pi.apiKey` is omitted and the provider supports OAuth, bootstrap can use a temporary auth relay if `PORT` is set; this relay is only for Pi login and is not a tool web route server
|
|
166
|
-
- unknown `--pi.provider` or `--pi.model` values are ignored and replaced by safe defaults
|
|
142
|
+
Arisa does not run a persistent HTTP health server or Telegram webhook; Telegram uses long polling.
|
|
167
143
|
|
|
168
144
|
Telegram bot tokens can be created with:
|
|
169
145
|
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { createServer } from "node:http";
|
|
4
|
-
import { execFile } from "node:child_process";
|
|
5
3
|
import { bootstrapIfNeeded } from "./runtime/bootstrap.js";
|
|
6
4
|
import { createApp } from "./runtime/create-app.js";
|
|
7
5
|
import { createLogger } from "./runtime/logger.js";
|
|
8
6
|
import { getServiceStatus, registerServiceProcess, startService, stopService, unregisterServiceProcess } from "./runtime/service-manager.js";
|
|
9
7
|
import { flushArisaHome } from "./runtime/flush.js";
|
|
10
8
|
import { arisaPackageDir } from "./runtime/paths.js";
|
|
11
|
-
import { parseProcessIds } from "./runtime/process-ids.js";
|
|
12
9
|
|
|
13
10
|
process.env.ARISA_PACKAGE_DIR = arisaPackageDir;
|
|
14
11
|
|
|
@@ -16,136 +13,13 @@ const args = process.argv.slice(2);
|
|
|
16
13
|
const cli = parseCliArgs(args);
|
|
17
14
|
const command = cli.positionals[0] || "run";
|
|
18
15
|
const forceBootstrap = Boolean(cli.flags.bootstrap);
|
|
19
|
-
const verbose =
|
|
16
|
+
const verbose = !cli.flags.silent;
|
|
20
17
|
const serviceRunner = Boolean(cli.flags["service-runner"]);
|
|
21
|
-
const
|
|
22
|
-
const runtimeOverrides = toRuntimeOverrides(cli.nestedFlags);
|
|
18
|
+
const runtimeOverrides = toNestedOverrides(cli.nestedFlags);
|
|
23
19
|
const logger = createLogger({ verbose });
|
|
24
20
|
let activeApp = null;
|
|
25
21
|
let shuttingDown = false;
|
|
26
22
|
|
|
27
|
-
const defaultHttpPort = 11970;
|
|
28
|
-
const httpPort = Number(process.env.ARISA_HTTP_PORT || defaultHttpPort);
|
|
29
|
-
const shouldStartHttpServer = Boolean(httpPort && !["stop", "status", "flush"].includes(command));
|
|
30
|
-
let httpRequestHandler = null;
|
|
31
|
-
let httpServerListening = false;
|
|
32
|
-
let httpServer = null;
|
|
33
|
-
|
|
34
|
-
function setHttpRequestHandler(handler) {
|
|
35
|
-
httpRequestHandler = handler;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const httpServerReady = shouldStartHttpServer
|
|
39
|
-
? startHttpServer()
|
|
40
|
-
: Promise.resolve(false);
|
|
41
|
-
|
|
42
|
-
async function runCommand(commandName, args = []) {
|
|
43
|
-
return new Promise((resolve) => {
|
|
44
|
-
execFile(commandName, args, (error, stdout = "") => {
|
|
45
|
-
resolve(error ? "" : stdout.trim());
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async function getListeningPids(port) {
|
|
51
|
-
const lsofOutput = await runCommand("lsof", ["-tiTCP", `-sTCP:LISTEN`, `-iTCP:${port}`]);
|
|
52
|
-
if (lsofOutput) {
|
|
53
|
-
return parseProcessIds(lsofOutput);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const fuserOutput = await runCommand("fuser", ["-n", "tcp", String(port)]);
|
|
57
|
-
return parseProcessIds(fuserOutput);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async function getProcessCommand(pid) {
|
|
61
|
-
return runCommand("ps", ["-p", String(pid), "-o", "command="]);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function looksLikeArisaProcess(commandText) {
|
|
65
|
-
return /\barisa\b/.test(commandText) || /\/arisa\/src\/index\.js\b/i.test(commandText);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function isProcessRunning(pid) {
|
|
69
|
-
try {
|
|
70
|
-
process.kill(pid, 0);
|
|
71
|
-
return true;
|
|
72
|
-
} catch {
|
|
73
|
-
return false;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
async function waitForProcessExit(pid, timeoutMs = 3000) {
|
|
78
|
-
const deadline = Date.now() + timeoutMs;
|
|
79
|
-
while (Date.now() < deadline) {
|
|
80
|
-
if (!isProcessRunning(pid)) return true;
|
|
81
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
82
|
-
}
|
|
83
|
-
return !isProcessRunning(pid);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
async function stopStaleArisaListeners(port) {
|
|
87
|
-
const pids = await getListeningPids(port);
|
|
88
|
-
for (const pid of pids) {
|
|
89
|
-
if (pid === process.pid) continue;
|
|
90
|
-
const commandText = await getProcessCommand(pid);
|
|
91
|
-
if (!looksLikeArisaProcess(commandText)) {
|
|
92
|
-
logger.error("http", `port ${port} is already used by pid ${pid}; not killing non-Arisa process`);
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
logger.log("http", `stopping stale Arisa listener on port ${port} (pid ${pid})`);
|
|
97
|
-
try {
|
|
98
|
-
process.kill(pid, "SIGTERM");
|
|
99
|
-
if (!await waitForProcessExit(pid)) {
|
|
100
|
-
process.kill(pid, "SIGKILL");
|
|
101
|
-
await waitForProcessExit(pid, 1000);
|
|
102
|
-
}
|
|
103
|
-
} catch (error) {
|
|
104
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
105
|
-
logger.error("http", `failed to stop stale Arisa listener pid ${pid}: ${message}`);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
async function startHttpServer() {
|
|
111
|
-
await stopStaleArisaListeners(httpPort);
|
|
112
|
-
return new Promise((resolve) => {
|
|
113
|
-
const server = createServer((req, res) => {
|
|
114
|
-
if (httpRequestHandler) return httpRequestHandler(req, res);
|
|
115
|
-
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
116
|
-
res.end("ok");
|
|
117
|
-
return undefined;
|
|
118
|
-
});
|
|
119
|
-
httpServer = server;
|
|
120
|
-
server.on("error", (error) => {
|
|
121
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
122
|
-
logger.error("http", `server failed on port ${httpPort}: ${message}`);
|
|
123
|
-
resolve(false);
|
|
124
|
-
});
|
|
125
|
-
server.listen(httpPort)
|
|
126
|
-
.on("listening", () => {
|
|
127
|
-
httpServerListening = true;
|
|
128
|
-
logger.log("http", `health server on port ${httpPort}`);
|
|
129
|
-
resolve(true);
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
async function getHttpOptions() {
|
|
135
|
-
await httpServerReady;
|
|
136
|
-
return httpServerListening ? { httpPort, setHttpRequestHandler } : {};
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
async function stopHttpServer() {
|
|
140
|
-
if (!httpServerListening || !httpServer) return;
|
|
141
|
-
await new Promise((resolve) => {
|
|
142
|
-
httpServer.close(() => resolve());
|
|
143
|
-
});
|
|
144
|
-
httpServer = null;
|
|
145
|
-
httpServerListening = false;
|
|
146
|
-
httpRequestHandler = null;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
23
|
function parseCliArgs(rawArgs) {
|
|
150
24
|
const flags = {};
|
|
151
25
|
const nestedFlags = {};
|
|
@@ -176,7 +50,7 @@ function parseCliArgs(rawArgs) {
|
|
|
176
50
|
return { flags, nestedFlags, positionals };
|
|
177
51
|
}
|
|
178
52
|
|
|
179
|
-
function
|
|
53
|
+
function toNestedOverrides(nestedFlags) {
|
|
180
54
|
const overrides = {};
|
|
181
55
|
for (const [flatKey, value] of Object.entries(nestedFlags)) {
|
|
182
56
|
const parts = flatKey.split(".");
|
|
@@ -193,10 +67,6 @@ function toBootstrapOverrides(nestedFlags) {
|
|
|
193
67
|
return overrides;
|
|
194
68
|
}
|
|
195
69
|
|
|
196
|
-
function toRuntimeOverrides(nestedFlags) {
|
|
197
|
-
return toBootstrapOverrides(nestedFlags);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
70
|
function toServiceRunnerArgs(nestedFlags) {
|
|
201
71
|
const args = [];
|
|
202
72
|
if (nestedFlags["pi.model"]) {
|
|
@@ -205,14 +75,11 @@ function toServiceRunnerArgs(nestedFlags) {
|
|
|
205
75
|
return args;
|
|
206
76
|
}
|
|
207
77
|
|
|
208
|
-
const webhookUrl = bootstrapOverrides.webhook?.url || "";
|
|
209
|
-
|
|
210
78
|
async function shutdown(exitCode = 0) {
|
|
211
79
|
if (shuttingDown) return;
|
|
212
80
|
shuttingDown = true;
|
|
213
81
|
try {
|
|
214
82
|
await activeApp?.stop?.();
|
|
215
|
-
await stopHttpServer();
|
|
216
83
|
} catch (error) {
|
|
217
84
|
logger.error("app", `shutdown failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
218
85
|
exitCode = exitCode || 1;
|
|
@@ -231,8 +98,8 @@ process.once("SIGINT", () => {
|
|
|
231
98
|
shutdown(0);
|
|
232
99
|
});
|
|
233
100
|
|
|
234
|
-
async function startRuntimeApp(
|
|
235
|
-
const app = await createApp({ logger, runtimeOverrides
|
|
101
|
+
async function startRuntimeApp() {
|
|
102
|
+
const app = await createApp({ logger, runtimeOverrides });
|
|
236
103
|
activeApp = app;
|
|
237
104
|
await app.start();
|
|
238
105
|
}
|
|
@@ -244,10 +111,9 @@ async function runForeground() {
|
|
|
244
111
|
|| runtimeOverrides?.pi?.apiKey
|
|
245
112
|
);
|
|
246
113
|
logger.log("app", `starting${verbose ? " in verbose mode" : ""}`);
|
|
247
|
-
|
|
248
|
-
await bootstrapIfNeeded({ force: forceBootstrap, cliConfigOverrides: bootstrapOverrides, ...httpOptions });
|
|
114
|
+
await bootstrapIfNeeded({ force: forceBootstrap });
|
|
249
115
|
try {
|
|
250
|
-
await startRuntimeApp(
|
|
116
|
+
await startRuntimeApp();
|
|
251
117
|
} catch (error) {
|
|
252
118
|
const message = error instanceof Error ? error.message : String(error);
|
|
253
119
|
if (message.includes("No auth found")) {
|
|
@@ -260,8 +126,8 @@ async function runForeground() {
|
|
|
260
126
|
throw error;
|
|
261
127
|
}
|
|
262
128
|
console.log("Reopening bootstrap so you can provide a Pi API key or switch to a provider you already authenticated with.\n");
|
|
263
|
-
await bootstrapIfNeeded({ force: true
|
|
264
|
-
await startRuntimeApp(
|
|
129
|
+
await bootstrapIfNeeded({ force: true });
|
|
130
|
+
await startRuntimeApp();
|
|
265
131
|
return;
|
|
266
132
|
}
|
|
267
133
|
throw error;
|
|
@@ -276,9 +142,7 @@ async function main() {
|
|
|
276
142
|
}
|
|
277
143
|
|
|
278
144
|
if (command === "start") {
|
|
279
|
-
|
|
280
|
-
await bootstrapIfNeeded({ force: forceBootstrap, cliConfigOverrides: bootstrapOverrides, ...httpOptions });
|
|
281
|
-
await stopHttpServer();
|
|
145
|
+
await bootstrapIfNeeded({ force: forceBootstrap });
|
|
282
146
|
const result = await startService({ verbose, cliArgs: toServiceRunnerArgs(cli.nestedFlags) });
|
|
283
147
|
if (!result.ok) {
|
|
284
148
|
console.log(`Arisa is already running in background (pid ${result.pid}).`);
|
package/src/runtime/bootstrap.js
CHANGED
|
@@ -24,17 +24,6 @@ async function exists(file) {
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
function normalizeString(value) {
|
|
28
|
-
if (typeof value !== "string") return "";
|
|
29
|
-
return value.trim();
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function parseMaxChatIds(value, fallback = 1) {
|
|
33
|
-
const parsed = Number(value);
|
|
34
|
-
if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
|
|
35
|
-
return parsed;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
27
|
function buildConfig({ telegramApiKey, telegramMaxChatIds, provider, model, piApiKey }) {
|
|
39
28
|
return {
|
|
40
29
|
telegram: {
|
|
@@ -52,37 +41,6 @@ function buildConfig({ telegramApiKey, telegramMaxChatIds, provider, model, piAp
|
|
|
52
41
|
};
|
|
53
42
|
}
|
|
54
43
|
|
|
55
|
-
function resolvePiDefaults(runtime, { provider: preferredProvider = "", model: preferredModel = "" } = {}) {
|
|
56
|
-
const providers = sortBootstrapProviders(listPiProviders(runtime));
|
|
57
|
-
if (!providers.length) {
|
|
58
|
-
throw new Error("No Pi providers are available for bootstrap.");
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const preferredProviderValue = normalizeString(preferredProvider);
|
|
62
|
-
const providerExists = providers.some((item) => item.provider === preferredProviderValue);
|
|
63
|
-
if (preferredProviderValue && !providerExists) {
|
|
64
|
-
console.log(`Ignoring unknown Pi provider override: ${preferredProviderValue}`);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const selectedProvider = providerExists
|
|
68
|
-
? preferredProviderValue
|
|
69
|
-
: providers[0].provider;
|
|
70
|
-
|
|
71
|
-
const models = sortBootstrapModels(selectedProvider, listProviderModels(selectedProvider, runtime));
|
|
72
|
-
if (!models.length) {
|
|
73
|
-
throw new Error(`No Pi models are available for provider ${selectedProvider}.`);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const preferredModelValue = normalizeString(preferredModel);
|
|
77
|
-
const modelExists = models.some((item) => item.id === preferredModelValue);
|
|
78
|
-
if (preferredModelValue && !modelExists) {
|
|
79
|
-
console.log(`Ignoring unknown Pi model override for ${selectedProvider}: ${preferredModelValue}`);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const selectedModel = modelExists ? preferredModelValue : models[0].id;
|
|
83
|
-
return { provider: selectedProvider, model: selectedModel };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
44
|
function sortBootstrapProviders(providers) {
|
|
87
45
|
const preferredOrder = ["openai-codex"];
|
|
88
46
|
const positions = new Map(providers.map((provider, index) => [provider.provider, index]));
|
|
@@ -131,89 +89,13 @@ async function maybeOpenExternal(url) {
|
|
|
131
89
|
});
|
|
132
90
|
}
|
|
133
91
|
|
|
134
|
-
function
|
|
135
|
-
let authUrl = "";
|
|
136
|
-
let resolveRedirectUrl;
|
|
137
|
-
const redirectUrlPromise = new Promise((resolve) => {
|
|
138
|
-
resolveRedirectUrl = resolve;
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
const page = (body) => [
|
|
142
|
-
"<!DOCTYPE html><html><head><meta charset='utf-8'><meta name='viewport' content='width=device-width'>",
|
|
143
|
-
"<title>Arisa Auth</title>",
|
|
144
|
-
"<style>body{font-family:system-ui,sans-serif;max-width:600px;margin:40px auto;padding:0 20px;line-height:1.6}",
|
|
145
|
-
"input[type=text]{width:100%;padding:8px;box-sizing:border-box;margin:8px 0}",
|
|
146
|
-
"button{padding:8px 24px;cursor:pointer}code{background:#f0f0f0;padding:2px 6px;border-radius:3px}</style>",
|
|
147
|
-
"</head><body>",
|
|
148
|
-
body,
|
|
149
|
-
"</body></html>"
|
|
150
|
-
].join("");
|
|
151
|
-
|
|
152
|
-
setHttpRequestHandler((req, res) => {
|
|
153
|
-
const parsed = new URL(req.url, `http://localhost:${httpPort}`);
|
|
154
|
-
|
|
155
|
-
if (req.method === "GET" && parsed.pathname === "/auth/callback" && parsed.searchParams.has("code")) {
|
|
156
|
-
const callbackUrl = `http://localhost:1455${parsed.pathname}${parsed.search}`;
|
|
157
|
-
resolveRedirectUrl(callbackUrl);
|
|
158
|
-
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
159
|
-
res.end(page("<h2>Authentication received</h2><p>You can close this page. Arisa is starting…</p>"));
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if (req.method === "GET" && parsed.pathname === "/") {
|
|
164
|
-
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
165
|
-
res.end(page([
|
|
166
|
-
"<h2>Arisa — Pi Authentication</h2>",
|
|
167
|
-
authUrl
|
|
168
|
-
? `<p><strong>1.</strong> <a href="${authUrl}" target="_blank">Click here to log in with Pi</a></p>`
|
|
169
|
-
: "<p>Waiting for authentication URL…</p>",
|
|
170
|
-
"<p><strong>2.</strong> After login your browser will redirect to a <code>localhost</code> URL that won't load. That's expected.</p>",
|
|
171
|
-
"<p><strong>3.</strong> In your browser's address bar, replace <code>localhost:1455</code> with your server's domain and press Enter.</p>",
|
|
172
|
-
"<hr>",
|
|
173
|
-
"<p><em>Or paste the full redirect URL here:</em></p>",
|
|
174
|
-
'<form method="POST" action="/auth/relay">',
|
|
175
|
-
'<input type="text" name="url" placeholder="Paste the localhost redirect URL here…" required />',
|
|
176
|
-
"<button type='submit'>Submit</button>",
|
|
177
|
-
"</form>"
|
|
178
|
-
].join("\n")));
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
if (req.method === "POST" && parsed.pathname === "/auth/relay") {
|
|
183
|
-
let body = "";
|
|
184
|
-
req.on("data", (chunk) => { body += chunk; });
|
|
185
|
-
req.on("end", () => {
|
|
186
|
-
const url = (new URLSearchParams(body).get("url") || "").trim();
|
|
187
|
-
if (url) resolveRedirectUrl(url);
|
|
188
|
-
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
189
|
-
res.end(page("<h2>Authentication received</h2><p>You can close this page. Arisa is starting…</p>"));
|
|
190
|
-
});
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
195
|
-
res.end("ok");
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
return {
|
|
199
|
-
setAuthUrl(url) { authUrl = url; },
|
|
200
|
-
waitForRedirectUrl() { return redirectUrlPromise; },
|
|
201
|
-
uninstall() { setHttpRequestHandler(null); }
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
async function runInternalPiLogin(provider, { rl = null, authRelay = null } = {}) {
|
|
92
|
+
async function runInternalPiLogin(provider, { rl = null } = {}) {
|
|
206
93
|
const login = createPiOAuthLogin({
|
|
207
94
|
provider,
|
|
208
95
|
onAuth: async ({ url, instructions, controller }) => {
|
|
209
96
|
console.log(`${instructions || "Open this URL to continue authentication:"}\n${url}\n`);
|
|
210
97
|
await maybeOpenExternal(url);
|
|
211
|
-
if (
|
|
212
|
-
authRelay.setAuthUrl(url);
|
|
213
|
-
console.log("Waiting for authentication via the web relay...");
|
|
214
|
-
const redirectUrl = await authRelay.waitForRedirectUrl();
|
|
215
|
-
if (redirectUrl) controller.submitManualCode(redirectUrl);
|
|
216
|
-
} else if (controller.oauthProvider.usesCallbackServer && rl) {
|
|
98
|
+
if (controller.oauthProvider.usesCallbackServer && rl) {
|
|
217
99
|
const pasted = (await rl.question("Paste the redirect URL here if the browser does not return automatically, or press Enter to keep waiting: ")).trim();
|
|
218
100
|
if (pasted) controller.submitManualCode(pasted);
|
|
219
101
|
}
|
|
@@ -236,54 +118,10 @@ async function runInternalPiLogin(provider, { rl = null, authRelay = null } = {}
|
|
|
236
118
|
await login.promise;
|
|
237
119
|
}
|
|
238
120
|
|
|
239
|
-
export async function bootstrapIfNeeded({ force = false
|
|
121
|
+
export async function bootstrapIfNeeded({ force = false } = {}) {
|
|
240
122
|
await ensureArisaHome();
|
|
241
123
|
if (!force && await exists(configFile)) return;
|
|
242
124
|
|
|
243
|
-
const telegramApiKeyFromCli = normalizeString(cliConfigOverrides?.telegram?.token);
|
|
244
|
-
if (telegramApiKeyFromCli) {
|
|
245
|
-
const runtime = createPiRuntime();
|
|
246
|
-
const resolvedPi = resolvePiDefaults(runtime, cliConfigOverrides?.pi || {});
|
|
247
|
-
const telegramMaxChatIds = parseMaxChatIds(cliConfigOverrides?.telegram?.maxChatIds, 1);
|
|
248
|
-
const piApiKey = normalizeString(cliConfigOverrides?.pi?.apiKey);
|
|
249
|
-
if (!piApiKey && !hasProviderAuth(resolvedPi.provider, runtime)) {
|
|
250
|
-
if (!supportsProviderOAuth(resolvedPi.provider, runtime)) {
|
|
251
|
-
throw new Error(
|
|
252
|
-
`No auth found for ${resolvedPi.provider}. Provide --pi.apiKey for non-interactive bootstrap, or use a provider that supports OAuth.`
|
|
253
|
-
);
|
|
254
|
-
}
|
|
255
|
-
if (!httpPort || !setHttpRequestHandler) {
|
|
256
|
-
throw new Error(
|
|
257
|
-
`No auth found for ${resolvedPi.provider}. Arisa's HTTP server is unavailable; set ARISA_HTTP_PORT or free the configured port.`
|
|
258
|
-
);
|
|
259
|
-
}
|
|
260
|
-
const authRelay = installAuthRelay(httpPort, setHttpRequestHandler);
|
|
261
|
-
console.log(`No existing Pi auth found for ${resolvedPi.provider}. Auth relay active on port ${httpPort}.`);
|
|
262
|
-
console.log(`Open your server URL in a browser to complete Pi authentication.\n`);
|
|
263
|
-
try {
|
|
264
|
-
await runInternalPiLogin(resolvedPi.provider, { authRelay });
|
|
265
|
-
} finally {
|
|
266
|
-
authRelay.uninstall();
|
|
267
|
-
}
|
|
268
|
-
if (!hasProviderAuth(resolvedPi.provider, createPiRuntime())) {
|
|
269
|
-
throw new Error(
|
|
270
|
-
`Pi login did not complete for ${resolvedPi.provider}. Retry or provide --pi.apiKey.`
|
|
271
|
-
);
|
|
272
|
-
}
|
|
273
|
-
console.log(`Detected Pi auth for ${resolvedPi.provider}. Continuing bootstrap.`);
|
|
274
|
-
}
|
|
275
|
-
const config = buildConfig({
|
|
276
|
-
telegramApiKey: telegramApiKeyFromCli,
|
|
277
|
-
telegramMaxChatIds,
|
|
278
|
-
provider: resolvedPi.provider,
|
|
279
|
-
model: resolvedPi.model,
|
|
280
|
-
piApiKey
|
|
281
|
-
});
|
|
282
|
-
await writeFile(configFile, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
283
|
-
console.log(`\nConfig saved to ${configFile} (non-interactive bootstrap)\n`);
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
125
|
const rl = readline.createInterface({ input, output });
|
|
288
126
|
const ask = async (label, fallback = "") => {
|
|
289
127
|
const suffix = fallback ? ` (${fallback})` : "";
|
|
@@ -46,7 +46,7 @@ function applyRuntimeOverrides(config, runtimeOverrides) {
|
|
|
46
46
|
};
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
export async function createApp({ logger, runtimeOverrides
|
|
49
|
+
export async function createApp({ logger, runtimeOverrides } = {}) {
|
|
50
50
|
logger?.log("app", "loading config");
|
|
51
51
|
const persistedConfig = await loadConfig();
|
|
52
52
|
const config = applyRuntimeOverrides(persistedConfig, runtimeOverrides);
|
|
@@ -64,7 +64,7 @@ export async function createApp({ logger, runtimeOverrides, webhookUrl, setHttpR
|
|
|
64
64
|
const agentManager = new AgentManager({ config, artifactStore, toolRegistry, taskStore, logger });
|
|
65
65
|
const arisaCapabilities = createArisaCapabilities({ artifactStore, taskStore });
|
|
66
66
|
const ipcServer = createIpcServer({ capabilities: arisaCapabilities, logger });
|
|
67
|
-
const bot = await createTelegramBot({ config, artifactStore, toolRegistry, taskStore, agentManager, saveConfig, updateConfig, logger
|
|
67
|
+
const bot = await createTelegramBot({ config, artifactStore, toolRegistry, taskStore, agentManager, saveConfig, updateConfig, logger });
|
|
68
68
|
|
|
69
69
|
return {
|
|
70
70
|
async start() {
|
|
@@ -36,7 +36,7 @@ export async function getServiceStatus() {
|
|
|
36
36
|
return { running: true, pid };
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
export async function startService({ verbose =
|
|
39
|
+
export async function startService({ verbose = true, cliArgs = [] } = {}) {
|
|
40
40
|
await ensureArisaHome();
|
|
41
41
|
const status = await getServiceStatus();
|
|
42
42
|
if (status.running) {
|
|
@@ -45,7 +45,7 @@ export async function startService({ verbose = false, cliArgs = [] } = {}) {
|
|
|
45
45
|
|
|
46
46
|
const logHandle = await open(serviceLogFile, "a");
|
|
47
47
|
const args = [entryFile, "--service-runner", ...cliArgs];
|
|
48
|
-
if (verbose) args.push("--
|
|
48
|
+
if (!verbose) args.push("--silent");
|
|
49
49
|
|
|
50
50
|
const child = spawn(process.execPath, args, {
|
|
51
51
|
detached: true,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Bot, InputFile
|
|
1
|
+
import { Bot, InputFile } from "grammy";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { authorizeChat } from "./auth.js";
|
|
4
4
|
import { captureIncomingArtifact } from "./media.js";
|
|
@@ -251,7 +251,7 @@ async function withTyping(ctx, work) {
|
|
|
251
251
|
}
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
-
export async function createTelegramBot({ config, artifactStore, toolRegistry, taskStore, agentManager, saveConfig, updateConfig, logger
|
|
254
|
+
export async function createTelegramBot({ config, artifactStore, toolRegistry, taskStore, agentManager, saveConfig, updateConfig, logger }) {
|
|
255
255
|
const bot = new Bot(config.telegram.token);
|
|
256
256
|
const perChatState = new Map();
|
|
257
257
|
const notifiedPromptErrors = new WeakSet();
|
|
@@ -736,25 +736,10 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
736
736
|
}, 1000);
|
|
737
737
|
taskTimer.unref();
|
|
738
738
|
}
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
onTimeout: "return",
|
|
744
|
-
});
|
|
745
|
-
webRouter.registerCoreRoute({
|
|
746
|
-
method: "POST",
|
|
747
|
-
path: webhookPath,
|
|
748
|
-
handler: handleUpdate
|
|
749
|
-
});
|
|
750
|
-
await bot.api.setWebhook(`${webhookUrl}${webhookPath}`);
|
|
751
|
-
logger?.log("telegram", `webhook mode: ${webhookUrl}${webhookPath}`);
|
|
752
|
-
scheduleStartupMessages({ skipAgentStartupPrompts });
|
|
753
|
-
} else {
|
|
754
|
-
logger?.log("telegram", "bot polling started");
|
|
755
|
-
scheduleStartupMessages({ skipAgentStartupPrompts });
|
|
756
|
-
await bot.start({ drop_pending_updates: true });
|
|
757
|
-
}
|
|
739
|
+
await bot.api.deleteWebhook({ drop_pending_updates: true });
|
|
740
|
+
logger?.log("telegram", "bot polling started");
|
|
741
|
+
scheduleStartupMessages({ skipAgentStartupPrompts });
|
|
742
|
+
await bot.start();
|
|
758
743
|
},
|
|
759
744
|
|
|
760
745
|
async stop() {
|
package/test/process-ids.test.js
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import test from "node:test";
|
|
3
|
-
import { parseProcessIds } from "../src/runtime/process-ids.js";
|
|
4
|
-
|
|
5
|
-
test("ignores empty PID command output", () => {
|
|
6
|
-
assert.deepEqual(parseProcessIds(""), []);
|
|
7
|
-
assert.deepEqual(parseProcessIds(" \n\t"), []);
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
test("parses only positive integer PID tokens", () => {
|
|
11
|
-
assert.deepEqual(parseProcessIds("123\n456"), [123, 456]);
|
|
12
|
-
assert.deepEqual(parseProcessIds("11970/tcp: 1234"), [1234]);
|
|
13
|
-
assert.deepEqual(parseProcessIds("0 nope -12 42"), [42]);
|
|
14
|
-
});
|