@inetafrica/open-claudia 2.6.21 → 2.6.23

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/core/handlers.js CHANGED
@@ -25,6 +25,7 @@ const { isNewerVersion } = require("./version");
25
25
  const jobs = require("./jobs");
26
26
  const scheduler = require("./scheduler");
27
27
  const skillsLib = require("./skills");
28
+ const packsLib = require("./packs");
28
29
  const {
29
30
  runClaude, compactActiveSession, getActiveSessionId, effectiveCompactThreshold,
30
31
  } = require("./runner");
@@ -946,7 +947,10 @@ register({
946
947
  },
947
948
  });
948
949
 
949
- // ── Learned skills ──────────────────────────────────────────────────
950
+ // ── Learned skills (now backed by context packs) ────────────────────
951
+ // Skills migrated into context packs (~/.open-claudia/packs). /skills is a
952
+ // thin window onto that store; any stragglers still in the legacy
953
+ // ~/.claude/skills dir get surfaced as a one-line migration nudge.
950
954
 
951
955
  register({
952
956
  name: "skills", description: "List, show, or remove learned skills", args: "[show|remove <name>]",
@@ -956,26 +960,35 @@ register({
956
960
  const name = rest.join(" ");
957
961
 
958
962
  if (sub === "show" && name) {
959
- const skill = skillsLib.findSkill(name);
960
- if (!skill) return send(`No skill named "${name}". /skills to list.`);
963
+ const pack = packsLib.findPack(name);
964
+ if (!pack) return send(`No skill named "${name}". /skills to list.`);
965
+ const file = path.join(packsLib.PACKS_DIR, pack.dir, "PACK.md");
961
966
  let content;
962
- try { content = fs.readFileSync(skill.file, "utf-8"); } catch (e) { return send(`Could not read ${skill.file}: ${e.message}`); }
967
+ try { content = fs.readFileSync(file, "utf-8"); } catch (e) { return send(`Could not read ${file}: ${e.message}`); }
963
968
  const preview = content.length > 3500 ? content.slice(0, 3500) + "\n…(truncated)" : content;
964
969
  await send(preview);
965
- return send(`File: ${skill.file}`);
970
+ return send(`File: ${file}`);
966
971
  }
967
972
 
968
973
  if (sub === "remove" && name) {
969
- const skill = skillsLib.findSkill(name);
970
- if (!skill) return send(`No skill named "${name}". /skills to list.`);
971
- skillsLib.removeSkill(name);
972
- return send(`Removed skill: ${skill.name} (${skill.file})`);
974
+ const pack = packsLib.findPack(name);
975
+ if (!pack) return send(`No skill named "${name}". /skills to list.`);
976
+ packsLib.removePack(pack.dir);
977
+ return send(`Removed skill pack: ${pack.name} (${pack.dir})`);
973
978
  }
974
979
 
975
- const skills = skillsLib.listSkills();
976
- if (skills.length === 0) return send("No learned skills yet. I save them automatically after complex tasks, or use /learn to capture the last piece of work.");
977
- const lines = skills.map((s) => `• ${s.dir} — ${s.description || "(no description)"}`);
978
- return send(`Learned skills (${skills.length}):\n\n${lines.join("\n")}\n\n/skills show <name> · /skills remove <name> · /learn [hint]`);
980
+ const packs = packsLib.listPacks();
981
+ const legacy = skillsLib.listSkills();
982
+ if (packs.length === 0 && legacy.length === 0) {
983
+ return send("No skills yet. I save them automatically as context packs after complex tasks, or use /learn to capture the last piece of work.");
984
+ }
985
+ const lines = packs.map((p) => `• ${p.dir} — ${p.description || p.name || "(no description)"}`);
986
+ let msg = `Skills / context packs (${packs.length}):\n\n${lines.join("\n")}`;
987
+ if (legacy.length) {
988
+ msg += `\n\n${legacy.length} legacy skill(s) still in ~/.claude/skills — run open-claudia pack migrate to fold them in.`;
989
+ }
990
+ msg += `\n\n/skills show <name> · /skills remove <name> · /learn [hint]`;
991
+ return send(msg);
979
992
  },
980
993
  });
981
994
 
@@ -988,9 +1001,10 @@ register({
988
1001
  const hint = tail ? ` The user's hint about what to capture: "${tail}".` : "";
989
1002
  const prompt =
990
1003
  `The user typed /learn: capture the most recent substantial piece of work in this conversation as a reusable skill.${hint} ` +
991
- `Follow the Skill learning rules in your system prompt: check ~/.claude/skills/ first and patch an existing skill instead of duplicating; ` +
992
- `otherwise write ~/.claude/skills/<kebab-name>/SKILL.md with name + a when-to-use description in the frontmatter, then prerequisites, exact commands in order, and pitfalls. ` +
993
- `No secrets or tokens. When done, reply with one short line saying which skill you created or updated and what it covers. ` +
1004
+ `Skills live in context packs (~/.open-claudia/packs/<dir>/PACK.md), NOT the legacy ~/.claude/skills dir. ` +
1005
+ `First check the already-injected packs and 'open-claudia pack list' for a pack this work belongs to; if one fits, fold the reusable how-to into that pack's Procedure section (exact commands in order, prerequisites, pitfalls) and append a dated Journal line — edit the PACK.md directly. ` +
1006
+ `Only if no pack fits, create a new one: 'open-claudia pack' has no create subcommand, so write ~/.open-claudia/packs/<kebab-name>/PACK.md following the four-section format (Stance, Procedure, State, Journal) with name + a when-to-use description in the frontmatter. ` +
1007
+ `No secrets or tokens. When done, reply with one short line naming the pack you created or updated and what it covers. ` +
994
1008
  `If the recent work is genuinely not reusable, say so instead of forcing a skill.`;
995
1009
  await runClaude(prompt, currentState().currentSession.dir, env.messageId);
996
1010
  },
@@ -291,6 +291,19 @@ const packsInjectedFor = new Map(); // `${adapterId}:${channelId}:${dir}` -> `${
291
291
  const PACK_INJECT_MAX_CHARS = 4000;
292
292
  const DEFAULT_MEMORY_RECALL_MAX_CHARS = 9000;
293
293
 
294
+ // Progressive disclosure: when on, a matched pack is injected as a cheap
295
+ // teaser (Stance + State + a pointer) rather than its full body. Heavy
296
+ // reference sections (Procedure, Journal) are fetched on demand via
297
+ // `open-claudia pack show`. Because each teaser is small we can widen the
298
+ // match count — surface more relevant packs for the same budget and let the
299
+ // agent decide which to read further.
300
+ function packProgressive() {
301
+ return String(config.PACK_PROGRESSIVE ?? process.env.PACK_PROGRESSIVE ?? "on").toLowerCase() === "on";
302
+ }
303
+ function packMatchLimit() {
304
+ return intSetting("PACK_MATCH_LIMIT", packProgressive() ? 6 : 3);
305
+ }
306
+
294
307
  function intSetting(key, fallback) {
295
308
  const raw = config[key] ?? process.env[key];
296
309
  if (raw === undefined || raw === null || raw === "") return fallback;
@@ -332,7 +345,8 @@ function consumeLastInjected() {
332
345
  return out;
333
346
  }
334
347
 
335
- function formatPackForContext(pack, packsLib) {
348
+ function formatPackForContext(pack, packsLib, opts = {}) {
349
+ const progressive = !!opts.progressive;
336
350
  const clip = (s, n) => (s.length > n ? s.slice(0, n) + "\n…[truncated — read the full pack file]" : s);
337
351
  const parts = [`### Pack: ${pack.name} (${pack.dir})`];
338
352
  if (pack.description) parts.push(pack.description);
@@ -345,12 +359,21 @@ function formatPackForContext(pack, packsLib) {
345
359
  const childLines = children.map((c) => `- ${c.dir}: ${c.description || c.name}`).join("\n");
346
360
  parts.push(`Sub-packs (dig deeper with \`open-claudia pack show <dir>\`):\n${childLines}`);
347
361
  }
348
- for (const section of ["Stance", "Procedure", "State"]) {
362
+ // Progressive mode injects only the live sections (Stance = how to think,
363
+ // State = where we are) plus a pointer; full mode also inlines Procedure
364
+ // and the recent Journal.
365
+ const sections = progressive ? ["Stance", "State"] : ["Stance", "Procedure", "State"];
366
+ for (const section of sections) {
349
367
  const body = (pack.sections[section] || "").trim();
350
368
  if (body) parts.push(`#### ${section}\n${body}`);
351
369
  }
352
- const journal = (pack.sections.Journal || "").trim().split("\n").filter(Boolean).slice(-10).join("\n");
353
- if (journal) parts.push(`#### Journal (recent)\n${journal}`);
370
+ if (progressive) {
371
+ const hasMore = (pack.sections.Procedure || "").trim() || (pack.sections.Journal || "").trim();
372
+ if (hasMore) parts.push(`#### More\nProcedure + Journal not shown — run \`open-claudia pack show ${pack.dir}\` to read them.`);
373
+ } else {
374
+ const journal = (pack.sections.Journal || "").trim().split("\n").filter(Boolean).slice(-10).join("\n");
375
+ if (journal) parts.push(`#### Journal (recent)\n${journal}`);
376
+ }
354
377
  return clip(parts.join("\n\n"), PACK_INJECT_MAX_CHARS);
355
378
  }
356
379
 
@@ -362,6 +385,7 @@ function buildPackBlock(matches, budget) {
362
385
  const adapter = currentAdapter();
363
386
  const channelId = currentChannelId();
364
387
  const sess = state.lastSessionId || "new";
388
+ const progressive = packProgressive();
365
389
  const blocks = [];
366
390
  const used = [];
367
391
  for (const m of matches) {
@@ -370,13 +394,13 @@ function buildPackBlock(matches, budget) {
370
394
  used.push(m.dir);
371
395
  const key = `${adapter?.id || "?"}:${channelId || "?"}:${m.dir}`;
372
396
  const stamp = `${sess}:${pack.updated}`;
373
- // Inject a pack's full body once per (channel, session, version). A
397
+ // Inject a pack's body once per (channel, session, version). A
374
398
  // compaction mints a new session id, which changes the stamp and
375
- // forces a fresh full re-injection on the next turn (same mechanism
399
+ // forces a fresh re-injection on the next turn (same mechanism
376
400
  // as the task tree), so the pack survives compaction without paying
377
401
  // to re-stamp an anchor on every intervening turn.
378
402
  if (packsInjectedFor.get(key) === stamp) continue;
379
- const block = formatPackForContext(pack, packsLib);
403
+ const block = formatPackForContext(pack, packsLib, { progressive });
380
404
  if (!tryUseRecallBudget(budget, block)) continue;
381
405
  packsInjectedFor.set(key, stamp);
382
406
  lastInjected.packs.push(pack.name || m.dir);
@@ -536,10 +560,11 @@ async function promptWithDynamicContext(prompt, opts = {}) {
536
560
  const entitiesLib = require("./entities");
537
561
  let packMatches = [];
538
562
  let entityMatches = [];
563
+ const packLimit = packMatchLimit();
539
564
  try {
540
565
  packMatches = mergeMatches(
541
- packsLib.matchPacks(userText, { limit: 3 }),
542
- fullContext ? packsLib.matchPacks(fullContext, { limit: 3 }) : [],
566
+ packsLib.matchPacks(userText, { limit: packLimit }),
567
+ fullContext ? packsLib.matchPacks(fullContext, { limit: packLimit }) : [],
543
568
  (m) => m.dir,
544
569
  );
545
570
  } catch (e) {}
@@ -553,7 +578,7 @@ async function promptWithDynamicContext(prompt, opts = {}) {
553
578
  const candPacks = packMatches;
554
579
  const candEntities = entityMatches;
555
580
  ({ packMatches, entityMatches } = await filterMatches(userText, fullContext, packMatches, entityMatches));
556
- packMatches = packMatches.slice(0, 3);
581
+ packMatches = packMatches.slice(0, packLimit);
557
582
  entityMatches = entityMatches.slice(0, 4);
558
583
  logRecall(userText, candPacks, candEntities, packMatches, entityMatches);
559
584
  const budget = memoryRecallBudget();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "2.6.21",
3
+ "version": "2.6.23",
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": {