carmoji 0.3.15 → 0.3.16

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 (3) hide show
  1. package/README.md +28 -17
  2. package/carmoji.js +205 -65
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -49,23 +49,25 @@ approval.
49
49
  To get reactions in every project:
50
50
 
51
51
  ```sh
52
- npx carmoji install claude
52
+ npx carmoji hook claude
53
53
  ```
54
54
 
55
55
  merges the hooks into `~/.claude/settings.json`, preserving everything
56
- already there (a backup is saved to `settings.json.bak` first, reruns are
57
- no-ops, and a file that doesn't parse is refused untouched). Prefer to do
56
+ already there (a backup is saved to `settings.json.bak` first, reruns replace
57
+ only CarMoji-owned entries, and a file that doesn't parse is refused untouched). Prefer to do
58
58
  it by hand? `npx carmoji config claude` prints the snippet instead. Every
59
- command answers `--help`; `npx carmoji hook --help` documents the JSON the
60
- hook accepts, and `npx carmoji uninstall <tool>|all` takes it all out again.
59
+ command answers `--help`; `npx carmoji hook --help` documents both setup and
60
+ the JSON event mode used by installed hooks. `npx carmoji unhook <tool>|all`
61
+ removes tool integrations again.
61
62
 
62
63
  ## All the other tools
63
64
 
64
65
  ```sh
65
66
  npx carmoji tools # what's supported, what's detected
66
- npx carmoji install codex # one tool…
67
- npx carmoji install all # …or hooks for everything found on this machine
68
- npx carmoji uninstall <tool>
67
+ npx carmoji hook codex # one tool…
68
+ npx carmoji hook all # …or hooks for everything found on this machine
69
+ npx carmoji unhook <tool> # remove one tool's integration
70
+ npx carmoji uninstall # remove ~/.config/carmoji/carmoji.js
69
71
  ```
70
72
 
71
73
  | Tool | Hooks written to | Notes |
@@ -85,12 +87,22 @@ npx carmoji uninstall <tool>
85
87
 
86
88
  Everything merges non-destructively: a backup is written next to each
87
89
  config, reruns are idempotent, only carmoji-owned entries are ever
88
- removed, and unparseable files are refused untouched. `install all` skips
90
+ removed, and unparseable files are refused untouched. `hook all` skips
89
91
  tools that aren't on the machine.
90
92
 
91
- Codex requires new or changed command hooks to be trusted. After installing,
93
+ Every `carmoji hook <tool>|all` also refreshes a persistent hook runtime
94
+ at `~/.config/carmoji/carmoji.js`, with its Node package metadata and
95
+ dependencies beside it. Installed hooks call that file directly with
96
+ `node`; they do not run `npx`, contact npm, or depend on npx's temporary
97
+ cache during an agent session. Run `npx carmoji hook …` again whenever
98
+ you want to update both the runtime and the selected tool's hooks. Pairing
99
+ state in the same directory is preserved. `npx carmoji uninstall` removes
100
+ the runtime script but preserves pairing state and tool configs; use
101
+ `npx carmoji unhook <tool>|all` for those configs.
102
+
103
+ Codex requires new or changed command hooks to be trusted. After hooking,
92
104
  open `/hooks` in Codex, review the CarMoji commands, and trust them. The
93
- installer removes a direct legacy CarMoji `notify` command, or removes only
105
+ setup command removes a direct legacy CarMoji `notify` command, or removes only
94
106
  CarMoji when another notifier has it chained via `--previous-notify`.
95
107
 
96
108
  All of them funnel through the same `carmoji hook` entry point
@@ -99,7 +111,7 @@ whose payloads lack one), so the pet reacts identically no matter which
99
111
  agent is working — each with its own colored dot on the dashboard.
100
112
 
101
113
  Older Codex versions without hooks support can still use
102
- `npx carmoji install-codex-notify`, which sets the `notify` program in
114
+ `npx carmoji hook-codex-notify`, which sets the `notify` program in
103
115
  `~/.codex/config.toml` (turn completions only). OpenCode users running the
104
116
  community **omo** plugin get events for free — omo fires your Claude Code
105
117
  hooks, which already carry carmoji.
@@ -155,7 +167,7 @@ Recomputed at turn boundaries with a 5-minute cache so hooks stay fast.
155
167
 
156
168
  ## Answer permissions from the phone (Claude Code and Codex)
157
169
 
158
- `carmoji install claude` and `carmoji install codex` hook each agent's
170
+ `carmoji hook claude` and `carmoji hook codex` hook each agent's
159
171
  `PermissionRequest` event, which fires **only when a permission dialog is
160
172
  about to appear** —
161
173
  allowlisted tools and auto-accepted edits never trigger it, so nothing
@@ -185,8 +197,7 @@ matcher (e.g. `Bash`). Unlike `PermissionRequest`, that variant fires on
185
197
  - The app runs an `NWListener` WebSocket server on port 8737, advertised
186
198
  via Bonjour as `_carmoji._tcp`. LAN only; every frame must carry the
187
199
  pairing code.
188
- - `carmoji.js hook` is invoked by the coding agent's hooks with event JSON on
189
- stdin, maps it to a wire event, sends one frame (≤0.9 s budget), and
190
- always exits 0 it can never slow down or break a coding session. When
191
- unpaired it exits immediately.
200
+ - `~/.config/carmoji/carmoji.js hook` is invoked by the coding agent's hooks
201
+ with event JSON on stdin, maps it to wire events, gives each delivery batch
202
+ a 0.9 s budget, and always exits 0. When unpaired it exits immediately.
192
203
  - Pairing state lives in `~/.config/carmoji/config.json`.
package/carmoji.js CHANGED
@@ -9,8 +9,9 @@
9
9
  // whose Code Buddy sheet shows <code>
10
10
  // (pairing is additive — events fan out to every device)
11
11
  // unpair [device] forget a paired device by name or ip[:port], or `all`
12
- // install <tool>|all install hooks for a tool (or every detected one)
13
- // uninstall <tool>|all remove CarMoji hooks from a tool's config
12
+ // hook <tool>|all install hooks for a tool (or every detected one)
13
+ // unhook <tool>|all remove CarMoji hooks from a tool's config
14
+ // uninstall remove the persistent hook runtime
14
15
  // tools list supported tools and where their hooks live
15
16
  // hook hook entry point, all tools (reads hook JSON on stdin;
16
17
  // --source <tool>, --event <name>, --gate)
@@ -25,26 +26,125 @@
25
26
  import {
26
27
  readFileSync, writeFileSync, mkdirSync,
27
28
  statSync, openSync, readSync, closeSync,
28
- copyFileSync, existsSync, readdirSync, unlinkSync, realpathSync,
29
+ copyFileSync, cpSync, existsSync, readdirSync, unlinkSync, realpathSync,
30
+ renameSync,
29
31
  } from 'node:fs';
30
32
  import { spawn } from 'node:child_process';
33
+ import { createRequire } from 'node:module';
31
34
  import { homedir, hostname } from 'node:os';
32
35
  import { join, dirname, basename } from 'node:path';
33
36
  import { fileURLToPath } from 'node:url';
34
37
 
35
38
  const CONFIG_PATH = join(homedir(), '.config', 'carmoji', 'config.json');
39
+ const RUNTIME_DIR = dirname(CONFIG_PATH);
40
+ const RUNTIME_SCRIPT_PATH = join(RUNTIME_DIR, 'carmoji.js');
41
+ const RUNTIME_PACKAGE_PATH = join(RUNTIME_DIR, 'package.json');
42
+ const RUNTIME_NODE_MODULES = join(RUNTIME_DIR, 'node_modules');
36
43
  const SCRIPT_PATH = fileURLToPath(import.meta.url);
37
- // Installed through npm/npx the script lives in an ephemeral cache, so
38
- // printed configs must go through `npx` rather than an absolute path.
44
+ const REQUIRE = createRequire(import.meta.url);
39
45
  const RUNNING_FROM_PACKAGE = SCRIPT_PATH.includes('node_modules')
40
46
  || SCRIPT_PATH.includes('_npx');
41
- const HOOK_COMMAND = RUNNING_FROM_PACKAGE
42
- ? 'npx -y carmoji hook'
43
- : `node "${SCRIPT_PATH}" hook`;
44
47
  // Never make the user's coding session wait on us.
45
48
  const SEND_TIMEOUT_MS = 900;
46
49
  const DISCOVER_TIMEOUT_MS = 10_000;
47
50
 
51
+ function nodeCommand(path, subcommand) {
52
+ return `node ${JSON.stringify(path)} ${subcommand}`;
53
+ }
54
+
55
+ function runtimeHookCommand() {
56
+ return nodeCommand(RUNTIME_SCRIPT_PATH, 'hook');
57
+ }
58
+
59
+ /** Manual config snippets prefer the installed runtime, but remain useful on
60
+ * a fresh machine before the first `hook <tool>` command. */
61
+ function hookCommand() {
62
+ if (existsSync(RUNTIME_SCRIPT_PATH)) return runtimeHookCommand();
63
+ return RUNNING_FROM_PACKAGE
64
+ ? 'npx -y carmoji hook'
65
+ : nodeCommand(SCRIPT_PATH, 'hook');
66
+ }
67
+
68
+ /** Find the npm installation containing CarMoji's runtime dependencies. */
69
+ function dependencyRoot() {
70
+ let path = dirname(REQUIRE.resolve('ws'));
71
+ while (dirname(path) !== path) {
72
+ if (basename(path) === 'node_modules') return path;
73
+ path = dirname(path);
74
+ }
75
+ throw new Error('could not locate CarMoji dependencies');
76
+ }
77
+
78
+ function samePath(left, right) {
79
+ try {
80
+ return realpathSync(left) === realpathSync(right);
81
+ } catch {
82
+ return false;
83
+ }
84
+ }
85
+
86
+ /** Replace one runtime file only after its complete new copy is on disk. */
87
+ function installRuntimeFile(source, destination, fallback) {
88
+ if (samePath(source, destination)) return;
89
+ const temporary = `${destination}.tmp-${process.pid}`;
90
+ try {
91
+ if (source && existsSync(source)) copyFileSync(source, temporary);
92
+ else writeFileSync(temporary, fallback);
93
+ renameSync(temporary, destination);
94
+ } catch (error) {
95
+ try { unlinkSync(temporary); } catch { /* nothing staged */ }
96
+ throw error;
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Install a network-independent hook runtime. `npx carmoji hook …` may run
102
+ * from an ephemeral npm cache, so every setup refreshes the script, its ESM
103
+ * package marker, and the dependencies beside the user's persistent config.
104
+ * Pairing state and transcript caches in the same directory are untouched.
105
+ */
106
+ function installRuntime() {
107
+ mkdirSync(RUNTIME_DIR, { recursive: true });
108
+ const existed = existsSync(RUNTIME_SCRIPT_PATH);
109
+ try {
110
+ const sourceModules = dependencyRoot();
111
+ if (!samePath(sourceModules, RUNTIME_NODE_MODULES)) {
112
+ cpSync(sourceModules, RUNTIME_NODE_MODULES, {
113
+ recursive: true,
114
+ force: true,
115
+ });
116
+ }
117
+ const sourcePackage = join(dirname(SCRIPT_PATH), 'package.json');
118
+ installRuntimeFile(sourcePackage, RUNTIME_PACKAGE_PATH,
119
+ '{"name":"carmoji-runtime","private":true,"type":"module"}\n');
120
+ // Install the entry point last: an old script can use updated dependencies,
121
+ // while a new script must never become visible before its dependencies do.
122
+ installRuntimeFile(SCRIPT_PATH, RUNTIME_SCRIPT_PATH);
123
+ } catch (error) {
124
+ console.error(`Could not install the CarMoji hook runtime in ${RUNTIME_DIR}: ${error.message}`);
125
+ process.exit(1);
126
+ }
127
+ console.log(`${existed ? 'Updated' : 'Installed'} CarMoji hook runtime at ${RUNTIME_SCRIPT_PATH}`);
128
+ }
129
+
130
+ /** Remove only the executable runtime. Pairing state, caches, and staged
131
+ * dependencies stay in place so a later `hook <tool>` can restore it without
132
+ * erasing the user's devices. Tool configs are managed separately by unhook. */
133
+ function uninstallRuntime() {
134
+ if (!existsSync(RUNTIME_SCRIPT_PATH)) {
135
+ console.log(`CarMoji hook runtime is not installed at ${RUNTIME_SCRIPT_PATH}`);
136
+ return;
137
+ }
138
+ try {
139
+ unlinkSync(RUNTIME_SCRIPT_PATH);
140
+ } catch (error) {
141
+ console.error(`Could not remove ${RUNTIME_SCRIPT_PATH}: ${error.message}`);
142
+ process.exit(1);
143
+ }
144
+ console.log(`Removed CarMoji hook runtime from ${RUNTIME_SCRIPT_PATH}`);
145
+ console.log('Run `carmoji unhook <tool>|all` separately to remove tool configuration.');
146
+ }
147
+
48
148
  function loadConfig() {
49
149
  try {
50
150
  return JSON.parse(readFileSync(CONFIG_PATH, 'utf8'));
@@ -749,7 +849,7 @@ const CURSOR_STYLE_EVENTS = [
749
849
  ];
750
850
 
751
851
  /**
752
- * Every tool `carmoji install <name>` knows. `source` defaults to the key;
852
+ * Every tool `carmoji hook <name>` knows. `source` defaults to the key;
753
853
  * `timeoutMs` marks tools whose hook timeouts are milliseconds; `eventFlag`
754
854
  * marks tools whose stdin lacks the event name (the command carries
755
855
  * `--event <name>`); `requiresDir` gates installation on the tool actually
@@ -838,7 +938,7 @@ function installJsonHooks(key, tool) {
838
938
  const hooks = (root.hooks && typeof root.hooks === 'object' && !Array.isArray(root.hooks))
839
939
  ? root.hooks : {};
840
940
  const source = tool.source ?? key;
841
- const base = `${HOOK_COMMAND} --source ${source}`;
941
+ const base = `${runtimeHookCommand()} --source ${source}`;
842
942
  const timeout = (seconds) => (tool.timeoutMs ? seconds * 1000 : seconds);
843
943
 
844
944
  // Remove every old CarMoji entry first, including events no longer in this
@@ -915,7 +1015,7 @@ function installKimiHooks(key, tool) {
915
1015
  const path = join(homedir(), tool.config);
916
1016
  let contents = existsSync(path) ? readFileSync(path, 'utf8') : '';
917
1017
  contents = removeKimiBlocks(contents);
918
- const command = `${HOOK_COMMAND} --source kimi`.replace(/"/g, '\\"');
1018
+ const command = `${runtimeHookCommand()} --source kimi`.replace(/"/g, '\\"');
919
1019
  const blocks = tool.events.map((event) => {
920
1020
  const toolEvent = ['PreToolUse', 'PostToolUse', 'PostToolUseFailure'].includes(event);
921
1021
  return `[[hooks]]\nevent = "${event}"\ncommand = "${command}"\ntimeout = 5`
@@ -935,7 +1035,7 @@ function installClineHooks(key, tool) {
935
1035
  mkdirSync(dir, { recursive: true });
936
1036
  const script = '#!/bin/sh\n'
937
1037
  + '# carmoji hook — forwards Cline events to the CarMoji pet\n'
938
- + `exec ${HOOK_COMMAND} --source cline\n`;
1038
+ + `exec ${runtimeHookCommand()} --source cline\n`;
939
1039
  for (const event of tool.events) {
940
1040
  const path = join(dir, event);
941
1041
  if (existsSync(path) && !readFileSync(path, 'utf8').includes('carmoji')) {
@@ -1071,7 +1171,7 @@ function uninstallTool(key, { skipMissing = false } = {}) {
1071
1171
  console.error(`Unknown tool "${key}". Known: ${Object.keys(TOOLS).join(', ')}`);
1072
1172
  process.exit(1);
1073
1173
  }
1074
- // `uninstall all` sweeps every known tool; stay quiet about the ones that
1174
+ // `unhook all` sweeps every known tool; stay quiet about the ones that
1075
1175
  // were never installed instead of printing 14 "nothing installed" lines.
1076
1176
  const nothing = () => {
1077
1177
  if (!skipMissing) console.log(`${tool.name}: nothing installed.`);
@@ -1119,9 +1219,8 @@ function uninstallTool(key, { skipMissing = false } = {}) {
1119
1219
 
1120
1220
  /**
1121
1221
  * Merge the CarMoji hooks into ~/.claude/settings.json, keeping a backup.
1122
- * Existing carmoji entries are replaced, not skipped, so switching between
1123
- * a repo clone (`node …/carmoji.js`) and the npm package (`npx carmoji`)
1124
- * always leaves the config pointing at whichever one ran the install.
1222
+ * Existing carmoji entries are replaced, not skipped, so every reinstall
1223
+ * points at the freshly staged runtime in ~/.config/carmoji.
1125
1224
  */
1126
1225
  function installClaude() {
1127
1226
  installJsonHooks('claude', TOOLS.claude);
@@ -1132,9 +1231,9 @@ function installClaude() {
1132
1231
  * Codex versions without hooks.json support), keeping a backup. */
1133
1232
  function installCodexNotify() {
1134
1233
  const path = join(codexHome(), 'config.toml');
1135
- const notifyLine = RUNNING_FROM_PACKAGE
1136
- ? 'notify = ["npx", "-y", "carmoji", "codex-notify"]'
1137
- : `notify = ["node", "${SCRIPT_PATH}", "codex-notify"]`;
1234
+ const notifyLine = `notify = ${JSON.stringify([
1235
+ 'node', RUNTIME_SCRIPT_PATH, 'codex-notify',
1236
+ ])}`;
1138
1237
 
1139
1238
  let text = existsSync(path) ? readFileSync(path, 'utf8') : '';
1140
1239
  const lines = text.split('\n');
@@ -1163,7 +1262,7 @@ function claudeConfigSnippet() {
1163
1262
  const hooks = {};
1164
1263
  for (const name of CLAUDE_HOOK_EVENTS) {
1165
1264
  const timeout = name === 'PermissionRequest' ? PERMISSION_HOOK_TIMEOUT_S : 5;
1166
- hooks[name] = [{ hooks: [{ type: 'command', command: HOOK_COMMAND, timeout }] }];
1265
+ hooks[name] = [{ hooks: [{ type: 'command', command: hookCommand(), timeout }] }];
1167
1266
  }
1168
1267
  return JSON.stringify({ hooks }, null, 2);
1169
1268
  }
@@ -1302,6 +1401,27 @@ async function main() {
1302
1401
  }
1303
1402
 
1304
1403
  case 'hook': {
1404
+ // Setup mode overloads the event entry point deliberately:
1405
+ // carmoji hook codex writes Codex's hook config
1406
+ // carmoji hook --source codex receives one event on stdin
1407
+ // Installed hooks always start with a flag, so the two paths cannot
1408
+ // be confused.
1409
+ if (arg && !arg.startsWith('-')) {
1410
+ if (arg !== 'all' && !TOOLS[arg]) {
1411
+ console.error(`Unknown tool "${arg}". Known: ${Object.keys(TOOLS).join(', ')}`);
1412
+ process.exit(1);
1413
+ }
1414
+ installRuntime();
1415
+ if (arg === 'all') {
1416
+ for (const key of Object.keys(TOOLS)) {
1417
+ installTool(key, { skipMissing: true });
1418
+ }
1419
+ break;
1420
+ }
1421
+ installTool(arg);
1422
+ break;
1423
+ }
1424
+
1305
1425
  // Runs inside the coding agent on every hook — must be silent, quick,
1306
1426
  // and always exit 0 so it can never disturb the session. The two
1307
1427
  // deliberate exceptions hold the hook open while the phone answers:
@@ -1631,38 +1751,36 @@ async function main() {
1631
1751
  break;
1632
1752
  }
1633
1753
 
1634
- case 'install': {
1754
+ case 'unhook': {
1635
1755
  if (arg === 'all') {
1636
1756
  for (const key of Object.keys(TOOLS)) {
1637
- installTool(key, { skipMissing: true });
1757
+ uninstallTool(key, { skipMissing: true });
1638
1758
  }
1639
1759
  break;
1640
1760
  }
1641
1761
  if (!arg) {
1642
- console.log(`Usage: carmoji install <tool>|all\nTools: ${Object.keys(TOOLS).join(', ')}`);
1762
+ console.log(`Usage: carmoji unhook <tool>|all\nTools: ${Object.keys(TOOLS).join(', ')}`);
1643
1763
  process.exit(1);
1644
1764
  }
1645
- installTool(arg);
1765
+ uninstallTool(arg);
1646
1766
  break;
1647
1767
  }
1648
1768
 
1649
- case 'uninstall': {
1650
- if (arg === 'all') {
1651
- for (const key of Object.keys(TOOLS)) {
1652
- uninstallTool(key, { skipMissing: true });
1653
- }
1654
- break;
1655
- }
1656
- if (!arg) {
1657
- console.log(`Usage: carmoji uninstall <tool>|all\nTools: ${Object.keys(TOOLS).join(', ')}`);
1769
+ case 'uninstall':
1770
+ if (arg) {
1771
+ console.error('`carmoji uninstall` takes no arguments. Use `carmoji unhook <tool>|all` to remove hooks.');
1658
1772
  process.exit(1);
1659
1773
  }
1660
- uninstallTool(arg);
1774
+ uninstallRuntime();
1775
+ break;
1776
+
1777
+ case 'install':
1778
+ console.error('`carmoji install <tool>|all` was renamed to `carmoji hook <tool>|all`.');
1779
+ process.exit(1);
1661
1780
  break;
1662
- }
1663
1781
 
1664
1782
  case 'tools': {
1665
- console.log('Supported tools (carmoji install <tool>):');
1783
+ console.log('Supported tools (carmoji hook <tool>):');
1666
1784
  for (const [key, tool] of Object.entries(TOOLS)) {
1667
1785
  const where = key === 'codex' ? '$CODEX_HOME/hooks.json' : `~/${tool.config}`;
1668
1786
  const found = isToolPresent(key, tool) ? '' : ' (not detected)';
@@ -1678,16 +1796,23 @@ async function main() {
1678
1796
  else printHelp('config');
1679
1797
  break;
1680
1798
 
1681
- case 'install-codex-notify':
1799
+ case 'hook-codex-notify':
1800
+ installRuntime();
1682
1801
  installCodexNotify();
1683
1802
  break;
1684
1803
 
1685
1804
  // Hidden back-compat aliases — the app's setup sheet and old READMEs
1686
1805
  // print these spellings, so they must keep working.
1806
+ case 'install-codex-notify':
1807
+ installRuntime();
1808
+ installCodexNotify();
1809
+ break;
1687
1810
  case 'install-claude':
1811
+ installRuntime();
1688
1812
  installTool('claude');
1689
1813
  break;
1690
1814
  case 'install-codex':
1815
+ installRuntime();
1691
1816
  installTool('codex');
1692
1817
  break;
1693
1818
  case 'claude-config':
@@ -1707,21 +1832,24 @@ async function main() {
1707
1832
 
1708
1833
  function printClaudeConfig() {
1709
1834
  console.log('Merge this into the project’s .claude/settings.json (or ~/.claude/settings.json for everywhere):\n');
1710
- console.log('(Or let `carmoji install claude` do it for you, with a backup.)\n');
1835
+ console.log('(Or let `carmoji hook claude` do it for you, with a backup.)\n');
1711
1836
  console.log(claudeConfigSnippet());
1712
1837
  }
1713
1838
 
1714
1839
  function printCodexConfig() {
1715
1840
  console.log('Modern Codex reads hooks from $CODEX_HOME/hooks.json — prefer:');
1716
- console.log(' carmoji install codex\n');
1841
+ console.log(' carmoji hook codex\n');
1717
1842
  console.log('For older Codex versions, add this line to ~/.codex/config.toml:\n');
1718
- console.log(RUNNING_FROM_PACKAGE
1719
- ? 'notify = ["npx", "-y", "carmoji", "codex-notify"]'
1720
- : `notify = ["node", "${SCRIPT_PATH}", "codex-notify"]`);
1843
+ const program = existsSync(RUNTIME_SCRIPT_PATH)
1844
+ ? ['node', RUNTIME_SCRIPT_PATH, 'codex-notify']
1845
+ : RUNNING_FROM_PACKAGE
1846
+ ? ['npx', '-y', 'carmoji', 'codex-notify']
1847
+ : ['node', SCRIPT_PATH, 'codex-notify'];
1848
+ console.log(`notify = ${JSON.stringify(program)}`);
1721
1849
  }
1722
1850
 
1723
1851
  function printGateConfig() {
1724
- console.log('Legacy/advanced: `carmoji install claude` now installs a PermissionRequest');
1852
+ console.log('Legacy/advanced: `carmoji hook claude` now installs a PermissionRequest');
1725
1853
  console.log('hook that answers real permission prompts from the phone by itself.');
1726
1854
  console.log('The PreToolUse gate below instead fires on EVERY matched tool call, even');
1727
1855
  console.log('allowlisted ones — useful only on older Claude Code versions without the');
@@ -1731,7 +1859,7 @@ function printGateConfig() {
1731
1859
  hooks: {
1732
1860
  PreToolUse: [{
1733
1861
  matcher: 'Bash',
1734
- hooks: [{ type: 'command', command: `${HOOK_COMMAND} --gate`, timeout: 60 }],
1862
+ hooks: [{ type: 'command', command: `${hookCommand()} --gate`, timeout: 60 }],
1735
1863
  }],
1736
1864
  },
1737
1865
  }, null, 2));
@@ -1744,11 +1872,12 @@ const USAGE = `carmoji bridge — talk to the CarMoji app on your phone
1744
1872
  pair <code> [ip[:port]] pair with a phone (code from its Code Buddy sheet)
1745
1873
  unpair [<name|ip[:port]>|all] forget paired devices
1746
1874
  discover list CarMoji devices on this Wi-Fi
1747
- install <tool>|all install hooks (tools: ${Object.keys(TOOLS).join(', ')})
1748
- uninstall <tool>|all remove CarMoji hooks from a tool's config
1875
+ hook <tool>|all install/update hooks (tools: ${Object.keys(TOOLS).join(', ')})
1876
+ unhook <tool>|all remove CarMoji hooks from a tool's config
1877
+ uninstall remove ~/.config/carmoji/carmoji.js
1749
1878
  tools show each tool's config path and whether it's present
1750
1879
  test <event> send a fake event; \`test approval\` pops the Allow/Deny card
1751
- hook entry point the agents' hooks invoke (reads JSON on stdin)
1880
+ hook [--source <tool>] event entry point used by installed hooks (reads stdin)
1752
1881
  config <claude|codex|gate> print a manual config snippet
1753
1882
  codex-notify legacy Codex notify entry point
1754
1883
 
@@ -1759,11 +1888,20 @@ accepted stdin JSON, \`carmoji test --help\` lists every test event.`;
1759
1888
  function printHelp(topic) {
1760
1889
  switch (topic) {
1761
1890
  case 'hook':
1762
- console.log(`carmoji hook [--source <tool>] [--event <name>] [--gate]
1891
+ console.log(`carmoji hook <tool>|all
1892
+ carmoji hook [--source <tool>] [--event <name>] [--gate]
1893
+
1894
+ With a tool name, installs or updates the network-independent runtime at
1895
+ ~/.config/carmoji/carmoji.js and writes that tool's hook configuration.
1896
+ The \`all\` target hooks every supported tool detected on this machine. Re-running the
1897
+ command updates the runtime and replaces earlier CarMoji-owned entries.
1898
+
1899
+ Tools: ${Object.keys(TOOLS).join(', ')}
1763
1900
 
1764
- Entry point the coding agents' hooks invoke: reads ONE hook event as JSON on
1765
- stdin, forwards it to every paired phone, and exits 0 no matter what so it can
1766
- never disturb a session. Unpaired = immediate no-op.
1901
+ Without a tool name, this is the event entry point used by those installed
1902
+ hooks. It reads ONE hook event as JSON on stdin, forwards it to every paired
1903
+ phone, and exits 0 no matter what so it can never disturb a session.
1904
+ Unpaired event delivery = immediate no-op.
1767
1905
 
1768
1906
  Flags:
1769
1907
  --source <tool> which agent is calling (default: claude) — sets the wire
@@ -1832,34 +1970,36 @@ without a coding agent.
1832
1970
  case 'config':
1833
1971
  console.log(`carmoji config <claude|codex|gate>
1834
1972
 
1835
- claude the .claude/settings.json hooks snippet (what \`install claude\`
1973
+ claude the .claude/settings.json hooks snippet (what \`hook claude\`
1836
1974
  writes, including the ${PERMISSION_HOOK_TIMEOUT_S}s PermissionRequest hook)
1837
1975
  codex the legacy ~/.codex/config.toml notify line for Codex versions
1838
- without hooks.json (modern Codex: \`carmoji install codex\`)
1976
+ without hooks.json (modern Codex: \`carmoji hook codex\`)
1839
1977
  gate the legacy PreToolUse gate snippet — fires on every matched call;
1840
- superseded by the PermissionRequest hook \`install claude\` sets up`);
1978
+ superseded by the PermissionRequest hook \`hook claude\` sets up`);
1841
1979
  break;
1842
1980
 
1843
1981
  case 'install':
1844
- console.log(`carmoji install <tool>|all
1845
-
1846
- Writes CarMoji's hooks into the tool's own config (a .bak backup is saved
1847
- next to anything modified; reinstalling replaces earlier CarMoji entries).
1848
- \`all\` installs into every tool detected on this machine.
1849
-
1850
- Tools: ${Object.keys(TOOLS).join(', ')}
1851
- \`carmoji tools\` shows each one's config path and whether it's present.`);
1982
+ console.log('`carmoji install <tool>|all` was renamed to `carmoji hook <tool>|all`.');
1852
1983
  break;
1853
1984
 
1854
- case 'uninstall':
1855
- console.log(`carmoji uninstall <tool>|all
1985
+ case 'unhook':
1986
+ console.log(`carmoji unhook <tool>|all
1856
1987
 
1857
1988
  Removes every CarMoji entry from the tool's hook config, leaving other
1858
- hooks untouched. \`all\` sweeps every known tool quietly.
1989
+ hooks untouched. \`all\` sweeps every known tool quietly. It does not remove
1990
+ the persistent runtime; use \`carmoji uninstall\` for that.
1859
1991
 
1860
1992
  Tools: ${Object.keys(TOOLS).join(', ')}`);
1861
1993
  break;
1862
1994
 
1995
+ case 'uninstall':
1996
+ console.log(`carmoji uninstall
1997
+
1998
+ Removes ~/.config/carmoji/carmoji.js. Pairing state and caches are preserved.
1999
+ This does not edit any tool config; run \`carmoji unhook <tool>|all\` first if
2000
+ you also want to remove installed hooks.`);
2001
+ break;
2002
+
1863
2003
  default:
1864
2004
  console.log(USAGE);
1865
2005
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "carmoji",
3
- "version": "0.3.15",
3
+ "version": "0.3.16",
4
4
  "description": "Bring your CarMoji pet to life from Claude Code / Codex — a LAN bridge that streams coding events to the CarMoji iOS app",
5
5
  "type": "module",
6
6
  "bin": {