@inetafrica/open-claudia 2.6.47 → 2.6.49
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 +3 -0
- package/bin/cli.js +6 -0
- package/bin/pack.js +10 -0
- package/bin/tool.js +148 -0
- package/channels/voice/adapter.js +21 -4
- package/channels/voice/manage.js +285 -0
- package/core/dream.js +23 -2
- package/core/handlers.js +2 -1
- package/core/io.js +11 -1
- package/core/media.js +53 -1
- package/core/runner.js +43 -4
- package/core/system-prompt.js +49 -1
- package/core/tool-graph.js +217 -0
- package/core/tools.js +227 -0
- package/package.json +5 -3
- package/test-tool-graph.js +97 -0
- package/test-tools.js +92 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inetafrica/open-claudia",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.49",
|
|
4
4
|
"description": "Your always-on AI coding assistant — Claude Code, Cursor Agent, and OpenAI Codex via Telegram or Kazee Chat",
|
|
5
5
|
"main": "bot.js",
|
|
6
6
|
"bin": {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"setup": "node setup.js",
|
|
11
11
|
"start": "node bot.js",
|
|
12
|
-
"test": "OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node -e \"require('./vault'); console.log('OK')\" && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-usage-accounting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-recall-engine.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-recall-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-recall-discoverer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-project-transcripts-smoke.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-abilities.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-extraction.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-couse.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-transfer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-tiers.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-merge-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-learning-e2e.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-pack-nesting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-read-signal.js"
|
|
12
|
+
"test": "OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node -e \"require('./vault'); console.log('OK')\" && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-usage-accounting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-recall-engine.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-recall-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-recall-discoverer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-project-transcripts-smoke.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-abilities.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-extraction.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-couse.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-transfer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-tiers.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-merge-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-learning-e2e.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-pack-nesting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-read-signal.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-tools.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-tool-graph.js"
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
15
|
"bot.js",
|
|
@@ -43,7 +43,9 @@
|
|
|
43
43
|
"test-ability-merge-guard.js",
|
|
44
44
|
"test-learning-e2e.js",
|
|
45
45
|
"test-pack-nesting.js",
|
|
46
|
-
"test-read-signal.js"
|
|
46
|
+
"test-read-signal.js",
|
|
47
|
+
"test-tools.js",
|
|
48
|
+
"test-tool-graph.js"
|
|
47
49
|
],
|
|
48
50
|
"keywords": [
|
|
49
51
|
"claude",
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Directed tool-graph: "tool B follows tool A". Unlike the recall graph this is
|
|
2
|
+
// DIRECTED (A→B carries info B→A does not) and has NO structural floor, so an
|
|
3
|
+
// unused chain decays to zero and is pruned. This exercises core/tool-graph.js
|
|
4
|
+
// end to end against an isolated DB so nothing touches the real graph.
|
|
5
|
+
const assert = require("assert");
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const os = require("os");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
|
|
10
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "tool-graph-test-"));
|
|
11
|
+
process.env.TOOL_GRAPH_DB = path.join(tmp, "tool-graph.db");
|
|
12
|
+
|
|
13
|
+
const graph = require("./core/tool-graph");
|
|
14
|
+
|
|
15
|
+
// node:sqlite arrived in node 22; on older runtimes the graph is a no-op. Skip
|
|
16
|
+
// cleanly so the suite stays green rather than failing on the environment.
|
|
17
|
+
if (!graph.available()) {
|
|
18
|
+
console.log("tool-graph OK (node:sqlite unavailable — skipped)");
|
|
19
|
+
process.exit(0);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ── reinforceSequence lays directed edges for IMMEDIATE succession only ──
|
|
23
|
+
graph.reinforceSequence(["auth", "list", "download"]);
|
|
24
|
+
let after = graph.followers("auth");
|
|
25
|
+
assert.strictEqual(after.length, 1, "auth has exactly one follower");
|
|
26
|
+
assert.strictEqual(after[0].name, "list", "list follows auth");
|
|
27
|
+
assert.strictEqual(after[0].weight, 1, "a single co-occurrence weighs 1 (no structural floor)");
|
|
28
|
+
|
|
29
|
+
// non-adjacent pairs are NOT linked — auth→download must not exist
|
|
30
|
+
assert.ok(!graph.followers("auth").some((r) => r.name === "download"), "auth→download skipped (not adjacent)");
|
|
31
|
+
|
|
32
|
+
// ── directionality: the edge is auth→list, never list→auth ──
|
|
33
|
+
assert.ok(graph.followers("list").some((r) => r.name === "download"), "download follows list");
|
|
34
|
+
assert.ok(!graph.followers("list").some((r) => r.name === "auth"), "auth does NOT follow list (directed)");
|
|
35
|
+
assert.strictEqual(graph.predecessors("download")[0].name, "list", "list precedes download");
|
|
36
|
+
assert.ok(graph.predecessors("list").some((r) => r.name === "auth"), "auth precedes list");
|
|
37
|
+
assert.ok(!graph.predecessors("list").some((r) => r.name === "download"), "download does NOT precede list (directed)");
|
|
38
|
+
|
|
39
|
+
// ── Hebbian bump: re-running the chain strengthens the same edges ──
|
|
40
|
+
graph.reinforceSequence(["auth", "list", "download"]);
|
|
41
|
+
assert.strictEqual(graph.followers("auth")[0].weight, 2, "second co-use bumps auth→list to 2");
|
|
42
|
+
|
|
43
|
+
// ── repeated names in a row collapse: a tool re-run in a loop must not self-link ──
|
|
44
|
+
graph.reinforceSequence(["loop", "loop", "loop", "end"]);
|
|
45
|
+
assert.ok(!graph.followers("loop").some((r) => r.name === "loop"), "no loop→loop self-edge");
|
|
46
|
+
assert.strictEqual(graph.followers("loop")[0].name, "end", "loop→end is the only edge from a repeated run");
|
|
47
|
+
|
|
48
|
+
// ── addFollow guards: src===dst is rejected ──
|
|
49
|
+
assert.strictEqual(graph.addFollow("x", "x"), false, "self-edge refused");
|
|
50
|
+
|
|
51
|
+
// ── a pure 'ensure' edge (no bump) has no last_reinforced and so survives decay ──
|
|
52
|
+
graph.addFollow("ensure-src", "ensure-dst"); // weight 1, last_reinforced=null
|
|
53
|
+
const ensured = graph.followers("ensure-src");
|
|
54
|
+
assert.strictEqual(ensured[0].weight, 1, "ensure edge weighs 1");
|
|
55
|
+
|
|
56
|
+
// ── decay: backdate a reinforced edge far into the past, then it prunes (no floor) ──
|
|
57
|
+
const db = graph.openDb();
|
|
58
|
+
const longAgo = new Date(Date.now() - 365 * 86400000).toISOString();
|
|
59
|
+
db.prepare("UPDATE tool_edges SET last_reinforced=? WHERE src=? AND dst=?").run(longAgo, "auth", "list");
|
|
60
|
+
let res = graph.decay({ halfLifeDays: 1, pruneBelow: 0.15 });
|
|
61
|
+
assert.ok(res.pruned >= 1, "a year-old edge under the floor is pruned");
|
|
62
|
+
assert.ok(!graph.followers("auth").some((r) => r.name === "list"), "pruned edge is gone");
|
|
63
|
+
|
|
64
|
+
// the ensure edge (last_reinforced=null) was skipped by decay, not pruned
|
|
65
|
+
assert.ok(graph.followers("ensure-src").some((r) => r.name === "ensure-dst"), "un-reinforced edge survives decay");
|
|
66
|
+
|
|
67
|
+
// ── decay that reduces but keeps an edge above the floor ──
|
|
68
|
+
graph.addFollow("warm-a", "warm-b", { bump: 10 });
|
|
69
|
+
const recent = new Date(Date.now() - 30 * 86400000).toISOString();
|
|
70
|
+
db.prepare("UPDATE tool_edges SET last_reinforced=? WHERE src=? AND dst=?").run(recent, "warm-a", "warm-b");
|
|
71
|
+
graph.decay({ halfLifeDays: 30, pruneBelow: 0.15 }); // one half-life → ~5
|
|
72
|
+
const warm = graph.followers("warm-a")[0];
|
|
73
|
+
assert.ok(warm && warm.name === "warm-b", "warm edge survives a single half-life");
|
|
74
|
+
assert.ok(warm.weight < 10 && warm.weight > 0.15, `warm edge decayed but kept (${warm.weight})`);
|
|
75
|
+
|
|
76
|
+
// ── removeEdge ──
|
|
77
|
+
assert.strictEqual(graph.removeEdge("warm-a", "warm-b"), true, "removeEdge succeeds");
|
|
78
|
+
assert.ok(!graph.followers("warm-a").length, "removed edge gone");
|
|
79
|
+
|
|
80
|
+
// ── pruneOrphans drops edges whose endpoints are no longer registered tools ──
|
|
81
|
+
graph.reinforceSequence(["live", "dead"]); // 'dead' will not be in the live set
|
|
82
|
+
const fakeLib = { listTools: () => [{ name: "live" }, { name: "end" }, { name: "loop" }, { name: "ensure-src" }, { name: "ensure-dst" }] };
|
|
83
|
+
const removed = graph.pruneOrphans(fakeLib);
|
|
84
|
+
assert.ok(removed >= 1, "edge pointing at an unregistered tool ('dead') is pruned");
|
|
85
|
+
assert.ok(!graph.followers("live").length, "live→dead removed because dead is not a live tool");
|
|
86
|
+
|
|
87
|
+
// ── tend() is the nightly entry: decays + prunes orphans + returns stats ──
|
|
88
|
+
const t = graph.tend(fakeLib);
|
|
89
|
+
assert.ok(typeof t.edges === "number" && typeof t.nodes === "number", "tend returns stats");
|
|
90
|
+
assert.ok(typeof t.pruned === "number" && typeof t.decayed === "number", "tend returns decay/prune counts");
|
|
91
|
+
|
|
92
|
+
// ── stats / allEdges sanity ──
|
|
93
|
+
const s = graph.stats();
|
|
94
|
+
assert.strictEqual(s.edges, graph.allEdges().length, "stats edge count matches allEdges");
|
|
95
|
+
|
|
96
|
+
graph._resetForTest();
|
|
97
|
+
console.log("tool-graph OK");
|
package/test-tools.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Reusable tools: a script the agent crystallises must register with a parseable
|
|
2
|
+
// header, run with the keyring merged into its env (preauth), and be findable by
|
|
3
|
+
// the always-on tool index. This exercises the core/tools.js library end to end
|
|
4
|
+
// in an isolated TOOLS_DIR so nothing touches the real ~/.open-claudia/tools.
|
|
5
|
+
const assert = require("assert");
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const os = require("os");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
|
|
10
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "tools-test-"));
|
|
11
|
+
process.env.TOOLS_DIR = path.join(tmp, "tools");
|
|
12
|
+
|
|
13
|
+
const tools = require("./core/tools");
|
|
14
|
+
|
|
15
|
+
// ── add a bare script (no header): a header is synthesised from opts, shebang
|
|
16
|
+
// is preserved on line 1, and the file is made executable ──
|
|
17
|
+
const srcA = path.join(tmp, "hello.sh");
|
|
18
|
+
fs.writeFileSync(srcA, "#!/usr/bin/env bash\necho \"hi $1\"\n");
|
|
19
|
+
const a = tools.addTool(srcA, {
|
|
20
|
+
name: "hello",
|
|
21
|
+
description: "say hi",
|
|
22
|
+
pack: "greetings",
|
|
23
|
+
requires: ["api_key", "api_secret"],
|
|
24
|
+
usage: "hello <name>",
|
|
25
|
+
});
|
|
26
|
+
assert.strictEqual(a.name, "hello", "synthesised tool keeps requested name");
|
|
27
|
+
assert.strictEqual(a.description, "say hi", "description parsed back from synthesised header");
|
|
28
|
+
assert.strictEqual(a.pack, "greetings", "pack link parsed back");
|
|
29
|
+
assert.deepStrictEqual(a.requires, ["api_key", "api_secret"], "requires parsed back as a list");
|
|
30
|
+
assert.strictEqual(a.usage, "hello <name>", "usage parsed back");
|
|
31
|
+
assert.ok(a.executable, "stored tool is chmod +x");
|
|
32
|
+
assert.ok(a.content.startsWith("#!/usr/bin/env bash\n"), "shebang stays on line 1");
|
|
33
|
+
assert.ok(/# open-claudia-tool: hello/.test(a.content), "marker line injected with '#' prefix for a shell script");
|
|
34
|
+
|
|
35
|
+
// ── findTool / listTools see it ──
|
|
36
|
+
assert.strictEqual(tools.findTool("hello").name, "hello", "findTool resolves by name");
|
|
37
|
+
assert.strictEqual(tools.findTool("HELLO").name, "hello", "findTool is case-insensitive (sanitized)");
|
|
38
|
+
assert.ok(tools.listTools().some((t) => t.name === "hello"), "listTools includes the new tool");
|
|
39
|
+
|
|
40
|
+
// ── a JS source with a shebang gets a '//' comment header, not '#' ──
|
|
41
|
+
const srcB = path.join(tmp, "fetch.js");
|
|
42
|
+
fs.writeFileSync(srcB, "#!/usr/bin/env node\nconsole.log('x');\n");
|
|
43
|
+
const b = tools.addTool(srcB, { name: "fetch", description: "fetch a thing" });
|
|
44
|
+
assert.ok(/\/\/ open-claudia-tool: fetch/.test(b.content), "JS script gets '//' marker prefix");
|
|
45
|
+
assert.strictEqual(b.requires.length, 0, "no requires when none supplied");
|
|
46
|
+
|
|
47
|
+
// ── a source that already declares its own header is respected; a --pack passed
|
|
48
|
+
// in is grafted on without clobbering the existing marker ──
|
|
49
|
+
const srcC = path.join(tmp, "self-doc.sh");
|
|
50
|
+
fs.writeFileSync(
|
|
51
|
+
srcC,
|
|
52
|
+
"#!/usr/bin/env bash\n# open-claudia-tool: selfdoc\n# description: pre-documented\necho hi\n"
|
|
53
|
+
);
|
|
54
|
+
const c = tools.addTool(srcC, { pack: "linked-pack" });
|
|
55
|
+
assert.strictEqual(c.name, "selfdoc", "existing marker name wins");
|
|
56
|
+
assert.strictEqual(c.description, "pre-documented", "existing description preserved");
|
|
57
|
+
assert.strictEqual(c.pack, "linked-pack", "--pack grafted onto a self-documented script");
|
|
58
|
+
|
|
59
|
+
// ── missingRequires: keys absent from keyring AND env are reported; present env
|
|
60
|
+
// keys are not ──
|
|
61
|
+
process.env.api_key = "present";
|
|
62
|
+
delete process.env.api_secret;
|
|
63
|
+
const missing = tools.missingRequires(tools.findTool("hello"));
|
|
64
|
+
assert.ok(!missing.includes("api_key"), "a key present in env is not 'missing'");
|
|
65
|
+
assert.ok(missing.includes("api_secret"), "a key absent everywhere is 'missing'");
|
|
66
|
+
delete process.env.api_key;
|
|
67
|
+
|
|
68
|
+
// ── runEnv returns an env object (keyring merged or graceful fallback) ──
|
|
69
|
+
const env = tools.runEnv();
|
|
70
|
+
assert.ok(env && typeof env === "object" && env.PATH, "runEnv yields an env object carrying PATH");
|
|
71
|
+
|
|
72
|
+
// ── toolNameFromPath only recognises a direct child of TOOLS_DIR ──
|
|
73
|
+
assert.strictEqual(
|
|
74
|
+
tools.toolNameFromPath(path.join(process.env.TOOLS_DIR, "hello")),
|
|
75
|
+
"hello",
|
|
76
|
+
"a file directly in TOOLS_DIR resolves to its tool name"
|
|
77
|
+
);
|
|
78
|
+
assert.strictEqual(tools.toolNameFromPath("/etc/passwd"), null, "a path outside TOOLS_DIR → null");
|
|
79
|
+
assert.strictEqual(
|
|
80
|
+
tools.toolNameFromPath(path.join(process.env.TOOLS_DIR, "sub", "deep")),
|
|
81
|
+
null,
|
|
82
|
+
"a nested path under TOOLS_DIR → null"
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// ── parseHeader returns null for a script with no marker line ──
|
|
86
|
+
assert.strictEqual(tools.parseHeader("#!/bin/sh\necho hi\n"), null, "no marker → not a tool");
|
|
87
|
+
|
|
88
|
+
// ── remove ──
|
|
89
|
+
assert.ok(tools.removeTool("hello"), "removeTool returns the removed tool");
|
|
90
|
+
assert.strictEqual(tools.findTool("hello"), null, "removed tool is gone");
|
|
91
|
+
|
|
92
|
+
console.log("tools OK");
|