arisa 3.0.14 → 3.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/AGENTS.md +67 -11
- package/README.md +38 -1
- package/package.json +7 -8
- package/pnpm-workspace.yaml +12 -0
- package/src/core/agent/agent-manager.js +127 -60
- package/src/core/agent/runtime-context.js +3 -2
- package/src/core/artifacts/artifact-store.js +50 -25
- package/src/core/artifacts/normalize-for-reasoning.js +91 -0
- package/src/core/skills/skill-registry.js +71 -0
- package/src/core/tasks/task-store.js +10 -6
- 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 +58 -13
- package/src/index.js +105 -12
- package/src/runtime/bootstrap.js +212 -20
- package/src/runtime/create-app.js +45 -3
- package/src/runtime/paths.js +40 -8
- package/src/runtime/service-manager.js +2 -2
- package/src/transport/telegram/bot.js +172 -63
- package/src/transport/telegram/media.js +37 -11
- package/tools/openai-transcribe/index.js +1 -1
- package/tools/openai-transcribe/tool.manifest.json +2 -2
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { mkdir, readdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
1
|
+
import { mkdir, readdir, readFile, rmdir, unlink, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { getToolConfigPath, getToolTmpDir, toolsDir as userToolsRoot } from "../../runtime/paths.js";
|
|
5
|
+
import { getToolConfigPath, getToolTmpDir, getChatToolTmpDir, toolsDir as userToolsRoot } from "../../runtime/paths.js";
|
|
6
6
|
import { loadToolConfig, parseConfigModule, writeToolConfig } from "./tool-config.js";
|
|
7
7
|
import { normalizeToolResult } from "./tool-result.js";
|
|
8
|
+
import { SkillRegistry } from "../skills/skill-registry.js";
|
|
8
9
|
|
|
9
10
|
const bundledToolsRoot = fileURLToPath(new URL("../../../tools", import.meta.url));
|
|
10
11
|
const toolRoots = [
|
|
@@ -27,6 +28,7 @@ export class ToolRegistry {
|
|
|
27
28
|
constructor({ logger } = {}) {
|
|
28
29
|
this.logger = logger;
|
|
29
30
|
this.tools = new Map();
|
|
31
|
+
this.skillRegistry = new SkillRegistry();
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
async load() {
|
|
@@ -52,8 +54,10 @@ export class ToolRegistry {
|
|
|
52
54
|
const configSource = await readFile(configPath, "utf8");
|
|
53
55
|
const defaults = parseConfigModule(configSource);
|
|
54
56
|
const config = await loadToolConfig(manifest.name, defaults);
|
|
57
|
+
const skillHints = this.skillRegistry.normalizeHints(manifest);
|
|
55
58
|
this.tools.set(manifest.name, {
|
|
56
59
|
...manifest,
|
|
60
|
+
skillHints,
|
|
57
61
|
dir: toolDir,
|
|
58
62
|
entry: path.join(toolDir, manifest.entry || "index.js"),
|
|
59
63
|
localConfigPath: configPath,
|
|
@@ -77,7 +81,8 @@ export class ToolRegistry {
|
|
|
77
81
|
description: tool.description,
|
|
78
82
|
input: tool.input,
|
|
79
83
|
output: tool.output,
|
|
80
|
-
configSchema: tool.configSchema || {}
|
|
84
|
+
configSchema: tool.configSchema || {},
|
|
85
|
+
skillHints: tool.skillHints || []
|
|
81
86
|
}));
|
|
82
87
|
}
|
|
83
88
|
|
|
@@ -89,33 +94,73 @@ export class ToolRegistry {
|
|
|
89
94
|
const tool = this.get(name);
|
|
90
95
|
if (!tool) throw new Error(`Tool not found: ${name}`);
|
|
91
96
|
const result = await runProcess("node", [tool.entry, "--help"], { cwd: tool.dir, env: process.env });
|
|
92
|
-
|
|
97
|
+
const help = result.stdout || result.stderr;
|
|
98
|
+
const skills = await this.resolveSkills(name);
|
|
99
|
+
if (!skills.length) return help;
|
|
100
|
+
const skillHelp = skills.map((item) => [
|
|
101
|
+
`- ${item.name}${item.when ? ` (${item.when})` : ""}`,
|
|
102
|
+
item.description ? ` ${item.description}` : null,
|
|
103
|
+
item.found ? ` path: ${item.path}` : " warning: skill not found"
|
|
104
|
+
].filter(Boolean).join("\n")).join("\n");
|
|
105
|
+
return `${help}\n\nAssigned skills:\n${skillHelp}\n`;
|
|
93
106
|
}
|
|
94
107
|
|
|
95
|
-
async
|
|
108
|
+
async resolveSkills(name) {
|
|
96
109
|
const tool = this.get(name);
|
|
97
110
|
if (!tool) throw new Error(`Tool not found: ${name}`);
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
111
|
+
const hints = await this.skillRegistry.resolveHints(tool.skillHints || []);
|
|
112
|
+
return hints.map((hint) => ({
|
|
113
|
+
name: hint.name,
|
|
114
|
+
when: hint.when,
|
|
115
|
+
found: hint.found,
|
|
116
|
+
description: hint.skill?.description || "",
|
|
117
|
+
path: hint.skill?.path || "",
|
|
118
|
+
content: hint.skill?.content || ""
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async resolveConfigForChat(name, chatId) {
|
|
123
|
+
const tool = this.get(name);
|
|
124
|
+
if (!tool) throw new Error(`Tool not found: ${name}`);
|
|
125
|
+
if (chatId == null) return tool.config || {};
|
|
126
|
+
return loadToolConfig(name, tool.defaults || {}, chatId);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async setConfig(name, field, value, chatId = null) {
|
|
130
|
+
const tool = this.get(name);
|
|
131
|
+
if (!tool) throw new Error(`Tool not found: ${name}`);
|
|
132
|
+
const current = chatId != null
|
|
133
|
+
? await this.resolveConfigForChat(name, chatId)
|
|
134
|
+
: { ...(tool.config || {}) };
|
|
135
|
+
current[field] = value;
|
|
136
|
+
const configPath = await writeToolConfig(name, current, chatId);
|
|
137
|
+
if (chatId == null) {
|
|
138
|
+
tool.config = current;
|
|
139
|
+
tool.configPath = configPath;
|
|
140
|
+
}
|
|
103
141
|
return { ok: true, tool: name, field, configPath };
|
|
104
142
|
}
|
|
105
143
|
|
|
106
|
-
async run({ name, request }) {
|
|
144
|
+
async run({ name, request, chatId = null }) {
|
|
107
145
|
const tool = this.get(name);
|
|
108
146
|
if (!tool) throw new Error(`Tool not found: ${name}`);
|
|
109
147
|
this.logger?.log("tools", `running ${name}`);
|
|
110
|
-
const tmpDir = getToolTmpDir(name);
|
|
148
|
+
const tmpDir = chatId != null ? getChatToolTmpDir(chatId, name) : getToolTmpDir(name);
|
|
111
149
|
await mkdir(tmpDir, { recursive: true });
|
|
112
150
|
const requestFile = path.join(tmpDir, `.request-${Date.now()}.json`);
|
|
113
|
-
await
|
|
151
|
+
const skills = await this.resolveSkills(name);
|
|
152
|
+
const enrichedRequest = { ...request, chatId, skills };
|
|
153
|
+
await writeFile(requestFile, `${JSON.stringify(enrichedRequest, null, 2)}\n`, "utf8");
|
|
114
154
|
const result = await runProcess("node", [tool.entry, "run", "--request-file", requestFile], {
|
|
115
155
|
cwd: tool.dir,
|
|
116
156
|
env: process.env
|
|
117
157
|
});
|
|
118
158
|
await unlink(requestFile).catch(() => {});
|
|
159
|
+
await rmdir(tmpDir).catch(() => {});
|
|
160
|
+
if (chatId != null) {
|
|
161
|
+
await rmdir(path.dirname(tmpDir)).catch(() => {});
|
|
162
|
+
await rmdir(path.dirname(path.dirname(tmpDir))).catch(() => {});
|
|
163
|
+
}
|
|
119
164
|
try {
|
|
120
165
|
const parsed = JSON.parse(result.stdout || result.stderr);
|
|
121
166
|
const normalized = normalizeToolResult(name, parsed);
|
package/src/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { createServer } from "node:http";
|
|
3
4
|
import { bootstrapIfNeeded } from "./runtime/bootstrap.js";
|
|
4
5
|
import { createApp } from "./runtime/create-app.js";
|
|
5
6
|
import { createLogger } from "./runtime/logger.js";
|
|
@@ -8,25 +9,117 @@ import { flushArisaHome } from "./runtime/flush.js";
|
|
|
8
9
|
import { installPiPackage, removePiPackage } from "./runtime/pi-package-manager.js";
|
|
9
10
|
|
|
10
11
|
const args = process.argv.slice(2);
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const
|
|
12
|
+
const cli = parseCliArgs(args);
|
|
13
|
+
const command = cli.positionals[0] || "run";
|
|
14
|
+
const forceBootstrap = Boolean(cli.flags.bootstrap);
|
|
15
|
+
const verbose = Boolean(cli.flags.verbose);
|
|
16
|
+
const serviceRunner = Boolean(cli.flags["service-runner"]);
|
|
17
|
+
const bootstrapOverrides = toBootstrapOverrides(cli.nestedFlags);
|
|
18
|
+
const runtimeOverrides = toRuntimeOverrides(cli.nestedFlags);
|
|
15
19
|
const logger = createLogger({ verbose });
|
|
16
20
|
|
|
21
|
+
const httpPort = Number(process.env.PORT);
|
|
22
|
+
let httpRequestHandler = null;
|
|
23
|
+
|
|
24
|
+
function setHttpRequestHandler(handler) {
|
|
25
|
+
httpRequestHandler = handler;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (httpPort && bootstrapOverrides.telegram) {
|
|
29
|
+
createServer((req, res) => {
|
|
30
|
+
if (httpRequestHandler) return httpRequestHandler(req, res);
|
|
31
|
+
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
32
|
+
res.end("ok");
|
|
33
|
+
}).listen(httpPort)
|
|
34
|
+
.on("listening", () => logger.log("http", `health server on port ${httpPort}`));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseCliArgs(rawArgs) {
|
|
38
|
+
const flags = {};
|
|
39
|
+
const nestedFlags = {};
|
|
40
|
+
const positionals = [];
|
|
41
|
+
|
|
42
|
+
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
43
|
+
const token = rawArgs[index];
|
|
44
|
+
if (!token.startsWith("--")) {
|
|
45
|
+
positionals.push(token);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const flagName = token.slice(2);
|
|
50
|
+
if (!flagName) continue;
|
|
51
|
+
if (flagName.includes(".")) {
|
|
52
|
+
const next = rawArgs[index + 1];
|
|
53
|
+
const hasValue = typeof next === "string" && !next.startsWith("--");
|
|
54
|
+
if (hasValue) {
|
|
55
|
+
nestedFlags[flagName] = next;
|
|
56
|
+
index += 1;
|
|
57
|
+
}
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
flags[flagName] = true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { flags, nestedFlags, positionals };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function toBootstrapOverrides(nestedFlags) {
|
|
68
|
+
const overrides = {};
|
|
69
|
+
for (const [flatKey, value] of Object.entries(nestedFlags)) {
|
|
70
|
+
const parts = flatKey.split(".");
|
|
71
|
+
let cursor = overrides;
|
|
72
|
+
for (let index = 0; index < parts.length - 1; index += 1) {
|
|
73
|
+
const key = parts[index];
|
|
74
|
+
if (!cursor[key] || typeof cursor[key] !== "object") {
|
|
75
|
+
cursor[key] = {};
|
|
76
|
+
}
|
|
77
|
+
cursor = cursor[key];
|
|
78
|
+
}
|
|
79
|
+
cursor[parts[parts.length - 1]] = value;
|
|
80
|
+
}
|
|
81
|
+
return overrides;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function toRuntimeOverrides(nestedFlags) {
|
|
85
|
+
return toBootstrapOverrides(nestedFlags);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function toServiceRunnerArgs(nestedFlags) {
|
|
89
|
+
const args = [];
|
|
90
|
+
if (nestedFlags["pi.model"]) {
|
|
91
|
+
args.push("--pi.model", nestedFlags["pi.model"]);
|
|
92
|
+
}
|
|
93
|
+
return args;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const bootstrapHttpOptions = httpPort ? { httpPort, setHttpRequestHandler } : {};
|
|
97
|
+
const webhookUrl = bootstrapOverrides.webhook?.url || "";
|
|
98
|
+
const appHttpOptions = httpPort ? { webhookUrl, setHttpRequestHandler } : {};
|
|
99
|
+
|
|
17
100
|
async function runForeground() {
|
|
101
|
+
const hasRuntimePiOverrides = Boolean(
|
|
102
|
+
runtimeOverrides?.pi?.model
|
|
103
|
+
|| runtimeOverrides?.pi?.provider
|
|
104
|
+
|| runtimeOverrides?.pi?.apiKey
|
|
105
|
+
);
|
|
18
106
|
logger.log("app", `starting${verbose ? " in verbose mode" : ""}`);
|
|
19
|
-
await bootstrapIfNeeded({ force: forceBootstrap });
|
|
107
|
+
await bootstrapIfNeeded({ force: forceBootstrap, cliConfigOverrides: bootstrapOverrides, ...bootstrapHttpOptions });
|
|
20
108
|
try {
|
|
21
|
-
const app = await createApp({ logger });
|
|
109
|
+
const app = await createApp({ logger, runtimeOverrides, ...appHttpOptions });
|
|
22
110
|
await app.start();
|
|
23
111
|
} catch (error) {
|
|
24
112
|
const message = error instanceof Error ? error.message : String(error);
|
|
25
113
|
if (message.includes("No auth found")) {
|
|
26
114
|
console.log(`\n${message}\n`);
|
|
115
|
+
if (hasRuntimePiOverrides) {
|
|
116
|
+
console.log("Skipping automatic bootstrap because Pi runtime overrides were provided.");
|
|
117
|
+
console.log("Keeping existing Telegram config. Run `arisa --bootstrap` manually if you want to update persisted auth/config.\n");
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
27
120
|
console.log("Reopening bootstrap so you can provide a Pi API key or switch to a provider you already authenticated with.\n");
|
|
28
|
-
await bootstrapIfNeeded({ force: true });
|
|
29
|
-
const app = await createApp({ logger });
|
|
121
|
+
await bootstrapIfNeeded({ force: true, cliConfigOverrides: bootstrapOverrides, ...bootstrapHttpOptions });
|
|
122
|
+
const app = await createApp({ logger, runtimeOverrides, ...appHttpOptions });
|
|
30
123
|
await app.start();
|
|
31
124
|
return;
|
|
32
125
|
}
|
|
@@ -42,8 +135,8 @@ async function main() {
|
|
|
42
135
|
}
|
|
43
136
|
|
|
44
137
|
if (command === "start") {
|
|
45
|
-
await bootstrapIfNeeded({ force: forceBootstrap });
|
|
46
|
-
const result = await startService({ verbose });
|
|
138
|
+
await bootstrapIfNeeded({ force: forceBootstrap, cliConfigOverrides: bootstrapOverrides, ...bootstrapHttpOptions });
|
|
139
|
+
const result = await startService({ verbose, cliArgs: toServiceRunnerArgs(cli.nestedFlags) });
|
|
47
140
|
if (!result.ok) {
|
|
48
141
|
console.log(`Arisa is already running in background (pid ${result.pid}).`);
|
|
49
142
|
return;
|
|
@@ -85,7 +178,7 @@ async function main() {
|
|
|
85
178
|
}
|
|
86
179
|
|
|
87
180
|
if (command === "install") {
|
|
88
|
-
const source =
|
|
181
|
+
const source = cli.positionals[1];
|
|
89
182
|
if (!source) {
|
|
90
183
|
console.log("Usage: arisa install <pi-package-source>");
|
|
91
184
|
return;
|
|
@@ -96,7 +189,7 @@ async function main() {
|
|
|
96
189
|
}
|
|
97
190
|
|
|
98
191
|
if (command === "remove") {
|
|
99
|
-
const source =
|
|
192
|
+
const source = cli.positionals[1];
|
|
100
193
|
if (!source) {
|
|
101
194
|
console.log("Usage: arisa remove <pi-package-source>");
|
|
102
195
|
return;
|
package/src/runtime/bootstrap.js
CHANGED
|
@@ -15,9 +15,82 @@ 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
|
-
"openai-codex": ["gpt-5.
|
|
93
|
+
"openai-codex": ["gpt-5.5"]
|
|
21
94
|
};
|
|
22
95
|
|
|
23
96
|
const priority = preferred[provider] || [];
|
|
@@ -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() {
|