@getmikie/cli 1.1.121 → 1.1.124

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/README.md CHANGED
@@ -9,7 +9,7 @@ npx --yes --ignore-scripts --registry=https://registry.npmjs.org @getmikie/cli@1
9
9
  For a persistent command path on Windows PowerShell:
10
10
 
11
11
  ```powershell
12
- npm.cmd install --global @getmikie/cli@1.1.108 --ignore-scripts --registry=https://registry.npmjs.org
12
+ npm.cmd install --global @getmikie/cli@1.1.108 --allow-scripts=@getmikie/cli --registry=https://registry.npmjs.org
13
13
  ```
14
14
 
15
15
  Mikie does not change your PowerShell execution policy. Keep the exact package
@@ -19,11 +19,13 @@ incorrectly targets the current directory.
19
19
  For a persistent command path on macOS or Linux:
20
20
 
21
21
  ```sh
22
- npm install --global @getmikie/cli@1.1.108 --ignore-scripts --registry=https://registry.npmjs.org
22
+ npm install --global @getmikie/cli@1.1.108 --allow-scripts=@getmikie/cli --registry=https://registry.npmjs.org
23
23
  ```
24
24
 
25
- These portable npm commands disable every lifecycle script and pin the public
26
- npm registry. The public OS installers additionally authenticate exact wrapper
25
+ These persistent npm commands allow only `@getmikie/cli`'s reconciliation
26
+ script and pin the public npm registry. npm 11.5.1 or newer is required for
27
+ `--allow-scripts`; the temporary `npx` command above keeps scripts disabled.
28
+ The public OS installers additionally authenticate exact wrapper
27
29
  and native tarball bytes, install them offline, and explicitly run only Mikie's
28
30
  authenticated reconciliation script.
29
31
 
package/bin/mikie.mjs CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  import { createRequire } from "node:module";
4
4
  import { homedir } from "node:os";
@@ -18,6 +18,7 @@ import {
18
18
  resolveDependencyPackageJson,
19
19
  resolveNativeBinary,
20
20
  selectPlatformPackage,
21
+ shouldCompleteSkippedInstall,
21
22
  shouldReconcileInstalledClients,
22
23
  translateArguments,
23
24
  } from "../lib/launcher.mjs";
@@ -33,6 +34,7 @@ import {
33
34
  runUpdateTransaction,
34
35
  } from "../lib/update.mjs";
35
36
  import {
37
+ completeSkippedInstall,
36
38
  installedClientRuntimeDigests,
37
39
  reconcileInstalledClients,
38
40
  } from "../scripts/reconcile-install.mjs";
@@ -48,6 +50,7 @@ if (["--version", "-v"].includes(singleFlag)) {
48
50
  console.log(`Mikie ${packageMetadata.version}
49
51
 
50
52
  Usage:
53
+ mikie activity --mode solo|ai_assisted|unknown --language <category>
51
54
  mikie savings history [options]
52
55
  mikie savings export-meter-history
53
56
  mikie mcp [--adapter codex|claude_code]
@@ -92,7 +95,7 @@ All commands execute the compiled Mikie native engine.`);
92
95
  const selected = selectPlatformPackage();
93
96
  const platformPackageJson = require.resolve(`${selected.packageName}/package.json`);
94
97
  const wrapperPackageJson = fileURLToPath(new URL("../package.json", import.meta.url));
95
- const binary = await resolveNativeBinary();
98
+ const binary = await resolveNativeBinary({ version: packageMetadata.version });
96
99
  const globalPrefix = npmPrefix.stdout.trim();
97
100
  const globalShims =
98
101
  process.platform === "win32"
@@ -150,7 +153,7 @@ All commands execute the compiled Mikie native engine.`);
150
153
  return {
151
154
  wrapperPackageJson,
152
155
  platformPackageJson: require.resolve(`${selected.packageName}/package.json`),
153
- binary: await resolveNativeBinary(),
156
+ binary: await resolveNativeBinary({ version: packageMetadata.version }),
154
157
  shims: globalShims,
155
158
  };
156
159
  },
@@ -160,16 +163,20 @@ All commands execute the compiled Mikie native engine.`);
160
163
  ownership: path.join(customerHome, ".claude", "mikie-evidence-hook-ownership.json"),
161
164
  },
162
165
  transactionRoot,
163
- prepareFinalLauncher: (candidate, version) =>
164
- process.platform === "win32"
166
+ prepareFinalLauncher: async (candidate, version) => {
167
+ const preserveSha256 = await installedClientRuntimeDigests();
168
+ return process.platform === "win32"
165
169
  ? materializeWindowsRuntime(candidate.binary, {
166
170
  version,
167
171
  cleanupHistorical: false,
172
+ preserveSha256,
168
173
  })
169
174
  : materializePortableRuntime(candidate.binary, {
170
175
  version,
171
176
  cleanupHistorical: false,
172
- }),
177
+ preserveSha256,
178
+ });
179
+ },
173
180
  retireFinalLauncher: async ({ path: launcher, sha256 }) => {
174
181
  const preserveSha256 = await installedClientRuntimeDigests();
175
182
  return process.platform === "win32"
@@ -239,19 +246,43 @@ All commands execute the compiled Mikie native engine.`);
239
246
  }
240
247
  } catch (error) {
241
248
  const message = error instanceof Error ? error.message : "Mikie update failed";
242
- console.error(`mikie: ${message}`);
249
+ const guidance = message === "invalid Mikie update recovery authority"
250
+ ? "; keep the recovery files and contact support before retrying"
251
+ : "";
252
+ console.error(`mikie: ${message}${guidance}`);
243
253
  process.exitCode = 1;
244
254
  }
245
255
  } else {
246
256
  try {
247
257
  const nativeArguments = translateArguments(arguments_);
248
258
  if (shouldReconcileInstalledClients(nativeArguments)) {
249
- await reconcileInstalledClients({ version: packageMetadata.version });
259
+ const clients = await reconcileInstalledClients({
260
+ version: packageMetadata.version,
261
+ allowDeferred: nativeArguments[0] === "serve",
262
+ });
263
+ if (clients.status === "deferred") {
264
+ console.error("mikie: client repair deferred; relay startup continues. Run `mikie doctor` to inspect client registration.");
265
+ }
266
+ } else if (shouldCompleteSkippedInstall(nativeArguments)) {
267
+ // An invalid install fails here, once, before any client repair.
268
+ await resolveNativeBinary();
269
+ try {
270
+ const clients = await completeSkippedInstall({ version: packageMetadata.version });
271
+ if (clients.status === "deferred") {
272
+ console.error("mikie: client repair deferred; run `mikie doctor` to inspect client registration.");
273
+ }
274
+ } catch (error) {
275
+ const reason = error instanceof Error ? error.message : "unavailable";
276
+ console.error(`mikie: install reconciliation is incomplete (${reason}); the command continues.`);
277
+ }
250
278
  }
251
279
  const binary = await prepareNativeLaunch(await resolveNativeBinary(), nativeArguments, {
252
280
  version: packageMetadata.version,
253
281
  });
254
282
  process.exitCode = await launchNative(binary, nativeArguments);
283
+ if (nativeArguments[0] === "serve" && process.exitCode !== 0) {
284
+ console.error(`mikie: Mikie relay exited with status ${process.exitCode}; run \`mikie doctor\`, then retry the MCP session`);
285
+ }
255
286
  } catch (error) {
256
287
  const message = error instanceof Error ? error.message : "Mikie is unavailable";
257
288
  console.error(`mikie: ${message}`);
package/lib/launcher.mjs CHANGED
@@ -20,7 +20,6 @@ import { spawn } from "node:child_process";
20
20
  const PLATFORM_PACKAGES = new Map([
21
21
  ["win32-x64", { packageName: "@getmikie/win32-x64", binaryPath: "bin/mikie.exe" }],
22
22
  ["darwin-arm64", { packageName: "@getmikie/darwin-arm64", binaryPath: "bin/mikie" }],
23
- ["darwin-x64", { packageName: "@getmikie/darwin-x64", binaryPath: "bin/mikie" }],
24
23
  ["linux-x64-gnu", { packageName: "@getmikie/linux-x64-gnu", binaryPath: "bin/mikie" }],
25
24
  ]);
26
25
 
@@ -68,13 +67,7 @@ async function cleanupHistoricalWindowsRuntimes(
68
67
  selectedDirectory,
69
68
  { graceMs = HISTORICAL_RUNTIME_GRACE_MS, preserveSha256 = [] } = {},
70
69
  ) {
71
- if (
72
- !Array.isArray(preserveSha256) ||
73
- preserveSha256.length > MAX_MATERIALIZED_WINDOWS_RUNTIMES ||
74
- preserveSha256.some((digest) => !/^[0-9a-f]{64}$/.test(digest))
75
- ) {
76
- throw new Error("Mikie cached runtime preservation authority is invalid");
77
- }
70
+ validateRuntimePreservation(preserveSha256);
78
71
  const preserved = new Set(preserveSha256);
79
72
  let entries;
80
73
  try {
@@ -104,6 +97,68 @@ async function cleanupHistoricalWindowsRuntimes(
104
97
  );
105
98
  }
106
99
 
100
+ function validateRuntimePreservation(preserveSha256) {
101
+ if (
102
+ !Array.isArray(preserveSha256) ||
103
+ preserveSha256.length > MAX_MATERIALIZED_WINDOWS_RUNTIMES ||
104
+ preserveSha256.some((digest) => !/^[0-9a-f]{64}$/.test(digest))
105
+ ) {
106
+ throw new Error("Mikie cached runtime preservation authority is invalid");
107
+ }
108
+ }
109
+
110
+ async function evictStaleUnreferencedWindowsRuntimes(
111
+ runtimeRoot,
112
+ selectedDirectory,
113
+ preserveSha256,
114
+ ) {
115
+ validateRuntimePreservation(preserveSha256);
116
+ let entries;
117
+ try {
118
+ entries = await readdir(runtimeRoot, { withFileTypes: true });
119
+ } catch {
120
+ return;
121
+ }
122
+ const materialized = entries.filter(
123
+ (entry) => entry.isDirectory() && SAFE_RUNTIME_DIRECTORY.test(entry.name),
124
+ );
125
+ const preserved = new Set(preserveSha256);
126
+ const cutoff = Date.now() - HISTORICAL_RUNTIME_GRACE_MS;
127
+ const candidates = [];
128
+ for (const entry of materialized) {
129
+ if (
130
+ entry.name === selectedDirectory ||
131
+ [...preserved].some((digest) => entry.name.endsWith(`-${digest}`))
132
+ ) {
133
+ continue;
134
+ }
135
+ const candidate = path.join(runtimeRoot, entry.name);
136
+ try {
137
+ const metadata = await lstat(candidate);
138
+ if (metadata.mtimeMs <= cutoff) {
139
+ candidates.push({ candidate, mtimeMs: metadata.mtimeMs });
140
+ }
141
+ } catch {
142
+ // A concurrent removal is already freeing capacity; re-count below.
143
+ }
144
+ }
145
+ candidates.sort((left, right) => left.mtimeMs - right.mtimeMs);
146
+ const required = Math.max(
147
+ 0,
148
+ materialized.length - (MAX_MATERIALIZED_WINDOWS_RUNTIMES - 1),
149
+ );
150
+ let removed = 0;
151
+ for (const { candidate } of candidates) {
152
+ if (removed >= required) break;
153
+ try {
154
+ await rm(candidate, { recursive: true, force: true, maxRetries: 0 });
155
+ removed += 1;
156
+ } catch {
157
+ // Active Windows executables remain locked and are retained safely.
158
+ }
159
+ }
160
+ }
161
+
107
162
  function abandonmentMarkerForExecutable(
108
163
  runtimeRoot,
109
164
  selectedDirectory,
@@ -246,7 +301,12 @@ async function scavengeAbandonedWindowsRuntimes(runtimeRoot) {
246
301
  }
247
302
  }
248
303
 
249
- async function enforceMaterializedRuntimeCapacity(runtimeRoot, selectedDirectory) {
304
+ async function enforceMaterializedRuntimeCapacity(
305
+ runtimeRoot,
306
+ selectedDirectory,
307
+ { preserveSha256 = [] } = {},
308
+ ) {
309
+ validateRuntimePreservation(preserveSha256);
250
310
  await scavengeAbandonedWindowsRuntimes(runtimeRoot);
251
311
  let entries;
252
312
  try {
@@ -264,6 +324,25 @@ async function enforceMaterializedRuntimeCapacity(runtimeRoot, selectedDirectory
264
324
  ) {
265
325
  return;
266
326
  }
327
+ await evictStaleUnreferencedWindowsRuntimes(
328
+ runtimeRoot,
329
+ selectedDirectory,
330
+ preserveSha256,
331
+ );
332
+ let remaining;
333
+ try {
334
+ remaining = (await readdir(runtimeRoot, { withFileTypes: true })).filter(
335
+ (entry) => entry.isDirectory() && SAFE_RUNTIME_DIRECTORY.test(entry.name),
336
+ );
337
+ } catch {
338
+ remaining = materialized;
339
+ }
340
+ if (
341
+ remaining.some((entry) => entry.name === selectedDirectory) ||
342
+ remaining.length < MAX_MATERIALIZED_WINDOWS_RUNTIMES
343
+ ) {
344
+ return;
345
+ }
267
346
  throw new Error(
268
347
  "Mikie cached Windows runtime retention capacity is full; reconcile exact-owned clients before installing another runtime",
269
348
  );
@@ -412,7 +491,7 @@ export async function retireHistoricalRuntimes(
412
491
 
413
492
  export function shouldStageWindowsRuntime(arguments_) {
414
493
  if (arguments_.length === 0) return true;
415
- if (["mcp", "serve", "run-relay", "doctor"].includes(arguments_[0])) return true;
494
+ if (["mcp", "serve", "run-relay"].includes(arguments_[0])) return true;
416
495
  if (arguments_.length === 1 && arguments_[0] === "uninstall") return true;
417
496
  if (
418
497
  arguments_.length === 2 &&
@@ -421,6 +500,17 @@ export function shouldStageWindowsRuntime(arguments_) {
421
500
  ) {
422
501
  return true;
423
502
  }
503
+ if (arguments_[0] === "mode" && arguments_[1] === "codex") {
504
+ const modeArguments = arguments_.slice(2);
505
+ return (
506
+ modeArguments.length === 0 ||
507
+ (modeArguments.length === 1 &&
508
+ ["active", "shadow", "--json"].includes(modeArguments[0])) ||
509
+ (modeArguments.length === 2 &&
510
+ ["active", "shadow"].includes(modeArguments[0]) &&
511
+ modeArguments[1] === "--json")
512
+ );
513
+ }
424
514
  return arguments_[0] === "control" && arguments_[1] === "run";
425
515
  }
426
516
 
@@ -429,9 +519,30 @@ export function shouldReconcileInstalledClients(arguments_) {
429
519
  return ["serve", "run-relay", "run-codex-hook", "run-claude-hook"].includes(arguments_[0]);
430
520
  }
431
521
 
522
+ /**
523
+ * Return whether a customer command first finishes an install whose npm
524
+ * postinstall never ran (npm's allow-scripts policy skips it by default).
525
+ * Only commands a person types: hook and internal entrypoints (for example
526
+ * `efficiency-hook`, run on every prompt) and read-only doctor never do.
527
+ */
528
+ const INSTALL_COMPLETING_COMMANDS = new Set([
529
+ "setup",
530
+ "login",
531
+ "connect",
532
+ "privacy",
533
+ "mode",
534
+ "savings",
535
+ "activity",
536
+ "control",
537
+ ]);
538
+
539
+ export function shouldCompleteSkippedInstall(arguments_) {
540
+ return INSTALL_COMPLETING_COMMANDS.has(arguments_[0]);
541
+ }
542
+
432
543
  async function materializeRuntime(
433
544
  binary,
434
- { version, runtimeRoot, executableName, cleanupHistorical },
545
+ { version, runtimeRoot, executableName, cleanupHistorical, preserveSha256 },
435
546
  ) {
436
547
  const sourceMetadata = await lstat(binary);
437
548
  if (!sourceMetadata.isFile() || sourceMetadata.isSymbolicLink() || sourceMetadata.size === 0) {
@@ -441,11 +552,15 @@ async function materializeRuntime(
441
552
  const selectedDirectory = `${version}-${sourceSha256}`;
442
553
  const runtimeDirectory = path.join(runtimeRoot, selectedDirectory);
443
554
  const materialized = path.join(runtimeDirectory, executableName);
444
- await enforceMaterializedRuntimeCapacity(runtimeRoot, selectedDirectory);
555
+ await enforceMaterializedRuntimeCapacity(runtimeRoot, selectedDirectory, {
556
+ preserveSha256,
557
+ });
445
558
  await mkdir(runtimeDirectory, { recursive: true });
446
559
  if (await verifyMaterializedRuntime(materialized, sourceSha256, { allowMissing: true })) {
447
560
  if (cleanupHistorical) {
448
- await cleanupHistoricalWindowsRuntimes(runtimeRoot, selectedDirectory);
561
+ await cleanupHistoricalWindowsRuntimes(runtimeRoot, selectedDirectory, {
562
+ preserveSha256,
563
+ });
449
564
  }
450
565
  return materialized;
451
566
  }
@@ -475,7 +590,9 @@ async function materializeRuntime(
475
590
  }
476
591
  await verifyMaterializedRuntime(materialized, sourceSha256);
477
592
  if (cleanupHistorical) {
478
- await cleanupHistoricalWindowsRuntimes(runtimeRoot, selectedDirectory);
593
+ await cleanupHistoricalWindowsRuntimes(runtimeRoot, selectedDirectory, {
594
+ preserveSha256,
595
+ });
479
596
  }
480
597
  return materialized;
481
598
  } finally {
@@ -489,6 +606,7 @@ export async function materializeWindowsRuntime(
489
606
  version,
490
607
  localAppData = process.env.LOCALAPPDATA,
491
608
  cleanupHistorical = true,
609
+ preserveSha256 = [],
492
610
  } = {},
493
611
  ) {
494
612
  if (
@@ -503,6 +621,7 @@ export async function materializeWindowsRuntime(
503
621
  runtimeRoot: path.join(localAppData, "Mikie", "runtime-v1"),
504
622
  executableName: "mikie.exe",
505
623
  cleanupHistorical,
624
+ preserveSha256,
506
625
  });
507
626
  }
508
627
 
@@ -553,6 +672,7 @@ export async function materializePortableRuntime(
553
672
  platform = process.platform,
554
673
  environment = process.env,
555
674
  cleanupHistorical = true,
675
+ preserveSha256 = [],
556
676
  } = {},
557
677
  ) {
558
678
  const runtimeRoot = portableRuntimeRoot(platform, environment);
@@ -568,6 +688,7 @@ export async function materializePortableRuntime(
568
688
  version,
569
689
  localAppData: environment.LOCALAPPDATA,
570
690
  cleanupHistorical,
691
+ preserveSha256,
571
692
  });
572
693
  }
573
694
  return materializeRuntime(binary, {
@@ -575,9 +696,46 @@ export async function materializePortableRuntime(
575
696
  runtimeRoot,
576
697
  executableName: "mikie",
577
698
  cleanupHistorical,
699
+ preserveSha256,
578
700
  });
579
701
  }
580
702
 
703
+ /** Inspect the existing pinned runtime without creating or reconciling any state. */
704
+ async function inspectMaterializedRuntime(binary, { platform, version, environment }) {
705
+ if (!path.isAbsolute(binary)) {
706
+ throw new Error("Mikie packaged runtime authority is invalid");
707
+ }
708
+ const sourceMetadata = await lstat(binary);
709
+ if (!sourceMetadata.isFile() || sourceMetadata.isSymbolicLink() || sourceMetadata.size === 0) {
710
+ throw new Error("Mikie packaged runtime is invalid; reinstall the CLI before retrying doctor");
711
+ }
712
+ const sourceSha256 = await sha256File(binary);
713
+ const selected = materializedRuntimePath({ version, sha256: sourceSha256, platform, environment });
714
+ const repair = "run `mikie update`; if it cannot restore the runtime, contact support";
715
+ try {
716
+ for (const directory of [path.dirname(path.dirname(selected)), path.dirname(selected)]) {
717
+ const metadata = await lstat(directory);
718
+ if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
719
+ throw new Error("invalid runtime directory");
720
+ }
721
+ }
722
+ await verifyMaterializedRuntime(selected, sourceSha256);
723
+ if (platform !== "win32") await access(selected, fsConstants.X_OK);
724
+ } catch (error) {
725
+ if (error?.code === "ENOENT") {
726
+ throw new Error("Mikie installed runtime is missing; run `mikie setup codex` or `mikie setup claude` for your configured client, then retry `mikie doctor`");
727
+ }
728
+ if (error?.message === "Mikie cached Windows runtime hash does not match") {
729
+ throw new Error(`Mikie installed runtime does not match the packaged engine; ${repair}`);
730
+ }
731
+ throw new Error(`Mikie installed runtime is invalid or unreadable; ${repair}`);
732
+ }
733
+ if ((await sha256File(binary)) !== sourceSha256) {
734
+ throw new Error("Mikie packaged runtime changed during doctor inspection; retry after installation finishes");
735
+ }
736
+ return selected;
737
+ }
738
+
581
739
  export async function prepareNativeLaunch(
582
740
  binary,
583
741
  arguments_,
@@ -585,8 +743,12 @@ export async function prepareNativeLaunch(
585
743
  platform = process.platform,
586
744
  version,
587
745
  environment = process.env,
746
+ preserveSha256 = [],
588
747
  } = {},
589
748
  ) {
749
+ if (arguments_[0] === "doctor") {
750
+ return inspectMaterializedRuntime(binary, { platform, version, environment });
751
+ }
590
752
  if (!shouldStageWindowsRuntime(arguments_)) {
591
753
  return binary;
592
754
  }
@@ -595,6 +757,7 @@ export async function prepareNativeLaunch(
595
757
  platform,
596
758
  environment,
597
759
  cleanupHistorical: false,
760
+ preserveSha256,
598
761
  });
599
762
  }
600
763
 
@@ -614,6 +777,12 @@ export function selectPlatformPackage({
614
777
  } = {}) {
615
778
  const key = platform === "linux" && glibc ? `${platform}-${arch}-gnu` : `${platform}-${arch}`;
616
779
  const selected = PLATFORM_PACKAGES.get(key);
780
+ if (!selected && key === "darwin-x64") {
781
+ throw new Error(
782
+ "Mikie no longer supports Intel Macs. The last version for this Mac is 1.1.121: " +
783
+ "npm install --global @getmikie/cli@1.1.121",
784
+ );
785
+ }
617
786
  if (!selected) {
618
787
  throw new Error("Mikie does not publish a native npm engine for this platform");
619
788
  }
@@ -645,6 +814,7 @@ export async function resolveNativeBinary({
645
814
  arch = process.arch,
646
815
  glibc,
647
816
  resolvePackage,
817
+ version,
648
818
  } = {}) {
649
819
  const selected = selectPlatformPackage({ platform, arch, glibc });
650
820
  const require = createRequire(import.meta.url);
@@ -657,6 +827,27 @@ export async function resolveNativeBinary({
657
827
  throw new Error(`Required Mikie native package ${selected.packageName} is unavailable`);
658
828
  }
659
829
 
830
+ let metadata;
831
+ try {
832
+ const handle = await open(packageJson, "r");
833
+ try {
834
+ const bytes = Buffer.alloc(65537);
835
+ const { bytesRead } = await handle.read(bytes, 0, bytes.length, 0);
836
+ if (bytesRead > 65536) throw new Error("oversized metadata");
837
+ metadata = JSON.parse(bytes.subarray(0, bytesRead).toString("utf8"));
838
+ } finally {
839
+ await handle.close();
840
+ }
841
+ if (metadata?.name !== selected.packageName) throw new Error("wrong package identity");
842
+ } catch {
843
+ throw new Error(`Required Mikie native package ${selected.packageName} metadata is invalid; reinstall the CLI`);
844
+ }
845
+ const wrapperVersion = version ?? require("../package.json").version;
846
+ if (typeof wrapperVersion !== "string" || !SAFE_RUNTIME_VERSION.test(wrapperVersion)
847
+ || metadata.version !== wrapperVersion) {
848
+ throw new Error("Mikie wrapper and native package versions do not match; reinstall the CLI before retrying");
849
+ }
850
+
660
851
  const binary = path.join(path.dirname(packageJson), ...selected.binaryPath.split("/"));
661
852
  try {
662
853
  const metadata = await stat(binary);
package/lib/update.mjs CHANGED
@@ -611,6 +611,38 @@ function reportedForeignSettings(registration, settings, candidateBinary) {
611
611
  return reported;
612
612
  }
613
613
 
614
+ function snapshotHasNoClaudeOwnedState(snapshot) {
615
+ const providers = new Map(snapshot.providerConfig.map((entry) => [entry.name, entry]));
616
+ const registration = parseOptionalJson(providers.get("registration"));
617
+ const settings = parseOptionalJson(providers.get("settings"));
618
+ if (providers.get("ownership")?.existed) return false;
619
+ if (
620
+ registration?.mcpServers &&
621
+ typeof registration.mcpServers === "object" &&
622
+ !Array.isArray(registration.mcpServers) &&
623
+ Object.hasOwn(registration.mcpServers, "mikie")
624
+ ) {
625
+ return false;
626
+ }
627
+ const hooks = settings?.hooks;
628
+ if (!hooks || typeof hooks !== "object" || Array.isArray(hooks)) return true;
629
+ return !Object.entries(hooks).some(([wireEvent, groups]) =>
630
+ Array.isArray(groups) &&
631
+ groups.some((group) =>
632
+ Array.isArray(group?.hooks) &&
633
+ group.hooks.some((handler) => {
634
+ if (handler?.type !== "command" || !Array.isArray(handler.args)) return false;
635
+ const commandEvent = CLAUDE_HOOK_EVENTS.find(([event]) => event === wireEvent)?.[1];
636
+ return (
637
+ (commandEvent && isDeepStrictEqual(handler.args, ["run-claude-hook", commandEvent])) ||
638
+ (["Stop", "StopFailure"].includes(wireEvent) &&
639
+ isDeepStrictEqual(handler.args, ["evidence", "ingest", "--client", "claude"]))
640
+ );
641
+ }),
642
+ ),
643
+ );
644
+ }
645
+
614
646
  export function verifyClaudeV2Contract({
615
647
  candidateBinary,
616
648
  registration,
@@ -826,13 +858,16 @@ export async function runUpdateTransaction({
826
858
  throw new Error("candidate package version verification failed");
827
859
  }
828
860
  finalLauncher = await files.finalLauncher(candidate, resolved.version);
829
- const externalRecovery = await files.recordPreparedCandidate(
861
+ await files.recordPreparedCandidate(
830
862
  snapshot,
831
863
  candidate,
832
864
  finalLauncher,
833
865
  );
834
- recovery = externalRecovery;
835
- output.writeLine(`Recovery: ${externalRecovery}`);
866
+ // The authenticated external recovery program remains in the transaction
867
+ // journal. The installed entrypoint loads it before resolving a runtime;
868
+ // its internal Node/bootstrap payload is not a customer-facing command.
869
+ recovery = "mikie update";
870
+ output.writeLine(`Recovery: ${recovery}`);
836
871
  await files.markProviderPreparing(snapshot);
837
872
  providerPrepared = true;
838
873
 
@@ -886,7 +921,7 @@ export async function runUpdateTransaction({
886
921
  (repairResult.receipt.action === "migrated" ||
887
922
  files.requiresFinalLauncherSettlement?.() === true);
888
923
  // A skipped receipt does not prove that no client still references a
889
- // historical launcher. In particular, Claude can report unavailable
924
+ // historical launcher. In particular, Claude reports foreign_conflict
890
925
  // when hooks/ownership are absent while an MCP-only registration remains.
891
926
  // Retain until a later authenticated reconciliation proves both surfaces
892
927
  // absent/current instead of deleting a possibly referenced runtime.
@@ -3211,10 +3246,14 @@ export function createFilesystemAdapter({
3211
3246
  }
3212
3247
  },
3213
3248
  async verifyClaudeV2({ candidate, snapshot, repair }) {
3214
- if (
3249
+ const refused =
3215
3250
  repair?.action === "refused" &&
3216
- ["foreign_conflict", "unavailable"].includes(repair.before)
3217
- ) {
3251
+ ["foreign_conflict", "unavailable"].includes(repair.before);
3252
+ const provenNoClaudeState =
3253
+ snapshotHasNoClaudeOwnedState(snapshot) &&
3254
+ ((repair?.action === "not_configured" && repair.before === "absent") ||
3255
+ (repair?.action === "already_current" && repair.before === "current"));
3256
+ if (refused || provenNoClaudeState) {
3218
3257
  const after = await Promise.all(
3219
3258
  snapshot.providerConfig.map(async (entry) => ({
3220
3259
  ...entry,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getmikie/cli",
3
- "version": "1.1.121",
3
+ "version": "1.1.124",
4
4
  "description": "Native Mikie relay launcher for MCP clients",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -24,10 +24,9 @@
24
24
  "node": ">=22"
25
25
  },
26
26
  "optionalDependencies": {
27
- "@getmikie/darwin-arm64": "1.1.121",
28
- "@getmikie/darwin-x64": "1.1.121",
29
- "@getmikie/linux-x64-gnu": "1.1.121",
30
- "@getmikie/win32-x64": "1.1.121"
27
+ "@getmikie/darwin-arm64": "1.1.124",
28
+ "@getmikie/linux-x64-gnu": "1.1.124",
29
+ "@getmikie/win32-x64": "1.1.124"
31
30
  },
32
31
  "publishConfig": {
33
32
  "access": "public",
@@ -456,8 +456,7 @@ export async function reconcileInstalledClients({
456
456
  version,
457
457
  markerPath,
458
458
  resolveBinary = resolveNativeBinary,
459
- prepareLauncher = (binary) =>
460
- prepareNativeLaunch(binary, ["setup", "codex"], { platform, version, environment }),
459
+ prepareLauncher,
461
460
  retireLauncher = (launcher, runtimeSha256, preserveSha256) =>
462
461
  retireHistoricalRuntimes(launcher, {
463
462
  expectedSha256: runtimeSha256,
@@ -487,11 +486,21 @@ export async function reconcileInstalledClients({
487
486
  throw new Error("installed client reconciliation authority is invalid");
488
487
  }
489
488
  const markerRoot = path.dirname(scopedMarker);
489
+ const preservedRuntimeSha256 = explicitMarker
490
+ ? []
491
+ : await retainedRuntimeDigests(markerRoot, platform);
492
+ const prepareLauncherForInstall = prepareLauncher ?? ((candidate) =>
493
+ prepareNativeLaunch(candidate, ["setup", "codex"], {
494
+ platform,
495
+ version,
496
+ environment,
497
+ preserveSha256: preservedRuntimeSha256,
498
+ }));
490
499
  if (!explicitMarker) {
491
500
  await retainedRuntimeDigests(markerRoot, platform, {
492
- replacementName: path.basename(scopedMarker),
493
- replacementSha256: authority.runtime_sha256,
494
- });
501
+ replacementName: path.basename(scopedMarker),
502
+ replacementSha256: authority.runtime_sha256,
503
+ });
495
504
  }
496
505
  if (await readClientMarker(scopedMarker, authority)) {
497
506
  const expectedLauncher = materializedRuntimePath({
@@ -510,7 +519,7 @@ export async function reconcileInstalledClients({
510
519
  if ((await sha256File(binary)) !== initialRuntimeSha256) {
511
520
  throw new Error("installed Mikie runtime changed during client reconciliation");
512
521
  }
513
- launcher = await prepareLauncher(binary);
522
+ launcher = await prepareLauncherForInstall(binary);
514
523
  if (
515
524
  pathIdentity(launcher, platform) !== pathIdentity(expectedLauncher, platform) ||
516
525
  (await sha256File(binary)) !== initialRuntimeSha256 ||
@@ -530,7 +539,7 @@ export async function reconcileInstalledClients({
530
539
  }
531
540
  return { status: "current" };
532
541
  }
533
- const finalLauncher = await prepareLauncher(binary);
542
+ const finalLauncher = await prepareLauncherForInstall(binary);
534
543
  if (![binary, finalLauncher].every((value) => typeof value === "string" && path.isAbsolute(value))) {
535
544
  throw new Error("installed client reconciliation authority is invalid");
536
545
  }
@@ -585,6 +594,23 @@ export async function reconcileInstalledClients({
585
594
  return { status: "reconciled", finalLauncher };
586
595
  }
587
596
 
597
+ /**
598
+ * Finish an install whose postinstall was skipped: the same client and
599
+ * trajectory-store reconciliation, once per installed runtime. A current
600
+ * marker makes this a no-op; client repair never blocks the command.
601
+ */
602
+ export async function completeSkippedInstall({
603
+ version,
604
+ reconcileClients = reconcileInstalledClients,
605
+ reconcileTrajectory = reconcileInstalledTrajectory,
606
+ } = {}) {
607
+ const clients = await reconcileClients({ version, allowDeferred: true });
608
+ if (clients.status === "reconciled") {
609
+ await reconcileTrajectory();
610
+ }
611
+ return clients;
612
+ }
613
+
588
614
  const scriptIdentity = pathIdentity(fileURLToPath(import.meta.url), process.platform);
589
615
  const invokedIdentity = pathIdentity(process.argv[1], process.platform);
590
616
  if (scriptIdentity !== null && scriptIdentity === invokedIdentity) {