arisa 4.1.16 → 4.2.2

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
@@ -23,6 +23,15 @@ Do not build runtime paths by hand. Use `src/runtime/paths.js`:
23
23
 
24
24
  Tools receive `chatId` from the registry. Any persisted or indexed user content must be scoped by chat. Avoid ad hoc roots like `~/.arisa/state/<toolName>`, `~/.arisa/state/chats`, or runtime data inside `~/.arisa/tools/<toolName>`.
25
25
 
26
+ ## Tool config rules
27
+ Every tool has a local `config.js` that exports generic, non-user-specific defaults. Keep user credentials, deployment-specific URLs, account ids, phone numbers, and other per-user values empty or neutral there; document required values in `tool.manifest.json` `configSchema`.
28
+
29
+ Tools must consume config through `src/core/tools/tool-config.js`:
30
+ - `loadToolConfig(toolName, defaults)` for global tools and daemons that run as one shared process.
31
+ - `loadToolConfig(toolName, defaults, request.chatId)` inside `run()` for request-scoped tools that should honor per-chat overrides.
32
+
33
+ Config precedence is a shallow merge: tool defaults -> global tool config (`~/.arisa/tools/<toolName>/config.js`) -> chat tool config (`~/.arisa/chats/<chatId>/config/tools/<toolName>/config.js`). Per-request `args` may override loaded config for that invocation only; do not persist request args as config unless the user explicitly asked to set a default.
34
+
26
35
  ## Main rule: everything is piped through artifacts
27
36
  A pipe transforms one input artifact into one output artifact.
28
37
  Examples:
@@ -34,6 +43,8 @@ Each tool declares in `tool.manifest.json`:
34
43
  - `input`: supported input types
35
44
  - `output`: produced output types
36
45
  - `configSchema`: required config fields
46
+ - `category`: optional broad capability bucket for discovery
47
+ - `keywords`: optional intent tags for capability discovery
37
48
  - `skillHints`: optional skills to apply when using or editing the tool
38
49
 
39
50
  ## Tool-to-Arisa IPC
@@ -157,6 +168,8 @@ Do not assume a rigid question/answer protocol. Continue the conversation natura
157
168
  ## Capability resolution
158
169
  Reason in terms of capabilities, not tool names. Do not stop at "I cannot do that" when the task is realistically implementable through the tool architecture.
159
170
 
171
+ Before asking the user for recurrent context, inspect available tools by `category` and `keywords`, especially `memory`, `context`, `contacts`, and `essential`.
172
+
160
173
  When the user asks for something new:
161
174
  1. check whether an existing registered tool, or an indirect use of one, can satisfy the task
162
175
  2. check the official catalog at `https://github.com/clasen/Arisa/tree/main/tools` before building anything
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arisa",
3
- "version": "4.1.16",
3
+ "version": "4.2.2",
4
4
  "description": "Telegram + Pi Agent modular assistant",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -1,9 +1,9 @@
1
1
  import path from "node:path";
2
- import { stat, unlink } from "node:fs/promises";
3
- import { createAgentSession, SessionManager, defineTool } from "@earendil-works/pi-coding-agent";
2
+ import { readFile, stat, unlink } from "node:fs/promises";
3
+ import { createAgentSession, DefaultResourceLoader, SessionManager, defineTool } from "@earendil-works/pi-coding-agent";
4
4
  import { Type } from "@sinclair/typebox";
5
5
  import { createPiRuntime, hasProviderAuth } from "./pi-runtime.js";
6
- import { arisaInstallDir, buildAgentRuntimeContext } from "./runtime-context.js";
6
+ import { appendArisaAgentsFile, arisaAgentsFile, arisaInstallDir, buildAgentRuntimeContext } from "./runtime-context.js";
7
7
  import { withTimeout } from "./prompt-timeout.js";
8
8
  import { buildPiToolPolicy, getCoreCodingTools } from "./core-tools.js";
9
9
  import { createSystemShellTool } from "./system-shell-tool.js";
@@ -88,6 +88,17 @@ async function assertDirectory(dir, label) {
88
88
  }
89
89
  }
90
90
 
91
+ async function createArisaResourceLoader({ cwd, agentDir }) {
92
+ const arisaAgentsContent = await readFile(arisaAgentsFile, "utf8");
93
+ const resourceLoader = new DefaultResourceLoader({
94
+ cwd,
95
+ agentDir,
96
+ agentsFilesOverride: (current) => appendArisaAgentsFile(current, arisaAgentsContent)
97
+ });
98
+ await resourceLoader.reload();
99
+ return resourceLoader;
100
+ }
101
+
91
102
  export class AgentManager {
92
103
  constructor({ config, artifactStore, toolRegistry, taskStore, logger }) {
93
104
  this.config = config;
@@ -188,9 +199,14 @@ export class AgentManager {
188
199
  ...this.createTools(telegram, chatId, policy),
189
200
  createSystemShellTool({ workspaceDir: policy.workspaceDir, shell: policy.shell })
190
201
  ];
202
+ const resourceLoader = await createArisaResourceLoader({
203
+ cwd: policy.workspaceDir,
204
+ agentDir: arisaHomeDir
205
+ });
191
206
  const { session } = await createAgentSession({
192
207
  cwd: policy.workspaceDir,
193
208
  agentDir: arisaHomeDir,
209
+ resourceLoader,
194
210
  authStorage,
195
211
  modelRegistry,
196
212
  model,
@@ -1,7 +1,19 @@
1
+ import path from "node:path";
1
2
  import { fileURLToPath } from "node:url";
2
3
  import { arisaHomeDir, arisaPackageDir, chatsDir, stateDir, toolStateDir, toolsDir } from "../../runtime/paths.js";
3
4
 
4
5
  export const arisaInstallDir = fileURLToPath(new URL("../../..", import.meta.url));
6
+ export const arisaAgentsFile = path.join(arisaPackageDir, "AGENTS.md");
7
+
8
+ export function appendArisaAgentsFile(current, content, agentsFilePath = arisaAgentsFile) {
9
+ return {
10
+ ...current,
11
+ agentsFiles: [
12
+ ...(current.agentsFiles || []).filter((file) => path.resolve(file.path) !== path.resolve(agentsFilePath)),
13
+ { path: agentsFilePath, content }
14
+ ]
15
+ };
16
+ }
5
17
 
6
18
  function formatCoreTools(coreTools = []) {
7
19
  const enabled = coreTools
@@ -20,6 +32,7 @@ export function buildAgentRuntimeContext({ workspaceDir = arisaHomeDir, coreTool
20
32
  `toolStateDir: ${toolStateDir}`,
21
33
  `chatsDir: ${chatsDir}`,
22
34
  `stateDir: ${stateDir}`,
35
+ `arisaAgentsFile: ${arisaAgentsFile}`,
23
36
  `enabledCoreTools: ${formatCoreTools(coreTools)}`,
24
37
  "Guidance: create or edit user tools under userToolsDir. Treat arisaPackageDir/arisaInstallDir as Arisa core and modify it only when the user explicitly asks for core changes."
25
38
  ].join("\n");
@@ -7,6 +7,11 @@ function id() {
7
7
  return crypto.randomUUID();
8
8
  }
9
9
 
10
+ function writeArtifactFile(filePath, content) {
11
+ if (typeof content === "string") return writeFile(filePath, content, "utf8");
12
+ return writeFile(filePath, content);
13
+ }
14
+
10
15
  class ChatArtifactStore {
11
16
  constructor(chatId) {
12
17
  this.chatId = String(chatId);
@@ -81,7 +86,7 @@ class ChatArtifactStore {
81
86
  const dir = path.join(this.rootDir, artifactId);
82
87
  await mkdir(dir, { recursive: true });
83
88
  const destPath = path.join(dir, fileName);
84
- await writeFile(destPath, content);
89
+ await writeArtifactFile(destPath, content);
85
90
  const artifact = {
86
91
  id: artifactId,
87
92
  chatId: this.chatId,
@@ -21,6 +21,28 @@ function runProcess(command, args, options = {}) {
21
21
  });
22
22
  }
23
23
 
24
+ function normalizeCategory(category) {
25
+ if (typeof category !== "string") return null;
26
+ const trimmed = category.trim();
27
+ return trimmed || null;
28
+ }
29
+
30
+ function normalizeKeywords(keywords) {
31
+ if (!Array.isArray(keywords)) return [];
32
+ return [...new Set(keywords
33
+ .filter((keyword) => typeof keyword === "string")
34
+ .map((keyword) => keyword.trim())
35
+ .filter(Boolean))];
36
+ }
37
+
38
+ function formatSemanticMetadata(tool) {
39
+ return [
40
+ "Semantic metadata:",
41
+ `- category: ${tool.category || "none"}`,
42
+ `- keywords: ${tool.keywords?.length ? tool.keywords.join(", ") : "none"}`
43
+ ].join("\n");
44
+ }
45
+
24
46
  export class ToolRegistry {
25
47
  constructor({ logger } = {}) {
26
48
  this.logger = logger;
@@ -52,6 +74,8 @@ export class ToolRegistry {
52
74
  const skillHints = this.skillRegistry.normalizeHints(manifest);
53
75
  this.tools.set(manifest.name, {
54
76
  ...manifest,
77
+ category: normalizeCategory(manifest.category),
78
+ keywords: normalizeKeywords(manifest.keywords),
55
79
  skillHints,
56
80
  dir: toolDir,
57
81
  entry: path.join(toolDir, manifest.entry || "index.js"),
@@ -75,6 +99,8 @@ export class ToolRegistry {
75
99
  input: tool.input,
76
100
  output: tool.output,
77
101
  configSchema: tool.configSchema || {},
102
+ category: tool.category,
103
+ keywords: tool.keywords || [],
78
104
  skillHints: tool.skillHints || []
79
105
  }));
80
106
  }
@@ -89,13 +115,19 @@ export class ToolRegistry {
89
115
  const result = await runProcess("node", [tool.entry, "--help"], { cwd: tool.dir, env: toolEnv() });
90
116
  const help = result.stdout || result.stderr;
91
117
  const skills = await this.resolveSkills(name);
92
- if (!skills.length) return help;
93
- const skillHelp = skills.map((item) => [
94
- `- ${item.name}${item.when ? ` (${item.when})` : ""}`,
95
- item.description ? ` ${item.description}` : null,
96
- item.found ? ` path: ${item.path}` : " warning: skill not found"
97
- ].filter(Boolean).join("\n")).join("\n");
98
- return `${help}\n\nAssigned skills:\n${skillHelp}\n`;
118
+ const sections = [
119
+ help.trimEnd(),
120
+ formatSemanticMetadata(tool)
121
+ ];
122
+ if (skills.length) {
123
+ const skillHelp = skills.map((item) => [
124
+ `- ${item.name}${item.when ? ` (${item.when})` : ""}`,
125
+ item.description ? ` ${item.description}` : null,
126
+ item.found ? ` path: ${item.path}` : " warning: skill not found"
127
+ ].filter(Boolean).join("\n")).join("\n");
128
+ sections.push(`Assigned skills:\n${skillHelp}`);
129
+ }
130
+ return `${sections.filter(Boolean).join("\n\n")}\n`;
99
131
  }
100
132
 
101
133
  async resolveSkills(name) {
@@ -4,6 +4,7 @@ import os from "node:os";
4
4
  import path from "node:path";
5
5
  import test from "node:test";
6
6
  import { buildPiToolPolicy } from "../src/core/agent/core-tools.js";
7
+ import { appendArisaAgentsFile, arisaAgentsFile } from "../src/core/agent/runtime-context.js";
7
8
  import { createSystemShellTool } from "../src/core/agent/system-shell-tool.js";
8
9
  import { applyRuntimeOverrides } from "../src/runtime/create-app.js";
9
10
  import { arisaHomeDir } from "../src/runtime/paths.js";
@@ -89,3 +90,22 @@ test("system_shell runs commands from the configured workspace", async () => {
89
90
  assert.equal(result.details.exitCode, 0);
90
91
  assert.equal(result.details.stdout, realWorkspaceDir);
91
92
  });
93
+
94
+ test("adds Arisa AGENTS.md to Pi context files without duplicating it", () => {
95
+ const current = {
96
+ agentsFiles: [
97
+ { path: "/workspace/AGENTS.md", content: "# Workspace" },
98
+ { path: arisaAgentsFile, content: "old" }
99
+ ],
100
+ diagnostics: []
101
+ };
102
+
103
+ const next = appendArisaAgentsFile(current, "# Arisa AGENTS");
104
+
105
+ assert.deepEqual(next.diagnostics, []);
106
+ assert.deepEqual(
107
+ next.agentsFiles.map((file) => file.path),
108
+ ["/workspace/AGENTS.md", arisaAgentsFile]
109
+ );
110
+ assert.equal(next.agentsFiles.at(-1).content, "# Arisa AGENTS");
111
+ });
@@ -83,6 +83,20 @@ test("creates generated file artifacts", async () => {
83
83
  assert.equal(artifact.mimeType, "text/markdown");
84
84
  });
85
85
 
86
+ test("writes generated text file artifacts as UTF-8", async () => {
87
+ await resetHome();
88
+ const content = "# Español\nÑandú\n";
89
+ const artifact = await new ArtifactStore().forChat("chat-1").createGeneratedFile({
90
+ fileName: "reply.txt",
91
+ content,
92
+ kind: "document",
93
+ mimeType: "text/plain",
94
+ source: { type: "assistant" }
95
+ });
96
+
97
+ assert.deepEqual(await readFile(artifact.path), Buffer.from(content, "utf8"));
98
+ });
99
+
86
100
  test("keeps artifact indexes isolated by chat", async () => {
87
101
  await resetHome();
88
102
  const store = new ArtifactStore();
@@ -20,10 +20,10 @@ async function resetHome() {
20
20
  await rm(arisaHomeDir, { recursive: true, force: true });
21
21
  }
22
22
 
23
- async function createFakeTool(name = "fake-tool") {
23
+ async function createFakeTool(name = "fake-tool", manifestOverrides = {}) {
24
24
  const dir = path.join(toolsDir, name);
25
25
  await mkdir(dir, { recursive: true });
26
- await writeFile(path.join(dir, "tool.manifest.json"), `${JSON.stringify({
26
+ const manifest = {
27
27
  name,
28
28
  description: "Fake test tool",
29
29
  entry: "index.js",
@@ -33,6 +33,10 @@ async function createFakeTool(name = "fake-tool") {
33
33
  apiKey: { type: "string", required: false }
34
34
  },
35
35
  skillHints: [{ name: "missing-skill", when: "testing" }]
36
+ };
37
+ await writeFile(path.join(dir, "tool.manifest.json"), `${JSON.stringify({
38
+ ...manifest,
39
+ ...manifestOverrides
36
40
  }, null, 2)}\n`, "utf8");
37
41
  await writeFile(path.join(dir, "config.js"), "export default {\n apiKey: \"default-key\"\n};\n", "utf8");
38
42
  await writeFile(path.join(dir, "index.js"), `import { readFile } from "node:fs/promises";
@@ -67,7 +71,10 @@ process.stdout.write(JSON.stringify({
67
71
 
68
72
  test("loads and lists installed tools from the user tools directory", async () => {
69
73
  await resetHome();
70
- await createFakeTool("fake-tool");
74
+ await createFakeTool("fake-tool", {
75
+ category: "memory",
76
+ keywords: ["memory", "essential"]
77
+ });
71
78
 
72
79
  const registry = new ToolRegistry();
73
80
  await registry.load();
@@ -80,10 +87,40 @@ test("loads and lists installed tools from the user tools directory", async () =
80
87
  configSchema: {
81
88
  apiKey: { type: "string", required: false }
82
89
  },
90
+ category: "memory",
91
+ keywords: ["memory", "essential"],
83
92
  skillHints: [{ name: "missing-skill", when: "testing" }]
84
93
  }]);
85
94
  });
86
95
 
96
+ test("lists optional semantic metadata with stable defaults", async () => {
97
+ await resetHome();
98
+ await createFakeTool("fake-tool");
99
+
100
+ const registry = new ToolRegistry();
101
+ await registry.load();
102
+
103
+ assert.equal(registry.list()[0].category, null);
104
+ assert.deepEqual(registry.list()[0].keywords, []);
105
+ });
106
+
107
+ test("shows semantic metadata in tool help", async () => {
108
+ await resetHome();
109
+ await createFakeTool("fake-tool", {
110
+ category: "memory",
111
+ keywords: ["memory", "essential"]
112
+ });
113
+
114
+ const registry = new ToolRegistry();
115
+ await registry.load();
116
+
117
+ const help = await registry.help("fake-tool");
118
+
119
+ assert.match(help, /Fake test tool help/);
120
+ assert.match(help, /Semantic metadata:\n- category: memory\n- keywords: memory, essential/);
121
+ assert.match(help, /Assigned skills:/);
122
+ });
123
+
87
124
  test("runs a registered tool process with an enriched request and cleans up request files", async () => {
88
125
  await resetHome();
89
126
  await createFakeTool("fake-tool");