arisa 4.0.22 → 4.1.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 +33 -58
- package/README.md +6 -4
- package/package.json +1 -1
- package/src/core/agent/agent-manager.js +69 -12
- package/src/core/agent/core-tools.js +139 -0
- package/src/core/agent/runtime-context.js +14 -3
- package/src/core/agent/system-shell-tool.js +207 -0
- package/src/index.js +44 -10
- package/src/runtime/bootstrap.js +412 -36
- package/src/runtime/create-app.js +52 -9
- package/src/transport/telegram/bot.js +4 -3
- package/test/agent-tool-policy.test.js +91 -0
package/AGENTS.md
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
# Arisa AGENTS
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## Core boundaries
|
|
4
|
+
Arisa core owns transport, sessions, artifacts, and tool orchestration:
|
|
4
5
|
- Telegram transport handles inbound and outbound messaging.
|
|
5
6
|
- Pi Agent keeps one session per authorized chat.
|
|
6
7
|
- Incoming messages and files (text, voice, photo, document) and generated files become artifacts.
|
|
7
|
-
-
|
|
8
|
-
- Tools are isolated
|
|
9
|
-
- No tools ship with the core
|
|
8
|
+
- The tool registry handles tool discovery, help lookup, config writes, and execution.
|
|
9
|
+
- Tools are isolated packages with their own manifest, entrypoint, and config defaults.
|
|
10
|
+
- No tools ship with the core; installed tools live under `~/.arisa/tools/<toolName>`.
|
|
11
|
+
- The Arisa install directory (your working directory) contains only the core. Never create or install tools inside it.
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
Modifying the Arisa core is the last resort, never the default. All work must be done in tools.
|
|
13
|
-
- Always solve the request by creating or editing a tool under `~/.arisa/tools/<toolName>`.
|
|
14
|
-
- Treat changing core code as the final option, only after confirming the capability genuinely cannot be delivered through the tool architecture.
|
|
15
|
-
- Never modify the core on your own initiative. Always consult the user first, explaining why a core change is unavoidable, and wait for explicit approval before touching core code.
|
|
13
|
+
New capabilities belong in tools by default. Solve requests by creating or editing a tool under `~/.arisa/tools/<toolName>`. Modifying core is the last resort: do it only after confirming the capability cannot be delivered through the tool architecture, explaining why the core change is unavoidable, and receiving explicit user approval.
|
|
16
14
|
|
|
17
15
|
## Runtime directory rules
|
|
18
16
|
Do not build runtime paths by hand. Use `src/runtime/paths.js`:
|
|
@@ -39,9 +37,9 @@ Each tool declares in `tool.manifest.json`:
|
|
|
39
37
|
- `skillHints`: optional skills to apply when using or editing the tool
|
|
40
38
|
|
|
41
39
|
## Tool-to-Arisa IPC
|
|
42
|
-
|
|
40
|
+
Tools that expose a web UI or HTTP endpoint own that server, usually through the shared daemon runtime; Arisa core does not mount tool routes or proxies. Use Arisa IPC when a tool needs registered tools, artifacts, tasks, agent events, or runtime paths.
|
|
43
41
|
|
|
44
|
-
|
|
42
|
+
Import the IPC client through `ARISA_PACKAGE_DIR`:
|
|
45
43
|
|
|
46
44
|
```js
|
|
47
45
|
import path from "node:path";
|
|
@@ -52,11 +50,6 @@ const { createArisaClient } = await importCore("core/tools/ipc-client.js");
|
|
|
52
50
|
|
|
53
51
|
const arisa = createArisaClient({ toolName: "example-tool", chatId });
|
|
54
52
|
await arisa.artifacts.createText({ text: "hello" });
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
For tool-owned web UI request/response flows, call another registered tool through IPC instead of adding core HTTP routes or proxies:
|
|
58
|
-
|
|
59
|
-
```js
|
|
60
53
|
const result = await arisa.tools.run({
|
|
61
54
|
name: "strudel-agent",
|
|
62
55
|
text: prompt,
|
|
@@ -64,7 +57,7 @@ const result = await arisa.tools.run({
|
|
|
64
57
|
}, { timeoutMs: 120_000 });
|
|
65
58
|
```
|
|
66
59
|
|
|
67
|
-
The IPC channel is a local socket under `~/.arisa/state`. Every request must include `toolName`; chat-scoped capabilities also require `chatId`. Exposed capabilities are explicit: tools (`run`), artifacts (`createText`, `listRecent`, `get`), tasks (`add`, `list`, `cancel`), agent events (`enqueueEvent`), and runtime paths (`getChatToolStateDir`, `getToolStateDir`, `getChatToolTmpDir`, `getToolTmpDir`, `getChatArtifactsDir`). Do not
|
|
60
|
+
The IPC channel is a local socket under `~/.arisa/state`. Every request must include `toolName`; chat-scoped capabilities also require `chatId`. Exposed capabilities are explicit: tools (`run`), artifacts (`createText`, `listRecent`, `get`), tasks (`add`, `list`, `cancel`), agent events (`enqueueEvent`), and runtime paths (`getChatToolStateDir`, `getToolStateDir`, `getChatToolTmpDir`, `getToolTmpDir`, `getChatArtifactsDir`). Do not expose raw `agentManager`, `taskStore`, `artifactStore`, or `toolRegistry` access.
|
|
68
61
|
|
|
69
62
|
## Conceptual pipe model
|
|
70
63
|
There are two different moments where pipes can happen:
|
|
@@ -159,43 +152,31 @@ If `run_tool` returns `missingConfig`, the agent should:
|
|
|
159
152
|
|
|
160
153
|
Do not assume a rigid question/answer protocol. Continue the conversation naturally and infer the config value from the user reply when possible.
|
|
161
154
|
|
|
162
|
-
##
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
When a capability is missing, check the catalog before building anything:
|
|
166
|
-
1. List the catalog: `curl -s https://api.github.com/repos/clasen/Arisa/contents/tools`.
|
|
167
|
-
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.
|
|
168
|
-
3. If a catalog tool solves the problem, decide by install footprint (the `Install footprint` column in the catalog `README.md`):
|
|
169
|
-
- **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.
|
|
170
|
-
- **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.
|
|
171
|
-
- 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.
|
|
172
|
-
4. Only when nothing in the catalog fits, fall back to creating a new tool.
|
|
173
|
-
|
|
174
|
-
### Installing a tool
|
|
175
|
-
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:
|
|
176
|
-
1. Download the tool directory into `~/.arisa/tools/<name>`. For the official catalog, clone shallowly and copy the subdirectory, for example:
|
|
177
|
-
`git clone --depth 1 https://github.com/clasen/Arisa /tmp/arisa-catalog && cp -R /tmp/arisa-catalog/tools/<name> ~/.arisa/tools/<name>`.
|
|
178
|
-
2. Install dependencies inside the tool directory (`pnpm install`, fall back to `npm install`).
|
|
179
|
-
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.
|
|
180
|
-
|
|
181
|
-
## Tool creation
|
|
182
|
-
Reason in terms of capabilities, not tool names.
|
|
155
|
+
## Capability resolution
|
|
156
|
+
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.
|
|
183
157
|
|
|
184
158
|
When the user asks for something new:
|
|
185
|
-
1. check whether an existing registered tool can
|
|
186
|
-
2.
|
|
187
|
-
3.
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
-
|
|
194
|
-
-
|
|
159
|
+
1. check whether an existing registered tool, or an indirect use of one, can satisfy the task
|
|
160
|
+
2. check the official catalog at `https://github.com/clasen/Arisa/tree/main/tools` before building anything
|
|
161
|
+
3. install from the catalog when it fits, or create a new tool only when the needed capability is truly missing
|
|
162
|
+
|
|
163
|
+
To evaluate the official catalog:
|
|
164
|
+
1. List it: `curl -s https://api.github.com/repos/clasen/Arisa/contents/tools`.
|
|
165
|
+
2. Read each candidate manifest at `https://raw.githubusercontent.com/clasen/Arisa/main/tools/<name>/tool.manifest.json`; `description`, `input`, and `output` show whether it solves the need.
|
|
166
|
+
3. Read the catalog `README.md` and use its `Install footprint` column:
|
|
167
|
+
- **Low footprint**: install it and resolve the request in the same turn, without asking first.
|
|
168
|
+
- **Medium/High footprint**: tell the user which tool it is, what it does, and wait for confirmation before installing.
|
|
169
|
+
- Missing config secrets are handled by the missing-config flow and are not, on their own, a reason to ask before installing.
|
|
170
|
+
|
|
171
|
+
When installing any approved or low-footprint tool:
|
|
172
|
+
1. Download it into `~/.arisa/tools/<name>`. For the official catalog, clone shallowly and copy the subdirectory, for example:
|
|
173
|
+
`git clone --depth 1 https://github.com/clasen/Arisa /tmp/arisa-catalog && cp -R /tmp/arisa-catalog/tools/<name> ~/.arisa/tools/<name>`.
|
|
174
|
+
2. Install dependencies inside the tool directory with `pnpm install`, falling back to `npm install`. Do not ask the user to install dependencies manually.
|
|
175
|
+
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.
|
|
195
176
|
|
|
196
177
|
When creating or editing tools:
|
|
197
|
-
-
|
|
198
|
-
- use the path helpers in `src/runtime/paths.js`
|
|
178
|
+
- follow the core boundary above: create or edit installed tools under `~/.arisa/tools/<toolName>`, never inside the Arisa install directory
|
|
179
|
+
- use the path helpers in `src/runtime/paths.js` for state, config, artifacts, and tmp paths
|
|
199
180
|
- 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
|
|
200
181
|
- follow the tools in the official catalog as the reference pattern for new tools
|
|
201
182
|
- keep all help text, usage instructions, manifests, and user-facing operational strings in English
|
|
@@ -214,13 +195,7 @@ Tools may declare skills in `tool.manifest.json`:
|
|
|
214
195
|
|
|
215
196
|
The tool registry resolves these from the installed skills directory and injects them into the tool request as `skills`. `list_tools` exposes the hints and `tool_help` shows their resolution status. Skills are guidance for the agent/tool; they are not separate runtime dependencies.
|
|
216
197
|
|
|
217
|
-
## Dependency installation
|
|
218
|
-
Tool dependencies are installed as part of building or running the tool, not delegated to the user.
|
|
219
|
-
- Prefer `pnpm install`.
|
|
220
|
-
- Fall back to `npm install`.
|
|
221
|
-
- Do not ask the user to do it manually.
|
|
222
|
-
|
|
223
198
|
## Safety
|
|
224
199
|
- Prefer tool manifests and CLI help over assumptions.
|
|
225
|
-
- Keep
|
|
226
|
-
- Be proactive about extending capabilities
|
|
200
|
+
- Keep config and runtime data inside the user runtime area through the path helpers above.
|
|
201
|
+
- Be proactive about extending capabilities through the tool architecture, not ad hoc one-off behavior.
|
package/README.md
CHANGED
|
@@ -132,13 +132,15 @@ Notes:
|
|
|
132
132
|
On first run, Arisa will:
|
|
133
133
|
|
|
134
134
|
1. ask for a Telegram bot token
|
|
135
|
-
2. ask
|
|
136
|
-
3.
|
|
137
|
-
4.
|
|
138
|
-
5.
|
|
135
|
+
2. validate the token and ask whether to continue bootstrap from Telegram (default: yes)
|
|
136
|
+
3. when Telegram setup is selected, show a `https://t.me/<bot>?start=<setup-token>` link
|
|
137
|
+
4. authorize that Telegram chat and continue setup there: Pi provider, model, and Pi auth
|
|
138
|
+
5. ask from Telegram whether Arisa should keep running in background
|
|
139
139
|
6. validate that Pi Agent works
|
|
140
140
|
7. only then start listening to Telegram
|
|
141
141
|
|
|
142
|
+
Choosing `n` at the Telegram setup prompt keeps the previous CLI-only bootstrap flow.
|
|
143
|
+
|
|
142
144
|
Arisa does not run a persistent HTTP health server or Telegram webhook; Telegram uses long polling.
|
|
143
145
|
|
|
144
146
|
Telegram bot tokens can be created with:
|
package/package.json
CHANGED
|
@@ -1,13 +1,26 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { unlink } from "node:fs/promises";
|
|
2
|
+
import { stat, unlink } from "node:fs/promises";
|
|
3
3
|
import { createAgentSession, 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
6
|
import { arisaInstallDir, buildAgentRuntimeContext } from "./runtime-context.js";
|
|
7
7
|
import { withTimeout } from "./prompt-timeout.js";
|
|
8
|
+
import { buildPiToolPolicy, getCoreCodingTools } from "./core-tools.js";
|
|
9
|
+
import { createSystemShellTool } from "./system-shell-tool.js";
|
|
8
10
|
import { arisaHomeDir, getChatPiSessionsDir } from "../../runtime/paths.js";
|
|
9
11
|
|
|
10
12
|
const piValidationTimeoutMs = 60_000;
|
|
13
|
+
const arisaToolNames = [
|
|
14
|
+
"list_tools",
|
|
15
|
+
"tool_help",
|
|
16
|
+
"tool_skills",
|
|
17
|
+
"set_tool_config",
|
|
18
|
+
"run_tool",
|
|
19
|
+
"list_scheduled_tasks",
|
|
20
|
+
"cancel_scheduled_task",
|
|
21
|
+
"cancel_all_scheduled_tasks",
|
|
22
|
+
"send_media_reply"
|
|
23
|
+
];
|
|
11
24
|
|
|
12
25
|
function isLocalBaseUrl(value) {
|
|
13
26
|
if (typeof value !== "string" || !value.trim()) return false;
|
|
@@ -68,6 +81,13 @@ function selectMediaReplyTool(toolRegistry) {
|
|
|
68
81
|
return tools.find(looksLikeTextToSpeechTool) || tools[0] || null;
|
|
69
82
|
}
|
|
70
83
|
|
|
84
|
+
async function assertDirectory(dir, label) {
|
|
85
|
+
const stats = await stat(dir);
|
|
86
|
+
if (!stats.isDirectory()) {
|
|
87
|
+
throw new Error(`${label} is not a directory: ${dir}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
71
91
|
export class AgentManager {
|
|
72
92
|
constructor({ config, artifactStore, toolRegistry, taskStore, logger }) {
|
|
73
93
|
this.config = config;
|
|
@@ -95,15 +115,15 @@ export class AgentManager {
|
|
|
95
115
|
this.sessions.delete(String(chatId));
|
|
96
116
|
}
|
|
97
117
|
|
|
98
|
-
createSessionManager(chatId) {
|
|
118
|
+
createSessionManager(chatId, workspaceDir = arisaInstallDir) {
|
|
99
119
|
const sessionKey = String(chatId);
|
|
100
120
|
const sessionDir = getChatPiSessionsDir(sessionKey);
|
|
101
121
|
if (this.pendingNewSessions.has(sessionKey)) {
|
|
102
122
|
this.logger?.log("agent", `starting new persisted session for chat ${sessionKey}`);
|
|
103
|
-
return { sessionManager: SessionManager.create(
|
|
123
|
+
return { sessionManager: SessionManager.create(workspaceDir, sessionDir), isNewSession: true };
|
|
104
124
|
}
|
|
105
125
|
this.logger?.log("agent", `recovering persisted session for chat ${sessionKey}`);
|
|
106
|
-
return { sessionManager: SessionManager.continueRecent(
|
|
126
|
+
return { sessionManager: SessionManager.continueRecent(workspaceDir, sessionDir), isNewSession: false };
|
|
107
127
|
}
|
|
108
128
|
|
|
109
129
|
async validatePiAgent() {
|
|
@@ -156,23 +176,36 @@ export class AgentManager {
|
|
|
156
176
|
throw new Error(`No auth found for ${this.config.pi.provider}. Re-run bootstrap and complete login for this provider before Telegram starts.`);
|
|
157
177
|
}
|
|
158
178
|
|
|
159
|
-
const
|
|
179
|
+
const policy = buildPiToolPolicy({
|
|
180
|
+
config: this.config,
|
|
181
|
+
customToolNames: [...arisaToolNames, "system_shell"]
|
|
182
|
+
});
|
|
183
|
+
await assertDirectory(policy.workspaceDir, "pi.workspaceDir");
|
|
184
|
+
const { sessionManager, isNewSession } = this.createSessionManager(sessionKey, policy.workspaceDir);
|
|
160
185
|
const hasExistingSession = sessionManager.buildSessionContext().messages.length > 0;
|
|
161
186
|
this.logger?.log("agent", `${hasExistingSession ? "resuming" : "creating"} session for chat ${sessionKey} with model ${effectiveModelId}`);
|
|
162
|
-
const customTools =
|
|
187
|
+
const customTools = [
|
|
188
|
+
...this.createTools(telegram, chatId, policy),
|
|
189
|
+
createSystemShellTool({ workspaceDir: policy.workspaceDir, shell: policy.shell })
|
|
190
|
+
];
|
|
163
191
|
const { session } = await createAgentSession({
|
|
164
|
-
cwd:
|
|
192
|
+
cwd: policy.workspaceDir,
|
|
165
193
|
agentDir: arisaHomeDir,
|
|
166
194
|
authStorage,
|
|
167
195
|
modelRegistry,
|
|
168
196
|
model,
|
|
197
|
+
tools: policy.tools,
|
|
198
|
+
excludeTools: policy.excludeTools,
|
|
169
199
|
customTools,
|
|
170
200
|
sessionManager
|
|
171
201
|
});
|
|
172
202
|
|
|
173
203
|
if (!hasExistingSession) {
|
|
174
204
|
this.logger?.log("agent", `created new session for chat ${sessionKey}`);
|
|
175
|
-
this.logger?.log("agent", `runtime context for chat ${sessionKey}:\n${buildAgentRuntimeContext(
|
|
205
|
+
this.logger?.log("agent", `runtime context for chat ${sessionKey}:\n${buildAgentRuntimeContext({
|
|
206
|
+
workspaceDir: policy.workspaceDir,
|
|
207
|
+
coreTools: policy.coreTools
|
|
208
|
+
})}`);
|
|
176
209
|
}
|
|
177
210
|
|
|
178
211
|
const ctx = { session, modelId: effectiveModelId };
|
|
@@ -226,20 +259,44 @@ export class AgentManager {
|
|
|
226
259
|
return result;
|
|
227
260
|
}
|
|
228
261
|
|
|
229
|
-
createTools(telegram, chatId) {
|
|
262
|
+
createTools(telegram, chatId, policy = buildPiToolPolicy({ config: this.config, customToolNames: arisaToolNames })) {
|
|
230
263
|
const chatArtifactStore = this.artifactStore.forChat(chatId);
|
|
231
264
|
|
|
232
265
|
return [
|
|
233
266
|
defineTool({
|
|
234
267
|
name: "list_tools",
|
|
235
268
|
label: "List tools",
|
|
236
|
-
description: "List
|
|
269
|
+
description: "List Arisa core, native shell, and modular CLI tools with their capabilities.",
|
|
237
270
|
parameters: Type.Object({}),
|
|
238
271
|
execute: async () => {
|
|
239
272
|
await this.toolRegistry.load();
|
|
273
|
+
const coreTools = getCoreCodingTools({
|
|
274
|
+
tools: policy.tools,
|
|
275
|
+
excludeTools: policy.excludeTools
|
|
276
|
+
});
|
|
277
|
+
const nativeTools = [{
|
|
278
|
+
name: "system_shell",
|
|
279
|
+
source: "arisa-native",
|
|
280
|
+
description: "Run native system shell commands in the active Arisa workspace.",
|
|
281
|
+
workspaceDir: policy.workspaceDir,
|
|
282
|
+
shell: policy.shell.shellPath || (process.platform === "win32" ? "powershell" : "sh"),
|
|
283
|
+
enabled: !(policy.excludeTools || []).includes("system_shell")
|
|
284
|
+
}];
|
|
285
|
+
const cliTools = this.toolRegistry.list().map((tool) => ({
|
|
286
|
+
...tool,
|
|
287
|
+
source: "arisa-modular",
|
|
288
|
+
invocation: "run_tool"
|
|
289
|
+
}));
|
|
290
|
+
const result = {
|
|
291
|
+
workspaceDir: policy.workspaceDir,
|
|
292
|
+
coreTools,
|
|
293
|
+
nativeTools,
|
|
294
|
+
cliTools,
|
|
295
|
+
tools: [...coreTools.filter((tool) => tool.enabled), ...nativeTools.filter((tool) => tool.enabled), ...cliTools]
|
|
296
|
+
};
|
|
240
297
|
return {
|
|
241
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
242
|
-
details:
|
|
298
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
299
|
+
details: result
|
|
243
300
|
};
|
|
244
301
|
}
|
|
245
302
|
}),
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { arisaHomeDir } from "../../runtime/paths.js";
|
|
4
|
+
|
|
5
|
+
const defaultShellTimeoutMs = 60_000;
|
|
6
|
+
|
|
7
|
+
export const coreCodingToolCatalog = [
|
|
8
|
+
{
|
|
9
|
+
name: "read",
|
|
10
|
+
source: "pi-builtin",
|
|
11
|
+
description: "Read files and supported images from the active workspace.",
|
|
12
|
+
defaultEnabled: true
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: "bash",
|
|
16
|
+
source: "pi-builtin",
|
|
17
|
+
description: "Run bash-compatible commands from the active workspace.",
|
|
18
|
+
defaultEnabled: true
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: "edit",
|
|
22
|
+
source: "pi-builtin",
|
|
23
|
+
description: "Patch existing files in the active workspace.",
|
|
24
|
+
defaultEnabled: true
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: "write",
|
|
28
|
+
source: "pi-builtin",
|
|
29
|
+
description: "Create or overwrite files in the active workspace.",
|
|
30
|
+
defaultEnabled: true
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: "grep",
|
|
34
|
+
source: "pi-builtin",
|
|
35
|
+
description: "Search file contents from the active workspace.",
|
|
36
|
+
defaultEnabled: false
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: "find",
|
|
40
|
+
source: "pi-builtin",
|
|
41
|
+
description: "Find files from the active workspace.",
|
|
42
|
+
defaultEnabled: false
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "ls",
|
|
46
|
+
source: "pi-builtin",
|
|
47
|
+
description: "List directories from the active workspace.",
|
|
48
|
+
defaultEnabled: false
|
|
49
|
+
}
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
function unique(values) {
|
|
53
|
+
return [...new Set(values)];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function expandHomeDir(value) {
|
|
57
|
+
if (typeof value !== "string") return value;
|
|
58
|
+
if (value === "~") return os.homedir();
|
|
59
|
+
if (value.startsWith("~/") || value.startsWith("~\\")) {
|
|
60
|
+
return path.join(os.homedir(), value.slice(2));
|
|
61
|
+
}
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function normalizeToolList(value) {
|
|
66
|
+
if (Array.isArray(value)) {
|
|
67
|
+
const tools = value
|
|
68
|
+
.map((item) => String(item || "").trim())
|
|
69
|
+
.filter(Boolean);
|
|
70
|
+
return tools.length ? unique(tools) : undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (typeof value === "string") {
|
|
74
|
+
const tools = value
|
|
75
|
+
.split(",")
|
|
76
|
+
.map((item) => item.trim())
|
|
77
|
+
.filter(Boolean);
|
|
78
|
+
return tools.length ? unique(tools) : undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function normalizePositiveInteger(value, fallback) {
|
|
85
|
+
const number = Number(value);
|
|
86
|
+
if (!Number.isFinite(number) || number <= 0) return fallback;
|
|
87
|
+
return Math.floor(number);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function resolveWorkspaceDir(value, defaultWorkspaceDir = arisaHomeDir) {
|
|
91
|
+
const raw = typeof value === "string" && value.trim()
|
|
92
|
+
? value.trim()
|
|
93
|
+
: defaultWorkspaceDir;
|
|
94
|
+
return path.resolve(expandHomeDir(raw));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function buildAllowedTools({ configuredTools, customToolNames }) {
|
|
98
|
+
if (!configuredTools) return undefined;
|
|
99
|
+
return unique([...configuredTools, ...customToolNames]);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function getCoreCodingTools({ tools, excludeTools } = {}) {
|
|
103
|
+
const allowed = tools ? new Set(tools) : null;
|
|
104
|
+
const excluded = new Set(excludeTools || []);
|
|
105
|
+
|
|
106
|
+
return coreCodingToolCatalog.map((tool) => {
|
|
107
|
+
const enabled = allowed
|
|
108
|
+
? allowed.has(tool.name)
|
|
109
|
+
: tool.defaultEnabled;
|
|
110
|
+
return {
|
|
111
|
+
...tool,
|
|
112
|
+
enabled: enabled && !excluded.has(tool.name)
|
|
113
|
+
};
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function buildPiToolPolicy({
|
|
118
|
+
config,
|
|
119
|
+
customToolNames = [],
|
|
120
|
+
defaultWorkspaceDir = arisaHomeDir
|
|
121
|
+
} = {}) {
|
|
122
|
+
const configuredTools = normalizeToolList(config?.pi?.tools);
|
|
123
|
+
const excludeTools = normalizeToolList(config?.pi?.excludeTools);
|
|
124
|
+
const tools = buildAllowedTools({ configuredTools, customToolNames });
|
|
125
|
+
const workspaceDir = resolveWorkspaceDir(config?.pi?.workspaceDir, defaultWorkspaceDir);
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
workspaceDir,
|
|
129
|
+
tools,
|
|
130
|
+
excludeTools,
|
|
131
|
+
coreTools: getCoreCodingTools({ tools, excludeTools }),
|
|
132
|
+
shell: {
|
|
133
|
+
shellPath: typeof config?.pi?.shellPath === "string" && config.pi.shellPath.trim()
|
|
134
|
+
? config.pi.shellPath.trim()
|
|
135
|
+
: "",
|
|
136
|
+
timeoutMs: normalizePositiveInteger(config?.pi?.shellTimeoutMs, defaultShellTimeoutMs)
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
}
|
|
@@ -1,15 +1,26 @@
|
|
|
1
1
|
import { fileURLToPath } from "node:url";
|
|
2
|
-
import { arisaHomeDir, chatsDir, stateDir, toolStateDir, toolsDir } from "../../runtime/paths.js";
|
|
2
|
+
import { arisaHomeDir, arisaPackageDir, chatsDir, stateDir, toolStateDir, toolsDir } from "../../runtime/paths.js";
|
|
3
3
|
|
|
4
4
|
export const arisaInstallDir = fileURLToPath(new URL("../../..", import.meta.url));
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
function formatCoreTools(coreTools = []) {
|
|
7
|
+
const enabled = coreTools
|
|
8
|
+
.filter((tool) => tool.enabled)
|
|
9
|
+
.map((tool) => tool.name);
|
|
10
|
+
return enabled.length ? enabled.join(", ") : "(none)";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function buildAgentRuntimeContext({ workspaceDir = arisaHomeDir, coreTools = [] } = {}) {
|
|
7
14
|
return [
|
|
15
|
+
`workspaceDir: ${workspaceDir}`,
|
|
8
16
|
`arisaHomeDir: ${arisaHomeDir}`,
|
|
17
|
+
`arisaPackageDir: ${arisaPackageDir}`,
|
|
9
18
|
`arisaInstallDir: ${arisaInstallDir}`,
|
|
10
19
|
`userToolsDir: ${toolsDir}`,
|
|
11
20
|
`toolStateDir: ${toolStateDir}`,
|
|
12
21
|
`chatsDir: ${chatsDir}`,
|
|
13
|
-
`stateDir: ${stateDir}
|
|
22
|
+
`stateDir: ${stateDir}`,
|
|
23
|
+
`enabledCoreTools: ${formatCoreTools(coreTools)}`,
|
|
24
|
+
"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."
|
|
14
25
|
].join("\n");
|
|
15
26
|
}
|