@inetafrica/open-claudia 2.7.5 → 2.7.6

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 CHANGED
@@ -1,5 +1,8 @@
1
1
  # Changelog
2
2
 
3
+ ## v2.7.6
4
+ - **/tooltrace now shows WHAT ran, not just THAT it ran.** The 🔧 line per tool run was `Ran tool: kticket` — no verb, no args, and one line per tool even when different verbs ran in the same turn. It now renders the **sanitized command shape** (quoted strings → `<text>`, ids/uuids → `<id>`, paths → `<path>` — same shaping the tool graph already computes, so this is display-only and free) plus a **human-readable doc sentence for that verb**: new `tools.verbDoc(name, verb)` parses the sentence mechanically from the tool's own leading comment block (the same docs `--help` prints — zero model cost, can't drift from the source). Both documented header shapes are recognised (indented description under the verb line, or same-line prose after 2+ spaces), with fallback to the tool description's first clause. Example: `🔧 kticket show <id> — One ticket in full`. Dedup is now per tool+verb per turn, so `kticket list` then `kticket show` both announce. Unit coverage in `test-tools.js` (both shapes, sentence cut, fallback, flag-token and unknown-tool cases).
5
+
3
6
  ## v2.7.5
4
7
  - **Tool composition with inherited approval — a composing tool cannot self-approve (Phase 4).** Higher-order tools may shell into lower tools (report → kticket + kchat), but the callee's risk gate stays intact: `tool run` now exports the tier the **outermost** invocation's flags acknowledged (`OPEN_CLAUDIA_APPROVED_TIER`) plus the call chain (`OPEN_CLAUDIA_TOOL_STACK`) into the child env, and a nested run gates on that inherited tier while **ignoring its own argv flags** — so a composing tool that hardcodes `--yes-destructive` in its source is refused with a message pointing at the outer re-run. An inherited tier passes through unchanged (depth-safe), a tool already in the chain is refused (cycle guard), and each nested run still stamps its own telemetry. Verified live: fabricated approval blocked at exit 3, legit outer `--yes-destructive` flowing through, self-call cycle caught at depth 1.
5
8
  - **`_lib/` shared plumbing with dependent journaling (Phase 4).** Genuinely shared code (central auth, mongo-exec) now lives in `tools/_lib` — tiny modules tools import via a literal `_lib/<name>` path, never tools themselves (the registry, surfacing index, and legacy migration all skip the `_` prefix). New `tool lib list|show|set`: `set` writes the module (secret-blocking, traversal-safe), finds every dependent by a boundary-safe source grep (`central-auth` matches `_lib/central-auth.js` and the `path.join` form, **not** `central-auth-v2`), journals each dependent `unverified since _lib change — re-verified by next real use`, and commits the lot in ONE commit naming the dependents — the PLAN's blast-radius rule, since a lib change can break several tools at once and health is verified by use, not smoke tests.
package/core/runner.js CHANGED
@@ -988,9 +988,14 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
988
988
  try {
989
989
  for (const run of require("./tool-graph").parseToolRuns(cmd)) {
990
990
  toolRunsThisTurn.push(run);
991
- // Visibility (gated by /tooltrace): one 🔧 line per distinct tool run
992
- // this turn. Telemetry (recordRun) fires at end-of-turn on success.
993
- notifyToolTrace(`ran:${run.name}`, `🔧 Ran tool: ${run.name}`);
991
+ // Visibility (gated by /tooltrace): one 🔧 line per distinct
992
+ // tool+verb this turn sanitized command shape plus the verb's own
993
+ // doc sentence (verbDoc: parsed mechanically from the tool header,
994
+ // zero model cost). Telemetry (recordRun) fires at end-of-turn.
995
+ const verb = (String(run.shape || "").split(/\s+/)[1] || "");
996
+ let doc = "";
997
+ try { doc = toolsLib.verbDoc(run.name, verb); } catch (e) {}
998
+ notifyToolTrace(`ran:${run.name}:${verb}`, `🔧 ${run.shape || run.name}${doc ? ` — ${doc}` : ""}`);
994
999
  }
995
1000
  } catch (e) { /* graph optional (old node) */ }
996
1001
  // `open-claudia tool add <src> [--name X]` — a tool being saved/updated.
package/core/tools.js CHANGED
@@ -270,6 +270,70 @@ function parseHeader(content) {
270
270
  return out.name ? out : null;
271
271
  }
272
272
 
273
+ // First readable clause of a doc string: cut at sentence end or " — ",
274
+ // collapse whitespace, cap length. Used for one-line verb docs in chat.
275
+ function firstClause(s) {
276
+ let t = String(s || "").replace(/\s+/g, " ").trim();
277
+ if (!t) return "";
278
+ const m = t.match(/^(.{10,}?[.!?])(?:\s|$)/);
279
+ if (m) t = m[1].replace(/[.!?]+$/, "");
280
+ else t = t.split(" — ")[0];
281
+ if (t.length > 90) t = t.slice(0, 87).trimEnd() + "…";
282
+ return t;
283
+ }
284
+
285
+ // One human-readable sentence for a tool verb, parsed mechanically from the
286
+ // tool's own leading comment block (the same docs --help prints) — zero model
287
+ // cost, always in sync with the source. Two documented shapes are recognised:
288
+ // // kticket list [--stage a,b] ← verb line; description on the
289
+ // // List tickets. --q matches … deeper-indented line(s) below
290
+ // // inet-central users [regex] list users; same-line prose after 2+ spaces
291
+ // Falls back to the tool description's first clause; "" when the tool is unknown.
292
+ function verbDoc(name, verb) {
293
+ let content;
294
+ try { content = fs.readFileSync(toolPaths(name).exec, "utf-8"); } catch (e) { return ""; }
295
+ const header = parseHeader(content);
296
+ if (!header) return "";
297
+ const fallback = firstClause(header.description);
298
+ const v = String(verb || "").trim();
299
+ if (!v || v.startsWith("-") || v.startsWith("<")) return fallback;
300
+ // Re-walk the leading comment block (same bounds as parseHeader).
301
+ const lines = String(content).split("\n");
302
+ const block = [];
303
+ let started = false;
304
+ for (let i = 0; i < lines.length; i++) {
305
+ if (i === 0 && lines[i].startsWith("#!")) continue;
306
+ const body = uncomment(lines[i]);
307
+ if (body === null) {
308
+ if (started) break;
309
+ if (lines[i].trim() === "") continue;
310
+ break;
311
+ }
312
+ started = true;
313
+ block.push(body);
314
+ }
315
+ const vEsc = v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
316
+ const verbLine = new RegExp(`^(\\s+)\\S+\\s+${vEsc}\\b(.*)$`);
317
+ for (let i = 0; i < block.length; i++) {
318
+ const m = block[i].match(verbLine);
319
+ if (!m) continue;
320
+ const same = m[2].match(/\s{2,}([A-Za-z].*)$/);
321
+ if (same) return firstClause(same[1]);
322
+ const indent = m[1].length;
323
+ let desc = "";
324
+ for (let j = i + 1; j < block.length; j++) {
325
+ const ind = (block[j].match(/^\s*/) || [""])[0].length;
326
+ const text = block[j].trim();
327
+ if (!text || ind <= indent) break;
328
+ desc += (desc ? " " : "") + text;
329
+ if (/[.!?]$/.test(text)) break;
330
+ }
331
+ if (desc) return firstClause(desc);
332
+ break;
333
+ }
334
+ return fallback;
335
+ }
336
+
273
337
  // Path of a tool's executable (legacy signature kept: several callers treat
274
338
  // "the tool file" as the thing you run/edit).
275
339
  function toolFile(name) {
@@ -848,7 +912,7 @@ function toolNameFromPath(filePath) {
848
912
 
849
913
  module.exports = {
850
914
  TOOLS_DIR, MARKER, USAGE_FILE, RISK_TIERS, LIB_DIR,
851
- parseHeader, readTool, readToolDoc, toolCondition, listTools, findTool, runsSince, docDrift, addTool, removeTool, updateToolHeader,
915
+ parseHeader, readTool, readToolDoc, toolCondition, verbDoc, listTools, findTool, runsSince, docDrift, addTool, removeTool, updateToolHeader,
852
916
  runEnv, missingRequires, toolNameFromPath, sanitizeName, ensureDir, normalizeRisk,
853
917
  toolPaths, readState, writeState, recordRunResult, runGate, approvedTierFromArgv,
854
918
  listLib, readLib, writeLib, libDependents,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "2.7.5",
3
+ "version": "2.7.6",
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": {
package/test-tools.js CHANGED
@@ -438,6 +438,34 @@ assert.ok(!cycle.ok && /cycle/i.test(cycle.message), "a tool already in the call
438
438
  const topLevel = tools.runGate({ name: "kticket", risk: "write" }, ["--yes"], {});
439
439
  assert.ok(topLevel.ok && !topLevel.nested, "no stack env → the classic argv-flag gate applies");
440
440
 
441
+ // ── verbDoc: one mechanical sentence per verb, parsed from the header docs ──
442
+ // Covers both documented shapes (indented-description-below and same-line
443
+ // prose after 2+ spaces), the description fallback, and the unknown-tool "".
444
+ const srcV = path.join(tmp, "vdoc.js");
445
+ fs.writeFileSync(srcV, [
446
+ "#!/usr/bin/env node",
447
+ "// open-claudia-tool: vdoc",
448
+ "// description: Query the demo platform (demo.example) as operator — login, list things, show one thing",
449
+ "// risk: read-only",
450
+ "// usage: vdoc <verb> [args]",
451
+ "//",
452
+ "// vdoc list [--stage a,b] [--json]",
453
+ "// List things. Filters are regexes and",
454
+ "// combine with AND.",
455
+ "// vdoc peek [regex] look at things; optional case-insensitive filter",
456
+ "// vdoc stages",
457
+ "console.log('hi');",
458
+ ].join("\n"));
459
+ tools.addTool(srcV, { name: "vdoc" });
460
+ assert.strictEqual(tools.verbDoc("vdoc", "list"), "List things", "indented description below the verb line, cut at first sentence");
461
+ assert.strictEqual(tools.verbDoc("vdoc", "peek"), "look at things; optional case-insensitive filter", "same-line prose after 2+ spaces");
462
+ assert.strictEqual(tools.verbDoc("vdoc", "stages"), "Query the demo platform (demo.example) as operator",
463
+ "undocumented-body verb falls back to the description's first clause");
464
+ assert.strictEqual(tools.verbDoc("vdoc", ""), "Query the demo platform (demo.example) as operator", "no verb → description clause");
465
+ assert.strictEqual(tools.verbDoc("vdoc", "--json"), "Query the demo platform (demo.example) as operator", "flag token is not a verb");
466
+ assert.strictEqual(tools.verbDoc("no-such-tool", "list"), "", "unknown tool → empty string");
467
+ tools.removeTool("vdoc");
468
+
441
469
  // ── remove ──
442
470
  assert.ok(tools.removeTool("hello"), "removeTool returns the removed tool");
443
471
  assert.strictEqual(tools.findTool("hello"), null, "removed tool is gone");