arisa 4.0.24 → 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/README.md +6 -4
- package/package.json +1 -1
- package/src/core/agent/agent-manager.js +69 -12
- package/src/core/agent/core-tools.js +139 -0
- package/src/core/agent/runtime-context.js +14 -3
- package/src/core/agent/system-shell-tool.js +207 -0
- package/src/index.js +44 -10
- package/src/runtime/bootstrap.js +452 -36
- package/src/runtime/create-app.js +52 -9
- package/src/runtime/ipc/ipc-server.js +21 -8
- package/src/runtime/paths.js +10 -1
- package/src/transport/telegram/bot.js +4 -3
- package/test/agent-tool-policy.test.js +91 -0
- package/test/ipc-server.test.js +10 -0
package/src/runtime/bootstrap.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
1
2
|
import { readFile, writeFile } from "node:fs/promises";
|
|
2
3
|
import readline from "node:readline/promises";
|
|
3
4
|
import { stdin as input, stdout as output } from "node:process";
|
|
4
5
|
import { spawn } from "node:child_process";
|
|
6
|
+
import { Bot } from "grammy";
|
|
5
7
|
import { createPiOAuthLogin } from "../core/agent/pi-auth-login.js";
|
|
6
8
|
import { createPiRuntime, hasProviderAuth, listPiProviders, listProviderModels, supportsProviderOAuth } from "../core/agent/pi-runtime.js";
|
|
7
9
|
import { configFile, ensureArisaHome } from "./paths.js";
|
|
@@ -24,13 +26,13 @@ async function exists(file) {
|
|
|
24
26
|
}
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
function buildConfig({ telegramApiKey, telegramMaxChatIds, provider, model, piApiKey }) {
|
|
29
|
+
function buildConfig({ telegramApiKey, telegramMaxChatIds, authorizedChatIds = [], chatMeta = {}, provider, model, piApiKey }) {
|
|
28
30
|
return {
|
|
29
31
|
telegram: {
|
|
30
32
|
token: telegramApiKey,
|
|
31
33
|
maxChatIds: telegramMaxChatIds,
|
|
32
|
-
authorizedChatIds
|
|
33
|
-
chatMeta
|
|
34
|
+
authorizedChatIds,
|
|
35
|
+
chatMeta
|
|
34
36
|
},
|
|
35
37
|
pi: {
|
|
36
38
|
provider,
|
|
@@ -73,6 +75,79 @@ function sortBootstrapModels(provider, models) {
|
|
|
73
75
|
});
|
|
74
76
|
}
|
|
75
77
|
|
|
78
|
+
function createSetupToken() {
|
|
79
|
+
return crypto.randomBytes(18).toString("base64url");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function parsePositiveInteger(value, fallback = null) {
|
|
83
|
+
const number = Number(value);
|
|
84
|
+
if (!Number.isFinite(number) || number <= 0) return fallback;
|
|
85
|
+
return Math.floor(number);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function selectByIndex(items, value, fallbackIndex = 0) {
|
|
89
|
+
const index = parsePositiveInteger(value, fallbackIndex + 1) - 1;
|
|
90
|
+
return items[Math.max(0, Math.min(items.length - 1, index))];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function parseYesNo(value, fallback = true) {
|
|
94
|
+
const text = String(value ?? "").trim().toLowerCase();
|
|
95
|
+
if (!text) return fallback;
|
|
96
|
+
if (["y", "yes", "s", "si", "sí"].includes(text)) return true;
|
|
97
|
+
if (["n", "no"].includes(text)) return false;
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
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 };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function getIncomingChatMeta(ctx) {
|
|
126
|
+
return {
|
|
127
|
+
languageCode: ctx.from?.language_code || "",
|
|
128
|
+
username: ctx.from?.username || "",
|
|
129
|
+
firstName: ctx.from?.first_name || "",
|
|
130
|
+
lastName: ctx.from?.last_name || ""
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function formatProviderOption(item) {
|
|
135
|
+
const authLabel = item.authConfigured ? "auth configured" : item.supportsOAuth ? "login or API key" : "API key";
|
|
136
|
+
return `${item.provider} (${item.modelCount} models, ${authLabel})`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function formatModelOption(model) {
|
|
140
|
+
const capabilities = [model.reasoning ? "reasoning" : null, model.input?.includes("image") ? "image" : null].filter(Boolean).join(", ");
|
|
141
|
+
return capabilities ? `${model.id} [${capabilities}]` : model.id;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function selectPiLoginOption(options = []) {
|
|
145
|
+
return options.find((option) => /device/i.test(`${option.id} ${option.label}`))
|
|
146
|
+
|| options.find((option) => /browser|oauth|web/i.test(`${option.id} ${option.label}`))
|
|
147
|
+
|| options[0]
|
|
148
|
+
|| null;
|
|
149
|
+
}
|
|
150
|
+
|
|
76
151
|
async function maybeOpenExternal(url) {
|
|
77
152
|
if (!url) return;
|
|
78
153
|
await new Promise((resolve) => {
|
|
@@ -92,6 +167,12 @@ async function maybeOpenExternal(url) {
|
|
|
92
167
|
async function runInternalPiLogin(provider, { rl = null } = {}) {
|
|
93
168
|
const login = createPiOAuthLogin({
|
|
94
169
|
provider,
|
|
170
|
+
onSelect: async ({ message, options }) => {
|
|
171
|
+
const selected = selectPiLoginOption(options);
|
|
172
|
+
if (!selected) return undefined;
|
|
173
|
+
console.log(`${message}\nUsing: ${selected.label || selected.id}\n`);
|
|
174
|
+
return selected.id;
|
|
175
|
+
},
|
|
95
176
|
onAuth: async ({ url, instructions, controller }) => {
|
|
96
177
|
console.log(`${instructions || "Open this URL to continue authentication:"}\n${url}\n`);
|
|
97
178
|
await maybeOpenExternal(url);
|
|
@@ -118,43 +199,24 @@ async function runInternalPiLogin(provider, { rl = null } = {}) {
|
|
|
118
199
|
await login.promise;
|
|
119
200
|
}
|
|
120
201
|
|
|
121
|
-
|
|
122
|
-
await ensureArisaHome();
|
|
123
|
-
if (!force && await exists(configFile)) return;
|
|
124
|
-
|
|
125
|
-
const rl = readline.createInterface({ input, output });
|
|
126
|
-
const ask = async (label, fallback = "") => {
|
|
127
|
-
const suffix = fallback ? ` (${fallback})` : "";
|
|
128
|
-
const value = (await rl.question(`${label}${suffix}: `)).trim();
|
|
129
|
-
return value || fallback;
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
console.log(`\n${ARISA_BANNER}`);
|
|
133
|
-
console.log("-------- https://arisa.sh --------\n");
|
|
134
|
-
console.log("Get Telegram bot token from https://t.me/BotFather");
|
|
135
|
-
const telegramApiKey = await ask("Telegram bot token");
|
|
202
|
+
async function collectCliBootstrapChoices({ telegramApiKey, rl, ask }) {
|
|
136
203
|
const telegramMaxChatIds = Number(await ask("Maximum authorized chat IDs", "1"));
|
|
137
204
|
|
|
138
205
|
const runtime = createPiRuntime();
|
|
139
206
|
const providers = sortBootstrapProviders(listPiProviders(runtime));
|
|
140
207
|
console.log("\nAvailable Pi providers:");
|
|
141
208
|
providers.forEach((item, index) => {
|
|
142
|
-
|
|
143
|
-
console.log(`${index + 1}. ${item.provider} (${item.modelCount} models, ${authLabel})`);
|
|
209
|
+
console.log(`${index + 1}. ${formatProviderOption(item)}`);
|
|
144
210
|
});
|
|
145
211
|
|
|
146
|
-
const
|
|
147
|
-
const selectedProvider = providers[Math.max(0, Math.min(providers.length - 1, selectedProviderIndex - 1))];
|
|
212
|
+
const selectedProvider = selectByIndex(providers, await ask("Select Pi provider by number", "1"));
|
|
148
213
|
const models = sortBootstrapModels(selectedProvider.provider, listProviderModels(selectedProvider.provider, runtime));
|
|
149
214
|
console.log(`\nAvailable models for ${selectedProvider.provider}:`);
|
|
150
215
|
models.forEach((model, index) => {
|
|
151
|
-
|
|
152
|
-
const suffix = capabilities ? ` [${capabilities}]` : "";
|
|
153
|
-
console.log(`${index + 1}. ${model.id}${suffix}`);
|
|
216
|
+
console.log(`${index + 1}. ${formatModelOption(model)}`);
|
|
154
217
|
});
|
|
155
218
|
|
|
156
|
-
const
|
|
157
|
-
const selectedModel = models[Math.max(0, Math.min(models.length - 1, selectedModelIndex - 1))];
|
|
219
|
+
const selectedModel = selectByIndex(models, await ask("Select Pi model by number", "1"));
|
|
158
220
|
const selectedAuthReady = hasProviderAuth(selectedProvider.provider, runtime);
|
|
159
221
|
const providerSupportsOAuth = supportsProviderOAuth(selectedProvider.provider, runtime);
|
|
160
222
|
console.log(`Selected model: ${selectedModel.provider}/${selectedModel.id}`);
|
|
@@ -189,17 +251,371 @@ export async function bootstrapIfNeeded({ force = false } = {}) {
|
|
|
189
251
|
console.log(`Pi auth for ${selectedProvider.provider} is still missing after login.`);
|
|
190
252
|
}
|
|
191
253
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
254
|
+
return {
|
|
255
|
+
config: buildConfig({
|
|
256
|
+
telegramApiKey,
|
|
257
|
+
telegramMaxChatIds,
|
|
258
|
+
provider: selectedProvider.provider,
|
|
259
|
+
model: selectedModel.id,
|
|
260
|
+
piApiKey
|
|
261
|
+
}),
|
|
262
|
+
startInBackground: false,
|
|
263
|
+
viaTelegram: false
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function runTelegramBootstrap({ telegramApiKey, setupToken, botInfo }) {
|
|
268
|
+
const bot = new Bot(telegramApiKey);
|
|
269
|
+
const runtime = createPiRuntime();
|
|
270
|
+
const providers = sortBootstrapProviders(listPiProviders(runtime));
|
|
271
|
+
let setupChatId = null;
|
|
272
|
+
let chatMeta = {};
|
|
273
|
+
let state = "await-start";
|
|
274
|
+
let telegramMaxChatIds = 1;
|
|
275
|
+
let selectedProvider = null;
|
|
276
|
+
let selectedModel = null;
|
|
277
|
+
let piApiKey = "";
|
|
278
|
+
let activeLogin = null;
|
|
279
|
+
let completed = false;
|
|
280
|
+
let resolveResult;
|
|
281
|
+
let rejectResult;
|
|
282
|
+
|
|
283
|
+
const resultPromise = new Promise((resolve, reject) => {
|
|
284
|
+
resolveResult = resolve;
|
|
285
|
+
rejectResult = reject;
|
|
198
286
|
});
|
|
199
287
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
288
|
+
const sendSetupMessage = async (text, extra = {}) => {
|
|
289
|
+
if (!setupChatId) return;
|
|
290
|
+
await bot.api.sendMessage(setupChatId, text, extra);
|
|
291
|
+
};
|
|
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
|
+
|
|
304
|
+
const isSetupChat = (ctx) => setupChatId && ctx.chat?.id === setupChatId;
|
|
305
|
+
|
|
306
|
+
const complete = (startInBackground) => {
|
|
307
|
+
if (completed) return;
|
|
308
|
+
completed = true;
|
|
309
|
+
resolveResult({
|
|
310
|
+
config: buildConfig({
|
|
311
|
+
telegramApiKey,
|
|
312
|
+
telegramMaxChatIds,
|
|
313
|
+
authorizedChatIds: [setupChatId],
|
|
314
|
+
chatMeta: { [setupChatId]: chatMeta },
|
|
315
|
+
provider: selectedProvider.provider,
|
|
316
|
+
model: selectedModel.id,
|
|
317
|
+
piApiKey
|
|
318
|
+
}),
|
|
319
|
+
startInBackground,
|
|
320
|
+
viaTelegram: true
|
|
321
|
+
});
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const askProvider = async (ctx = null, page = 0) => {
|
|
325
|
+
state = "provider";
|
|
326
|
+
await showSetupPrompt(ctx, "Select the Pi provider Arisa should use:", {
|
|
327
|
+
reply_markup: buildPagedInlineKeyboard("provider", providers.map((provider) => ({ text: formatProviderOption(provider) })), { page })
|
|
328
|
+
});
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const askModel = async (ctx = null, page = 0) => {
|
|
332
|
+
state = "model";
|
|
333
|
+
const models = sortBootstrapModels(selectedProvider.provider, listProviderModels(selectedProvider.provider, createPiRuntime()));
|
|
334
|
+
await showSetupPrompt(ctx, `Select the model for ${selectedProvider.provider}:`, {
|
|
335
|
+
reply_markup: buildPagedInlineKeyboard("model", models.map((model) => ({ text: formatModelOption(model) })), { page })
|
|
336
|
+
});
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
const askBackground = async (ctx = null) => {
|
|
340
|
+
state = "background";
|
|
341
|
+
await showSetupPrompt(ctx, "Bootstrap complete. Keep Arisa running in background now?", {
|
|
342
|
+
reply_markup: {
|
|
343
|
+
inline_keyboard: [
|
|
344
|
+
[{ text: "Yes, start in background", callback_data: "background:yes" }],
|
|
345
|
+
[{ text: "No, continue in foreground", callback_data: "background:no" }]
|
|
346
|
+
]
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
const askApiKey = async (ctx = null) => {
|
|
352
|
+
state = "pi-api-key";
|
|
353
|
+
await showSetupPrompt(ctx, `Send the Pi API key for ${selectedProvider.provider}.`, {
|
|
354
|
+
reply_markup: { inline_keyboard: [] }
|
|
355
|
+
});
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const askAuthMethod = async (ctx = null) => {
|
|
359
|
+
const providerRuntime = createPiRuntime();
|
|
360
|
+
const selectedAuthReady = hasProviderAuth(selectedProvider.provider, providerRuntime);
|
|
361
|
+
const providerSupportsOAuth = supportsProviderOAuth(selectedProvider.provider, providerRuntime);
|
|
362
|
+
const buttons = [];
|
|
363
|
+
|
|
364
|
+
if (selectedAuthReady) {
|
|
365
|
+
buttons.push([{ text: "Use existing Pi auth", callback_data: "auth:existing" }]);
|
|
366
|
+
}
|
|
367
|
+
if (providerSupportsOAuth) {
|
|
368
|
+
buttons.push([{ text: selectedAuthReady ? "Run Pi login again" : "Start Pi login", callback_data: "auth:login" }]);
|
|
369
|
+
}
|
|
370
|
+
buttons.push([{ text: "Enter API key", callback_data: "auth:key" }]);
|
|
371
|
+
|
|
372
|
+
state = "auth-method";
|
|
373
|
+
await showSetupPrompt(ctx, [
|
|
374
|
+
`Selected model: ${selectedProvider.provider}/${selectedModel.id}`,
|
|
375
|
+
`Existing Pi auth for ${selectedProvider.provider}: ${selectedAuthReady ? "yes" : "no"}`,
|
|
376
|
+
"Choose how Arisa should authenticate Pi."
|
|
377
|
+
].join("\n"), {
|
|
378
|
+
reply_markup: { inline_keyboard: buttons }
|
|
379
|
+
});
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
const finishPiLogin = async (login) => {
|
|
383
|
+
try {
|
|
384
|
+
await login.promise;
|
|
385
|
+
activeLogin = null;
|
|
386
|
+
if (hasProviderAuth(selectedProvider.provider, createPiRuntime())) {
|
|
387
|
+
await sendSetupMessage(`Detected Pi auth for ${selectedProvider.provider}.`);
|
|
388
|
+
await askBackground();
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
await sendSetupMessage(`Pi auth for ${selectedProvider.provider} is still missing after login.`);
|
|
392
|
+
await askAuthMethod();
|
|
393
|
+
} catch (error) {
|
|
394
|
+
activeLogin = null;
|
|
395
|
+
await sendSetupMessage(`Pi login failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
396
|
+
await askAuthMethod();
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
const startPiLogin = async () => {
|
|
401
|
+
if (hasProviderAuth(selectedProvider.provider, createPiRuntime())) {
|
|
402
|
+
await sendSetupMessage(`Existing Pi auth for ${selectedProvider.provider} detected.`);
|
|
403
|
+
await askBackground();
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
state = "pi-login";
|
|
408
|
+
const login = createPiOAuthLogin({
|
|
409
|
+
provider: selectedProvider.provider,
|
|
410
|
+
onSelect: async ({ message, options }) => {
|
|
411
|
+
const selected = selectPiLoginOption(options);
|
|
412
|
+
if (!selected) return undefined;
|
|
413
|
+
await sendSetupMessage(`${message}\nUsing: ${selected.label || selected.id}`);
|
|
414
|
+
return selected.id;
|
|
415
|
+
},
|
|
416
|
+
onAuth: async ({ url, instructions }) => {
|
|
417
|
+
await sendSetupMessage([
|
|
418
|
+
instructions || "Open this URL to continue Pi authentication:",
|
|
419
|
+
url,
|
|
420
|
+
"After login, paste the full redirect URL back here."
|
|
421
|
+
].join("\n"));
|
|
422
|
+
},
|
|
423
|
+
onDeviceCode: async ({ userCode, verificationUri, expiresInSeconds }) => {
|
|
424
|
+
const expiry = expiresInSeconds ? `\nExpires in ${Math.round(expiresInSeconds / 60)} minute(s).` : "";
|
|
425
|
+
await sendSetupMessage(`Open this URL: ${verificationUri}\nThen enter code: ${userCode}${expiry}`);
|
|
426
|
+
},
|
|
427
|
+
onPrompt: async ({ message, controller }) => {
|
|
428
|
+
await sendSetupMessage(`${message}\nReply here with the value.`);
|
|
429
|
+
return controller.waitForManualCode();
|
|
430
|
+
},
|
|
431
|
+
onProgress: (message) => {
|
|
432
|
+
if (message) console.log(`[bootstrap] Pi auth progress: ${message}`);
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
activeLogin = login;
|
|
437
|
+
finishPiLogin(login);
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
bot.catch((error) => {
|
|
441
|
+
console.error("Telegram setup bot error:", error);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
bot.command("start", async (ctx) => {
|
|
445
|
+
if (String(ctx.match || "").trim() !== setupToken) {
|
|
446
|
+
await ctx.reply("Invalid setup link. Use the link shown in the Arisa bootstrap terminal.");
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (setupChatId && ctx.chat.id !== setupChatId) {
|
|
451
|
+
await ctx.reply("This setup session is already connected to another chat.");
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
setupChatId = ctx.chat.id;
|
|
456
|
+
chatMeta = getIncomingChatMeta(ctx);
|
|
457
|
+
await ctx.reply([
|
|
458
|
+
`Connected to @${botInfo.username}.`,
|
|
459
|
+
"This chat will be the only Telegram chat authorized during setup."
|
|
460
|
+
].join("\n"));
|
|
461
|
+
await askProvider();
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
bot.on("callback_query:data", async (ctx) => {
|
|
465
|
+
await ctx.answerCallbackQuery().catch(() => {});
|
|
466
|
+
if (!isSetupChat(ctx)) return;
|
|
467
|
+
|
|
468
|
+
const data = String(ctx.callbackQuery.data || "");
|
|
469
|
+
const [action, rawValue] = data.split(":");
|
|
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
|
+
|
|
483
|
+
if (action === "provider" && state === "provider") {
|
|
484
|
+
selectedProvider = providers[Number(rawValue)];
|
|
485
|
+
if (!selectedProvider) return;
|
|
486
|
+
await askModel(ctx);
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (action === "model" && state === "model") {
|
|
491
|
+
const models = sortBootstrapModels(selectedProvider.provider, listProviderModels(selectedProvider.provider, createPiRuntime()));
|
|
492
|
+
selectedModel = models[Number(rawValue)];
|
|
493
|
+
if (!selectedModel) return;
|
|
494
|
+
await askAuthMethod(ctx);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (action === "auth" && state === "auth-method") {
|
|
499
|
+
if (rawValue === "existing") {
|
|
500
|
+
if (hasProviderAuth(selectedProvider.provider, createPiRuntime())) {
|
|
501
|
+
await askBackground(ctx);
|
|
502
|
+
} else {
|
|
503
|
+
await sendSetupMessage(`No existing Pi auth found for ${selectedProvider.provider}.`);
|
|
504
|
+
await askAuthMethod(ctx);
|
|
505
|
+
}
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
if (rawValue === "login") {
|
|
509
|
+
await startPiLogin();
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
if (rawValue === "key") {
|
|
513
|
+
await askApiKey(ctx);
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (action === "background" && state === "background") {
|
|
519
|
+
await sendSetupMessage(rawValue === "yes"
|
|
520
|
+
? "Saving config. Arisa will start in background now."
|
|
521
|
+
: "Saving config. Arisa will continue in foreground.");
|
|
522
|
+
complete(rawValue === "yes");
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
bot.on("message:text", async (ctx) => {
|
|
527
|
+
if (!isSetupChat(ctx)) return;
|
|
528
|
+
const text = String(ctx.message.text || "").trim();
|
|
529
|
+
if (!text || text.startsWith("/")) return;
|
|
530
|
+
|
|
531
|
+
if (state === "pi-api-key") {
|
|
532
|
+
piApiKey = text;
|
|
533
|
+
await askBackground();
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (state === "pi-login" && activeLogin?.manualInputRequested) {
|
|
538
|
+
if (activeLogin.submitManualCode(text)) {
|
|
539
|
+
await ctx.reply("Got it. Finishing Pi login now...");
|
|
540
|
+
}
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (state === "background") {
|
|
545
|
+
const answer = parseYesNo(text, true);
|
|
546
|
+
if (answer === null) {
|
|
547
|
+
await ctx.reply("Please answer yes or no.");
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
await ctx.reply(answer
|
|
551
|
+
? "Saving config. Arisa will start in background now."
|
|
552
|
+
: "Saving config. Arisa will continue in foreground.");
|
|
553
|
+
complete(answer);
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
await bot.api.deleteWebhook({ drop_pending_updates: true });
|
|
558
|
+
console.log("Waiting for Telegram setup to complete...");
|
|
559
|
+
const polling = bot.start().then(() => {
|
|
560
|
+
if (!completed) throw new Error("Telegram setup bot stopped before bootstrap completed.");
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
try {
|
|
564
|
+
return await Promise.race([resultPromise, polling]);
|
|
565
|
+
} catch (error) {
|
|
566
|
+
rejectResult(error);
|
|
567
|
+
throw error;
|
|
568
|
+
} finally {
|
|
569
|
+
bot.stop();
|
|
570
|
+
await polling.catch(() => {});
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
export async function bootstrapIfNeeded({ force = false } = {}) {
|
|
575
|
+
await ensureArisaHome();
|
|
576
|
+
if (!force && await exists(configFile)) {
|
|
577
|
+
return { configCreated: false, viaTelegram: false, startInBackground: false };
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const rl = readline.createInterface({ input, output });
|
|
581
|
+
const ask = async (label, fallback = "") => {
|
|
582
|
+
const suffix = fallback ? ` (${fallback})` : "";
|
|
583
|
+
const value = (await rl.question(`${label}${suffix}: `)).trim();
|
|
584
|
+
return value || fallback;
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
console.log(`\n${ARISA_BANNER}`);
|
|
588
|
+
console.log("-------- https://arisa.sh --------\n");
|
|
589
|
+
console.log("Get Telegram bot token from https://t.me/BotFather");
|
|
590
|
+
const telegramApiKey = await ask("Telegram bot token");
|
|
591
|
+
|
|
592
|
+
try {
|
|
593
|
+
const setupProbeBot = new Bot(telegramApiKey);
|
|
594
|
+
const botInfo = await setupProbeBot.api.getMe();
|
|
595
|
+
const answer = parseYesNo(await ask("Continue bootstrap from Telegram?", "Y"), true);
|
|
596
|
+
const continueFromTelegram = answer === null ? true : answer;
|
|
597
|
+
|
|
598
|
+
let result;
|
|
599
|
+
if (continueFromTelegram) {
|
|
600
|
+
const setupToken = createSetupToken();
|
|
601
|
+
const setupLink = `https://t.me/${botInfo.username}?start=${setupToken}`;
|
|
602
|
+
console.log(`\nOpen this link to continue setup in Telegram:\n${setupLink}\n`);
|
|
603
|
+
await maybeOpenExternal(setupLink);
|
|
604
|
+
result = await runTelegramBootstrap({ telegramApiKey, setupToken, botInfo });
|
|
605
|
+
} else {
|
|
606
|
+
result = await collectCliBootstrapChoices({ telegramApiKey, rl, ask });
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
await writeFile(configFile, `${JSON.stringify(result.config, null, 2)}\n`, "utf8");
|
|
610
|
+
console.log(`\nConfig saved to ${configFile}\n`);
|
|
611
|
+
return {
|
|
612
|
+
configCreated: true,
|
|
613
|
+
viaTelegram: result.viaTelegram,
|
|
614
|
+
startInBackground: result.startInBackground
|
|
615
|
+
};
|
|
616
|
+
} finally {
|
|
617
|
+
rl.close();
|
|
618
|
+
}
|
|
203
619
|
}
|
|
204
620
|
|
|
205
621
|
export { configFile };
|
|
@@ -14,6 +14,31 @@ function normalizeString(value) {
|
|
|
14
14
|
return text ? text : "";
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
function normalizeStringList(value) {
|
|
18
|
+
if (Array.isArray(value)) {
|
|
19
|
+
const items = value
|
|
20
|
+
.map((item) => normalizeString(item))
|
|
21
|
+
.filter(Boolean);
|
|
22
|
+
return items.length ? [...new Set(items)] : undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (typeof value === "string") {
|
|
26
|
+
const items = value
|
|
27
|
+
.split(",")
|
|
28
|
+
.map((item) => item.trim())
|
|
29
|
+
.filter(Boolean);
|
|
30
|
+
return items.length ? [...new Set(items)] : undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function normalizePositiveInteger(value) {
|
|
37
|
+
const number = Number(value);
|
|
38
|
+
if (!Number.isFinite(number) || number <= 0) return undefined;
|
|
39
|
+
return Math.floor(number);
|
|
40
|
+
}
|
|
41
|
+
|
|
17
42
|
function splitModelOverride(modelOverride) {
|
|
18
43
|
const separatorIndex = modelOverride.indexOf("/");
|
|
19
44
|
if (separatorIndex <= 0 || separatorIndex === modelOverride.length - 1) {
|
|
@@ -25,23 +50,41 @@ function splitModelOverride(modelOverride) {
|
|
|
25
50
|
};
|
|
26
51
|
}
|
|
27
52
|
|
|
28
|
-
function applyRuntimeOverrides(config, runtimeOverrides) {
|
|
53
|
+
export function applyRuntimeOverrides(config, runtimeOverrides) {
|
|
54
|
+
const piRuntimeOverrides = runtimeOverrides?.pi || {};
|
|
55
|
+
const pi = {};
|
|
29
56
|
const providerOverride = normalizeString(runtimeOverrides?.pi?.provider);
|
|
30
57
|
const modelOverride = normalizeString(runtimeOverrides?.pi?.model);
|
|
31
|
-
if (!providerOverride && !modelOverride) return config;
|
|
32
58
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
59
|
+
if (providerOverride || modelOverride) {
|
|
60
|
+
const splitOverride = modelOverride ? splitModelOverride(modelOverride) : null;
|
|
61
|
+
pi.provider = providerOverride || splitOverride?.provider || config.pi.provider;
|
|
62
|
+
pi.model = splitOverride && (!providerOverride || providerOverride === splitOverride.provider)
|
|
63
|
+
? splitOverride.model
|
|
64
|
+
: (modelOverride || config.pi.model);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (const key of ["apiKey", "workspaceDir", "shellPath"]) {
|
|
68
|
+
const value = normalizeString(piRuntimeOverrides[key]);
|
|
69
|
+
if (value) pi[key] = value;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const tools = normalizeStringList(piRuntimeOverrides.tools);
|
|
73
|
+
if (tools) pi.tools = tools;
|
|
74
|
+
|
|
75
|
+
const excludeTools = normalizeStringList(piRuntimeOverrides.excludeTools);
|
|
76
|
+
if (excludeTools) pi.excludeTools = excludeTools;
|
|
77
|
+
|
|
78
|
+
const shellTimeoutMs = normalizePositiveInteger(piRuntimeOverrides.shellTimeoutMs);
|
|
79
|
+
if (shellTimeoutMs) pi.shellTimeoutMs = shellTimeoutMs;
|
|
80
|
+
|
|
81
|
+
if (!Object.keys(pi).length) return config;
|
|
38
82
|
|
|
39
83
|
return {
|
|
40
84
|
...config,
|
|
41
85
|
pi: {
|
|
42
86
|
...config.pi,
|
|
43
|
-
|
|
44
|
-
model
|
|
87
|
+
...pi
|
|
45
88
|
}
|
|
46
89
|
};
|
|
47
90
|
}
|
|
@@ -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 {
|