@inetafrica/open-claudia 2.6.58 → 2.7.0
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/CHANGELOG.md +7 -0
- package/README.md +8 -3
- package/bin/cli.js +5 -1
- package/bin/kpi.js +55 -0
- package/bin/tool.js +177 -23
- package/core/dream.js +384 -11
- package/core/pack-review.js +32 -4
- package/core/recall/discoverer.js +92 -24
- package/core/recall/graph.js +29 -2
- package/core/recall/kpi.js +227 -0
- package/core/recall/metrics.js +198 -4
- package/core/recall/tuning.js +125 -0
- package/core/runner.js +4 -0
- package/core/system-prompt.js +1 -0
- package/core/tool-repo.js +270 -0
- package/core/tools.js +268 -59
- package/core/transcript-index.js +21 -1
- package/package.json +2 -2
- package/test-recall-discoverer.js +21 -6
- package/test-tools.js +139 -19
package/test-tools.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Reusable tools: a script the agent crystallises must register with a parseable
|
|
2
|
-
// header,
|
|
3
|
-
// the
|
|
2
|
+
// header, live in the git-tracked directory layout (tool + TOOL.md + state.json),
|
|
3
|
+
// run with the keyring merged into its env (preauth), be risk-gated, and be
|
|
4
|
+
// findable by the always-on tool index. Exercises core/tools.js + core/tool-repo.js
|
|
4
5
|
// in an isolated TOOLS_DIR so nothing touches the real ~/.open-claudia/tools.
|
|
5
6
|
const assert = require("assert");
|
|
6
7
|
const fs = require("fs");
|
|
@@ -11,9 +12,11 @@ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "tools-test-"));
|
|
|
11
12
|
process.env.TOOLS_DIR = path.join(tmp, "tools");
|
|
12
13
|
|
|
13
14
|
const tools = require("./core/tools");
|
|
15
|
+
const repo = require("./core/tool-repo");
|
|
14
16
|
|
|
15
17
|
// ── add a bare script (no header): a header is synthesised from opts, shebang
|
|
16
|
-
// is preserved on line 1,
|
|
18
|
+
// is preserved on line 1, the tool lands in the directory layout, and the
|
|
19
|
+
// undeclared risk tier defaults to write (fail-safe) ──
|
|
17
20
|
const srcA = path.join(tmp, "hello.sh");
|
|
18
21
|
fs.writeFileSync(srcA, "#!/usr/bin/env bash\necho \"hi $1\"\n");
|
|
19
22
|
const a = tools.addTool(srcA, {
|
|
@@ -31,6 +34,20 @@ assert.strictEqual(a.usage, "hello <name>", "usage parsed back");
|
|
|
31
34
|
assert.ok(a.executable, "stored tool is chmod +x");
|
|
32
35
|
assert.ok(a.content.startsWith("#!/usr/bin/env bash\n"), "shebang stays on line 1");
|
|
33
36
|
assert.ok(/# open-claudia-tool: hello/.test(a.content), "marker line injected with '#' prefix for a shell script");
|
|
37
|
+
assert.strictEqual(a.risk, "write", "undeclared risk defaults to write");
|
|
38
|
+
assert.strictEqual(a.defaultedRisk, true, "defaulted risk is flagged for the CLI warning");
|
|
39
|
+
assert.strictEqual(a.layout, "dir", "new tools land in the directory layout");
|
|
40
|
+
assert.strictEqual(a.file, path.join(process.env.TOOLS_DIR, "hello", "tool"), "executable lives at <name>/tool");
|
|
41
|
+
assert.ok(fs.existsSync(path.join(process.env.TOOLS_DIR, "hello", "TOOL.md")), "TOOL.md skeleton generated");
|
|
42
|
+
assert.ok(fs.existsSync(path.join(process.env.TOOLS_DIR, "hello", "state.json")), "state.json created");
|
|
43
|
+
assert.ok(/## Known issues/.test(tools.readToolDoc("hello")), "TOOL.md carries the four sections");
|
|
44
|
+
|
|
45
|
+
// ── explicit risk is honoured and normalised ──
|
|
46
|
+
const srcRo = path.join(tmp, "peek.sh");
|
|
47
|
+
fs.writeFileSync(srcRo, "#!/usr/bin/env bash\necho peek\n");
|
|
48
|
+
const ro = tools.addTool(srcRo, { name: "peek", description: "look at things", risk: "READONLY" });
|
|
49
|
+
assert.strictEqual(ro.risk, "read-only", "risk value is normalised (READONLY → read-only)");
|
|
50
|
+
assert.ok(!ro.defaultedRisk, "explicit risk is not flagged as defaulted");
|
|
34
51
|
|
|
35
52
|
// ── findTool / listTools see it ──
|
|
36
53
|
assert.strictEqual(tools.findTool("hello").name, "hello", "findTool resolves by name");
|
|
@@ -45,7 +62,8 @@ assert.ok(/\/\/ open-claudia-tool: fetch/.test(b.content), "JS script gets '//'
|
|
|
45
62
|
assert.strictEqual(b.requires.length, 0, "no requires when none supplied");
|
|
46
63
|
|
|
47
64
|
// ── a source that already declares its own header is respected; a --pack passed
|
|
48
|
-
// in is grafted on without clobbering the existing marker
|
|
65
|
+
// in is grafted on without clobbering the existing marker; missing risk is
|
|
66
|
+
// grafted as write ──
|
|
49
67
|
const srcC = path.join(tmp, "self-doc.sh");
|
|
50
68
|
fs.writeFileSync(
|
|
51
69
|
srcC,
|
|
@@ -55,6 +73,24 @@ const c = tools.addTool(srcC, { pack: "linked-pack" });
|
|
|
55
73
|
assert.strictEqual(c.name, "selfdoc", "existing marker name wins");
|
|
56
74
|
assert.strictEqual(c.description, "pre-documented", "existing description preserved");
|
|
57
75
|
assert.strictEqual(c.pack, "linked-pack", "--pack grafted onto a self-documented script");
|
|
76
|
+
assert.strictEqual(c.risk, "write", "missing risk grafted as write on a self-documented script");
|
|
77
|
+
assert.strictEqual((c.content.match(/open-claudia-tool: selfdoc/g) || []).length, 1, "marker survives exactly once");
|
|
78
|
+
|
|
79
|
+
// ── a source declaring its own risk keeps it ──
|
|
80
|
+
const srcD = path.join(tmp, "declared.sh");
|
|
81
|
+
fs.writeFileSync(
|
|
82
|
+
srcD,
|
|
83
|
+
"#!/usr/bin/env bash\n# open-claudia-tool: declared\n# risk: destructive\necho boom\n"
|
|
84
|
+
);
|
|
85
|
+
const d = tools.addTool(srcD, {});
|
|
86
|
+
assert.strictEqual(d.risk, "destructive", "declared risk tier survives add");
|
|
87
|
+
assert.ok(!d.defaultedRisk, "declared risk is not flagged as defaulted");
|
|
88
|
+
|
|
89
|
+
// ── addTool REFUSES a hardcoded secret (the tools repo history is forever) ──
|
|
90
|
+
const srcSecret = path.join(tmp, "leaky.sh");
|
|
91
|
+
fs.writeFileSync(srcSecret, "#!/usr/bin/env bash\ncurl -H 'x: sk-ant-abcdefgh12345678'\n");
|
|
92
|
+
assert.throws(() => tools.addTool(srcSecret, { name: "leaky" }), /secret/i, "hardcoded secret blocks the save");
|
|
93
|
+
assert.strictEqual(tools.findTool("leaky"), null, "refused tool is not registered");
|
|
58
94
|
|
|
59
95
|
// ── missingRequires: keys absent from keyring AND env are reported; present env
|
|
60
96
|
// keys are not ──
|
|
@@ -69,17 +105,33 @@ delete process.env.api_key;
|
|
|
69
105
|
const env = tools.runEnv();
|
|
70
106
|
assert.ok(env && typeof env === "object" && env.PATH, "runEnv yields an env object carrying PATH");
|
|
71
107
|
|
|
72
|
-
// ── toolNameFromPath
|
|
108
|
+
// ── toolNameFromPath recognises legacy flat files and the directory layout's
|
|
109
|
+
// agent-editable members; state.json and outside paths do not resolve ──
|
|
73
110
|
assert.strictEqual(
|
|
74
|
-
tools.toolNameFromPath(path.join(process.env.TOOLS_DIR, "hello")),
|
|
111
|
+
tools.toolNameFromPath(path.join(process.env.TOOLS_DIR, "hello", "tool")),
|
|
75
112
|
"hello",
|
|
76
|
-
"
|
|
113
|
+
"<name>/tool resolves to the tool name"
|
|
114
|
+
);
|
|
115
|
+
assert.strictEqual(
|
|
116
|
+
tools.toolNameFromPath(path.join(process.env.TOOLS_DIR, "hello", "TOOL.md")),
|
|
117
|
+
"hello",
|
|
118
|
+
"<name>/TOOL.md resolves to the tool name"
|
|
119
|
+
);
|
|
120
|
+
assert.strictEqual(
|
|
121
|
+
tools.toolNameFromPath(path.join(process.env.TOOLS_DIR, "hello", "state.json")),
|
|
122
|
+
null,
|
|
123
|
+
"state.json is machine telemetry — not a tool edit"
|
|
124
|
+
);
|
|
125
|
+
assert.strictEqual(
|
|
126
|
+
tools.toolNameFromPath(path.join(process.env.TOOLS_DIR, "legacy-flat")),
|
|
127
|
+
"legacy-flat",
|
|
128
|
+
"a direct child of TOOLS_DIR still resolves (legacy layout)"
|
|
77
129
|
);
|
|
78
130
|
assert.strictEqual(tools.toolNameFromPath("/etc/passwd"), null, "a path outside TOOLS_DIR → null");
|
|
79
131
|
assert.strictEqual(
|
|
80
|
-
tools.toolNameFromPath(path.join(process.env.TOOLS_DIR, "sub", "deep")),
|
|
132
|
+
tools.toolNameFromPath(path.join(process.env.TOOLS_DIR, "sub", "deep", "deeper")),
|
|
81
133
|
null,
|
|
82
|
-
"a
|
|
134
|
+
"a 3-deep path under TOOLS_DIR → null"
|
|
83
135
|
);
|
|
84
136
|
|
|
85
137
|
// ── parseHeader returns null for a script with no marker line ──
|
|
@@ -95,15 +147,53 @@ assert.ok(!mFetch.some((m) => m.name === "hello"), "an unrelated tool does not m
|
|
|
95
147
|
assert.deepStrictEqual(tools.matchTools("zzzznomatchatall"), [], "no terms hit → empty match");
|
|
96
148
|
assert.deepStrictEqual(tools.matchTools(""), [], "empty query → empty match");
|
|
97
149
|
|
|
98
|
-
// ──
|
|
99
|
-
//
|
|
150
|
+
// ── recordRunResult telemetry: stamps state.json with run counts, exit codes,
|
|
151
|
+
// per-verb health, and a capped history ──
|
|
100
152
|
assert.strictEqual(tools.findTool("hello").runCount, 0, "fresh tool has runCount 0");
|
|
101
|
-
tools.
|
|
102
|
-
|
|
103
|
-
assert.
|
|
153
|
+
tools.recordRunResult("hello", { args: ["greet", "world"], exit: 0, ms: 42 });
|
|
154
|
+
let hState = tools.readState("hello");
|
|
155
|
+
assert.strictEqual(hState.runCount, 1, "recordRunResult bumps runCount in state.json");
|
|
156
|
+
assert.ok(hState.lastUsed, "recordRunResult stamps lastUsed");
|
|
157
|
+
assert.strictEqual(hState.lastExit, 0, "recordRunResult records the exit code");
|
|
158
|
+
assert.ok(hState.lastSuccess, "a zero exit stamps lastSuccess");
|
|
159
|
+
assert.strictEqual(hState.verbs.greet.runs, 1, "per-verb run count tracked (verb = first non-flag arg)");
|
|
160
|
+
assert.ok(hState.verbs.greet.lastSuccess, "per-verb lastSuccess tracked");
|
|
161
|
+
assert.strictEqual(hState.history[0].args, "greet world", "history records the arg line");
|
|
104
162
|
assert.strictEqual(tools.findTool("hello").runCount, 1, "runCount surfaces on the tool record");
|
|
163
|
+
|
|
164
|
+
tools.recordRunResult("hello", { args: ["--yes", "assign"], exit: 2, ms: 10 });
|
|
165
|
+
hState = tools.readState("hello");
|
|
166
|
+
assert.strictEqual(hState.runCount, 2, "a second run increments again");
|
|
167
|
+
assert.strictEqual(hState.lastExit, 2, "failure exit recorded");
|
|
168
|
+
assert.ok(hState.lastFailure, "a non-zero exit stamps lastFailure");
|
|
169
|
+
assert.strictEqual(hState.verbs.assign.lastExit, 2, "flags are skipped when picking the verb");
|
|
170
|
+
|
|
171
|
+
for (let i = 0; i < 25; i++) tools.recordRunResult("hello", { args: ["spam"], exit: 0, ms: 1 });
|
|
172
|
+
assert.ok(tools.readState("hello").history.length <= 20, "run history is capped");
|
|
173
|
+
|
|
174
|
+
// ── recordRun (end-of-turn transcript parse) is a no-op for directory tools —
|
|
175
|
+
// the CLI already stamped the authoritative result; double-counting would lie ──
|
|
176
|
+
const beforeNoop = tools.readState("hello").runCount;
|
|
105
177
|
tools.recordRun("hello");
|
|
106
|
-
assert.strictEqual(tools.
|
|
178
|
+
assert.strictEqual(tools.readState("hello").runCount, beforeNoop, "recordRun no-ops on a dir-layout tool");
|
|
179
|
+
|
|
180
|
+
// ── legacy flat tools: recordRun still feeds the sidecar until migration ──
|
|
181
|
+
const legacyFile = path.join(process.env.TOOLS_DIR, "oldy");
|
|
182
|
+
fs.writeFileSync(legacyFile, "#!/usr/bin/env bash\n# open-claudia-tool: oldy\n# description: legacy widget probe\necho old\n", { mode: 0o700 });
|
|
183
|
+
assert.strictEqual(tools.findTool("oldy").layout, "file", "flat file reads as legacy layout");
|
|
184
|
+
tools.recordRun("oldy");
|
|
185
|
+
assert.strictEqual(tools.readUsage().oldy.runCount, 1, "recordRun bumps the legacy sidecar");
|
|
186
|
+
assert.strictEqual(tools.findTool("oldy").runCount, 1, "legacy runCount surfaces on the tool record");
|
|
187
|
+
|
|
188
|
+
// ── migration: flat file → directory, telemetry carried over, sidecar cleaned ──
|
|
189
|
+
const mig = repo.migrateLegacyTools();
|
|
190
|
+
assert.ok(mig.migrated.includes("oldy"), "legacy tool is migrated");
|
|
191
|
+
const oldy = tools.findTool("oldy");
|
|
192
|
+
assert.strictEqual(oldy.layout, "dir", "migrated tool now uses the directory layout");
|
|
193
|
+
assert.strictEqual(oldy.runCount, 1, "telemetry carried over from the sidecar");
|
|
194
|
+
assert.ok(fs.existsSync(path.join(process.env.TOOLS_DIR, "oldy", "TOOL.md")), "migration generates TOOL.md");
|
|
195
|
+
assert.ok(!tools.readUsage().oldy, "sidecar entry cleaned after migration");
|
|
196
|
+
assert.ok(!repo.hasLegacyTools(), "no legacy tools remain after migration");
|
|
107
197
|
|
|
108
198
|
// ── matchTools tie-break: equal score → more-run tool ranks first ──
|
|
109
199
|
for (const n of ["alpha", "beta"]) {
|
|
@@ -111,7 +201,7 @@ for (const n of ["alpha", "beta"]) {
|
|
|
111
201
|
fs.writeFileSync(s, "#!/usr/bin/env bash\necho x\n");
|
|
112
202
|
tools.addTool(s, { name: n, description: "common widget gadget" });
|
|
113
203
|
}
|
|
114
|
-
tools.
|
|
204
|
+
tools.recordRunResult("beta", { args: [], exit: 0, ms: 1 });
|
|
115
205
|
const ranked = tools.matchTools("common widget gadget").map((m) => m.name);
|
|
116
206
|
assert.ok(ranked.indexOf("beta") < ranked.indexOf("alpha"), "on equal score, the more-run tool ranks first");
|
|
117
207
|
|
|
@@ -123,6 +213,25 @@ const bornHello = tools.findTool("hello").createdAt;
|
|
|
123
213
|
tools.recordCreate("hello"); // a second create-record must NOT move the stamp
|
|
124
214
|
assert.strictEqual(tools.findTool("hello").createdAt, bornHello, "createdAt is immutable once set");
|
|
125
215
|
|
|
216
|
+
// ── runGate: read-only runs freely; write demands --yes; destructive demands
|
|
217
|
+
// --yes-destructive (a plain --yes must NOT unlock it); undeclared → write ──
|
|
218
|
+
assert.ok(tools.runGate({ name: "t", risk: "read-only" }, []).ok, "read-only runs without flags");
|
|
219
|
+
assert.ok(!tools.runGate({ name: "t", risk: "write" }, []).ok, "write without --yes is refused");
|
|
220
|
+
assert.ok(tools.runGate({ name: "t", risk: "write" }, ["--yes"]).ok, "write with --yes runs");
|
|
221
|
+
assert.ok(!tools.runGate({ name: "t", risk: "destructive" }, ["--yes"]).ok, "plain --yes does not unlock destructive");
|
|
222
|
+
assert.ok(tools.runGate({ name: "t", risk: "destructive" }, ["--yes-destructive"]).ok, "destructive with --yes-destructive runs");
|
|
223
|
+
const undeclared = tools.runGate({ name: "t", risk: "" }, []);
|
|
224
|
+
assert.ok(!undeclared.ok && undeclared.tier === "write", "undeclared tier is treated as write (fail-safe)");
|
|
225
|
+
assert.ok(/destructive/i.test(tools.runGate({ name: "t", risk: "destructive" }, []).message), "destructive refusal spells out the warning");
|
|
226
|
+
|
|
227
|
+
// ── scaffold: creates dir + stub + TOOL.md, registered and runnable ──
|
|
228
|
+
const sc = repo.scaffoldTool("kwidget", { description: "widget system CLI", risk: "read-only", pack: "widgets", usage: "kwidget <verb>" });
|
|
229
|
+
assert.strictEqual(sc.name, "kwidget", "scaffold registers the tool");
|
|
230
|
+
assert.strictEqual(sc.risk, "read-only", "scaffold honours the risk tier");
|
|
231
|
+
assert.ok(sc.executable, "scaffolded stub is executable");
|
|
232
|
+
assert.ok(/## Journal/.test(tools.readToolDoc("kwidget")), "scaffolded TOOL.md has the sections");
|
|
233
|
+
assert.throws(() => repo.scaffoldTool("kwidget", {}), /already exists/, "re-scaffolding an existing tool is refused");
|
|
234
|
+
|
|
126
235
|
// ── envRefsIn: pulls every shape of env read out of a script ──
|
|
127
236
|
const refs = tools.envRefsIn('echo $FOO ${BAR}\nx=process.env.BAZ\ny=process.env["QUX"]\n');
|
|
128
237
|
for (const k of ["FOO", "BAR", "BAZ", "QUX"]) assert.ok(refs.has(k), `envRefsIn finds ${k}`);
|
|
@@ -152,7 +261,7 @@ assert.strictEqual(tools.checkSyntax("/tmp/x.txt", "plain text, no shebang\n"),
|
|
|
152
261
|
|
|
153
262
|
// ── updateToolHeader (the 'fix' verb): refreshes recognised field lines in place,
|
|
154
263
|
// preserves the marker (exactly once), hand-written comments, and createdAt;
|
|
155
|
-
// leaves the body untouched ──
|
|
264
|
+
// leaves the body untouched; sets risk; refuses an invalid tier ──
|
|
156
265
|
const fixSrc = path.join(tmp, "fixme.sh");
|
|
157
266
|
fs.writeFileSync(
|
|
158
267
|
fixSrc,
|
|
@@ -160,22 +269,33 @@ fs.writeFileSync(
|
|
|
160
269
|
);
|
|
161
270
|
const fixed0 = tools.addTool(fixSrc, {});
|
|
162
271
|
const bornFix = fixed0.createdAt;
|
|
163
|
-
const fixed = tools.updateToolHeader("fixme", { description: "new desc", requires: ["WIDGET_KEY"], usage: "fixme <x>" });
|
|
272
|
+
const fixed = tools.updateToolHeader("fixme", { description: "new desc", requires: ["WIDGET_KEY"], usage: "fixme <x>", risk: "read-only" });
|
|
164
273
|
assert.strictEqual(fixed.description, "new desc", "updateToolHeader rewrites the description");
|
|
165
274
|
assert.deepStrictEqual(fixed.requires, ["WIDGET_KEY"], "updateToolHeader sets requires");
|
|
166
275
|
assert.strictEqual(fixed.usage, "fixme <x>", "updateToolHeader sets usage");
|
|
276
|
+
assert.strictEqual(fixed.risk, "read-only", "updateToolHeader sets the risk tier");
|
|
167
277
|
assert.strictEqual(fixed.createdAt, bornFix, "updateToolHeader preserves createdAt");
|
|
168
278
|
assert.ok(/a hand note worth keeping/.test(fixed.content), "updateToolHeader keeps hand-written comments");
|
|
169
279
|
assert.strictEqual((fixed.content.match(/open-claudia-tool: fixme/g) || []).length, 1, "marker survives exactly once");
|
|
170
280
|
assert.ok(/echo "\$WIDGET_KEY"/.test(fixed.content), "updateToolHeader leaves the body untouched");
|
|
281
|
+
assert.throws(() => tools.updateToolHeader("fixme", { risk: "yolo" }), /invalid risk/i, "an invalid tier is refused");
|
|
171
282
|
// requires-string form is normalised to a list
|
|
172
283
|
const fixed2 = tools.updateToolHeader("fixme", { requires: "WIDGET_KEY, OTHER_KEY" });
|
|
173
284
|
assert.deepStrictEqual(fixed2.requires, ["WIDGET_KEY", "OTHER_KEY"], "string requires split into a list");
|
|
174
|
-
// a body that now reads OTHER_KEY would no longer be 'unused' — lint stays in sync
|
|
175
285
|
assert.strictEqual(tools.updateToolHeader("does-not-exist", { description: "x" }), null, "updateToolHeader on a missing tool → null");
|
|
176
286
|
|
|
287
|
+
// ── git history: the tools dir is a repo and changes commit (skipped without git) ──
|
|
288
|
+
try {
|
|
289
|
+
require("child_process").execFileSync("git", ["--version"], { stdio: "pipe" });
|
|
290
|
+
assert.ok(fs.existsSync(path.join(process.env.TOOLS_DIR, ".git")), "tools dir is git-initialised");
|
|
291
|
+
const log = repo.toolLog("hello", 10);
|
|
292
|
+
assert.ok(log.length >= 1, "tool changes are committed (git log has entries)");
|
|
293
|
+
assert.ok(fs.readFileSync(path.join(process.env.TOOLS_DIR, ".gitignore"), "utf-8").includes("state.json"), "state.json is gitignored");
|
|
294
|
+
} catch (e) { console.log(" (git not available — history assertions skipped)"); }
|
|
295
|
+
|
|
177
296
|
// ── remove ──
|
|
178
297
|
assert.ok(tools.removeTool("hello"), "removeTool returns the removed tool");
|
|
179
298
|
assert.strictEqual(tools.findTool("hello"), null, "removed tool is gone");
|
|
299
|
+
assert.ok(!fs.existsSync(path.join(process.env.TOOLS_DIR, "hello")), "removed tool's directory is gone");
|
|
180
300
|
|
|
181
301
|
console.log("tools OK");
|