@inetafrica/open-claudia 2.6.59 → 2.7.1
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 +11 -0
- package/bin/cli.js +1 -1
- package/bin/tool.js +177 -23
- package/core/recall/discoverer.js +71 -3
- package/core/system-prompt.js +14 -7
- package/core/tool-repo.js +270 -0
- package/core/tools.js +268 -59
- package/package.json +1 -1
- package/test-recall-discoverer.js +36 -1
- package/test-tools.js +139 -19
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
// Git-backed tool repository: every tool is a directory (executable + TOOL.md +
|
|
2
|
+
// state.json) inside a git-initialised TOOLS_DIR, so `git log` IS the upgrade
|
|
3
|
+
// history and any change is one revert from undone.
|
|
4
|
+
//
|
|
5
|
+
// Split from core/tools.js so the registry (read/parse/match) stays pure fs;
|
|
6
|
+
// this module owns everything that shells out to git, plus the scaffold and
|
|
7
|
+
// legacy-migration machinery that writes whole tool directories. Requires
|
|
8
|
+
// between the two modules are lazy to tolerate the natural circularity.
|
|
9
|
+
//
|
|
10
|
+
// state.json is deliberately gitignored: it is runtime telemetry stamped on
|
|
11
|
+
// every run — committing it would bury real code/doc changes in noise. The
|
|
12
|
+
// curated, human-readable condition of a tool lives in TOOL.md's State section.
|
|
13
|
+
|
|
14
|
+
const fs = require("fs");
|
|
15
|
+
const path = require("path");
|
|
16
|
+
const { spawnSync } = require("child_process");
|
|
17
|
+
|
|
18
|
+
const GIT_IDENT = ["-c", "user.name=open-claudia", "-c", "user.email=tools@open-claudia.local"];
|
|
19
|
+
const GITIGNORE = ["*/state.json", ".usage.json", "__pycache__/", ".DS_Store", ""].join("\n");
|
|
20
|
+
|
|
21
|
+
function toolsDir() {
|
|
22
|
+
return require("./tools").TOOLS_DIR;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function git(args, { ident = false } = {}) {
|
|
26
|
+
try {
|
|
27
|
+
const full = ["-C", toolsDir(), ...(ident ? GIT_IDENT : []), ...args];
|
|
28
|
+
const r = spawnSync("git", full, { encoding: "utf-8" });
|
|
29
|
+
if (r.error) return { ok: false, status: -1, stdout: "", stderr: String(r.error.message || r.error) };
|
|
30
|
+
return { ok: r.status === 0, status: r.status, stdout: (r.stdout || "").trim(), stderr: (r.stderr || "").trim() };
|
|
31
|
+
} catch (e) {
|
|
32
|
+
return { ok: false, status: -1, stdout: "", stderr: String(e.message || e) };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function gitAvailable() {
|
|
37
|
+
try { return !spawnSync("git", ["--version"], { encoding: "utf-8" }).error; }
|
|
38
|
+
catch (e) { return false; }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Initialise the tools dir as a git repo (idempotent). Best-effort: on a box
|
|
42
|
+
// without git everything else still works, there's just no history.
|
|
43
|
+
function ensureRepo() {
|
|
44
|
+
const dir = toolsDir();
|
|
45
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
46
|
+
const ignoreFile = path.join(dir, ".gitignore");
|
|
47
|
+
if (!fs.existsSync(ignoreFile)) { try { fs.writeFileSync(ignoreFile, GITIGNORE, { mode: 0o600 }); } catch (e) {} }
|
|
48
|
+
if (fs.existsSync(path.join(dir, ".git"))) return true;
|
|
49
|
+
if (!gitAvailable()) return false;
|
|
50
|
+
if (!git(["init", "-q"]).ok) return false;
|
|
51
|
+
git(["add", "-A"]);
|
|
52
|
+
git(["commit", "-q", "-m", "init tool repository"], { ident: true });
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Stage + commit one tool's paths (or everything with "."). No-op when the tree
|
|
57
|
+
// is clean or git is unavailable. Every add/change/remove funnels through here.
|
|
58
|
+
function commitTool(name, message) {
|
|
59
|
+
if (!ensureRepo()) return false;
|
|
60
|
+
const target = name ? require("./tools").sanitizeName(name) : ".";
|
|
61
|
+
if (!git(["add", "-A", "--", target || "."]).ok) return false;
|
|
62
|
+
const staged = git(["diff", "--cached", "--quiet"]);
|
|
63
|
+
if (staged.status === 0) return false; // nothing to commit
|
|
64
|
+
return git(["commit", "-q", "-m", message], { ident: true }).ok;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Recent history for one tool — surfaced by `tool show` as the upgrade log.
|
|
68
|
+
function toolLog(name, limit = 5) {
|
|
69
|
+
const n = require("./tools").sanitizeName(name);
|
|
70
|
+
if (!n || !fs.existsSync(path.join(toolsDir(), ".git"))) return [];
|
|
71
|
+
const r = git(["log", "--date=short", `--pretty=%ad %s`, "-n", String(limit), "--", n]);
|
|
72
|
+
return r.ok && r.stdout ? r.stdout.split("\n").filter(Boolean) : [];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── Scaffold ─────────────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
function today() { return new Date().toISOString().slice(0, 10); }
|
|
78
|
+
|
|
79
|
+
function toolMdSkeleton({ name, description, usage, risk, requires, pack, stateLine, journalLine }) {
|
|
80
|
+
return [
|
|
81
|
+
`# ${name}`,
|
|
82
|
+
"",
|
|
83
|
+
"## Interface",
|
|
84
|
+
description || "(describe what system this tool fronts)",
|
|
85
|
+
"",
|
|
86
|
+
`Usage: \`open-claudia tool run ${name}${usage ? " " + usage.replace(new RegExp("^" + name + "\\s*"), "") : " <verb> [args]"}\``,
|
|
87
|
+
`Risk: ${risk || "write"}${requires && requires.length ? ` · Requires: ${requires.join(", ")}` : ""}${pack ? ` · Pack: ${pack}` : ""}`,
|
|
88
|
+
"",
|
|
89
|
+
"## Known issues",
|
|
90
|
+
"(none yet)",
|
|
91
|
+
"",
|
|
92
|
+
"## State",
|
|
93
|
+
stateLine || `Scaffolded ${today()} — not yet proven against the live system.`,
|
|
94
|
+
"",
|
|
95
|
+
"## Journal",
|
|
96
|
+
`- [${today()}] ${journalLine || "Scaffolded."}`,
|
|
97
|
+
"",
|
|
98
|
+
].join("\n");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function nodeStub({ name, description, pack, risk, requires, usage }) {
|
|
102
|
+
const req = requires && requires.length ? `// requires: ${requires.join(", ")}\n` : "";
|
|
103
|
+
return `#!/usr/bin/env node
|
|
104
|
+
// open-claudia-tool: ${name}
|
|
105
|
+
// description: ${description || ""}
|
|
106
|
+
${pack ? `// pack: ${pack}\n` : ""}// risk: ${risk}
|
|
107
|
+
${req}// usage: ${usage || `${name} <verb> [args]`}
|
|
108
|
+
//
|
|
109
|
+
// One CLI per system, verbs as subcommands. Add a function per verb below.
|
|
110
|
+
// Confirmation flags (--yes / --yes-destructive) are enforced by the tool
|
|
111
|
+
// runner and may appear in argv — ignore them unless you need them.
|
|
112
|
+
|
|
113
|
+
const args = process.argv.slice(2).filter((a) => a !== "--yes" && a !== "--yes-destructive");
|
|
114
|
+
const verb = args[0] || "help";
|
|
115
|
+
|
|
116
|
+
const HANDLERS = {
|
|
117
|
+
help() {
|
|
118
|
+
console.log("${name} — ${(description || "").replace(/"/g, '\\"')}");
|
|
119
|
+
console.log("verbs: " + Object.keys(HANDLERS).join(", "));
|
|
120
|
+
},
|
|
121
|
+
// example(rest) { ... },
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const fn = HANDLERS[verb] || (verb === "--help" ? HANDLERS.help : null);
|
|
125
|
+
if (!fn) { console.error("unknown verb: " + verb); HANDLERS.help(); process.exit(1); }
|
|
126
|
+
Promise.resolve(fn(args.slice(1))).catch((e) => { console.error(e.message || e); process.exit(1); });
|
|
127
|
+
`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function bashStub({ name, description, pack, risk, requires, usage }) {
|
|
131
|
+
const req = requires && requires.length ? `# requires: ${requires.join(", ")}\n` : "";
|
|
132
|
+
return `#!/usr/bin/env bash
|
|
133
|
+
# open-claudia-tool: ${name}
|
|
134
|
+
# description: ${description || ""}
|
|
135
|
+
${pack ? `# pack: ${pack}\n` : ""}# risk: ${risk}
|
|
136
|
+
${req}# usage: ${usage || `${name} <verb> [args]`}
|
|
137
|
+
#
|
|
138
|
+
# One CLI per system, verbs as subcommands: add a case per verb below.
|
|
139
|
+
# Confirmation flags (--yes / --yes-destructive) are enforced by the tool
|
|
140
|
+
# runner and may appear in "$@" — ignore them unless you need them.
|
|
141
|
+
set -euo pipefail
|
|
142
|
+
|
|
143
|
+
verb="\${1:-help}"; shift || true
|
|
144
|
+
|
|
145
|
+
case "$verb" in
|
|
146
|
+
help|--help)
|
|
147
|
+
echo "${name} — ${description || ""}"
|
|
148
|
+
echo "verbs: help" ;;
|
|
149
|
+
*)
|
|
150
|
+
echo "unknown verb: $verb" >&2; exit 1 ;;
|
|
151
|
+
esac
|
|
152
|
+
`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Create a new tool directory with an executable stub, TOOL.md skeleton, and
|
|
156
|
+
// initial state.json — then commit. The generated stub already carries the full
|
|
157
|
+
// header (name, desc, pack, risk, requires, usage) so the tool is registered
|
|
158
|
+
// and runnable the moment it exists.
|
|
159
|
+
function scaffoldTool(name, opts = {}) {
|
|
160
|
+
const tools = require("./tools");
|
|
161
|
+
const n = tools.sanitizeName(name);
|
|
162
|
+
if (!n) throw new Error("scaffold needs a tool name");
|
|
163
|
+
if (tools.findTool(n)) throw new Error(`tool "${n}" already exists — extend it (tool edit ${n}) instead of re-scaffolding`);
|
|
164
|
+
const risk = tools.normalizeRisk(opts.risk) || "write";
|
|
165
|
+
const requires = [].concat(opts.requires || []).filter(Boolean);
|
|
166
|
+
const spec = { name: n, description: opts.description || "", pack: opts.pack || "", risk, requires, usage: opts.usage || "" };
|
|
167
|
+
|
|
168
|
+
ensureRepo();
|
|
169
|
+
const dir = path.join(toolsDir(), n);
|
|
170
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
171
|
+
const stub = (opts.lang === "bash" || opts.lang === "sh") ? bashStub(spec) : nodeStub(spec);
|
|
172
|
+
fs.writeFileSync(path.join(dir, "tool"), stub, { mode: 0o700 });
|
|
173
|
+
try { fs.chmodSync(path.join(dir, "tool"), 0o700); } catch (e) {}
|
|
174
|
+
fs.writeFileSync(path.join(dir, "TOOL.md"), toolMdSkeleton(spec), { mode: 0o600 });
|
|
175
|
+
tools.writeState(n, { createdAt: new Date().toISOString(), runCount: 0 });
|
|
176
|
+
commitTool(n, `scaffold ${n}`);
|
|
177
|
+
return tools.findTool(n);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ── Legacy migration ─────────────────────────────────────────────────────────
|
|
181
|
+
// Convert flat-file tools (pre-2.7 layout) into directories, one commit each.
|
|
182
|
+
// Seeds each tool's state.json from the old .usage.json sidecar and generates a
|
|
183
|
+
// TOOL.md so every migrated tool arrives with its memory intact. Files in the
|
|
184
|
+
// tools dir that don't parse as tools are left untouched.
|
|
185
|
+
function migrateLegacyTools() {
|
|
186
|
+
const tools = require("./tools");
|
|
187
|
+
const dir = toolsDir();
|
|
188
|
+
let entries;
|
|
189
|
+
try { entries = fs.readdirSync(dir); } catch (e) { return { migrated: [], skipped: [] }; }
|
|
190
|
+
|
|
191
|
+
const legacy = [];
|
|
192
|
+
for (const name of entries) {
|
|
193
|
+
if (name.startsWith(".")) continue;
|
|
194
|
+
const p = path.join(dir, name);
|
|
195
|
+
try { if (!fs.statSync(p).isFile()) continue; } catch (e) { continue; }
|
|
196
|
+
legacy.push(name);
|
|
197
|
+
}
|
|
198
|
+
if (!legacy.length) return { migrated: [], skipped: [] };
|
|
199
|
+
|
|
200
|
+
ensureRepo();
|
|
201
|
+
const usage = tools.readUsage();
|
|
202
|
+
const migrated = [], skipped = [];
|
|
203
|
+
|
|
204
|
+
for (const fileName of legacy) {
|
|
205
|
+
const src = path.join(dir, fileName);
|
|
206
|
+
let content;
|
|
207
|
+
try { content = fs.readFileSync(src, "utf-8"); } catch (e) { skipped.push(fileName); continue; }
|
|
208
|
+
const header = tools.parseHeader(content);
|
|
209
|
+
if (!header) { skipped.push(fileName); continue; } // not a registered tool — leave it alone
|
|
210
|
+
|
|
211
|
+
const n = header.name || tools.sanitizeName(fileName);
|
|
212
|
+
const tmp = path.join(dir, `.migrating-${n}`);
|
|
213
|
+
try {
|
|
214
|
+
fs.renameSync(src, tmp);
|
|
215
|
+
const toolDir = path.join(dir, n);
|
|
216
|
+
fs.mkdirSync(toolDir, { recursive: true, mode: 0o700 });
|
|
217
|
+
fs.renameSync(tmp, path.join(toolDir, "tool"));
|
|
218
|
+
try { fs.chmodSync(path.join(toolDir, "tool"), 0o700); } catch (e) {}
|
|
219
|
+
|
|
220
|
+
const u = usage[n] || {};
|
|
221
|
+
tools.writeState(n, {
|
|
222
|
+
createdAt: u.createdAt || new Date().toISOString(),
|
|
223
|
+
lastUsed: u.lastUsed || "",
|
|
224
|
+
runCount: u.runCount || 0,
|
|
225
|
+
});
|
|
226
|
+
if (!fs.existsSync(path.join(toolDir, "TOOL.md"))) {
|
|
227
|
+
fs.writeFileSync(path.join(toolDir, "TOOL.md"), toolMdSkeleton({
|
|
228
|
+
...header,
|
|
229
|
+
risk: header.risk || "",
|
|
230
|
+
stateLine: `Migrated from flat layout ${today()}. Risk tier ${header.risk ? `"${header.risk}"` : "not yet classified — treated as write (fail-safe) until set"}.`,
|
|
231
|
+
journalLine: "Migrated from flat file layout; telemetry carried over from .usage.json.",
|
|
232
|
+
}), { mode: 0o600 });
|
|
233
|
+
}
|
|
234
|
+
delete usage[n];
|
|
235
|
+
commitTool(n, `migrate ${n} to directory layout`);
|
|
236
|
+
migrated.push(n);
|
|
237
|
+
} catch (e) {
|
|
238
|
+
// Roll the file back if anything failed mid-move.
|
|
239
|
+
try { if (fs.existsSync(tmp)) fs.renameSync(tmp, src); } catch (e2) {}
|
|
240
|
+
skipped.push(fileName);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (migrated.length) {
|
|
245
|
+
try {
|
|
246
|
+
if (Object.keys(usage).length) fs.writeFileSync(tools.USAGE_FILE, JSON.stringify(usage), { mode: 0o600 });
|
|
247
|
+
else fs.rmSync(tools.USAGE_FILE, { force: true });
|
|
248
|
+
} catch (e) {}
|
|
249
|
+
}
|
|
250
|
+
return { migrated, skipped };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// True when flat-file tools still exist (cheap check used to auto-trigger
|
|
254
|
+
// migration from the CLI entry point).
|
|
255
|
+
function hasLegacyTools() {
|
|
256
|
+
const tools = require("./tools");
|
|
257
|
+
let entries;
|
|
258
|
+
try { entries = fs.readdirSync(toolsDir()); } catch (e) { return false; }
|
|
259
|
+
for (const name of entries) {
|
|
260
|
+
if (name.startsWith(".")) continue;
|
|
261
|
+
const p = path.join(toolsDir(), name);
|
|
262
|
+
try {
|
|
263
|
+
if (!fs.statSync(p).isFile()) continue;
|
|
264
|
+
if (tools.parseHeader(fs.readFileSync(p, "utf-8"))) return true;
|
|
265
|
+
} catch (e) {}
|
|
266
|
+
}
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
module.exports = { ensureRepo, commitTool, toolLog, scaffoldTool, migrateLegacyTools, hasLegacyTools, toolMdSkeleton };
|