arisa 3.0.12 → 3.1.2

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.
@@ -15,6 +15,79 @@ async function exists(file) {
15
15
  }
16
16
  }
17
17
 
18
+ function normalizeString(value) {
19
+ if (typeof value !== "string") return "";
20
+ return value.trim();
21
+ }
22
+
23
+ function parseMaxChatIds(value, fallback = 1) {
24
+ const parsed = Number(value);
25
+ if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
26
+ return parsed;
27
+ }
28
+
29
+ function buildConfig({ telegramApiKey, telegramMaxChatIds, provider, model, piApiKey }) {
30
+ return {
31
+ telegram: {
32
+ token: telegramApiKey,
33
+ maxChatIds: telegramMaxChatIds,
34
+ authorizedChatIds: [],
35
+ chatMeta: {}
36
+ },
37
+ pi: {
38
+ provider,
39
+ model,
40
+ apiKey: piApiKey
41
+ },
42
+ createdAt: new Date().toISOString()
43
+ };
44
+ }
45
+
46
+ function resolvePiDefaults(runtime, { provider: preferredProvider = "", model: preferredModel = "" } = {}) {
47
+ const providers = sortBootstrapProviders(listPiProviders(runtime));
48
+ if (!providers.length) {
49
+ throw new Error("No Pi providers are available for bootstrap.");
50
+ }
51
+
52
+ const preferredProviderValue = normalizeString(preferredProvider);
53
+ const providerExists = providers.some((item) => item.provider === preferredProviderValue);
54
+ if (preferredProviderValue && !providerExists) {
55
+ console.log(`Ignoring unknown Pi provider override: ${preferredProviderValue}`);
56
+ }
57
+
58
+ const selectedProvider = providerExists
59
+ ? preferredProviderValue
60
+ : providers[0].provider;
61
+
62
+ const models = sortBootstrapModels(selectedProvider, listProviderModels(selectedProvider, runtime));
63
+ if (!models.length) {
64
+ throw new Error(`No Pi models are available for provider ${selectedProvider}.`);
65
+ }
66
+
67
+ const preferredModelValue = normalizeString(preferredModel);
68
+ const modelExists = models.some((item) => item.id === preferredModelValue);
69
+ if (preferredModelValue && !modelExists) {
70
+ console.log(`Ignoring unknown Pi model override for ${selectedProvider}: ${preferredModelValue}`);
71
+ }
72
+
73
+ const selectedModel = modelExists ? preferredModelValue : models[0].id;
74
+ return { provider: selectedProvider, model: selectedModel };
75
+ }
76
+
77
+ function sortBootstrapProviders(providers) {
78
+ const preferredOrder = ["openai-codex"];
79
+ const positions = new Map(providers.map((provider, index) => [provider.provider, index]));
80
+
81
+ return [...providers].sort((a, b) => {
82
+ const aPref = preferredOrder.indexOf(a.provider);
83
+ const bPref = preferredOrder.indexOf(b.provider);
84
+ const aRank = aPref === -1 ? Number.MAX_SAFE_INTEGER : aPref;
85
+ const bRank = bPref === -1 ? Number.MAX_SAFE_INTEGER : bPref;
86
+ if (aRank !== bRank) return aRank - bRank;
87
+ return (positions.get(a.provider) || 0) - (positions.get(b.provider) || 0);
88
+ });
89
+ }
90
+
18
91
  function sortBootstrapModels(provider, models) {
19
92
  const preferred = {
20
93
  "openai-codex": ["gpt-5.4"]
@@ -49,7 +122,78 @@ async function maybeOpenExternal(url) {
49
122
  });
50
123
  }
51
124
 
52
- async function runInternalPiLogin(provider, rl) {
125
+ function installAuthRelay(httpPort, setHttpRequestHandler) {
126
+ let authUrl = "";
127
+ let resolveRedirectUrl;
128
+ const redirectUrlPromise = new Promise((resolve) => {
129
+ resolveRedirectUrl = resolve;
130
+ });
131
+
132
+ const page = (body) => [
133
+ "<!DOCTYPE html><html><head><meta charset='utf-8'><meta name='viewport' content='width=device-width'>",
134
+ "<title>Arisa Auth</title>",
135
+ "<style>body{font-family:system-ui,sans-serif;max-width:600px;margin:40px auto;padding:0 20px;line-height:1.6}",
136
+ "input[type=text]{width:100%;padding:8px;box-sizing:border-box;margin:8px 0}",
137
+ "button{padding:8px 24px;cursor:pointer}code{background:#f0f0f0;padding:2px 6px;border-radius:3px}</style>",
138
+ "</head><body>",
139
+ body,
140
+ "</body></html>"
141
+ ].join("");
142
+
143
+ setHttpRequestHandler((req, res) => {
144
+ const parsed = new URL(req.url, `http://localhost:${httpPort}`);
145
+
146
+ if (req.method === "GET" && parsed.pathname === "/auth/callback" && parsed.searchParams.has("code")) {
147
+ const callbackUrl = `http://localhost:1455${parsed.pathname}${parsed.search}`;
148
+ resolveRedirectUrl(callbackUrl);
149
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
150
+ res.end(page("<h2>Authentication received</h2><p>You can close this page. Arisa is starting&hellip;</p>"));
151
+ return;
152
+ }
153
+
154
+ if (req.method === "GET" && parsed.pathname === "/") {
155
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
156
+ res.end(page([
157
+ "<h2>Arisa &mdash; Pi Authentication</h2>",
158
+ authUrl
159
+ ? `<p><strong>1.</strong> <a href="${authUrl}" target="_blank">Click here to log in with Pi</a></p>`
160
+ : "<p>Waiting for authentication URL&hellip;</p>",
161
+ "<p><strong>2.</strong> After login your browser will redirect to a <code>localhost</code> URL that won't load. That's expected.</p>",
162
+ "<p><strong>3.</strong> In your browser's address bar, replace <code>localhost:1455</code> with your server's domain and press Enter.</p>",
163
+ "<hr>",
164
+ "<p><em>Or paste the full redirect URL here:</em></p>",
165
+ '<form method="POST" action="/auth/relay">',
166
+ '<input type="text" name="url" placeholder="Paste the localhost redirect URL here&hellip;" required />',
167
+ "<button type='submit'>Submit</button>",
168
+ "</form>"
169
+ ].join("\n")));
170
+ return;
171
+ }
172
+
173
+ if (req.method === "POST" && parsed.pathname === "/auth/relay") {
174
+ let body = "";
175
+ req.on("data", (chunk) => { body += chunk; });
176
+ req.on("end", () => {
177
+ const url = (new URLSearchParams(body).get("url") || "").trim();
178
+ if (url) resolveRedirectUrl(url);
179
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
180
+ res.end(page("<h2>Authentication received</h2><p>You can close this page. Arisa is starting&hellip;</p>"));
181
+ });
182
+ return;
183
+ }
184
+
185
+ res.writeHead(200, { "Content-Type": "text/plain" });
186
+ res.end("ok");
187
+ });
188
+
189
+ return {
190
+ setAuthUrl(url) { authUrl = url; },
191
+ waitForRedirectUrl() { return redirectUrlPromise; },
192
+ uninstall() { setHttpRequestHandler(null); }
193
+ };
194
+ }
195
+
196
+ async function runInternalPiLogin(provider, { rl = null, authRelay = null } = {}) {
53
197
  const authStorage = AuthStorage.create();
54
198
  const selected = authStorage.getOAuthProviders().find((item) => item.id === provider);
55
199
  if (!selected) {
@@ -67,7 +211,15 @@ async function runInternalPiLogin(provider, rl) {
67
211
  onAuth: async ({ url, instructions }) => {
68
212
  console.log(`${instructions || "Open this URL to continue authentication:"}\n${url}\n`);
69
213
  await maybeOpenExternal(url);
70
- if (selected.usesCallbackServer) {
214
+ if (authRelay) {
215
+ authRelay.setAuthUrl(url);
216
+ console.log("Waiting for authentication via the web relay...");
217
+ const redirectUrl = await authRelay.waitForRedirectUrl();
218
+ if (redirectUrl && manualCodeResolve) {
219
+ manualCodeResolve(redirectUrl);
220
+ manualCodeResolve = undefined;
221
+ }
222
+ } else if (selected.usesCallbackServer && rl) {
71
223
  const pasted = (await rl.question("Paste the redirect URL here if the browser does not return automatically, or press Enter to keep waiting: ")).trim();
72
224
  if (pasted && manualCodeResolve) {
73
225
  manualCodeResolve(pasted);
@@ -81,6 +233,9 @@ async function runInternalPiLogin(provider, rl) {
81
233
  await maybeOpenExternal(verificationUri);
82
234
  },
83
235
  onPrompt: async ({ message }) => {
236
+ if (!rl) {
237
+ throw new Error(`Pi login for ${provider} requires interactive input: ${message}`);
238
+ }
84
239
  return (await rl.question(`${message} `)).trim();
85
240
  },
86
241
  onProgress: (message) => {
@@ -96,10 +251,54 @@ async function runInternalPiLogin(provider, rl) {
96
251
  });
97
252
  }
98
253
 
99
- export async function bootstrapIfNeeded({ force = false } = {}) {
254
+ export async function bootstrapIfNeeded({ force = false, cliConfigOverrides = {}, httpPort = 0, setHttpRequestHandler } = {}) {
100
255
  await ensureArisaHome();
101
256
  if (!force && await exists(configFile)) return;
102
257
 
258
+ const telegramApiKeyFromCli = normalizeString(cliConfigOverrides?.telegram?.token);
259
+ if (telegramApiKeyFromCli) {
260
+ const runtime = createPiRuntime();
261
+ const resolvedPi = resolvePiDefaults(runtime, cliConfigOverrides?.pi || {});
262
+ const telegramMaxChatIds = parseMaxChatIds(cliConfigOverrides?.telegram?.maxChatIds, 1);
263
+ const piApiKey = normalizeString(cliConfigOverrides?.pi?.apiKey);
264
+ if (!piApiKey && !hasProviderAuth(resolvedPi.provider, runtime)) {
265
+ if (!supportsProviderOAuth(resolvedPi.provider, runtime)) {
266
+ throw new Error(
267
+ `No auth found for ${resolvedPi.provider}. Provide --pi.apiKey for non-interactive bootstrap, or use a provider that supports OAuth.`
268
+ );
269
+ }
270
+ if (!httpPort || !setHttpRequestHandler) {
271
+ throw new Error(
272
+ `No auth found for ${resolvedPi.provider}. Auth relay requires an HTTP server on PORT. Provide --pi.apiKey or set the PORT environment variable.`
273
+ );
274
+ }
275
+ const authRelay = installAuthRelay(httpPort, setHttpRequestHandler);
276
+ console.log(`No existing Pi auth found for ${resolvedPi.provider}. Auth relay active on port ${httpPort}.`);
277
+ console.log(`Open your server URL in a browser to complete Pi authentication.\n`);
278
+ try {
279
+ await runInternalPiLogin(resolvedPi.provider, { authRelay });
280
+ } finally {
281
+ authRelay.uninstall();
282
+ }
283
+ if (!hasProviderAuth(resolvedPi.provider, createPiRuntime())) {
284
+ throw new Error(
285
+ `Pi login did not complete for ${resolvedPi.provider}. Retry or provide --pi.apiKey.`
286
+ );
287
+ }
288
+ console.log(`Detected Pi auth for ${resolvedPi.provider}. Continuing bootstrap.`);
289
+ }
290
+ const config = buildConfig({
291
+ telegramApiKey: telegramApiKeyFromCli,
292
+ telegramMaxChatIds,
293
+ provider: resolvedPi.provider,
294
+ model: resolvedPi.model,
295
+ piApiKey
296
+ });
297
+ await writeFile(configFile, `${JSON.stringify(config, null, 2)}\n`, "utf8");
298
+ console.log(`\nConfig saved to ${configFile} (non-interactive bootstrap)\n`);
299
+ return;
300
+ }
301
+
103
302
  const rl = readline.createInterface({ input, output });
104
303
  const ask = async (label, fallback = "") => {
105
304
  const suffix = fallback ? ` (${fallback})` : "";
@@ -113,7 +312,7 @@ export async function bootstrapIfNeeded({ force = false } = {}) {
113
312
  const telegramMaxChatIds = Number(await ask("Maximum authorized chat IDs", "1"));
114
313
 
115
314
  const runtime = createPiRuntime();
116
- const providers = listPiProviders(runtime);
315
+ const providers = sortBootstrapProviders(listPiProviders(runtime));
117
316
  console.log("\nAvailable Pi providers:");
118
317
  providers.forEach((item, index) => {
119
318
  const authLabel = item.authConfigured ? "auth: configured" : item.supportsOAuth ? "auth: login or API key" : "auth: API key";
@@ -153,7 +352,7 @@ export async function bootstrapIfNeeded({ force = false } = {}) {
153
352
 
154
353
  console.log(`No existing Pi auth found for ${selectedProvider.provider}. Starting internal Pi login...`);
155
354
  try {
156
- await runInternalPiLogin(selectedProvider.provider, rl);
355
+ await runInternalPiLogin(selectedProvider.provider, { rl });
157
356
  } catch (error) {
158
357
  console.log(`Internal Pi login failed: ${error instanceof Error ? error.message : String(error)}`);
159
358
  }
@@ -166,20 +365,13 @@ export async function bootstrapIfNeeded({ force = false } = {}) {
166
365
  console.log(`Pi auth for ${selectedProvider.provider} is still missing after login.`);
167
366
  }
168
367
 
169
- const config = {
170
- telegram: {
171
- apiKey: telegramApiKey,
172
- maxChatIds: telegramMaxChatIds,
173
- authorizedChatIds: [],
174
- chatMeta: {}
175
- },
176
- pi: {
177
- provider: selectedProvider.provider,
178
- model: selectedModel.id,
179
- apiKey: piApiKey
180
- },
181
- createdAt: new Date().toISOString()
182
- };
368
+ const config = buildConfig({
369
+ telegramApiKey,
370
+ telegramMaxChatIds,
371
+ provider: selectedProvider.provider,
372
+ model: selectedModel.id,
373
+ piApiKey
374
+ });
183
375
 
184
376
  await writeFile(configFile, `${JSON.stringify(config, null, 2)}\n`, "utf8");
185
377
  rl.close();
@@ -1,19 +1,63 @@
1
1
  import { loadConfig, saveConfig, updateConfig } from "../core/config/config-store.js";
2
2
  import { ArtifactStore } from "../core/artifacts/artifact-store.js";
3
3
  import { ToolRegistry } from "../core/tools/tool-registry.js";
4
+ import { TaskStore } from "../core/tasks/task-store.js";
4
5
  import { AgentManager } from "../core/agent/agent-manager.js";
5
6
  import { createTelegramBot } from "../transport/telegram/bot.js";
6
7
 
7
- export async function createApp({ logger } = {}) {
8
+ function normalizeString(value) {
9
+ const text = String(value ?? "").trim();
10
+ return text ? text : "";
11
+ }
12
+
13
+ function splitModelOverride(modelOverride) {
14
+ const separatorIndex = modelOverride.indexOf("/");
15
+ if (separatorIndex <= 0 || separatorIndex === modelOverride.length - 1) {
16
+ return null;
17
+ }
18
+ return {
19
+ provider: modelOverride.slice(0, separatorIndex),
20
+ model: modelOverride.slice(separatorIndex + 1)
21
+ };
22
+ }
23
+
24
+ function applyRuntimeOverrides(config, runtimeOverrides) {
25
+ const providerOverride = normalizeString(runtimeOverrides?.pi?.provider);
26
+ const modelOverride = normalizeString(runtimeOverrides?.pi?.model);
27
+ if (!providerOverride && !modelOverride) return config;
28
+
29
+ const splitOverride = modelOverride ? splitModelOverride(modelOverride) : null;
30
+ const provider = providerOverride || splitOverride?.provider || config.pi.provider;
31
+ const model = splitOverride && (!providerOverride || providerOverride === splitOverride.provider)
32
+ ? splitOverride.model
33
+ : (modelOverride || config.pi.model);
34
+
35
+ return {
36
+ ...config,
37
+ pi: {
38
+ ...config.pi,
39
+ provider,
40
+ model
41
+ }
42
+ };
43
+ }
44
+
45
+ export async function createApp({ logger, runtimeOverrides, webhookUrl, setHttpRequestHandler } = {}) {
8
46
  logger?.log("app", "loading config");
9
- const config = await loadConfig();
47
+ const persistedConfig = await loadConfig();
48
+ const config = applyRuntimeOverrides(persistedConfig, runtimeOverrides);
49
+ if (config.pi.provider !== persistedConfig.pi.provider || config.pi.model !== persistedConfig.pi.model) {
50
+ logger?.log("app", `applying runtime model override: ${persistedConfig.pi.provider}/${persistedConfig.pi.model} -> ${config.pi.provider}/${config.pi.model}`);
51
+ }
52
+
10
53
  const artifactStore = new ArtifactStore();
11
54
  const toolRegistry = new ToolRegistry({ logger });
55
+ const taskStore = new TaskStore();
12
56
  await toolRegistry.load();
13
57
  logger?.log("app", `loaded ${toolRegistry.list().length} tools`);
14
58
 
15
- const agentManager = new AgentManager({ config, artifactStore, toolRegistry, logger });
16
- const bot = await createTelegramBot({ config, artifactStore, toolRegistry, agentManager, saveConfig, updateConfig, logger });
59
+ const agentManager = new AgentManager({ config, artifactStore, toolRegistry, taskStore, logger });
60
+ const bot = await createTelegramBot({ config, artifactStore, toolRegistry, taskStore, agentManager, saveConfig, updateConfig, logger, webhookUrl, setHttpRequestHandler });
17
61
 
18
62
  return {
19
63
  async start() {
@@ -7,9 +7,25 @@ export const stateDir = path.join(arisaHomeDir, "state");
7
7
  export const configFile = path.join(stateDir, "config.json");
8
8
  export const servicePidFile = path.join(stateDir, "arisa.pid");
9
9
  export const serviceLogFile = path.join(stateDir, "arisa.log");
10
- export const artifactsDir = path.join(arisaHomeDir, "artifacts");
11
- export const artifactsIndexFile = path.join(stateDir, "artifacts.json");
10
+ export const tasksFile = path.join(stateDir, "tasks.json");
12
11
  export const toolsDir = path.join(arisaHomeDir, "tools");
12
+ export const chatsDir = path.join(arisaHomeDir, "chats");
13
+
14
+ export function getChatDir(chatId) {
15
+ return path.join(chatsDir, String(chatId));
16
+ }
17
+
18
+ export function getChatArtifactsDir(chatId) {
19
+ return path.join(getChatDir(chatId), "artifacts");
20
+ }
21
+
22
+ export function getChatArtifactsIndexFile(chatId) {
23
+ return path.join(getChatDir(chatId), "state", "artifacts.json");
24
+ }
25
+
26
+ export function getChatPiSessionsDir(chatId) {
27
+ return path.join(getChatDir(chatId), "state", "pi-sessions");
28
+ }
13
29
 
14
30
  export function getToolDir(toolName) {
15
31
  return path.join(toolsDir, toolName);
@@ -19,6 +35,10 @@ export function getToolConfigPath(toolName) {
19
35
  return path.join(getToolDir(toolName), "config.js");
20
36
  }
21
37
 
38
+ export function getChatToolConfigPath(chatId, toolName) {
39
+ return path.join(getChatDir(chatId), "tools", toolName, "config.js");
40
+ }
41
+
22
42
  export function getToolRuntimeDir(toolName) {
23
43
  return getToolDir(toolName);
24
44
  }
@@ -31,9 +51,13 @@ export function getToolTmpDir(toolName) {
31
51
  return path.join(getToolRuntimeDir(toolName), "tmp");
32
52
  }
33
53
 
54
+ export function getChatToolTmpDir(chatId, toolName) {
55
+ return path.join(getChatDir(chatId), "tools", toolName, "tmp");
56
+ }
57
+
34
58
  export async function ensureArisaHome() {
35
59
  await mkdir(stateDir, { recursive: true });
36
- await mkdir(artifactsDir, { recursive: true });
37
60
  await mkdir(toolsDir, { recursive: true });
61
+ await mkdir(chatsDir, { recursive: true });
38
62
  }
39
63
 
@@ -36,7 +36,7 @@ export async function getServiceStatus() {
36
36
  return { running: true, pid };
37
37
  }
38
38
 
39
- export async function startService({ verbose = false } = {}) {
39
+ export async function startService({ verbose = false, cliArgs = [] } = {}) {
40
40
  await ensureArisaHome();
41
41
  const status = await getServiceStatus();
42
42
  if (status.running) {
@@ -44,7 +44,7 @@ export async function startService({ verbose = false } = {}) {
44
44
  }
45
45
 
46
46
  const logHandle = await open(serviceLogFile, "a");
47
- const args = [entryFile, "--service-runner"];
47
+ const args = [entryFile, "--service-runner", ...cliArgs];
48
48
  if (verbose) args.push("--verbose");
49
49
 
50
50
  const child = spawn(process.execPath, args, {