@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 +1 -1
- package/bin/pack.js +29 -2
- package/core/dream.js +51 -9
- package/core/handlers.js +23 -1
- package/core/packs.js +64 -1
- package/package.json +1 -1
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|
|
|
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.
|
|
87
|
-
8.
|
|
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.
|
|
309
|
-
//
|
|
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 (
|
|
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