arisa 3.0.10 → 3.0.11

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Arisa
2
2
 
3
- Arisa is a personal Telegram assistant powered by Pi Agent.
3
+ Arisa is a personal Telegram assistant powered by [Pi Agent](https://pi.dev).
4
4
 
5
5
  ## Origin
6
6
 
@@ -64,13 +64,6 @@ That isolation is part of the architecture:
64
64
  - each tool can have its own dependencies
65
65
  - one tool can be changed or replaced without tightly coupling the rest of the system
66
66
 
67
- Each tool must support:
68
-
69
- ```bash
70
- node index.js --help
71
- node index.js run --request-file <json>
72
- ```
73
-
74
67
  ### Configuration model
75
68
  - all runtime state lives under `~/.arisa/`
76
69
  - Telegram runtime config is stored in `~/.arisa/state/config.json`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arisa",
3
- "version": "3.0.10",
3
+ "version": "3.0.11",
4
4
  "description": "Telegram + Pi Agent modular assistant",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -27,6 +27,7 @@
27
27
  "zeroclaw",
28
28
  "jarvis",
29
29
  "AGENTS.md",
30
+ "pi.dev",
30
31
  "clasen"
31
32
  ],
32
33
  "author": "",
@@ -123,7 +123,7 @@ export class AgentManager {
123
123
  defineTool({
124
124
  name: "run_tool",
125
125
  label: "Run tool",
126
- description: "Run a CLI tool using text input or an artifactId. If config is missing, ask the user naturally and then use set_tool_config.",
126
+ 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.",
127
127
  parameters: Type.Object({
128
128
  name: Type.String(),
129
129
  artifactId: Type.Optional(Type.String()),
@@ -4,6 +4,7 @@ import { spawn } from "node:child_process";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { getToolConfigPath, getToolTmpDir, toolsDir as userToolsRoot } from "../../runtime/paths.js";
6
6
  import { loadToolConfig, parseConfigModule, writeToolConfig } from "./tool-config.js";
7
+ import { normalizeToolResult } from "./tool-result.js";
7
8
 
8
9
  const bundledToolsRoot = fileURLToPath(new URL("../../../tools", import.meta.url));
9
10
  const toolRoots = [
@@ -117,15 +118,16 @@ export class ToolRegistry {
117
118
  await unlink(requestFile).catch(() => {});
118
119
  try {
119
120
  const parsed = JSON.parse(result.stdout || result.stderr);
120
- this.logger?.log("tools", `${name} -> ${parsed.ok === false ? "error" : "ok"}`);
121
- return parsed;
121
+ const normalized = normalizeToolResult(name, parsed);
122
+ this.logger?.log("tools", `${name} -> ${normalized.ok === false ? normalized.status || "error" : "ok"}`);
123
+ return normalized;
122
124
  } catch {
123
- return {
125
+ return normalizeToolResult(name, {
124
126
  ok: false,
125
127
  error: `Invalid tool response for ${name}`,
126
128
  stdout: result.stdout,
127
129
  stderr: result.stderr
128
- };
130
+ });
129
131
  }
130
132
  }
131
133
  }
@@ -0,0 +1,71 @@
1
+ export function toolOk(output = {}, extra = {}) {
2
+ return { ok: true, output, ...extra };
3
+ }
4
+
5
+ export function toolError(error, extra = {}) {
6
+ return {
7
+ ok: false,
8
+ status: extra.status || "failed",
9
+ error,
10
+ ...extra
11
+ };
12
+ }
13
+
14
+ export function toolNeedsConfig({ tool, missingConfig = [], configPath, message } = {}) {
15
+ return {
16
+ ok: false,
17
+ status: "needs_config",
18
+ error: message || `Missing tool configuration${tool ? ` for ${tool}` : ""}.`,
19
+ missingConfig,
20
+ configPath,
21
+ resolution: {
22
+ type: "user_config_required",
23
+ tool,
24
+ missingConfig,
25
+ configPath
26
+ }
27
+ };
28
+ }
29
+
30
+ export function normalizeToolResult(name, result = {}) {
31
+ if (!result || typeof result !== "object") {
32
+ return toolError(`Invalid tool response for ${name}`);
33
+ }
34
+
35
+ if (result.ok === false && result.missingConfig?.length) {
36
+ return {
37
+ ...toolNeedsConfig({
38
+ tool: name,
39
+ missingConfig: result.missingConfig,
40
+ configPath: result.configPath,
41
+ message: result.error
42
+ }),
43
+ ...result,
44
+ status: result.status || "needs_config",
45
+ resolution: result.resolution || {
46
+ type: "user_config_required",
47
+ tool: name,
48
+ missingConfig: result.missingConfig,
49
+ configPath: result.configPath
50
+ }
51
+ };
52
+ }
53
+
54
+ if (result.ok === false) {
55
+ return {
56
+ ...toolError(result.error || `Tool failed: ${name}`),
57
+ ...result,
58
+ status: result.status || "failed"
59
+ };
60
+ }
61
+
62
+ if (result.ok === true) {
63
+ return {
64
+ ...toolOk(result.output || {}),
65
+ ...result,
66
+ status: result.status || "ok"
67
+ };
68
+ }
69
+
70
+ return toolError(`Invalid tool response for ${name}`, { rawResult: result });
71
+ }
@@ -10,7 +10,6 @@ export const serviceLogFile = path.join(stateDir, "arisa.log");
10
10
  export const artifactsDir = path.join(arisaHomeDir, "artifacts");
11
11
  export const artifactsIndexFile = path.join(stateDir, "artifacts.json");
12
12
  export const toolsDir = path.join(arisaHomeDir, "tools");
13
- export const tmpDir = path.join(arisaHomeDir, "tmp");
14
13
 
15
14
  export function getToolDir(toolName) {
16
15
  return path.join(toolsDir, toolName);
@@ -36,6 +35,5 @@ export async function ensureArisaHome() {
36
35
  await mkdir(stateDir, { recursive: true });
37
36
  await mkdir(artifactsDir, { recursive: true });
38
37
  await mkdir(toolsDir, { recursive: true });
39
- await mkdir(tmpDir, { recursive: true });
40
38
  }
41
39
 
@@ -2,7 +2,7 @@ import { Bot, InputFile } from "grammy";
2
2
  import path from "node:path";
3
3
  import { authorizeChat } from "./auth.js";
4
4
  import { captureIncomingArtifact } from "./media.js";
5
- import { renderTelegramHtml, splitTelegramText } from "./text-format.js";
5
+ import { renderTelegramHtml } from "./text-format.js";
6
6
 
7
7
  function quotedMessageSummary(message) {
8
8
  if (!message) return [];
@@ -32,13 +32,14 @@ function quotedMessageSummary(message) {
32
32
  return parts;
33
33
  }
34
34
 
35
- function buildPrompt({ ctx, artifact, transcript }) {
35
+ function buildPrompt({ ctx, artifact, transcript, toolResult }) {
36
36
  const parts = [
37
37
  `New Telegram message.`,
38
38
  `chatId: ${ctx.chat.id}`,
39
39
  `userId: ${ctx.from.id}`,
40
40
  `username: ${ctx.from.username || "(no username)"}`,
41
- `messageId: ${ctx.msg.message_id}`
41
+ `messageId: ${ctx.msg.message_id}`,
42
+ `preferredTelegramLanguageCode: ${ctx.from?.language_code || "unknown"}`
42
43
  ];
43
44
 
44
45
  if (ctx.message?.text) parts.push(`text: ${ctx.message.text}`);
@@ -52,6 +53,10 @@ function buildPrompt({ ctx, artifact, transcript }) {
52
53
  parts.push(`transcriptText: ${transcript.text}`);
53
54
  parts.push(`Important: the incoming audio has already been transcribed. Use the transcript as the user message content. Do not answer with a raw transcription unless the user explicitly asked for one.`);
54
55
  }
56
+ if (artifact?.kind === "audio" && !transcript && toolResult) {
57
+ parts.push(`audioNormalizationResult: ${JSON.stringify(toolResult)}`);
58
+ parts.push(`Important: pre-reasoning audio normalization could not be completed, so you do not have a transcript for this voice/audio message.`);
59
+ }
55
60
 
56
61
  parts.push(`If you need a CLI tool, use list_tools/tool_help/run_tool.`);
57
62
  parts.push(`If a tool config is missing, ask the user naturally and then use set_tool_config.`);
@@ -75,7 +80,7 @@ async function maybeTranscribeIncomingAudio({ artifact, toolRegistry, artifactSt
75
80
  }
76
81
 
77
82
  if (!result.output?.text) {
78
- return { transcript: null, toolResult: { ok: false, error: "Transcription returned no text." } };
83
+ return { transcript: null, toolResult: { ok: false, status: "failed", error: "Transcription returned no text." } };
79
84
  }
80
85
 
81
86
  const transcript = await artifactStore.createText({
@@ -139,18 +144,15 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, a
139
144
  const { transcript, toolResult } = await maybeTranscribeIncomingAudio({ artifact, toolRegistry, artifactStore });
140
145
  if (transcript) logger?.log("telegram", `audio transcribed to artifact ${transcript.id}`);
141
146
  if (artifact?.kind === "audio" && !transcript) {
142
- if (toolResult?.missingConfig?.includes("OPENAI_API_KEY")) {
143
- throw new Error("I need the OpenAI API key for ~/.arisa/tools/openai-transcribe/config.js before I can transcribe incoming audio.");
144
- }
145
- throw new Error(toolResult?.error || "Audio transcription failed.");
147
+ logger?.log("telegram", `audio normalization unavailable for chat ${ctx.chat.id}: ${toolResult?.error || toolResult?.missingConfig?.join(", ") || "unknown error"}`);
146
148
  }
147
- return buildPrompt({ ctx, artifact, transcript });
149
+ return buildPrompt({ ctx, artifact, transcript, toolResult });
148
150
  }
149
151
 
150
152
  async function sendTextReply({ sendText, sendDocument, chatId, text }) {
151
- const attachmentThreshold = 12000;
153
+ const maxInlineReplyLength = 3500;
152
154
 
153
- if (text.length > attachmentThreshold) {
155
+ if (text.length > maxInlineReplyLength) {
154
156
  logger?.log("telegram", `sending long reply as markdown attachment for chat ${chatId}`);
155
157
  const artifact = await artifactStore.createGeneratedFile({
156
158
  fileName: `reply-${Date.now()}.md`,
@@ -167,9 +169,7 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, a
167
169
  }
168
170
 
169
171
  logger?.log("telegram", `sending text reply for chat ${chatId}`);
170
- for (const chunk of splitTelegramText(text)) {
171
- await sendText(renderTelegramHtml(chunk), { parse_mode: "HTML" });
172
- }
172
+ await sendText(renderTelegramHtml(text), { parse_mode: "HTML" });
173
173
  }
174
174
 
175
175
  async function processPrompt(ctx, prompt) {
@@ -250,7 +250,7 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, a
250
250
  const chatState = getChatState(ctx.chat.id);
251
251
  chatState.processing = false;
252
252
  const message = error instanceof Error ? error.message : String(error);
253
- await ctx.reply(`Error: ${message}`);
253
+ await ctx.reply(message);
254
254
  }
255
255
  });
256
256
 
@@ -2,6 +2,7 @@ import path from "node:path";
2
2
  import { readFile, stat } from "node:fs/promises";
3
3
  import defaults from "./config.js";
4
4
  import { loadToolConfig } from "../../src/core/tools/tool-config.js";
5
+ import { toolError, toolNeedsConfig, toolOk } from "../../src/core/tools/tool-result.js";
5
6
  import { getToolConfigPath } from "../../src/runtime/paths.js";
6
7
 
7
8
  const toolName = "openai-transcribe";
@@ -13,14 +14,18 @@ function printHelp() {
13
14
 
14
15
  async function run(requestFile) {
15
16
  if (!config.OPENAI_API_KEY) {
16
- console.log(JSON.stringify({ ok: false, missingConfig: ["OPENAI_API_KEY"], configPath: getToolConfigPath(toolName) }));
17
+ console.log(JSON.stringify(toolNeedsConfig({
18
+ tool: toolName,
19
+ missingConfig: ["OPENAI_API_KEY"],
20
+ configPath: getToolConfigPath(toolName)
21
+ })));
17
22
  return;
18
23
  }
19
24
 
20
25
  const request = JSON.parse(await readFile(requestFile, "utf8"));
21
26
  const artifact = request.artifact;
22
27
  if (!artifact?.path) {
23
- console.log(JSON.stringify({ ok: false, error: "artifact.path is required" }));
28
+ console.log(JSON.stringify(toolError("artifact.path is required")));
24
29
  return;
25
30
  }
26
31
 
@@ -38,11 +43,11 @@ async function run(requestFile) {
38
43
 
39
44
  const payload = await response.json();
40
45
  if (!response.ok) {
41
- console.log(JSON.stringify({ ok: false, error: payload.error?.message || "OpenAI transcription failed" }));
46
+ console.log(JSON.stringify(toolError(payload.error?.message || "OpenAI transcription failed")));
42
47
  return;
43
48
  }
44
49
 
45
- console.log(JSON.stringify({ ok: true, output: { text: payload.text || "" } }));
50
+ console.log(JSON.stringify(toolOk({ text: payload.text || "" })));
46
51
  }
47
52
 
48
53
  const args = process.argv.slice(2);
@@ -2,6 +2,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import defaults from "./config.js";
4
4
  import { loadToolConfig } from "../../src/core/tools/tool-config.js";
5
+ import { toolError, toolNeedsConfig, toolOk } from "../../src/core/tools/tool-result.js";
5
6
  import { getToolConfigPath, getToolOutDir } from "../../src/runtime/paths.js";
6
7
 
7
8
  const toolName = "openai-tts";
@@ -13,14 +14,18 @@ function printHelp() {
13
14
 
14
15
  async function run(requestFile) {
15
16
  if (!config.OPENAI_API_KEY) {
16
- console.log(JSON.stringify({ ok: false, missingConfig: ["OPENAI_API_KEY"], configPath: getToolConfigPath(toolName) }));
17
+ console.log(JSON.stringify(toolNeedsConfig({
18
+ tool: toolName,
19
+ missingConfig: ["OPENAI_API_KEY"],
20
+ configPath: getToolConfigPath(toolName)
21
+ })));
17
22
  return;
18
23
  }
19
24
 
20
25
  const request = JSON.parse(await readFile(requestFile, "utf8"));
21
26
  const inputText = request.text || request.artifact?.text;
22
27
  if (!inputText) {
23
- console.log(JSON.stringify({ ok: false, error: "text or artifact.text is required" }));
28
+ console.log(JSON.stringify(toolError("text or artifact.text is required")));
24
29
  return;
25
30
  }
26
31
 
@@ -40,7 +45,7 @@ async function run(requestFile) {
40
45
 
41
46
  if (!response.ok) {
42
47
  const payload = await response.text();
43
- console.log(JSON.stringify({ ok: false, error: payload }));
48
+ console.log(JSON.stringify(toolError(payload)));
44
49
  return;
45
50
  }
46
51
 
@@ -49,16 +54,13 @@ async function run(requestFile) {
49
54
  const filePath = path.join(outDir, `speech-${Date.now()}.ogg`);
50
55
  const buffer = Buffer.from(await response.arrayBuffer());
51
56
  await writeFile(filePath, buffer);
52
- console.log(JSON.stringify({
53
- ok: true,
54
- output: {
55
- filePath,
56
- fileName: path.basename(filePath),
57
- mimeType: "audio/ogg",
58
- kind: "audio",
59
- delivery: { method: "voice" }
60
- }
61
- }));
57
+ console.log(JSON.stringify(toolOk({
58
+ filePath,
59
+ fileName: path.basename(filePath),
60
+ mimeType: "audio/ogg",
61
+ kind: "audio",
62
+ delivery: { method: "voice" }
63
+ })));
62
64
  }
63
65
 
64
66
  const args = process.argv.slice(2);
@@ -1,4 +1,5 @@
1
1
  import { readFile } from "node:fs/promises";
2
+ import { toolError, toolOk } from "../../src/core/tools/tool-result.js";
2
3
 
3
4
  function printHelp() {
4
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`);
@@ -121,7 +122,7 @@ async function run(requestFile) {
121
122
  const maxResults = Number.parseInt(request.args?.maxResults || "5", 10);
122
123
 
123
124
  if (!rawInput.trim()) {
124
- console.log(JSON.stringify({ ok: false, error: "text, artifact.text, or args.url is required" }));
125
+ console.log(JSON.stringify(toolError("text, artifact.text, or args.url is required")));
125
126
  return;
126
127
  }
127
128
 
@@ -129,9 +130,9 @@ async function run(requestFile) {
129
130
  const outputText = mode === "open"
130
131
  ? await openWebPage(rawInput)
131
132
  : await searchWeb(rawInput, Number.isFinite(maxResults) ? maxResults : 5);
132
- console.log(JSON.stringify({ ok: true, output: { text: outputText } }));
133
+ console.log(JSON.stringify(toolOk({ text: outputText })));
133
134
  } catch (error) {
134
- console.log(JSON.stringify({ ok: false, error: error.message || String(error) }));
135
+ console.log(JSON.stringify(toolError(error.message || String(error))));
135
136
  }
136
137
  }
137
138