arisa 4.1.6 → 4.1.10
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 +5 -1
- package/README.md +36 -62
- package/package.json +2 -2
- package/src/core/agent/agent-manager.js +50 -52
- package/src/transport/telegram/bot.js +3 -3
package/AGENTS.md
CHANGED
|
@@ -121,7 +121,9 @@ To run a pipe, the agent should:
|
|
|
121
121
|
Example manual pipe:
|
|
122
122
|
1. `run_tool(openai-transcribe, artifact audio)`
|
|
123
123
|
2. take the returned text `artifactId`
|
|
124
|
-
3. `run_tool(openai-tts, artifact text)` or `
|
|
124
|
+
3. `run_tool(openai-tts, artifact text)` to generate audio, then `send_artifact(artifactId)` to deliver it (or `run_tool(openai-tts, artifact text, deliver: true)` to generate and send in one step)
|
|
125
|
+
|
|
126
|
+
Delivery is generic: any `run_tool` output that produces a file becomes an artifact, and `send_artifact(artifactId)` delivers it to the chat. The delivery method and caption are derived from the artifact (its `delivery` hint, `kind`, and stored name); internal local paths are never exposed. Tools declare their delivery intent by returning `delivery: { method }` in their output; they do not deliver to the transport themselves. As a shortcut, `run_tool` accepts `deliver: true` to generate and deliver in a single step; use it only when the user should receive the file now, not for intermediate pipe steps.
|
|
125
127
|
|
|
126
128
|
## Async event queue flow
|
|
127
129
|
Beyond time-based scheduling, tools can drive an event queue that wakes the agent only when there is something to evaluate. Everything goes through the `asyncTask` (single) or `asyncTasks` (array) field the pipeline already supports; no new Pi tools are needed. The 1s poller drains tasks by `kind`:
|
|
@@ -195,6 +197,8 @@ Tools may declare skills in `tool.manifest.json`:
|
|
|
195
197
|
|
|
196
198
|
The tool registry resolves these from the installed skills directory and injects them into the tool request as `skills`. `list_tools` exposes the hints and `tool_help` shows their resolution status. Skills are guidance for the agent/tool; they are not separate runtime dependencies.
|
|
197
199
|
|
|
200
|
+
If a tool executes deterministic logic over skill content, bundle the required skill assets inside the tool package and treat injected `skills` only as an optional override. A catalog tool must still work when the hinted skill is not installed on the target Arisa instance.
|
|
201
|
+
|
|
198
202
|
## Safety
|
|
199
203
|
- Prefer tool manifests and CLI help over assumptions.
|
|
200
204
|
- Keep config and runtime data inside the user runtime area through the path helpers above.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Arisa
|
|
2
2
|
|
|
3
|
-
[Arisa](https://arisa.sh) is a personal Telegram
|
|
3
|
+
[Arisa](https://arisa.sh) is a personal assistant you talk to through Telegram, powered by [Pi Agent](https://pi.dev).
|
|
4
4
|
|
|
5
5
|
## Origin
|
|
6
6
|
|
|
@@ -56,7 +56,7 @@ The result is a toolset shaped by how you actually use the assistant, not by def
|
|
|
56
56
|
|
|
57
57
|
### Telegram input
|
|
58
58
|
- text messages go directly to Pi Agent
|
|
59
|
-
- audio/voice messages are transcribed first when a transcription tool is installed, then passed to Pi Agent as text; otherwise
|
|
59
|
+
- audio/voice messages are transcribed first when a transcription tool is installed, then passed to Pi Agent as text; otherwise the agent is told transcription failed and can offer to install one
|
|
60
60
|
- media is stored as artifacts
|
|
61
61
|
|
|
62
62
|
### Tool model
|
|
@@ -82,17 +82,24 @@ That isolation is part of the architecture:
|
|
|
82
82
|
- one tool can be changed or replaced without tightly coupling the rest of the system
|
|
83
83
|
|
|
84
84
|
### Configuration model
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
-
|
|
89
|
-
-
|
|
90
|
-
-
|
|
91
|
-
- tool runtime
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
85
|
+
All runtime state lives under `~/.arisa/`, split between global state and per-chat state.
|
|
86
|
+
|
|
87
|
+
Global:
|
|
88
|
+
- runtime config is stored in `~/.arisa/state/config.json`
|
|
89
|
+
- the scheduled-task queue is stored in `~/.arisa/state/tasks.json`
|
|
90
|
+
- installed tools live under `~/.arisa/tools/<tool>/`, each with a default `config.js` template
|
|
91
|
+
- global tool runtime state (daemons, caches, temp) lives under `~/.arisa/state/tools/<tool>/`
|
|
92
|
+
|
|
93
|
+
Per chat (`~/.arisa/chats/<chatId>/`):
|
|
94
|
+
- artifact files are stored under `artifacts/`
|
|
95
|
+
- the artifact index is stored in `state/artifacts.json`
|
|
96
|
+
- the Pi session lives under `state/pi-sessions/`
|
|
97
|
+
- chat-scoped tool config overrides live in `config/tools/<tool>/config.js`
|
|
98
|
+
- ephemeral scratch lives under `tmp/`
|
|
99
|
+
|
|
100
|
+
Pi authentication can use either:
|
|
101
|
+
- an API key entered during bootstrap
|
|
102
|
+
- or Pi's existing OAuth login when supported, such as `openai-codex`
|
|
96
103
|
|
|
97
104
|
## Install globally
|
|
98
105
|
|
|
@@ -129,54 +136,15 @@ Notes:
|
|
|
129
136
|
|
|
130
137
|
## Bootstrap flow
|
|
131
138
|
|
|
132
|
-
On first run, Arisa
|
|
133
|
-
|
|
134
|
-
1. ask for a Telegram bot token
|
|
135
|
-
2. validate the token and ask whether to continue bootstrap from Telegram (default: yes)
|
|
136
|
-
3. when Telegram setup is selected, show a `https://t.me/<bot>?start=<setup-token>` link
|
|
137
|
-
4. authorize that Telegram chat and continue setup there: Pi provider, model, and Pi auth
|
|
138
|
-
5. ask from Telegram whether Arisa should keep running in background
|
|
139
|
-
6. validate that Pi Agent works
|
|
140
|
-
7. only then start listening to Telegram
|
|
141
|
-
|
|
142
|
-
Choosing `n` at the Telegram setup prompt keeps the previous CLI-only bootstrap flow.
|
|
143
|
-
|
|
144
|
-
Arisa does not run a persistent HTTP health server or Telegram webhook; Telegram uses long polling.
|
|
145
|
-
|
|
146
|
-
Telegram bot tokens can be created with:
|
|
147
|
-
|
|
148
|
-
- https://t.me/BotFather
|
|
149
|
-
|
|
150
|
-
## Using Pi authentication
|
|
139
|
+
On first run, Arisa walks you through three steps:
|
|
151
140
|
|
|
152
|
-
|
|
141
|
+
1. Give it a Telegram bot token (create one with [@BotFather](https://t.me/BotFather)) and Arisa validates it.
|
|
142
|
+
2. Continue setup in Telegram (the default): open the link to authorize your chat, then pick the Pi provider, model, and auth, and whether to keep running in the background.
|
|
143
|
+
3. Once Pi is working, Arisa starts listening.
|
|
153
144
|
|
|
154
|
-
|
|
145
|
+
## ChatGPT subscription is enough to run Arisa
|
|
155
146
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
allows Arisa to authenticate through Pi's Codex OAuth flow instead of requiring a normal OpenAI API key.
|
|
159
|
-
|
|
160
|
-
## Running model
|
|
161
|
-
|
|
162
|
-
Arisa keeps one Pi session per authorized Telegram chat.
|
|
163
|
-
|
|
164
|
-
If a message arrives while Pi Agent is still processing another one:
|
|
165
|
-
|
|
166
|
-
- the current message keeps running
|
|
167
|
-
- the new message is appended to a queued buffer
|
|
168
|
-
- additional incoming messages are concatenated to that same buffer
|
|
169
|
-
- once the current processing finishes, the buffered messages are sent together as the next prompt
|
|
170
|
-
|
|
171
|
-
Conceptually:
|
|
172
|
-
|
|
173
|
-
```txt
|
|
174
|
-
message 1 is processing
|
|
175
|
-
message 2 arrives -> queued
|
|
176
|
-
message 3 arrives -> appended to queued
|
|
177
|
-
message 1 finishes
|
|
178
|
-
queued batch is processed next
|
|
179
|
-
```
|
|
147
|
+
Allows Arisa to authenticate through Pi's Codex OAuth flow instead of requiring a normal OpenAI API key. This means a regular ChatGPT subscription is enough to run Arisa on GPT-5.5, which is powerful and a genuine pleasure to use.
|
|
180
148
|
|
|
181
149
|
## Project structure
|
|
182
150
|
|
|
@@ -186,10 +154,16 @@ src/
|
|
|
186
154
|
transport/ Telegram integration
|
|
187
155
|
core/ agent, tools, artifacts, config
|
|
188
156
|
~/.arisa/
|
|
189
|
-
state/
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
157
|
+
state/ global config, task queue, IPC socket
|
|
158
|
+
config.json
|
|
159
|
+
tasks.json
|
|
160
|
+
tools/<tool>/ global tool state (daemons, caches, tmp)
|
|
161
|
+
tools/<tool>/ installed tools (catalog, user-chosen, or agent-created)
|
|
162
|
+
chats/<chatId>/
|
|
163
|
+
artifacts/ per-chat artifact files
|
|
164
|
+
state/ artifact index + Pi session
|
|
165
|
+
config/tools/ chat-scoped tool config overrides
|
|
166
|
+
tmp/ ephemeral scratch
|
|
193
167
|
```
|
|
194
168
|
|
|
195
169
|
The official tool catalog lives in the repository under [`tools/`](https://github.com/clasen/Arisa/tree/main/tools) and is not part of the npm package.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arisa",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.10",
|
|
4
4
|
"description": "Telegram + Pi Agent modular assistant",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"bugs": {
|
|
36
36
|
"url": "https://github.com/clasen/Arisa/issues"
|
|
37
37
|
},
|
|
38
|
-
"homepage": "https://
|
|
38
|
+
"homepage": "https://arisa.sh",
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@earendil-works/pi-coding-agent": "^0.79.9",
|
|
41
41
|
"@sinclair/typebox": "^0.34.41",
|
|
@@ -19,7 +19,7 @@ const arisaToolNames = [
|
|
|
19
19
|
"list_scheduled_tasks",
|
|
20
20
|
"cancel_scheduled_task",
|
|
21
21
|
"cancel_all_scheduled_tasks",
|
|
22
|
-
"
|
|
22
|
+
"send_artifact"
|
|
23
23
|
];
|
|
24
24
|
|
|
25
25
|
function isLocalBaseUrl(value) {
|
|
@@ -55,30 +55,31 @@ async function promptAndThrowOnAssistantError(session, prompt) {
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
function
|
|
59
|
-
if (
|
|
60
|
-
|
|
61
|
-
if (pattern.endsWith("/*")) return mimeType.startsWith(`${pattern.slice(0, -2)}/`);
|
|
62
|
-
return false;
|
|
58
|
+
function inferDeliveryMethod(artifact) {
|
|
59
|
+
if (artifact.kind === "audio" || (artifact.mimeType || "").startsWith("audio/")) return "audio";
|
|
60
|
+
return "document";
|
|
63
61
|
}
|
|
64
62
|
|
|
65
|
-
function
|
|
66
|
-
|
|
63
|
+
function containsAbsolutePath(value) {
|
|
64
|
+
if (typeof value !== "string") return false;
|
|
65
|
+
return /(^|\s)(\/[^\s]|[A-Za-z]:[\\/])/.test(value);
|
|
67
66
|
}
|
|
68
67
|
|
|
69
|
-
function
|
|
70
|
-
|
|
68
|
+
function resolveMediaCaption({ caption, output, method }) {
|
|
69
|
+
if (caption && !containsAbsolutePath(caption)) return caption;
|
|
70
|
+
if (method === "document" && output?.fileName) return output.fileName;
|
|
71
|
+
if (output?.text && !containsAbsolutePath(output.text)) return output.text;
|
|
72
|
+
if (output?.fileName) return output.fileName;
|
|
73
|
+
return undefined;
|
|
71
74
|
}
|
|
72
75
|
|
|
73
|
-
function
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
.filter(toolProducesAudio);
|
|
81
|
-
return tools.find(looksLikeTextToSpeechTool) || tools[0] || null;
|
|
76
|
+
async function deliverArtifactToChat({ artifact, telegram, caption, method, logger }) {
|
|
77
|
+
const resolvedMethod = method || artifact.metadata?.delivery?.method || inferDeliveryMethod(artifact);
|
|
78
|
+
const fileName = path.basename(artifact.path);
|
|
79
|
+
const resolvedCaption = resolveMediaCaption({ caption, output: { fileName }, method: resolvedMethod });
|
|
80
|
+
logger?.log("agent", `deliver artifact ${artifact.id} as ${resolvedMethod}`);
|
|
81
|
+
await telegram.sendMedia(artifact.path, { method: resolvedMethod, caption: resolvedCaption, filename: fileName });
|
|
82
|
+
return { method: resolvedMethod, fileName, artifactId: artifact.id };
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
async function assertDirectory(dir, label) {
|
|
@@ -238,7 +239,7 @@ export class AgentManager {
|
|
|
238
239
|
kind: result.output.kind || "file",
|
|
239
240
|
mimeType: result.output.mimeType || "application/octet-stream",
|
|
240
241
|
source: { type: "tool", toolName: name },
|
|
241
|
-
metadata: { tool: name }
|
|
242
|
+
metadata: { tool: name, delivery: result.output.delivery }
|
|
242
243
|
});
|
|
243
244
|
result.output.artifactId = generated.id;
|
|
244
245
|
await unlink(result.output.filePath).catch(() => {});
|
|
@@ -337,12 +338,13 @@ export class AgentManager {
|
|
|
337
338
|
defineTool({
|
|
338
339
|
name: "run_tool",
|
|
339
340
|
label: "Run tool",
|
|
340
|
-
description: "Run a CLI tool using text input or an artifactId. Inspect the returned status/resolution fields. If a tool reports missing config, ask the user naturally, use set_tool_config, and retry.",
|
|
341
|
+
description: "Run a CLI tool using text input or an artifactId. Inspect the returned status/resolution fields. If a tool reports missing config, ask the user naturally, use set_tool_config, and retry. Set `deliver: true` to also send the generated file to the chat in one step (only when you want the user to receive it now, not for intermediate pipe steps).",
|
|
341
342
|
parameters: Type.Object({
|
|
342
343
|
name: Type.String(),
|
|
343
344
|
artifactId: Type.Optional(Type.String()),
|
|
344
345
|
text: Type.Optional(Type.String()),
|
|
345
|
-
args: Type.Optional(Type.Record(Type.String(), Type.String()))
|
|
346
|
+
args: Type.Optional(Type.Record(Type.String(), Type.String())),
|
|
347
|
+
deliver: Type.Optional(Type.Boolean())
|
|
346
348
|
}),
|
|
347
349
|
execute: async (_id, params) => {
|
|
348
350
|
let artifact = null;
|
|
@@ -362,6 +364,13 @@ export class AgentManager {
|
|
|
362
364
|
chatId
|
|
363
365
|
});
|
|
364
366
|
|
|
367
|
+
if (params.deliver && result.output?.artifactId) {
|
|
368
|
+
const generated = await chatArtifactStore.get(result.output.artifactId);
|
|
369
|
+
if (generated?.path) {
|
|
370
|
+
result.sent = await deliverArtifactToChat({ artifact: generated, telegram, logger: this.logger });
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
365
374
|
return {
|
|
366
375
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
367
376
|
details: result
|
|
@@ -417,12 +426,12 @@ export class AgentManager {
|
|
|
417
426
|
}
|
|
418
427
|
}),
|
|
419
428
|
defineTool({
|
|
420
|
-
name: "
|
|
421
|
-
label: "Send
|
|
422
|
-
description: "
|
|
429
|
+
name: "send_artifact",
|
|
430
|
+
label: "Send artifact",
|
|
431
|
+
description: "Deliver an existing chat artifact to the current Telegram chat. Pass the `artifactId` returned by run_tool or from an inbound file. The delivery method, caption, and filename are derived from the artifact (its delivery hint, kind, and stored name); internal local paths are never exposed. Set `caption` for a visible label, or `method` to override the delivery method. The artifact is not deleted.",
|
|
423
432
|
parameters: Type.Object({
|
|
424
|
-
|
|
425
|
-
|
|
433
|
+
artifactId: Type.String(),
|
|
434
|
+
caption: Type.Optional(Type.String()),
|
|
426
435
|
method: Type.Optional(Type.Union([
|
|
427
436
|
Type.Literal("voice"),
|
|
428
437
|
Type.Literal("audio"),
|
|
@@ -430,36 +439,25 @@ export class AgentManager {
|
|
|
430
439
|
]))
|
|
431
440
|
}),
|
|
432
441
|
execute: async (_id, params) => {
|
|
433
|
-
await
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
: selectMediaReplyTool(this.toolRegistry);
|
|
437
|
-
if (!selectedTool) {
|
|
438
|
-
const result = {
|
|
439
|
-
ok: false,
|
|
440
|
-
status: "failed",
|
|
441
|
-
error: params.toolName
|
|
442
|
-
? `Tool not found: ${params.toolName}`
|
|
443
|
-
: "No registered text-to-speech tool can generate an audio reply."
|
|
444
|
-
};
|
|
442
|
+
const artifact = await chatArtifactStore.get(params.artifactId);
|
|
443
|
+
if (!artifact) {
|
|
444
|
+
const result = { ok: false, status: "failed", error: `Artifact not found: ${params.artifactId}` };
|
|
445
445
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
|
|
446
446
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
const result = await this.toolRegistry.run({
|
|
450
|
-
name: toolName,
|
|
451
|
-
request: { text: params.text, args: {} },
|
|
452
|
-
chatId
|
|
453
|
-
});
|
|
454
|
-
if (!result.ok || !result.output?.filePath) {
|
|
447
|
+
if (!artifact.path) {
|
|
448
|
+
const result = { ok: false, status: "failed", error: `Artifact ${params.artifactId} has no file to deliver.` };
|
|
455
449
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
|
|
456
450
|
}
|
|
457
|
-
const
|
|
458
|
-
|
|
459
|
-
|
|
451
|
+
const sent = await deliverArtifactToChat({
|
|
452
|
+
artifact,
|
|
453
|
+
telegram,
|
|
454
|
+
caption: params.caption,
|
|
455
|
+
method: params.method,
|
|
456
|
+
logger: this.logger
|
|
457
|
+
});
|
|
460
458
|
return {
|
|
461
|
-
content: [{ type: "text", text: `Media sent to Telegram as ${method}.` }],
|
|
462
|
-
details: {
|
|
459
|
+
content: [{ type: "text", text: `Media sent to Telegram as ${sent.method}.` }],
|
|
460
|
+
details: { ok: true, sent }
|
|
463
461
|
};
|
|
464
462
|
}
|
|
465
463
|
})
|
|
@@ -77,7 +77,7 @@ function buildPrompt({ ctx, artifact, transcript, toolResult }) {
|
|
|
77
77
|
parts.push(`Use read/write/edit for file work in the active workspace, bash for bash-compatible commands, and system_shell for native system commands such as PowerShell on Windows.`);
|
|
78
78
|
parts.push(`If you need an Arisa modular CLI tool, use list_tools/tool_help/run_tool.`);
|
|
79
79
|
parts.push(`If a tool config is missing, ask the user naturally and then use set_tool_config.`);
|
|
80
|
-
parts.push(`
|
|
80
|
+
parts.push(`To deliver a file to the chat: run_tool with deliver:true to generate and send in one step, or send_artifact with an existing artifactId (e.g. an inbound file).`);
|
|
81
81
|
return parts.join("\n");
|
|
82
82
|
}
|
|
83
83
|
|
|
@@ -426,9 +426,9 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
426
426
|
|
|
427
427
|
function createTelegramSessionBridge(chatId) {
|
|
428
428
|
return {
|
|
429
|
-
sendMedia: async (filePath, { method = "audio", caption } = {}) => {
|
|
429
|
+
sendMedia: async (filePath, { method = "audio", caption, filename } = {}) => {
|
|
430
430
|
logger?.log("telegram", `sending ${method} reply for chat ${chatId}`);
|
|
431
|
-
const input = new InputFile(filePath);
|
|
431
|
+
const input = new InputFile(filePath, filename || undefined);
|
|
432
432
|
if (method === "voice") return bot.api.sendVoice(chatId, input, { caption });
|
|
433
433
|
if (method === "document") return bot.api.sendDocument(chatId, input, { caption });
|
|
434
434
|
return bot.api.sendAudio(chatId, input, { caption });
|