arisa 3.0.1 → 3.0.3
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 +6 -1
- package/README.md +32 -13
- package/cli/openai-transcribe/index.js +8 -4
- package/cli/openai-tts/index.js +9 -4
- package/package.json +1 -1
- package/src/core/agent/agent-manager.js +22 -48
- package/src/core/agent/pi-runtime.js +61 -0
- package/src/core/artifacts/artifact-store.js +1 -3
- package/src/core/tools/tool-config.js +35 -0
- package/src/core/tools/tool-registry.js +14 -16
- package/src/runtime/bootstrap.js +41 -83
- package/src/runtime/paths.js +39 -0
- package/src/transport/telegram/bot.js +1 -1
- package/src/transport/telegram/media.js +11 -20
package/AGENTS.md
CHANGED
|
@@ -93,10 +93,15 @@ Prefer responses like:
|
|
|
93
93
|
|
|
94
94
|
For example, if the user asks for live weather and no weather tool exists, the correct attitude is to propose building a weather tool for the bot rather than only saying real-time access is unavailable.
|
|
95
95
|
|
|
96
|
+
When creating or editing tools, follow the shared path helpers in `src/runtime/paths.js` and `src/core/tools/tool-config.js`:
|
|
97
|
+
- config in `~/.arisa/tools/<tool>/config.js`
|
|
98
|
+
- temp/runtime files in `~/.arisa/tmp/tools/<tool>/`
|
|
99
|
+
- durable generated files should become artifacts in `~/.arisa/artifacts/`
|
|
100
|
+
|
|
96
101
|
Consult the local skill for that workflow when building new tools.
|
|
97
102
|
|
|
98
103
|
## Safety
|
|
99
104
|
- Do not install or run arbitrary tools outside registered `cli/*` manifests in V1.
|
|
100
105
|
- Prefer tool manifests and CLI help over assumptions.
|
|
101
|
-
- Keep tool configs inside
|
|
106
|
+
- Keep tool configs inside `~/.arisa/tools/<tool>/config.js`.
|
|
102
107
|
- Be proactive about extending capabilities, but do it through the project's tool architecture, not through ad hoc one-off behavior.
|
package/README.md
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
# Arisa
|
|
2
2
|
|
|
3
|
-
Arisa is a
|
|
3
|
+
Arisa is a personal Telegram assistant powered by Pi Agent.
|
|
4
|
+
|
|
5
|
+
## Origin
|
|
6
|
+
|
|
7
|
+
The initial inspiration was [OpenClaw](https://github.com/openclaw/openclaw). OpenClaw has interesting ideas but carries too much weight: when it generates tools they end up disorganized, and the overall framework feels overloaded for personal use.
|
|
8
|
+
|
|
9
|
+
The real heart of OpenClaw is Pi Agent — a [minimal terminal coding harness](https://www.youtube.com/watch?v=Dli5slNaJu0) that lets an AI agent reason and act with very little infrastructure. That part is genuinely good.
|
|
10
|
+
|
|
11
|
+
Telegram bots, on the other hand, work extremely well as a human interface. Simple, reliable, always in your pocket.
|
|
12
|
+
|
|
13
|
+
So Arisa keeps exactly those two things — Pi Agent and Telegram — and nothing more. No pre-loaded opinions about what the agent should do or which tools it should have. The idea is that the agent builds itself around the user, not the other way around.
|
|
4
14
|
|
|
5
15
|
It is designed around a simple idea:
|
|
6
16
|
|
|
@@ -10,7 +20,7 @@ It is designed around a simple idea:
|
|
|
10
20
|
- **capabilities live in isolated CLI tools**
|
|
11
21
|
- **tools can be chained through pipes**
|
|
12
22
|
|
|
13
|
-
|
|
23
|
+
If a capability does not exist yet, the system adds a new tool for it. The agent grows from real use, not from assumptions.
|
|
14
24
|
|
|
15
25
|
## Core concept
|
|
16
26
|
|
|
@@ -59,8 +69,13 @@ node index.js run --request-file <json>
|
|
|
59
69
|
```
|
|
60
70
|
|
|
61
71
|
### Configuration model
|
|
62
|
-
-
|
|
63
|
-
-
|
|
72
|
+
- all runtime state lives under `~/.arisa/`
|
|
73
|
+
- Telegram runtime config is stored in `~/.arisa/state/config.json`
|
|
74
|
+
- artifact index is stored in `~/.arisa/state/artifacts.json`
|
|
75
|
+
- incoming Telegram attachments are stored directly in `~/.arisa/artifacts/`
|
|
76
|
+
- tool-specific secrets/config live in `~/.arisa/tools/<tool>/config.js`
|
|
77
|
+
- tool runtime temp files and generated outputs live in `~/.arisa/tmp/tools/<tool>/`
|
|
78
|
+
- durable files should end up in `~/.arisa/artifacts/`
|
|
64
79
|
- Pi authentication can use either:
|
|
65
80
|
- an API key entered during bootstrap
|
|
66
81
|
- or Pi's existing OAuth login when supported, such as `openai-codex`
|
|
@@ -83,10 +98,11 @@ On first run, Arisa will:
|
|
|
83
98
|
|
|
84
99
|
1. ask for a Telegram bot token
|
|
85
100
|
2. ask for the maximum number of authorized chat ids
|
|
86
|
-
3. show
|
|
87
|
-
4.
|
|
88
|
-
5.
|
|
89
|
-
6.
|
|
101
|
+
3. show Pi providers discovered from Pi Agent's model registry
|
|
102
|
+
4. show the models available for the selected provider
|
|
103
|
+
5. resolve authentication for the selected Pi provider
|
|
104
|
+
6. validate that Pi Agent works
|
|
105
|
+
7. only then start listening to Telegram
|
|
90
106
|
|
|
91
107
|
Telegram bot tokens can be created with:
|
|
92
108
|
|
|
@@ -133,22 +149,25 @@ src/
|
|
|
133
149
|
cli/
|
|
134
150
|
openai-transcribe/
|
|
135
151
|
openai-tts/
|
|
136
|
-
|
|
152
|
+
~/.arisa/
|
|
137
153
|
state/
|
|
138
154
|
artifacts/
|
|
139
|
-
|
|
155
|
+
tools/
|
|
156
|
+
tmp/
|
|
140
157
|
```
|
|
141
158
|
|
|
142
159
|
## Philosophy
|
|
143
160
|
|
|
144
|
-
|
|
161
|
+
The agent should not come preloaded with vices or assumptions. It starts minimal and grows through real use — shaped by the user, not by the framework.
|
|
145
162
|
|
|
146
|
-
|
|
163
|
+
When a capability is missing:
|
|
147
164
|
|
|
148
165
|
1. check whether an existing tool can solve the task
|
|
149
|
-
2. if not,
|
|
166
|
+
2. if not, build the missing tool
|
|
150
167
|
3. keep the solution inside the tool architecture
|
|
151
168
|
|
|
169
|
+
No "I can't do that" when the thing is realistically buildable.
|
|
170
|
+
|
|
152
171
|
## Notes
|
|
153
172
|
|
|
154
173
|
- `AGENTS.md` defines the project-level behavioral rules for Pi Agent
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { readFile, stat } from "node:fs/promises";
|
|
2
|
-
import
|
|
3
|
-
import
|
|
2
|
+
import defaults from "./config.js";
|
|
3
|
+
import { loadToolConfig } from "../../src/core/tools/tool-config.js";
|
|
4
|
+
import { getToolConfigPath } from "../../src/runtime/paths.js";
|
|
5
|
+
|
|
6
|
+
const toolName = "openai-transcribe";
|
|
7
|
+
const config = await loadToolConfig(toolName, defaults);
|
|
4
8
|
|
|
5
9
|
function printHelp() {
|
|
6
|
-
console.log(`openai-transcribe\n\nUso:\n node index.js --help\n node index.js run --request-file <json>\n\nInput esperado:\n {\n \"artifact\": { \"path\": \"/abs/audio.ogg\", \"mimeType\": \"audio/ogg\" },\n \"args\": {}\n }\n\nConfig en
|
|
10
|
+
console.log(`openai-transcribe\n\nUso:\n node index.js --help\n node index.js run --request-file <json>\n\nInput esperado:\n {\n \"artifact\": { \"path\": \"/abs/audio.ogg\", \"mimeType\": \"audio/ogg\" },\n \"args\": {}\n }\n\nConfig en ${getToolConfigPath(toolName)}:\n OPENAI_API_KEY\n MODEL\n`);
|
|
7
11
|
}
|
|
8
12
|
|
|
9
13
|
async function run(requestFile) {
|
|
10
14
|
if (!config.OPENAI_API_KEY) {
|
|
11
|
-
console.log(JSON.stringify({ ok: false, missingConfig: ["OPENAI_API_KEY"], configPath:
|
|
15
|
+
console.log(JSON.stringify({ ok: false, missingConfig: ["OPENAI_API_KEY"], configPath: getToolConfigPath(toolName) }));
|
|
12
16
|
return;
|
|
13
17
|
}
|
|
14
18
|
|
package/cli/openai-tts/index.js
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import
|
|
3
|
+
import defaults from "./config.js";
|
|
4
|
+
import { loadToolConfig } from "../../src/core/tools/tool-config.js";
|
|
5
|
+
import { getToolConfigPath, getToolOutDir } from "../../src/runtime/paths.js";
|
|
6
|
+
|
|
7
|
+
const toolName = "openai-tts";
|
|
8
|
+
const config = await loadToolConfig(toolName, defaults);
|
|
4
9
|
|
|
5
10
|
function printHelp() {
|
|
6
|
-
console.log(`openai-tts\n\nUso:\n node index.js --help\n node index.js run --request-file <json>\n\nInput esperado:\n {\n \"text\": \"hola\",\n \"artifact\": { \"text\": \"hola\" },\n \"args\": { \"voice\": \"alloy\" }\n }\n\nConfig en
|
|
11
|
+
console.log(`openai-tts\n\nUso:\n node index.js --help\n node index.js run --request-file <json>\n\nInput esperado:\n {\n \"text\": \"hola\",\n \"artifact\": { \"text\": \"hola\" },\n \"args\": { \"voice\": \"alloy\" }\n }\n\nConfig en ${getToolConfigPath(toolName)}:\n OPENAI_API_KEY\n MODEL\n VOICE\n`);
|
|
7
12
|
}
|
|
8
13
|
|
|
9
14
|
async function run(requestFile) {
|
|
10
15
|
if (!config.OPENAI_API_KEY) {
|
|
11
|
-
console.log(JSON.stringify({ ok: false, missingConfig: ["OPENAI_API_KEY"], configPath:
|
|
16
|
+
console.log(JSON.stringify({ ok: false, missingConfig: ["OPENAI_API_KEY"], configPath: getToolConfigPath(toolName) }));
|
|
12
17
|
return;
|
|
13
18
|
}
|
|
14
19
|
|
|
@@ -39,7 +44,7 @@ async function run(requestFile) {
|
|
|
39
44
|
return;
|
|
40
45
|
}
|
|
41
46
|
|
|
42
|
-
const outDir =
|
|
47
|
+
const outDir = getToolOutDir(toolName);
|
|
43
48
|
await mkdir(outDir, { recursive: true });
|
|
44
49
|
const filePath = path.join(outDir, `speech-${Date.now()}.mp3`);
|
|
45
50
|
const buffer = Buffer.from(await response.arrayBuffer());
|
package/package.json
CHANGED
|
@@ -1,32 +1,11 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { mkdir } from "node:fs/promises";
|
|
3
|
-
import {
|
|
2
|
+
import { mkdir, unlink } from "node:fs/promises";
|
|
3
|
+
import { createAgentSession, SessionManager, defineTool } from "@mariozechner/pi-coding-agent";
|
|
4
4
|
import { Type } from "@sinclair/typebox";
|
|
5
|
+
import { createPiRuntime, hasProviderAuth } from "./pi-runtime.js";
|
|
5
6
|
|
|
6
7
|
const agentDir = path.resolve("data/pi-agent");
|
|
7
8
|
|
|
8
|
-
function getOAuthProviderForModelProvider(provider) {
|
|
9
|
-
if (provider === "openai-codex") return "openai-codex";
|
|
10
|
-
if (provider === "anthropic") return "anthropic";
|
|
11
|
-
if (provider === "google") return "google-gemini-cli";
|
|
12
|
-
if (provider === "google-antigravity") return "google-antigravity";
|
|
13
|
-
if (provider === "github-copilot") return "github-copilot";
|
|
14
|
-
return provider;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function normalizePiConfig(pi) {
|
|
18
|
-
const provider = pi.provider === "codex" ? "openai" : pi.provider;
|
|
19
|
-
let model = pi.model;
|
|
20
|
-
if (pi.provider === "codex") {
|
|
21
|
-
if (model === "5.4") model = "gpt-5.4";
|
|
22
|
-
else if (model === "5.4-mini") model = "gpt-5.4-mini";
|
|
23
|
-
else if (model === "5.4-nano") model = "gpt-5.4-nano";
|
|
24
|
-
else if (model === "5.4-pro") model = "gpt-5.4-pro";
|
|
25
|
-
else if (!model.startsWith("gpt-")) model = `gpt-${model}`;
|
|
26
|
-
}
|
|
27
|
-
return { provider, model };
|
|
28
|
-
}
|
|
29
|
-
|
|
30
9
|
export class AgentManager {
|
|
31
10
|
constructor({ config, artifactStore, toolRegistry }) {
|
|
32
11
|
this.config = config;
|
|
@@ -41,20 +20,16 @@ export class AgentManager {
|
|
|
41
20
|
}
|
|
42
21
|
|
|
43
22
|
async validatePiAgent() {
|
|
44
|
-
const authStorage =
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const modelRegistry = ModelRegistry.create(authStorage);
|
|
51
|
-
const model = modelRegistry.find(normalized.provider, normalized.model);
|
|
23
|
+
const { authStorage, modelRegistry } = createPiRuntime({
|
|
24
|
+
provider: this.config.pi.provider,
|
|
25
|
+
apiKey: this.config.pi.apiKey
|
|
26
|
+
});
|
|
27
|
+
const model = modelRegistry.find(this.config.pi.provider, this.config.pi.model);
|
|
52
28
|
if (!model) {
|
|
53
|
-
throw new Error(`Model not found: ${this.config.pi.provider}/${this.config.pi.model}
|
|
29
|
+
throw new Error(`Model not found: ${this.config.pi.provider}/${this.config.pi.model}`);
|
|
54
30
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
throw new Error(`No auth found for ${normalized.provider}. Provide a Pi API key in bootstrap, or authenticate with the internal /login flow during bootstrap.`);
|
|
31
|
+
if (!this.config.pi.apiKey && !hasProviderAuth(this.config.pi.provider, { authStorage, modelRegistry })) {
|
|
32
|
+
throw new Error(`No auth found for ${this.config.pi.provider}. Provide a Pi API key in bootstrap, or authenticate with Pi login for this provider during bootstrap.`);
|
|
58
33
|
}
|
|
59
34
|
|
|
60
35
|
const { session } = await createAgentSession({
|
|
@@ -70,17 +45,14 @@ export class AgentManager {
|
|
|
70
45
|
if (this.sessions.has(chatId)) return this.sessions.get(chatId);
|
|
71
46
|
|
|
72
47
|
await mkdir(agentDir, { recursive: true });
|
|
73
|
-
const authStorage =
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const oauthProvider = getOAuthProviderForModelProvider(normalized.provider);
|
|
82
|
-
if (!this.config.pi.apiKey && !modelRegistry.hasConfiguredAuth(normalized.provider) && !authStorage.hasAuth(oauthProvider)) {
|
|
83
|
-
throw new Error(`No auth found for ${normalized.provider}. Re-run bootstrap and complete login for this provider before Telegram starts.`);
|
|
48
|
+
const { authStorage, modelRegistry } = createPiRuntime({
|
|
49
|
+
provider: this.config.pi.provider,
|
|
50
|
+
apiKey: this.config.pi.apiKey
|
|
51
|
+
});
|
|
52
|
+
const model = modelRegistry.find(this.config.pi.provider, this.config.pi.model);
|
|
53
|
+
if (!model) throw new Error(`Model not found: ${this.config.pi.provider}/${this.config.pi.model}`);
|
|
54
|
+
if (!this.config.pi.apiKey && !hasProviderAuth(this.config.pi.provider, { authStorage, modelRegistry })) {
|
|
55
|
+
throw new Error(`No auth found for ${this.config.pi.provider}. Re-run bootstrap and complete login for this provider before Telegram starts.`);
|
|
84
56
|
}
|
|
85
57
|
|
|
86
58
|
const cwd = path.resolve("data/chats", String(chatId));
|
|
@@ -131,7 +103,7 @@ export class AgentManager {
|
|
|
131
103
|
defineTool({
|
|
132
104
|
name: "set_tool_config",
|
|
133
105
|
label: "Set tool config",
|
|
134
|
-
description: "Write a value into
|
|
106
|
+
description: "Write a value into ~/.arisa/tools/<tool>/config.js.",
|
|
135
107
|
parameters: Type.Object({ name: Type.String(), field: Type.String(), value: Type.String() }),
|
|
136
108
|
execute: async (_id, params) => {
|
|
137
109
|
await this.toolRegistry.load();
|
|
@@ -186,6 +158,7 @@ export class AgentManager {
|
|
|
186
158
|
metadata: { tool: params.name }
|
|
187
159
|
});
|
|
188
160
|
result.output.artifactId = generated.id;
|
|
161
|
+
await unlink(result.output.filePath).catch(() => {});
|
|
189
162
|
}
|
|
190
163
|
|
|
191
164
|
return {
|
|
@@ -210,6 +183,7 @@ export class AgentManager {
|
|
|
210
183
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
|
|
211
184
|
}
|
|
212
185
|
await telegram.sendAudio(result.output.filePath, params.text);
|
|
186
|
+
await unlink(result.output.filePath).catch(() => {});
|
|
213
187
|
return { content: [{ type: "text", text: "Audio enviado por Telegram." }], details: result };
|
|
214
188
|
}
|
|
215
189
|
})
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
function compareText(a, b) {
|
|
4
|
+
return a.localeCompare(b, undefined, { sensitivity: "base", numeric: true });
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function createPiRuntime({ provider, apiKey } = {}) {
|
|
8
|
+
const authStorage = AuthStorage.create();
|
|
9
|
+
if (provider && apiKey) {
|
|
10
|
+
authStorage.setRuntimeApiKey(provider, apiKey);
|
|
11
|
+
}
|
|
12
|
+
const modelRegistry = ModelRegistry.create(authStorage);
|
|
13
|
+
const oauthProviders = authStorage.getOAuthProviders();
|
|
14
|
+
return { authStorage, modelRegistry, oauthProviders };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function hasProviderAuth(provider, { authStorage, modelRegistry }) {
|
|
18
|
+
return modelRegistry.hasConfiguredAuth(provider) || authStorage.hasAuth(provider);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function supportsProviderOAuth(provider, { oauthProviders }) {
|
|
22
|
+
return oauthProviders.some((item) => item.id === provider);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function listPiProviders(runtime = createPiRuntime()) {
|
|
26
|
+
const { modelRegistry, oauthProviders } = runtime;
|
|
27
|
+
const allModels = modelRegistry.getAll();
|
|
28
|
+
const oauthIds = new Set(oauthProviders.map((item) => item.id));
|
|
29
|
+
const counts = new Map();
|
|
30
|
+
for (const model of allModels) {
|
|
31
|
+
counts.set(model.provider, (counts.get(model.provider) || 0) + 1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return [...counts.keys()]
|
|
35
|
+
.map((provider) => ({
|
|
36
|
+
provider,
|
|
37
|
+
authConfigured: hasProviderAuth(provider, runtime),
|
|
38
|
+
supportsOAuth: oauthIds.has(provider),
|
|
39
|
+
modelCount: counts.get(provider) || 0
|
|
40
|
+
}))
|
|
41
|
+
.sort((a, b) => {
|
|
42
|
+
if (a.authConfigured !== b.authConfigured) return a.authConfigured ? -1 : 1;
|
|
43
|
+
if (a.supportsOAuth !== b.supportsOAuth) return a.supportsOAuth ? -1 : 1;
|
|
44
|
+
return compareText(a.provider, b.provider);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function listProviderModels(provider, runtime = createPiRuntime()) {
|
|
49
|
+
return runtime.modelRegistry
|
|
50
|
+
.getAll()
|
|
51
|
+
.filter((model) => model.provider === provider)
|
|
52
|
+
.sort((a, b) => compareText(a.name || a.id, b.name || b.id));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function findPiModel({ provider, model, apiKey } = {}) {
|
|
56
|
+
const runtime = createPiRuntime({ provider, apiKey });
|
|
57
|
+
return {
|
|
58
|
+
...runtime,
|
|
59
|
+
model: provider && model ? runtime.modelRegistry.find(provider, model) : null
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -1,9 +1,7 @@
|
|
|
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
|
-
|
|
5
|
-
const rootDir = path.resolve("data/artifacts");
|
|
6
|
-
const indexFile = path.resolve("data/state/artifacts.json");
|
|
4
|
+
import { artifactsDir as rootDir, artifactsIndexFile as indexFile } from "../../runtime/paths.js";
|
|
7
5
|
|
|
8
6
|
async function loadIndex() {
|
|
9
7
|
try {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { getToolConfigPath } from "../../runtime/paths.js";
|
|
4
|
+
|
|
5
|
+
export function parseConfigModule(source) {
|
|
6
|
+
const normalized = source.replace(/^export\s+default/, "return");
|
|
7
|
+
return new Function(normalized)();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function serializeConfigModule(config) {
|
|
11
|
+
const lines = Object.entries(config).map(([key, value]) => ` ${key}: ${JSON.stringify(value)}`);
|
|
12
|
+
return `export default {\n${lines.join(",\n")}\n};\n`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function readConfigModule(filePath, fallback = {}) {
|
|
16
|
+
try {
|
|
17
|
+
const source = await readFile(filePath, "utf8");
|
|
18
|
+
return parseConfigModule(source);
|
|
19
|
+
} catch {
|
|
20
|
+
return fallback;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function loadToolConfig(toolName, defaults = {}) {
|
|
25
|
+
const configPath = getToolConfigPath(toolName);
|
|
26
|
+
const stored = await readConfigModule(configPath, {});
|
|
27
|
+
return { ...defaults, ...stored };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function writeToolConfig(toolName, config) {
|
|
31
|
+
const configPath = getToolConfigPath(toolName);
|
|
32
|
+
await mkdir(path.dirname(configPath), { recursive: true });
|
|
33
|
+
await writeFile(configPath, serializeConfigModule(config), "utf8");
|
|
34
|
+
return configPath;
|
|
35
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { readdir, readFile,
|
|
1
|
+
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
|
+
import { getToolConfigPath, getToolTmpDir } from "../../runtime/paths.js";
|
|
5
|
+
import { loadToolConfig, parseConfigModule, writeToolConfig } from "./tool-config.js";
|
|
4
6
|
|
|
5
7
|
const cliRoot = path.resolve("cli");
|
|
6
8
|
|
|
@@ -15,16 +17,6 @@ function runProcess(command, args, options = {}) {
|
|
|
15
17
|
});
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
function parseConfigModule(source) {
|
|
19
|
-
const normalized = source.replace(/^export\s+default/, "return");
|
|
20
|
-
return new Function(normalized)();
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function serializeConfigModule(config) {
|
|
24
|
-
const lines = Object.entries(config).map(([key, value]) => ` ${key}: ${JSON.stringify(value)}`);
|
|
25
|
-
return `export default {\n${lines.join(",\n")}\n};\n`;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
20
|
export class ToolRegistry {
|
|
29
21
|
constructor() {
|
|
30
22
|
this.tools = new Map();
|
|
@@ -47,12 +39,15 @@ export class ToolRegistry {
|
|
|
47
39
|
try {
|
|
48
40
|
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
49
41
|
const configSource = await readFile(configPath, "utf8");
|
|
50
|
-
const
|
|
42
|
+
const defaults = parseConfigModule(configSource);
|
|
43
|
+
const config = await loadToolConfig(manifest.name, defaults);
|
|
51
44
|
this.tools.set(manifest.name, {
|
|
52
45
|
...manifest,
|
|
53
46
|
dir: toolDir,
|
|
54
47
|
entry: path.join(toolDir, manifest.entry || "index.js"),
|
|
55
|
-
configPath,
|
|
48
|
+
localConfigPath: configPath,
|
|
49
|
+
configPath: getToolConfigPath(manifest.name),
|
|
50
|
+
defaults,
|
|
56
51
|
config
|
|
57
52
|
});
|
|
58
53
|
} catch {
|
|
@@ -87,15 +82,18 @@ export class ToolRegistry {
|
|
|
87
82
|
if (!tool) throw new Error(`Tool not found: ${name}`);
|
|
88
83
|
const config = { ...(tool.config || {}) };
|
|
89
84
|
config[field] = value;
|
|
90
|
-
await
|
|
85
|
+
const configPath = await writeToolConfig(name, config);
|
|
91
86
|
tool.config = config;
|
|
92
|
-
|
|
87
|
+
tool.configPath = configPath;
|
|
88
|
+
return { ok: true, tool: name, field, configPath };
|
|
93
89
|
}
|
|
94
90
|
|
|
95
91
|
async run({ name, request }) {
|
|
96
92
|
const tool = this.get(name);
|
|
97
93
|
if (!tool) throw new Error(`Tool not found: ${name}`);
|
|
98
|
-
const
|
|
94
|
+
const tmpDir = getToolTmpDir(name);
|
|
95
|
+
await mkdir(tmpDir, { recursive: true });
|
|
96
|
+
const requestFile = path.join(tmpDir, `.request-${Date.now()}.json`);
|
|
99
97
|
await writeFile(requestFile, `${JSON.stringify(request, null, 2)}\n`, "utf8");
|
|
100
98
|
const result = await runProcess("node", [tool.entry, "run", "--request-file", requestFile], {
|
|
101
99
|
cwd: tool.dir,
|
package/src/runtime/bootstrap.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import path from "node:path";
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
3
2
|
import readline from "node:readline/promises";
|
|
4
3
|
import { stdin as input, stdout as output } from "node:process";
|
|
5
4
|
import { spawn } from "node:child_process";
|
|
6
|
-
import { AuthStorage
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const configFile = path.join(stateDir, "config.json");
|
|
5
|
+
import { AuthStorage } from "@mariozechner/pi-coding-agent";
|
|
6
|
+
import { createPiRuntime, hasProviderAuth, listPiProviders, listProviderModels, supportsProviderOAuth } from "../core/agent/pi-runtime.js";
|
|
7
|
+
import { configFile, ensureArisaHome } from "./paths.js";
|
|
10
8
|
|
|
11
9
|
async function exists(file) {
|
|
12
10
|
try {
|
|
@@ -17,49 +15,6 @@ async function exists(file) {
|
|
|
17
15
|
}
|
|
18
16
|
}
|
|
19
17
|
|
|
20
|
-
function getBootstrapModels() {
|
|
21
|
-
const authStorage = AuthStorage.create();
|
|
22
|
-
const modelRegistry = ModelRegistry.create(authStorage);
|
|
23
|
-
const preferred = [
|
|
24
|
-
["openai-codex", "gpt-5.4"],
|
|
25
|
-
["openai-codex", "gpt-5.4-mini"],
|
|
26
|
-
["openai", "gpt-4.1"],
|
|
27
|
-
["anthropic", "claude-sonnet-4-6"],
|
|
28
|
-
["anthropic", "claude-opus-4-6"],
|
|
29
|
-
["google", "gemini-3.1-pro-preview"],
|
|
30
|
-
];
|
|
31
|
-
|
|
32
|
-
const models = preferred
|
|
33
|
-
.map(([provider, model]) => modelRegistry.find(provider, model))
|
|
34
|
-
.filter(Boolean)
|
|
35
|
-
.map((model) => ({ provider: model.provider, id: model.id, label: `${model.provider}/${model.id}` }));
|
|
36
|
-
|
|
37
|
-
if (!models.length) {
|
|
38
|
-
return modelRegistry.getAll().slice(0, 10).map((model) => ({
|
|
39
|
-
provider: model.provider,
|
|
40
|
-
id: model.id,
|
|
41
|
-
label: `${model.provider}/${model.id}`,
|
|
42
|
-
}));
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return models;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function getOAuthProviderForModelProvider(provider) {
|
|
49
|
-
if (provider === "openai-codex") return "openai-codex";
|
|
50
|
-
if (provider === "anthropic") return "anthropic";
|
|
51
|
-
if (provider === "google") return "google-gemini-cli";
|
|
52
|
-
if (provider === "google-antigravity") return "google-antigravity";
|
|
53
|
-
if (provider === "github-copilot") return "github-copilot";
|
|
54
|
-
return provider;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function hasExistingPiAuth(provider) {
|
|
58
|
-
const authStorage = AuthStorage.create();
|
|
59
|
-
const modelRegistry = ModelRegistry.create(authStorage);
|
|
60
|
-
const oauthProvider = getOAuthProviderForModelProvider(provider);
|
|
61
|
-
return modelRegistry.hasConfiguredAuth(provider) || authStorage.hasAuth(oauthProvider);
|
|
62
|
-
}
|
|
63
18
|
|
|
64
19
|
async function maybeOpenExternal(url) {
|
|
65
20
|
if (!url) return;
|
|
@@ -79,9 +34,7 @@ async function maybeOpenExternal(url) {
|
|
|
79
34
|
|
|
80
35
|
async function runInternalPiLogin(provider, rl) {
|
|
81
36
|
const authStorage = AuthStorage.create();
|
|
82
|
-
const
|
|
83
|
-
const available = authStorage.getOAuthProviders();
|
|
84
|
-
const selected = available.find((item) => item.id === oauthProvider);
|
|
37
|
+
const selected = authStorage.getOAuthProviders().find((item) => item.id === provider);
|
|
85
38
|
if (!selected) {
|
|
86
39
|
throw new Error(`No internal OAuth login flow is available for ${provider}.`);
|
|
87
40
|
}
|
|
@@ -93,7 +46,7 @@ async function runInternalPiLogin(provider, rl) {
|
|
|
93
46
|
manualCodeReject = reject;
|
|
94
47
|
});
|
|
95
48
|
|
|
96
|
-
await authStorage.login(
|
|
49
|
+
await authStorage.login(provider, {
|
|
97
50
|
onAuth: async ({ url, instructions }) => {
|
|
98
51
|
console.log(`${instructions || "Open this URL to continue authentication:"}\n${url}\n`);
|
|
99
52
|
await maybeOpenExternal(url);
|
|
@@ -127,7 +80,7 @@ async function runInternalPiLogin(provider, rl) {
|
|
|
127
80
|
}
|
|
128
81
|
|
|
129
82
|
export async function bootstrapIfNeeded({ force = false } = {}) {
|
|
130
|
-
await
|
|
83
|
+
await ensureArisaHome();
|
|
131
84
|
if (!force && await exists(configFile)) return;
|
|
132
85
|
|
|
133
86
|
const rl = readline.createInterface({ input, output });
|
|
@@ -137,58 +90,63 @@ export async function bootstrapIfNeeded({ force = false } = {}) {
|
|
|
137
90
|
return value || fallback;
|
|
138
91
|
};
|
|
139
92
|
|
|
140
|
-
const askYesNo = async (label, fallback = true) => {
|
|
141
|
-
const hint = fallback ? "Y/n" : "y/N";
|
|
142
|
-
const value = (await rl.question(`${label} (${hint}): `)).trim().toLowerCase();
|
|
143
|
-
if (!value) return fallback;
|
|
144
|
-
return value === "y" || value === "yes";
|
|
145
|
-
};
|
|
146
|
-
|
|
147
93
|
console.log("\n== Arisa bootstrap ==\n");
|
|
148
94
|
console.log("Telegram bot token tip: get it from https://t.me/BotFather");
|
|
149
95
|
const telegramApiKey = await ask("Telegram API key / bot token");
|
|
150
96
|
const telegramMaxChatIds = Number(await ask("Maximum authorized chat IDs", "1"));
|
|
151
97
|
|
|
152
|
-
const
|
|
153
|
-
|
|
98
|
+
const runtime = createPiRuntime();
|
|
99
|
+
const providers = listPiProviders(runtime);
|
|
100
|
+
console.log("\nAvailable Pi providers:");
|
|
101
|
+
providers.forEach((item, index) => {
|
|
102
|
+
const authLabel = item.authConfigured ? "auth: configured" : item.supportsOAuth ? "auth: login or API key" : "auth: API key";
|
|
103
|
+
console.log(`${index + 1}. ${item.provider} (${item.modelCount} models, ${authLabel})`);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const selectedProviderIndex = Number(await ask("Select Pi provider by number", "1"));
|
|
107
|
+
const selectedProvider = providers[Math.max(0, Math.min(providers.length - 1, selectedProviderIndex - 1))];
|
|
108
|
+
const models = listProviderModels(selectedProvider.provider, runtime);
|
|
109
|
+
console.log(`\nAvailable models for ${selectedProvider.provider}:`);
|
|
154
110
|
models.forEach((model, index) => {
|
|
155
|
-
const
|
|
156
|
-
const
|
|
157
|
-
console.log(`${index + 1}. ${
|
|
111
|
+
const capabilities = [model.reasoning ? "reasoning" : null, model.input?.includes("image") ? "image" : null].filter(Boolean).join(", ");
|
|
112
|
+
const suffix = capabilities ? ` [${capabilities}]` : "";
|
|
113
|
+
console.log(`${index + 1}. ${model.id}${suffix}`);
|
|
158
114
|
});
|
|
159
|
-
|
|
160
|
-
const
|
|
161
|
-
const
|
|
115
|
+
|
|
116
|
+
const selectedModelIndex = Number(await ask("Select Pi model by number", "1"));
|
|
117
|
+
const selectedModel = models[Math.max(0, Math.min(models.length - 1, selectedModelIndex - 1))];
|
|
118
|
+
const selectedAuthReady = hasProviderAuth(selectedProvider.provider, runtime);
|
|
119
|
+
const providerSupportsOAuth = supportsProviderOAuth(selectedProvider.provider, runtime);
|
|
162
120
|
console.log(`Selected model: ${selectedModel.provider}/${selectedModel.id}`);
|
|
163
|
-
console.log(`Existing Pi auth for ${
|
|
164
|
-
|
|
121
|
+
console.log(`Existing Pi auth for ${selectedProvider.provider}: ${selectedAuthReady ? "yes" : "no"}`);
|
|
122
|
+
if (providerSupportsOAuth) {
|
|
123
|
+
console.log("Pi auth tip: leaving the API key empty will start Pi's internal login flow for this provider.");
|
|
124
|
+
}
|
|
165
125
|
|
|
166
126
|
let piApiKey = "";
|
|
167
127
|
while (true) {
|
|
168
|
-
piApiKey = (await rl.question(`Pi API key for ${
|
|
128
|
+
piApiKey = (await rl.question(`Pi API key for ${selectedProvider.provider} (optional): `)).trim();
|
|
169
129
|
if (piApiKey) break;
|
|
170
|
-
if (
|
|
130
|
+
if (hasProviderAuth(selectedProvider.provider, createPiRuntime())) break;
|
|
171
131
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
if (!supportsInternalLogin) {
|
|
175
|
-
console.log(`No existing Pi auth found for ${selectedModel.provider}. This provider requires an API key.`);
|
|
132
|
+
if (!providerSupportsOAuth) {
|
|
133
|
+
console.log(`No existing Pi auth found for ${selectedProvider.provider}. This provider requires an API key.`);
|
|
176
134
|
continue;
|
|
177
135
|
}
|
|
178
136
|
|
|
179
|
-
console.log(`No existing Pi auth found for ${
|
|
137
|
+
console.log(`No existing Pi auth found for ${selectedProvider.provider}. Starting internal Pi login...`);
|
|
180
138
|
try {
|
|
181
|
-
await runInternalPiLogin(
|
|
139
|
+
await runInternalPiLogin(selectedProvider.provider, rl);
|
|
182
140
|
} catch (error) {
|
|
183
141
|
console.log(`Internal Pi login failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
184
142
|
}
|
|
185
143
|
|
|
186
|
-
if (
|
|
187
|
-
console.log(`Detected Pi auth for ${
|
|
144
|
+
if (hasProviderAuth(selectedProvider.provider, createPiRuntime())) {
|
|
145
|
+
console.log(`Detected Pi auth for ${selectedProvider.provider}. Continuing bootstrap.`);
|
|
188
146
|
break;
|
|
189
147
|
}
|
|
190
148
|
|
|
191
|
-
console.log(`Pi auth for ${
|
|
149
|
+
console.log(`Pi auth for ${selectedProvider.provider} is still missing after login.`);
|
|
192
150
|
}
|
|
193
151
|
|
|
194
152
|
const config = {
|
|
@@ -198,7 +156,7 @@ export async function bootstrapIfNeeded({ force = false } = {}) {
|
|
|
198
156
|
authorizedChatIds: []
|
|
199
157
|
},
|
|
200
158
|
pi: {
|
|
201
|
-
provider:
|
|
159
|
+
provider: selectedProvider.provider,
|
|
202
160
|
model: selectedModel.id,
|
|
203
161
|
apiKey: piApiKey
|
|
204
162
|
},
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
export const arisaHomeDir = path.join(os.homedir(), ".arisa");
|
|
6
|
+
export const stateDir = path.join(arisaHomeDir, "state");
|
|
7
|
+
export const configFile = path.join(stateDir, "config.json");
|
|
8
|
+
export const artifactsDir = path.join(arisaHomeDir, "artifacts");
|
|
9
|
+
export const artifactsIndexFile = path.join(stateDir, "artifacts.json");
|
|
10
|
+
export const toolsDir = path.join(arisaHomeDir, "tools");
|
|
11
|
+
export const tmpDir = path.join(arisaHomeDir, "tmp");
|
|
12
|
+
|
|
13
|
+
export function getToolDir(toolName) {
|
|
14
|
+
return path.join(toolsDir, toolName);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getToolConfigPath(toolName) {
|
|
18
|
+
return path.join(getToolDir(toolName), "config.js");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getToolRuntimeDir(toolName) {
|
|
22
|
+
return path.join(tmpDir, "tools", toolName);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function getToolOutDir(toolName) {
|
|
26
|
+
return path.join(getToolRuntimeDir(toolName), "out");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getToolTmpDir(toolName) {
|
|
30
|
+
return path.join(getToolRuntimeDir(toolName), "tmp");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function ensureArisaHome() {
|
|
34
|
+
await mkdir(stateDir, { recursive: true });
|
|
35
|
+
await mkdir(artifactsDir, { recursive: true });
|
|
36
|
+
await mkdir(toolsDir, { recursive: true });
|
|
37
|
+
await mkdir(tmpDir, { recursive: true });
|
|
38
|
+
}
|
|
39
|
+
|
|
@@ -97,7 +97,7 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, a
|
|
|
97
97
|
const { transcript, toolResult } = await maybeTranscribeIncomingAudio({ artifact, toolRegistry, artifactStore });
|
|
98
98
|
if (artifact?.kind === "audio" && !transcript) {
|
|
99
99
|
if (toolResult?.missingConfig?.includes("OPENAI_API_KEY")) {
|
|
100
|
-
throw new Error("I need the OpenAI API key for
|
|
100
|
+
throw new Error("I need the OpenAI API key for ~/.arisa/tools/openai-transcribe/config.js before I can transcribe incoming audio.");
|
|
101
101
|
}
|
|
102
102
|
throw new Error(toolResult?.error || "Audio transcription failed.");
|
|
103
103
|
}
|
|
@@ -1,18 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
|
|
4
|
-
const inboxDir = path.resolve("data/inbox");
|
|
5
|
-
|
|
6
|
-
async function downloadToFile(ctx, fileId, fileName) {
|
|
7
|
-
await mkdir(inboxDir, { recursive: true });
|
|
1
|
+
async function downloadToBuffer(ctx, fileId) {
|
|
8
2
|
const file = await ctx.api.getFile(fileId);
|
|
9
3
|
const url = `https://api.telegram.org/file/bot${ctx.api.token}/${file.file_path}`;
|
|
10
4
|
const response = await fetch(url);
|
|
11
5
|
if (!response.ok) throw new Error(`Download failed: ${response.status}`);
|
|
12
|
-
|
|
13
|
-
const target = path.join(inboxDir, fileName);
|
|
14
|
-
await writeFile(target, buffer);
|
|
15
|
-
return target;
|
|
6
|
+
return Buffer.from(await response.arrayBuffer());
|
|
16
7
|
}
|
|
17
8
|
|
|
18
9
|
export async function captureIncomingArtifact(ctx, artifactStore) {
|
|
@@ -25,10 +16,10 @@ export async function captureIncomingArtifact(ctx, artifactStore) {
|
|
|
25
16
|
|
|
26
17
|
if (ctx.message?.voice) {
|
|
27
18
|
const fileName = `${ctx.chat.id}-${ctx.msg.message_id}.ogg`;
|
|
28
|
-
const
|
|
29
|
-
return artifactStore.
|
|
30
|
-
originalPath: tempPath,
|
|
19
|
+
const content = await downloadToBuffer(ctx, ctx.message.voice.file_id);
|
|
20
|
+
return artifactStore.createGeneratedFile({
|
|
31
21
|
fileName,
|
|
22
|
+
content,
|
|
32
23
|
kind: "audio",
|
|
33
24
|
mimeType: "audio/ogg",
|
|
34
25
|
source: baseSource,
|
|
@@ -38,10 +29,10 @@ export async function captureIncomingArtifact(ctx, artifactStore) {
|
|
|
38
29
|
|
|
39
30
|
if (ctx.message?.document) {
|
|
40
31
|
const fileName = ctx.message.document.file_name || `${ctx.chat.id}-${ctx.msg.message_id}`;
|
|
41
|
-
const
|
|
42
|
-
return artifactStore.
|
|
43
|
-
originalPath: tempPath,
|
|
32
|
+
const content = await downloadToBuffer(ctx, ctx.message.document.file_id);
|
|
33
|
+
return artifactStore.createGeneratedFile({
|
|
44
34
|
fileName,
|
|
35
|
+
content,
|
|
45
36
|
kind: "document",
|
|
46
37
|
mimeType: ctx.message.document.mime_type || "application/octet-stream",
|
|
47
38
|
source: baseSource,
|
|
@@ -52,10 +43,10 @@ export async function captureIncomingArtifact(ctx, artifactStore) {
|
|
|
52
43
|
if (ctx.message?.photo?.length) {
|
|
53
44
|
const photo = ctx.message.photo.at(-1);
|
|
54
45
|
const fileName = `${ctx.chat.id}-${ctx.msg.message_id}.jpg`;
|
|
55
|
-
const
|
|
56
|
-
return artifactStore.
|
|
57
|
-
originalPath: tempPath,
|
|
46
|
+
const content = await downloadToBuffer(ctx, photo.file_id);
|
|
47
|
+
return artifactStore.createGeneratedFile({
|
|
58
48
|
fileName,
|
|
49
|
+
content,
|
|
59
50
|
kind: "image",
|
|
60
51
|
mimeType: "image/jpeg",
|
|
61
52
|
source: baseSource,
|