arisa 3.0.14 → 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.
- package/AGENTS.md +11 -0
- package/README.md +37 -0
- package/docs/async-event-queue-flow.md +68 -0
- package/package.json +7 -8
- package/pnpm-workspace.yaml +12 -0
- package/src/core/agent/agent-manager.js +73 -25
- package/src/core/agent/runtime-context.js +2 -2
- package/src/core/artifacts/artifact-store.js +50 -25
- package/src/core/artifacts/normalize-for-reasoning.js +90 -0
- package/src/core/tasks/task-store.js +9 -5
- package/src/core/tools/daemon-runtime.js +167 -0
- package/src/core/tools/tool-config.js +15 -7
- package/src/core/tools/tool-registry.js +20 -9
- package/src/index.js +105 -12
- package/src/runtime/bootstrap.js +211 -19
- package/src/runtime/create-app.js +45 -3
- package/src/runtime/paths.js +26 -3
- package/src/runtime/service-manager.js +2 -2
- package/src/transport/telegram/bot.js +83 -38
- package/src/transport/telegram/media.js +17 -11
- package/tools/openai-transcribe/index.js +1 -1
package/src/runtime/bootstrap.js
CHANGED
|
@@ -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
|
-
|
|
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…</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 — 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…</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…" 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…</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 (
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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();
|
|
@@ -5,9 +5,51 @@ import { TaskStore } from "../core/tasks/task-store.js";
|
|
|
5
5
|
import { AgentManager } from "../core/agent/agent-manager.js";
|
|
6
6
|
import { createTelegramBot } from "../transport/telegram/bot.js";
|
|
7
7
|
|
|
8
|
-
|
|
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 } = {}) {
|
|
9
46
|
logger?.log("app", "loading config");
|
|
10
|
-
const
|
|
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
|
+
|
|
11
53
|
const artifactStore = new ArtifactStore();
|
|
12
54
|
const toolRegistry = new ToolRegistry({ logger });
|
|
13
55
|
const taskStore = new TaskStore();
|
|
@@ -15,7 +57,7 @@ export async function createApp({ logger } = {}) {
|
|
|
15
57
|
logger?.log("app", `loaded ${toolRegistry.list().length} tools`);
|
|
16
58
|
|
|
17
59
|
const agentManager = new AgentManager({ config, artifactStore, toolRegistry, taskStore, logger });
|
|
18
|
-
const bot = await createTelegramBot({ config, artifactStore, toolRegistry, taskStore, agentManager, saveConfig, updateConfig, logger });
|
|
60
|
+
const bot = await createTelegramBot({ config, artifactStore, toolRegistry, taskStore, agentManager, saveConfig, updateConfig, logger, webhookUrl, setHttpRequestHandler });
|
|
19
61
|
|
|
20
62
|
return {
|
|
21
63
|
async start() {
|
package/src/runtime/paths.js
CHANGED
|
@@ -7,10 +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");
|
|
12
10
|
export const tasksFile = path.join(stateDir, "tasks.json");
|
|
13
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
|
+
}
|
|
14
29
|
|
|
15
30
|
export function getToolDir(toolName) {
|
|
16
31
|
return path.join(toolsDir, toolName);
|
|
@@ -20,6 +35,10 @@ export function getToolConfigPath(toolName) {
|
|
|
20
35
|
return path.join(getToolDir(toolName), "config.js");
|
|
21
36
|
}
|
|
22
37
|
|
|
38
|
+
export function getChatToolConfigPath(chatId, toolName) {
|
|
39
|
+
return path.join(getChatDir(chatId), "tools", toolName, "config.js");
|
|
40
|
+
}
|
|
41
|
+
|
|
23
42
|
export function getToolRuntimeDir(toolName) {
|
|
24
43
|
return getToolDir(toolName);
|
|
25
44
|
}
|
|
@@ -32,9 +51,13 @@ export function getToolTmpDir(toolName) {
|
|
|
32
51
|
return path.join(getToolRuntimeDir(toolName), "tmp");
|
|
33
52
|
}
|
|
34
53
|
|
|
54
|
+
export function getChatToolTmpDir(chatId, toolName) {
|
|
55
|
+
return path.join(getChatDir(chatId), "tools", toolName, "tmp");
|
|
56
|
+
}
|
|
57
|
+
|
|
35
58
|
export async function ensureArisaHome() {
|
|
36
59
|
await mkdir(stateDir, { recursive: true });
|
|
37
|
-
await mkdir(artifactsDir, { recursive: true });
|
|
38
60
|
await mkdir(toolsDir, { recursive: true });
|
|
61
|
+
await mkdir(chatsDir, { recursive: true });
|
|
39
62
|
}
|
|
40
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, {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { Bot, InputFile } from "grammy";
|
|
1
|
+
import { Bot, InputFile, webhookCallback } from "grammy";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { authorizeChat } from "./auth.js";
|
|
4
4
|
import { captureIncomingArtifact } from "./media.js";
|
|
5
5
|
import { renderTelegramHtml } from "./text-format.js";
|
|
6
|
+
import { normalizeArtifactForReasoning } from "../../core/artifacts/normalize-for-reasoning.js";
|
|
6
7
|
|
|
7
8
|
function quotedMessageSummary(message) {
|
|
8
9
|
if (!message) return [];
|
|
@@ -39,16 +40,21 @@ function getTelegramCommand(ctx) {
|
|
|
39
40
|
return text.slice(1, entity.length).split("@")[0].trim().toLowerCase();
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
function getIncomingMessageText(message) {
|
|
44
|
+
return message?.text || message?.caption || "";
|
|
45
|
+
}
|
|
46
|
+
|
|
42
47
|
function buildPrompt({ ctx, artifact, transcript, toolResult }) {
|
|
43
48
|
const parts = [
|
|
44
|
-
`
|
|
49
|
+
`Incoming Telegram message.`,
|
|
45
50
|
`chatId: ${ctx.chat.id}`,
|
|
46
51
|
`userId: ${ctx.from.id}`,
|
|
47
52
|
`username: ${ctx.from.username || "(no username)"}`,
|
|
48
53
|
`messageId: ${ctx.msg.message_id}`
|
|
49
54
|
];
|
|
50
55
|
|
|
51
|
-
|
|
56
|
+
const messageText = getIncomingMessageText(ctx.message);
|
|
57
|
+
if (messageText) parts.push(`text: ${messageText}`);
|
|
52
58
|
parts.push(...quotedMessageSummary(ctx.message?.reply_to_message));
|
|
53
59
|
if (artifact?.path) parts.push(`artifactPath: ${artifact.path}`);
|
|
54
60
|
if (artifact?.id) parts.push(`artifactId: ${artifact.id}`);
|
|
@@ -79,43 +85,61 @@ function buildNewSessionPrompt(ctx) {
|
|
|
79
85
|
].join("\n");
|
|
80
86
|
}
|
|
81
87
|
|
|
82
|
-
function buildAsyncTaskPrompt(task) {
|
|
83
|
-
|
|
88
|
+
async function buildAsyncTaskPrompt({ task, artifactStore, toolRegistry, logger }) {
|
|
89
|
+
const parts = [
|
|
84
90
|
"Scheduled task fired.",
|
|
85
91
|
`taskId: ${task.id}`,
|
|
86
92
|
`chatId: ${task.payload.chatId}`,
|
|
87
|
-
task.payload.prompt ? `text: ${task.payload.prompt}` : null
|
|
88
|
-
|
|
89
|
-
"If needed, use tools."
|
|
90
|
-
].filter(Boolean).join("\n");
|
|
91
|
-
}
|
|
93
|
+
task.payload.prompt ? `text: ${task.payload.prompt}` : null
|
|
94
|
+
];
|
|
92
95
|
|
|
93
|
-
|
|
94
|
-
|
|
96
|
+
if (task.payload.artifactId) {
|
|
97
|
+
const chatArtifactStore = artifactStore.forChat(task.payload.chatId);
|
|
98
|
+
const artifact = await chatArtifactStore.get(task.payload.artifactId);
|
|
99
|
+
if (artifact) {
|
|
100
|
+
parts.push(`artifactPath: ${artifact.path || ""}`);
|
|
101
|
+
parts.push(`artifactId: ${artifact.id}`);
|
|
102
|
+
parts.push(`mimeType: ${artifact.mimeType}`);
|
|
103
|
+
parts.push(`kind: ${artifact.kind}`);
|
|
104
|
+
|
|
105
|
+
const { normalizedArtifact, toolResult } = await normalizeArtifactForReasoning({
|
|
106
|
+
artifact,
|
|
107
|
+
desiredMimeType: "text/plain",
|
|
108
|
+
toolRegistry,
|
|
109
|
+
chatArtifactStore,
|
|
110
|
+
chatId: task.payload.chatId
|
|
111
|
+
});
|
|
95
112
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
113
|
+
if (normalizedArtifact) {
|
|
114
|
+
logger?.log("tasks", `artifact ${artifact.id} normalized to ${normalizedArtifact.id}`);
|
|
115
|
+
parts.push(`transcriptArtifactId: ${normalizedArtifact.id}`);
|
|
116
|
+
parts.push(`transcriptText: ${normalizedArtifact.text}`);
|
|
117
|
+
parts.push("Important: the attached audio artifact has already been normalized for reasoning. Use the transcript as the message content.");
|
|
118
|
+
} else if (artifact.kind === "audio" && toolResult) {
|
|
119
|
+
parts.push(`audioNormalizationResult: ${JSON.stringify(toolResult)}`);
|
|
120
|
+
parts.push("Important: pre-reasoning audio normalization could not be completed, so you do not have a transcript for this audio artifact.");
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
parts.push(`artifactId: ${task.payload.artifactId}`);
|
|
124
|
+
parts.push("Important: referenced artifact was not found.");
|
|
101
125
|
}
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
if (!result.ok) {
|
|
105
|
-
return { transcript: null, toolResult: result };
|
|
106
126
|
}
|
|
107
127
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
128
|
+
parts.push("Treat this as a new request for the chat and fulfill it now.");
|
|
129
|
+
parts.push("If needed, use tools.");
|
|
130
|
+
return parts.filter(Boolean).join("\n");
|
|
131
|
+
}
|
|
111
132
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
133
|
+
async function normalizeIncomingArtifact({ artifact, toolRegistry, chatArtifactStore, chatId }) {
|
|
134
|
+
if (!artifact) return { transcript: null, toolResult: null };
|
|
135
|
+
const { normalizedArtifact, toolResult } = await normalizeArtifactForReasoning({
|
|
136
|
+
artifact,
|
|
137
|
+
desiredMimeType: "text/plain",
|
|
138
|
+
toolRegistry,
|
|
139
|
+
chatArtifactStore,
|
|
140
|
+
chatId
|
|
116
141
|
});
|
|
117
|
-
|
|
118
|
-
return { transcript, toolResult: result };
|
|
142
|
+
return { transcript: normalizedArtifact, toolResult };
|
|
119
143
|
}
|
|
120
144
|
|
|
121
145
|
async function collectText(session, prompt) {
|
|
@@ -143,8 +167,8 @@ async function withTyping(ctx, work) {
|
|
|
143
167
|
}
|
|
144
168
|
}
|
|
145
169
|
|
|
146
|
-
export async function createTelegramBot({ config, artifactStore, toolRegistry, taskStore, agentManager, saveConfig, updateConfig, logger }) {
|
|
147
|
-
const bot = new Bot(config.telegram.
|
|
170
|
+
export async function createTelegramBot({ config, artifactStore, toolRegistry, taskStore, agentManager, saveConfig, updateConfig, logger, webhookUrl, setHttpRequestHandler }) {
|
|
171
|
+
const bot = new Bot(config.telegram.token);
|
|
148
172
|
const perChatState = new Map();
|
|
149
173
|
|
|
150
174
|
function getIncomingChatMeta(ctx) {
|
|
@@ -164,10 +188,12 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
164
188
|
}
|
|
165
189
|
|
|
166
190
|
async function buildIncomingPrompt(ctx) {
|
|
167
|
-
|
|
191
|
+
const chatId = ctx.chat.id;
|
|
192
|
+
logger?.log("telegram", `message ${ctx.msg.message_id} in chat ${chatId}`);
|
|
193
|
+
const chatArtifactStore = artifactStore.forChat(chatId);
|
|
168
194
|
const artifact = await captureIncomingArtifact(ctx, artifactStore);
|
|
169
195
|
if (artifact) logger?.log("telegram", `captured artifact ${artifact.kind}${artifact.id ? ` ${artifact.id}` : ""}`);
|
|
170
|
-
const { transcript, toolResult } = await
|
|
196
|
+
const { transcript, toolResult } = await normalizeIncomingArtifact({ artifact, toolRegistry, chatArtifactStore, chatId });
|
|
171
197
|
if (transcript) logger?.log("telegram", `audio transcribed to artifact ${transcript.id}`);
|
|
172
198
|
if (artifact?.kind === "audio" && !transcript) {
|
|
173
199
|
logger?.log("telegram", `audio normalization unavailable for chat ${ctx.chat.id}: ${toolResult?.error || toolResult?.missingConfig?.join(", ") || "unknown error"}`);
|
|
@@ -180,7 +206,8 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
180
206
|
|
|
181
207
|
if (text.length > maxInlineReplyLength) {
|
|
182
208
|
logger?.log("telegram", `sending long reply as markdown attachment for chat ${chatId}`);
|
|
183
|
-
const
|
|
209
|
+
const chatArtifactStore = artifactStore.forChat(chatId);
|
|
210
|
+
const artifact = await chatArtifactStore.createGeneratedFile({
|
|
184
211
|
fileName: `reply-${Date.now()}.md`,
|
|
185
212
|
content: text,
|
|
186
213
|
kind: "document",
|
|
@@ -365,7 +392,7 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
365
392
|
logger?.log("tasks", `running task ${task.id} for chat ${task.payload.chatId}`);
|
|
366
393
|
await enqueuePrompt({
|
|
367
394
|
chatId: task.payload.chatId,
|
|
368
|
-
prompt: buildAsyncTaskPrompt(task),
|
|
395
|
+
prompt: await buildAsyncTaskPrompt({ task, artifactStore, toolRegistry, logger }),
|
|
369
396
|
label: `scheduled task ${task.id}`
|
|
370
397
|
});
|
|
371
398
|
await taskStore.complete(task.id);
|
|
@@ -374,8 +401,26 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
374
401
|
}
|
|
375
402
|
}
|
|
376
403
|
}, 1000).unref();
|
|
377
|
-
|
|
378
|
-
|
|
404
|
+
if (webhookUrl && setHttpRequestHandler) {
|
|
405
|
+
const webhookPath = `/telegram-${config.telegram.token.slice(-8)}`;
|
|
406
|
+
const handleUpdate = webhookCallback(bot, "http", {
|
|
407
|
+
timeoutMilliseconds: 60_000,
|
|
408
|
+
onTimeout: "return",
|
|
409
|
+
});
|
|
410
|
+
setHttpRequestHandler((req, res) => {
|
|
411
|
+
const parsed = new URL(req.url, "http://localhost");
|
|
412
|
+
if (req.method === "POST" && parsed.pathname === webhookPath) {
|
|
413
|
+
return handleUpdate(req, res);
|
|
414
|
+
}
|
|
415
|
+
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
416
|
+
res.end("ok");
|
|
417
|
+
});
|
|
418
|
+
await bot.api.setWebhook(`${webhookUrl}${webhookPath}`);
|
|
419
|
+
logger?.log("telegram", `webhook mode: ${webhookUrl}${webhookPath}`);
|
|
420
|
+
} else {
|
|
421
|
+
logger?.log("telegram", "bot polling started");
|
|
422
|
+
await bot.start({ drop_pending_updates: true });
|
|
423
|
+
}
|
|
379
424
|
}
|
|
380
425
|
};
|
|
381
426
|
}
|