arisa 4.1.2 → 4.1.4

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.4",
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,24 @@ 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
+ await showSetupPrompt(ctx, `Select the model for ${selectedProvider.provider}:`, {
335
+ reply_markup: buildPagedInlineKeyboard("model", models.map((model) => ({ text: formatModelOption(model) })), { page })
310
336
  });
311
337
  };
312
338
 
313
- const askBackground = async () => {
339
+ const askBackground = async (ctx = null) => {
314
340
  state = "background";
315
- await sendSetupMessage("Bootstrap complete. Keep Arisa running in background now?", {
341
+ await showSetupPrompt(ctx, "Bootstrap complete. Keep Arisa running in background now?", {
316
342
  reply_markup: {
317
343
  inline_keyboard: [
318
344
  [{ text: "Yes, start in background", callback_data: "background:yes" }],
@@ -322,12 +348,14 @@ async function runTelegramBootstrap({ telegramApiKey, setupToken, botInfo }) {
322
348
  });
323
349
  };
324
350
 
325
- const askApiKey = async () => {
351
+ const askApiKey = async (ctx = null) => {
326
352
  state = "pi-api-key";
327
- await sendSetupMessage(`Send the Pi API key for ${selectedProvider.provider}.`);
353
+ await showSetupPrompt(ctx, `Send the Pi API key for ${selectedProvider.provider}.`, {
354
+ reply_markup: { inline_keyboard: [] }
355
+ });
328
356
  };
329
357
 
330
- const askAuthMethod = async () => {
358
+ const askAuthMethod = async (ctx = null) => {
331
359
  const providerRuntime = createPiRuntime();
332
360
  const selectedAuthReady = hasProviderAuth(selectedProvider.provider, providerRuntime);
333
361
  const providerSupportsOAuth = supportsProviderOAuth(selectedProvider.provider, providerRuntime);
@@ -342,7 +370,7 @@ async function runTelegramBootstrap({ telegramApiKey, setupToken, botInfo }) {
342
370
  buttons.push([{ text: "Enter API key", callback_data: "auth:key" }]);
343
371
 
344
372
  state = "auth-method";
345
- await sendSetupMessage([
373
+ await showSetupPrompt(ctx, [
346
374
  `Selected model: ${selectedProvider.provider}/${selectedModel.id}`,
347
375
  `Existing Pi auth for ${selectedProvider.provider}: ${selectedAuthReady ? "yes" : "no"}`,
348
376
  "Choose how Arisa should authenticate Pi."
@@ -440,10 +468,22 @@ async function runTelegramBootstrap({ telegramApiKey, setupToken, botInfo }) {
440
468
  const data = String(ctx.callbackQuery.data || "");
441
469
  const [action, rawValue] = data.split(":");
442
470
 
471
+ if (action === "noop") return;
472
+
473
+ if (action === "provider-page" && state === "provider") {
474
+ await askProvider(ctx, Number(rawValue));
475
+ return;
476
+ }
477
+
478
+ if (action === "model-page" && state === "model") {
479
+ await askModel(ctx, Number(rawValue));
480
+ return;
481
+ }
482
+
443
483
  if (action === "provider" && state === "provider") {
444
484
  selectedProvider = providers[Number(rawValue)];
445
485
  if (!selectedProvider) return;
446
- await askModel();
486
+ await askModel(ctx);
447
487
  return;
448
488
  }
449
489
 
@@ -451,17 +491,17 @@ async function runTelegramBootstrap({ telegramApiKey, setupToken, botInfo }) {
451
491
  const models = sortBootstrapModels(selectedProvider.provider, listProviderModels(selectedProvider.provider, createPiRuntime()));
452
492
  selectedModel = models[Number(rawValue)];
453
493
  if (!selectedModel) return;
454
- await askAuthMethod();
494
+ await askAuthMethod(ctx);
455
495
  return;
456
496
  }
457
497
 
458
498
  if (action === "auth" && state === "auth-method") {
459
499
  if (rawValue === "existing") {
460
500
  if (hasProviderAuth(selectedProvider.provider, createPiRuntime())) {
461
- await askBackground();
501
+ await askBackground(ctx);
462
502
  } else {
463
503
  await sendSetupMessage(`No existing Pi auth found for ${selectedProvider.provider}.`);
464
- await askAuthMethod();
504
+ await askAuthMethod(ctx);
465
505
  }
466
506
  return;
467
507
  }
@@ -470,7 +510,7 @@ async function runTelegramBootstrap({ telegramApiKey, setupToken, botInfo }) {
470
510
  return;
471
511
  }
472
512
  if (rawValue === "key") {
473
- await askApiKey();
513
+ await askApiKey(ctx);
474
514
  return;
475
515
  }
476
516
  }
@@ -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
  }