arisa 4.2.0 → 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 +4 -0
- package/package.json +1 -1
- package/src/core/tools/tool-registry.js +39 -7
- package/test/tool-registry-run.test.js +40 -3
package/AGENTS.md
CHANGED
|
@@ -43,6 +43,8 @@ Each tool declares in `tool.manifest.json`:
|
|
|
43
43
|
- `input`: supported input types
|
|
44
44
|
- `output`: produced output types
|
|
45
45
|
- `configSchema`: required config fields
|
|
46
|
+
- `category`: optional broad capability bucket for discovery
|
|
47
|
+
- `keywords`: optional intent tags for capability discovery
|
|
46
48
|
- `skillHints`: optional skills to apply when using or editing the tool
|
|
47
49
|
|
|
48
50
|
## Tool-to-Arisa IPC
|
|
@@ -166,6 +168,8 @@ Do not assume a rigid question/answer protocol. Continue the conversation natura
|
|
|
166
168
|
## Capability resolution
|
|
167
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.
|
|
168
170
|
|
|
171
|
+
Before asking the user for recurrent context, inspect available tools by `category` and `keywords`, especially `memory`, `context`, `contacts`, and `essential`.
|
|
172
|
+
|
|
169
173
|
When the user asks for something new:
|
|
170
174
|
1. check whether an existing registered tool, or an indirect use of one, can satisfy the task
|
|
171
175
|
2. check the official catalog at `https://github.com/clasen/Arisa/tree/main/tools` before building anything
|
package/package.json
CHANGED
|
@@ -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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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) {
|
|
@@ -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
|
-
|
|
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");
|