@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 +30 -16
- package/core/system-prompt.js +35 -10
- package/package.json +1 -1
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
|
|
960
|
-
if (!
|
|
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(
|
|
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: ${
|
|
970
|
+
return send(`File: ${file}`);
|
|
966
971
|
}
|
|
967
972
|
|
|
968
973
|
if (sub === "remove" && name) {
|
|
969
|
-
const
|
|
970
|
-
if (!
|
|
971
|
-
|
|
972
|
-
return send(`Removed skill: ${
|
|
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
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
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
|
-
`
|
|
992
|
-
`
|
|
993
|
-
`
|
|
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
|
},
|
package/core/system-prompt.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
353
|
-
|
|
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
|
|
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
|
|
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:
|
|
542
|
-
fullContext ? packsLib.matchPacks(fullContext, { limit:
|
|
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,
|
|
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