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 +1 -1
- package/src/runtime/bootstrap.js +80 -24
- package/src/runtime/ipc/ipc-server.js +21 -8
- package/src/runtime/paths.js +10 -1
- package/test/ipc-server.test.js +10 -0
package/package.json
CHANGED
package/src/runtime/bootstrap.js
CHANGED
|
@@ -98,13 +98,28 @@ function parseYesNo(value, fallback = true) {
|
|
|
98
98
|
return null;
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
function
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
|
301
|
-
reply_markup:
|
|
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
|
-
|
|
309
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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
|
-
|
|
97
|
-
|
|
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 {
|
package/src/runtime/paths.js
CHANGED
|
@@ -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
|
|
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");
|
package/test/ipc-server.test.js
CHANGED
|
@@ -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
|
}
|