@inetafrica/open-claudia 2.6.24 → 2.6.25

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/pack.js CHANGED
@@ -56,6 +56,9 @@ function run(args) {
56
56
  const p = packs.findPack(rest[0]);
57
57
  if (!p) { console.error(`No pack: ${rest[0]}`); process.exitCode = 1; return; }
58
58
  console.log(fs.readFileSync(path.join(packs.PACKS_DIR, p.dir, "PACK.md"), "utf-8"));
59
+ const prov = packs.readProvenance(p.dir);
60
+ const authored = packs.SECTIONS.map((s) => `${s}=${prov.sections[s] || "user"}`).join(" ");
61
+ console.log(`\n# provenance: ${authored}${prov.lastWriter ? ` (last: ${prov.lastWriter} ${prov.ts})` : ""}`);
59
62
  break;
60
63
  }
61
64
 
package/core/dream.js CHANGED
@@ -62,7 +62,7 @@ function buildDreamPrompt() {
62
62
  `description: ${p.description}`,
63
63
  `tags: ${p.tags.join(", ")}`,
64
64
  `last_used: ${p.last_used || "never"} (used ${p.usage_count || 0}×)`,
65
- `Stance: ${p.sections.Stance}`,
65
+ `Stance${packs.provenanceOf(p.dir, "Stance") === "user" ? " (USER-AUTHORED — do not rewrite)" : ""}: ${p.sections.Stance}`,
66
66
  `Procedure: ${p.sections.Procedure}`,
67
67
  `State: ${p.sections.State}`,
68
68
  `Journal:\n${p.sections.Journal}`,
@@ -93,7 +93,7 @@ ${persona.loadPersona()}
93
93
 
94
94
  Your job — decide what consolidation, if any, is warranted:
95
95
 
96
- 1. merges: packs that are clearly the SAME topic under different names get merged into one. Supply the merged Stance/Procedure/State (synthesised, not concatenated; null leaves the target's section alone) and a one-sentence journal note. Be conservative: when in doubt, do not merge.
96
+ 1. merges: packs that are clearly the SAME topic under different names get merged into one. Supply the merged Stance/Procedure/State (synthesised, not concatenated; null leaves the target's section alone) and a one-sentence journal note. Be conservative: when in doubt, do not merge. A Stance marked USER-AUTHORED is off-limits — leave its stance null; the system preserves it verbatim regardless.
97
97
  2. umbrellas: when 3+ packs are siblings under one theme, create an umbrella pack whose State is a 3-6 line map of the family ("for X see pack Y"), and list its children. The umbrella is a router, not a duplicate.
98
98
  3. parents: assign an existing pack as parent of another (sub-topic relationship) without creating anything.
99
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.
@@ -180,18 +180,19 @@ function applyDream(decision, backupRoot) {
180
180
  const into = m?.into && packs.readPack(m.into);
181
181
  const from = [].concat(m?.from || []).filter((d) => d && d !== m.into && !gone.has(d) && packs.readPack(d));
182
182
  if (!into || from.length === 0) continue;
183
- packs.updatePack(m.into, {
183
+ const mr = packs.updatePack(m.into, {
184
184
  stance: typeof m.stance === "string" ? m.stance : "",
185
185
  procedure: typeof m.procedure === "string" ? m.procedure : "",
186
186
  state: typeof m.state === "string" ? m.state : "",
187
187
  journal: m.journal || `absorbed ${from.join(", ")}`,
188
- });
188
+ }, "dream");
189
189
  for (const d of from) {
190
190
  backupPack(d, backupRoot);
191
191
  packs.removePack(d);
192
192
  gone.add(d);
193
193
  }
194
194
  lines.push(`📦 Merged ${from.join(" + ")} into ${m.into}`);
195
+ if (mr.protectedStance) lines.push(`🛡️ Kept ${m.into}'s user-authored Stance as-is`);
195
196
  } catch (e) { console.warn(`[dream] merge failed: ${e.message}`); }
196
197
  }
197
198
 
@@ -209,7 +210,7 @@ function applyDream(decision, backupRoot) {
209
210
  tags: Array.isArray(u.tags) ? u.tags.slice(0, 6) : [],
210
211
  state: u.state || "",
211
212
  journal: `umbrella created over ${children.join(", ")}`,
212
- });
213
+ }, "dream");
213
214
  }
214
215
  for (const c of children) {
215
216
  const child = packs.readPack(c);
@@ -241,7 +242,7 @@ function applyDream(decision, backupRoot) {
241
242
  const desc = typeof r.description === "string" ? r.description.trim() : "";
242
243
  const tags = Array.isArray(r.tags) ? r.tags.map((t) => String(t).trim()).filter(Boolean).slice(0, 6) : [];
243
244
  if (!desc && tags.length === 0) continue;
244
- packs.updatePack(r.pack, { description: desc, tags });
245
+ packs.updatePack(r.pack, { description: desc, tags }, "dream");
245
246
  lines.push(`🏷️ Sharpened ${r.pack}'s description/tags`);
246
247
  } catch (e) { console.warn(`[dream] retag failed: ${e.message}`); }
247
248
  }
@@ -129,18 +129,18 @@ function applyAction(a) {
129
129
  if (a.action === "update" && a.pack) {
130
130
  const existing = packs.readPack(a.pack);
131
131
  if (!existing) return null;
132
- packs.updatePack(a.pack, {
132
+ const r = packs.updatePack(a.pack, {
133
133
  journal: a.journal || "",
134
134
  state: typeof a.state === "string" ? a.state : "",
135
135
  stance: typeof a.stance === "string" ? a.stance : "",
136
136
  procedure: typeof a.procedure === "string" ? a.procedure : "",
137
- });
138
- return { kind: "update", dir: a.pack, name: existing.name, note: a.journal || "state updated" };
137
+ }, "reviewer");
138
+ return { kind: "update", dir: a.pack, name: existing.name, note: a.journal || "state updated", protectedStance: !!r.protectedStance };
139
139
  }
140
140
  if (a.action === "create" && (a.dir || a.name)) {
141
141
  const dir = packs.slugify(a.dir || a.name);
142
142
  if (packs.readPack(dir)) {
143
- packs.updatePack(dir, { journal: a.journal || "", state: a.state || "" });
143
+ packs.updatePack(dir, { journal: a.journal || "", state: a.state || "" }, "reviewer");
144
144
  return { kind: "update", dir, name: a.name || dir, note: a.journal || "state updated" };
145
145
  }
146
146
  const pack = packs.createPack({
@@ -152,7 +152,7 @@ function applyAction(a) {
152
152
  procedure: a.procedure || "",
153
153
  state: a.state || "",
154
154
  journal: a.journal || "",
155
- });
155
+ }, "reviewer");
156
156
  return { kind: "create", dir: pack.dir, name: pack.name, note: a.description || "" };
157
157
  }
158
158
  return null;
package/core/packs.js CHANGED
@@ -137,7 +137,7 @@ function writePack(pack) {
137
137
  return pack.dir;
138
138
  }
139
139
 
140
- function createPack({ dir, name, description, tags, stance, procedure, state, journal, parent }) {
140
+ function createPack({ dir, name, description, tags, stance, procedure, state, journal, parent }, origin = "user") {
141
141
  const d = slugify(dir || name);
142
142
  if (!d) throw new Error("pack needs a name");
143
143
  if (readPack(d)) throw new Error(`pack ${d} already exists`);
@@ -156,6 +156,7 @@ function createPack({ dir, name, description, tags, stance, procedure, state, jo
156
156
  },
157
157
  };
158
158
  writePack(pack);
159
+ setProvenance(d, SECTIONS.filter((s) => (pack.sections[s] || "").trim()), origin);
159
160
  return pack;
160
161
  }
161
162
 
@@ -163,23 +164,98 @@ function today() {
163
164
  return new Date().toISOString().slice(0, 10);
164
165
  }
165
166
 
167
+ // ---------------------------------------------------------------------------
168
+ // Provenance: a sidecar .provenance.json per pack records which origin
169
+ // (user | reviewer | dream) last authored each section. Sections we never
170
+ // explicitly stamped default to "user" — anything not proven to be automated
171
+ // is protected as if you wrote it. This is what lets the reviewer and dream
172
+ // rewrite their own output freely while never silently clobbering your words.
173
+ function provFile(dir) {
174
+ return path.join(PACKS_DIR, dir, ".provenance.json");
175
+ }
176
+
177
+ function readProvenance(dir) {
178
+ let data = {};
179
+ try { data = JSON.parse(fs.readFileSync(provFile(dir), "utf-8")) || {}; } catch (e) {}
180
+ const sections = data.sections && typeof data.sections === "object" ? data.sections : {};
181
+ return { sections, lastWriter: data.lastWriter || "", ts: data.ts || "" };
182
+ }
183
+
184
+ function provenanceOf(dir, section) {
185
+ return readProvenance(dir).sections[section] || "user";
186
+ }
187
+
188
+ function setProvenance(dir, sectionNames, origin) {
189
+ if (!origin) return;
190
+ const names = [].concat(sectionNames || []).filter(Boolean);
191
+ if (!names.length) return;
192
+ const cur = readProvenance(dir);
193
+ for (const n of names) cur.sections[n] = origin;
194
+ cur.lastWriter = origin;
195
+ cur.ts = new Date().toISOString();
196
+ try {
197
+ fs.mkdirSync(path.join(PACKS_DIR, dir), { recursive: true, mode: 0o700 });
198
+ fs.writeFileSync(provFile(dir), JSON.stringify(cur, null, 2), { mode: 0o600 });
199
+ } catch (e) {}
200
+ }
201
+
202
+ // A foreground Write/Edit to a PACK.md is you (or me acting on what you told
203
+ // me), so stamp the touched section(s) "user" — this also resets a section the
204
+ // reviewer/dream previously claimed back to user-authored once you edit it.
205
+ function recordForegroundWrite(dir, { tool, oldString, content } = {}) {
206
+ try {
207
+ if (tool === "Write" && typeof content === "string") {
208
+ const { body } = parseFrontmatter(content);
209
+ const { sections } = parseSections(body);
210
+ const applied = SECTIONS.filter((s) => sections[s] && sections[s].trim());
211
+ setProvenance(dir, applied.length ? applied : SECTIONS, "user");
212
+ return;
213
+ }
214
+ if (tool === "Edit" && typeof oldString === "string" && oldString) {
215
+ const cur = fs.readFileSync(packFile(dir), "utf-8");
216
+ const idx = cur.indexOf(oldString);
217
+ if (idx < 0) { setProvenance(dir, SECTIONS, "user"); return; }
218
+ let found = null;
219
+ for (const s of SECTIONS) {
220
+ const h = cur.indexOf(`## ${s}`);
221
+ if (h >= 0 && h <= idx && (found === null || h > found.pos)) found = { sec: s, pos: h };
222
+ }
223
+ setProvenance(dir, found ? [found.sec] : SECTIONS, "user");
224
+ }
225
+ } catch (e) {}
226
+ }
227
+
166
228
  // Apply a reviewer mutation. Only supplied fields change; journal entries
167
229
  // append (capped); state replaces (it represents "current truth").
168
- function updatePack(dir, { description, tags, stance, procedure, state, journal } = {}) {
230
+ function updatePack(dir, { description, tags, stance, procedure, state, journal } = {}, origin = "user") {
169
231
  const pack = readPack(dir);
170
232
  if (!pack) throw new Error(`no pack: ${dir}`);
171
233
  pack.updated = new Date().toISOString();
172
234
  if (description) pack.description = description;
173
235
  if (Array.isArray(tags) && tags.length) pack.tags = tags;
174
- if (typeof stance === "string" && stance.trim()) pack.sections.Stance = stance.trim();
175
- if (typeof procedure === "string" && procedure.trim()) pack.sections.Procedure = procedure.trim();
176
- if (typeof state === "string" && state.trim()) pack.sections.State = state.trim();
236
+ const applied = [];
237
+ let protectedStance = false;
238
+ if (typeof stance === "string" && stance.trim()) {
239
+ // Stance shapes behaviour, so a user-authored Stance is never silently
240
+ // overwritten by an automated writer — your intent wins.
241
+ if (origin !== "user" && provenanceOf(dir, "Stance") === "user") {
242
+ protectedStance = true;
243
+ } else {
244
+ pack.sections.Stance = stance.trim();
245
+ applied.push("Stance");
246
+ }
247
+ }
248
+ if (typeof procedure === "string" && procedure.trim()) { pack.sections.Procedure = procedure.trim(); applied.push("Procedure"); }
249
+ if (typeof state === "string" && state.trim()) { pack.sections.State = state.trim(); applied.push("State"); }
177
250
  if (typeof journal === "string" && journal.trim()) {
178
251
  const entries = pack.sections.Journal.split("\n").filter((l) => l.trim());
179
252
  entries.push(`- [${today()}] ${journal.trim().replace(/\n+/g, " ")}`);
180
253
  pack.sections.Journal = entries.slice(-MAX_JOURNAL_ENTRIES).join("\n");
254
+ applied.push("Journal");
181
255
  }
182
256
  writePack(pack);
257
+ if (applied.length) setProvenance(dir, applied, origin);
258
+ pack.protectedStance = protectedStance;
183
259
  return pack;
184
260
  }
185
261
 
@@ -396,4 +472,5 @@ module.exports = {
396
472
  listPacks, findPack, readPack, writePack, createPack, updatePack, removePack,
397
473
  touchUsed, packNameFromPath, matchPacks, reindex, markIndexDirty,
398
474
  archivePack, restorePack, listArchived,
475
+ readProvenance, provenanceOf, setProvenance, recordForegroundWrite,
399
476
  };
package/core/runner.js CHANGED
@@ -806,6 +806,13 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
806
806
  if (packDir) {
807
807
  if (packsLib.readPack(packDir)) notifySkill(`pack:${packDir}`, `✏️ Updating my notes on ${packDir}…`);
808
808
  else notifySkill(`pack:${packDir}`, `📦 Starting a new pack: ${packDir} — open-claudia pack show ${packDir} to peek.`);
809
+ try {
810
+ packsLib.recordForegroundWrite(packDir, {
811
+ tool: toolName,
812
+ oldString: input?.old_string ?? input?.oldString,
813
+ content: input?.content,
814
+ });
815
+ } catch (e) {}
809
816
  return;
810
817
  }
811
818
  const entSlug = entitiesLib.entityNameFromPath(filePath);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "2.6.24",
3
+ "version": "2.6.25",
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": {