@inetafrica/open-claudia 2.6.23 → 2.6.24

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/bin/cli.js CHANGED
@@ -335,7 +335,7 @@ Memory tools:
335
335
  (alias: ts; --all for every project; --help for options)
336
336
  open-claudia transcript-window <pattern> Search project transcript, show hits with context
337
337
  (alias: tw; --help for options)
338
- open-claudia pack list|show|match|migrate Context packs: living topic docs (skills + memory)
338
+ open-claudia pack list|show|match|archive|restore|archived Context packs: living topic docs (skills + memory)
339
339
  open-claudia entity list|show|match|note Entity notes: people/places/projects memory
340
340
  open-claudia dream [--dry-run] Run the memory consolidation pass now
341
341
 
package/bin/pack.js CHANGED
@@ -19,9 +19,36 @@ function run(args) {
19
19
  const all = packs.listPacks();
20
20
  if (all.length === 0) return console.log(`No packs yet (${packs.PACKS_DIR}).`);
21
21
  for (const p of all) {
22
- const used = p.last_used ? ` last-used ${p.last_used.slice(0, 10)}` : "";
22
+ const used = p.last_used ? ` last-used ${p.last_used.slice(0, 10)} (${p.usage_count || 0}×)` : "";
23
23
  console.log(`${p.dir} — ${p.name}${p.tags.length ? ` [${p.tags.join(", ")}]` : ""}${used}\n ${p.description}`);
24
24
  }
25
+ const archived = packs.listArchived();
26
+ if (archived.length) console.log(`\n(${archived.length} archived — open-claudia pack archived)`);
27
+ break;
28
+ }
29
+
30
+ case "archive": {
31
+ const p = packs.archivePack(rest[0]);
32
+ if (!p) { console.error(`No pack: ${rest[0]}`); process.exitCode = 1; return; }
33
+ console.log(`Archived pack ${p.dir} → ${path.join(packs.PACKS_DIR, ".archived", p.dir)}\nRestore with: open-claudia pack restore ${p.dir}`);
34
+ break;
35
+ }
36
+
37
+ case "restore": {
38
+ let p;
39
+ try { p = packs.restorePack(rest[0]); }
40
+ catch (e) { console.error(e.message); process.exitCode = 1; return; }
41
+ if (!p) { console.error(`No archived pack: ${rest[0]}`); process.exitCode = 1; return; }
42
+ console.log(`Restored pack ${p.dir}.`);
43
+ break;
44
+ }
45
+
46
+ case "archived": {
47
+ const all = packs.listArchived();
48
+ if (all.length === 0) return console.log("No archived packs.");
49
+ for (const p of all) {
50
+ console.log(`${p.dir} — ${p.name} (archived ${(p.archived || "").slice(0, 10)}, last-used ${(p.last_used || "never").slice(0, 10)}, ${p.usage_count || 0}×)`);
51
+ }
25
52
  break;
26
53
  }
27
54
 
@@ -93,7 +120,7 @@ function run(args) {
93
120
  }
94
121
 
95
122
  default:
96
- console.log("Usage: open-claudia pack [list|show <dir>|match \"<text>\"|migrate|remove <dir>|reindex]");
123
+ console.log("Usage: open-claudia pack [list|show <dir>|match \"<text>\"|migrate|remove <dir>|archive <dir>|restore <dir>|archived|reindex]");
97
124
  }
98
125
  }
99
126
 
package/core/dream.js CHANGED
@@ -14,6 +14,7 @@ const path = require("path");
14
14
  const cron = require("node-cron");
15
15
 
16
16
  const CONFIG_DIR = require("../config-dir");
17
+ const { config } = require("./config");
17
18
  const packs = require("./packs");
18
19
  const entities = require("./entities");
19
20
  const persona = require("./persona");
@@ -23,7 +24,13 @@ const DREAM_MODEL = process.env.DREAM_MODEL || "sonnet";
23
24
  const DREAM_CRON = process.env.DREAM_CRON || "0 4 * * *";
24
25
  const MAX_PACK_CHARS = 2500;
25
26
  const MAX_ENTITY_CHARS = 900;
26
- const LIMITS = { merges: 3, umbrellas: 2, parents: 8, retag: 8, entity_merges: 3, entity_notes: 4 };
27
+ const LIMITS = { merges: 3, umbrellas: 2, parents: 8, retag: 8, entity_merges: 3, entity_notes: 4, archive: 5 };
28
+
29
+ // Retirement guard: even if the model proposes archiving, only act when a pack
30
+ // is genuinely cold — old enough, idle long enough, and rarely used. Protects
31
+ // freshly-created packs the user just made.
32
+ const ARCHIVE_IDLE_DAYS = Number(process.env.DREAM_ARCHIVE_IDLE_DAYS || 30);
33
+ const ARCHIVE_MAX_USAGE = Number(process.env.DREAM_ARCHIVE_MAX_USAGE || 3);
27
34
 
28
35
  let _dreaming = false;
29
36
 
@@ -31,6 +38,15 @@ function enabled() {
31
38
  return (process.env.DREAM || "on").toLowerCase() !== "off";
32
39
  }
33
40
 
41
+ // Whether to post the post-dream summary into the owner's chat. On by
42
+ // default; toggled with /dreamsummary (persisted to .env, read back from
43
+ // config on restart, with the live process.env taking precedence so the
44
+ // toggle takes effect without a restart).
45
+ function summaryEnabled() {
46
+ const v = process.env.DREAM_SUMMARY ?? config.DREAM_SUMMARY ?? "on";
47
+ return String(v).toLowerCase() !== "off";
48
+ }
49
+
34
50
  function clip(s, n) {
35
51
  const t = String(s || "");
36
52
  return t.length > n ? t.slice(0, n) + "\n…[truncated]" : t;
@@ -45,7 +61,7 @@ function buildDreamPrompt() {
45
61
  `name: ${p.name}`,
46
62
  `description: ${p.description}`,
47
63
  `tags: ${p.tags.join(", ")}`,
48
- `last_used: ${p.last_used || "never"}`,
64
+ `last_used: ${p.last_used || "never"} (used ${p.usage_count || 0}×)`,
49
65
  `Stance: ${p.sections.Stance}`,
50
66
  `Procedure: ${p.sections.Procedure}`,
51
67
  `State: ${p.sections.State}`,
@@ -83,13 +99,14 @@ Your job — decide what consolidation, if any, is warranted:
83
99
  4. retag: tighten descriptions and tags. The router FTS-matches incoming messages against name/description/tags, so generic words there cause false matches. Descriptions should be one specific line; tags specific nouns.
84
100
  5. entity_merges: the same real-world entity recorded twice gets merged (the better slug wins).
85
101
  6. entity_notes: rewrite an entity's Notes to be current and cross-linked — mention related packs as [[pack-dir]] and related entities by name.
86
- 7. persona: evolve the persona GENTLYkeep its structure and length (under 2200 chars), adjust only what recent work justifies (a new habit, a sharpened quirk). Most dreams should return null here.
87
- 8. report: a short chat message to the owner, written AS Open Claudia in first person warm, a little playful, a few emojis, mobile-friendly (2-6 short lines). Say what you tidied and why it helps. If you changed nothing, say the memory is in good shape, charmingly.
102
+ 7. archive: retire packs that have gone cold long unused AND rarely used over their life (consult last_used and the usage count). Archiving moves a pack out of the live index (reversibly, backed up) so it stops adding recall noise. ONLY propose packs idle ≥${ARCHIVE_IDLE_DAYS} days and used ≤${ARCHIVE_MAX_USAGE}× total; never an umbrella/parent pack that still has children; when in doubt, leave it. Give a one-line reason.
103
+ 8. persona: evolve the persona GENTLY keep its structure and length (under 2200 chars), adjust only what recent work justifies (a new habit, a sharpened quirk). Most dreams should return null here.
104
+ 9. report: a short chat message to the owner, written AS Open Claudia in first person — warm, a little playful, a few emojis, mobile-friendly (2-6 short lines). Say what you tidied and why it helps. If you changed nothing, say the memory is in good shape, charmingly.
88
105
 
89
106
  Hard rules:
90
107
  - Never invent packs/entities not listed above; reference them by exact dir/slug.
91
108
  - Never store secrets, tokens, passwords, or credentials anywhere.
92
- - Limits: ≤${LIMITS.merges} merges, ≤${LIMITS.umbrellas} umbrellas, ≤${LIMITS.parents} parents, ≤${LIMITS.retag} retags, ≤${LIMITS.entity_merges} entity merges, ≤${LIMITS.entity_notes} entity note rewrites.
109
+ - Limits: ≤${LIMITS.merges} merges, ≤${LIMITS.umbrellas} umbrellas, ≤${LIMITS.parents} parents, ≤${LIMITS.retag} retags, ≤${LIMITS.entity_merges} entity merges, ≤${LIMITS.entity_notes} entity note rewrites, ≤${LIMITS.archive} archives.
93
110
  - An empty decision is a perfectly good decision.
94
111
 
95
112
  Reply with ONLY a JSON object, no prose, no code fences:
@@ -99,6 +116,7 @@ Reply with ONLY a JSON object, no prose, no code fences:
99
116
  "retag": [{"pack": "<dir>", "description": "<one specific line>", "tags": ["..."]}],
100
117
  "entity_merges": [{"into": "<slug>", "from": ["<slug>"], "notes": "<merged Notes or null>"}],
101
118
  "entity_notes": [{"entity": "<slug>", "notes": "<rewritten Notes>"}],
119
+ "archive": [{"pack": "<dir>", "reason": "<one line: why it's cold>"}],
102
120
  "persona": null,
103
121
  "report": "<chat message>"}`;
104
122
  }
@@ -118,6 +136,7 @@ function parseDream(text) {
118
136
  retag: arr("retag"),
119
137
  entity_merges: arr("entity_merges"),
120
138
  entity_notes: arr("entity_notes"),
139
+ archive: arr("archive"),
121
140
  persona: typeof obj.persona === "string" ? obj.persona : null,
122
141
  report: typeof obj.report === "string" ? obj.report.trim() : "",
123
142
  };
@@ -259,6 +278,27 @@ function applyDream(decision, backupRoot) {
259
278
  } catch (e) { console.warn(`[dream] entity notes failed: ${e.message}`); }
260
279
  }
261
280
 
281
+ if (decision.archive && decision.archive.length) {
282
+ const allPacks = packs.listPacks();
283
+ for (const a of decision.archive) {
284
+ try {
285
+ const dir = a?.pack;
286
+ if (!dir || gone.has(dir)) continue;
287
+ const pack = packs.readPack(dir);
288
+ if (!pack) continue;
289
+ if (allPacks.some((p) => p.parent === dir)) continue; // never orphan children
290
+ const ageDays = pack.created ? (Date.now() - Date.parse(pack.created)) / 86400000 : Infinity;
291
+ const idleDays = pack.last_used ? (Date.now() - Date.parse(pack.last_used)) / 86400000 : ageDays;
292
+ const uses = Number(pack.usage_count) || 0;
293
+ if (ageDays < ARCHIVE_IDLE_DAYS || idleDays < ARCHIVE_IDLE_DAYS || uses > ARCHIVE_MAX_USAGE) continue;
294
+ backupPack(dir, backupRoot);
295
+ packs.archivePack(dir);
296
+ gone.add(dir);
297
+ lines.push(`🗃 Archived ${dir} — ${a.reason || `idle ${Math.round(idleDays)}d, used ${uses}×`}`);
298
+ } catch (e) { console.warn(`[dream] archive failed: ${e.message}`); }
299
+ }
300
+ }
301
+
262
302
  if (decision.persona) {
263
303
  try {
264
304
  if (persona.personaExists()) {
@@ -305,8 +345,9 @@ async function runDream({ trigger = "manual" } = {}) {
305
345
  }
306
346
 
307
347
  // In-bot scheduler: one quiet-hours cron, reporting to the owner's
308
- // Telegram chat. Silent (console only) when the dream changed nothing
309
- // a nightly "all tidy" ping would be noise.
348
+ // Telegram chat. When the dream summary is on (default) it posts a short
349
+ // note after every run — including the charming "all tidy" line when
350
+ // nothing changed. /dreamsummary off mutes it to console only.
310
351
  function initDream(adapters) {
311
352
  if (!enabled()) return;
312
353
  if (!cron.validate(DREAM_CRON)) {
@@ -318,7 +359,8 @@ function initDream(adapters) {
318
359
  const res = await runDream({ trigger: "cron" });
319
360
  if (res.skipped) { console.log(`dream: skipped — ${res.skipped}`); return; }
320
361
  console.log(`dream: applied ${res.applied.length} change(s)`);
321
- if (res.applied.length === 0) return;
362
+ if (!summaryEnabled()) { console.log("dream: summary muted (DREAM_SUMMARY=off)"); return; }
363
+ if (!res.message) return;
322
364
  const { CHAT_ID } = require("./config");
323
365
  const tg = (adapters || []).find((a) => a.type === "telegram");
324
366
  if (tg && CHAT_ID) await tg.send(CHAT_ID, res.message);
@@ -329,4 +371,4 @@ function initDream(adapters) {
329
371
  console.log(`dream: scheduled (${DREAM_CRON}, model ${DREAM_MODEL})`);
330
372
  }
331
373
 
332
- module.exports = { runDream, initDream, buildDreamPrompt, parseDream, applyDream, enabled, DREAM_CRON, DREAM_MODEL };
374
+ module.exports = { runDream, initDream, buildDreamPrompt, parseDream, applyDream, enabled, summaryEnabled, DREAM_CRON, DREAM_MODEL };
package/core/handlers.js CHANGED
@@ -125,7 +125,7 @@ register({
125
125
  "Settings: /model /effort /budget /plan /compact /compactwindow /worktree /mode",
126
126
  "Identity: /whoami /link",
127
127
  "Team: /people /intros /auth (owner)",
128
- "Automation: /cron /vault /soul",
128
+ "Automation: /cron /vault /soul /dreamsummary",
129
129
  "Claude auth: /auth_status /login /setup_token /use_oauth_token /clear_oauth_token",
130
130
  "Codex auth: /codex_auth_status /codex_login /codex_setup_token",
131
131
  "System: /doctor /requirements /restart /upgrade",
@@ -947,6 +947,24 @@ register({
947
947
  },
948
948
  });
949
949
 
950
+ register({
951
+ name: "dreamsummary", description: "Toggle the post-dream memory summary in chat", args: "[on|off]",
952
+ handler: async (env, { tail }) => {
953
+ if (!authorized(env)) return;
954
+ const dream = require("./dream");
955
+ const arg = (tail || "").trim().toLowerCase();
956
+ if (arg !== "on" && arg !== "off") {
957
+ const on = dream.summaryEnabled();
958
+ return send(`Dream summary is ${on ? "on" : "off"}.\n\nAfter each nightly memory consolidation I ${on ? "post a short summary here" : "tidy up silently"}.\n\nToggle: /dreamsummary on · /dreamsummary off`);
959
+ }
960
+ saveEnvKey("DREAM_SUMMARY", arg);
961
+ process.env.DREAM_SUMMARY = arg;
962
+ return send(arg === "on"
963
+ ? "Dream summary on — I'll drop a short note here after each nightly memory tidy-up. 💤"
964
+ : "Dream summary off — I'll consolidate memory silently. Backups are still kept, so nothing is lost.");
965
+ },
966
+ });
967
+
950
968
  // ── Learned skills (now backed by context packs) ────────────────────
951
969
  // Skills migrated into context packs (~/.open-claudia/packs). /skills is a
952
970
  // thin window onto that store; any stragglers still in the legacy
@@ -984,6 +1002,10 @@ register({
984
1002
  }
985
1003
  const lines = packs.map((p) => `• ${p.dir} — ${p.description || p.name || "(no description)"}`);
986
1004
  let msg = `Skills / context packs (${packs.length}):\n\n${lines.join("\n")}`;
1005
+ const archived = packsLib.listArchived();
1006
+ if (archived.length) {
1007
+ msg += `\n\n${archived.length} archived (cold packs retired by the nightly dream) — open-claudia pack archived to view, pack restore <dir> to bring one back.`;
1008
+ }
987
1009
  if (legacy.length) {
988
1010
  msg += `\n\n${legacy.length} legacy skill(s) still in ~/.claude/skills — run open-claudia pack migrate to fold them in.`;
989
1011
  }
package/core/packs.js CHANGED
@@ -70,8 +70,10 @@ function serialize(pack) {
70
70
  `created: ${pack.created || new Date().toISOString()}`,
71
71
  `updated: ${pack.updated || new Date().toISOString()}`,
72
72
  `last_used: ${pack.last_used || ""}`,
73
- "---",
73
+ `usage_count: ${pack.usage_count || 0}`,
74
74
  );
75
+ if (pack.archived) fmLines.push(`archived: ${pack.archived}`);
76
+ fmLines.push("---");
75
77
  const sections = SECTIONS
76
78
  .map((s) => `## ${s}\n\n${(pack.sections?.[s] || "").trim()}`)
77
79
  .join("\n\n");
@@ -99,6 +101,8 @@ function readPack(dir) {
99
101
  created: fm.created || "",
100
102
  updated: fm.updated || (stat ? stat.mtime.toISOString() : ""),
101
103
  last_used: fm.last_used || "",
104
+ usage_count: Number(fm.usage_count || 0) || 0,
105
+ archived: fm.archived || "",
102
106
  sections,
103
107
  };
104
108
  }
@@ -193,10 +197,68 @@ function touchUsed(dirs) {
193
197
  const pack = readPack(dir);
194
198
  if (!pack) continue;
195
199
  pack.last_used = now;
200
+ pack.usage_count = (Number(pack.usage_count) || 0) + 1;
196
201
  try { writePack(pack); } catch (e) {}
197
202
  }
198
203
  }
199
204
 
205
+ // ---------------------------------------------------------------------------
206
+ // Lifecycle: archive stale packs to PACKS_DIR/.archived/<dir>. listPacks and
207
+ // the FTS index skip dot-dirs, so an archived pack stops matching but the file
208
+ // is kept and fully restorable. This is the brake on an index that only grows.
209
+
210
+ const ARCHIVE_DIRNAME = ".archived";
211
+ function archiveRoot() { return path.join(PACKS_DIR, ARCHIVE_DIRNAME); }
212
+
213
+ function archivePack(nameOrDir) {
214
+ const pack = findPack(nameOrDir);
215
+ if (!pack) return null;
216
+ ensureDir();
217
+ fs.mkdirSync(archiveRoot(), { recursive: true, mode: 0o700 });
218
+ pack.archived = new Date().toISOString();
219
+ try { fs.writeFileSync(packFile(pack.dir), serialize(pack), { mode: 0o600 }); } catch (e) {}
220
+ const dest = path.join(archiveRoot(), pack.dir);
221
+ fs.rmSync(dest, { recursive: true, force: true });
222
+ fs.renameSync(path.join(PACKS_DIR, pack.dir), dest);
223
+ markIndexDirty();
224
+ return pack;
225
+ }
226
+
227
+ function listArchived() {
228
+ let entries;
229
+ try { entries = fs.readdirSync(archiveRoot()); } catch (e) { return []; }
230
+ const out = [];
231
+ for (const name of entries) {
232
+ if (name.startsWith(".")) continue;
233
+ let content;
234
+ try { content = fs.readFileSync(path.join(archiveRoot(), name, "PACK.md"), "utf-8"); }
235
+ catch (e) { continue; }
236
+ const { fm } = parseFrontmatter(content);
237
+ out.push({
238
+ dir: name,
239
+ name: fm.name || name,
240
+ description: fm.description || "",
241
+ last_used: fm.last_used || "",
242
+ usage_count: Number(fm.usage_count || 0) || 0,
243
+ archived: fm.archived || "",
244
+ });
245
+ }
246
+ return out.sort((a, b) => a.dir.localeCompare(b.dir));
247
+ }
248
+
249
+ function restorePack(nameOrDir) {
250
+ const needle = String(nameOrDir || "").trim().toLowerCase();
251
+ if (!needle) return null;
252
+ const found = listArchived().find((p) => p.dir.toLowerCase() === needle || p.name.toLowerCase() === needle);
253
+ if (!found) return null;
254
+ if (readPack(found.dir)) throw new Error(`an active pack ${found.dir} already exists`);
255
+ fs.renameSync(path.join(archiveRoot(), found.dir), path.join(PACKS_DIR, found.dir));
256
+ const pack = readPack(found.dir);
257
+ if (pack) { pack.archived = ""; pack.updated = new Date().toISOString(); try { writePack(pack); } catch (e) {} }
258
+ markIndexDirty();
259
+ return pack;
260
+ }
261
+
200
262
  // Recognise a Write/Edit aimed at a pack file (for chat announcements).
201
263
  function packNameFromPath(filePath) {
202
264
  if (!filePath) return null;
@@ -333,4 +395,5 @@ module.exports = {
333
395
  PACKS_DIR, SECTIONS, slugify,
334
396
  listPacks, findPack, readPack, writePack, createPack, updatePack, removePack,
335
397
  touchUsed, packNameFromPath, matchPacks, reindex, markIndexDirty,
398
+ archivePack, restorePack, listArchived,
336
399
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "2.6.23",
3
+ "version": "2.6.24",
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": {