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 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
- ### Non-interactive bootstrap (CLI overrides)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arisa",
3
- "version": "4.0.18",
3
+ "version": "4.0.20",
4
4
  "description": "Telegram + Pi Agent modular assistant",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
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 = Boolean(cli.flags.verbose);
16
+ const verbose = !cli.flags.silent;
20
17
  const serviceRunner = Boolean(cli.flags["service-runner"]);
21
- const bootstrapOverrides = toBootstrapOverrides(cli.nestedFlags);
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 toBootstrapOverrides(nestedFlags) {
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(httpOptions = {}) {
235
- const app = await createApp({ logger, runtimeOverrides, webhookUrl, ...httpOptions });
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
- const httpOptions = await getHttpOptions();
248
- await bootstrapIfNeeded({ force: forceBootstrap, cliConfigOverrides: bootstrapOverrides, ...httpOptions });
114
+ await bootstrapIfNeeded({ force: forceBootstrap });
249
115
  try {
250
- await startRuntimeApp(httpOptions);
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, cliConfigOverrides: bootstrapOverrides, ...httpOptions });
264
- await startRuntimeApp(httpOptions);
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
- const httpOptions = await getHttpOptions();
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}).`);
@@ -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 installAuthRelay(httpPort, setHttpRequestHandler) {
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&hellip;</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 &mdash; 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&hellip;</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&hellip;" 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&hellip;</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 (authRelay) {
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, cliConfigOverrides = {}, httpPort = 0, setHttpRequestHandler } = {}) {
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, webhookUrl, setHttpRequestHandler } = {}) {
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, webhookUrl, setHttpRequestHandler });
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 = false, cliArgs = [] } = {}) {
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("--verbose");
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, webhookCallback } from "grammy";
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, webhookUrl, webRouter }) {
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
- if (webhookUrl && webRouter) {
740
- const webhookPath = `/telegram-${config.telegram.token.slice(-8)}`;
741
- const handleUpdate = webhookCallback(bot, "http", {
742
- timeoutMilliseconds: 60_000,
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() {
@@ -1,9 +0,0 @@
1
- export function parseProcessIds(commandOutput) {
2
- if (!commandOutput) return [];
3
-
4
- return commandOutput
5
- .split(/\s+/)
6
- .filter((token) => /^\d+$/.test(token))
7
- .map(Number)
8
- .filter((pid) => Number.isInteger(pid) && pid > 0);
9
- }
@@ -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
- });