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,39 +1,44 @@
|
|
|
1
1
|
import { mkdir, readFile, writeFile, copyFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import crypto from "node:crypto";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
async function loadIndex() {
|
|
7
|
-
try {
|
|
8
|
-
return JSON.parse(await readFile(indexFile, "utf8"));
|
|
9
|
-
} catch {
|
|
10
|
-
return [];
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
async function saveIndex(items) {
|
|
15
|
-
await mkdir(path.dirname(indexFile), { recursive: true });
|
|
16
|
-
await writeFile(indexFile, `${JSON.stringify(items, null, 2)}\n`, "utf8");
|
|
17
|
-
}
|
|
4
|
+
import { getChatArtifactsDir, getChatArtifactsIndexFile } from "../../runtime/paths.js";
|
|
18
5
|
|
|
19
6
|
function id() {
|
|
20
7
|
return crypto.randomUUID();
|
|
21
8
|
}
|
|
22
9
|
|
|
23
|
-
|
|
24
|
-
constructor() {
|
|
10
|
+
class ChatArtifactStore {
|
|
11
|
+
constructor(chatId) {
|
|
12
|
+
this.chatId = String(chatId);
|
|
13
|
+
this.rootDir = getChatArtifactsDir(this.chatId);
|
|
14
|
+
this.indexFile = getChatArtifactsIndexFile(this.chatId);
|
|
25
15
|
this.items = null;
|
|
26
16
|
}
|
|
27
17
|
|
|
18
|
+
async reload() {
|
|
19
|
+
try {
|
|
20
|
+
this.items = JSON.parse(await readFile(this.indexFile, "utf8"));
|
|
21
|
+
} catch {
|
|
22
|
+
this.items = [];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
28
26
|
async init() {
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
await mkdir(this.rootDir, { recursive: true });
|
|
28
|
+
if (!this.items) await this.reload();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async saveIndex() {
|
|
32
|
+
await mkdir(path.dirname(this.indexFile), { recursive: true });
|
|
33
|
+
await writeFile(this.indexFile, `${JSON.stringify(this.items, null, 2)}\n`, "utf8");
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
async createText({ text, mimeType = "text/plain", source, metadata = {} }) {
|
|
34
37
|
await this.init();
|
|
38
|
+
await this.reload();
|
|
35
39
|
const artifact = {
|
|
36
40
|
id: id(),
|
|
41
|
+
chatId: this.chatId,
|
|
37
42
|
kind: "text",
|
|
38
43
|
mimeType,
|
|
39
44
|
text,
|
|
@@ -42,19 +47,21 @@ export class ArtifactStore {
|
|
|
42
47
|
createdAt: new Date().toISOString()
|
|
43
48
|
};
|
|
44
49
|
this.items.push(artifact);
|
|
45
|
-
await saveIndex(
|
|
50
|
+
await this.saveIndex();
|
|
46
51
|
return artifact;
|
|
47
52
|
}
|
|
48
53
|
|
|
49
54
|
async createFromFile({ originalPath, fileName, kind, mimeType, source, metadata = {} }) {
|
|
50
55
|
await this.init();
|
|
56
|
+
await this.reload();
|
|
51
57
|
const artifactId = id();
|
|
52
|
-
const dir = path.join(rootDir, artifactId);
|
|
58
|
+
const dir = path.join(this.rootDir, artifactId);
|
|
53
59
|
await mkdir(dir, { recursive: true });
|
|
54
60
|
const destPath = path.join(dir, fileName);
|
|
55
61
|
await copyFile(originalPath, destPath);
|
|
56
62
|
const artifact = {
|
|
57
63
|
id: artifactId,
|
|
64
|
+
chatId: this.chatId,
|
|
58
65
|
kind,
|
|
59
66
|
mimeType,
|
|
60
67
|
path: destPath,
|
|
@@ -63,19 +70,21 @@ export class ArtifactStore {
|
|
|
63
70
|
createdAt: new Date().toISOString()
|
|
64
71
|
};
|
|
65
72
|
this.items.push(artifact);
|
|
66
|
-
await saveIndex(
|
|
73
|
+
await this.saveIndex();
|
|
67
74
|
return artifact;
|
|
68
75
|
}
|
|
69
76
|
|
|
70
77
|
async createGeneratedFile({ fileName, content, kind, mimeType, source, metadata = {} }) {
|
|
71
78
|
await this.init();
|
|
79
|
+
await this.reload();
|
|
72
80
|
const artifactId = id();
|
|
73
|
-
const dir = path.join(rootDir, artifactId);
|
|
81
|
+
const dir = path.join(this.rootDir, artifactId);
|
|
74
82
|
await mkdir(dir, { recursive: true });
|
|
75
83
|
const destPath = path.join(dir, fileName);
|
|
76
84
|
await writeFile(destPath, content);
|
|
77
85
|
const artifact = {
|
|
78
86
|
id: artifactId,
|
|
87
|
+
chatId: this.chatId,
|
|
79
88
|
kind,
|
|
80
89
|
mimeType,
|
|
81
90
|
path: destPath,
|
|
@@ -84,17 +93,33 @@ export class ArtifactStore {
|
|
|
84
93
|
createdAt: new Date().toISOString()
|
|
85
94
|
};
|
|
86
95
|
this.items.push(artifact);
|
|
87
|
-
await saveIndex(
|
|
96
|
+
await this.saveIndex();
|
|
88
97
|
return artifact;
|
|
89
98
|
}
|
|
90
99
|
|
|
91
|
-
async get(
|
|
100
|
+
async get(artifactId) {
|
|
92
101
|
await this.init();
|
|
93
|
-
|
|
102
|
+
await this.reload();
|
|
103
|
+
return this.items.find((item) => item.id === artifactId) || null;
|
|
94
104
|
}
|
|
95
105
|
|
|
96
106
|
async listRecent(limit = 20) {
|
|
97
107
|
await this.init();
|
|
108
|
+
await this.reload();
|
|
98
109
|
return [...this.items].slice(-limit).reverse();
|
|
99
110
|
}
|
|
100
111
|
}
|
|
112
|
+
|
|
113
|
+
export class ArtifactStore {
|
|
114
|
+
constructor() {
|
|
115
|
+
this.chatStores = new Map();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
forChat(chatId) {
|
|
119
|
+
const key = String(chatId);
|
|
120
|
+
if (!this.chatStores.has(key)) {
|
|
121
|
+
this.chatStores.set(key, new ChatArtifactStore(key));
|
|
122
|
+
}
|
|
123
|
+
return this.chatStores.get(key);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
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
|
+
export function shouldNormalizeArtifactToText(artifact, desiredMimeType = "text/plain") {
|
|
23
|
+
return desiredMimeType === "text/plain"
|
|
24
|
+
&& (artifact?.mimeType?.startsWith("audio/") || artifact?.mimeType?.startsWith("video/"));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function selectPipeTool({ toolRegistry, artifact, desiredMimeType }) {
|
|
28
|
+
const tools = toolRegistry.list()
|
|
29
|
+
.filter((tool) => toolSupportsArtifact(tool, artifact))
|
|
30
|
+
.filter((tool) => toolProduces(tool, desiredMimeType));
|
|
31
|
+
|
|
32
|
+
if (shouldNormalizeArtifactToText(artifact, desiredMimeType)) {
|
|
33
|
+
return tools.find(looksLikeAudioTranscriptionTool) || null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function normalizeArtifactForReasoning({
|
|
40
|
+
artifact,
|
|
41
|
+
desiredMimeType = "text/plain",
|
|
42
|
+
toolRegistry,
|
|
43
|
+
chatArtifactStore,
|
|
44
|
+
chatId
|
|
45
|
+
}) {
|
|
46
|
+
if (!artifact) return { normalizedArtifact: null, toolResult: null, toolName: "" };
|
|
47
|
+
|
|
48
|
+
if (!shouldNormalizeArtifactToText(artifact, desiredMimeType)) {
|
|
49
|
+
return { normalizedArtifact: null, toolResult: null, toolName: "" };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const tool = selectPipeTool({ toolRegistry, artifact, desiredMimeType });
|
|
53
|
+
if (!tool) {
|
|
54
|
+
return {
|
|
55
|
+
normalizedArtifact: null,
|
|
56
|
+
toolResult: {
|
|
57
|
+
ok: false,
|
|
58
|
+
status: "failed",
|
|
59
|
+
error: `No registered tool can normalize ${artifact.mimeType} to ${desiredMimeType}.`
|
|
60
|
+
},
|
|
61
|
+
toolName: ""
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const result = await toolRegistry.run({
|
|
66
|
+
name: tool.name,
|
|
67
|
+
request: { artifact, args: {} },
|
|
68
|
+
chatId
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (!result.ok) {
|
|
72
|
+
return { normalizedArtifact: null, toolResult: result, toolName: tool.name };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!result.output?.text) {
|
|
76
|
+
return {
|
|
77
|
+
normalizedArtifact: null,
|
|
78
|
+
toolResult: { ok: false, status: "failed", error: "Normalization returned no text." },
|
|
79
|
+
toolName: tool.name
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const normalizedArtifact = await chatArtifactStore.createText({
|
|
84
|
+
text: result.output.text,
|
|
85
|
+
mimeType: desiredMimeType,
|
|
86
|
+
source: { type: "tool", toolName: tool.name },
|
|
87
|
+
metadata: { fromArtifactId: artifact.id, tool: tool.name, normalization: true }
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return { normalizedArtifact, toolResult: result, toolName: tool.name };
|
|
91
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
|
|
5
|
+
const defaultSkillsDir = path.join(os.homedir(), ".agents", "skills");
|
|
6
|
+
|
|
7
|
+
function parseFrontmatter(source = "") {
|
|
8
|
+
if (!source.startsWith("---")) return {};
|
|
9
|
+
const end = source.indexOf("\n---", 3);
|
|
10
|
+
if (end === -1) return {};
|
|
11
|
+
const block = source.slice(3, end).trim();
|
|
12
|
+
const data = {};
|
|
13
|
+
for (const line of block.split("\n")) {
|
|
14
|
+
const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
|
|
15
|
+
if (match) data[match[1]] = match[2].replace(/^['"]|['"]$/g, "");
|
|
16
|
+
}
|
|
17
|
+
return data;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function normalizeSkillHint(value) {
|
|
21
|
+
if (typeof value === "string") return { name: value, when: "" };
|
|
22
|
+
if (value && typeof value === "object" && value.name) {
|
|
23
|
+
return { name: String(value.name), when: String(value.when || "") };
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class SkillRegistry {
|
|
29
|
+
constructor({ skillsDir = defaultSkillsDir } = {}) {
|
|
30
|
+
this.skillsDir = skillsDir;
|
|
31
|
+
this.cache = new Map();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async get(name) {
|
|
35
|
+
const key = String(name || "").trim();
|
|
36
|
+
if (!key) return null;
|
|
37
|
+
if (this.cache.has(key)) return this.cache.get(key);
|
|
38
|
+
|
|
39
|
+
const file = path.join(this.skillsDir, key, "SKILL.md");
|
|
40
|
+
try {
|
|
41
|
+
const content = await readFile(file, "utf8");
|
|
42
|
+
const metadata = parseFrontmatter(content);
|
|
43
|
+
const skill = {
|
|
44
|
+
name: metadata.name || key,
|
|
45
|
+
description: metadata.description || "",
|
|
46
|
+
path: file,
|
|
47
|
+
content
|
|
48
|
+
};
|
|
49
|
+
this.cache.set(key, skill);
|
|
50
|
+
return skill;
|
|
51
|
+
} catch {
|
|
52
|
+
this.cache.set(key, null);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
normalizeHints(manifest = {}) {
|
|
58
|
+
const raw = manifest.skillHints || manifest.skills || [];
|
|
59
|
+
if (!Array.isArray(raw)) return [];
|
|
60
|
+
return raw.map(normalizeSkillHint).filter(Boolean);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async resolveHints(hints = []) {
|
|
64
|
+
const resolved = [];
|
|
65
|
+
for (const hint of hints) {
|
|
66
|
+
const skill = await this.get(hint.name);
|
|
67
|
+
resolved.push({ ...hint, found: Boolean(skill), skill });
|
|
68
|
+
}
|
|
69
|
+
return resolved;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -27,7 +27,7 @@ function normalizeTask(task, defaults = {}) {
|
|
|
27
27
|
createdAt: task.createdAt || new Date().toISOString(),
|
|
28
28
|
updatedAt: new Date().toISOString(),
|
|
29
29
|
kind: task.kind,
|
|
30
|
-
runAt: task.runAt,
|
|
30
|
+
runAt: task.runAt || new Date().toISOString(),
|
|
31
31
|
payload: {
|
|
32
32
|
...(defaults.payload || {}),
|
|
33
33
|
...(task.payload || {})
|
|
@@ -56,6 +56,10 @@ export class TaskStore {
|
|
|
56
56
|
if (!this.tasks) this.tasks = await loadTasksFile();
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
async reload() {
|
|
60
|
+
this.tasks = await loadTasksFile();
|
|
61
|
+
}
|
|
62
|
+
|
|
59
63
|
async save() {
|
|
60
64
|
await saveTasksFile(this.tasks || []);
|
|
61
65
|
}
|
|
@@ -77,7 +81,7 @@ export class TaskStore {
|
|
|
77
81
|
}
|
|
78
82
|
|
|
79
83
|
async claimDue(limit = 10) {
|
|
80
|
-
await this.
|
|
84
|
+
await this.reload();
|
|
81
85
|
const now = Date.now();
|
|
82
86
|
const due = [];
|
|
83
87
|
|
|
@@ -126,7 +130,7 @@ export class TaskStore {
|
|
|
126
130
|
}
|
|
127
131
|
|
|
128
132
|
async list(filter = {}) {
|
|
129
|
-
await this.
|
|
133
|
+
await this.reload();
|
|
130
134
|
return this.tasks.filter((task) => {
|
|
131
135
|
if (filter.chatId && task.payload?.chatId !== filter.chatId) return false;
|
|
132
136
|
if (filter.status && task.status !== filter.status) return false;
|
|
@@ -136,12 +140,12 @@ export class TaskStore {
|
|
|
136
140
|
}
|
|
137
141
|
|
|
138
142
|
async get(taskId) {
|
|
139
|
-
await this.
|
|
143
|
+
await this.reload();
|
|
140
144
|
return this.tasks.find((item) => item.id === taskId) || null;
|
|
141
145
|
}
|
|
142
146
|
|
|
143
147
|
async cancel(taskId) {
|
|
144
|
-
await this.
|
|
148
|
+
await this.reload();
|
|
145
149
|
const index = this.tasks.findIndex((item) => item.id === taskId);
|
|
146
150
|
if (index === -1) return null;
|
|
147
151
|
const [task] = this.tasks.splice(index, 1);
|
|
@@ -150,7 +154,7 @@ export class TaskStore {
|
|
|
150
154
|
}
|
|
151
155
|
|
|
152
156
|
async cancelAll(filter = {}) {
|
|
153
|
-
await this.
|
|
157
|
+
await this.reload();
|
|
154
158
|
const removed = [];
|
|
155
159
|
this.tasks = this.tasks.filter((task) => {
|
|
156
160
|
if (filter.chatId && task.payload?.chatId !== filter.chatId) return true;
|
|
@@ -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 { getToolStateDir } from "../../runtime/paths.js";
|
|
7
|
+
|
|
8
|
+
export function daemonPaths(toolName) {
|
|
9
|
+
const root = getToolStateDir(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;
|