@inetafrica/open-claudia 2.6.36 → 2.6.38
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/README.md +2 -1
- package/bin/cli.js +18 -0
- package/bin/ideas.js +69 -0
- package/bin/keyring.js +64 -0
- package/bin/lessons.js +72 -0
- package/bin/pack.js +45 -2
- package/bot.js +8 -0
- package/core/actions.js +10 -2
- package/core/config.js +10 -1
- package/core/day-seeds.js +98 -0
- package/core/dream.js +413 -18
- package/core/handlers.js +153 -9
- package/core/ideas.js +114 -0
- package/core/keyring.js +79 -0
- package/core/lessons.js +276 -0
- package/core/pack-review.js +95 -14
- package/core/packs.js +95 -2
- package/core/recall/discoverer.js +5 -2
- package/core/recall/graph.js +17 -0
- package/core/recall/index.js +12 -5
- package/core/redact.js +25 -3
- package/core/runner.js +44 -2
- package/core/subagent.js +20 -4
- package/core/system-prompt.js +51 -4
- package/package.json +11 -3
- package/test-abilities.js +53 -0
- package/test-ability-couse.js +68 -0
- package/test-ability-extraction.js +109 -0
- package/test-ability-merge-guard.js +42 -0
- package/test-ability-tiers.js +57 -0
- package/test-ability-transfer.js +70 -0
- package/test-learning-e2e.js +98 -0
- package/test-project-transcripts-smoke.js +50 -0
- package/test-recall-discoverer.js +3 -0
- package/test-recall-engine.js +7 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v2.6.37
|
|
4
|
+
- **`/recall [on|off]` — watch recall work.** A per-chat debug toggle that, when on, posts a short `🧠 Recall this turn` line just before each reply listing the packs/entities that surfaced — and on the **discoverer** engine, the one-line why-bullet for each. On a gated turn (pre-gate skipped recall) it says so; on a quiet turn with no matches it stays silent. The engines now return a `why` map + `gated` flag, `promptWithDynamicContext` captures a recall summary into the per-turn `consumeLastInjected()` buffer, and `runner.js` renders it when `settings.showRecall` is set. Off by default; flip with `/recall` (buttons) or `/recall on`.
|
|
5
|
+
|
|
3
6
|
## v2.6.36
|
|
4
7
|
- **Docs: document the dual-engine recall feature.** v2.6.34/v2.6.35 shipped the pluggable recall engine and `/engine` switch but the README and `.env.example` were never updated. This release fills the gap: README gains a "Pluggable recall" Features bullet, the `/engine` command row, a "Recall engines" narrative section, the `recall-stats` / `recall graph` CLI commands, and env-table rows for `RECALL_ENGINE` / `RECALL_GRAPH_DB` / `RECALL_METRICS` / `DREAM_TIER` (plus the corrected `DREAM_MODEL` default). `.env.example` gains `RECALL_ENGINE` and `DREAM_TIER`. Docs-only — no code changes.
|
|
5
8
|
|
package/README.md
CHANGED
|
@@ -189,6 +189,7 @@ When you select a project, the last conversation is automatically resumed. Tap "
|
|
|
189
189
|
| `/learn [<hint>]` | Capture the last piece of work into the matching context pack |
|
|
190
190
|
| `/skills [show\|remove <name>]` | List, show, or remove legacy learned skills |
|
|
191
191
|
| `/engine [classic\|discoverer]` | Switch the per-chat memory recall engine (default `classic`) |
|
|
192
|
+
| `/recall [on\|off]` | Toggle a per-turn "🧠 Recall this turn" debug line showing which packs/entities surfaced (and why, on discoverer) |
|
|
192
193
|
| `/soul` | View/edit assistant identity and personality |
|
|
193
194
|
| `/dreamsummary [on\|off]` | Toggle the post-dream memory summary in chat |
|
|
194
195
|
|
|
@@ -264,7 +265,7 @@ Open Claudia layers three memory systems on top of the backend's native sessions
|
|
|
264
265
|
|
|
265
266
|
**Entity memory** (`~/.open-claudia/entities/<slug>.md`) works the same way for the people, places, projects, orgs, and systems you mention — who they are, current truth, and a dated observation log. Mentioning a name injects its note.
|
|
266
267
|
|
|
267
|
-
**Recall engines** — how packs and entities get matched and surfaced is pluggable per chat via `/engine` (or the `RECALL_ENGINE` env default). **classic** (the default) is keyword FTS plus a relevance judge with headline injection — stable and unchanged. **discoverer** (opt-in) adds a typed-edge graph over the same corpus (`parent`/`governed-by`/`related` edges with weights in `recall-graph.db`) and runs: a pre-gate that skips recall on trivial turns → FTS seeding → spreading activation across the graph (1–2 hops — auto-pulls cross-cutting concerns the query never named) → a walker that reads each candidate and returns the genuinely-relevant set with one-line why-bullets (fail-open to keyword seeds, so it never recalls worse than classic). Edges form structurally from pack `parent` frontmatter and `[[links]]`, and strengthen via Hebbian co-use when the agent opens packs together (📖); weights decay over time. Inspect with `open-claudia recall-stats` and `open-claudia recall graph [--sync]
|
|
268
|
+
**Recall engines** — how packs and entities get matched and surfaced is pluggable per chat via `/engine` (or the `RECALL_ENGINE` env default). **classic** (the default) is keyword FTS plus a relevance judge with headline injection — stable and unchanged. **discoverer** (opt-in) adds a typed-edge graph over the same corpus (`parent`/`governed-by`/`related` edges with weights in `recall-graph.db`) and runs: a pre-gate that skips recall on trivial turns → FTS seeding → spreading activation across the graph (1–2 hops — auto-pulls cross-cutting concerns the query never named) → a walker that reads each candidate and returns the genuinely-relevant set with one-line why-bullets (fail-open to keyword seeds, so it never recalls worse than classic). Edges form structurally from pack `parent` frontmatter and `[[links]]`, and strengthen via Hebbian co-use when the agent opens packs together (📖); weights decay over time. Inspect with `open-claudia recall-stats` and `open-claudia recall graph [--sync]`, or flip on `/recall` to watch — per turn — which packs/entities surfaced and why, right in the chat. Switch back any time with `/engine classic`.
|
|
268
269
|
|
|
269
270
|
**Dream consolidation** — while the per-turn reviewer takes quick notes, *dream* is the slow overnight pass (default 4am, on a high-tier model — opus by default, set `DREAM_TIER` or `DREAM_MODEL`): it merges packs that drifted into the same topic, builds parent/sub pack trees with umbrella summaries, tightens descriptions and tags so the router matches with less noise, dedupes entities, cross-links notes, and tends the recall graph (structural sync, weight decay, orphan prune). Anything merged away is backed up under `~/.open-claudia/backup/dream-<stamp>/` first, and every dream that changes something reports in chat. Configure with `DREAM_CRON` / `DREAM_MODEL`, disable with `DREAM=off`.
|
|
270
271
|
|
package/bin/cli.js
CHANGED
|
@@ -289,6 +289,21 @@ switch (command) {
|
|
|
289
289
|
break;
|
|
290
290
|
}
|
|
291
291
|
|
|
292
|
+
case "lessons": {
|
|
293
|
+
require("./lessons").run(args.slice(1));
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
case "ideas": {
|
|
298
|
+
require("./ideas").run(args.slice(1));
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
case "keyring": {
|
|
303
|
+
require("./keyring").run(args.slice(1));
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
|
|
292
307
|
case "agent": {
|
|
293
308
|
require("./agent").run(args.slice(1));
|
|
294
309
|
break;
|
|
@@ -351,6 +366,9 @@ Memory tools:
|
|
|
351
366
|
(alias: tw; --help for options)
|
|
352
367
|
open-claudia pack list|show|match|archive|restore|archived Context packs: living topic docs (skills + memory)
|
|
353
368
|
open-claudia entity list|show|match|note Entity notes: people/places/projects memory
|
|
369
|
+
open-claudia lessons list|add|remove|show Always-loaded learned rules (cross-cutting, promoted after a miss)
|
|
370
|
+
open-claudia ideas list|add|remove|show Self-improvement backlog (captured by the nightly dream)
|
|
371
|
+
open-claudia keyring list|get|set|remove Operational creds the agent can reuse (plaintext; vault for secrets)
|
|
354
372
|
open-claudia dream [--dry-run] Run the memory consolidation pass now
|
|
355
373
|
|
|
356
374
|
Background work (only inside an active bot-spawned task):
|
package/bin/ideas.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// CLI: inspect and manage the self-improvement ideas backlog (ideas.md).
|
|
2
|
+
// open-claudia ideas list — show all ideas (newest first)
|
|
3
|
+
// open-claudia ideas add "<idea>" [--scope oc|work] — add one
|
|
4
|
+
// open-claudia ideas remove <id|text> — remove one
|
|
5
|
+
// open-claudia ideas show — raw file
|
|
6
|
+
// open-claudia ideas path — print the file path
|
|
7
|
+
|
|
8
|
+
const fs = require("fs");
|
|
9
|
+
const ideas = require("../core/ideas");
|
|
10
|
+
|
|
11
|
+
function run(args) {
|
|
12
|
+
const cmd = (args[0] || "list").toLowerCase();
|
|
13
|
+
const rest = args.slice(1);
|
|
14
|
+
|
|
15
|
+
switch (cmd) {
|
|
16
|
+
case "list": {
|
|
17
|
+
const all = ideas.listIdeas();
|
|
18
|
+
if (all.length === 0) return console.log(`No ideas yet (${ideas.IDEAS_FILE}).`);
|
|
19
|
+
console.log(`${all.length} idea(s) — self-improvement backlog (newest first):\n`);
|
|
20
|
+
for (const i of all) {
|
|
21
|
+
const date = i.date ? `${i.date} ` : "";
|
|
22
|
+
const scope = i.scope ? `(${i.scope}) ` : "";
|
|
23
|
+
console.log(`[${i.id}] ${date}${scope}${i.text}`);
|
|
24
|
+
}
|
|
25
|
+
console.log(`\nCap ${ideas.MAX_IDEAS}; edit: ${ideas.IDEAS_FILE}`);
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
case "add": {
|
|
30
|
+
let scope = "";
|
|
31
|
+
const words = [];
|
|
32
|
+
for (let i = 0; i < rest.length; i++) {
|
|
33
|
+
if (rest[i] === "--scope") { scope = rest[++i] || ""; continue; }
|
|
34
|
+
words.push(rest[i]);
|
|
35
|
+
}
|
|
36
|
+
const text = words.join(" ").trim();
|
|
37
|
+
if (!text) { console.error('Usage: ideas add "<idea>" [--scope oc|work]'); process.exitCode = 1; return; }
|
|
38
|
+
const r = ideas.addIdea({ text, scope });
|
|
39
|
+
if (r.added) console.log(`Added idea [${r.id}] (now ${r.count}).`);
|
|
40
|
+
else if (r.duplicate) console.log(`Already had that one [${r.id}].`);
|
|
41
|
+
else { console.error(`Not added: ${r.reason}`); process.exitCode = 1; }
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
case "remove":
|
|
46
|
+
case "rm": {
|
|
47
|
+
const key = rest.join(" ").trim();
|
|
48
|
+
if (!key) { console.error("Usage: ideas remove <id|text>"); process.exitCode = 1; return; }
|
|
49
|
+
console.log(ideas.removeIdea(key) ? `Removed ${key}.` : `No matching idea: ${key}`);
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
case "show": {
|
|
54
|
+
let raw = "";
|
|
55
|
+
try { raw = fs.readFileSync(ideas.IDEAS_FILE, "utf-8"); } catch (e) { raw = "(no ideas file yet)"; }
|
|
56
|
+
console.log(`=== ${ideas.IDEAS_FILE} ===\n${raw}`);
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
case "path":
|
|
61
|
+
console.log(ideas.IDEAS_FILE);
|
|
62
|
+
break;
|
|
63
|
+
|
|
64
|
+
default:
|
|
65
|
+
console.log('Usage: open-claudia ideas [list|add "<idea>" [--scope oc|work]|remove <id|text>|show|path]');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = { run };
|
package/bin/keyring.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// CLI: inspect and manage the operational keyring — plaintext creds the
|
|
2
|
+
// agent is allowed to use on its own (API keys, base URLs, account IDs).
|
|
3
|
+
// open-claudia keyring list — show all keys (values masked)
|
|
4
|
+
// open-claudia keyring get <name> — print the full value (local use)
|
|
5
|
+
// open-claudia keyring set <name> <value> — store/replace a credential
|
|
6
|
+
// open-claudia keyring remove <name> — delete one
|
|
7
|
+
// open-claudia keyring path — print the file path
|
|
8
|
+
//
|
|
9
|
+
// For genuine personal secrets the bot should NOT use unattended, use the
|
|
10
|
+
// vault (/vault) instead — it stays encrypted and locked.
|
|
11
|
+
|
|
12
|
+
const keyring = require("../core/keyring");
|
|
13
|
+
|
|
14
|
+
function run(args) {
|
|
15
|
+
const cmd = (args[0] || "list").toLowerCase();
|
|
16
|
+
const rest = args.slice(1);
|
|
17
|
+
|
|
18
|
+
switch (cmd) {
|
|
19
|
+
case "list": {
|
|
20
|
+
const entries = keyring.list();
|
|
21
|
+
const names = Object.keys(entries);
|
|
22
|
+
if (names.length === 0) return console.log(`Keyring is empty (${keyring.KEYRING_FILE}).`);
|
|
23
|
+
console.log(`${names.length} credential(s) — available to the agent as env vars:\n`);
|
|
24
|
+
for (const k of names) console.log(`${k} = ${entries[k]}`);
|
|
25
|
+
console.log(`\nFile: ${keyring.KEYRING_FILE}`);
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
case "get": {
|
|
30
|
+
const key = rest[0];
|
|
31
|
+
if (!key) { console.error("Usage: keyring get <name>"); process.exitCode = 1; return; }
|
|
32
|
+
const v = keyring.get(key);
|
|
33
|
+
if (v === null) { console.error(`No such key: ${key}`); process.exitCode = 1; return; }
|
|
34
|
+
console.log(v);
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
case "set": {
|
|
39
|
+
const key = rest[0];
|
|
40
|
+
const value = rest.slice(1).join(" ").trim();
|
|
41
|
+
if (!key || !value) { console.error('Usage: keyring set <name> <value>'); process.exitCode = 1; return; }
|
|
42
|
+
keyring.set(key, value);
|
|
43
|
+
console.log(`Stored ${key} (available to the agent next turn).`);
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
case "remove":
|
|
48
|
+
case "rm": {
|
|
49
|
+
const key = rest[0];
|
|
50
|
+
if (!key) { console.error("Usage: keyring remove <name>"); process.exitCode = 1; return; }
|
|
51
|
+
console.log(keyring.remove(key) ? `Removed ${key}.` : `No such key: ${key}`);
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
case "path":
|
|
56
|
+
console.log(keyring.KEYRING_FILE);
|
|
57
|
+
break;
|
|
58
|
+
|
|
59
|
+
default:
|
|
60
|
+
console.log('Usage: open-claudia keyring [list|get <name>|set <name> <value>|remove <name>|path]');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = { run };
|
package/bin/lessons.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// CLI: inspect and manage the always-loaded Lessons tier.
|
|
2
|
+
// open-claudia lessons list — show all lessons
|
|
3
|
+
// open-claudia lessons add "<rule>" [--src <pack>] — add one (origin=user)
|
|
4
|
+
// open-claudia lessons remove <id|text> — remove one
|
|
5
|
+
// open-claudia lessons show — raw file + injected block
|
|
6
|
+
// open-claudia lessons path — print the file path
|
|
7
|
+
|
|
8
|
+
const fs = require("fs");
|
|
9
|
+
const lessons = require("../core/lessons");
|
|
10
|
+
|
|
11
|
+
function run(args) {
|
|
12
|
+
const cmd = (args[0] || "list").toLowerCase();
|
|
13
|
+
const rest = args.slice(1);
|
|
14
|
+
|
|
15
|
+
switch (cmd) {
|
|
16
|
+
case "list": {
|
|
17
|
+
const all = lessons.listLessons();
|
|
18
|
+
if (all.length === 0) return console.log(`No lessons yet (${lessons.LESSONS_FILE}).`);
|
|
19
|
+
console.log(`${all.length} lesson(s) — always loaded into every conversation:\n`);
|
|
20
|
+
for (const l of all) {
|
|
21
|
+
const r = l.reinforced ? ` · reinforced ${l.reinforced}×` : "";
|
|
22
|
+
const src = l.src ? ` · src: ${l.src}` : "";
|
|
23
|
+
console.log(`[${l.id}] ${l.text}\n origin: ${l.origin || "user"}${src}${r}`);
|
|
24
|
+
}
|
|
25
|
+
console.log(`\nCap ${lessons.MAX_LESSONS}; edit: ${lessons.LESSONS_FILE}`);
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
case "add": {
|
|
30
|
+
// Pull optional --src <pack>, rest is the lesson text.
|
|
31
|
+
let src = "";
|
|
32
|
+
const words = [];
|
|
33
|
+
for (let i = 0; i < rest.length; i++) {
|
|
34
|
+
if (rest[i] === "--src") { src = rest[++i] || ""; continue; }
|
|
35
|
+
words.push(rest[i]);
|
|
36
|
+
}
|
|
37
|
+
const text = words.join(" ").trim();
|
|
38
|
+
if (!text) { console.error('Usage: lessons add "<rule>" [--src <pack>]'); process.exitCode = 1; return; }
|
|
39
|
+
const r = lessons.addLesson({ text, src, origin: "user" });
|
|
40
|
+
if (r.added) console.log(`Added lesson [${r.id}]${r.overCap ? ` (now ${r.count}, over cap ${lessons.MAX_LESSONS} — the nightly dream will tidy)` : ""}.`);
|
|
41
|
+
else if (r.reinforced) console.log(`Already had that one — reinforced [${r.id}].`);
|
|
42
|
+
else { console.error(`Not added: ${r.reason}`); process.exitCode = 1; }
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
case "remove":
|
|
47
|
+
case "rm": {
|
|
48
|
+
const key = rest.join(" ").trim();
|
|
49
|
+
if (!key) { console.error("Usage: lessons remove <id|text>"); process.exitCode = 1; return; }
|
|
50
|
+
console.log(lessons.removeLesson(key) ? `Removed ${key}.` : `No matching lesson: ${key}`);
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
case "show": {
|
|
55
|
+
let raw = "";
|
|
56
|
+
try { raw = fs.readFileSync(lessons.LESSONS_FILE, "utf-8"); } catch (e) { raw = "(no lessons file yet)"; }
|
|
57
|
+
console.log(`=== ${lessons.LESSONS_FILE} ===\n${raw}`);
|
|
58
|
+
const block = lessons.loadLessonsBlock();
|
|
59
|
+
console.log(`\n=== injected block (${block.length} chars, budget ${lessons.MAX_LESSONS_CHARS}) ===\n${block || "(empty — nothing injected)"}`);
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
case "path":
|
|
64
|
+
console.log(lessons.LESSONS_FILE);
|
|
65
|
+
break;
|
|
66
|
+
|
|
67
|
+
default:
|
|
68
|
+
console.log('Usage: open-claudia lessons [list|add "<rule>" [--src <pack>]|remove <id|text>|show|path]');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = { run };
|
package/bin/pack.js
CHANGED
|
@@ -20,13 +20,56 @@ function run(args) {
|
|
|
20
20
|
if (all.length === 0) return console.log(`No packs yet (${packs.PACKS_DIR}).`);
|
|
21
21
|
for (const p of all) {
|
|
22
22
|
const used = p.last_used ? ` last-used ${p.last_used.slice(0, 10)} (${p.usage_count || 0}×)` : "";
|
|
23
|
-
|
|
23
|
+
const skill = p.skill ? " ★skill" : "";
|
|
24
|
+
const ability = p.kind === "ability" ? " ◆ability" : "";
|
|
25
|
+
console.log(`${p.dir} — ${p.name}${skill}${ability}${p.tags.length ? ` [${p.tags.join(", ")}]` : ""}${used}\n ${p.description}`);
|
|
24
26
|
}
|
|
25
27
|
const archived = packs.listArchived();
|
|
26
28
|
if (archived.length) console.log(`\n(${archived.length} archived — open-claudia pack archived)`);
|
|
27
29
|
break;
|
|
28
30
|
}
|
|
29
31
|
|
|
32
|
+
case "skills": {
|
|
33
|
+
const all = packs.listSkillPacks();
|
|
34
|
+
if (all.length === 0) return console.log("No skill-flagged packs yet. Flag one: open-claudia pack skill <dir> on");
|
|
35
|
+
console.log(`${all.length} skill(s) — always surfaced in the system prompt:\n`);
|
|
36
|
+
for (const p of all) console.log(`${p.dir} — ${p.name}\n ${p.description}`);
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
case "skill": {
|
|
41
|
+
const dir = rest[0];
|
|
42
|
+
const on = (rest[1] || "on").toLowerCase() !== "off";
|
|
43
|
+
if (!dir) { console.error("Usage: pack skill <dir> [on|off]"); process.exitCode = 1; return; }
|
|
44
|
+
const p = packs.setSkill(dir, on);
|
|
45
|
+
if (!p) { console.error(`No pack: ${dir}`); process.exitCode = 1; return; }
|
|
46
|
+
console.log(`${p.dir} skill flag → ${on ? "on (now always indexed)" : "off"}.`);
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
case "abilities": {
|
|
51
|
+
const all = packs.listAbilities();
|
|
52
|
+
if (all.length === 0) return console.log("No abilities yet. Mark one: open-claudia pack kind <dir> ability");
|
|
53
|
+
console.log(`${all.length} ability(ies) — reusable how-tos that transfer across projects:\n`);
|
|
54
|
+
for (const p of all) {
|
|
55
|
+
const prov = p.learned_on
|
|
56
|
+
? `\n learned on ${p.learned_on}${p.applied_on.length ? `, applied on ${p.applied_on.join(", ")}` : ""}`
|
|
57
|
+
: "";
|
|
58
|
+
console.log(`${p.dir} — ${p.name}${p.skill ? " ★always-on" : ""}\n ${p.description}${prov}`);
|
|
59
|
+
}
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
case "kind": {
|
|
64
|
+
const dir = rest[0];
|
|
65
|
+
const kind = rest[1];
|
|
66
|
+
if (!dir || !kind) { console.error("Usage: pack kind <dir> <ability|context>"); process.exitCode = 1; return; }
|
|
67
|
+
const p = packs.setKind(dir, kind);
|
|
68
|
+
if (!p) { console.error(`No pack: ${dir}`); process.exitCode = 1; return; }
|
|
69
|
+
console.log(`${p.dir} kind → ${p.kind}.`);
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
|
|
30
73
|
case "archive": {
|
|
31
74
|
const p = packs.archivePack(rest[0]);
|
|
32
75
|
if (!p) { console.error(`No pack: ${rest[0]}`); process.exitCode = 1; return; }
|
|
@@ -123,7 +166,7 @@ function run(args) {
|
|
|
123
166
|
}
|
|
124
167
|
|
|
125
168
|
default:
|
|
126
|
-
console.log("Usage: open-claudia pack [list|show <dir>|match \"<text>\"|migrate|remove <dir>|archive <dir>|restore <dir>|archived|reindex]");
|
|
169
|
+
console.log("Usage: open-claudia pack [list|skills|abilities|skill <dir> [on|off]|kind <dir> <ability|context>|show <dir>|match \"<text>\"|migrate|remove <dir>|archive <dir>|restore <dir>|archived|reindex]");
|
|
127
170
|
}
|
|
128
171
|
}
|
|
129
172
|
|
package/bot.js
CHANGED
|
@@ -23,6 +23,14 @@ require("./core/handlers"); // side-effect: register slash commands
|
|
|
23
23
|
|
|
24
24
|
const CURRENT_VERSION = require(path.join(__dirname, "package.json")).version;
|
|
25
25
|
|
|
26
|
+
// Register existing operational-keyring values so the redactor scrubs them
|
|
27
|
+
// from any output we ship to chat. (New ones are registered on /keyring set.)
|
|
28
|
+
try {
|
|
29
|
+
const keyring = require("./core/keyring");
|
|
30
|
+
const { registerSecrets } = require("./core/redact");
|
|
31
|
+
registerSecrets(Object.values(keyring.all()));
|
|
32
|
+
} catch (e) { /* keyring optional — never block startup */ }
|
|
33
|
+
|
|
26
34
|
registry.setHandlers({ onMessage, onAction });
|
|
27
35
|
const adapters = registry.bootstrap();
|
|
28
36
|
|
package/core/actions.js
CHANGED
|
@@ -277,9 +277,17 @@ async function handleAction(envelope) {
|
|
|
277
277
|
if (d.startsWith("eng:")) {
|
|
278
278
|
const v = d.slice(4);
|
|
279
279
|
const recall = require("./recall");
|
|
280
|
-
|
|
280
|
+
// The default engine is stored as null (so it shows "(default)"); any other
|
|
281
|
+
// known engine is stored explicitly as an opt-out.
|
|
282
|
+
state.settings.recallEngine = (v === recall.DEFAULT_ENGINE || !recall.listEngines().includes(v)) ? null : v;
|
|
281
283
|
saveState();
|
|
282
|
-
await send(`Recall engine: ${state.settings.recallEngine
|
|
284
|
+
await send(`Recall engine: ${recall.activeEngineName(state.settings)}${state.settings.recallEngine ? "" : " (default)"}`);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (d.startsWith("rcl:")) {
|
|
288
|
+
state.settings.showRecall = d.slice(4) === "on";
|
|
289
|
+
saveState();
|
|
290
|
+
await send(`Recall debug: ${state.settings.showRecall ? "on" : "off"}`);
|
|
283
291
|
return;
|
|
284
292
|
}
|
|
285
293
|
if (d.startsWith("cw:")) {
|
package/core/config.js
CHANGED
|
@@ -200,7 +200,16 @@ function loadChannels() {
|
|
|
200
200
|
}
|
|
201
201
|
|
|
202
202
|
function botSubprocessEnv() {
|
|
203
|
-
|
|
203
|
+
const base = { ...process.env, PATH: FULL_PATH, HOME: process.env.HOME || require("os").homedir() };
|
|
204
|
+
// Merge operational keyring creds so the agent can *use* what it remembered.
|
|
205
|
+
// Read fresh each spawn so a cred set mid-session is live next turn without a
|
|
206
|
+
// restart. process.env wins on conflict — bot infra config is authoritative.
|
|
207
|
+
try {
|
|
208
|
+
const keyring = require("./keyring");
|
|
209
|
+
return { ...keyring.all(), ...base };
|
|
210
|
+
} catch (e) {
|
|
211
|
+
return base;
|
|
212
|
+
}
|
|
204
213
|
}
|
|
205
214
|
|
|
206
215
|
module.exports = {
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Per-day seed log. The compaction engine already archives each full brief to
|
|
2
|
+
// briefs/ (one file per compaction). This module keeps a per-DAY rollup — one
|
|
3
|
+
// file per calendar day, global across projects/channels — that appends the
|
|
4
|
+
// condensed "what seeds the next conversation" digest every time a compaction
|
|
5
|
+
// fires. It is the raw material the nightly dream reads to learn what was
|
|
6
|
+
// actually worked on that day: the input for self-introspection [3b] and for
|
|
7
|
+
// second-chance lesson promotion [1]. Detailed text stays in briefs/; this is
|
|
8
|
+
// the digest, labelled by project + time so the dream can reconstruct the day.
|
|
9
|
+
//
|
|
10
|
+
// Seeds are read-only for the dream (raw input, never edited by it) and never
|
|
11
|
+
// hold secrets beyond whatever the compaction summary already contains.
|
|
12
|
+
|
|
13
|
+
const fs = require("fs");
|
|
14
|
+
const path = require("path");
|
|
15
|
+
const CONFIG_DIR = require("../config-dir");
|
|
16
|
+
|
|
17
|
+
const SEEDS_DIR = process.env.DAY_SEEDS_DIR ? path.resolve(process.env.DAY_SEEDS_DIR) : path.join(CONFIG_DIR, "seeds");
|
|
18
|
+
// Safety cap on a single day's file; if exceeded we trim from the front so the
|
|
19
|
+
// most recent compactions always survive.
|
|
20
|
+
const MAX_DAY_SEED_CHARS = Number(process.env.DAY_SEED_MAX_CHARS || 60000);
|
|
21
|
+
|
|
22
|
+
// Local calendar day (YYYY-MM-DD). Local, not UTC, so "what I worked on today"
|
|
23
|
+
// lines up with the user's day and the 4am dream reads the right file.
|
|
24
|
+
function dayStamp(d = new Date()) {
|
|
25
|
+
const y = d.getFullYear();
|
|
26
|
+
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
27
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
28
|
+
return `${y}-${m}-${day}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function daySeedPath(date) {
|
|
32
|
+
return path.join(SEEDS_DIR, `${date || dayStamp()}.md`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Append one compaction digest to today's seed file. Best-effort: never throws
|
|
36
|
+
// into the compaction path (a seed-log failure must not break compaction).
|
|
37
|
+
function appendDaySeed(entry = {}) {
|
|
38
|
+
try {
|
|
39
|
+
const body = String(entry.summary || "").trim();
|
|
40
|
+
if (!body) return null;
|
|
41
|
+
fs.mkdirSync(SEEDS_DIR, { recursive: true, mode: 0o700 });
|
|
42
|
+
const when = entry.when ? new Date(entry.when) : new Date();
|
|
43
|
+
const date = dayStamp(when);
|
|
44
|
+
const file = daySeedPath(date);
|
|
45
|
+
const head = [
|
|
46
|
+
`## ${when.toISOString()}`,
|
|
47
|
+
entry.project ? `Project: ${entry.project}` : null,
|
|
48
|
+
entry.channel ? `Channel: ${entry.channel}` : null,
|
|
49
|
+
entry.briefPath ? `Full brief: ${entry.briefPath}` : null,
|
|
50
|
+
"",
|
|
51
|
+
].filter((l) => l !== null).join("\n");
|
|
52
|
+
const block = `${head}${body}\n\n`;
|
|
53
|
+
|
|
54
|
+
let existing = "";
|
|
55
|
+
try { existing = fs.readFileSync(file, "utf-8"); } catch (e) {}
|
|
56
|
+
const fileHeader = `# Day seed — ${date}\nCondensed compaction digests captured this day. The nightly dream reviews these to learn what was worked on.\n\n`;
|
|
57
|
+
let next = existing ? existing + block : fileHeader + block;
|
|
58
|
+
if (next.length > MAX_DAY_SEED_CHARS) next = next.slice(next.length - MAX_DAY_SEED_CHARS);
|
|
59
|
+
fs.writeFileSync(file, next, { mode: 0o600 });
|
|
60
|
+
return file;
|
|
61
|
+
} catch (e) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function listDaySeedDates() {
|
|
67
|
+
try {
|
|
68
|
+
return fs.readdirSync(SEEDS_DIR)
|
|
69
|
+
.filter((f) => /^\d{4}-\d{2}-\d{2}\.md$/.test(f))
|
|
70
|
+
.map((f) => f.replace(/\.md$/, ""))
|
|
71
|
+
.sort();
|
|
72
|
+
} catch (e) { return []; }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function readDaySeed(date) {
|
|
76
|
+
try { return fs.readFileSync(daySeedPath(date), "utf-8"); }
|
|
77
|
+
catch (e) { return ""; }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// The dream's view: the last `days` calendar days that actually have content.
|
|
81
|
+
// The dream fires at 04:00, so "today" is usually empty and the work lives in
|
|
82
|
+
// the prior day(s); pulling a small window catches work that straddles midnight.
|
|
83
|
+
function recentDaySeeds({ days = 2 } = {}) {
|
|
84
|
+
const all = listDaySeedDates();
|
|
85
|
+
const picked = [];
|
|
86
|
+
const texts = [];
|
|
87
|
+
for (let i = all.length - 1; i >= 0 && picked.length < days; i--) {
|
|
88
|
+
const t = readDaySeed(all[i]);
|
|
89
|
+
if (t.trim()) { picked.unshift(all[i]); texts.unshift(t); }
|
|
90
|
+
}
|
|
91
|
+
return { dates: picked, text: texts.join("\n\n") };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = {
|
|
95
|
+
SEEDS_DIR, MAX_DAY_SEED_CHARS,
|
|
96
|
+
dayStamp, daySeedPath,
|
|
97
|
+
appendDaySeed, readDaySeed, listDaySeedDates, recentDaySeeds,
|
|
98
|
+
};
|