arisa 3.0.8 → 3.0.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 CHANGED
@@ -76,19 +76,30 @@ If `run_tool` returns `missingConfig`, the agent should:
76
76
  Do not assume a rigid question/answer protocol. Continue the conversation naturally and infer the config value from the user reply when possible.
77
77
 
78
78
  ## Tool creation
79
- If the user asks for a capability that is not currently available, first check whether an existing registered tool can satisfy the task.
80
- If no existing tool can do it, the default attitude should be to propose creating a new CLI tool following the project conventions.
81
- All newly created tools must document their help text, usage instructions, manifests, and user-facing operational strings in English.
82
- Do not stop at "I cannot do that" when the task is realistically implementable through a new tool.
83
- Prefer responses like:
84
- - identify that no current tool satisfies the request
85
- - state that the missing capability can be added
86
- - propose or start creating the tool needed to fulfill the request
79
+ Reason in terms of capabilities, not tool names.
87
80
 
88
- 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.
81
+ When the user asks for something new:
82
+ 1. check whether an existing registered tool can already satisfy the task
83
+ 2. also check whether the task can be satisfied indirectly through an existing capability
84
+ 3. only propose creating a new tool when the needed capability is truly missing
89
85
 
90
- When creating or editing tools, use the shared path helpers and the runtime paths provided in the prompt instead of assuming fixed locations.
91
- Consult the local skill for that workflow when building new tools.
86
+ Do not stop at "I cannot do that" when the task is realistically implementable through the tool architecture.
87
+ The default attitude is:
88
+ - identify that no current tool satisfies the request
89
+ - state that the missing capability can be added
90
+ - propose or start creating the needed tool
91
+
92
+ When creating or editing tools:
93
+ - use the shared path helpers and the runtime paths provided in the prompt instead of assuming fixed locations
94
+ - consult the local skill for that workflow when building new tools
95
+ - keep all help text, usage instructions, manifests, and user-facing operational strings in English
96
+ - 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
97
+
98
+ ## Dependency installation
99
+ Arisa installs tool dependencies itself.
100
+ - Prefer `pnpm install`.
101
+ - Fall back to `npm install`.
102
+ - Do not ask the user to do it manually.
92
103
 
93
104
  ## Safety
94
105
  - Do not install or run arbitrary tools outside registered tool manifests in V1.
package/README.md CHANGED
@@ -4,7 +4,7 @@ Arisa is a personal Telegram assistant powered by Pi Agent.
4
4
 
5
5
  ## Origin
6
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.
7
+ The initial inspiration was [OpenClaw](https://github.com/openclaw/openclaw). OpenClaw has interesting ideas but carries a lot of weight (about **185 MB**) compared to Arisa (**76.7 kB**): 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
 
@@ -99,13 +99,26 @@ arisa
99
99
  Command modes:
100
100
 
101
101
  ```bash
102
- arisa # foreground, blocking
103
- arisa start # start in background
104
- arisa stop # stop background service
105
- arisa status # show background service status
106
- arisa flush # remove ~/.arisa
102
+ arisa # foreground, blocking
103
+ arisa start # start in background
104
+ arisa stop # stop background service
105
+ arisa status # show background service status
106
+ arisa flush # remove ~/.arisa
107
+ arisa install <source> # install a Pi package into Arisa's runtime
108
+ arisa remove <source> # remove a Pi package from Arisa's runtime
107
109
  ```
108
110
 
111
+ ## Experimental features
112
+
113
+ ### Pi Agent packages
114
+
115
+ 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).
116
+
117
+ - `arisa install <source>` installs a package (by registry name or other source supported by Pi).
118
+ - `arisa remove <source>` removes a previously installed package.
119
+
120
+ 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.
121
+
109
122
  ## Bootstrap flow
110
123
 
111
124
  On first run, Arisa will:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arisa",
3
- "version": "3.0.8",
3
+ "version": "3.0.10",
4
4
  "description": "Telegram + Pi Agent modular assistant",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -1,11 +1,11 @@
1
1
  import path from "node:path";
2
- import { mkdir, unlink } from "node:fs/promises";
2
+ import { unlink } from "node:fs/promises";
3
3
  import { createAgentSession, SessionManager, defineTool } from "@mariozechner/pi-coding-agent";
4
4
  import { Type } from "@sinclair/typebox";
5
5
  import { createPiRuntime, hasProviderAuth } from "./pi-runtime.js";
6
6
  import { loadProjectInstructions } from "./project-instructions.js";
7
- import { buildAgentRuntimeContext } from "./runtime-context.js";
8
- import { getChatDir, piAgentDir as agentDir } from "../../runtime/paths.js";
7
+ import { arisaInstallDir, buildAgentRuntimeContext } from "./runtime-context.js";
8
+ import { arisaHomeDir } from "../../runtime/paths.js";
9
9
 
10
10
  export class AgentManager {
11
11
  constructor({ config, artifactStore, toolRegistry, logger }) {
@@ -50,7 +50,6 @@ export class AgentManager {
50
50
  return this.sessions.get(chatId);
51
51
  }
52
52
 
53
- await mkdir(agentDir, { recursive: true });
54
53
  const { authStorage, modelRegistry } = createPiRuntime({
55
54
  provider: this.config.pi.provider,
56
55
  apiKey: this.config.pi.apiKey
@@ -61,23 +60,20 @@ export class AgentManager {
61
60
  throw new Error(`No auth found for ${this.config.pi.provider}. Re-run bootstrap and complete login for this provider before Telegram starts.`);
62
61
  }
63
62
 
64
- const cwd = getChatDir(chatId);
65
- await mkdir(cwd, { recursive: true });
66
-
67
63
  this.logger?.log("agent", `creating session for chat ${chatId}`);
68
64
  const customTools = this.createTools(telegram);
69
65
  const { session } = await createAgentSession({
70
- cwd,
71
- agentDir,
66
+ cwd: arisaInstallDir,
67
+ agentDir: arisaHomeDir,
72
68
  authStorage,
73
69
  modelRegistry,
74
70
  model,
75
71
  customTools,
76
- sessionManager: SessionManager.continueRecent(cwd)
72
+ sessionManager: SessionManager.inMemory()
77
73
  });
78
74
 
79
75
  const instructions = await loadProjectInstructions();
80
- const runtimeContext = buildAgentRuntimeContext(chatId);
76
+ const runtimeContext = buildAgentRuntimeContext();
81
77
  this.logger?.log("agent", `injecting project instructions for chat ${chatId}`);
82
78
  this.logger?.log("agent", `runtime context for chat ${chatId}:\n${runtimeContext}`);
83
79
  await session.prompt(`${instructions}\n\n${runtimeContext}\n\nAcknowledge with exactly: OK`);
@@ -1,17 +1,16 @@
1
1
  import { fileURLToPath } from "node:url";
2
- import { arisaHomeDir, artifactsDir, getChatDir, stateDir, toolsDir } from "../../runtime/paths.js";
2
+ import { arisaHomeDir, artifactsDir, stateDir, toolsDir } from "../../runtime/paths.js";
3
3
 
4
4
  export const arisaInstallDir = fileURLToPath(new URL("../../..", import.meta.url));
5
5
  export const bundledToolsDir = fileURLToPath(new URL("../../../tools", import.meta.url));
6
6
 
7
- export function buildAgentRuntimeContext(chatId) {
7
+ export function buildAgentRuntimeContext() {
8
8
  return [
9
9
  `arisaHomeDir: ${arisaHomeDir}`,
10
10
  `arisaInstallDir: ${arisaInstallDir}`,
11
11
  `bundledToolsDir: ${bundledToolsDir}`,
12
12
  `userToolsDir: ${toolsDir}`,
13
13
  `artifactsDir: ${artifactsDir}`,
14
- `stateDir: ${stateDir}`,
15
- `chatWorkspaceDir: ${getChatDir(chatId)}`
14
+ `stateDir: ${stateDir}`
16
15
  ].join("\n");
17
16
  }
package/src/index.js CHANGED
@@ -5,6 +5,7 @@ import { createApp } from "./runtime/create-app.js";
5
5
  import { createLogger } from "./runtime/logger.js";
6
6
  import { getServiceStatus, registerServiceProcess, startService, stopService } from "./runtime/service-manager.js";
7
7
  import { flushArisaHome } from "./runtime/flush.js";
8
+ import { installPiPackage, removePiPackage } from "./runtime/pi-package-manager.js";
8
9
 
9
10
  const args = process.argv.slice(2);
10
11
  const command = args.find((arg) => !arg.startsWith("--")) || "run";
@@ -83,6 +84,28 @@ async function main() {
83
84
  return;
84
85
  }
85
86
 
87
+ if (command === "install") {
88
+ const source = args.filter((arg) => !arg.startsWith("--")).slice(1)[0];
89
+ if (!source) {
90
+ console.log("Usage: arisa install <pi-package-source>");
91
+ return;
92
+ }
93
+ const result = await installPiPackage(source);
94
+ process.exitCode = result.ok ? 0 : result.code;
95
+ return;
96
+ }
97
+
98
+ if (command === "remove") {
99
+ const source = args.filter((arg) => !arg.startsWith("--")).slice(1)[0];
100
+ if (!source) {
101
+ console.log("Usage: arisa remove <pi-package-source>");
102
+ return;
103
+ }
104
+ const result = await removePiPackage(source);
105
+ process.exitCode = result.ok ? 0 : result.code;
106
+ return;
107
+ }
108
+
86
109
  await runForeground();
87
110
  }
88
111
 
@@ -9,15 +9,9 @@ export const servicePidFile = path.join(stateDir, "arisa.pid");
9
9
  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
- export const piAgentDir = path.join(stateDir, "pi-agent");
13
- export const chatsDir = path.join(arisaHomeDir, "chats");
14
12
  export const toolsDir = path.join(arisaHomeDir, "tools");
15
13
  export const tmpDir = path.join(arisaHomeDir, "tmp");
16
14
 
17
- export function getChatDir(chatId) {
18
- return path.join(chatsDir, String(chatId));
19
- }
20
-
21
15
  export function getToolDir(toolName) {
22
16
  return path.join(toolsDir, toolName);
23
17
  }
@@ -41,8 +35,6 @@ export function getToolTmpDir(toolName) {
41
35
  export async function ensureArisaHome() {
42
36
  await mkdir(stateDir, { recursive: true });
43
37
  await mkdir(artifactsDir, { recursive: true });
44
- await mkdir(piAgentDir, { recursive: true });
45
- await mkdir(chatsDir, { recursive: true });
46
38
  await mkdir(toolsDir, { recursive: true });
47
39
  await mkdir(tmpDir, { recursive: true });
48
40
  }
@@ -0,0 +1,49 @@
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,4 +1,5 @@
1
1
  import { Bot, InputFile } from "grammy";
2
+ import path from "node:path";
2
3
  import { authorizeChat } from "./auth.js";
3
4
  import { captureIncomingArtifact } from "./media.js";
4
5
  import { renderTelegramHtml, splitTelegramText } from "./text-format.js";
@@ -146,10 +147,28 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, a
146
147
  return buildPrompt({ ctx, artifact, transcript });
147
148
  }
148
149
 
149
- async function sendTextReply(send, chatId, text) {
150
+ async function sendTextReply({ sendText, sendDocument, chatId, text }) {
151
+ const attachmentThreshold = 12000;
152
+
153
+ if (text.length > attachmentThreshold) {
154
+ logger?.log("telegram", `sending long reply as markdown attachment for chat ${chatId}`);
155
+ const artifact = await artifactStore.createGeneratedFile({
156
+ fileName: `reply-${Date.now()}.md`,
157
+ content: text,
158
+ kind: "document",
159
+ mimeType: "text/markdown",
160
+ source: { type: "assistant", chatId },
161
+ metadata: { delivery: "telegram-document" }
162
+ });
163
+ await sendDocument(new InputFile(artifact.path, path.basename(artifact.path)), {
164
+ caption: "Response attached as Markdown."
165
+ });
166
+ return;
167
+ }
168
+
150
169
  logger?.log("telegram", `sending text reply for chat ${chatId}`);
151
170
  for (const chunk of splitTelegramText(text)) {
152
- await send(renderTelegramHtml(chunk), { parse_mode: "HTML" });
171
+ await sendText(renderTelegramHtml(chunk), { parse_mode: "HTML" });
153
172
  }
154
173
  }
155
174
 
@@ -167,7 +186,12 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, a
167
186
  const { session } = await agentManager.getSessionContext(ctx.chat.id, telegram);
168
187
  const text = await collectText(session, prompt);
169
188
  if (text) {
170
- await sendTextReply((message, extra) => ctx.reply(message, extra), ctx.chat.id, text);
189
+ await sendTextReply({
190
+ sendText: (message, extra) => ctx.reply(message, extra),
191
+ sendDocument: (file, extra) => ctx.replyWithDocument(file, extra),
192
+ chatId: ctx.chat.id,
193
+ text
194
+ });
171
195
  }
172
196
  });
173
197
  }
@@ -260,7 +284,12 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, a
260
284
  ].filter(Boolean).join("\n");
261
285
  const text = await collectText(session, welcomePrompt);
262
286
  if (text) {
263
- await sendTextReply((message, extra) => bot.api.sendMessage(chatId, message, extra), chatId, text);
287
+ await sendTextReply({
288
+ sendText: (message, extra) => bot.api.sendMessage(chatId, message, extra),
289
+ sendDocument: (file, extra) => bot.api.sendDocument(chatId, file, extra),
290
+ chatId,
291
+ text
292
+ });
264
293
  }
265
294
  } catch (error) {
266
295
  logger?.log("telegram", `startup message failed for chat ${chatId}: ${error instanceof Error ? error.message : String(error)}`);