arisa 3.2.0 → 4.0.0
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 +33 -6
- package/README.md +33 -30
- package/package.json +9 -1
- package/src/core/agent/runtime-context.js +0 -2
- package/src/core/tools/tool-registry.js +35 -42
- package/src/index.js +3 -23
- package/src/runtime/paths.js +2 -0
- package/src/transport/telegram/bot.js +30 -8
- package/src/runtime/pi-package-manager.js +0 -49
- package/tools/openai-transcribe/config.js +0 -4
- package/tools/openai-transcribe/index.js +0 -108
- package/tools/openai-transcribe/package.json +0 -6
- package/tools/openai-transcribe/tool.manifest.json +0 -32
- package/tools/openai-tts/config.js +0 -5
- package/tools/openai-tts/index.js +0 -76
- package/tools/openai-tts/package.json +0 -6
- package/tools/openai-tts/tool.manifest.json +0 -20
- package/tools/schedule-agent-task/config.js +0 -1
- package/tools/schedule-agent-task/index.js +0 -68
- package/tools/schedule-agent-task/package.json +0 -6
- package/tools/schedule-agent-task/tool.manifest.json +0 -8
- package/tools/web-browser/config.js +0 -1
- package/tools/web-browser/index.js +0 -147
- package/tools/web-browser/package.json +0 -6
- package/tools/web-browser/tool.manifest.json +0 -8
package/AGENTS.md
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
- Incoming messages and files (text, voice, photo, document) and generated files become artifacts.
|
|
7
7
|
- A tool registry handles tool discovery, help lookup, config writes, and execution.
|
|
8
8
|
- Tools are isolated and each one has its own manifest, entrypoint, and config defaults.
|
|
9
|
+
- No tools ship with the core. All installed tools live under `~/.arisa/tools/<toolName>`; the install directory of Arisa (your working directory) contains only the core. Never create or install tools inside the install directory.
|
|
9
10
|
|
|
10
11
|
## Runtime directory rules
|
|
11
12
|
Do not build runtime paths by hand. Use `src/runtime/paths.js`:
|
|
@@ -56,6 +57,11 @@ Not every pipe should be decided by Pi Agent at runtime. Some pipes are part of
|
|
|
56
57
|
If inbound media was normalized before reasoning, Pi Agent should use the normalized result as the actual message content.
|
|
57
58
|
For example, if a voice note was transcribed, Pi Agent should answer the meaning of the transcript, not simply return the raw transcript unless the user explicitly asked for transcription.
|
|
58
59
|
|
|
60
|
+
## Telegram outbound replies
|
|
61
|
+
- Short textual replies are sent inline as a normal Telegram message.
|
|
62
|
+
- When a textual reply is too large to read comfortably inline, it is delivered as a generated Markdown artifact instead of a long inline message. The transport handles this automatically in `sendTextReply`: replies over the inline length limit become a `reply-<timestamp>.md` artifact sent as a document.
|
|
63
|
+
- This is a transport-layer concern. Pi Agent should write the full answer it wants to deliver and not pre-split or truncate it to fit the chat; the transport decides between inline text and a Markdown attachment.
|
|
64
|
+
|
|
59
65
|
## How to inspect CLI tools
|
|
60
66
|
Before using a tool, inspect its help:
|
|
61
67
|
- via the custom tool: `tool_help`
|
|
@@ -66,7 +72,7 @@ Every CLI must support (the entrypoint comes from `manifest.entry`, currently al
|
|
|
66
72
|
- `node index.js run --request-file <json>`
|
|
67
73
|
|
|
68
74
|
### Tools that need daemons
|
|
69
|
-
A
|
|
75
|
+
A tool may need a persistent process, for example to keep a browser session alive or a local model warm. The shared daemon runtime exists for this (the `whispermix-transcribe` catalog tool uses it).
|
|
70
76
|
When such a tool is built, implement it with the shared daemon runtime instead of custom ad hoc process management:
|
|
71
77
|
- use `src/core/tools/daemon-runtime.js`
|
|
72
78
|
- keep runtime files under the tool state directory (`~/.arisa/state/tools/<toolName>`)
|
|
@@ -98,7 +104,7 @@ Beyond time-based scheduling, tools can drive an event queue that wakes the agen
|
|
|
98
104
|
|
|
99
105
|
Tasks without a `runAt` fire immediately, so `agent_event` and the first `poll_tool` run on the next tick.
|
|
100
106
|
|
|
101
|
-
The poller dispatches all three kinds, but only `agent_task` is exercised by a
|
|
107
|
+
The poller dispatches all three kinds, but only `agent_task` is exercised by a catalog tool today (`schedule-agent-task`). The following is the pattern to follow when a checker tool is built:
|
|
102
108
|
|
|
103
109
|
How a tool wires its own polling:
|
|
104
110
|
1. From any tool `run`, start the poll by returning an `asyncTask` (or several in `asyncTasks`):
|
|
@@ -118,23 +124,45 @@ If `run_tool` returns `missingConfig`, the agent should:
|
|
|
118
124
|
|
|
119
125
|
Do not assume a rigid question/answer protocol. Continue the conversation naturally and infer the config value from the user reply when possible.
|
|
120
126
|
|
|
127
|
+
## Official tool catalog
|
|
128
|
+
The official tool catalog lives at `https://github.com/clasen/Arisa/tree/main/tools`. Each subdirectory is an installable tool following the standard tool contract.
|
|
129
|
+
|
|
130
|
+
When a capability is missing, check the catalog before building anything:
|
|
131
|
+
1. List the catalog: `curl -s https://api.github.com/repos/clasen/Arisa/contents/tools`.
|
|
132
|
+
2. Read the manifest of any candidate: `https://raw.githubusercontent.com/clasen/Arisa/main/tools/<name>/tool.manifest.json` (the `description`, `input`, and `output` fields tell you whether it solves the need). Also read the catalog `README.md` to check the tool's install footprint.
|
|
133
|
+
3. If a catalog tool solves the problem, decide by install footprint (the `Install footprint` column in the catalog `README.md`):
|
|
134
|
+
- **Low footprint** (no extra npm dependencies, no external binaries): install it and resolve the user's request in the same turn, without asking first. Favor autonomy here.
|
|
135
|
+
- **Medium/High footprint** (heavy dependency trees, external binaries, or interactive setup such as a login): propose it to the user, say which tool it is and what it does, and wait for their confirmation in the chat before installing.
|
|
136
|
+
- A missing config secret (for example an API key) is handled by the missing-config flow and is not, on its own, a reason to ask before installing.
|
|
137
|
+
4. Only when nothing in the catalog fits, fall back to creating a new tool.
|
|
138
|
+
|
|
139
|
+
### Installing a tool
|
|
140
|
+
After deciding to install (low footprint), after the user confirms, or when the user directly asks to install a tool from any source they choose:
|
|
141
|
+
1. Download the tool directory into `~/.arisa/tools/<name>`. For the official catalog, clone shallowly and copy the subdirectory, for example:
|
|
142
|
+
`git clone --depth 1 https://github.com/clasen/Arisa /tmp/arisa-catalog && cp -R /tmp/arisa-catalog/tools/<name> ~/.arisa/tools/<name>`.
|
|
143
|
+
2. Install dependencies inside the tool directory (`pnpm install`, fall back to `npm install`).
|
|
144
|
+
3. Run it through `run_tool`; the registry picks up new tools automatically and exposes the Arisa package root through `ARISA_PACKAGE_DIR`. If it returns `missingConfig`, follow the missing config flow.
|
|
145
|
+
|
|
121
146
|
## Tool creation
|
|
122
147
|
Reason in terms of capabilities, not tool names.
|
|
123
148
|
|
|
124
149
|
When the user asks for something new:
|
|
125
150
|
1. check whether an existing registered tool can already satisfy the task
|
|
126
151
|
2. also check whether the task can be satisfied indirectly through an existing capability
|
|
127
|
-
3.
|
|
152
|
+
3. check whether a tool in the official catalog satisfies the task and propose installing it
|
|
153
|
+
4. only propose creating a new tool when the needed capability is truly missing
|
|
128
154
|
|
|
129
155
|
Do not stop at "I cannot do that" when the task is realistically implementable through the tool architecture.
|
|
130
156
|
The default attitude is:
|
|
131
157
|
- identify that no current tool satisfies the request
|
|
132
158
|
- state that the missing capability can be added
|
|
133
|
-
- propose or start creating the needed tool
|
|
159
|
+
- propose installing from the official catalog, or propose or start creating the needed tool
|
|
134
160
|
|
|
135
161
|
When creating or editing tools:
|
|
162
|
+
- always create tools under `~/.arisa/tools/<toolName>`, never inside the Arisa install directory
|
|
136
163
|
- use the path helpers in `src/runtime/paths.js`
|
|
137
|
-
-
|
|
164
|
+
- import Arisa core helpers dynamically through `ARISA_PACKAGE_DIR` (for example `await importCore("core/tools/tool-result.js")`); never use `../../src/...` or rewritten absolute paths
|
|
165
|
+
- follow the tools in the official catalog as the reference pattern for new tools
|
|
138
166
|
- keep all help text, usage instructions, manifests, and user-facing operational strings in English
|
|
139
167
|
- follow the One Thing Rule: each function or method should do one thing well; if it mixes low-level operations with high-level policy, split it into smaller focused units
|
|
140
168
|
|
|
@@ -158,7 +186,6 @@ Tool dependencies are installed as part of building or running the tool, not del
|
|
|
158
186
|
- Do not ask the user to do it manually.
|
|
159
187
|
|
|
160
188
|
## Safety
|
|
161
|
-
- Do not install or run arbitrary tools outside registered tool manifests in V1.
|
|
162
189
|
- Prefer tool manifests and CLI help over assumptions.
|
|
163
190
|
- Keep tool config and runtime data inside the user runtime area.
|
|
164
191
|
- 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,10 +1,10 @@
|
|
|
1
1
|
# Arisa
|
|
2
2
|
|
|
3
|
-
Arisa is a personal Telegram assistant powered by [Pi Agent](https://pi.dev).
|
|
3
|
+
[Arisa](https://arisa.sh) is a personal Telegram assistant powered by [Pi Agent](https://pi.dev).
|
|
4
4
|
|
|
5
5
|
## Origin
|
|
6
6
|
|
|
7
|
-
The initial inspiration was
|
|
7
|
+
The initial inspiration was OpenClaw, which has interesting ideas but carries a lot of weight (about **85 MB**, **55 dependencies**) compared to Arisa (**37 kB**, **3 dependencies**): when it generates tools they end up disorganized, and the overall framework feels overloaded.
|
|
8
8
|
|
|
9
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
10
|
|
|
@@ -38,15 +38,31 @@ Arisa separates two different kinds of pipes:
|
|
|
38
38
|
|
|
39
39
|
This distinction is important. Some transformations belong to the transport/input layer, not to the agent's runtime decision making.
|
|
40
40
|
|
|
41
|
+
## Zero tools, assembled on demand
|
|
42
|
+
|
|
43
|
+
A fresh install ships with **zero tools**. The core is only Telegram transport, the Pi Agent reasoning loop, the artifact store, and the tool registry. Out of the box Arisa cannot transcribe audio, browse the web, or speak; it gains each capability only once a tool that provides it is installed.
|
|
44
|
+
|
|
45
|
+
Arisa assembles its own toolset from real use:
|
|
46
|
+
|
|
47
|
+
1. A request arrives that the current tools cannot satisfy.
|
|
48
|
+
2. Arisa checks the [official catalog](https://github.com/clasen/Arisa/tree/main/tools) for a tool that fits the need.
|
|
49
|
+
3. If one fits, it installs it: autonomously for low-footprint tools, or after asking you to confirm for heavier ones (extra dependencies, external binaries, or interactive setup such as a login).
|
|
50
|
+
4. If nothing fits, it builds a new tool for the missing capability.
|
|
51
|
+
5. The installed tool stays in `~/.arisa/tools/` and is reused from then on.
|
|
52
|
+
|
|
53
|
+
The result is a toolset shaped by how you actually use the assistant, not by defaults someone else chose. Two people running the same Arisa build can end up with completely different capabilities.
|
|
54
|
+
|
|
41
55
|
## Current behavior
|
|
42
56
|
|
|
43
57
|
### Telegram input
|
|
44
58
|
- text messages go directly to Pi Agent
|
|
45
|
-
- audio/voice messages are transcribed first, then passed to Pi Agent as text
|
|
59
|
+
- audio/voice messages are transcribed first when a transcription tool is installed, then passed to Pi Agent as text; otherwise Arisa offers to install one
|
|
46
60
|
- media is stored as artifacts
|
|
47
61
|
|
|
48
62
|
### Tool model
|
|
49
|
-
|
|
63
|
+
No tools ship with the core. All installed tools live under `~/.arisa/tools/<tool-name>`, whether they come from the [official catalog](https://github.com/clasen/Arisa/tree/main/tools), from another source the user chooses, or are created by the agent itself.
|
|
64
|
+
|
|
65
|
+
When the agent needs a capability it does not have, it checks the official catalog first. Low-footprint tools (no extra dependencies) are installed on the spot so the request is resolved in the same turn; heavier tools (extra dependencies, external binaries, or interactive setup) are proposed for you to confirm before anything is installed.
|
|
50
66
|
|
|
51
67
|
Each tool folder contains:
|
|
52
68
|
|
|
@@ -59,8 +75,7 @@ Each tool is isolated from the root project and from other tools.
|
|
|
59
75
|
That isolation is part of the architecture:
|
|
60
76
|
|
|
61
77
|
- each tool has its own folder
|
|
62
|
-
-
|
|
63
|
-
- user-created tools can live entirely inside `~/.arisa/tools/<tool>/`
|
|
78
|
+
- each tool has a local `config.js` for defaults/template values
|
|
64
79
|
- each tool can have its own dependencies
|
|
65
80
|
- one tool can be changed or replaced without tightly coupling the rest of the system
|
|
66
81
|
|
|
@@ -97,8 +112,6 @@ arisa start # start in background
|
|
|
97
112
|
arisa stop # stop background service
|
|
98
113
|
arisa status # show background service status
|
|
99
114
|
arisa flush # remove ~/.arisa
|
|
100
|
-
arisa install <source> # install a Pi package into Arisa's runtime
|
|
101
|
-
arisa remove <source> # remove a Pi package from Arisa's runtime
|
|
102
115
|
```
|
|
103
116
|
|
|
104
117
|
Runtime model override (current process only):
|
|
@@ -111,17 +124,6 @@ Notes:
|
|
|
111
124
|
|
|
112
125
|
- it only affects the current Arisa process and does not update `~/.arisa/state/config.json`
|
|
113
126
|
|
|
114
|
-
## Experimental features
|
|
115
|
-
|
|
116
|
-
### Pi Agent packages
|
|
117
|
-
|
|
118
|
-
Arisa can install **Pi Agent packages** from the public registry into your user runtime (`~/.arisa/`), using the same package manager as Pi Agent. Browse and discover packages at [pi.dev/packages](https://pi.dev/packages).
|
|
119
|
-
|
|
120
|
-
- `arisa install <source>` installs a package (by registry name or other source supported by Pi).
|
|
121
|
-
- `arisa remove <source>` removes a previously installed package.
|
|
122
|
-
|
|
123
|
-
Treat this as **experimental**: the registry, package formats, and install behavior follow Pi Agent and may change. Not every listed package is tailored to Arisa’s Telegram transport and artifact-based tools; prefer packages you understand and verify after install.
|
|
124
|
-
|
|
125
127
|
## Bootstrap flow
|
|
126
128
|
|
|
127
129
|
On first run, Arisa will:
|
|
@@ -203,16 +205,15 @@ src/
|
|
|
203
205
|
runtime/ bootstrap + app startup
|
|
204
206
|
transport/ Telegram integration
|
|
205
207
|
core/ agent, tools, artifacts, config
|
|
206
|
-
tools/
|
|
207
|
-
openai-transcribe/
|
|
208
|
-
openai-tts/
|
|
209
208
|
~/.arisa/
|
|
210
209
|
state/
|
|
211
210
|
artifacts/
|
|
212
|
-
tools/
|
|
211
|
+
tools/ installed tools (catalog, user-chosen, or agent-created)
|
|
213
212
|
tmp/
|
|
214
213
|
```
|
|
215
214
|
|
|
215
|
+
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.
|
|
216
|
+
|
|
216
217
|
## Philosophy
|
|
217
218
|
|
|
218
219
|
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.
|
|
@@ -221,9 +222,10 @@ For consistency, the entire Arisa codebase was built using Pi Agent itself, runn
|
|
|
221
222
|
|
|
222
223
|
When a capability is missing:
|
|
223
224
|
|
|
224
|
-
1. check whether an
|
|
225
|
-
2. if not,
|
|
226
|
-
3.
|
|
225
|
+
1. check whether an installed tool can solve the task
|
|
226
|
+
2. if not, check the official catalog and propose installing the matching tool
|
|
227
|
+
3. if nothing fits, build the missing tool
|
|
228
|
+
4. keep the solution inside the tool architecture
|
|
227
229
|
|
|
228
230
|
No "I can't do that" when the thing is realistically buildable.
|
|
229
231
|
|
|
@@ -235,14 +237,15 @@ No "I can't do that" when the thing is realistically buildable.
|
|
|
235
237
|
|
|
236
238
|
## Status
|
|
237
239
|
|
|
238
|
-
This is currently a functional V1
|
|
240
|
+
This is currently a functional V1. The core provides:
|
|
239
241
|
|
|
240
242
|
- Telegram transport
|
|
241
243
|
- Pi Agent integration
|
|
242
244
|
- artifact-based message handling
|
|
243
|
-
- isolated CLI
|
|
244
|
-
-
|
|
245
|
-
- text-to-speech replies
|
|
245
|
+
- the isolated CLI tool registry (starts empty)
|
|
246
|
+
- pre-reasoning and post-reasoning pipes
|
|
246
247
|
- queued follow-up message batching
|
|
247
248
|
|
|
249
|
+
Concrete capabilities such as audio transcription or text-to-speech are not built in; they are added as tools from the catalog (or built on demand) when a request needs them.
|
|
250
|
+
|
|
248
251
|
Future capabilities should be added as new tools and pipes, not as tightly coupled one-off code paths.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arisa",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "Telegram + Pi Agent modular assistant",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -28,6 +28,14 @@
|
|
|
28
28
|
],
|
|
29
29
|
"author": "Martin Clasen",
|
|
30
30
|
"license": "MIT",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/clasen/Arisa.git"
|
|
34
|
+
},
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/clasen/Arisa/issues"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/clasen/Arisa#readme",
|
|
31
39
|
"dependencies": {
|
|
32
40
|
"@mariozechner/pi-coding-agent": "^0.73.1",
|
|
33
41
|
"@sinclair/typebox": "^0.34.41",
|
|
@@ -2,13 +2,11 @@ import { fileURLToPath } from "node:url";
|
|
|
2
2
|
import { arisaHomeDir, chatsDir, stateDir, toolStateDir, toolsDir } from "../../runtime/paths.js";
|
|
3
3
|
|
|
4
4
|
export const arisaInstallDir = fileURLToPath(new URL("../../..", import.meta.url));
|
|
5
|
-
export const bundledToolsDir = fileURLToPath(new URL("../../../tools", import.meta.url));
|
|
6
5
|
|
|
7
6
|
export function buildAgentRuntimeContext() {
|
|
8
7
|
return [
|
|
9
8
|
`arisaHomeDir: ${arisaHomeDir}`,
|
|
10
9
|
`arisaInstallDir: ${arisaInstallDir}`,
|
|
11
|
-
`bundledToolsDir: ${bundledToolsDir}`,
|
|
12
10
|
`userToolsDir: ${toolsDir}`,
|
|
13
11
|
`toolStateDir: ${toolStateDir}`,
|
|
14
12
|
`chatsDir: ${chatsDir}`,
|
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
import { mkdir, readdir, readFile, rmdir, unlink, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
|
-
import {
|
|
5
|
-
import { getToolConfigPath, getToolTmpDir, getChatToolTmpDir, toolsDir as userToolsRoot } from "../../runtime/paths.js";
|
|
4
|
+
import { arisaPackageDir, getToolConfigPath, getToolTmpDir, getChatToolTmpDir, toolsDir as userToolsRoot } from "../../runtime/paths.js";
|
|
6
5
|
import { loadToolConfig, parseConfigModule, writeToolConfig } from "./tool-config.js";
|
|
7
6
|
import { normalizeToolResult } from "./tool-result.js";
|
|
8
7
|
import { SkillRegistry } from "../skills/skill-registry.js";
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
{ root: bundledToolsRoot, kind: "bundled" }
|
|
14
|
-
];
|
|
9
|
+
function toolEnv() {
|
|
10
|
+
return { ...process.env, ARISA_PACKAGE_DIR: arisaPackageDir };
|
|
11
|
+
}
|
|
15
12
|
|
|
16
13
|
function runProcess(command, args, options = {}) {
|
|
17
14
|
return new Promise((resolve) => {
|
|
@@ -34,41 +31,37 @@ export class ToolRegistry {
|
|
|
34
31
|
async load() {
|
|
35
32
|
this.tools.clear();
|
|
36
33
|
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
let entries = [];
|
|
35
|
+
try {
|
|
36
|
+
entries = await readdir(userToolsRoot, { withFileTypes: true });
|
|
37
|
+
} catch {
|
|
38
|
+
this.logger?.log("tools", `tools directory not found: ${userToolsRoot}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for (const entry of entries) {
|
|
42
|
+
if (!entry.isDirectory()) continue;
|
|
43
|
+
const toolDir = path.join(userToolsRoot, entry.name);
|
|
44
|
+
const manifestPath = path.join(toolDir, "tool.manifest.json");
|
|
45
|
+
const configPath = path.join(toolDir, "config.js");
|
|
39
46
|
try {
|
|
40
|
-
|
|
47
|
+
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
48
|
+
if (this.tools.has(manifest.name)) continue;
|
|
49
|
+
const configSource = await readFile(configPath, "utf8");
|
|
50
|
+
const defaults = parseConfigModule(configSource);
|
|
51
|
+
const config = await loadToolConfig(manifest.name, defaults);
|
|
52
|
+
const skillHints = this.skillRegistry.normalizeHints(manifest);
|
|
53
|
+
this.tools.set(manifest.name, {
|
|
54
|
+
...manifest,
|
|
55
|
+
skillHints,
|
|
56
|
+
dir: toolDir,
|
|
57
|
+
entry: path.join(toolDir, manifest.entry || "index.js"),
|
|
58
|
+
localConfigPath: configPath,
|
|
59
|
+
configPath: getToolConfigPath(manifest.name),
|
|
60
|
+
defaults,
|
|
61
|
+
config
|
|
62
|
+
});
|
|
41
63
|
} catch {
|
|
42
|
-
|
|
43
|
-
continue;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
for (const entry of entries) {
|
|
47
|
-
if (!entry.isDirectory()) continue;
|
|
48
|
-
const toolDir = path.join(root, entry.name);
|
|
49
|
-
const manifestPath = path.join(toolDir, "tool.manifest.json");
|
|
50
|
-
const configPath = path.join(toolDir, "config.js");
|
|
51
|
-
try {
|
|
52
|
-
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
53
|
-
if (this.tools.has(manifest.name)) continue;
|
|
54
|
-
const configSource = await readFile(configPath, "utf8");
|
|
55
|
-
const defaults = parseConfigModule(configSource);
|
|
56
|
-
const config = await loadToolConfig(manifest.name, defaults);
|
|
57
|
-
const skillHints = this.skillRegistry.normalizeHints(manifest);
|
|
58
|
-
this.tools.set(manifest.name, {
|
|
59
|
-
...manifest,
|
|
60
|
-
skillHints,
|
|
61
|
-
dir: toolDir,
|
|
62
|
-
entry: path.join(toolDir, manifest.entry || "index.js"),
|
|
63
|
-
localConfigPath: configPath,
|
|
64
|
-
configPath: getToolConfigPath(manifest.name),
|
|
65
|
-
defaults,
|
|
66
|
-
config,
|
|
67
|
-
sourceKind: kind
|
|
68
|
-
});
|
|
69
|
-
} catch {
|
|
70
|
-
// ignore invalid tool dirs in v1
|
|
71
|
-
}
|
|
64
|
+
// ignore invalid tool dirs in v1
|
|
72
65
|
}
|
|
73
66
|
}
|
|
74
67
|
|
|
@@ -93,7 +86,7 @@ export class ToolRegistry {
|
|
|
93
86
|
async help(name) {
|
|
94
87
|
const tool = this.get(name);
|
|
95
88
|
if (!tool) throw new Error(`Tool not found: ${name}`);
|
|
96
|
-
const result = await runProcess("node", [tool.entry, "--help"], { cwd: tool.dir, env:
|
|
89
|
+
const result = await runProcess("node", [tool.entry, "--help"], { cwd: tool.dir, env: toolEnv() });
|
|
97
90
|
const help = result.stdout || result.stderr;
|
|
98
91
|
const skills = await this.resolveSkills(name);
|
|
99
92
|
if (!skills.length) return help;
|
|
@@ -153,7 +146,7 @@ export class ToolRegistry {
|
|
|
153
146
|
await writeFile(requestFile, `${JSON.stringify(enrichedRequest, null, 2)}\n`, "utf8");
|
|
154
147
|
const result = await runProcess("node", [tool.entry, "run", "--request-file", requestFile], {
|
|
155
148
|
cwd: tool.dir,
|
|
156
|
-
env:
|
|
149
|
+
env: toolEnv()
|
|
157
150
|
});
|
|
158
151
|
await unlink(requestFile).catch(() => {});
|
|
159
152
|
await rmdir(tmpDir).catch(() => {});
|
package/src/index.js
CHANGED
|
@@ -6,7 +6,9 @@ import { createApp } from "./runtime/create-app.js";
|
|
|
6
6
|
import { createLogger } from "./runtime/logger.js";
|
|
7
7
|
import { getServiceStatus, registerServiceProcess, startService, stopService, unregisterServiceProcess } from "./runtime/service-manager.js";
|
|
8
8
|
import { flushArisaHome } from "./runtime/flush.js";
|
|
9
|
-
import {
|
|
9
|
+
import { arisaPackageDir } from "./runtime/paths.js";
|
|
10
|
+
|
|
11
|
+
process.env.ARISA_PACKAGE_DIR = arisaPackageDir;
|
|
10
12
|
|
|
11
13
|
const args = process.argv.slice(2);
|
|
12
14
|
const cli = parseCliArgs(args);
|
|
@@ -208,28 +210,6 @@ async function main() {
|
|
|
208
210
|
return;
|
|
209
211
|
}
|
|
210
212
|
|
|
211
|
-
if (command === "install") {
|
|
212
|
-
const source = cli.positionals[1];
|
|
213
|
-
if (!source) {
|
|
214
|
-
console.log("Usage: arisa install <pi-package-source>");
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
const result = await installPiPackage(source);
|
|
218
|
-
process.exitCode = result.ok ? 0 : result.code;
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
if (command === "remove") {
|
|
223
|
-
const source = cli.positionals[1];
|
|
224
|
-
if (!source) {
|
|
225
|
-
console.log("Usage: arisa remove <pi-package-source>");
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
const result = await removePiPackage(source);
|
|
229
|
-
process.exitCode = result.ok ? 0 : result.code;
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
213
|
await runForeground();
|
|
234
214
|
}
|
|
235
215
|
|
package/src/runtime/paths.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { mkdir } from "node:fs/promises";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
4
5
|
|
|
6
|
+
export const arisaPackageDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
5
7
|
export const arisaHomeDir = path.join(os.homedir(), ".arisa");
|
|
6
8
|
export const stateDir = path.join(arisaHomeDir, "state");
|
|
7
9
|
export const configFile = path.join(stateDir, "config.json");
|
|
@@ -3,10 +3,9 @@ import path from "node:path";
|
|
|
3
3
|
import { authorizeChat } from "./auth.js";
|
|
4
4
|
import { captureIncomingArtifact } from "./media.js";
|
|
5
5
|
import { renderTelegramHtml } from "./text-format.js";
|
|
6
|
-
import { withTimeout } from "../../core/agent/prompt-timeout.js";
|
|
7
6
|
import { normalizeArtifactForReasoning, shouldNormalizeArtifactToText } from "../../core/artifacts/normalize-for-reasoning.js";
|
|
8
7
|
|
|
9
|
-
const
|
|
8
|
+
const slowPromptNoticeMs = 300_000;
|
|
10
9
|
|
|
11
10
|
function quotedMessageSummary(message) {
|
|
12
11
|
if (!message) return [];
|
|
@@ -184,22 +183,38 @@ function sessionEventLogMessage(event) {
|
|
|
184
183
|
return "";
|
|
185
184
|
}
|
|
186
185
|
|
|
187
|
-
async function collectText(session, prompt, { logger, chatId } = {}) {
|
|
186
|
+
async function collectText(session, prompt, { logger, chatId, onSlowPrompt } = {}) {
|
|
188
187
|
let text = "";
|
|
188
|
+
let shouldSeparateAssistantMessage = false;
|
|
189
|
+
let slowPromptTimer = null;
|
|
189
190
|
const unsubscribe = session.subscribe((event) => {
|
|
191
|
+
if (event.type === "message_start" && event.message.role === "assistant") {
|
|
192
|
+
shouldSeparateAssistantMessage = text.trim().length > 0;
|
|
193
|
+
}
|
|
190
194
|
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
|
|
195
|
+
if (shouldSeparateAssistantMessage && event.assistantMessageEvent.delta) {
|
|
196
|
+
text += "\n\n";
|
|
197
|
+
shouldSeparateAssistantMessage = false;
|
|
198
|
+
}
|
|
191
199
|
text += event.assistantMessageEvent.delta;
|
|
192
200
|
}
|
|
193
201
|
const logMessage = sessionEventLogMessage(event);
|
|
194
202
|
if (logMessage) logger?.log("agent", `chat ${chatId} ${logMessage}`);
|
|
195
203
|
});
|
|
196
204
|
|
|
205
|
+
if (onSlowPrompt) {
|
|
206
|
+
slowPromptTimer = setTimeout(() => {
|
|
207
|
+
logger?.log("telegram", `prompt for chat ${chatId} is still running after ${slowPromptNoticeMs}ms`);
|
|
208
|
+
onSlowPrompt().catch((error) => {
|
|
209
|
+
logger?.error("telegram", `slow prompt notice failed for chat ${chatId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
210
|
+
});
|
|
211
|
+
}, slowPromptNoticeMs);
|
|
212
|
+
}
|
|
213
|
+
|
|
197
214
|
try {
|
|
198
|
-
await
|
|
199
|
-
timeoutMs: promptTimeoutMs,
|
|
200
|
-
label: `Telegram prompt for chat ${chatId}`
|
|
201
|
-
});
|
|
215
|
+
await session.prompt(prompt);
|
|
202
216
|
} finally {
|
|
217
|
+
if (slowPromptTimer) clearTimeout(slowPromptTimer);
|
|
203
218
|
unsubscribe();
|
|
204
219
|
}
|
|
205
220
|
|
|
@@ -295,7 +310,14 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
295
310
|
const { session } = await agentManager.getSessionContext(chatId, createTelegramSessionBridge(chatId));
|
|
296
311
|
let text = "";
|
|
297
312
|
try {
|
|
298
|
-
text = await collectText(session, prompt, {
|
|
313
|
+
text = await collectText(session, prompt, {
|
|
314
|
+
logger,
|
|
315
|
+
chatId,
|
|
316
|
+
onSlowPrompt: () => bot.api.sendMessage(
|
|
317
|
+
chatId,
|
|
318
|
+
"This is taking longer than 5 minutes, so I will keep the current session running instead of starting over. Send /new if you want to abandon it and start fresh."
|
|
319
|
+
)
|
|
320
|
+
});
|
|
299
321
|
} catch (error) {
|
|
300
322
|
agentManager.resetSession(chatId);
|
|
301
323
|
throw error;
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { DefaultPackageManager, SettingsManager } from "@mariozechner/pi-coding-agent";
|
|
2
|
-
import { arisaHomeDir } from "./paths.js";
|
|
3
|
-
|
|
4
|
-
function createPackageManager() {
|
|
5
|
-
const settingsManager = SettingsManager.create(arisaHomeDir, arisaHomeDir);
|
|
6
|
-
const packageManager = new DefaultPackageManager({
|
|
7
|
-
cwd: arisaHomeDir,
|
|
8
|
-
agentDir: arisaHomeDir,
|
|
9
|
-
settingsManager
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
packageManager.setProgressCallback((event) => {
|
|
13
|
-
if (event.type === "start") {
|
|
14
|
-
process.stdout.write(`${event.message}\n`);
|
|
15
|
-
}
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
return packageManager;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export async function installPiPackage(source) {
|
|
22
|
-
const packageManager = createPackageManager();
|
|
23
|
-
|
|
24
|
-
try {
|
|
25
|
-
await packageManager.installAndPersist(source, { local: false });
|
|
26
|
-
console.log(`Installed ${source}`);
|
|
27
|
-
return { ok: true, code: 0 };
|
|
28
|
-
} catch (error) {
|
|
29
|
-
console.error(error instanceof Error ? error.message : String(error));
|
|
30
|
-
return { ok: false, code: 1 };
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export async function removePiPackage(source) {
|
|
35
|
-
const packageManager = createPackageManager();
|
|
36
|
-
|
|
37
|
-
try {
|
|
38
|
-
const removed = await packageManager.removeAndPersist(source, { local: false });
|
|
39
|
-
if (!removed) {
|
|
40
|
-
console.error(`No matching package found for ${source}`);
|
|
41
|
-
return { ok: false, code: 1 };
|
|
42
|
-
}
|
|
43
|
-
console.log(`Removed ${source}`);
|
|
44
|
-
return { ok: true, code: 0 };
|
|
45
|
-
} catch (error) {
|
|
46
|
-
console.error(error instanceof Error ? error.message : String(error));
|
|
47
|
-
return { ok: false, code: 1 };
|
|
48
|
-
}
|
|
49
|
-
}
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import { readFile, stat } from "node:fs/promises";
|
|
3
|
-
import defaults from "./config.js";
|
|
4
|
-
import { loadToolConfig } from "../../src/core/tools/tool-config.js";
|
|
5
|
-
import { toolError, toolNeedsConfig, toolOk } from "../../src/core/tools/tool-result.js";
|
|
6
|
-
import { getToolConfigPath } from "../../src/runtime/paths.js";
|
|
7
|
-
|
|
8
|
-
const toolName = "openai-transcribe";
|
|
9
|
-
const config = await loadToolConfig(toolName, defaults);
|
|
10
|
-
|
|
11
|
-
const supportedUploadExtensions = new Set([
|
|
12
|
-
".flac",
|
|
13
|
-
".m4a",
|
|
14
|
-
".mp3",
|
|
15
|
-
".mp4",
|
|
16
|
-
".mpeg",
|
|
17
|
-
".mpga",
|
|
18
|
-
".ogg",
|
|
19
|
-
".wav",
|
|
20
|
-
".webm"
|
|
21
|
-
]);
|
|
22
|
-
|
|
23
|
-
const mimeUploadExtensions = new Map([
|
|
24
|
-
["audio/aac", ".m4a"],
|
|
25
|
-
["audio/flac", ".flac"],
|
|
26
|
-
["audio/m4a", ".m4a"],
|
|
27
|
-
["audio/mp3", ".mp3"],
|
|
28
|
-
["audio/mp4", ".m4a"],
|
|
29
|
-
["audio/mpeg", ".mp3"],
|
|
30
|
-
["audio/mpga", ".mpga"],
|
|
31
|
-
["audio/ogg", ".ogg"],
|
|
32
|
-
["audio/opus", ".ogg"],
|
|
33
|
-
["audio/wav", ".wav"],
|
|
34
|
-
["audio/wave", ".wav"],
|
|
35
|
-
["audio/webm", ".webm"],
|
|
36
|
-
["audio/x-m4a", ".m4a"],
|
|
37
|
-
["audio/x-wav", ".wav"],
|
|
38
|
-
["video/mp4", ".mp4"],
|
|
39
|
-
["video/webm", ".webm"]
|
|
40
|
-
]);
|
|
41
|
-
|
|
42
|
-
function baseMimeType(mimeType = "") {
|
|
43
|
-
return mimeType.split(";")[0].trim().toLowerCase();
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function uploadFileNameForArtifact(artifact) {
|
|
47
|
-
const currentName = path.basename(artifact.path);
|
|
48
|
-
const currentExtension = path.extname(currentName).toLowerCase();
|
|
49
|
-
if (supportedUploadExtensions.has(currentExtension)) return currentName;
|
|
50
|
-
|
|
51
|
-
const extension = mimeUploadExtensions.get(baseMimeType(artifact.mimeType));
|
|
52
|
-
if (!extension) return currentName;
|
|
53
|
-
|
|
54
|
-
const parsed = path.parse(currentName);
|
|
55
|
-
return `${parsed.name || "audio"}${extension}`;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function printHelp() {
|
|
59
|
-
console.log(`openai-transcribe\n\nUsage:\n node index.js --help\n node index.js run --request-file <json>\n\nExpected input:\n {\n "artifact": { "path": "/abs/media.ogg", "mimeType": "audio/ogg" },\n "args": {}\n }\n\nSupported upload formats include flac, mp3, mp4, mpeg, mpga, m4a, ogg, wav, and webm.\n\nConfig at ${getToolConfigPath(toolName)}:\n OPENAI_API_KEY\n MODEL\n`);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
async function run(requestFile) {
|
|
63
|
-
if (!config.OPENAI_API_KEY) {
|
|
64
|
-
console.log(JSON.stringify(toolNeedsConfig({
|
|
65
|
-
tool: toolName,
|
|
66
|
-
missingConfig: ["OPENAI_API_KEY"],
|
|
67
|
-
configPath: getToolConfigPath(toolName)
|
|
68
|
-
})));
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const request = JSON.parse(await readFile(requestFile, "utf8"));
|
|
73
|
-
const artifact = request.artifact;
|
|
74
|
-
if (!artifact?.path) {
|
|
75
|
-
console.log(JSON.stringify(toolError("artifact.path is required")));
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
await stat(artifact.path);
|
|
80
|
-
const form = new FormData();
|
|
81
|
-
const data = await readFile(artifact.path);
|
|
82
|
-
form.append("file", new Blob([data], { type: baseMimeType(artifact.mimeType) || "application/octet-stream" }), uploadFileNameForArtifact(artifact));
|
|
83
|
-
form.append("model", config.MODEL);
|
|
84
|
-
|
|
85
|
-
const response = await fetch("https://api.openai.com/v1/audio/transcriptions", {
|
|
86
|
-
method: "POST",
|
|
87
|
-
headers: { Authorization: `Bearer ${config.OPENAI_API_KEY}` },
|
|
88
|
-
body: form
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
const payload = await response.json();
|
|
92
|
-
if (!response.ok) {
|
|
93
|
-
console.log(JSON.stringify(toolError(payload.error?.message || "OpenAI transcription failed")));
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
console.log(JSON.stringify(toolOk({ text: payload.text || "" })));
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const args = process.argv.slice(2);
|
|
101
|
-
if (!args.length || args.includes("--help") || args[0] === "help") {
|
|
102
|
-
printHelp();
|
|
103
|
-
} else if (args[0] === "run") {
|
|
104
|
-
const fileIndex = args.indexOf("--request-file");
|
|
105
|
-
await run(args[fileIndex + 1]);
|
|
106
|
-
} else {
|
|
107
|
-
printHelp();
|
|
108
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "openai-transcribe",
|
|
3
|
-
"description": "Transcribe audio files and video audio tracks with OpenAI audio transcription API.",
|
|
4
|
-
"entry": "index.js",
|
|
5
|
-
"input": [
|
|
6
|
-
"audio/aac",
|
|
7
|
-
"audio/flac",
|
|
8
|
-
"audio/m4a",
|
|
9
|
-
"audio/mp3",
|
|
10
|
-
"audio/mp4",
|
|
11
|
-
"audio/mpeg",
|
|
12
|
-
"audio/mpga",
|
|
13
|
-
"audio/ogg",
|
|
14
|
-
"audio/opus",
|
|
15
|
-
"audio/wav",
|
|
16
|
-
"audio/wave",
|
|
17
|
-
"audio/webm",
|
|
18
|
-
"audio/x-m4a",
|
|
19
|
-
"audio/x-wav",
|
|
20
|
-
"video/mp4",
|
|
21
|
-
"video/webm"
|
|
22
|
-
],
|
|
23
|
-
"output": ["text/plain"],
|
|
24
|
-
"configSchema": {
|
|
25
|
-
"OPENAI_API_KEY": {
|
|
26
|
-
"type": "string",
|
|
27
|
-
"required": true,
|
|
28
|
-
"secret": true,
|
|
29
|
-
"prompt": "I need your OPENAI_API_KEY to transcribe audio."
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import defaults from "./config.js";
|
|
4
|
-
import { loadToolConfig } from "../../src/core/tools/tool-config.js";
|
|
5
|
-
import { toolError, toolNeedsConfig, toolOk } from "../../src/core/tools/tool-result.js";
|
|
6
|
-
import { getChatToolTmpDir, getToolConfigPath, getToolTmpDir } from "../../src/runtime/paths.js";
|
|
7
|
-
|
|
8
|
-
const toolName = "openai-tts";
|
|
9
|
-
const config = await loadToolConfig(toolName, defaults);
|
|
10
|
-
|
|
11
|
-
function printHelp() {
|
|
12
|
-
console.log(`openai-tts\n\nUsage:\n node index.js --help\n node index.js run --request-file <json>\n\nExpected input:\n {\n \"text\": \"hello\",\n \"artifact\": { \"text\": \"hello\" },\n \"args\": { \"voice\": \"alloy\" }\n }\n\nOutput:\n - generates OGG/Opus audio\n - suggests Telegram voice-note delivery via output.delivery.method = \"voice\"\n\nConfig at ${getToolConfigPath(toolName)}:\n OPENAI_API_KEY\n MODEL\n VOICE\n`);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
async function run(requestFile) {
|
|
16
|
-
if (!config.OPENAI_API_KEY) {
|
|
17
|
-
console.log(JSON.stringify(toolNeedsConfig({
|
|
18
|
-
tool: toolName,
|
|
19
|
-
missingConfig: ["OPENAI_API_KEY"],
|
|
20
|
-
configPath: getToolConfigPath(toolName)
|
|
21
|
-
})));
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const request = JSON.parse(await readFile(requestFile, "utf8"));
|
|
26
|
-
const inputText = request.text || request.artifact?.text;
|
|
27
|
-
if (!inputText) {
|
|
28
|
-
console.log(JSON.stringify(toolError("text or artifact.text is required")));
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const response = await fetch("https://api.openai.com/v1/audio/speech", {
|
|
33
|
-
method: "POST",
|
|
34
|
-
headers: {
|
|
35
|
-
Authorization: `Bearer ${config.OPENAI_API_KEY}`,
|
|
36
|
-
"Content-Type": "application/json"
|
|
37
|
-
},
|
|
38
|
-
body: JSON.stringify({
|
|
39
|
-
model: config.MODEL,
|
|
40
|
-
voice: request.args?.voice || config.VOICE,
|
|
41
|
-
input: inputText,
|
|
42
|
-
format: "opus"
|
|
43
|
-
})
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
if (!response.ok) {
|
|
47
|
-
const payload = await response.text();
|
|
48
|
-
console.log(JSON.stringify(toolError(payload)));
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const outDir = request.chatId != null
|
|
53
|
-
? getChatToolTmpDir(request.chatId, toolName)
|
|
54
|
-
: getToolTmpDir(toolName);
|
|
55
|
-
await mkdir(outDir, { recursive: true });
|
|
56
|
-
const filePath = path.join(outDir, `speech-${Date.now()}.ogg`);
|
|
57
|
-
const buffer = Buffer.from(await response.arrayBuffer());
|
|
58
|
-
await writeFile(filePath, buffer);
|
|
59
|
-
console.log(JSON.stringify(toolOk({
|
|
60
|
-
filePath,
|
|
61
|
-
fileName: path.basename(filePath),
|
|
62
|
-
mimeType: "audio/ogg",
|
|
63
|
-
kind: "audio",
|
|
64
|
-
delivery: { method: "voice" }
|
|
65
|
-
})));
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const args = process.argv.slice(2);
|
|
69
|
-
if (!args.length || args.includes("--help") || args[0] === "help") {
|
|
70
|
-
printHelp();
|
|
71
|
-
} else if (args[0] === "run") {
|
|
72
|
-
const fileIndex = args.indexOf("--request-file");
|
|
73
|
-
await run(args[fileIndex + 1]);
|
|
74
|
-
} else {
|
|
75
|
-
printHelp();
|
|
76
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "openai-tts",
|
|
3
|
-
"description": "Convert text into OGG/Opus speech audio using the OpenAI speech API.",
|
|
4
|
-
"entry": "index.js",
|
|
5
|
-
"input": ["text/plain"],
|
|
6
|
-
"output": ["audio/ogg"],
|
|
7
|
-
"configSchema": {
|
|
8
|
-
"OPENAI_API_KEY": {
|
|
9
|
-
"type": "string",
|
|
10
|
-
"required": true,
|
|
11
|
-
"secret": true,
|
|
12
|
-
"prompt": "I need your OPENAI_API_KEY to generate speech audio."
|
|
13
|
-
},
|
|
14
|
-
"VOICE": {
|
|
15
|
-
"type": "string",
|
|
16
|
-
"required": false,
|
|
17
|
-
"prompt": "Voice to use, for example alloy."
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export default {};
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { toolError, toolOk } from "../../src/core/tools/tool-result.js";
|
|
3
|
-
|
|
4
|
-
function printHelp() {
|
|
5
|
-
console.log(`schedule-agent-task\n\nUsage:\n node index.js --help\n node index.js run --request-file <json>\n\nExpected input:\n {\n "text": "tell me the temperature in Toronto",\n "artifact": { "text": "tell me the temperature in Toronto" },\n "args": {\n "prompt": "tell me the temperature in Toronto",\n "runAt": "2026-04-07T14:00:00.000Z",\n "delaySeconds": "30",\n "intervalSeconds": "3600"\n }\n }\n\nBehavior:\n - schedules a future agent task for the current chat\n - provide either args.runAt or args.delaySeconds\n - optional args.intervalSeconds makes the task recurring\n`);
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
function firstNonEmpty(...values) {
|
|
9
|
-
return values.find((value) => String(value || "").trim()) || "";
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function buildRunAt(args = {}) {
|
|
13
|
-
const runAtValue = firstNonEmpty(args.runAt, args.at, args.when);
|
|
14
|
-
if (runAtValue) {
|
|
15
|
-
const parsed = Date.parse(runAtValue);
|
|
16
|
-
if (Number.isNaN(parsed)) return "";
|
|
17
|
-
return new Date(parsed).toISOString();
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const delaySeconds = Number(firstNonEmpty(args.delaySeconds, args.delay, args.seconds));
|
|
21
|
-
if (Number.isFinite(delaySeconds) && delaySeconds > 0) {
|
|
22
|
-
return new Date(Date.now() + (delaySeconds * 1000)).toISOString();
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
return "";
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
async function run(requestFile) {
|
|
29
|
-
const request = JSON.parse(await readFile(requestFile, "utf8"));
|
|
30
|
-
const args = request.args || {};
|
|
31
|
-
const prompt = firstNonEmpty(args.prompt, args.message, args.task, request.text, request.artifact?.text);
|
|
32
|
-
const runAt = buildRunAt(args);
|
|
33
|
-
const intervalSeconds = Number(firstNonEmpty(args.intervalSeconds, args.interval, args.everySeconds));
|
|
34
|
-
|
|
35
|
-
if (!prompt.trim()) {
|
|
36
|
-
console.log(JSON.stringify(toolError("prompt/message/task, text, or artifact.text is required")));
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (!runAt) {
|
|
41
|
-
console.log(JSON.stringify(toolError("args.runAt/at/when or args.delaySeconds/delay/seconds is required")));
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const asyncTask = {
|
|
46
|
-
kind: "agent_task",
|
|
47
|
-
runAt,
|
|
48
|
-
payload: { prompt },
|
|
49
|
-
recurrence: Number.isFinite(intervalSeconds) && intervalSeconds > 0
|
|
50
|
-
? { type: "interval", everySeconds: intervalSeconds }
|
|
51
|
-
: null
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
console.log(JSON.stringify(toolOk({ runAt }, {
|
|
55
|
-
status: "scheduled",
|
|
56
|
-
asyncTask
|
|
57
|
-
})));
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const args = process.argv.slice(2);
|
|
61
|
-
if (!args.length || args.includes("--help") || args[0] === "help") {
|
|
62
|
-
printHelp();
|
|
63
|
-
} else if (args[0] === "run") {
|
|
64
|
-
const fileIndex = args.indexOf("--request-file");
|
|
65
|
-
await run(args[fileIndex + 1]);
|
|
66
|
-
} else {
|
|
67
|
-
printHelp();
|
|
68
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export default {};
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { toolError, toolOk } from "../../src/core/tools/tool-result.js";
|
|
3
|
-
|
|
4
|
-
function printHelp() {
|
|
5
|
-
console.log(`web-browser\n\nUsage:\n node index.js --help\n node index.js run --request-file <json>\n\nExpected input:\n {\n "text": "weather toronto" | "https://example.com",\n "artifact": { "text": "weather toronto" },\n "args": {\n "mode": "search" | "open",\n "url": "https://example.com",\n "maxResults": "5"\n }\n }\n\nBehavior:\n - If the input looks like a URL, open the page.\n - Otherwise, perform a web search.\n - When possible, opening pages uses r.jina.ai with a direct fetch fallback.\n`);
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
function decodeHtml(text = "") {
|
|
9
|
-
return text
|
|
10
|
-
.replace(/&/g, "&")
|
|
11
|
-
.replace(/"/g, '"')
|
|
12
|
-
.replace(/'/g, "'")
|
|
13
|
-
.replace(/</g, "<")
|
|
14
|
-
.replace(/>/g, ">")
|
|
15
|
-
.replace(/ /g, " ");
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function stripHtml(html = "") {
|
|
19
|
-
return decodeHtml(
|
|
20
|
-
html
|
|
21
|
-
.replace(/<script[\s\S]*?<\/script>/gi, " ")
|
|
22
|
-
.replace(/<style[\s\S]*?<\/style>/gi, " ")
|
|
23
|
-
.replace(/<[^>]+>/g, " ")
|
|
24
|
-
.replace(/\s+/g, " ")
|
|
25
|
-
).trim();
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function normalizeUrl(value = "") {
|
|
29
|
-
const text = value.trim();
|
|
30
|
-
if (!text) return "";
|
|
31
|
-
if (/^https?:\/\//i.test(text)) return text;
|
|
32
|
-
if (/^[\w.-]+\.[a-z]{2,}(\/|$)/i.test(text)) return `https://${text}`;
|
|
33
|
-
return "";
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function extractActualUrl(duckUrl) {
|
|
37
|
-
try {
|
|
38
|
-
const parsed = new URL(duckUrl.startsWith("//") ? `https:${duckUrl}` : duckUrl);
|
|
39
|
-
const uddg = parsed.searchParams.get("uddg");
|
|
40
|
-
return uddg ? decodeURIComponent(uddg) : parsed.toString();
|
|
41
|
-
} catch {
|
|
42
|
-
return duckUrl;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
async function fetchText(url) {
|
|
47
|
-
const response = await fetch(url, {
|
|
48
|
-
headers: {
|
|
49
|
-
"user-agent": "Mozilla/5.0",
|
|
50
|
-
"accept-language": "en-US,en;q=0.9"
|
|
51
|
-
},
|
|
52
|
-
redirect: "follow"
|
|
53
|
-
});
|
|
54
|
-
return { response, text: await response.text() };
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async function searchWeb(query, maxResults = 5) {
|
|
58
|
-
const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
|
|
59
|
-
const { response, text: html } = await fetchText(url);
|
|
60
|
-
if (!response.ok) throw new Error(`Search failed with status ${response.status}`);
|
|
61
|
-
|
|
62
|
-
const results = [];
|
|
63
|
-
const blocks = html.split(/<div class="result results_links[\s\S]*?web-result ">/i).slice(1);
|
|
64
|
-
for (const block of blocks) {
|
|
65
|
-
if (results.length >= maxResults) break;
|
|
66
|
-
const titleMatch = block.match(/<a[^>]*class="result__a"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/i);
|
|
67
|
-
if (!titleMatch) continue;
|
|
68
|
-
const snippetMatch = block.match(/<a[^>]*class="result__snippet"[^>]*>([\s\S]*?)<\/a>/i);
|
|
69
|
-
const displayUrlMatch = block.match(/<a[^>]*class="result__url"[^>]*>([\s\S]*?)<\/a>/i);
|
|
70
|
-
results.push({
|
|
71
|
-
title: stripHtml(titleMatch[2]),
|
|
72
|
-
url: extractActualUrl(titleMatch[1]),
|
|
73
|
-
snippet: stripHtml(snippetMatch?.[1] || ""),
|
|
74
|
-
displayUrl: stripHtml(displayUrlMatch?.[1] || "")
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (!results.length) {
|
|
79
|
-
return `Search: ${query}\n\nNo parseable results were found.`;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return [
|
|
83
|
-
`Search: ${query}`,
|
|
84
|
-
"",
|
|
85
|
-
...results.flatMap((item, index) => [
|
|
86
|
-
`${index + 1}. ${item.title}`,
|
|
87
|
-
`URL: ${item.url}`,
|
|
88
|
-
`Snippet: ${item.snippet}`,
|
|
89
|
-
item.displayUrl ? `Displayed: ${item.displayUrl}` : null,
|
|
90
|
-
""
|
|
91
|
-
].filter(Boolean))
|
|
92
|
-
].join("\n").trim();
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
async function openWebPage(inputUrl) {
|
|
96
|
-
const targetUrl = normalizeUrl(inputUrl);
|
|
97
|
-
if (!targetUrl) throw new Error("A valid URL is required");
|
|
98
|
-
|
|
99
|
-
const jinaUrl = `https://r.jina.ai/http://${targetUrl.replace(/^https?:\/\//i, "")}`;
|
|
100
|
-
let body = "";
|
|
101
|
-
let source = "jina-ai";
|
|
102
|
-
|
|
103
|
-
try {
|
|
104
|
-
const { response, text } = await fetchText(jinaUrl);
|
|
105
|
-
if (!response.ok) throw new Error(`r.jina.ai status ${response.status}`);
|
|
106
|
-
body = text.trim();
|
|
107
|
-
} catch {
|
|
108
|
-
const { response, text } = await fetchText(targetUrl);
|
|
109
|
-
if (!response.ok) throw new Error(`Open failed with status ${response.status}`);
|
|
110
|
-
body = stripHtml(text);
|
|
111
|
-
source = "direct-fetch";
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const shortened = body.length > 12000 ? `${body.slice(0, 12000)}\n\n[content truncated]` : body;
|
|
115
|
-
return [`Page: ${targetUrl}`, `Source: ${source}`, "", shortened].join("\n").trim();
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
async function run(requestFile) {
|
|
119
|
-
const request = JSON.parse(await readFile(requestFile, "utf8"));
|
|
120
|
-
const rawInput = request.args?.url || request.text || request.artifact?.text || "";
|
|
121
|
-
const mode = request.args?.mode || (normalizeUrl(rawInput) ? "open" : "search");
|
|
122
|
-
const maxResults = Number.parseInt(request.args?.maxResults || "5", 10);
|
|
123
|
-
|
|
124
|
-
if (!rawInput.trim()) {
|
|
125
|
-
console.log(JSON.stringify(toolError("text, artifact.text, or args.url is required")));
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
try {
|
|
130
|
-
const outputText = mode === "open"
|
|
131
|
-
? await openWebPage(rawInput)
|
|
132
|
-
: await searchWeb(rawInput, Number.isFinite(maxResults) ? maxResults : 5);
|
|
133
|
-
console.log(JSON.stringify(toolOk({ text: outputText })));
|
|
134
|
-
} catch (error) {
|
|
135
|
-
console.log(JSON.stringify(toolError(error.message || String(error))));
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const args = process.argv.slice(2);
|
|
140
|
-
if (!args.length || args.includes("--help") || args[0] === "help") {
|
|
141
|
-
printHelp();
|
|
142
|
-
} else if (args[0] === "run") {
|
|
143
|
-
const fileIndex = args.indexOf("--request-file");
|
|
144
|
-
await run(args[fileIndex + 1]);
|
|
145
|
-
} else {
|
|
146
|
-
printHelp();
|
|
147
|
-
}
|