mirror-mind 0.33.4 → 0.35.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 (56) hide show
  1. package/.agents/plugins/marketplace.json +16 -0
  2. package/.pi/skills/mm-build/SKILL.md +1 -1
  3. package/.pi/skills/mm-explore/SKILL.md +1 -1
  4. package/.pi/skills/mm-help/SKILL.md +2 -2
  5. package/.pi/skills/mm-soul/SKILL.md +1 -1
  6. package/AGENTS.md +9 -8
  7. package/README.md +5 -6
  8. package/docs/releases/index.md +2 -0
  9. package/docs/releases/v0.34.0.md +142 -0
  10. package/docs/releases/v0.35.0.md +199 -0
  11. package/package.json +3 -6
  12. package/plugins/mirror-mind/.claude-plugin/plugin.json +1 -1
  13. package/plugins/mirror-mind/agy/pre-invocation.sh +92 -0
  14. package/plugins/mirror-mind/agy/session-start.sh +92 -0
  15. package/plugins/mirror-mind/agy/stop.sh +92 -0
  16. package/plugins/mirror-mind/hooks.json +26 -0
  17. package/plugins/mirror-mind/mcp_config.json +11 -0
  18. package/plugins/mirror-mind/plugin.json +4 -0
  19. package/plugins/mirror-mind/rules/AGENTS.md +189 -0
  20. package/plugins/mirror-mind/skills/mm-build/SKILL.md +1 -1
  21. package/ts/src/conversation/agyTranscript.ts +105 -0
  22. package/ts/src/conversation/backfill.ts +13 -129
  23. package/ts/src/conversation/commandTurn.ts +42 -0
  24. package/ts/src/conversation/deferredCloseTail.ts +154 -0
  25. package/ts/src/conversation/logger.ts +104 -20
  26. package/ts/src/conversation/loggerCli.ts +6 -18
  27. package/ts/src/conversation/loggerRuntime.ts +6 -1
  28. package/ts/src/conversation/runtimePolicy.ts +59 -0
  29. package/ts/src/conversation/sessionComposites.ts +54 -8
  30. package/ts/src/docs/docsLint.ts +14 -1
  31. package/ts/src/frontDoor/routing.ts +3 -2
  32. package/ts/src/frontDoor/runtimeRoute.ts +13 -1
  33. package/ts/src/frontDoor/stdinSource.ts +2 -2
  34. package/ts/src/guards/claudePlugin.ts +113 -2
  35. package/ts/src/guards/packContents.ts +7 -6
  36. package/ts/src/guards/piSourcedSkills.ts +1 -1
  37. package/ts/src/guards/retiredSurfaces.ts +67 -0
  38. package/ts/src/hooks/agy.ts +229 -0
  39. package/ts/src/hooks/claude.ts +16 -4
  40. package/ts/src/hooks/main.ts +17 -31
  41. package/ts/src/hooks/payload.ts +8 -15
  42. package/ts/src/hooks/runtime.ts +11 -10
  43. package/ts/src/providers/config.ts +11 -0
  44. package/ts/src/runtime/diagnose.ts +115 -1
  45. package/ts/src/runtime/pluginWiring.ts +135 -0
  46. package/ts/src/runtime/update.ts +39 -6
  47. package/ts/src/runtime/wiring.ts +42 -46
  48. package/.gemini/hooks/log-assistant.sh +0 -85
  49. package/.gemini/hooks/log-user.sh +0 -85
  50. package/.gemini/hooks/session-end.sh +0 -85
  51. package/.gemini/hooks/session-start.sh +0 -85
  52. package/scripts/codex-hooks/session-end.sh +0 -85
  53. package/scripts/codex-hooks/session-start.sh +0 -85
  54. package/scripts/codex-mirror.sh +0 -48
  55. package/ts/src/hooks/codex.ts +0 -45
  56. package/ts/src/hooks/gemini.ts +0 -126
@@ -0,0 +1,135 @@
1
+ // The plugin wiring readers (CV21.DS4.TS6).
2
+ //
3
+ // Two runtimes hold the plugin outside the install tree. Codex copies it into
4
+ // a versioned cache (`$CODEX_HOME/plugins/cache/mirror-mind/mirror-mind/
5
+ // <version>`; CV21.DS4.US1), so an npm update changes the install but not
6
+ // that copy — the one wiring that goes stale. Claude Code links the plugin in
7
+ // place (`~/.claude/skills/mirror-mind`), so its failure mode is a dangling
8
+ // link, not a stale copy.
9
+ //
10
+ // Both readers take their roots as ARGUMENTS, always. The defaulting —
11
+ // `$CODEX_HOME` else `~/.codex`, and the Claude home — happens once, at the
12
+ // diagnose entry, from the env the caller already carries, never in here.
13
+ // That is the CR123/128/137/142 lesson: a maintainer's machine has a real
14
+ // ~/.codex and a real ~/.claude, and a reader that consults the environment
15
+ // makes every fixture test machine-dependent exactly where maintainers work.
16
+ //
17
+ // Nothing here runs `codex` or mutates anything: Mirror reads directories and
18
+ // prints a command for the person to run. On the installed Codex 0.163.0,
19
+ // `codex plugin` has no update verb (add, list, marketplace, remove), so the
20
+ // level-up command is the re-add — `CODEX_PLUGIN_ADD_COMMAND`, the same
21
+ // constant `mirror init`'s wiring lines print (the CR071 lesson at the
22
+ // string level).
23
+
24
+ import { lstatSync, readdirSync, readFileSync, readlinkSync, statSync } from "node:fs";
25
+ import { join } from "node:path";
26
+ import { compareSemver, parseSemver } from "#util/semver.ts";
27
+
28
+ /** Where Codex keeps its copy, below a Codex home. */
29
+ export const CODEX_PLUGIN_CACHE_SEGMENTS: readonly string[] = [
30
+ "plugins",
31
+ "cache",
32
+ "mirror-mind",
33
+ "mirror-mind",
34
+ ];
35
+
36
+ export interface CodexCacheState {
37
+ /** `absent` covers no cache and a cache with no version-named entry. */
38
+ state: "absent" | "current" | "behind" | "ahead";
39
+ cachePath: string;
40
+ /** The newest version-named entry, by semver — never a string sort. */
41
+ newestCached: string | null;
42
+ }
43
+
44
+ /**
45
+ * Read the Codex plugin cache below `codexHome`, read-only. Entries whose
46
+ * names do not parse as versions (a partial download, an editor's leavings)
47
+ * are ignored; a cache holding only those claims nothing and reads as absent.
48
+ *
49
+ * `ahead` is a state, not a finding: `runtime update` refuses downgrades but
50
+ * `npm install -g mirror-mind@old` does not ask, and the person who
51
+ * downgraded on purpose needs no Codex advice from the older tree.
52
+ */
53
+ export function codexPluginCacheState(
54
+ codexHome: string,
55
+ installedVersion: string,
56
+ ): CodexCacheState {
57
+ const cachePath = join(codexHome, ...CODEX_PLUGIN_CACHE_SEGMENTS);
58
+ let entries: string[];
59
+ try {
60
+ entries = readdirSync(cachePath);
61
+ } catch {
62
+ return { state: "absent", cachePath, newestCached: null };
63
+ }
64
+ let newest: string | null = null;
65
+ for (const entry of entries) {
66
+ if (parseSemver(entry)[0] < 0) continue;
67
+ try {
68
+ if (!statSync(join(cachePath, entry)).isDirectory()) continue;
69
+ } catch {
70
+ continue;
71
+ }
72
+ if (newest === null || compareSemver(entry, newest) > 0) newest = entry;
73
+ }
74
+ if (newest === null) return { state: "absent", cachePath, newestCached: null };
75
+ const order = compareSemver(newest, installedVersion);
76
+ const state = order < 0 ? "behind" : order > 0 ? "ahead" : "current";
77
+ return { state, cachePath, newestCached: newest };
78
+ }
79
+
80
+ export interface ClaudeLinkState {
81
+ /** `healthy` means the target reads — including a plain directory copy. */
82
+ state: "absent" | "healthy" | "dangling";
83
+ linkPath: string;
84
+ /** The link's own target when it is a symlink, else null. */
85
+ target: string | null;
86
+ }
87
+
88
+ /**
89
+ * Read the Claude Code wiring below `claudeHome`. Healthy means the target
90
+ * reads; a link that resolves to *another* install tree is deliberately
91
+ * healthy too (TS6's documented silence) — chasing where it points is a
92
+ * one-line amendment here if that case ever bites.
93
+ */
94
+ export function claudePluginLinkState(claudeHome: string): ClaudeLinkState {
95
+ const linkPath = join(claudeHome, "skills", "mirror-mind");
96
+ let isLink = false;
97
+ try {
98
+ isLink = lstatSync(linkPath).isSymbolicLink();
99
+ } catch {
100
+ return { state: "absent", linkPath, target: null };
101
+ }
102
+ const target = isLink ? readlinkSync(linkPath) : null;
103
+ try {
104
+ statSync(linkPath);
105
+ } catch {
106
+ return { state: "dangling", linkPath, target };
107
+ }
108
+ return { state: "healthy", linkPath, target };
109
+ }
110
+
111
+ /**
112
+ * The install tree's own plugin version, from the generated manifest.
113
+ * `plugins/mirror-mind/.claude-plugin/plugin.json` is written by
114
+ * `claudePlugin.ts` from `packageVersion()`, and the guard holds the
115
+ * committed manifest equal to the generated one — the file is the authority,
116
+ * not an assumption. Reading it FROM DISK is also what makes a post-update
117
+ * hint honest: the process that ran the update is the old version's code,
118
+ * but the tree it reads has already landed the new one.
119
+ */
120
+ export function installedPluginVersion(installRoot: string): string | null {
121
+ try {
122
+ const raw = readFileSync(
123
+ join(installRoot, "plugins", "mirror-mind", ".claude-plugin", "plugin.json"),
124
+ "utf8",
125
+ );
126
+ const parsed: unknown = JSON.parse(raw);
127
+ const version =
128
+ typeof parsed === "object" && parsed !== null
129
+ ? (parsed as Record<string, unknown>).version
130
+ : undefined;
131
+ return typeof version === "string" && version !== "" ? version : null;
132
+ } catch {
133
+ return null;
134
+ }
135
+ }
@@ -26,8 +26,10 @@
26
26
  // chosen once, below; no step asks which lane it is in.
27
27
 
28
28
  import { spawnSync } from "node:child_process";
29
+ import { join } from "node:path";
29
30
  import { verifyBackupArchive } from "#runtime/backup.ts";
30
31
  import type { InstallKind } from "#runtime/installKind.ts";
32
+ import { codexPluginCacheState, installedPluginVersion } from "#runtime/pluginWiring.ts";
31
33
  import { cloneStrategy, defaultGitRunner, type GitRunner } from "#runtime/strategies/clone.ts";
32
34
  import { defaultNpmRunner, type NpmRunner, packageStrategy } from "#runtime/strategies/package.ts";
33
35
  import type { statusAllowsUpdatePreflight } from "#runtime/updateGate.ts";
@@ -37,6 +39,7 @@ import {
37
39
  type UpdateResult,
38
40
  type UpdateStage,
39
41
  } from "#runtime/updatePipeline.ts";
42
+ import { CODEX_PLUGIN_ADD_COMMAND } from "#runtime/wiring.ts";
40
43
  import { commandOnPath } from "#util/paths.ts";
41
44
  import { PROGRAM } from "#util/program.ts";
42
45
  import { shellWord } from "#util/shellWord.ts";
@@ -405,12 +408,42 @@ export function postUpdateHints(
405
408
  env: NodeJS.ProcessEnv,
406
409
  canExecute?: (path: string) => boolean,
407
410
  ): string[] {
408
- if (install.kind !== "clone" || !result.success) return [];
409
- if (commandOnPath(PROGRAM, env, canExecute) !== null) return [];
410
- // A command the person will paste: the path is one shell word (CR104).
411
- return [
412
- `\`${PROGRAM}\` is not on the PATH: run \`npm link\` once in ${shellWord(install.repository)}, so the skills can call it.`,
413
- ];
411
+ if (!result.success) return [];
412
+ const hints: string[] = [];
413
+
414
+ if (install.kind === "clone" && commandOnPath(PROGRAM, env, canExecute) === null) {
415
+ // A command the person will paste: the path is one shell word (CR104).
416
+ hints.push(
417
+ `\`${PROGRAM}\` is not on the PATH: run \`npm link\` once in ${shellWord(install.repository)}, so the skills can call it.`,
418
+ );
419
+ }
420
+
421
+ // CV21.DS4.TS6: Codex runs the plugin copy it cached at install, so the
422
+ // update that just landed did not reach it. The version authority is the
423
+ // LANDED version, read from the just-updated tree's plugin.json -- this
424
+ // process is the old version's code, and its own constant would read a
425
+ // just-staled cache as current.
426
+ const installRoot =
427
+ install.kind === "clone"
428
+ ? install.repository
429
+ : install.kind === "package"
430
+ ? install.root
431
+ : null;
432
+ const home = env.HOME?.trim() || null;
433
+ 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
+ }
443
+ }
444
+ }
445
+
446
+ return hints;
414
447
  }
415
448
 
416
449
  function firstLine(text: string): string {
@@ -17,22 +17,36 @@
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
- // Codex `~/.codex/skills/<dir>` is discovered recursively, and
27
- // `~/.codex/AGENTS.md` is the global instructions file
28
- // (`codex debug prompt-input`, 0.157.0). Two steps, and the
29
- // session is run through the shipped wrapper, since Codex has
30
- // no hooks this integration uses.
20
+ // Codex `codex plugin marketplace add <root>` reads the package's
21
+ // `.agents/plugins/marketplace.json`, and `codex plugin add
22
+ // mirror-mind@mirror-mind` copies the plugin into Codex's cache
23
+ // with its skills, hooks, and MCP server (0.162.1, CV21.DS12.TS2
24
+ // static 5-7; CV21.DS4.US1). Two steps. Codex asks once, at the
25
+ // next session, to trust the plugin's hooks (L1); the Operating
26
+ // Instructions then arrive through its SessionStart hook as a
27
+ // developer message (L4). Until DS4.US1 the two steps were a
28
+ // skills link and an AGENTS.md link, with sessions run through
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
+ //
33
+ // Gemini CLI had an entry here (a skills link, then four hooks merged into
34
+ // `~/.gemini/settings.json` by absolute path) until CV21.DS8.US1 retired the
35
+ // integration; Antigravity is its successor.
31
36
 
32
37
  import { join } from "node:path";
33
38
  import { commandOnPath } from "#util/paths.ts";
34
39
 
35
- export type RuntimeName = "pi" | "claude" | "gemini" | "codex";
40
+ export type RuntimeName = "pi" | "claude" | "codex" | "antigravity";
41
+
42
+ /**
43
+ * The one Codex level-up command (CV21.DS4.TS6). Codex 0.163.0 has no
44
+ * plugin-update verb, so re-adding is how a cached plugin reaches the
45
+ * installed version. One constant so the wiring line `mirror init` prints
46
+ * and the recommendation `runtime diagnose` prints cannot disagree (the
47
+ * CR071 lesson at the string level).
48
+ */
49
+ export const CODEX_PLUGIN_ADD_COMMAND = "codex plugin add mirror-mind@mirror-mind";
36
50
 
37
51
  export interface RuntimeWiring {
38
52
  /** The runtime, as a person names it. */
@@ -43,24 +57,6 @@ export interface RuntimeWiring {
43
57
  readonly steps: readonly ((root: string) => readonly string[])[];
44
58
  }
45
59
 
46
- /** Gemini CLI's four hooks, as the user-level settings.json wants them. */
47
- function geminiHooksSettings(root: string): string {
48
- const hook = (event: string, name: string, script: string) =>
49
- ` "${event}": [{ "hooks": [{ "name": "mirror-${name}", "type": "command", "command": "${join(root, ".gemini/hooks", script)}", "timeout": 30000 }] }]`;
50
- return [
51
- "{",
52
- ' "hooks": {',
53
- [
54
- hook("SessionStart", "session-start", "session-start.sh"),
55
- hook("BeforeAgent", "log-user", "log-user.sh"),
56
- hook("AfterAgent", "log-assistant", "log-assistant.sh"),
57
- hook("SessionEnd", "session-end", "session-end.sh"),
58
- ].join(",\n"),
59
- " }",
60
- "}",
61
- ].join("\n");
62
- }
63
-
64
60
  export const RUNTIME_WIRING: Readonly<Record<RuntimeName, RuntimeWiring>> = {
65
61
  pi: {
66
62
  title: "Pi",
@@ -77,34 +73,34 @@ export const RUNTIME_WIRING: Readonly<Record<RuntimeName, RuntimeWiring>> = {
77
73
  ],
78
74
  ],
79
75
  },
80
- gemini: {
81
- title: "Gemini CLI",
82
- command: "gemini",
76
+ antigravity: {
77
+ title: "Antigravity",
78
+ command: "agy",
83
79
  steps: [
84
- (root) => [`gemini skills link --consent "${join(root, ".pi/skills")}"`],
85
- (root) => [
86
- "merge into ~/.gemini/settings.json:",
87
- ...geminiHooksSettings(root)
88
- .split("\n")
89
- .map((line) => ` ${line}`),
90
- ],
80
+ // CV21.DS7.US1, probed on agy 1.4.2: one command stages the 25 skills
81
+ // and the agy-native layer (the root plugin.json marker makes agy
82
+ // treat the directory as a native plugin; without it, install converts
83
+ // the Claude surfaces and overwrites the layer's hooks file). Per-turn
84
+ // injection and logging land with DS7's TS2/TS3; this line promises
85
+ // the install, nothing more.
86
+ (root) => [`agy plugin install "${root}/plugins/mirror-mind"`],
91
87
  ],
92
88
  },
93
89
  codex: {
94
90
  title: "Codex",
95
91
  command: "codex",
96
92
  steps: [
97
- (root) => [`ln -s "${join(root, ".pi/skills")}" ~/.codex/skills/mirror-mind`],
98
- (root) => [
99
- `ln -s "${join(root, "AGENTS.md")}" ~/.codex/AGENTS.md`,
100
- "(or append that file to the ~/.codex/AGENTS.md you already have)",
101
- `then start Codex through "${join(root, "scripts/codex-mirror.sh")}" instead of codex`,
93
+ (root) => [`codex plugin marketplace add "${root}"`],
94
+ () => [
95
+ CODEX_PLUGIN_ADD_COMMAND,
96
+ "(Codex asks once, at the next session, to trust the plugin's hooks; the plugin",
97
+ "then loads every session: its skills, the hooks, and the MCP server)",
102
98
  ],
103
99
  ],
104
100
  },
105
101
  };
106
102
 
107
- const ORDER: readonly RuntimeName[] = ["pi", "claude", "gemini", "codex"];
103
+ const ORDER: readonly RuntimeName[] = ["pi", "claude", "codex", "antigravity"];
108
104
 
109
105
  /** The runtimes whose command is on the PATH, in display order. */
110
106
  export function detectRuntimes(
@@ -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