mirror-mind 0.34.0 → 0.36.0

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.
Files changed (58) hide show
  1. package/.pi/skills/mm-build/SKILL.md +1 -1
  2. package/.pi/skills/mm-explore/SKILL.md +1 -1
  3. package/.pi/skills/mm-help/SKILL.md +2 -2
  4. package/.pi/skills/mm-soul/SKILL.md +1 -1
  5. package/AGENTS.md +7 -6
  6. package/README.md +5 -6
  7. package/docs/releases/index.md +2 -0
  8. package/docs/releases/v0.35.0.md +199 -0
  9. package/docs/releases/v0.36.0.md +222 -0
  10. package/package.json +2 -4
  11. package/plugins/mirror-mind/.claude-plugin/plugin.json +1 -1
  12. package/plugins/mirror-mind/agy/pre-invocation.sh +92 -0
  13. package/plugins/mirror-mind/agy/session-start.sh +92 -0
  14. package/plugins/mirror-mind/agy/stop.sh +92 -0
  15. package/plugins/mirror-mind/hooks.json +26 -0
  16. package/plugins/mirror-mind/mcp_config.json +11 -0
  17. package/plugins/mirror-mind/plugin.json +14 -0
  18. package/plugins/mirror-mind/rules/AGENTS.md +189 -0
  19. package/plugins/mirror-mind/skills/mm-build/SKILL.md +1 -1
  20. package/ts/src/conversation/agyTranscript.ts +105 -0
  21. package/ts/src/conversation/commandTurn.ts +1 -1
  22. package/ts/src/conversation/deferredCloseTail.ts +2 -2
  23. package/ts/src/conversation/logger.ts +1 -1
  24. package/ts/src/conversation/runtimePolicy.ts +36 -4
  25. package/ts/src/docs/docsLint.ts +14 -1
  26. package/ts/src/extensions/bindings.ts +53 -2
  27. package/ts/src/extensions/catalogCommands.ts +82 -23
  28. package/ts/src/extensions/catalogWrites.ts +12 -0
  29. package/ts/src/extensions/contextRuntime.ts +36 -1
  30. package/ts/src/extensions/dispatch.ts +43 -10
  31. package/ts/src/extensions/extensionPlugin.ts +325 -0
  32. package/ts/src/extensions/manifest.ts +7 -1
  33. package/ts/src/extensions/settings.ts +147 -0
  34. package/ts/src/frontDoor/cli.ts +16 -1
  35. package/ts/src/frontDoor/extensionCatalogRoute.ts +8 -2
  36. package/ts/src/frontDoor/runtimeRoute.ts +11 -0
  37. package/ts/src/frontDoor/stdinSource.ts +2 -2
  38. package/ts/src/guards/claudePlugin.ts +91 -0
  39. package/ts/src/guards/packContents.ts +3 -2
  40. package/ts/src/guards/piSourcedSkills.ts +1 -1
  41. package/ts/src/guards/retiredSurfaces.ts +31 -0
  42. package/ts/src/hooks/agy.ts +229 -0
  43. package/ts/src/hooks/claude.ts +6 -0
  44. package/ts/src/hooks/main.ts +23 -16
  45. package/ts/src/hooks/payload.ts +8 -15
  46. package/ts/src/hooks/runtime.ts +11 -10
  47. package/ts/src/runtime/brokenPipe.ts +52 -0
  48. package/ts/src/runtime/diagnose.ts +122 -13
  49. package/ts/src/runtime/extensionPluginWiring.ts +222 -0
  50. package/ts/src/runtime/pluginWiring.ts +168 -29
  51. package/ts/src/runtime/status.ts +28 -1
  52. package/ts/src/runtime/update.ts +25 -11
  53. package/ts/src/runtime/wiring.ts +149 -35
  54. package/.gemini/hooks/log-assistant.sh +0 -85
  55. package/.gemini/hooks/log-user.sh +0 -85
  56. package/.gemini/hooks/session-end.sh +0 -85
  57. package/.gemini/hooks/session-start.sh +0 -85
  58. package/ts/src/hooks/gemini.ts +0 -128
@@ -17,10 +17,11 @@
17
17
  // the 2026-09-07 decision assigned to CV22.DS10.
18
18
 
19
19
  import { existsSync, readdirSync, realpathSync, statSync } from "node:fs";
20
- import { join, resolve } from "node:path";
20
+ import { basename, join, resolve } from "node:path";
21
21
  import { type Database, openDatabaseReadOnly } from "#db/database.ts";
22
22
  import { KNOWN_MIGRATION_IDS } from "#db/schemaState.ts";
23
23
  import { ExtensionError } from "#extensions/errors.ts";
24
+ import { readExtensionPlugin } from "#extensions/extensionPlugin.ts";
24
25
  import { loadExtensionManifest } from "#extensions/manifest.ts";
25
26
  import { inspectMigrationFiles } from "#extensions/migrations.ts";
26
27
  import { type DbPathEnv, dbNameForEnv, resolveMirrorHome } from "#frontDoor/dbPath.ts";
@@ -61,6 +62,12 @@ export interface ExtensionHealth {
61
62
  unknown_migrations: string[];
62
63
  }
63
64
 
65
+ export interface ExtensionPluginSummary {
66
+ name: string;
67
+ version: string;
68
+ skills: number;
69
+ }
70
+
64
71
  export interface RuntimeStatusReport {
65
72
  version: string;
66
73
  /** How the running front door is installed (CV22.DS10.US3 plateau 4; US2 D3). */
@@ -73,6 +80,8 @@ export interface RuntimeStatusReport {
73
80
  core_migrations: CoreMigrationHealth;
74
81
  extensions: string[];
75
82
  extension_health: ExtensionHealth[];
83
+ /** The home's generated extensions plugin (CV21.DS5.US2), or null when none. */
84
+ extension_plugin: ExtensionPluginSummary | null;
76
85
  clone_role: MarkerValue;
77
86
  memory_env: string;
78
87
  update_channel: MarkerValue;
@@ -425,6 +434,7 @@ export function buildRuntimeStatus(options: BuildStatusOptions = {}): RuntimeSta
425
434
  core_migrations: inspectCoreMigrations(dbPath, dbExists),
426
435
  extensions: listInstalledExtensions(mirrorHome),
427
436
  extension_health: inspectExtensionHealth(mirrorHome, dbPath, dbExists),
437
+ extension_plugin: inspectExtensionPlugin(mirrorHome),
428
438
  clone_role: cloneRoleFor(install, root),
429
439
  memory_env: memoryEnv,
430
440
  update_channel: channelForReport(install, root, env, undefined, options.channel ?? null),
@@ -461,6 +471,18 @@ export function renderCoreMigrations(item: CoreMigrationHealth): string {
461
471
  return `Core migrations: attention needed (${detail})`;
462
472
  }
463
473
 
474
+ /** The generated extensions plugin, read from its manifest; absent is null, not an error. */
475
+ export function inspectExtensionPlugin(mirrorHome: string | null): ExtensionPluginSummary | null {
476
+ if (mirrorHome === null) return null;
477
+ const previous = readExtensionPlugin(mirrorHome);
478
+ if (previous === null) return null;
479
+ return {
480
+ name: `mirror-ext-${basename(mirrorHome)}`,
481
+ version: `0.1.${previous.generation}`,
482
+ skills: Object.keys(previous.files).filter((path) => path.endsWith("/SKILL.md")).length,
483
+ };
484
+ }
485
+
464
486
  /** Port of `_render_extension_health`. */
465
487
  export function renderExtensionHealth(items: readonly ExtensionHealth[]): string[] {
466
488
  if (items.length === 0) return ["Extension health: ready (0 checked)"];
@@ -528,6 +550,11 @@ export function renderRuntimeStatus(
528
550
  lines.push("Installed extensions: 0");
529
551
  }
530
552
  lines.push(...renderExtensionHealth(report.extension_health));
553
+ // Printed only when the home has one, so a report without extensions reads as before.
554
+ if (report.extension_plugin) {
555
+ const { name, version, skills } = report.extension_plugin;
556
+ lines.push(`Extensions plugin: ${name} ${version} (${skills} skill${skills === 1 ? "" : "s"})`);
557
+ }
531
558
  lines.push(...renderCloneRoleLines(report.install, report.clone_role));
532
559
  lines.push(`Update channel: ${report.update_channel.value}`);
533
560
  if (report.update_channel.note) lines.push(`Update channel note: ${report.update_channel.note}`);
@@ -29,7 +29,11 @@ import { spawnSync } from "node:child_process";
29
29
  import { join } from "node:path";
30
30
  import { verifyBackupArchive } from "#runtime/backup.ts";
31
31
  import type { InstallKind } from "#runtime/installKind.ts";
32
- import { codexPluginCacheState, installedPluginVersion } from "#runtime/pluginWiring.ts";
32
+ import {
33
+ codexPluginCacheState,
34
+ grokPluginCopyState,
35
+ installedPluginVersion,
36
+ } from "#runtime/pluginWiring.ts";
33
37
  import { cloneStrategy, defaultGitRunner, type GitRunner } from "#runtime/strategies/clone.ts";
34
38
  import { defaultNpmRunner, type NpmRunner, packageStrategy } from "#runtime/strategies/package.ts";
35
39
  import type { statusAllowsUpdatePreflight } from "#runtime/updateGate.ts";
@@ -39,7 +43,7 @@ import {
39
43
  type UpdateResult,
40
44
  type UpdateStage,
41
45
  } from "#runtime/updatePipeline.ts";
42
- import { CODEX_PLUGIN_ADD_COMMAND } from "#runtime/wiring.ts";
46
+ import { CODEX_PLUGIN_ADD_COMMAND, grokPluginReinstallCommand } from "#runtime/wiring.ts";
43
47
  import { commandOnPath } from "#util/paths.ts";
44
48
  import { PROGRAM } from "#util/program.ts";
45
49
  import { shellWord } from "#util/shellWord.ts";
@@ -431,15 +435,25 @@ export function postUpdateHints(
431
435
  : null;
432
436
  const home = env.HOME?.trim() || null;
433
437
  const codexHome = env.CODEX_HOME?.trim() || (home ? join(home, ".codex") : null);
434
- if (installRoot && codexHome) {
435
- const landed = installedPluginVersion(installRoot);
436
- if (landed) {
437
- const cache = codexPluginCacheState(codexHome, landed);
438
- if (cache.state === "behind") {
439
- hints.push(
440
- `Codex's cached plugin is behind the update (${cache.newestCached} < ${landed}): run \`${CODEX_PLUGIN_ADD_COMMAND}\` to bring it level.`,
441
- );
442
- }
438
+ const grokHome = env.GROK_HOME?.trim() || (home ? join(home, ".grok") : null);
439
+ const landed = installRoot ? installedPluginVersion(installRoot) : null;
440
+ if (installRoot && codexHome && landed) {
441
+ const cache = codexPluginCacheState(codexHome, landed);
442
+ if (cache.state === "behind") {
443
+ hints.push(
444
+ `Codex's cached plugin is behind the update (${cache.newestCached} < ${landed}): run \`${CODEX_PLUGIN_ADD_COMMAND}\` to bring it level.`,
445
+ );
446
+ }
447
+ }
448
+ // CV21.DS10.US1: Grok copies the plugin too, and its update verb leaves a
449
+ // local-path copy as it is; the level-up is uninstall, then install with
450
+ // the path init printed.
451
+ if (installRoot && grokHome && landed) {
452
+ const copy = grokPluginCopyState(grokHome, landed);
453
+ if (copy.state === "behind") {
454
+ hints.push(
455
+ `Grok's copy of the plugin is behind the update (${copy.newestCopied} < ${landed}): run \`${grokPluginReinstallCommand(installRoot)}\` to bring it level.`,
456
+ );
443
457
  }
444
458
  }
445
459
 
@@ -17,12 +17,6 @@
17
17
  // server (`claude plugin list` / `details`, 2.1.283). One step.
18
18
  // The Operating Instructions arrive through the plugin's
19
19
  // SessionStart hook (D13).
20
- // Gemini CLI `gemini skills link --consent <dir>` links every skill in the
21
- // directory into `~/.gemini/skills/` (0.61.0; a nested directory
22
- // symlink is NOT discovered, the user tier is one level deep),
23
- // and hooks live in `~/.gemini/settings.json`, absolute paths.
24
- // Two steps. The Operating Instructions arrive through the
25
- // SessionStart hook (D13).
26
20
  // Codex `codex plugin marketplace add <root>` reads the package's
27
21
  // `.agents/plugins/marketplace.json`, and `codex plugin add
28
22
  // mirror-mind@mirror-mind` copies the plugin into Codex's cache
@@ -33,11 +27,29 @@
33
27
  // developer message (L4). Until DS4.US1 the two steps were a
34
28
  // skills link and an AGENTS.md link, with sessions run through
35
29
  // the shipped wrapper.
30
+ // Antigravity `agy plugin install <plugin dir>` stages the plugin with its
31
+ // agy-native layer in one step (CV21.DS7.US1, agy 1.4.2).
32
+ // Grok Build `grok plugin install <plugin dir> --trust` copies the plugin
33
+ // under `~/.grok/installed-plugins/` and enables it: its skills,
34
+ // the hooks, and the MCP server, with no prompt left to answer
35
+ // (CV21.DS10.US1, probed on grok 1.0.62). Then a link under
36
+ // `~/.grok/rules/` carries the Operating Instructions as a rule
37
+ // Grok loads in every project (~2.2k tokens by Grok's count):
38
+ // Grok shows a SessionStart hook's context on screen only and
39
+ // ignores a prompt hook's (DS12.TS4 L2/L3), so the plugin's
40
+ // session-start delivery of D13 does not reach its model. Two
41
+ // steps. The shapes not chosen -- the Claude link alone (skills,
42
+ // no hooks), a link in `~/.grok/plugins/` (loads nothing until
43
+ // a config entry is hand-written) -- are in decisions.md.
44
+ //
45
+ // Gemini CLI had an entry here (a skills link, then four hooks merged into
46
+ // `~/.gemini/settings.json` by absolute path) until CV21.DS8.US1 retired the
47
+ // integration; Antigravity is its successor.
36
48
 
37
49
  import { join } from "node:path";
38
50
  import { commandOnPath } from "#util/paths.ts";
39
51
 
40
- export type RuntimeName = "pi" | "claude" | "gemini" | "codex";
52
+ export type RuntimeName = "pi" | "claude" | "codex" | "antigravity" | "grok";
41
53
 
42
54
  /**
43
55
  * The one Codex level-up command (CV21.DS4.TS6). Codex 0.163.0 has no
@@ -48,6 +60,29 @@ export type RuntimeName = "pi" | "claude" | "gemini" | "codex";
48
60
  */
49
61
  export const CODEX_PLUGIN_ADD_COMMAND = "codex plugin add mirror-mind@mirror-mind";
50
62
 
63
+ /** Where the plugin directory lives below an installed package. */
64
+ export function pluginDirectory(root: string): string {
65
+ return join(root, "plugins/mirror-mind");
66
+ }
67
+
68
+ /**
69
+ * The one Grok level-up command (CV21.DS10.US1). Grok copies a local-path
70
+ * plugin and keys the copy by the path STRING: `grok plugin update` is a
71
+ * no-op for it (it answers "local symlink, already live" over a real copy,
72
+ * grok 1.0.62), an install over the same string is refused as already
73
+ * installed, and a differently spelt string installs a second copy beside
74
+ * the first. So the level-up is uninstall, then install with the path `init`
75
+ * printed -- a function of the root, so `diagnose` and `update` cannot spell
76
+ * it differently (the CR071 lesson at the string level).
77
+ */
78
+ export function grokPluginReinstallCommand(root: string): string {
79
+ return `grok plugin uninstall mirror-mind --confirm && ${grokPluginInstallCommand(root)}`;
80
+ }
81
+
82
+ function grokPluginInstallCommand(root: string): string {
83
+ return `grok plugin install "${pluginDirectory(root)}" --trust`;
84
+ }
85
+
51
86
  export interface RuntimeWiring {
52
87
  /** The runtime, as a person names it. */
53
88
  readonly title: string;
@@ -57,24 +92,6 @@ export interface RuntimeWiring {
57
92
  readonly steps: readonly ((root: string) => readonly string[])[];
58
93
  }
59
94
 
60
- /** Gemini CLI's four hooks, as the user-level settings.json wants them. */
61
- function geminiHooksSettings(root: string): string {
62
- const hook = (event: string, name: string, script: string) =>
63
- ` "${event}": [{ "hooks": [{ "name": "mirror-${name}", "type": "command", "command": "${join(root, ".gemini/hooks", script)}", "timeout": 30000 }] }]`;
64
- return [
65
- "{",
66
- ' "hooks": {',
67
- [
68
- hook("SessionStart", "session-start", "session-start.sh"),
69
- hook("BeforeAgent", "log-user", "log-user.sh"),
70
- hook("AfterAgent", "log-assistant", "log-assistant.sh"),
71
- hook("SessionEnd", "session-end", "session-end.sh"),
72
- ].join(",\n"),
73
- " }",
74
- "}",
75
- ].join("\n");
76
- }
77
-
78
95
  export const RUNTIME_WIRING: Readonly<Record<RuntimeName, RuntimeWiring>> = {
79
96
  pi: {
80
97
  title: "Pi",
@@ -85,22 +102,48 @@ export const RUNTIME_WIRING: Readonly<Record<RuntimeName, RuntimeWiring>> = {
85
102
  title: "Claude Code",
86
103
  command: "claude",
87
104
  steps: [
105
+ // `mkdir -p` first: a home that has never held a personal skill has no
106
+ // ~/.claude/skills/ yet, and the link alone fails there (CV21.DS5.US2's
107
+ // route, on the extensions plugin's step; the same line, paid here).
88
108
  (root) => [
89
- `ln -s "${join(root, "plugins/mirror-mind")}" ~/.claude/skills/mirror-mind`,
109
+ `mkdir -p ~/.claude/skills && ln -s "${pluginDirectory(root)}" ~/.claude/skills/mirror-mind`,
90
110
  "(the plugin loads every session: its skills, the hooks, and the MCP server)",
91
111
  ],
92
112
  ],
93
113
  },
94
- gemini: {
95
- title: "Gemini CLI",
96
- command: "gemini",
114
+ antigravity: {
115
+ title: "Antigravity",
116
+ command: "agy",
97
117
  steps: [
98
- (root) => [`gemini skills link --consent "${join(root, ".pi/skills")}"`],
118
+ // CV21.DS7.US1, probed on agy 1.4.2: one command stages the 25 skills
119
+ // and the agy-native layer (the root plugin.json marker makes agy
120
+ // treat the directory as a native plugin; without it, install converts
121
+ // the Claude surfaces and overwrites the layer's hooks file). Per-turn
122
+ // injection and logging land with DS7's TS2/TS3; this line promises
123
+ // the install, nothing more.
124
+ (root) => [`agy plugin install "${root}/plugins/mirror-mind"`],
125
+ ],
126
+ },
127
+ grok: {
128
+ title: "Grok Build",
129
+ command: "grok",
130
+ steps: [
131
+ // CV21.DS10.US1, probed on grok 1.0.62: one command copies the plugin
132
+ // and enables it. Consent lives in the line: what --trust activates,
133
+ // and that Grok asks nothing afterwards (Codex's note says Codex asks
134
+ // once; Grok's must say it does not).
135
+ (root) => [
136
+ grokPluginInstallCommand(root),
137
+ "(--trust activates the plugin's hooks and MCP server, and Grok does not ask again;",
138
+ "Grok keeps a copy of the plugin, so after an update `mirror runtime diagnose`",
139
+ "names the command that brings it level)",
140
+ ],
141
+ // The Operating Instructions (D13) as a home-level rule. Grok loads
142
+ // every *.md under ~/.grok/rules/ in every project, link or file.
99
143
  (root) => [
100
- "merge into ~/.gemini/settings.json:",
101
- ...geminiHooksSettings(root)
102
- .split("\n")
103
- .map((line) => ` ${line}`),
144
+ `ln -s "${join(root, "AGENTS.md")}" ~/.grok/rules/mirror-mind.md`,
145
+ "(the Operating Instructions, loaded as a rule in every Grok session, in",
146
+ "every project: about 2.2k tokens per session)",
104
147
  ],
105
148
  ],
106
149
  },
@@ -118,7 +161,7 @@ export const RUNTIME_WIRING: Readonly<Record<RuntimeName, RuntimeWiring>> = {
118
161
  },
119
162
  };
120
163
 
121
- const ORDER: readonly RuntimeName[] = ["pi", "claude", "gemini", "codex"];
164
+ const ORDER: readonly RuntimeName[] = ["pi", "claude", "codex", "antigravity", "grok"];
122
165
 
123
166
  /** The runtimes whose command is on the PATH, in display order. */
124
167
  export function detectRuntimes(
@@ -142,3 +185,74 @@ export function runtimeWiringLines(runtime: RuntimeName, root: string): string[]
142
185
  });
143
186
  return lines;
144
187
  }
188
+
189
+ // ---------------------------------------------------------------------------
190
+ // The per-home extensions plugin (CV21.DS5.US2). A Mirror home's installed
191
+ // extensions reach the four plugin runtimes as one generated plugin per home,
192
+ // `<home>/runtime/plugin/mirror-ext-<user>/`, in the canonical plugin's format
193
+ // but carrying skills only (DS5.TS1's decision). Each runtime takes it the way
194
+ // it takes the canonical plugin, and the three that copy it (Codex, agy, Grok)
195
+ // need one command per change to bring the copy level — measured in TS1.
196
+ // `mirror init` prints none of this: at init there is no extension. The
197
+ // `extensions` verbs print the step a runtime still needs, and `runtime
198
+ // diagnose` names the reinstall for a stale copy. Printing is not automating.
199
+
200
+ export interface ExtensionPluginPaths {
201
+ /** The plugin directory: linked by Claude Code, installed by agy and Grok. */
202
+ readonly dir: string;
203
+ /** The marketplace root Codex adds: the plugin's parent, carrying `.agents/plugins/marketplace.json`. */
204
+ readonly marketplace: string;
205
+ /** `mirror-ext-<user>`: the plugin's name and its marketplace's. */
206
+ readonly name: string;
207
+ }
208
+
209
+ /** The consent line printed under Grok's step: what `--trust` would activate here, which is nothing. */
210
+ export const EXTENSION_PLUGIN_GROK_TRUST_NOTE =
211
+ "(this plugin carries skills only — no hooks and no MCP server for --trust to activate)";
212
+
213
+ /** The one-time wiring per runtime; Pi takes extensions through its own catalog and needs none. */
214
+ export function extensionPluginWiringSteps(
215
+ runtime: RuntimeName,
216
+ paths: ExtensionPluginPaths,
217
+ ): string[] {
218
+ switch (runtime) {
219
+ case "claude":
220
+ // `mkdir -p` first: a home that has never held a personal skill has no
221
+ // ~/.claude/skills/ yet, and the link alone fails there (measured, US2's route).
222
+ return [`mkdir -p ~/.claude/skills && ln -s "${paths.dir}" ~/.claude/skills/${paths.name}`];
223
+ case "codex":
224
+ return [
225
+ `codex plugin marketplace add "${paths.marketplace}"`,
226
+ `codex plugin add ${paths.name}@${paths.name}`,
227
+ ];
228
+ case "antigravity":
229
+ return [`agy plugin install "${paths.dir}"`];
230
+ case "grok":
231
+ return [`grok plugin install "${paths.dir}" --trust`];
232
+ case "pi":
233
+ return [];
234
+ }
235
+ }
236
+
237
+ /**
238
+ * What brings a runtime's copy level after the plugin changed, measured in
239
+ * DS5.TS1: Codex re-adds at the same version when the marketplace serves the
240
+ * live directory; agy installs again; Grok refuses an install over the same
241
+ * path and must uninstall first. Claude Code links and has no copy.
242
+ */
243
+ export function extensionPluginReinstallCommand(
244
+ runtime: RuntimeName,
245
+ paths: ExtensionPluginPaths,
246
+ ): string | null {
247
+ switch (runtime) {
248
+ case "claude":
249
+ case "pi":
250
+ return null;
251
+ case "codex":
252
+ return `codex plugin add ${paths.name}@${paths.name}`;
253
+ case "antigravity":
254
+ return `agy plugin install "${paths.dir}"`;
255
+ case "grok":
256
+ return `grok plugin uninstall ${paths.name} --confirm && grok plugin install "${paths.dir}" --trust`;
257
+ }
258
+ }
@@ -1,85 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Mirror Mind hook wrapper — GENERATED by scripts/generate_hook_wrappers.sh.
3
- # Edit the generator, never this file: every wrapper shares these bytes and
4
- # differs only in the hook name and in how it finds the Mirror entry
5
- # (CV22.DS10.TS5 decision D4; CV22.DS10.US3 decision D5).
6
- set -uo pipefail
7
- HOOK="gemini:log-assistant"
8
-
9
- # Resolve the tree from THIS FILE, never from the cwd: a runtime spawns hooks
10
- # from whatever directory the session is in. The tree is a checkout or the
11
- # installed package; the wrapper's depth to its root is the same in both.
12
- HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
13
- REPO_ROOT="$(cd "$HERE/../.." && pwd -P)"
14
-
15
- # A hook never fails the user's turn, and never fails in silence: when it
16
- # cannot do its job it appends ONE line to <mirror home>/hooks.log and exits 0.
17
- # The home is found the way the core finds it -- MIRROR_HOME, else
18
- # MIRROR_USER -- from the environment when set there, else from the tree's
19
- # .env when the wrapper is in a tree, else from the user's config file
20
- # (~/.config/mirror/env), in the core's order (US3, D3). A GUI launch carries
21
- # neither variable, and the first wrappers then wrote to a home nobody reads.
22
- env_file_value() {
23
- [ -f "$2" ] || return 0
24
- sed -n -E "s/^[[:space:]]*(export[[:space:]]+)?$1[[:space:]]*=[[:space:]]*//p" "$2" \
25
- | tail -n 1 | tr -d "\"'\r"
26
- }
27
- env_value() {
28
- local value=""
29
- if [ -n "${!1+set}" ]; then printf '%s' "${!1}"; return; fi
30
- [ -z "$REPO_ROOT" ] || value="$(env_file_value "$1" "$REPO_ROOT/.env")"
31
- [ -n "$value" ] || value="$(env_file_value "$1" "${XDG_CONFIG_HOME:-$HOME/.config}/mirror/env")"
32
- printf '%s' "$value"
33
- }
34
- note() {
35
- local home user
36
- home="$(env_value MIRROR_HOME)"
37
- user="$(env_value MIRROR_USER)"
38
- if [ -n "$home" ]; then
39
- home="${home/#\~/$HOME}"
40
- elif [ -n "$user" ]; then
41
- home="$HOME/.mirror-minds/$user"
42
- [ -d "$home" ] || [ ! -d "$HOME/.mirror/$user" ] || home="$HOME/.mirror/$user"
43
- else
44
- return 0
45
- fi
46
- mkdir -p "$home" 2>/dev/null \
47
- && printf '%s %s: %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$HOOK" "$1" >> "$home/hooks.log" 2>/dev/null
48
- }
49
-
50
- ENTRY="$REPO_ROOT/bin/mirror-hook.js"
51
-
52
- # Find Node. /usr/bin/python3 exists on every macOS; node usually lives under
53
- # nvm or Homebrew and is NOT on the PATH of a GUI-launched runtime. The Python
54
- # hooks could end in `|| true` because they always found their interpreter;
55
- # the same silence here would hide a new failure — no logging, no inject, no
56
- # close tail — that a user would meet weeks later as "Mirror stopped
57
- # remembering". So an unresolvable Node is RECORDED, never swallowed. The
58
- # directory the entry was found in is searched first: an npm global bin holds
59
- # the `node` that installed the package beside `mirror-hook`, while a system
60
- # `/usr/local/bin/node` can be years older (CI's Ubuntu runner has one).
61
- NODE="${MIRROR_NODE:-}"
62
- if [ -z "$NODE" ] || [ ! -x "$NODE" ]; then
63
- NODE="$(command -v node 2>/dev/null || true)"
64
- fi
65
- for candidate in "$(dirname "$ENTRY")/node" "$HOME/.nvm/current/bin/node" /opt/homebrew/bin/node /usr/local/bin/node; do
66
- [ -n "$NODE" ] && break
67
- [ -x "$candidate" ] && NODE="$candidate"
68
- done
69
-
70
- if [ -z "$NODE" ]; then
71
- note "node not found on PATH; hook skipped. Set MIRROR_NODE."
72
- exit 0
73
- fi
74
-
75
- # Not `exec`: the entry exits 0 whatever happens inside it, so a non-zero exit
76
- # means Node itself could not run the hook -- too old for the loader shim, or
77
- # it crashed. That used to vanish into a stderr no runtime shows (TS5 handoff
78
- # review, finding N1). No flags: the entry reads configuration and silences
79
- # its own warnings (US3, D3).
80
- "$NODE" "$ENTRY" "$HOOK" "$@"
81
- status=$?
82
- if [ "$status" -ne 0 ]; then
83
- note "node exited $status before the hook finished ($NODE, $("$NODE" --version 2>/dev/null || echo 'version unknown')); Mirror needs Node 24 or later. Set MIRROR_NODE."
84
- fi
85
- exit 0
@@ -1,85 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Mirror Mind hook wrapper — GENERATED by scripts/generate_hook_wrappers.sh.
3
- # Edit the generator, never this file: every wrapper shares these bytes and
4
- # differs only in the hook name and in how it finds the Mirror entry
5
- # (CV22.DS10.TS5 decision D4; CV22.DS10.US3 decision D5).
6
- set -uo pipefail
7
- HOOK="gemini:log-user"
8
-
9
- # Resolve the tree from THIS FILE, never from the cwd: a runtime spawns hooks
10
- # from whatever directory the session is in. The tree is a checkout or the
11
- # installed package; the wrapper's depth to its root is the same in both.
12
- HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
13
- REPO_ROOT="$(cd "$HERE/../.." && pwd -P)"
14
-
15
- # A hook never fails the user's turn, and never fails in silence: when it
16
- # cannot do its job it appends ONE line to <mirror home>/hooks.log and exits 0.
17
- # The home is found the way the core finds it -- MIRROR_HOME, else
18
- # MIRROR_USER -- from the environment when set there, else from the tree's
19
- # .env when the wrapper is in a tree, else from the user's config file
20
- # (~/.config/mirror/env), in the core's order (US3, D3). A GUI launch carries
21
- # neither variable, and the first wrappers then wrote to a home nobody reads.
22
- env_file_value() {
23
- [ -f "$2" ] || return 0
24
- sed -n -E "s/^[[:space:]]*(export[[:space:]]+)?$1[[:space:]]*=[[:space:]]*//p" "$2" \
25
- | tail -n 1 | tr -d "\"'\r"
26
- }
27
- env_value() {
28
- local value=""
29
- if [ -n "${!1+set}" ]; then printf '%s' "${!1}"; return; fi
30
- [ -z "$REPO_ROOT" ] || value="$(env_file_value "$1" "$REPO_ROOT/.env")"
31
- [ -n "$value" ] || value="$(env_file_value "$1" "${XDG_CONFIG_HOME:-$HOME/.config}/mirror/env")"
32
- printf '%s' "$value"
33
- }
34
- note() {
35
- local home user
36
- home="$(env_value MIRROR_HOME)"
37
- user="$(env_value MIRROR_USER)"
38
- if [ -n "$home" ]; then
39
- home="${home/#\~/$HOME}"
40
- elif [ -n "$user" ]; then
41
- home="$HOME/.mirror-minds/$user"
42
- [ -d "$home" ] || [ ! -d "$HOME/.mirror/$user" ] || home="$HOME/.mirror/$user"
43
- else
44
- return 0
45
- fi
46
- mkdir -p "$home" 2>/dev/null \
47
- && printf '%s %s: %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$HOOK" "$1" >> "$home/hooks.log" 2>/dev/null
48
- }
49
-
50
- ENTRY="$REPO_ROOT/bin/mirror-hook.js"
51
-
52
- # Find Node. /usr/bin/python3 exists on every macOS; node usually lives under
53
- # nvm or Homebrew and is NOT on the PATH of a GUI-launched runtime. The Python
54
- # hooks could end in `|| true` because they always found their interpreter;
55
- # the same silence here would hide a new failure — no logging, no inject, no
56
- # close tail — that a user would meet weeks later as "Mirror stopped
57
- # remembering". So an unresolvable Node is RECORDED, never swallowed. The
58
- # directory the entry was found in is searched first: an npm global bin holds
59
- # the `node` that installed the package beside `mirror-hook`, while a system
60
- # `/usr/local/bin/node` can be years older (CI's Ubuntu runner has one).
61
- NODE="${MIRROR_NODE:-}"
62
- if [ -z "$NODE" ] || [ ! -x "$NODE" ]; then
63
- NODE="$(command -v node 2>/dev/null || true)"
64
- fi
65
- for candidate in "$(dirname "$ENTRY")/node" "$HOME/.nvm/current/bin/node" /opt/homebrew/bin/node /usr/local/bin/node; do
66
- [ -n "$NODE" ] && break
67
- [ -x "$candidate" ] && NODE="$candidate"
68
- done
69
-
70
- if [ -z "$NODE" ]; then
71
- note "node not found on PATH; hook skipped. Set MIRROR_NODE."
72
- exit 0
73
- fi
74
-
75
- # Not `exec`: the entry exits 0 whatever happens inside it, so a non-zero exit
76
- # means Node itself could not run the hook -- too old for the loader shim, or
77
- # it crashed. That used to vanish into a stderr no runtime shows (TS5 handoff
78
- # review, finding N1). No flags: the entry reads configuration and silences
79
- # its own warnings (US3, D3).
80
- "$NODE" "$ENTRY" "$HOOK" "$@"
81
- status=$?
82
- if [ "$status" -ne 0 ]; then
83
- note "node exited $status before the hook finished ($NODE, $("$NODE" --version 2>/dev/null || echo 'version unknown')); Mirror needs Node 24 or later. Set MIRROR_NODE."
84
- fi
85
- exit 0
@@ -1,85 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Mirror Mind hook wrapper — GENERATED by scripts/generate_hook_wrappers.sh.
3
- # Edit the generator, never this file: every wrapper shares these bytes and
4
- # differs only in the hook name and in how it finds the Mirror entry
5
- # (CV22.DS10.TS5 decision D4; CV22.DS10.US3 decision D5).
6
- set -uo pipefail
7
- HOOK="gemini:session-end"
8
-
9
- # Resolve the tree from THIS FILE, never from the cwd: a runtime spawns hooks
10
- # from whatever directory the session is in. The tree is a checkout or the
11
- # installed package; the wrapper's depth to its root is the same in both.
12
- HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
13
- REPO_ROOT="$(cd "$HERE/../.." && pwd -P)"
14
-
15
- # A hook never fails the user's turn, and never fails in silence: when it
16
- # cannot do its job it appends ONE line to <mirror home>/hooks.log and exits 0.
17
- # The home is found the way the core finds it -- MIRROR_HOME, else
18
- # MIRROR_USER -- from the environment when set there, else from the tree's
19
- # .env when the wrapper is in a tree, else from the user's config file
20
- # (~/.config/mirror/env), in the core's order (US3, D3). A GUI launch carries
21
- # neither variable, and the first wrappers then wrote to a home nobody reads.
22
- env_file_value() {
23
- [ -f "$2" ] || return 0
24
- sed -n -E "s/^[[:space:]]*(export[[:space:]]+)?$1[[:space:]]*=[[:space:]]*//p" "$2" \
25
- | tail -n 1 | tr -d "\"'\r"
26
- }
27
- env_value() {
28
- local value=""
29
- if [ -n "${!1+set}" ]; then printf '%s' "${!1}"; return; fi
30
- [ -z "$REPO_ROOT" ] || value="$(env_file_value "$1" "$REPO_ROOT/.env")"
31
- [ -n "$value" ] || value="$(env_file_value "$1" "${XDG_CONFIG_HOME:-$HOME/.config}/mirror/env")"
32
- printf '%s' "$value"
33
- }
34
- note() {
35
- local home user
36
- home="$(env_value MIRROR_HOME)"
37
- user="$(env_value MIRROR_USER)"
38
- if [ -n "$home" ]; then
39
- home="${home/#\~/$HOME}"
40
- elif [ -n "$user" ]; then
41
- home="$HOME/.mirror-minds/$user"
42
- [ -d "$home" ] || [ ! -d "$HOME/.mirror/$user" ] || home="$HOME/.mirror/$user"
43
- else
44
- return 0
45
- fi
46
- mkdir -p "$home" 2>/dev/null \
47
- && printf '%s %s: %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$HOOK" "$1" >> "$home/hooks.log" 2>/dev/null
48
- }
49
-
50
- ENTRY="$REPO_ROOT/bin/mirror-hook.js"
51
-
52
- # Find Node. /usr/bin/python3 exists on every macOS; node usually lives under
53
- # nvm or Homebrew and is NOT on the PATH of a GUI-launched runtime. The Python
54
- # hooks could end in `|| true` because they always found their interpreter;
55
- # the same silence here would hide a new failure — no logging, no inject, no
56
- # close tail — that a user would meet weeks later as "Mirror stopped
57
- # remembering". So an unresolvable Node is RECORDED, never swallowed. The
58
- # directory the entry was found in is searched first: an npm global bin holds
59
- # the `node` that installed the package beside `mirror-hook`, while a system
60
- # `/usr/local/bin/node` can be years older (CI's Ubuntu runner has one).
61
- NODE="${MIRROR_NODE:-}"
62
- if [ -z "$NODE" ] || [ ! -x "$NODE" ]; then
63
- NODE="$(command -v node 2>/dev/null || true)"
64
- fi
65
- for candidate in "$(dirname "$ENTRY")/node" "$HOME/.nvm/current/bin/node" /opt/homebrew/bin/node /usr/local/bin/node; do
66
- [ -n "$NODE" ] && break
67
- [ -x "$candidate" ] && NODE="$candidate"
68
- done
69
-
70
- if [ -z "$NODE" ]; then
71
- note "node not found on PATH; hook skipped. Set MIRROR_NODE."
72
- exit 0
73
- fi
74
-
75
- # Not `exec`: the entry exits 0 whatever happens inside it, so a non-zero exit
76
- # means Node itself could not run the hook -- too old for the loader shim, or
77
- # it crashed. That used to vanish into a stderr no runtime shows (TS5 handoff
78
- # review, finding N1). No flags: the entry reads configuration and silences
79
- # its own warnings (US3, D3).
80
- "$NODE" "$ENTRY" "$HOOK" "$@"
81
- status=$?
82
- if [ "$status" -ne 0 ]; then
83
- note "node exited $status before the hook finished ($NODE, $("$NODE" --version 2>/dev/null || echo 'version unknown')); Mirror needs Node 24 or later. Set MIRROR_NODE."
84
- fi
85
- exit 0