arisa 3.0.6 → 3.0.9

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
@@ -1,11 +1,11 @@
1
1
  # Arisa AGENTS
2
2
 
3
3
  ## Architecture
4
- - `src/transport/telegram/*`: Telegram inbound and outbound transport.
5
- - `src/core/agent/*`: Pi Agent sessions, one per authorized chat.
6
- - `src/core/artifacts/*`: every incoming or generated message/file becomes an artifact.
7
- - `src/core/tools/*`: CLI tool registry, help lookup, config writes, execution.
8
- - `tools/*`: isolated tools. Each tool has `package.json`, `config.js`, `tool.manifest.json`, and `index.js`.
4
+ - Telegram transport handles inbound and outbound messaging.
5
+ - Pi Agent keeps one session per authorized chat.
6
+ - Every incoming or generated message or file becomes an artifact.
7
+ - A tool registry handles tool discovery, help lookup, config writes, and execution.
8
+ - Tools are isolated and each one has its own manifest, entrypoint, and config defaults.
9
9
 
10
10
  ## Main rule: everything is piped through artifacts
11
11
  A pipe transforms one input artifact into one output artifact.
@@ -70,45 +70,39 @@ Example manual pipe:
70
70
  ## Missing config flow
71
71
  If `run_tool` returns `missingConfig`, the agent should:
72
72
  1. ask the user naturally in Telegram for the missing value
73
- 2. write the value into `~/.arisa/tools/<tool>/config.js` with `set_tool_config`
73
+ 2. write the value with `set_tool_config`
74
74
  3. retry the tool
75
75
 
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
- ## Long-running work
79
- If a task is likely to take noticeable time — for example creating a new tool, editing multiple files, or doing multi-step work — the agent should first acknowledge the request briefly and naturally, then continue the work.
80
-
81
- The acknowledgment should:
82
- - be short and clear
83
- - tell the user the work is starting
84
- - mention when the task may take a while
78
+ ## Tool creation
79
+ Reason in terms of capabilities, not tool names.
85
80
 
86
- Examples:
87
- - "Understood. I'll build that tool now. This may take a couple of minutes."
88
- - "Got it. I'll inspect the project and make the change now."
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
- ## Tool creation
91
- Do not assume specific future tools such as YouTube support exist.
92
- If the user asks for a capability that is not currently available, first check whether an existing registered tool can satisfy the task.
93
- If no existing tool can do it, the default attitude should be to propose creating a new CLI tool under `tools/<tool-name>` following the project conventions.
94
- All newly created tools must document their help text, usage instructions, manifests, and user-facing operational strings in English.
95
- Do not stop at "I cannot do that" when the task is realistically implementable through a new tool.
96
- Prefer responses like:
86
+ Do not stop at "I cannot do that" when the task is realistically implementable through the tool architecture.
87
+ The default attitude is:
97
88
  - identify that no current tool satisfies the request
98
89
  - state that the missing capability can be added
99
- - propose or start creating the tool needed to fulfill the request
100
-
101
- 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.
90
+ - propose or start creating the needed tool
102
91
 
103
- When creating or editing tools, follow the shared path helpers in `src/runtime/paths.js` and `src/core/tools/tool-config.js`:
104
- - config in `~/.arisa/tools/<tool>/config.js`
105
- - temp/runtime files in `~/.arisa/tmp/tools/<tool>/`
106
- - durable generated files should become artifacts in `~/.arisa/artifacts/`
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
107
97
 
108
- Consult the local skill for that workflow when building new tools.
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.
109
103
 
110
104
  ## Safety
111
- - Do not install or run arbitrary tools outside registered `tools/*` manifests in V1.
105
+ - Do not install or run arbitrary tools outside registered tool manifests in V1.
112
106
  - Prefer tool manifests and CLI help over assumptions.
113
- - Keep tool configs inside `~/.arisa/tools/<tool>/config.js`.
107
+ - Keep tool config and runtime data inside the user runtime area.
114
108
  - 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
@@ -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
 
@@ -46,7 +46,9 @@ This distinction is important. Some transformations belong to the transport/inpu
46
46
  - media is stored as artifacts
47
47
 
48
48
  ### Tool model
49
- Each tool lives in its own folder under `tools/<tool-name>` and contains:
49
+ Bundled tools live under `<arisa-install-dir>/tools/<tool-name>` and user-created tools live under `~/.arisa/tools/<tool-name>`.
50
+
51
+ Each tool folder contains:
50
52
 
51
53
  - `package.json`
52
54
  - `config.js`
@@ -57,7 +59,8 @@ Each tool is isolated from the root project and from other tools.
57
59
  That isolation is part of the architecture:
58
60
 
59
61
  - each tool has its own folder
60
- - each tool has a local `config.js` only for defaults/template values
62
+ - bundled tools have a local `config.js` for defaults/template values
63
+ - user-created tools can live entirely inside `~/.arisa/tools/<tool>/`
61
64
  - each tool can have its own dependencies
62
65
  - one tool can be changed or replaced without tightly coupling the rest of the system
63
66
 
@@ -74,8 +77,8 @@ node index.js run --request-file <json>
74
77
  - artifact index is stored in `~/.arisa/state/artifacts.json`
75
78
  - incoming Telegram attachments are stored directly in `~/.arisa/artifacts/`
76
79
  - tool-specific secrets/config live in `~/.arisa/tools/<tool>/config.js`
77
- - bundled tools and generated tools should both use the same source layout under `tools/<tool>/`
78
- - tool runtime temp files and generated outputs live in `~/.arisa/tmp/tools/<tool>/`
80
+ - user-created tools also live under `~/.arisa/tools/<tool>/`
81
+ - tool runtime temp files and generated outputs live under `~/.arisa/tools/<tool>/` (for example `tmp/` and `out/`)
79
82
  - durable files should end up in `~/.arisa/artifacts/`
80
83
  - Pi authentication can use either:
81
84
  - an API key entered during bootstrap
@@ -96,11 +99,13 @@ arisa
96
99
  Command modes:
97
100
 
98
101
  ```bash
99
- arisa # foreground, blocking
100
- arisa start # start in background
101
- arisa stop # stop background service
102
- arisa status # show background service status
103
- 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
104
109
  ```
105
110
 
106
111
  ## Bootstrap flow
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arisa",
3
- "version": "3.0.6",
3
+ "version": "3.0.9",
4
4
  "description": "Telegram + Pi Agent modular assistant",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -1,10 +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 { getChatDir, piAgentDir as agentDir } from "../../runtime/paths.js";
7
+ import { arisaInstallDir, buildAgentRuntimeContext } from "./runtime-context.js";
8
+ import { arisaHomeDir } from "../../runtime/paths.js";
8
9
 
9
10
  export class AgentManager {
10
11
  constructor({ config, artifactStore, toolRegistry, logger }) {
@@ -49,7 +50,6 @@ export class AgentManager {
49
50
  return this.sessions.get(chatId);
50
51
  }
51
52
 
52
- await mkdir(agentDir, { recursive: true });
53
53
  const { authStorage, modelRegistry } = createPiRuntime({
54
54
  provider: this.config.pi.provider,
55
55
  apiKey: this.config.pi.apiKey
@@ -60,24 +60,23 @@ export class AgentManager {
60
60
  throw new Error(`No auth found for ${this.config.pi.provider}. Re-run bootstrap and complete login for this provider before Telegram starts.`);
61
61
  }
62
62
 
63
- const cwd = getChatDir(chatId);
64
- await mkdir(cwd, { recursive: true });
65
-
66
63
  this.logger?.log("agent", `creating session for chat ${chatId}`);
67
64
  const customTools = this.createTools(telegram);
68
65
  const { session } = await createAgentSession({
69
- cwd,
70
- agentDir,
66
+ cwd: arisaInstallDir,
67
+ agentDir: arisaHomeDir,
71
68
  authStorage,
72
69
  modelRegistry,
73
70
  model,
74
71
  customTools,
75
- sessionManager: SessionManager.continueRecent(cwd)
72
+ sessionManager: SessionManager.inMemory()
76
73
  });
77
74
 
78
75
  const instructions = await loadProjectInstructions();
76
+ const runtimeContext = buildAgentRuntimeContext();
79
77
  this.logger?.log("agent", `injecting project instructions for chat ${chatId}`);
80
- await session.prompt(`${instructions}\n\nAcknowledge with exactly: OK`);
78
+ this.logger?.log("agent", `runtime context for chat ${chatId}:\n${runtimeContext}`);
79
+ await session.prompt(`${instructions}\n\n${runtimeContext}\n\nAcknowledge with exactly: OK`);
81
80
 
82
81
  const ctx = { session };
83
82
  this.sessions.set(chatId, ctx);
@@ -0,0 +1,16 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { arisaHomeDir, artifactsDir, stateDir, toolsDir } from "../../runtime/paths.js";
3
+
4
+ export const arisaInstallDir = fileURLToPath(new URL("../../..", import.meta.url));
5
+ export const bundledToolsDir = fileURLToPath(new URL("../../../tools", import.meta.url));
6
+
7
+ export function buildAgentRuntimeContext() {
8
+ return [
9
+ `arisaHomeDir: ${arisaHomeDir}`,
10
+ `arisaInstallDir: ${arisaInstallDir}`,
11
+ `bundledToolsDir: ${bundledToolsDir}`,
12
+ `userToolsDir: ${toolsDir}`,
13
+ `artifactsDir: ${artifactsDir}`,
14
+ `stateDir: ${stateDir}`
15
+ ].join("\n");
16
+ }
@@ -1,10 +1,15 @@
1
1
  import { mkdir, readdir, readFile, unlink, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { spawn } from "node:child_process";
4
- import { getToolConfigPath, getToolTmpDir } from "../../runtime/paths.js";
4
+ import { fileURLToPath } from "node:url";
5
+ import { getToolConfigPath, getToolTmpDir, toolsDir as userToolsRoot } from "../../runtime/paths.js";
5
6
  import { loadToolConfig, parseConfigModule, writeToolConfig } from "./tool-config.js";
6
7
 
7
- const toolsRoot = path.resolve("tools");
8
+ const bundledToolsRoot = fileURLToPath(new URL("../../../tools", import.meta.url));
9
+ const toolRoots = [
10
+ { root: userToolsRoot, kind: "user" },
11
+ { root: bundledToolsRoot, kind: "bundled" }
12
+ ];
8
13
 
9
14
  function runProcess(command, args, options = {}) {
10
15
  return new Promise((resolve) => {
@@ -26,35 +31,39 @@ export class ToolRegistry {
26
31
  async load() {
27
32
  this.tools.clear();
28
33
 
29
- let entries = [];
30
- try {
31
- entries = await readdir(toolsRoot, { withFileTypes: true });
32
- } catch {
33
- this.logger?.log("tools", `tools directory not found: ${toolsRoot}`);
34
- return;
35
- }
36
-
37
- for (const entry of entries) {
38
- if (!entry.isDirectory()) continue;
39
- const toolDir = path.join(toolsRoot, entry.name);
40
- const manifestPath = path.join(toolDir, "tool.manifest.json");
41
- const configPath = path.join(toolDir, "config.js");
34
+ for (const { root, kind } of toolRoots) {
35
+ let entries = [];
42
36
  try {
43
- const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
44
- const configSource = await readFile(configPath, "utf8");
45
- const defaults = parseConfigModule(configSource);
46
- const config = await loadToolConfig(manifest.name, defaults);
47
- this.tools.set(manifest.name, {
48
- ...manifest,
49
- dir: toolDir,
50
- entry: path.join(toolDir, manifest.entry || "index.js"),
51
- localConfigPath: configPath,
52
- configPath: getToolConfigPath(manifest.name),
53
- defaults,
54
- config
55
- });
37
+ entries = await readdir(root, { withFileTypes: true });
56
38
  } catch {
57
- // ignore invalid tool dirs in v1
39
+ this.logger?.log("tools", `${kind} tools directory not found: ${root}`);
40
+ continue;
41
+ }
42
+
43
+ for (const entry of entries) {
44
+ if (!entry.isDirectory()) continue;
45
+ const toolDir = path.join(root, entry.name);
46
+ const manifestPath = path.join(toolDir, "tool.manifest.json");
47
+ const configPath = path.join(toolDir, "config.js");
48
+ try {
49
+ const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
50
+ if (this.tools.has(manifest.name)) continue;
51
+ const configSource = await readFile(configPath, "utf8");
52
+ const defaults = parseConfigModule(configSource);
53
+ const config = await loadToolConfig(manifest.name, defaults);
54
+ this.tools.set(manifest.name, {
55
+ ...manifest,
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
+ sourceKind: kind
63
+ });
64
+ } catch {
65
+ // ignore invalid tool dirs in v1
66
+ }
58
67
  }
59
68
  }
60
69
 
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(stateDir, "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
  }
@@ -27,7 +21,7 @@ export function getToolConfigPath(toolName) {
27
21
  }
28
22
 
29
23
  export function getToolRuntimeDir(toolName) {
30
- return path.join(tmpDir, "tools", toolName);
24
+ return getToolDir(toolName);
31
25
  }
32
26
 
33
27
  export function getToolOutDir(toolName) {
@@ -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
+ }