arisa 4.0.5 → 4.0.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arisa",
3
- "version": "4.0.5",
3
+ "version": "4.0.6",
4
4
  "description": "Telegram + Pi Agent modular assistant",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -82,13 +82,28 @@ export function buildPiAuthTelegramMessage({ config, issue = null, verified = fa
82
82
  } else if (status.hasApiKey) {
83
83
  lines.push("A Pi API key is configured, but the provider rejected the current request. Update the key and restart Arisa.");
84
84
  } else if (status.supportsOAuth) {
85
- lines.push("For now, re-run `arisa --bootstrap` on the host and complete Pi login again.");
85
+ lines.push("Run `/auth` here in Telegram to renew the Pi login.");
86
86
  } else {
87
87
  lines.push("This provider needs a Pi API key. Re-run `arisa --bootstrap`, provide a key, and restart Arisa.");
88
88
  }
89
89
 
90
- if (issue) {
91
- lines.push("Telegram-based renewal is not wired yet, but this /auth path is ready for that flow.");
90
+ return lines.join("\n");
91
+ }
92
+
93
+ export function buildPiAuthRecoveryBlockedMessage({ config, issue = null, renewalActive = false }) {
94
+ const status = getPiAuthStatus(config);
95
+ const lines = [
96
+ `Pi authentication is not ready for ${status.provider}/${status.model}.`,
97
+ "I did not send your message to the agent."
98
+ ];
99
+
100
+ if (issue?.message) {
101
+ lines.push(`Details: ${issue.message}`);
92
102
  }
103
+
104
+ lines.push(renewalActive
105
+ ? "A Pi login is already in progress. Paste the redirect URL or code here when the provider gives it to you."
106
+ : "Send `/auth` to start Pi login from Telegram.");
107
+
93
108
  return lines.join("\n");
94
109
  }
@@ -0,0 +1,57 @@
1
+ import { AuthStorage } from "@mariozechner/pi-coding-agent";
2
+
3
+ export function createPiOAuthLogin({ provider, onAuth, onDeviceCode, onPrompt, onProgress, onSelect } = {}) {
4
+ const authStorage = AuthStorage.create();
5
+ const oauthProvider = authStorage.getOAuthProviders().find((item) => item.id === provider);
6
+ if (!oauthProvider) {
7
+ throw new Error(`No internal OAuth login flow is available for ${provider}.`);
8
+ }
9
+
10
+ let resolveManualCode;
11
+ const manualCodePromise = new Promise((resolve) => {
12
+ resolveManualCode = resolve;
13
+ });
14
+
15
+ const controller = {
16
+ provider,
17
+ oauthProvider,
18
+ manualInputRequested: false,
19
+ submitManualCode(value) {
20
+ if (!resolveManualCode) return false;
21
+ controller.manualInputRequested = false;
22
+ resolveManualCode(String(value || "").trim());
23
+ resolveManualCode = null;
24
+ return true;
25
+ },
26
+ waitForManualCode() {
27
+ controller.manualInputRequested = true;
28
+ return manualCodePromise;
29
+ },
30
+ promise: null
31
+ };
32
+
33
+ controller.promise = authStorage.login(provider, {
34
+ onAuth: async (params) => {
35
+ await onAuth?.({ ...params, controller });
36
+ },
37
+ onDeviceCode: async (params) => {
38
+ await onDeviceCode?.({ ...params, controller });
39
+ },
40
+ onPrompt: async (params) => {
41
+ if (!onPrompt) return "";
42
+ return onPrompt({ ...params, controller });
43
+ },
44
+ onProgress: (message) => {
45
+ onProgress?.(message);
46
+ },
47
+ onSelect: async (params) => {
48
+ if (!onSelect) return params.options?.[0]?.id;
49
+ return onSelect({ ...params, controller });
50
+ },
51
+ onManualCodeInput: () => controller.waitForManualCode()
52
+ }).finally(() => {
53
+ controller.submitManualCode("");
54
+ });
55
+
56
+ return controller;
57
+ }
@@ -2,7 +2,7 @@ import { readFile, writeFile } from "node:fs/promises";
2
2
  import readline from "node:readline/promises";
3
3
  import { stdin as input, stdout as output } from "node:process";
4
4
  import { spawn } from "node:child_process";
5
- import { AuthStorage } from "@mariozechner/pi-coding-agent";
5
+ import { createPiOAuthLogin } from "../core/agent/pi-auth-login.js";
6
6
  import { createPiRuntime, hasProviderAuth, listPiProviders, listProviderModels, supportsProviderOAuth } from "../core/agent/pi-runtime.js";
7
7
  import { configFile, ensureArisaHome } from "./paths.js";
8
8
 
@@ -194,37 +194,19 @@ function installAuthRelay(httpPort, setHttpRequestHandler) {
194
194
  }
195
195
 
196
196
  async function runInternalPiLogin(provider, { rl = null, authRelay = null } = {}) {
197
- const authStorage = AuthStorage.create();
198
- const selected = authStorage.getOAuthProviders().find((item) => item.id === provider);
199
- if (!selected) {
200
- throw new Error(`No internal OAuth login flow is available for ${provider}.`);
201
- }
202
-
203
- let manualCodeResolve;
204
- let manualCodeReject;
205
- const manualCodePromise = new Promise((resolve, reject) => {
206
- manualCodeResolve = resolve;
207
- manualCodeReject = reject;
208
- });
209
-
210
- await authStorage.login(provider, {
211
- onAuth: async ({ url, instructions }) => {
197
+ const login = createPiOAuthLogin({
198
+ provider,
199
+ onAuth: async ({ url, instructions, controller }) => {
212
200
  console.log(`${instructions || "Open this URL to continue authentication:"}\n${url}\n`);
213
201
  await maybeOpenExternal(url);
214
202
  if (authRelay) {
215
203
  authRelay.setAuthUrl(url);
216
204
  console.log("Waiting for authentication via the web relay...");
217
205
  const redirectUrl = await authRelay.waitForRedirectUrl();
218
- if (redirectUrl && manualCodeResolve) {
219
- manualCodeResolve(redirectUrl);
220
- manualCodeResolve = undefined;
221
- }
222
- } else if (selected.usesCallbackServer && rl) {
206
+ if (redirectUrl) controller.submitManualCode(redirectUrl);
207
+ } else if (controller.oauthProvider.usesCallbackServer && rl) {
223
208
  const pasted = (await rl.question("Paste the redirect URL here if the browser does not return automatically, or press Enter to keep waiting: ")).trim();
224
- if (pasted && manualCodeResolve) {
225
- manualCodeResolve(pasted);
226
- manualCodeResolve = undefined;
227
- }
209
+ if (pasted) controller.submitManualCode(pasted);
228
210
  }
229
211
  },
230
212
  onDeviceCode: async ({ userCode, verificationUri }) => {
@@ -240,15 +222,9 @@ async function runInternalPiLogin(provider, { rl = null, authRelay = null } = {}
240
222
  },
241
223
  onProgress: (message) => {
242
224
  console.log(message);
243
- },
244
- onManualCodeInput: () => manualCodePromise,
245
- }).finally(() => {
246
- if (manualCodeResolve) {
247
- manualCodeResolve("");
248
- manualCodeResolve = undefined;
249
225
  }
250
- manualCodeReject = undefined;
251
226
  });
227
+ await login.promise;
252
228
  }
253
229
 
254
230
  export async function bootstrapIfNeeded({ force = false, cliConfigOverrides = {}, httpPort = 0, setHttpRequestHandler } = {}) {
@@ -3,7 +3,8 @@ import path from "node:path";
3
3
  import { authorizeChat } from "./auth.js";
4
4
  import { captureIncomingArtifact } from "./media.js";
5
5
  import { renderTelegramHtml } from "./text-format.js";
6
- import { buildPiAuthTelegramMessage, getErrorMessage, getPiAuthIssue } from "../../core/agent/auth-flow.js";
6
+ import { buildPiAuthRecoveryBlockedMessage, buildPiAuthTelegramMessage, getErrorMessage, getPiAuthIssue, getPiAuthStatus } from "../../core/agent/auth-flow.js";
7
+ import { createPiOAuthLogin } from "../../core/agent/pi-auth-login.js";
7
8
  import { normalizeArtifactForReasoning, shouldNormalizeArtifactToText } from "../../core/artifacts/normalize-for-reasoning.js";
8
9
 
9
10
  const slowPromptNoticeMs = 300_000;
@@ -247,8 +248,14 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
247
248
  const bot = new Bot(config.telegram.token);
248
249
  const perChatState = new Map();
249
250
  const notifiedPromptErrors = new WeakSet();
251
+ const authRenewals = new Map();
252
+ let piAuthIssue = null;
250
253
  let taskTimer = null;
251
254
 
255
+ function chatKey(chatId) {
256
+ return String(chatId);
257
+ }
258
+
252
259
  function wasPromptErrorNotified(error) {
253
260
  return error instanceof Error && notifiedPromptErrors.has(error);
254
261
  }
@@ -257,8 +264,14 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
257
264
  if (error instanceof Error) notifiedPromptErrors.add(error);
258
265
  }
259
266
 
260
- async function notifyPiAuthIssueIfNeeded(chatId, error) {
267
+ function rememberPiAuthIssue(error) {
261
268
  const issue = getPiAuthIssue(error);
269
+ if (issue) piAuthIssue = issue;
270
+ return issue;
271
+ }
272
+
273
+ async function notifyPiAuthIssueIfNeeded(chatId, error) {
274
+ const issue = rememberPiAuthIssue(error);
262
275
  if (!issue) return false;
263
276
 
264
277
  try {
@@ -271,6 +284,84 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
271
284
  }
272
285
  }
273
286
 
287
+ function selectTelegramLoginOption(options = []) {
288
+ return options.find((option) => /device/i.test(`${option.id} ${option.label}`))
289
+ || options.find((option) => /browser|oauth|web/i.test(`${option.id} ${option.label}`))
290
+ || options[0]
291
+ || null;
292
+ }
293
+
294
+ async function finishAuthRenewal(chatId, renewal) {
295
+ try {
296
+ await renewal.promise;
297
+ await agentManager.validatePiAgent();
298
+ agentManager.clearSessionCache(chatId);
299
+ piAuthIssue = null;
300
+ logger?.log("telegram", `Pi auth renewal completed for chat ${chatId}`);
301
+ await bot.api.sendMessage(chatId, buildPiAuthTelegramMessage({ config, verified: true }));
302
+ } catch (error) {
303
+ const issue = rememberPiAuthIssue(error) || { kind: "validation-failed", message: getErrorMessage(error) };
304
+ piAuthIssue = issue;
305
+ logger?.error("telegram", `Pi auth renewal failed for chat ${chatId}: ${getErrorMessage(error)}`);
306
+ await bot.api.sendMessage(chatId, buildPiAuthTelegramMessage({ config, issue })).catch((notifyError) => {
307
+ logger?.error("telegram", `auth renewal failure notice failed for chat ${chatId}: ${getErrorMessage(notifyError)}`);
308
+ });
309
+ } finally {
310
+ authRenewals.delete(chatKey(chatId));
311
+ }
312
+ }
313
+
314
+ async function startAuthRenewal(chatId) {
315
+ const key = chatKey(chatId);
316
+ const existing = authRenewals.get(key);
317
+ if (existing) {
318
+ return { started: false, renewal: existing };
319
+ }
320
+
321
+ const renewal = createPiOAuthLogin({
322
+ provider: config.pi.provider,
323
+ onSelect: async ({ message, options }) => {
324
+ const selected = selectTelegramLoginOption(options);
325
+ if (!selected) return undefined;
326
+ logger?.log("telegram", `Pi auth option for chat ${chatId}: ${selected.id}`);
327
+ await bot.api.sendMessage(chatId, `${message}\nUsing: ${selected.label || selected.id}`);
328
+ return selected.id;
329
+ },
330
+ onAuth: async ({ url, instructions }) => {
331
+ await bot.api.sendMessage(chatId, [
332
+ instructions || "Open this URL to continue Pi authentication:",
333
+ url,
334
+ "After login, paste the full redirect URL back here."
335
+ ].join("\n"));
336
+ },
337
+ onDeviceCode: async ({ userCode, verificationUri, expiresInSeconds }) => {
338
+ const expiry = expiresInSeconds ? `\nExpires in ${Math.round(expiresInSeconds / 60)} minute(s).` : "";
339
+ await bot.api.sendMessage(chatId, `Open this URL: ${verificationUri}\nThen enter code: ${userCode}${expiry}`);
340
+ },
341
+ onPrompt: async ({ message, controller }) => {
342
+ await bot.api.sendMessage(chatId, `${message}\nReply here with the value.`);
343
+ return controller.waitForManualCode();
344
+ },
345
+ onProgress: (message) => {
346
+ if (message) logger?.log("telegram", `Pi auth progress for chat ${chatId}: ${message}`);
347
+ }
348
+ });
349
+
350
+ authRenewals.set(key, renewal);
351
+ finishAuthRenewal(chatId, renewal);
352
+ return { started: true, renewal };
353
+ }
354
+
355
+ async function submitAuthRenewalInput(ctx) {
356
+ const renewal = authRenewals.get(chatKey(ctx.chat.id));
357
+ const text = getIncomingMessageText(ctx.message).trim();
358
+ if (!renewal || !renewal.manualInputRequested || !text) return false;
359
+
360
+ if (!renewal.submitManualCode(text)) return false;
361
+ await ctx.reply("Got it. Finishing Pi login now...");
362
+ return true;
363
+ }
364
+
274
365
  function getIncomingChatMeta(ctx) {
275
366
  return {
276
367
  languageCode: ctx.from?.language_code || "",
@@ -523,22 +614,48 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
523
614
  bot.command("new", async (ctx) => {
524
615
  const auth = await authorizeChat({ config, chatId: ctx.chat.id, saveConfig, chatMeta: getIncomingChatMeta(ctx) });
525
616
  if (!auth.ok) return;
617
+ if (piAuthIssue) {
618
+ await ctx.reply(buildPiAuthRecoveryBlockedMessage({
619
+ config,
620
+ issue: piAuthIssue,
621
+ renewalActive: authRenewals.has(chatKey(ctx.chat.id))
622
+ }));
623
+ return;
624
+ }
526
625
  await handleNewCommand(ctx);
527
626
  });
528
627
 
529
628
  bot.command("auth", async (ctx) => {
530
629
  const auth = await authorizeChat({ config, chatId: ctx.chat.id, saveConfig, chatMeta: getIncomingChatMeta(ctx) });
531
630
  if (!auth.ok) return;
532
- await withTyping(ctx, async () => {
533
- try {
534
- await agentManager.validatePiAgent();
535
- agentManager.clearSessionCache(ctx.chat.id);
536
- await ctx.reply(buildPiAuthTelegramMessage({ config, verified: true }));
537
- } catch (error) {
538
- const issue = getPiAuthIssue(error) || { kind: "validation-failed", message: getErrorMessage(error) };
539
- await ctx.reply(buildPiAuthTelegramMessage({ config, issue }));
540
- }
541
- });
631
+
632
+ const status = getPiAuthStatus(config);
633
+ if (status.hasApiKey || !status.supportsOAuth) {
634
+ await withTyping(ctx, async () => {
635
+ try {
636
+ await agentManager.validatePiAgent();
637
+ agentManager.clearSessionCache(ctx.chat.id);
638
+ piAuthIssue = null;
639
+ await ctx.reply(buildPiAuthTelegramMessage({ config, verified: true }));
640
+ } catch (error) {
641
+ const issue = rememberPiAuthIssue(error) || { kind: "validation-failed", message: getErrorMessage(error) };
642
+ piAuthIssue = issue;
643
+ await ctx.reply(buildPiAuthTelegramMessage({ config, issue }));
644
+ }
645
+ });
646
+ return;
647
+ }
648
+
649
+ try {
650
+ const { started } = await startAuthRenewal(ctx.chat.id);
651
+ await ctx.reply(started
652
+ ? "Starting Pi login from Telegram..."
653
+ : "Pi login is already in progress. Paste the redirect URL or code here when you have it.");
654
+ } catch (error) {
655
+ const issue = rememberPiAuthIssue(error) || { kind: "validation-failed", message: getErrorMessage(error) };
656
+ piAuthIssue = issue;
657
+ await ctx.reply(buildPiAuthTelegramMessage({ config, issue }));
658
+ }
542
659
  });
543
660
 
544
661
  bot.on("message", async (ctx) => {
@@ -548,6 +665,17 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
548
665
  const command = getTelegramCommand(ctx);
549
666
  if (command) return;
550
667
 
668
+ if (await submitAuthRenewalInput(ctx)) return;
669
+
670
+ if (piAuthIssue) {
671
+ await ctx.reply(buildPiAuthRecoveryBlockedMessage({
672
+ config,
673
+ issue: piAuthIssue,
674
+ renewalActive: authRenewals.has(chatKey(ctx.chat.id))
675
+ }));
676
+ return;
677
+ }
678
+
551
679
  try {
552
680
  await enqueueOrProcess(ctx);
553
681
  } catch (error) {