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.
- package/AGENTS.md +11 -0
- package/README.md +37 -0
- package/docs/async-event-queue-flow.md +68 -0
- package/package.json +9 -10
- package/pnpm-workspace.yaml +12 -0
- package/src/core/agent/agent-manager.js +137 -28
- 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 +169 -0
- 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 +25 -10
- package/src/index.js +105 -12
- package/src/runtime/bootstrap.js +211 -19
- package/src/runtime/create-app.js +48 -4
- package/src/runtime/paths.js +27 -3
- package/src/runtime/service-manager.js +2 -2
- package/src/transport/telegram/bot.js +190 -77
- package/src/transport/telegram/media.js +17 -11
- package/tools/openai-transcribe/index.js +1 -1
- package/tools/schedule-agent-task/config.js +1 -0
- package/tools/schedule-agent-task/index.js +68 -0
- package/tools/schedule-agent-task/package.json +6 -0
- package/tools/schedule-agent-task/tool.manifest.json +8 -0
- package/tools/web-browser/index.js +1 -1
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
function mimeMatches(pattern, mimeType = "") {
|
|
2
|
+
if (!pattern || !mimeType) return false;
|
|
3
|
+
if (pattern === mimeType) return true;
|
|
4
|
+
if (pattern.endsWith("/*")) return mimeType.startsWith(`${pattern.slice(0, -2)}/`);
|
|
5
|
+
return false;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function toolSupportsArtifact(tool, artifact) {
|
|
9
|
+
const inputs = Array.isArray(tool.input) ? tool.input : [];
|
|
10
|
+
return inputs.some((input) => mimeMatches(input, artifact.mimeType));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function toolProduces(tool, mimeType) {
|
|
14
|
+
const outputs = Array.isArray(tool.output) ? tool.output : [];
|
|
15
|
+
return outputs.some((output) => mimeMatches(output, mimeType));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function looksLikeAudioTranscriptionTool(tool) {
|
|
19
|
+
return /transcri|whisper|speech.?to.?text|audio.?to.?text/i.test(`${tool.name} ${tool.description || ""}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function shouldNormalizeAudioToText(artifact, desiredMimeType) {
|
|
23
|
+
return artifact?.mimeType?.startsWith("audio/") && desiredMimeType === "text/plain";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function selectPipeTool({ toolRegistry, artifact, desiredMimeType }) {
|
|
27
|
+
const tools = toolRegistry.list()
|
|
28
|
+
.filter((tool) => toolSupportsArtifact(tool, artifact))
|
|
29
|
+
.filter((tool) => toolProduces(tool, desiredMimeType));
|
|
30
|
+
|
|
31
|
+
if (shouldNormalizeAudioToText(artifact, desiredMimeType)) {
|
|
32
|
+
return tools.find(looksLikeAudioTranscriptionTool) || null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function normalizeArtifactForReasoning({
|
|
39
|
+
artifact,
|
|
40
|
+
desiredMimeType = "text/plain",
|
|
41
|
+
toolRegistry,
|
|
42
|
+
chatArtifactStore,
|
|
43
|
+
chatId
|
|
44
|
+
}) {
|
|
45
|
+
if (!artifact) return { normalizedArtifact: null, toolResult: null, toolName: "" };
|
|
46
|
+
|
|
47
|
+
if (!shouldNormalizeAudioToText(artifact, desiredMimeType)) {
|
|
48
|
+
return { normalizedArtifact: null, toolResult: null, toolName: "" };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const tool = selectPipeTool({ toolRegistry, artifact, desiredMimeType });
|
|
52
|
+
if (!tool) {
|
|
53
|
+
return {
|
|
54
|
+
normalizedArtifact: null,
|
|
55
|
+
toolResult: {
|
|
56
|
+
ok: false,
|
|
57
|
+
status: "failed",
|
|
58
|
+
error: `No registered tool can normalize ${artifact.mimeType} to ${desiredMimeType}.`
|
|
59
|
+
},
|
|
60
|
+
toolName: ""
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const result = await toolRegistry.run({
|
|
65
|
+
name: tool.name,
|
|
66
|
+
request: { artifact, args: {} },
|
|
67
|
+
chatId
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
if (!result.ok) {
|
|
71
|
+
return { normalizedArtifact: null, toolResult: result, toolName: tool.name };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!result.output?.text) {
|
|
75
|
+
return {
|
|
76
|
+
normalizedArtifact: null,
|
|
77
|
+
toolResult: { ok: false, status: "failed", error: "Normalization returned no text." },
|
|
78
|
+
toolName: tool.name
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const normalizedArtifact = await chatArtifactStore.createText({
|
|
83
|
+
text: result.output.text,
|
|
84
|
+
mimeType: desiredMimeType,
|
|
85
|
+
source: { type: "tool", toolName: tool.name },
|
|
86
|
+
metadata: { fromArtifactId: artifact.id, tool: tool.name, normalization: true }
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return { normalizedArtifact, toolResult: result, toolName: tool.name };
|
|
90
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import crypto from "node:crypto";
|
|
4
|
+
import { tasksFile } from "../../runtime/paths.js";
|
|
5
|
+
|
|
6
|
+
async function loadTasksFile() {
|
|
7
|
+
try {
|
|
8
|
+
return JSON.parse(await readFile(tasksFile, "utf8"));
|
|
9
|
+
} catch {
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function saveTasksFile(tasks) {
|
|
15
|
+
await mkdir(path.dirname(tasksFile), { recursive: true });
|
|
16
|
+
await writeFile(tasksFile, `${JSON.stringify(tasks, null, 2)}\n`, "utf8");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function taskId() {
|
|
20
|
+
return crypto.randomUUID();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function normalizeTask(task, defaults = {}) {
|
|
24
|
+
return {
|
|
25
|
+
id: task.id || taskId(),
|
|
26
|
+
status: task.status || "pending",
|
|
27
|
+
createdAt: task.createdAt || new Date().toISOString(),
|
|
28
|
+
updatedAt: new Date().toISOString(),
|
|
29
|
+
kind: task.kind,
|
|
30
|
+
runAt: task.runAt,
|
|
31
|
+
payload: {
|
|
32
|
+
...(defaults.payload || {}),
|
|
33
|
+
...(task.payload || {})
|
|
34
|
+
},
|
|
35
|
+
recurrence: task.recurrence || defaults.recurrence || null,
|
|
36
|
+
source: {
|
|
37
|
+
...(defaults.source || {}),
|
|
38
|
+
...(task.source || {})
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function computeNextRunAt(task) {
|
|
44
|
+
if (task.recurrence?.type === "interval" && Number(task.recurrence.everySeconds) > 0) {
|
|
45
|
+
return new Date(Date.now() + (Number(task.recurrence.everySeconds) * 1000)).toISOString();
|
|
46
|
+
}
|
|
47
|
+
return "";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class TaskStore {
|
|
51
|
+
constructor() {
|
|
52
|
+
this.tasks = null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async init() {
|
|
56
|
+
if (!this.tasks) this.tasks = await loadTasksFile();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async reload() {
|
|
60
|
+
this.tasks = await loadTasksFile();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async save() {
|
|
64
|
+
await saveTasksFile(this.tasks || []);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async add(task, defaults = {}) {
|
|
68
|
+
await this.init();
|
|
69
|
+
const normalized = normalizeTask(task, defaults);
|
|
70
|
+
this.tasks.push(normalized);
|
|
71
|
+
await this.save();
|
|
72
|
+
return normalized;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async addMany(tasks = [], defaults = {}) {
|
|
76
|
+
const created = [];
|
|
77
|
+
for (const task of tasks) {
|
|
78
|
+
created.push(await this.add(task, defaults));
|
|
79
|
+
}
|
|
80
|
+
return created;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async claimDue(limit = 10) {
|
|
84
|
+
await this.reload();
|
|
85
|
+
const now = Date.now();
|
|
86
|
+
const due = [];
|
|
87
|
+
|
|
88
|
+
for (const task of this.tasks) {
|
|
89
|
+
if (due.length >= limit) break;
|
|
90
|
+
if (task.status !== "pending") continue;
|
|
91
|
+
if (!task.runAt || Number.isNaN(Date.parse(task.runAt))) continue;
|
|
92
|
+
if (Date.parse(task.runAt) > now) continue;
|
|
93
|
+
task.status = "running";
|
|
94
|
+
task.updatedAt = new Date().toISOString();
|
|
95
|
+
due.push({ ...task });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (due.length) await this.save();
|
|
99
|
+
return due;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async complete(taskId) {
|
|
103
|
+
await this.init();
|
|
104
|
+
const task = this.tasks.find((item) => item.id === taskId);
|
|
105
|
+
if (!task) return null;
|
|
106
|
+
|
|
107
|
+
const nextRunAt = computeNextRunAt(task);
|
|
108
|
+
if (nextRunAt) {
|
|
109
|
+
task.status = "pending";
|
|
110
|
+
task.runAt = nextRunAt;
|
|
111
|
+
task.lastRunAt = new Date().toISOString();
|
|
112
|
+
} else {
|
|
113
|
+
task.status = "done";
|
|
114
|
+
task.completedAt = new Date().toISOString();
|
|
115
|
+
}
|
|
116
|
+
task.updatedAt = new Date().toISOString();
|
|
117
|
+
await this.save();
|
|
118
|
+
return task;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async fail(taskId, error) {
|
|
122
|
+
await this.init();
|
|
123
|
+
const task = this.tasks.find((item) => item.id === taskId);
|
|
124
|
+
if (!task) return null;
|
|
125
|
+
task.status = "failed";
|
|
126
|
+
task.error = error;
|
|
127
|
+
task.updatedAt = new Date().toISOString();
|
|
128
|
+
await this.save();
|
|
129
|
+
return task;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async list(filter = {}) {
|
|
133
|
+
await this.reload();
|
|
134
|
+
return this.tasks.filter((task) => {
|
|
135
|
+
if (filter.chatId && task.payload?.chatId !== filter.chatId) return false;
|
|
136
|
+
if (filter.status && task.status !== filter.status) return false;
|
|
137
|
+
if (filter.kind && task.kind !== filter.kind) return false;
|
|
138
|
+
return true;
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async get(taskId) {
|
|
143
|
+
await this.reload();
|
|
144
|
+
return this.tasks.find((item) => item.id === taskId) || null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async cancel(taskId) {
|
|
148
|
+
await this.reload();
|
|
149
|
+
const index = this.tasks.findIndex((item) => item.id === taskId);
|
|
150
|
+
if (index === -1) return null;
|
|
151
|
+
const [task] = this.tasks.splice(index, 1);
|
|
152
|
+
await this.save();
|
|
153
|
+
return task;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async cancelAll(filter = {}) {
|
|
157
|
+
await this.reload();
|
|
158
|
+
const removed = [];
|
|
159
|
+
this.tasks = this.tasks.filter((task) => {
|
|
160
|
+
if (filter.chatId && task.payload?.chatId !== filter.chatId) return true;
|
|
161
|
+
if (filter.status && task.status !== filter.status) return true;
|
|
162
|
+
if (task.status === "done" || task.status === "failed") return true;
|
|
163
|
+
removed.push({ ...task });
|
|
164
|
+
return false;
|
|
165
|
+
});
|
|
166
|
+
if (removed.length) await this.save();
|
|
167
|
+
return removed;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { openSync } from "node:fs";
|
|
4
|
+
import { mkdir, readFile, readdir, rename, rm, unlink, writeFile } from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { stateDir } from "../../runtime/paths.js";
|
|
7
|
+
|
|
8
|
+
export function daemonPaths(toolName) {
|
|
9
|
+
const root = path.join(stateDir, toolName);
|
|
10
|
+
return {
|
|
11
|
+
root,
|
|
12
|
+
commandsDir: path.join(root, "commands"),
|
|
13
|
+
pidFile: path.join(root, "daemon.pid"),
|
|
14
|
+
statusFile: path.join(root, "status.json"),
|
|
15
|
+
logFile: path.join(root, "daemon.log")
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function readJson(file, fallback = {}) {
|
|
20
|
+
try { return JSON.parse(await readFile(file, "utf8")); } catch { return fallback; }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function writeJson(file, value) {
|
|
24
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
25
|
+
await writeFile(file, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function isProcessAlive(pid) {
|
|
29
|
+
if (!pid) return false;
|
|
30
|
+
try { process.kill(pid, 0); return true; } catch { return false; }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function createDaemonRuntime({ toolName, entryPath, beforeStart = null }) {
|
|
34
|
+
const paths = daemonPaths(toolName);
|
|
35
|
+
|
|
36
|
+
async function ensure() {
|
|
37
|
+
await mkdir(paths.commandsDir, { recursive: true });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function jobPaths(id) {
|
|
41
|
+
return {
|
|
42
|
+
request: path.join(paths.commandsDir, `${id}.request.json`),
|
|
43
|
+
processing: path.join(paths.commandsDir, `${id}.processing.json`),
|
|
44
|
+
result: path.join(paths.commandsDir, `${id}.result.json`)
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function getPid() {
|
|
49
|
+
return (await readJson(paths.pidFile, {})).pid;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function writeStatus(patch) {
|
|
53
|
+
const current = await readJson(paths.statusFile, {});
|
|
54
|
+
await writeJson(paths.statusFile, { ...current, ...patch, updatedAt: new Date().toISOString() });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function clearJobs() {
|
|
58
|
+
await rm(paths.commandsDir, { recursive: true, force: true });
|
|
59
|
+
await mkdir(paths.commandsDir, { recursive: true });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function start() {
|
|
63
|
+
await ensure();
|
|
64
|
+
const pid = await getPid();
|
|
65
|
+
if (isProcessAlive(pid)) return pid;
|
|
66
|
+
|
|
67
|
+
await clearJobs();
|
|
68
|
+
if (beforeStart) await beforeStart();
|
|
69
|
+
const out = openSync(paths.logFile, "a");
|
|
70
|
+
const child = spawn(process.execPath, [entryPath, "daemon"], {
|
|
71
|
+
detached: true,
|
|
72
|
+
stdio: ["ignore", out, out],
|
|
73
|
+
env: process.env
|
|
74
|
+
});
|
|
75
|
+
child.unref();
|
|
76
|
+
await writeJson(paths.pidFile, { pid: child.pid, startedAt: new Date().toISOString() });
|
|
77
|
+
return child.pid;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function stop() {
|
|
81
|
+
const pid = await getPid();
|
|
82
|
+
if (isProcessAlive(pid)) process.kill(pid, "SIGTERM");
|
|
83
|
+
await rm(paths.pidFile, { force: true });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function waitReady({ timeoutMs = 120000, readyStates = ["ready"] } = {}) {
|
|
87
|
+
const startTime = Date.now();
|
|
88
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
89
|
+
const status = await readJson(paths.statusFile, {});
|
|
90
|
+
const pid = await getPid();
|
|
91
|
+
if (readyStates.includes(status.state) && isProcessAlive(pid)) return status;
|
|
92
|
+
if (status.state === "error") throw new Error(status.message || `${toolName} daemon failed`);
|
|
93
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
94
|
+
}
|
|
95
|
+
throw new Error(`${toolName} daemon was not ready after ${timeoutMs}ms`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function submit(payload, { timeoutMs = 180000, readyTimeoutMs = 120000 } = {}) {
|
|
99
|
+
await start();
|
|
100
|
+
await waitReady({ timeoutMs: readyTimeoutMs });
|
|
101
|
+
const id = crypto.randomUUID();
|
|
102
|
+
const files = jobPaths(id);
|
|
103
|
+
await writeJson(files.request, { id, ...payload });
|
|
104
|
+
|
|
105
|
+
const startTime = Date.now();
|
|
106
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
107
|
+
const result = await readJson(files.result, null);
|
|
108
|
+
if (result) {
|
|
109
|
+
await unlink(files.result).catch(() => {});
|
|
110
|
+
if (!result.ok) throw new Error(result.error || `${toolName} job failed`);
|
|
111
|
+
return result.output || {};
|
|
112
|
+
}
|
|
113
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
114
|
+
}
|
|
115
|
+
throw new Error(`${toolName} job timed out after ${timeoutMs}ms`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function claimNext() {
|
|
119
|
+
await ensure();
|
|
120
|
+
const files = (await readdir(paths.commandsDir)).filter((file) => file.endsWith(".request.json"));
|
|
121
|
+
for (const file of files) {
|
|
122
|
+
const id = file.replace(/\.request\.json$/, "");
|
|
123
|
+
const item = jobPaths(id);
|
|
124
|
+
try {
|
|
125
|
+
await rename(item.request, item.processing);
|
|
126
|
+
return { id, ...item, payload: await readJson(item.processing, null) };
|
|
127
|
+
} catch {}
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function complete(job, output) {
|
|
133
|
+
await writeJson(job.result, { ok: true, output });
|
|
134
|
+
await unlink(job.processing).catch(() => {});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function fail(job, error) {
|
|
138
|
+
await writeJson(job.result, { ok: false, error: error?.message || String(error) });
|
|
139
|
+
await unlink(job.processing).catch(() => {});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function workLoop({ processJob, idleTimeoutMs = 0, intervalMs = 250 }) {
|
|
143
|
+
let lastActivity = Date.now();
|
|
144
|
+
setInterval(async () => {
|
|
145
|
+
try {
|
|
146
|
+
const job = await claimNext();
|
|
147
|
+
if (job) {
|
|
148
|
+
lastActivity = Date.now();
|
|
149
|
+
try {
|
|
150
|
+
await complete(job, await processJob(job.payload));
|
|
151
|
+
} catch (error) {
|
|
152
|
+
await fail(job, error);
|
|
153
|
+
}
|
|
154
|
+
lastActivity = Date.now();
|
|
155
|
+
}
|
|
156
|
+
if (idleTimeoutMs > 0 && Date.now() - lastActivity > idleTimeoutMs) {
|
|
157
|
+
await writeStatus({ state: "stopped", message: "Idle timeout reached" });
|
|
158
|
+
process.exit(0);
|
|
159
|
+
}
|
|
160
|
+
} catch (error) {
|
|
161
|
+
await writeStatus({ state: "error", message: error?.message || String(error) });
|
|
162
|
+
}
|
|
163
|
+
}, intervalMs);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return { paths, ensure, getPid, writeStatus, start, stop, waitReady, submit, workLoop };
|
|
167
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { getToolConfigPath } from "../../runtime/paths.js";
|
|
3
|
+
import { getToolConfigPath, getChatToolConfigPath } from "../../runtime/paths.js";
|
|
4
4
|
|
|
5
5
|
export function parseConfigModule(source) {
|
|
6
6
|
const normalized = source.replace(/^export\s+default/, "return");
|
|
@@ -21,14 +21,22 @@ export async function readConfigModule(filePath, fallback = {}) {
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
export async function loadToolConfig(toolName, defaults = {}) {
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
|
|
24
|
+
export async function loadToolConfig(toolName, defaults = {}, chatId = null) {
|
|
25
|
+
const globalPath = getToolConfigPath(toolName);
|
|
26
|
+
const globalStored = await readConfigModule(globalPath, {});
|
|
27
|
+
const merged = { ...defaults, ...globalStored };
|
|
28
|
+
|
|
29
|
+
if (chatId == null) return merged;
|
|
30
|
+
|
|
31
|
+
const chatPath = getChatToolConfigPath(chatId, toolName);
|
|
32
|
+
const chatStored = await readConfigModule(chatPath, {});
|
|
33
|
+
return { ...merged, ...chatStored };
|
|
28
34
|
}
|
|
29
35
|
|
|
30
|
-
export async function writeToolConfig(toolName, config) {
|
|
31
|
-
const configPath =
|
|
36
|
+
export async function writeToolConfig(toolName, config, chatId = null) {
|
|
37
|
+
const configPath = chatId != null
|
|
38
|
+
? getChatToolConfigPath(chatId, toolName)
|
|
39
|
+
: getToolConfigPath(toolName);
|
|
32
40
|
await mkdir(path.dirname(configPath), { recursive: true });
|
|
33
41
|
await writeFile(configPath, serializeConfigModule(config), "utf8");
|
|
34
42
|
return configPath;
|
|
@@ -2,7 +2,7 @@ import { mkdir, readdir, readFile, 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
8
|
|
|
@@ -92,22 +92,33 @@ export class ToolRegistry {
|
|
|
92
92
|
return result.stdout || result.stderr;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
async
|
|
95
|
+
async resolveConfigForChat(name, chatId) {
|
|
96
96
|
const tool = this.get(name);
|
|
97
97
|
if (!tool) throw new Error(`Tool not found: ${name}`);
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
98
|
+
if (chatId == null) return tool.config || {};
|
|
99
|
+
return loadToolConfig(name, tool.defaults || {}, chatId);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async setConfig(name, field, value, chatId = null) {
|
|
103
|
+
const tool = this.get(name);
|
|
104
|
+
if (!tool) throw new Error(`Tool not found: ${name}`);
|
|
105
|
+
const current = chatId != null
|
|
106
|
+
? await this.resolveConfigForChat(name, chatId)
|
|
107
|
+
: { ...(tool.config || {}) };
|
|
108
|
+
current[field] = value;
|
|
109
|
+
const configPath = await writeToolConfig(name, current, chatId);
|
|
110
|
+
if (chatId == null) {
|
|
111
|
+
tool.config = current;
|
|
112
|
+
tool.configPath = configPath;
|
|
113
|
+
}
|
|
103
114
|
return { ok: true, tool: name, field, configPath };
|
|
104
115
|
}
|
|
105
116
|
|
|
106
|
-
async run({ name, request }) {
|
|
117
|
+
async run({ name, request, chatId = null }) {
|
|
107
118
|
const tool = this.get(name);
|
|
108
119
|
if (!tool) throw new Error(`Tool not found: ${name}`);
|
|
109
120
|
this.logger?.log("tools", `running ${name}`);
|
|
110
|
-
const tmpDir = getToolTmpDir(name);
|
|
121
|
+
const tmpDir = chatId != null ? getChatToolTmpDir(chatId, name) : getToolTmpDir(name);
|
|
111
122
|
await mkdir(tmpDir, { recursive: true });
|
|
112
123
|
const requestFile = path.join(tmpDir, `.request-${Date.now()}.json`);
|
|
113
124
|
await writeFile(requestFile, `${JSON.stringify(request, null, 2)}\n`, "utf8");
|
|
@@ -119,7 +130,11 @@ export class ToolRegistry {
|
|
|
119
130
|
try {
|
|
120
131
|
const parsed = JSON.parse(result.stdout || result.stderr);
|
|
121
132
|
const normalized = normalizeToolResult(name, parsed);
|
|
122
|
-
|
|
133
|
+
if (normalized.ok === false) {
|
|
134
|
+
this.logger?.log("tools", `${name} -> ${normalized.status || "error"}: ${normalized.error || "unknown error"}`);
|
|
135
|
+
} else {
|
|
136
|
+
this.logger?.log("tools", `${name} -> ok`);
|
|
137
|
+
}
|
|
123
138
|
return normalized;
|
|
124
139
|
} catch {
|
|
125
140
|
return normalizeToolResult(name, {
|
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;
|