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 +1 -1
- package/src/runtime/bootstrap.js +64 -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,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
|
|
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
|
-
await
|
|
309
|
-
reply_markup:
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
}
|