@hraness/message-like-me 0.8.5 → 0.8.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/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.6 (2026-09-07)
4
+
5
+ - Make the exact public `@hraness/message-like-me@0.8.6` npm package the
6
+ supported default install, with the same reviewed bytes mirrored in the
7
+ immutable GitHub Release.
8
+ - Give the paired Message Like Me and Ensoul skill installer one command-owned
9
+ lifecycle through copying, backup, publication and sequential recovery.
10
+ - Bound physical inventories and verify copied bytes before publication. Retain
11
+ unknown or substituted entries during cleanup, and preserve missing project
12
+ directory creation beneath a verified physical ancestor.
13
+ - Keep both installed skills after publication succeeds, even when backup cleanup
14
+ or output confirmation fails. Report incomplete cleanup with fixed diagnostics
15
+ that omit private data while preserving the original failure.
16
+ - Strengthen the development Effect checker with generator alias and wrapper
17
+ regressions. Public protocol graphs and local-only behavior remain unchanged.
18
+
3
19
  ## 0.8.5 (2026-09-06)
4
20
 
5
21
  - Make the exact public `@hraness/message-like-me@0.8.5` npm package the
package/README.md CHANGED
@@ -43,7 +43,7 @@ Message Like Me requires Bun 1.3.14 or newer. Install the exact public npm
43
43
  package, then install both bundled Agent Skills:
44
44
 
45
45
  ```sh
46
- bun add --global @hraness/message-like-me@0.8.5
46
+ bun add --global @hraness/message-like-me@0.8.6
47
47
  messagelikeme skill install
48
48
  ```
49
49
 
package/dist/cli.js CHANGED
@@ -19637,117 +19637,527 @@ async function readStablePrivateJson(path, label, maximumBytes) {
19637
19637
  }
19638
19638
 
19639
19639
  // src/skill-install.ts
19640
- import { randomBytes as randomBytes2 } from "crypto";
19641
- import { cp, lstat as lstat4, mkdir as mkdir2, realpath as realpath3, rename, rm } from "fs/promises";
19642
- import { homedir as homedir4 } from "os";
19643
- import { dirname as dirname3, join as join7, resolve as resolve5 } from "path";
19640
+ import { dirname as dirname3, resolve as resolve5 } from "path";
19644
19641
  import { fileURLToPath } from "url";
19645
- async function exists5(path) {
19646
- try {
19647
- await lstat4(path);
19648
- return true;
19649
- } catch (error) {
19650
- if (error.code === "ENOENT")
19651
- return false;
19652
- throw error;
19653
- }
19654
- }
19655
19642
  function bundledSkillPath() {
19656
19643
  return resolve5(dirname3(fileURLToPath(import.meta.url)), "../skills/message-like-me");
19657
19644
  }
19658
19645
  function bundledEnsoulSkillPath() {
19659
19646
  return resolve5(dirname3(fileURLToPath(import.meta.url)), "../skills/ensoul");
19660
19647
  }
19661
- function targetRoot(target, scope5, projectDirectory) {
19662
- const directory = target === "codex" ? ".codex" : target === "claude" ? ".claude" : ".agents";
19663
- return scope5 === "user" ? join7(homedir4(), directory, "skills") : join7(resolve5(projectDirectory), directory, "skills");
19648
+
19649
+ // src/skill-install-model.ts
19650
+ var SKILL_INSTALL_PAIR = ["message-like-me", "ensoul"];
19651
+ function skillInstallWarning(outcome) {
19652
+ if (outcome.residuals.length === 0 && !(outcome.committed && outcome.operation._tag === "Failure"))
19653
+ return null;
19654
+ const state = outcome.committed ? "committed" : "not committed";
19655
+ const details = outcome.residuals.map(({ skill, phase }) => `${skill}:${phase}`).join(", ");
19656
+ return `Skill installation ${state}; ${details.length > 0 ? `cleanup incomplete (${details})` : "output confirmation failed"}. Retained installation files need review.
19657
+ `;
19664
19658
  }
19665
- async function installSkill(options) {
19666
- const sources = Object.freeze([
19667
- Object.freeze({ name: "message-like-me", path: bundledSkillPath() }),
19668
- Object.freeze({ name: "ensoul", path: bundledEnsoulSkillPath() })
19669
- ]);
19670
- for (const source of sources) {
19671
- if (!await exists5(source.path)) {
19672
- throw new CliError("not-found", `Bundled ${source.name} skill is missing at ${source.path}`);
19659
+
19660
+ // src/skill-install-program.ts
19661
+ class SkillInstallPlatform extends exports_Context.Tag("@hraness/message-like-me/SkillInstallPlatform")() {
19662
+ }
19663
+ function installSkillProgram(options) {
19664
+ return exports_Effect.uninterruptible(exports_Effect.gen(function* () {
19665
+ const platform2 = yield* SkillInstallPlatform;
19666
+ const prepared = yield* exports_Effect.exit(platform2.preflight(options).pipe(exports_Effect.flatMap((plan) => platform2.transaction(plan))));
19667
+ if (exports_Exit.isFailure(prepared))
19668
+ return { operation: exports_Exit.failCause(prepared.cause), committed: false, residuals: [] };
19669
+ const transaction = prepared.value;
19670
+ let committed = false;
19671
+ const operation = yield* exports_Effect.exit(exports_Effect.gen(function* () {
19672
+ yield* platform2.acquire(transaction);
19673
+ for (const skill of SKILL_INSTALL_PAIR)
19674
+ yield* platform2.copy(transaction, skill);
19675
+ for (const skill of SKILL_INSTALL_PAIR)
19676
+ yield* platform2.backup(transaction, skill);
19677
+ for (const skill of SKILL_INSTALL_PAIR)
19678
+ yield* platform2.publish(transaction, skill);
19679
+ committed = true;
19680
+ }));
19681
+ const residuals = [];
19682
+ const cleanup = (skill, phase) => exports_Effect.gen(function* () {
19683
+ const result = yield* exports_Effect.exit(platform2.cleanup(transaction, skill, phase));
19684
+ if (exports_Exit.isFailure(result))
19685
+ residuals.push({ skill, phase, cause: result.cause });
19686
+ });
19687
+ if (!committed) {
19688
+ for (const skill of [...SKILL_INSTALL_PAIR].reverse()) {
19689
+ yield* cleanup(skill, "rollback-published");
19690
+ yield* cleanup(skill, "restore-backup");
19691
+ yield* cleanup(skill, "stage-cleanup");
19692
+ }
19693
+ } else {
19694
+ for (const skill of SKILL_INSTALL_PAIR)
19695
+ yield* cleanup(skill, "backup-cleanup");
19673
19696
  }
19674
- const sourceMetadata = await lstat4(source.path);
19675
- if (sourceMetadata.isSymbolicLink() || !sourceMetadata.isDirectory()) {
19676
- throw new CliError("unsafe-path", `Bundled ${source.name} skill must be a physical directory`);
19697
+ const output = exports_Exit.isFailure(operation) ? exports_Exit.failCause(operation.cause) : yield* exports_Effect.exit(platform2.destinations(transaction));
19698
+ yield* cleanup("pair", "transaction-cleanup");
19699
+ yield* cleanup("pair", "lock-cleanup");
19700
+ return { operation: output, committed, residuals };
19701
+ }));
19702
+ }
19703
+
19704
+ // src/skill-install-platform.ts
19705
+ import { createHash as createHash5, randomBytes as randomBytes2 } from "crypto";
19706
+ import { constants as constants2 } from "fs";
19707
+ import * as fs from "fs/promises";
19708
+ import { homedir as homedir4 } from "os";
19709
+ import { basename as basename4, dirname as dirname4, join as join7, relative, resolve as resolve6, sep } from "path";
19710
+ var MAX_ENTRIES = 64;
19711
+ var MAX_FILE_BYTES = 256 * 1024;
19712
+ var MAX_TOTAL_BYTES = 2 * 1024 * 1024;
19713
+ var MAX_DEPTH = 8;
19714
+ var plans = new WeakMap;
19715
+ var transactions = new WeakMap;
19716
+ var unsafe = () => new CliError("unsafe-path", "Skill installation path changed or cannot be used safely");
19717
+ var bounded = () => new CliError("invalid-data", "Skill directory exceeds the supported physical inventory bounds");
19718
+ function isMissing(error) {
19719
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
19720
+ }
19721
+ async function optionalStat(path) {
19722
+ try {
19723
+ return await fs.lstat(path, { bigint: true });
19724
+ } catch (error) {
19725
+ if (isMissing(error))
19726
+ return null;
19727
+ throw error;
19728
+ }
19729
+ }
19730
+ function physical(stat2) {
19731
+ if (typeof process.getuid !== "function" || !stat2.isDirectory() && !stat2.isFile() || stat2.uid !== BigInt(process.getuid()))
19732
+ throw unsafe();
19733
+ }
19734
+ function same(left3, right3) {
19735
+ return left3.dev === right3.dev && left3.ino === right3.ino && left3.mode === right3.mode && left3.uid === right3.uid && left3.isDirectory() === right3.isDirectory() && (left3.isDirectory() || left3.size === right3.size && left3.nlink === right3.nlink && left3.mtimeNs === right3.mtimeNs && left3.ctimeNs === right3.ctimeNs);
19736
+ }
19737
+ async function assertIdentity(path, expected) {
19738
+ const actual = await optionalStat(path);
19739
+ if (actual === null || !same(actual, expected))
19740
+ throw unsafe();
19741
+ }
19742
+ function contains5(parent, child) {
19743
+ const path = relative(parent, child);
19744
+ return path === "" || path !== ".." && !path.startsWith(`..${sep}`) && !path.startsWith(sep);
19745
+ }
19746
+
19747
+ class ReadCloseFailure {
19748
+ primary;
19749
+ cleanup;
19750
+ constructor(primary, cleanup) {
19751
+ this.primary = primary;
19752
+ this.cleanup = cleanup;
19753
+ }
19754
+ }
19755
+ function nativeFailure(error) {
19756
+ const cleanup = [];
19757
+ while (error instanceof ReadCloseFailure) {
19758
+ cleanup.push(error.cleanup);
19759
+ error = error.primary;
19760
+ }
19761
+ return cleanup.length === 0 ? commandFailure(error) : { ...commandFailure(error), nativeCleanupCauses: Object.freeze(cleanup.reverse()) };
19762
+ }
19763
+ async function digestFile(path, expected) {
19764
+ if (expected.size > BigInt(MAX_FILE_BYTES) || expected.size < 0n)
19765
+ throw bounded();
19766
+ const handle = await fs.open(path, constants2.O_RDONLY | constants2.O_NOFOLLOW);
19767
+ let result;
19768
+ try {
19769
+ if (!same(await handle.stat({ bigint: true }), expected))
19770
+ throw unsafe();
19771
+ const hash2 = createHash5("sha256");
19772
+ const buffer = Buffer.alloc(16 * 1024);
19773
+ let count = 0;
19774
+ for (;; ) {
19775
+ const { bytesRead } = await handle.read(buffer, 0, Math.min(buffer.length, Number(expected.size) + 1 - count), count);
19776
+ count += bytesRead;
19777
+ if (count > Number(expected.size))
19778
+ throw unsafe();
19779
+ if (bytesRead === 0)
19780
+ break;
19781
+ hash2.update(buffer.subarray(0, bytesRead));
19677
19782
  }
19783
+ if (count !== Number(expected.size) || !same(await handle.stat({ bigint: true }), expected))
19784
+ throw unsafe();
19785
+ result = { ok: true, value: hash2.digest("hex") };
19786
+ } catch (error) {
19787
+ result = { ok: false, error };
19678
19788
  }
19679
- const root = targetRoot(options.target, options.scope, options.projectDirectory ?? process.cwd());
19680
- await mkdir2(root, { recursive: true, mode: 448 });
19681
- const destinations = sources.map((source) => Object.freeze({
19682
- ...source,
19683
- destination: join7(root, source.name)
19684
- }));
19685
- for (const item of destinations) {
19686
- if (await exists5(item.destination)) {
19687
- const metadata = await lstat4(item.destination);
19688
- if (metadata.isSymbolicLink() || !metadata.isDirectory()) {
19689
- throw new CliError("unsafe-path", `Refusing to replace non-directory ${item.destination}`);
19789
+ try {
19790
+ await handle.close();
19791
+ } catch (error) {
19792
+ if (!result.ok)
19793
+ throw new ReadCloseFailure(result.error, error);
19794
+ throw error;
19795
+ }
19796
+ if (!result.ok)
19797
+ throw result.error;
19798
+ return result.value;
19799
+ }
19800
+ async function inventory(root) {
19801
+ const entries2 = [];
19802
+ let bytes = 0n;
19803
+ const visit = async (name, depth) => {
19804
+ if (depth > MAX_DEPTH || name.length > 4096 || entries2.length >= MAX_ENTRIES)
19805
+ throw bounded();
19806
+ const path = join7(root, name);
19807
+ const stat2 = await fs.lstat(path, { bigint: true });
19808
+ physical(stat2);
19809
+ if (stat2.isFile()) {
19810
+ bytes += stat2.size;
19811
+ if (bytes > BigInt(MAX_TOTAL_BYTES))
19812
+ throw bounded();
19813
+ entries2.push({ name, stat: stat2, digest: await digestFile(path, stat2) });
19814
+ } else {
19815
+ entries2.push({ name, stat: stat2 });
19816
+ const directory = await fs.opendir(path);
19817
+ let read = { ok: true };
19818
+ try {
19819
+ for (;; ) {
19820
+ const child = await directory.read();
19821
+ if (child === null)
19822
+ break;
19823
+ await visit(join7(name, child.name), depth + 1);
19824
+ }
19825
+ } catch (error) {
19826
+ read = { ok: false, error };
19690
19827
  }
19691
- if (!options.force) {
19692
- throw new CliError("conflict", `Skill already exists at ${item.destination}; pass --force to replace both bundled skills`);
19828
+ try {
19829
+ await directory.close();
19830
+ } catch (error) {
19831
+ if (!read.ok)
19832
+ throw new ReadCloseFailure(read.error, error);
19833
+ throw error;
19693
19834
  }
19835
+ if (!read.ok)
19836
+ throw read.error;
19837
+ await assertIdentity(path, stat2);
19694
19838
  }
19695
- }
19696
- const nonce = `${process.pid}.${randomBytes2(8).toString("hex")}`;
19697
- const state = destinations.map((item) => ({
19698
- ...item,
19699
- stage: join7(root, `.${item.name}.install.${nonce}`),
19700
- backup: join7(root, `.${item.name}.backup.${nonce}`),
19701
- hadExisting: false,
19702
- published: false
19703
- }));
19704
- try {
19705
- for (const item of state) {
19706
- await cp(item.path, item.stage, { recursive: true, errorOnExist: true });
19839
+ };
19840
+ await visit("", 0);
19841
+ if (!entries2[0]?.stat.isDirectory())
19842
+ throw unsafe();
19843
+ return entries2;
19844
+ }
19845
+ function sameContents(left3, right3) {
19846
+ const byName = new Map(right3.map((entry) => [entry.name, entry]));
19847
+ return left3.length === right3.length && left3.every((entry) => {
19848
+ const other = byName.get(entry.name);
19849
+ return other !== undefined && entry.stat.isDirectory() === other.stat.isDirectory() && entry.stat.size === (entry.stat.isFile() ? other.stat.size : entry.stat.size) && entry.digest === other.digest;
19850
+ });
19851
+ }
19852
+ async function assertInventory(root, expected) {
19853
+ const actual = await inventory(root);
19854
+ if (!sameContents(expected, actual))
19855
+ throw unsafe();
19856
+ const byName = new Map(actual.map((entry) => [entry.name, entry.stat]));
19857
+ if (!expected.every((entry) => {
19858
+ const value = byName.get(entry.name);
19859
+ return value !== undefined && same(entry.stat, value);
19860
+ }))
19861
+ throw unsafe();
19862
+ }
19863
+ async function parents(plan) {
19864
+ for (const parent of plan.parents)
19865
+ await assertIdentity(parent.path, parent.stat);
19866
+ }
19867
+ function state(token3) {
19868
+ const value = transactions.get(token3);
19869
+ if (value === undefined)
19870
+ throw unsafe();
19871
+ return value;
19872
+ }
19873
+ async function custody(transaction) {
19874
+ await parents(transaction.plan);
19875
+ if (transaction.lockStat !== undefined)
19876
+ await assertIdentity(transaction.lock, transaction.lockStat);
19877
+ if (transaction.directoryStat !== undefined)
19878
+ await assertIdentity(transaction.directory, transaction.directoryStat);
19879
+ }
19880
+ async function absent(path) {
19881
+ if (await optionalStat(path) !== null)
19882
+ throw unsafe();
19883
+ }
19884
+ async function projectAnchor(requested) {
19885
+ let cursor = resolve6(requested);
19886
+ if (Buffer.byteLength(cursor, "utf8") > 4096)
19887
+ throw unsafe();
19888
+ const suffix = [];
19889
+ for (;; ) {
19890
+ let existing;
19891
+ try {
19892
+ existing = await fs.realpath(cursor);
19893
+ } catch (error) {
19894
+ if (!isMissing(error))
19895
+ throw error;
19896
+ if (await optionalStat(cursor) !== null || suffix.length >= 64)
19897
+ throw unsafe();
19898
+ const parent = dirname4(cursor);
19899
+ if (parent === cursor)
19900
+ throw error;
19901
+ suffix.push(basename4(cursor));
19902
+ cursor = parent;
19903
+ continue;
19707
19904
  }
19708
- for (const item of state) {
19709
- if (await exists5(item.destination)) {
19710
- await rename(item.destination, item.backup);
19711
- item.hadExisting = true;
19905
+ const stat2 = await fs.lstat(existing, { bigint: true });
19906
+ physical(stat2);
19907
+ if (!stat2.isDirectory())
19908
+ throw unsafe();
19909
+ const missing = [];
19910
+ let anchor = existing;
19911
+ for (const name of suffix.reverse()) {
19912
+ anchor = join7(anchor, name);
19913
+ missing.push(anchor);
19914
+ }
19915
+ return { anchor, parents: [{ path: existing, stat: stat2 }], missing };
19916
+ }
19917
+ }
19918
+ async function preflight(options) {
19919
+ const { anchor, parents: observed, missing } = await projectAnchor(options.scope === "user" ? homedir4() : options.projectDirectory ?? process.cwd());
19920
+ const folder = options.target === "agents" ? ".agents" : options.target === "codex" ? ".codex" : ".claude";
19921
+ const root = join7(anchor, folder, "skills");
19922
+ for (const path of [join7(anchor, folder), root]) {
19923
+ const stat2 = await optionalStat(path);
19924
+ if (stat2 === null)
19925
+ missing.push(path);
19926
+ else {
19927
+ physical(stat2);
19928
+ if (!stat2.isDirectory())
19929
+ throw unsafe();
19930
+ observed.push({ path, stat: stat2 });
19931
+ }
19932
+ }
19933
+ const items = {};
19934
+ for (const name of SKILL_INSTALL_PAIR) {
19935
+ const sourcePath = name === "message-like-me" ? bundledSkillPath() : bundledEnsoulSkillPath();
19936
+ const sourceStat = await optionalStat(sourcePath);
19937
+ if (sourceStat === null)
19938
+ throw new CliError("not-found", `Bundled ${name} skill is missing at ${sourcePath}`);
19939
+ physical(sourceStat);
19940
+ if (!sourceStat.isDirectory())
19941
+ throw unsafe();
19942
+ const source = await fs.realpath(sourcePath);
19943
+ if (contains5(source, root) || contains5(root, source))
19944
+ throw unsafe();
19945
+ const sourceInventory = await inventory(source);
19946
+ const destination = join7(root, name);
19947
+ const current = await optionalStat(destination);
19948
+ if (current !== null) {
19949
+ physical(current);
19950
+ if (!current.isDirectory())
19951
+ throw new CliError("unsafe-path", `Refusing to replace non-directory ${destination}`);
19952
+ if (!options.force)
19953
+ throw new CliError("conflict", `Skill already exists at ${destination}; pass --force to replace both bundled skills`);
19954
+ }
19955
+ items[name] = { source, destination, sourceInventory, original: current === null ? null : await inventory(destination) };
19956
+ }
19957
+ return { parents: observed, missing, root, items };
19958
+ }
19959
+ async function removeObserved(transaction, root, entries2) {
19960
+ for (const entry of [...entries2].reverse()) {
19961
+ await custody(transaction);
19962
+ for (const parent of entries2) {
19963
+ if (parent.stat.isDirectory() && contains5(join7(root, parent.name), join7(root, entry.name))) {
19964
+ await assertIdentity(join7(root, parent.name), parent.stat);
19965
+ }
19966
+ }
19967
+ const path = join7(root, entry.name);
19968
+ await assertIdentity(path, entry.stat);
19969
+ if (entry.stat.isDirectory())
19970
+ await fs.rmdir(path);
19971
+ else
19972
+ await fs.unlink(path);
19973
+ }
19974
+ }
19975
+ var foreign = (operation) => exports_Effect.uninterruptible(exports_Effect.tryPromise({ try: operation, catch: nativeFailure }));
19976
+ var skillInstallPlatform = {
19977
+ preflight: (options) => foreign(async () => {
19978
+ const plan = await preflight(options);
19979
+ const token3 = Object.freeze({ _tag: "SkillInstallPlan" });
19980
+ plans.set(token3, plan);
19981
+ return token3;
19982
+ }),
19983
+ transaction: (token3) => exports_Effect.try({ try: () => {
19984
+ const plan = plans.get(token3);
19985
+ if (plan === undefined)
19986
+ throw unsafe();
19987
+ const directory = join7(plan.root, `.skill-install.${randomBytes2(16).toString("hex")}`);
19988
+ const itemState = (name) => ({ stage: join7(directory, `${name}.stage`), backup: join7(directory, `${name}.backup`), staged: [], backupAttempted: false, publishAttempted: false });
19989
+ const transaction = Object.freeze({ _tag: "SkillInstallTransaction" });
19990
+ transactions.set(transaction, {
19991
+ plan,
19992
+ lock: join7(plan.root, ".message-like-me.install-lock"),
19993
+ lockAttempted: false,
19994
+ directory,
19995
+ directoryAttempted: false,
19996
+ items: { "message-like-me": itemState("message-like-me"), ensoul: itemState("ensoul") }
19997
+ });
19998
+ return transaction;
19999
+ }, catch: commandFailure }),
20000
+ acquire: (token3) => foreign(async () => {
20001
+ const transaction = state(token3);
20002
+ await parents(transaction.plan);
20003
+ for (const path of transaction.plan.missing) {
20004
+ await parents(transaction.plan);
20005
+ await fs.mkdir(path, { mode: 448 });
20006
+ const stat2 = await fs.lstat(path, { bigint: true });
20007
+ physical(stat2);
20008
+ if (!stat2.isDirectory())
20009
+ throw unsafe();
20010
+ transaction.plan.parents.push({ path, stat: stat2 });
20011
+ }
20012
+ await parents(transaction.plan);
20013
+ transaction.lockAttempted = true;
20014
+ await fs.mkdir(transaction.lock, { mode: 448 });
20015
+ transaction.lockStat = await fs.lstat(transaction.lock, { bigint: true });
20016
+ physical(transaction.lockStat);
20017
+ if (!transaction.lockStat.isDirectory())
20018
+ throw unsafe();
20019
+ await custody(transaction);
20020
+ transaction.directoryAttempted = true;
20021
+ await fs.mkdir(transaction.directory, { mode: 448 });
20022
+ transaction.directoryStat = await fs.lstat(transaction.directory, { bigint: true });
20023
+ physical(transaction.directoryStat);
20024
+ if (!transaction.directoryStat.isDirectory())
20025
+ throw unsafe();
20026
+ }),
20027
+ copy: (token3, name) => foreign(async () => {
20028
+ const transaction = state(token3);
20029
+ const item = transaction.plan.items[name];
20030
+ const current = transaction.items[name];
20031
+ await custody(transaction);
20032
+ await assertInventory(item.source, item.sourceInventory);
20033
+ for (const entry of item.sourceInventory) {
20034
+ await custody(transaction);
20035
+ for (const parent of current.staged) {
20036
+ if (parent.stat.isDirectory() && contains5(join7(current.stage, parent.name), join7(current.stage, entry.name))) {
20037
+ await assertIdentity(join7(current.stage, parent.name), parent.stat);
20038
+ }
19712
20039
  }
20040
+ const target = join7(current.stage, entry.name);
20041
+ await assertIdentity(join7(item.source, entry.name), entry.stat);
20042
+ if (entry.stat.isDirectory())
20043
+ await fs.mkdir(target, { mode: 448 });
20044
+ else
20045
+ await fs.cp(join7(item.source, entry.name), target, { force: false, errorOnExist: true, dereference: false });
20046
+ const stat2 = await fs.lstat(target, { bigint: true });
20047
+ physical(stat2);
20048
+ if (stat2.isDirectory() !== entry.stat.isDirectory())
20049
+ throw unsafe();
20050
+ current.staged.push({ name: entry.name, stat: stat2, ...entry.digest === undefined ? {} : { digest: entry.digest } });
20051
+ }
20052
+ const copied = await inventory(current.stage);
20053
+ if (!sameContents(item.sourceInventory, copied))
20054
+ throw unsafe();
20055
+ await assertInventory(item.source, item.sourceInventory);
20056
+ current.staged = [...copied];
20057
+ }),
20058
+ backup: (token3, name) => foreign(async () => {
20059
+ const transaction = state(token3);
20060
+ const item = transaction.plan.items[name];
20061
+ const current = transaction.items[name];
20062
+ await custody(transaction);
20063
+ await absent(current.backup);
20064
+ if (item.original === null) {
20065
+ await absent(item.destination);
20066
+ return;
19713
20067
  }
19714
- for (const item of state) {
19715
- await rename(item.stage, item.destination);
19716
- item.published = true;
19717
- }
19718
- } catch (error) {
19719
- for (const item of [...state].reverse()) {
19720
- if (item.published)
19721
- await rm(item.destination, { recursive: true, force: true }).catch(() => {
19722
- return;
19723
- });
19724
- if (item.hadExisting) {
19725
- await rename(item.backup, item.destination).catch(() => {
19726
- return;
19727
- });
20068
+ await assertInventory(item.destination, item.original);
20069
+ await custody(transaction);
20070
+ current.backupAttempted = true;
20071
+ await fs.rename(item.destination, current.backup);
20072
+ }),
20073
+ publish: (token3, name) => foreign(async () => {
20074
+ const transaction = state(token3);
20075
+ const item = transaction.plan.items[name];
20076
+ const current = transaction.items[name];
20077
+ await custody(transaction);
20078
+ await assertInventory(current.stage, current.staged);
20079
+ await custody(transaction);
20080
+ await absent(item.destination);
20081
+ current.publishAttempted = true;
20082
+ await fs.rename(current.stage, item.destination);
20083
+ }),
20084
+ cleanup: (token3, name, phase) => foreign(async () => {
20085
+ const transaction = state(token3);
20086
+ if (phase === "lock-cleanup") {
20087
+ if (transaction.lockStat === undefined) {
20088
+ if (transaction.lockAttempted) {
20089
+ await parents(transaction.plan);
20090
+ await absent(transaction.lock);
20091
+ }
20092
+ return;
19728
20093
  }
19729
- await rm(item.stage, { recursive: true, force: true }).catch(() => {
20094
+ await parents(transaction.plan);
20095
+ await assertIdentity(transaction.lock, transaction.lockStat);
20096
+ await fs.rmdir(transaction.lock);
20097
+ return;
20098
+ }
20099
+ if (phase === "transaction-cleanup") {
20100
+ if (transaction.directoryStat === undefined) {
20101
+ if (transaction.directoryAttempted) {
20102
+ await parents(transaction.plan);
20103
+ await absent(transaction.directory);
20104
+ }
19730
20105
  return;
19731
- });
20106
+ }
20107
+ await custody(transaction);
20108
+ await fs.rmdir(transaction.directory);
20109
+ return;
19732
20110
  }
19733
- throw error;
19734
- }
19735
- for (const item of state) {
19736
- if (item.hadExisting) {
19737
- await rm(item.backup, { recursive: true, force: true }).catch(() => {
20111
+ if (name === "pair")
20112
+ throw unsafe();
20113
+ const current = transaction.items[name];
20114
+ const item = transaction.plan.items[name];
20115
+ if (phase === "rollback-published") {
20116
+ if (!current.publishAttempted)
19738
20117
  return;
19739
- });
20118
+ await custody(transaction);
20119
+ const destination = await optionalStat(item.destination);
20120
+ if (destination === null)
20121
+ return;
20122
+ const root = current.staged[0];
20123
+ if (root === undefined || !same(root.stat, destination))
20124
+ throw unsafe();
20125
+ await removeObserved(transaction, item.destination, current.staged);
20126
+ } else if (phase === "restore-backup") {
20127
+ if (!current.backupAttempted || item.original === null)
20128
+ return;
20129
+ await custody(transaction);
20130
+ if (await optionalStat(current.backup) === null) {
20131
+ await assertInventory(item.destination, item.original);
20132
+ return;
20133
+ }
20134
+ await assertInventory(current.backup, item.original);
20135
+ await absent(item.destination);
20136
+ await fs.rename(current.backup, item.destination);
20137
+ } else if (phase === "stage-cleanup") {
20138
+ if (current.staged.length === 0)
20139
+ return;
20140
+ await custody(transaction);
20141
+ if (await optionalStat(current.stage) !== null)
20142
+ await removeObserved(transaction, current.stage, current.staged);
20143
+ } else if (phase === "backup-cleanup") {
20144
+ if (item.original !== null)
20145
+ await removeObserved(transaction, current.backup, item.original);
19740
20146
  }
19741
- }
19742
- return Object.freeze({
19743
- messageLikeMe: await realpath3(join7(root, "message-like-me")),
19744
- ensoul: await realpath3(join7(root, "ensoul"))
19745
- });
19746
- }
20147
+ }),
20148
+ destinations: (token3) => foreign(async () => {
20149
+ const transaction = state(token3);
20150
+ await custody(transaction);
20151
+ for (const name of SKILL_INSTALL_PAIR)
20152
+ await assertInventory(transaction.plan.items[name].destination, transaction.items[name].staged);
20153
+ return Object.freeze({ messageLikeMe: await fs.realpath(transaction.plan.items["message-like-me"].destination), ensoul: await fs.realpath(transaction.plan.items.ensoul.destination) });
20154
+ })
20155
+ };
20156
+ var skillInstallPlatformLive = exports_Layer.succeed(SkillInstallPlatform, skillInstallPlatform);
19747
20157
 
19748
20158
  // src/store.ts
19749
20159
  import { Database as Database3 } from "bun:sqlite";
19750
- import { createHash as createHash5 } from "crypto";
20160
+ import { createHash as createHash6 } from "crypto";
19751
20161
  import {
19752
20162
  closeSync as closeSync2,
19753
20163
  constants as fsConstants6,
@@ -21068,7 +21478,7 @@ function globalCorpusRevision(database) {
21068
21478
  }));
21069
21479
  }
21070
21480
  function sourceStateRevision(database, sourceId) {
21071
- const hash2 = createHash5("sha256");
21481
+ const hash2 = createHash6("sha256");
21072
21482
  hash2.update("message-like-me\x00stored-source-state-v1\x00", "utf8");
21073
21483
  const append4 = (kind, row) => {
21074
21484
  const encoded = canonicalJson(row);
@@ -22677,8 +23087,8 @@ class LocalStore {
22677
23087
  }
22678
23088
  preparedHandoffReceiptStatus(value) {
22679
23089
  const handoff = parseAgentMessageHandoffV1(value);
22680
- const exists6 = get13(this.#database, "SELECT handoff_id FROM agent_message_handoffs WHERE handoff_id=?", handoff.handoffId);
22681
- if (exists6 === null)
23090
+ const exists5 = get13(this.#database, "SELECT handoff_id FROM agent_message_handoffs WHERE handoff_id=?", handoff.handoffId);
23091
+ if (exists5 === null)
22682
23092
  return "absent";
22683
23093
  const audit = this.handoffAudit(handoff.handoffId);
22684
23094
  return audit.handoffSha256 === handoff.integrity.canonicalSha256 && audit.contactIdSha256 === sha256(handoff.contact.contactId) && audit.routeCandidateIdSha256 === sha256(handoff.contact.routeCandidateId) && audit.sourceIdSha256 === sha256(handoff.contact.sourceId) && audit.conversationIdSha256 === sha256(handoff.contact.conversationId) && audit.corpusRevision === handoff.evidence.corpusRevision && audit.sourceRevision === handoff.evidence.sourceRevision && audit.profileState === handoff.evidence.profileState && audit.profileEvidenceRevision === handoff.evidence.profileEvidenceRevision && audit.wrenchContractHash === handoff.wrench.contractHash && audit.routeRefSha256 === sha256(handoff.wrench.routeRef) && audit.contextRefSha256 === sha256(handoff.wrench.contextRef) && audit.exactDataRevisionSha256 === handoff.wrench.exactDataRevision && audit.latestMessageRevisionSha256 === handoff.wrench.latestMessageRevision && audit.turnDigest === wrenchMessagingTurnDigestV1(handoff) && audit.partCount === handoff.turn.bubbles.length && audit.createdAt === handoff.createdAt && audit.expiresAt === handoff.expiresAt ? "committed" : "different";
@@ -22813,17 +23223,17 @@ class LocalStore {
22813
23223
  }
22814
23224
 
22815
23225
  // src/x-archive.ts
22816
- import { createHash as createHash6 } from "crypto";
23226
+ import { createHash as createHash7 } from "crypto";
22817
23227
  import {
22818
23228
  closeSync as closeSync3,
22819
- constants as constants2,
23229
+ constants as constants3,
22820
23230
  fstatSync as fstatSync3,
22821
23231
  lstatSync as lstatSync5,
22822
23232
  openSync as openSync3,
22823
23233
  readSync as readSync3,
22824
23234
  realpathSync as realpathSync4
22825
23235
  } from "fs";
22826
- import { isAbsolute as isAbsolute5, resolve as resolve6 } from "path";
23236
+ import { isAbsolute as isAbsolute5, resolve as resolve7 } from "path";
22827
23237
 
22828
23238
  // src/x-archive-zip.ts
22829
23239
  import { readSync as readSync2 } from "fs";
@@ -22854,7 +23264,7 @@ var U32_MAX = 4294967295;
22854
23264
  var MAX_X_ZIP_ARCHIVE_BYTES = 16 * 1024 * 1024 * 1024;
22855
23265
  var MAX_X_ZIP_MEMBER_BYTES = 256 * 1024 * 1024;
22856
23266
  var MAX_COMPRESSED_MEMBER_BYTES = 64 * 1024 * 1024;
22857
- var MAX_ENTRIES = 1e5;
23267
+ var MAX_ENTRIES2 = 1e5;
22858
23268
  var MAX_CENTRAL_BYTES = 64 * 1024 * 1024;
22859
23269
  var MAX_TOTAL_SELECTED_BYTES = 768 * 1024 * 1024;
22860
23270
  var MAX_TOTAL_DECLARED_BYTES = 64 * 1024 * 1024 * 1024;
@@ -22872,8 +23282,8 @@ var CRC32_TABLE = (() => {
22872
23282
  }
22873
23283
  return table;
22874
23284
  })();
22875
- function updateCrc32(state, bytes) {
22876
- let value = state;
23285
+ function updateCrc32(state2, bytes) {
23286
+ let value = state2;
22877
23287
  for (const byte of bytes)
22878
23288
  value = CRC32_TABLE[(value ^ byte) & 255] ^ value >>> 8;
22879
23289
  return value;
@@ -22941,7 +23351,7 @@ function directoryFromEocd(descriptor3, archiveSize) {
22941
23351
  if (legacyDisk !== 0 || legacyCentralDisk !== 0 || legacyOnDisk !== legacyCount) {
22942
23352
  throw new Error("X ZIP multi-disk archives are not supported");
22943
23353
  }
22944
- if (legacyCount < 1 || legacyCount > MAX_ENTRIES || legacySize > MAX_CENTRAL_BYTES) {
23354
+ if (legacyCount < 1 || legacyCount > MAX_ENTRIES2 || legacySize > MAX_CENTRAL_BYTES) {
22945
23355
  throw new Error("X ZIP central directory exceeds its bounds");
22946
23356
  }
22947
23357
  const end4 = checkedEnd(legacyOffset, legacySize, "central directory");
@@ -22966,7 +23376,7 @@ function directoryFromEocd(descriptor3, archiveSize) {
22966
23376
  const offset = u64(zip64, 48, "ZIP64 central directory offset");
22967
23377
  if (onDisk !== count)
22968
23378
  throw new Error("X ZIP multi-disk archives are not supported");
22969
- if (count < 1 || count > MAX_ENTRIES || size9 > MAX_CENTRAL_BYTES) {
23379
+ if (count < 1 || count > MAX_ENTRIES2 || size9 > MAX_CENTRAL_BYTES) {
22970
23380
  throw new Error("X ZIP central directory exceeds its bounds");
22971
23381
  }
22972
23382
  const end3 = checkedEnd(offset, size9, "ZIP64 central directory");
@@ -23379,7 +23789,7 @@ var PROVIDER_ID = /^[1-9][0-9]{0,39}$/u;
23379
23789
  var OPAQUE_PROVIDER_ID = /^-?[0-9]{1,40}$/u;
23380
23790
  var HANDLE = /^[A-Za-z0-9_]{1,15}$/u;
23381
23791
  function sha2563(value) {
23382
- return createHash6("sha256").update(value).digest("hex");
23792
+ return createHash7("sha256").update(value).digest("hex");
23383
23793
  }
23384
23794
  function plain(value, label) {
23385
23795
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
@@ -23725,16 +24135,16 @@ function parseConversations(member, selfId, group, seenConversationIds, seenMess
23725
24135
  if (!/^[0-9]+(?:-[0-9]+)?$/u.test(conversationId)) {
23726
24136
  throw new Error(`${label}.dmConversation.conversationId is invalid`);
23727
24137
  }
23728
- let state = combined.get(conversationId);
23729
- if (state === undefined) {
24138
+ let state2 = combined.get(conversationId);
24139
+ if (state2 === undefined) {
23730
24140
  if (seenConversationIds.has(conversationId)) {
23731
24141
  throw new Error(`${label} repeats an X conversation across direct and group members`);
23732
24142
  }
23733
24143
  seenConversationIds.add(conversationId);
23734
- state = { participants: new Set([selfId]), events: [] };
23735
- combined.set(conversationId, state);
24144
+ state2 = { participants: new Set([selfId]), events: [] };
24145
+ combined.set(conversationId, state2);
23736
24146
  }
23737
- const participants = state.participants;
24147
+ const participants = state2.participants;
23738
24148
  const directIds = group ? null : conversationId.split("-").map((id, index) => providerId(id, `${label}.conversationId[${index}]`));
23739
24149
  if (directIds !== null) {
23740
24150
  if (directIds.length !== 2 || !directIds.includes(selfId)) {
@@ -23779,17 +24189,17 @@ function parseConversations(member, selfId, group, seenConversationIds, seenMess
23779
24189
  if (directIds !== null && [...participants].some((id) => !directIds.includes(id))) {
23780
24190
  throw new Error(`${eventLabel} names a user outside its direct conversation`);
23781
24191
  }
23782
- state.events.push(parsed);
24192
+ state2.events.push(parsed);
23783
24193
  }
23784
24194
  }
23785
24195
  const signatures = new Map;
23786
- const conversations = [...combined.entries()].map(([conversationId, state]) => {
23787
- const orderedEvents = state.events.sort((left3, right3) => left3.createdAt.localeCompare(right3.createdAt) || eventSortKey(left3).localeCompare(eventSortKey(right3)));
24196
+ const conversations = [...combined.entries()].map(([conversationId, state2]) => {
24197
+ const orderedEvents = state2.events.sort((left3, right3) => left3.createdAt.localeCompare(right3.createdAt) || eventSortKey(left3).localeCompare(eventSortKey(right3)));
23788
24198
  signatures.set(conversationId, orderedEvents.filter((event) => event.kind === "message-create").map(eventHeaderSignature).sort());
23789
24199
  return {
23790
24200
  conversationId,
23791
24201
  kind: group ? "group" : "direct",
23792
- participantIds: [...state.participants].sort(),
24202
+ participantIds: [...state2.participants].sort(),
23793
24203
  events: orderedEvents
23794
24204
  };
23795
24205
  });
@@ -24067,7 +24477,7 @@ function parseXArchiveMembers(members) {
24067
24477
  };
24068
24478
  }
24069
24479
  function sha256Descriptor(descriptor3, size9) {
24070
- const digest5 = createHash6("sha256");
24480
+ const digest5 = createHash7("sha256");
24071
24481
  const buffer = Buffer.allocUnsafe(8 * 1024 * 1024);
24072
24482
  let position = 0;
24073
24483
  while (position < size9) {
@@ -24083,18 +24493,18 @@ function sameStat(left3, right3) {
24083
24493
  return left3.dev === right3.dev && left3.ino === right3.ino && left3.size === right3.size && left3.mtimeNs === right3.mtimeNs && left3.ctimeNs === right3.ctimeNs && left3.mode === right3.mode && left3.uid === right3.uid && left3.nlink === right3.nlink;
24084
24494
  }
24085
24495
  async function readXArchive(path) {
24086
- if (typeof path !== "string" || path.length < 1 || path.includes("\x00") || !isAbsolute5(path) || resolve6(path) !== path)
24496
+ if (typeof path !== "string" || path.length < 1 || path.includes("\x00") || !isAbsolute5(path) || resolve7(path) !== path)
24087
24497
  throw new Error("X archive path must be a normalized absolute path");
24088
- let physical;
24498
+ let physical2;
24089
24499
  try {
24090
- physical = realpathSync4(path);
24500
+ physical2 = realpathSync4(path);
24091
24501
  } catch (error) {
24092
24502
  throw new Error("X archive path cannot be resolved", { cause: error });
24093
24503
  }
24094
- if (physical !== path)
24504
+ if (physical2 !== path)
24095
24505
  throw new Error("X archive path must not traverse a symbolic link");
24096
24506
  const pathBefore = lstatSync5(path, { bigint: true });
24097
- const descriptor3 = openSync3(path, constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0));
24507
+ const descriptor3 = openSync3(path, constants3.O_RDONLY | (constants3.O_NOFOLLOW ?? 0));
24098
24508
  try {
24099
24509
  const before2 = fstatSync3(descriptor3, { bigint: true });
24100
24510
  const uid = typeof process.getuid === "function" ? BigInt(process.getuid()) : null;
@@ -24146,11 +24556,11 @@ function translatedFailure(translate, error) {
24146
24556
  }
24147
24557
  function commandPlatformLive(io, cleanupFailure) {
24148
24558
  const attempt = (tryOperation) => exports_Effect.try({ try: tryOperation, catch: commandFailure });
24149
- const foreign = (tryOperation) => exports_Effect.tryPromise({ try: tryOperation, catch: commandFailure });
24559
+ const foreign2 = (tryOperation) => exports_Effect.tryPromise({ try: tryOperation, catch: commandFailure });
24150
24560
  const paths = (explicit) => attempt(() => dataPaths(explicit));
24151
- const initialize = (requested) => foreign(() => initializeDataPaths(requested));
24152
- const installKey = (path) => foreign(() => loadOrCreateInstallKey(path));
24153
- const exists6 = (path) => foreign(async () => {
24561
+ const initialize = (requested) => foreign2(() => initializeDataPaths(requested));
24562
+ const installKey = (path) => foreign2(() => loadOrCreateInstallKey(path));
24563
+ const exists5 = (path) => foreign2(async () => {
24154
24564
  try {
24155
24565
  await lstat5(path);
24156
24566
  return true;
@@ -24190,7 +24600,7 @@ function commandPlatformLive(io, cleanupFailure) {
24190
24600
  })));
24191
24601
  const existingSession = (explicit) => exports_Effect.gen(function* () {
24192
24602
  const requested = yield* paths(explicit);
24193
- if (!(yield* exists6(requested.root)) || !(yield* exists6(requested.database))) {
24603
+ if (!(yield* exists5(requested.root)) || !(yield* exists5(requested.database))) {
24194
24604
  return yield* exports_Effect.fail(commandFailure(new CliError("not-found", "Message Like Me is not initialized; run messagelikeme init or an ingest command")));
24195
24605
  }
24196
24606
  const initialized = yield* initialize(requested);
@@ -24218,7 +24628,7 @@ function commandPlatformLive(io, cleanupFailure) {
24218
24628
  paths,
24219
24629
  initialize,
24220
24630
  installKey,
24221
- exists: exists6,
24631
+ exists: exists5,
24222
24632
  existingSession,
24223
24633
  writableSession,
24224
24634
  openStore,
@@ -24226,9 +24636,9 @@ function commandPlatformLive(io, cleanupFailure) {
24226
24636
  readContacts: (path, key) => exports_Effect.try({ try: () => readMacOSContacts(path, { hmacKey: key }), catch: (error) => translatedFailure(translateContactsError, error) }),
24227
24637
  readBundle: (path, key) => exports_Effect.tryPromise({ try: () => readMessageBundle(path, { hmacKey: key }), catch: (error) => translatedFailure(translateBundleError, error) }),
24228
24638
  readXArchive: (path) => exports_Effect.tryPromise({ try: () => readXArchive(path), catch: (error) => translatedFailure(translateXArchiveError, error) }),
24229
- readProfile: (path) => foreign(() => readStyleProfile(path)),
24230
- readPrivateJson: (path, label, maximumBytes) => foreign(() => readStablePrivateJson(path, label, maximumBytes)),
24231
- installSkill: (options) => foreign(() => installSkill(options))
24639
+ readProfile: (path) => foreign2(() => readStyleProfile(path)),
24640
+ readPrivateJson: (path, label, maximumBytes) => foreign2(() => readStablePrivateJson(path, label, maximumBytes)),
24641
+ installSkill: (options) => installSkillProgram(options).pipe(exports_Effect.provide(skillInstallPlatformLive))
24232
24642
  });
24233
24643
  }
24234
24644
 
@@ -24329,10 +24739,10 @@ function rejectUnused(parsed, allowedOptions, allowedFlags) {
24329
24739
  }
24330
24740
 
24331
24741
  // src/command-input.ts
24332
- import { isAbsolute as isAbsolute6, resolve as resolve7 } from "path";
24742
+ import { isAbsolute as isAbsolute6, resolve as resolve8 } from "path";
24333
24743
 
24334
24744
  // src/version.ts
24335
- var MESSAGE_LIKE_ME_VERSION = "0.8.5";
24745
+ var MESSAGE_LIKE_ME_VERSION = "0.8.6";
24336
24746
 
24337
24747
  // src/command-input.ts
24338
24748
  var HELP = `Message Like Me ${MESSAGE_LIKE_ME_VERSION}
@@ -24435,7 +24845,7 @@ function absolutePrivatePath(value, label) {
24435
24845
  throw new CliError("usage", `${label} is required`);
24436
24846
  if (!isAbsolute6(value))
24437
24847
  throw new CliError("unsafe-path", `${label} must be an absolute private path`);
24438
- return resolve7(value);
24848
+ return resolve8(value);
24439
24849
  }
24440
24850
  function handoffExpiry(createdAt, contextExpiresAt, lifetimeMilliseconds) {
24441
24851
  return new Date(Math.min(Date.parse(createdAt) + lifetimeMilliseconds, Date.parse(contextExpiresAt))).toISOString();
@@ -24487,7 +24897,7 @@ function commandArtifactsLive(reports) {
24487
24897
  }
24488
24898
 
24489
24899
  // src/metrics.ts
24490
- import { createHash as createHash7 } from "crypto";
24900
+ import { createHash as createHash8 } from "crypto";
24491
24901
  var DEFAULT_SESSION_GAP_SECONDS = 8 * 60 * 60;
24492
24902
  var DEFAULT_BURST_GAP_SECONDS = 5 * 60;
24493
24903
  var DEFAULT_STUDY_LIMIT = 12;
@@ -24500,7 +24910,7 @@ var DEFAULT_MAX_STUDY_PACKET_BODY_BYTES = 256 * 1024;
24500
24910
  var MAX_STUDY_PACKET_BODY_BYTES = 1024 * 1024;
24501
24911
  var MAX_GAP_SECONDS = 30 * 24 * 60 * 60;
24502
24912
  function digest5(namespace, parts2) {
24503
- const hash2 = createHash7("sha256");
24913
+ const hash2 = createHash8("sha256");
24504
24914
  hash2.update(`message-like-me\x00${namespace}\x00`, "utf8");
24505
24915
  for (const part of parts2)
24506
24916
  hash2.update(`${part.length}:`, "utf8").update(part, "utf8");
@@ -24938,15 +25348,15 @@ function studyMessages(response, byId, maximumTextBytes, maximumMessagesPerDirec
24938
25348
  let emittedBodyBytes = sourceBodyBytes;
24939
25349
  if (sourceBodyBytes > maximumTextBytes) {
24940
25350
  let bytes = 0;
24941
- let bounded = "";
25351
+ let bounded2 = "";
24942
25352
  for (const symbol3 of sourceBody) {
24943
25353
  const symbolBytes = Buffer.byteLength(symbol3, "utf8");
24944
25354
  if (bytes + symbolBytes > maximumTextBytes)
24945
25355
  break;
24946
- bounded += symbol3;
25356
+ bounded2 += symbol3;
24947
25357
  bytes += symbolBytes;
24948
25358
  }
24949
- body = bounded;
25359
+ body = bounded2;
24950
25360
  emittedBodyBytes = bytes;
24951
25361
  }
24952
25362
  return Object.freeze({
@@ -27010,13 +27420,19 @@ function commandProgram(argv) {
27010
27420
  if (project3 !== undefined && scope5 !== "project") {
27011
27421
  return yield* exports_Effect.fail(commandFailure(new CliError("usage", "--project requires --scope project")));
27012
27422
  }
27013
- const destinations = yield* platform2.installSkill({
27423
+ const installed = yield* platform2.installSkill({
27014
27424
  target,
27015
27425
  scope: scope5,
27016
27426
  ...project3 === undefined ? {} : { projectDirectory: project3 },
27017
27427
  force: parsed.flags.has("force")
27018
27428
  });
27019
- yield* platform2.emit(json, { destination: destinations.messageLikeMe, destinations, target, scope: scope5 }, `Installed message-like-me and ensoul skills at ${destinations.messageLikeMe} and ${destinations.ensoul}`);
27429
+ const destinations = exports_Exit.isSuccess(installed.operation) ? installed.operation.value : null;
27430
+ const output = destinations === null ? exports_Exit.asVoid(installed.operation) : yield* exports_Effect.exit(platform2.emit(json, { destination: destinations.messageLikeMe, destinations, target, scope: scope5 }, `Installed message-like-me and ensoul skills at ${destinations.messageLikeMe} and ${destinations.ensoul}`));
27431
+ const warning = skillInstallWarning({ ...installed, operation: exports_Exit.isFailure(output) ? output : installed.operation });
27432
+ const diagnostic = warning === null ? exports_Exit.void : yield* exports_Effect.exit(platform2.stderr(warning));
27433
+ if (exports_Exit.isFailure(output))
27434
+ return yield* output;
27435
+ yield* diagnostic;
27020
27436
  return;
27021
27437
  }
27022
27438
  if (command === "doctor" && subcommand === undefined) {
@@ -18,7 +18,8 @@ failures retain the original error as a cause in the closed `CommandFailure`
18
18
  union. Native adapters translate source-specific failures at the same boundaries
19
19
  as before. The runtime has a narrow compatibility bridge for existing pure
20
20
  validators that throw `CliError`; other defects remain defects. CLI text, stdout
21
- formatting, diagnostics, and exit-code selection are unchanged.
21
+ formatting and exit-code selection are unchanged. The paired skill installer adds
22
+ the bounded residual diagnostics described below.
22
23
 
23
24
  ## Publication and receipt recovery
24
25
 
@@ -60,12 +61,51 @@ Effect and TypeScript do not remove that operating-system limitation. Directory
60
61
  sync failures after removal are reported as `removed-unconfirmed`. The runtime
61
62
  never claims that uncertain cleanup succeeded.
62
63
 
64
+ ## Paired skill installation
65
+
66
+ The existing command runtime directly composes the fixed `message-like-me` and
67
+ `ensoul` installer. Its program owns preflight, exclusive directory lock, private
68
+ transaction directory, copies, backups, both publish renames, recovery, cleanup,
69
+ and output. Both publish acknowledgements commit the pair immediately. Backup
70
+ cleanup, path confirmation, or stdout failure afterward never rolls it back.
71
+ There is no automatic retry or additional runtime. Admitted native work and
72
+ sequential recovery remain masked until the actual filesystem promises settle.
73
+
74
+ Before creating missing installation parents, read-only preflight rejects
75
+ conflicts, observed source/target overlap, symlinked installation parents, and
76
+ nonphysical source entries. Project anchors resolve existing aliases first;
77
+ missing project directories are then created in order beneath the observed
78
+ physical ancestor. Requested paths allow 4,096 UTF-8 bytes and at most 64 missing
79
+ project components. These directories are retained on later failure. Each
80
+ skill inventory permits at most 64 entries, eight levels, 256 KiB per file, and
81
+ 2 MiB total. These bounds also apply to the old directory on forced replacement.
82
+ Copied types, bytes, and digests must match the admitted source before publication.
83
+ Copies explicitly refuse existing entries. A cooperating installer's existing
84
+ lock is never reclaimed; an ambiguous acquisition is left for inspection.
85
+
86
+ Before publication and cleanup the adapter rechecks observed physical parents
87
+ and entry identities. Recovery runs in reverse pair order; independent actions
88
+ continue after cleanup failure. Restoration requires an absent destination and
89
+ the observed backup. Cleanup removes individually observed leaves and then empty
90
+ directories, retaining unknown or substituted entries. Creating missing parent
91
+ directories is not rolled back. This is observation-bound custody, not a kernel
92
+ conditional unlink, cross-process atomic visibility, hostile same-user race
93
+ immunity, or crash recovery.
94
+
95
+ Clean success stdout is unchanged. Residual cleanup or a postcommit output
96
+ failure adds one stderr warning containing only fixed skill, phase, and commit
97
+ state labels. Private cleanup causes remain internal. The original primary
98
+ rejection, including `undefined`, `null`, or `false`, wins over cleanup and a
99
+ failed warning writer. A successful install with failed warning output instead
100
+ reports that output failure; it does not repeat installation.
101
+
63
102
  ## Enforced architecture and distribution
64
103
 
65
104
  `bun run check:effect` uses the repository's TypeScript compiler API and the
66
105
  reviewed policy in `scripts/check-effect-architecture.ts`. Only `commands.ts`
67
106
  may execute a runtime; native calls belong in `command-platform.ts` or
68
- `command-artifacts.ts`. The checker rejects unclassified Effect modules,
107
+ `command-artifacts.ts`, or `skill-install-platform.ts`. The installer model and
108
+ program compose under that same command owner. The checker rejects unclassified Effect modules,
69
109
  floating Effects, erased channels, unsafe assertions, failure-discard shortcuts,
70
110
  suppressions, and selected ambient I/O. Its paired fixtures cover common aliases.
71
111
  It does not prove purity, linear lifetime safety, domain correctness, foreign
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hraness/message-like-me",
3
- "version": "0.8.5",
3
+ "version": "0.8.6",
4
4
  "description": "A local-first CLI and Agent Skill for studying private messaging history and drafting messages that sound like you.",
5
5
  "license": "MIT",
6
6
  "type": "module",