arisa 4.1.2 → 4.1.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.1.2",
3
+ "version": "4.1.6",
4
4
  "description": "Telegram + Pi Agent modular assistant",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -98,13 +98,28 @@ function parseYesNo(value, fallback = true) {
98
98
  return null;
99
99
  }
100
100
 
101
- function buildInlineKeyboard(action, items) {
102
- return {
103
- inline_keyboard: items.map((item, index) => ([{
104
- text: item.text,
105
- callback_data: `${action}:${index}`
106
- }]))
107
- };
101
+ function buildPagedInlineKeyboard(action, items, { page = 0, pageSize = 8 } = {}) {
102
+ const pageCount = Math.max(1, Math.ceil(items.length / pageSize));
103
+ const currentPage = Math.max(0, Math.min(pageCount - 1, page));
104
+ const startIndex = currentPage * pageSize;
105
+ const rows = items.slice(startIndex, startIndex + pageSize).map((item, index) => ([{
106
+ text: item.text,
107
+ callback_data: `${action}:${startIndex + index}`
108
+ }]));
109
+
110
+ if (pageCount > 1) {
111
+ const navigation = [];
112
+ if (currentPage > 0) {
113
+ navigation.push({ text: "Previous", callback_data: `${action}-page:${currentPage - 1}` });
114
+ }
115
+ navigation.push({ text: `${currentPage + 1}/${pageCount}`, callback_data: "noop:page" });
116
+ if (currentPage < pageCount - 1) {
117
+ navigation.push({ text: "Next", callback_data: `${action}-page:${currentPage + 1}` });
118
+ }
119
+ rows.push(navigation);
120
+ }
121
+
122
+ return { inline_keyboard: rows };
108
123
  }
109
124
 
110
125
  function getIncomingChatMeta(ctx) {
@@ -275,6 +290,17 @@ async function runTelegramBootstrap({ telegramApiKey, setupToken, botInfo }) {
275
290
  await bot.api.sendMessage(setupChatId, text, extra);
276
291
  };
277
292
 
293
+ const showSetupPrompt = async (ctx, text, extra = {}) => {
294
+ const messageId = ctx?.callbackQuery?.message?.message_id;
295
+ if (messageId && ctx.chat?.id) {
296
+ try {
297
+ await ctx.api.editMessageText(ctx.chat.id, messageId, text, extra);
298
+ return;
299
+ } catch {}
300
+ }
301
+ await sendSetupMessage(text, extra);
302
+ };
303
+
278
304
  const isSetupChat = (ctx) => setupChatId && ctx.chat?.id === setupChatId;
279
305
 
280
306
  const complete = (startInBackground) => {
@@ -295,24 +321,26 @@ async function runTelegramBootstrap({ telegramApiKey, setupToken, botInfo }) {
295
321
  });
296
322
  };
297
323
 
298
- const askProvider = async () => {
324
+ const askProvider = async (ctx = null, page = 0) => {
299
325
  state = "provider";
300
- await sendSetupMessage("Select the Pi provider Arisa should use:", {
301
- reply_markup: buildInlineKeyboard("provider", providers.map((provider) => ({ text: formatProviderOption(provider) })))
326
+ await showSetupPrompt(ctx, "Select the Pi provider Arisa should use:", {
327
+ reply_markup: buildPagedInlineKeyboard("provider", providers.map((provider) => ({ text: formatProviderOption(provider) })), { page })
302
328
  });
303
329
  };
304
330
 
305
- const askModel = async () => {
331
+ const askModel = async (ctx = null, page = 0) => {
306
332
  state = "model";
307
333
  const models = sortBootstrapModels(selectedProvider.provider, listProviderModels(selectedProvider.provider, createPiRuntime()));
308
- await sendSetupMessage(`Select the model for ${selectedProvider.provider}:`, {
309
- reply_markup: buildInlineKeyboard("model", models.map((model) => ({ text: formatModelOption(model) })))
334
+ const keyboard = buildPagedInlineKeyboard("model", models.map((model) => ({ text: formatModelOption(model) })), { page });
335
+ keyboard.inline_keyboard.push([{ text: "Back to providers", callback_data: "back:provider" }]);
336
+ await showSetupPrompt(ctx, `Select the model for ${selectedProvider.provider}:`, {
337
+ reply_markup: keyboard
310
338
  });
311
339
  };
312
340
 
313
- const askBackground = async () => {
341
+ const askBackground = async (ctx = null) => {
314
342
  state = "background";
315
- await sendSetupMessage("Bootstrap complete. Keep Arisa running in background now?", {
343
+ await showSetupPrompt(ctx, "Bootstrap complete. Keep Arisa running in background now?", {
316
344
  reply_markup: {
317
345
  inline_keyboard: [
318
346
  [{ text: "Yes, start in background", callback_data: "background:yes" }],
@@ -322,12 +350,14 @@ async function runTelegramBootstrap({ telegramApiKey, setupToken, botInfo }) {
322
350
  });
323
351
  };
324
352
 
325
- const askApiKey = async () => {
353
+ const askApiKey = async (ctx = null) => {
326
354
  state = "pi-api-key";
327
- await sendSetupMessage(`Send the Pi API key for ${selectedProvider.provider}.`);
355
+ await showSetupPrompt(ctx, `Send the Pi API key for ${selectedProvider.provider}.`, {
356
+ reply_markup: { inline_keyboard: [] }
357
+ });
328
358
  };
329
359
 
330
- const askAuthMethod = async () => {
360
+ const askAuthMethod = async (ctx = null) => {
331
361
  const providerRuntime = createPiRuntime();
332
362
  const selectedAuthReady = hasProviderAuth(selectedProvider.provider, providerRuntime);
333
363
  const providerSupportsOAuth = supportsProviderOAuth(selectedProvider.provider, providerRuntime);
@@ -340,9 +370,10 @@ async function runTelegramBootstrap({ telegramApiKey, setupToken, botInfo }) {
340
370
  buttons.push([{ text: selectedAuthReady ? "Run Pi login again" : "Start Pi login", callback_data: "auth:login" }]);
341
371
  }
342
372
  buttons.push([{ text: "Enter API key", callback_data: "auth:key" }]);
373
+ buttons.push([{ text: "Back to models", callback_data: "back:model" }]);
343
374
 
344
375
  state = "auth-method";
345
- await sendSetupMessage([
376
+ await showSetupPrompt(ctx, [
346
377
  `Selected model: ${selectedProvider.provider}/${selectedModel.id}`,
347
378
  `Existing Pi auth for ${selectedProvider.provider}: ${selectedAuthReady ? "yes" : "no"}`,
348
379
  "Choose how Arisa should authenticate Pi."
@@ -440,10 +471,35 @@ async function runTelegramBootstrap({ telegramApiKey, setupToken, botInfo }) {
440
471
  const data = String(ctx.callbackQuery.data || "");
441
472
  const [action, rawValue] = data.split(":");
442
473
 
474
+ if (action === "noop") return;
475
+
476
+ if (action === "back" && rawValue === "provider" && state === "model") {
477
+ selectedProvider = null;
478
+ selectedModel = null;
479
+ await askProvider(ctx);
480
+ return;
481
+ }
482
+
483
+ if (action === "back" && rawValue === "model" && state === "auth-method") {
484
+ selectedModel = null;
485
+ await askModel(ctx);
486
+ return;
487
+ }
488
+
489
+ if (action === "provider-page" && state === "provider") {
490
+ await askProvider(ctx, Number(rawValue));
491
+ return;
492
+ }
493
+
494
+ if (action === "model-page" && state === "model") {
495
+ await askModel(ctx, Number(rawValue));
496
+ return;
497
+ }
498
+
443
499
  if (action === "provider" && state === "provider") {
444
500
  selectedProvider = providers[Number(rawValue)];
445
501
  if (!selectedProvider) return;
446
- await askModel();
502
+ await askModel(ctx);
447
503
  return;
448
504
  }
449
505
 
@@ -451,17 +507,17 @@ async function runTelegramBootstrap({ telegramApiKey, setupToken, botInfo }) {
451
507
  const models = sortBootstrapModels(selectedProvider.provider, listProviderModels(selectedProvider.provider, createPiRuntime()));
452
508
  selectedModel = models[Number(rawValue)];
453
509
  if (!selectedModel) return;
454
- await askAuthMethod();
510
+ await askAuthMethod(ctx);
455
511
  return;
456
512
  }
457
513
 
458
514
  if (action === "auth" && state === "auth-method") {
459
515
  if (rawValue === "existing") {
460
516
  if (hasProviderAuth(selectedProvider.provider, createPiRuntime())) {
461
- await askBackground();
517
+ await askBackground(ctx);
462
518
  } else {
463
519
  await sendSetupMessage(`No existing Pi auth found for ${selectedProvider.provider}.`);
464
- await askAuthMethod();
520
+ await askAuthMethod(ctx);
465
521
  }
466
522
  return;
467
523
  }
@@ -470,7 +526,7 @@ async function runTelegramBootstrap({ telegramApiKey, setupToken, botInfo }) {
470
526
  return;
471
527
  }
472
528
  if (rawValue === "key") {
473
- await askApiKey();
529
+ await askApiKey(ctx);
474
530
  return;
475
531
  }
476
532
  }
@@ -11,6 +11,10 @@ function isStaleSocketError(error) {
11
11
  return ["ENOENT", "ECONNREFUSED"].includes(error?.code);
12
12
  }
13
13
 
14
+ function isNamedPipe(socketPath) {
15
+ return typeof socketPath === "string" && /^\\\\[.?]\\pipe\\/i.test(socketPath);
16
+ }
17
+
14
18
  async function hasLiveSocket(socketPath) {
15
19
  return new Promise((resolve, reject) => {
16
20
  const client = net.createConnection(socketPath);
@@ -52,14 +56,19 @@ export function createIpcServer({ capabilities, socketPath = arisaIpcSocketFile,
52
56
 
53
57
  async function start() {
54
58
  if (server) return { socketPath };
55
- await mkdir(path.dirname(socketPath), { recursive: true });
59
+ const namedPipe = isNamedPipe(socketPath);
60
+ if (!namedPipe) {
61
+ await mkdir(path.dirname(socketPath), { recursive: true });
62
+ }
56
63
 
57
64
  if (await hasLiveSocket(socketPath)) {
58
65
  throw new Error(`Arisa IPC socket already in use: ${socketPath}`);
59
66
  }
60
- await unlink(socketPath).catch((error) => {
61
- if (error?.code !== "ENOENT") throw error;
62
- });
67
+ if (!namedPipe) {
68
+ await unlink(socketPath).catch((error) => {
69
+ if (error?.code !== "ENOENT") throw error;
70
+ });
71
+ }
63
72
 
64
73
  server = net.createServer((socket) => {
65
74
  socket.setEncoding("utf8");
@@ -83,7 +92,9 @@ export function createIpcServer({ capabilities, socketPath = arisaIpcSocketFile,
83
92
  server.once("error", reject);
84
93
  server.listen(socketPath, resolve);
85
94
  });
86
- await chmod(socketPath, 0o600).catch(() => {});
95
+ if (!namedPipe) {
96
+ await chmod(socketPath, 0o600).catch(() => {});
97
+ }
87
98
  logger?.log?.("ipc", `listening on ${socketPath}`);
88
99
  return { socketPath };
89
100
  }
@@ -93,9 +104,11 @@ export function createIpcServer({ capabilities, socketPath = arisaIpcSocketFile,
93
104
  const closingServer = server;
94
105
  server = null;
95
106
  await new Promise((resolve) => closingServer.close(resolve));
96
- await unlink(socketPath).catch((error) => {
97
- if (error?.code !== "ENOENT") throw error;
98
- });
107
+ if (!isNamedPipe(socketPath)) {
108
+ await unlink(socketPath).catch((error) => {
109
+ if (error?.code !== "ENOENT") throw error;
110
+ });
111
+ }
99
112
  }
100
113
 
101
114
  return {
@@ -1,4 +1,5 @@
1
1
  import { mkdir } from "node:fs/promises";
2
+ import crypto from "node:crypto";
2
3
  import os from "node:os";
3
4
  import path from "node:path";
4
5
  import { fileURLToPath } from "node:url";
@@ -9,7 +10,15 @@ export const stateDir = path.join(arisaHomeDir, "state");
9
10
  export const configFile = path.join(stateDir, "config.json");
10
11
  export const servicePidFile = path.join(stateDir, "arisa.pid");
11
12
  export const serviceLogFile = path.join(stateDir, "arisa.log");
12
- export const arisaIpcSocketFile = path.join(stateDir, "arisa.sock");
13
+ export function createIpcSocketPath({ homeDir = arisaHomeDir, platform = process.platform } = {}) {
14
+ if (platform === "win32") {
15
+ const suffix = crypto.createHash("sha256").update(homeDir).digest("hex").slice(0, 16);
16
+ return `\\\\.\\pipe\\arisa-${suffix}`;
17
+ }
18
+ return path.join(homeDir, "state", "arisa.sock");
19
+ }
20
+
21
+ export const arisaIpcSocketFile = createIpcSocketPath();
13
22
  export const tasksFile = path.join(stateDir, "tasks.json");
14
23
  export const toolsDir = path.join(arisaHomeDir, "tools");
15
24
  export const chatsDir = path.join(arisaHomeDir, "chats");
@@ -7,6 +7,7 @@ import test from "node:test";
7
7
  import { createArisaClient } from "../src/core/tools/ipc-client.js";
8
8
  import { createArisaCapabilities } from "../src/runtime/arisa-capabilities.js";
9
9
  import { createIpcServer } from "../src/runtime/ipc/ipc-server.js";
10
+ import { createIpcSocketPath } from "../src/runtime/paths.js";
10
11
 
11
12
  function createFakeArtifactStore() {
12
13
  const stores = new Map();
@@ -67,6 +68,15 @@ function createCapabilities(overrides = {}) {
67
68
  });
68
69
  }
69
70
 
71
+ test("uses a Windows named pipe for IPC on Windows", () => {
72
+ const socketPath = createIpcSocketPath({
73
+ homeDir: "C:\\Users\\Arisa\\.arisa",
74
+ platform: "win32"
75
+ });
76
+
77
+ assert.match(socketPath, /^\\\\\.\\pipe\\arisa-[a-f0-9]{16}$/);
78
+ });
79
+
70
80
  function wait(ms) {
71
81
  return new Promise((resolve) => setTimeout(resolve, ms));
72
82
  }