@lmzhen/dsh-evolution-core 0.1.0-rc.4 → 0.1.0-rc.6

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/lib/index.js CHANGED
@@ -1278,6 +1278,117 @@ var SkillLibrary = class {
1278
1278
  path: dest
1279
1279
  };
1280
1280
  }
1281
+ /**
1282
+ * Merge the bodies of `sources` into `target` and archive the sources with
1283
+ * an absorbed-into marker. Hermes-style consolidation: overlapping skills
1284
+ * collapse into one, and the originals stay recoverable under `.archive/`.
1285
+ */
1286
+ async consolidate(target, sources) {
1287
+ const normalizedSources = [...new Set(sources)].filter((name) => name !== target);
1288
+ if (normalizedSources.length === 0) return {
1289
+ ok: false,
1290
+ message: "Consolidation requires at least one distinct source skill."
1291
+ };
1292
+ for (const name of [target, ...normalizedSources]) if (!SKILL_NAME_RE.test(name)) return {
1293
+ ok: false,
1294
+ message: `Invalid skill name "${name}". Use lowercase letters, digits, and hyphens.`
1295
+ };
1296
+ const targetDir = skillDir(this.root, target);
1297
+ const targetMd = await this.io.readText(join(targetDir, "SKILL.md"));
1298
+ if (!targetMd) return {
1299
+ ok: false,
1300
+ message: `Skill "${target}" not found.`
1301
+ };
1302
+ const targetProtection = await this.writeProtection(target);
1303
+ if (targetProtection) return {
1304
+ ok: false,
1305
+ message: `Skill "${target}" is protected (${targetProtection}).`
1306
+ };
1307
+ const parts = [];
1308
+ for (const source of normalizedSources) {
1309
+ const protection = await this.deleteProtection(source);
1310
+ if (protection) return {
1311
+ ok: false,
1312
+ message: `Skill "${source}" is protected (${protection}).`
1313
+ };
1314
+ const sourceMd = await this.io.readText(join(skillDir(this.root, source), "SKILL.md"));
1315
+ if (!sourceMd) return {
1316
+ ok: false,
1317
+ message: `Skill "${source}" not found.`
1318
+ };
1319
+ const parsed = parseFrontmatter(sourceMd);
1320
+ if (!parsed) return {
1321
+ ok: false,
1322
+ message: `Skill "${source}" has no valid frontmatter; refusing to merge.`
1323
+ };
1324
+ parts.push(`\n<!-- consolidated from ${source} at ${(/* @__PURE__ */ new Date()).toISOString()} -->\n${parsed.body.trim()}`);
1325
+ }
1326
+ const merged = targetMd.trimEnd() + parts.join("\n") + "\n";
1327
+ const validation = validateFrontmatter(merged, target, this.limits);
1328
+ if (validation) return {
1329
+ ok: false,
1330
+ message: `Consolidation rejected: ${validation}`
1331
+ };
1332
+ const threat = scanContentThreats(merged);
1333
+ if (threat) return {
1334
+ ok: false,
1335
+ message: threat
1336
+ };
1337
+ await this.io.writeText(join(targetDir, "SKILL.md"), merged);
1338
+ for (const source of normalizedSources) {
1339
+ const archived = await this.archive(source, target);
1340
+ if (!archived.ok) return archived;
1341
+ }
1342
+ return {
1343
+ ok: true,
1344
+ message: `Consolidated ${normalizedSources.join(", ")} into "${target}".`,
1345
+ path: targetDir
1346
+ };
1347
+ }
1348
+ /**
1349
+ * Restore one skill from `.archive/` back to the active root. Hermes-style
1350
+ * recoverability: archival never deletes, and this is the control-plane
1351
+ * path back. The `.archive-reason` marker is dropped on restore.
1352
+ */
1353
+ async restoreFromArchive(name) {
1354
+ if (!SKILL_NAME_RE.test(name)) return {
1355
+ ok: false,
1356
+ message: `Invalid skill name "${name}". Use lowercase letters, digits, and hyphens.`
1357
+ };
1358
+ if (await this.io.exists(join(skillDir(this.root, name), "SKILL.md"))) return {
1359
+ ok: false,
1360
+ message: `Skill "${name}" already exists in the active root; refusing to overwrite.`
1361
+ };
1362
+ const archiveRoot = join(this.root, ".archive");
1363
+ let entries;
1364
+ try {
1365
+ entries = await this.io.list(archiveRoot);
1366
+ } catch {
1367
+ return {
1368
+ ok: false,
1369
+ message: "No skill archive available."
1370
+ };
1371
+ }
1372
+ const chosen = entries.filter((entry) => entry === name || entry.startsWith(`${name}-`)).sort().reverse()[0];
1373
+ if (!chosen) return {
1374
+ ok: false,
1375
+ message: `Skill "${name}" is not in .archive.`
1376
+ };
1377
+ const source = join(archiveRoot, chosen);
1378
+ const dest = skillDir(this.root, name);
1379
+ try {
1380
+ await this.io.rename(source, dest);
1381
+ } catch {
1382
+ await this.io.copy(source, dest);
1383
+ await this.io.remove(source);
1384
+ }
1385
+ if (await this.io.exists(join(dest, ".archive-reason"))) await this.io.remove(join(dest, ".archive-reason"));
1386
+ return {
1387
+ ok: true,
1388
+ message: `Skill "${name}" restored from .archive.`,
1389
+ path: dest
1390
+ };
1391
+ }
1281
1392
  async writeSupportFile(name, filePath, content) {
1282
1393
  const dir = skillDir(this.root, name);
1283
1394
  if (!await this.io.exists(join(dir, "SKILL.md"))) return {
@@ -58,6 +58,18 @@ export declare class SkillLibrary {
58
58
  update(name: string, content: string): Promise<SkillActionResult>;
59
59
  patch(name: string, oldString: string, newString: string, filePath?: string, replaceAll?: boolean): Promise<SkillActionResult>;
60
60
  archive(name: string, absorbedInto?: string): Promise<SkillActionResult>;
61
+ /**
62
+ * Merge the bodies of `sources` into `target` and archive the sources with
63
+ * an absorbed-into marker. Hermes-style consolidation: overlapping skills
64
+ * collapse into one, and the originals stay recoverable under `.archive/`.
65
+ */
66
+ consolidate(target: string, sources: string[]): Promise<SkillActionResult>;
67
+ /**
68
+ * Restore one skill from `.archive/` back to the active root. Hermes-style
69
+ * recoverability: archival never deletes, and this is the control-plane
70
+ * path back. The `.archive-reason` marker is dropped on restore.
71
+ */
72
+ restoreFromArchive(name: string): Promise<SkillActionResult>;
61
73
  writeSupportFile(name: string, filePath: string, content: string): Promise<SkillActionResult>;
62
74
  removeSupportFile(name: string, filePath: string): Promise<SkillActionResult>;
63
75
  snapshotAll(reason?: string): Promise<string>;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lmzhen/dsh-evolution-core",
3
3
  "description": "Shared stores, prompts, signals and lifecycle logic for the dsh-evolution plugin family (community build)",
4
- "version": "0.1.0-rc.4",
4
+ "version": "0.1.0-rc.6",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },