carmoji 0.3.14 → 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 +218 -67
  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'));
@@ -532,8 +632,19 @@ function eventFromHook(payload) {
532
632
  case 'PostToolUse': return { event: 'tool_end', tool };
533
633
  case 'PostToolUseFailure':
534
634
  case 'Error': return { event: 'error' };
535
- case 'PermissionRequest':
536
- case 'Notification': return { event: 'permission', tool };
635
+ case 'PermissionRequest': return { event: 'permission', tool };
636
+ case 'Notification': {
637
+ // Claude Code routes several nudges through this one hook. Only a
638
+ // real permission prompt deserves the phone's pleading face; the
639
+ // "waiting for your input" idle reminder lands a minute after a
640
+ // *finished* reply, and mapping it to permission flipped "done"
641
+ // into "blocked". Anything not clearly a permission stays a bare
642
+ // heartbeat.
643
+ const note = firstString(payload.message, payload.detail) ?? '';
644
+ return /permission|approv/i.test(note)
645
+ ? { event: 'permission', tool }
646
+ : { event: 'tool_end' };
647
+ }
537
648
  case 'Stop': return { event: 'turn_done' };
538
649
  case 'SessionEnd': return { event: 'session_end' };
539
650
  // Liveness only: keep the session fresh without a face reaction.
@@ -738,7 +849,7 @@ const CURSOR_STYLE_EVENTS = [
738
849
  ];
739
850
 
740
851
  /**
741
- * Every tool `carmoji install <name>` knows. `source` defaults to the key;
852
+ * Every tool `carmoji hook <name>` knows. `source` defaults to the key;
742
853
  * `timeoutMs` marks tools whose hook timeouts are milliseconds; `eventFlag`
743
854
  * marks tools whose stdin lacks the event name (the command carries
744
855
  * `--event <name>`); `requiresDir` gates installation on the tool actually
@@ -827,7 +938,7 @@ function installJsonHooks(key, tool) {
827
938
  const hooks = (root.hooks && typeof root.hooks === 'object' && !Array.isArray(root.hooks))
828
939
  ? root.hooks : {};
829
940
  const source = tool.source ?? key;
830
- const base = `${HOOK_COMMAND} --source ${source}`;
941
+ const base = `${runtimeHookCommand()} --source ${source}`;
831
942
  const timeout = (seconds) => (tool.timeoutMs ? seconds * 1000 : seconds);
832
943
 
833
944
  // Remove every old CarMoji entry first, including events no longer in this
@@ -904,7 +1015,7 @@ function installKimiHooks(key, tool) {
904
1015
  const path = join(homedir(), tool.config);
905
1016
  let contents = existsSync(path) ? readFileSync(path, 'utf8') : '';
906
1017
  contents = removeKimiBlocks(contents);
907
- const command = `${HOOK_COMMAND} --source kimi`.replace(/"/g, '\\"');
1018
+ const command = `${runtimeHookCommand()} --source kimi`.replace(/"/g, '\\"');
908
1019
  const blocks = tool.events.map((event) => {
909
1020
  const toolEvent = ['PreToolUse', 'PostToolUse', 'PostToolUseFailure'].includes(event);
910
1021
  return `[[hooks]]\nevent = "${event}"\ncommand = "${command}"\ntimeout = 5`
@@ -924,7 +1035,7 @@ function installClineHooks(key, tool) {
924
1035
  mkdirSync(dir, { recursive: true });
925
1036
  const script = '#!/bin/sh\n'
926
1037
  + '# carmoji hook — forwards Cline events to the CarMoji pet\n'
927
- + `exec ${HOOK_COMMAND} --source cline\n`;
1038
+ + `exec ${runtimeHookCommand()} --source cline\n`;
928
1039
  for (const event of tool.events) {
929
1040
  const path = join(dir, event);
930
1041
  if (existsSync(path) && !readFileSync(path, 'utf8').includes('carmoji')) {
@@ -1060,7 +1171,7 @@ function uninstallTool(key, { skipMissing = false } = {}) {
1060
1171
  console.error(`Unknown tool "${key}". Known: ${Object.keys(TOOLS).join(', ')}`);
1061
1172
  process.exit(1);
1062
1173
  }
1063
- // `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
1064
1175
  // were never installed instead of printing 14 "nothing installed" lines.
1065
1176
  const nothing = () => {
1066
1177
  if (!skipMissing) console.log(`${tool.name}: nothing installed.`);
@@ -1108,9 +1219,8 @@ function uninstallTool(key, { skipMissing = false } = {}) {
1108
1219
 
1109
1220
  /**
1110
1221
  * Merge the CarMoji hooks into ~/.claude/settings.json, keeping a backup.
1111
- * Existing carmoji entries are replaced, not skipped, so switching between
1112
- * a repo clone (`node …/carmoji.js`) and the npm package (`npx carmoji`)
1113
- * 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.
1114
1224
  */
1115
1225
  function installClaude() {
1116
1226
  installJsonHooks('claude', TOOLS.claude);
@@ -1121,9 +1231,9 @@ function installClaude() {
1121
1231
  * Codex versions without hooks.json support), keeping a backup. */
1122
1232
  function installCodexNotify() {
1123
1233
  const path = join(codexHome(), 'config.toml');
1124
- const notifyLine = RUNNING_FROM_PACKAGE
1125
- ? 'notify = ["npx", "-y", "carmoji", "codex-notify"]'
1126
- : `notify = ["node", "${SCRIPT_PATH}", "codex-notify"]`;
1234
+ const notifyLine = `notify = ${JSON.stringify([
1235
+ 'node', RUNTIME_SCRIPT_PATH, 'codex-notify',
1236
+ ])}`;
1127
1237
 
1128
1238
  let text = existsSync(path) ? readFileSync(path, 'utf8') : '';
1129
1239
  const lines = text.split('\n');
@@ -1152,7 +1262,7 @@ function claudeConfigSnippet() {
1152
1262
  const hooks = {};
1153
1263
  for (const name of CLAUDE_HOOK_EVENTS) {
1154
1264
  const timeout = name === 'PermissionRequest' ? PERMISSION_HOOK_TIMEOUT_S : 5;
1155
- hooks[name] = [{ hooks: [{ type: 'command', command: HOOK_COMMAND, timeout }] }];
1265
+ hooks[name] = [{ hooks: [{ type: 'command', command: hookCommand(), timeout }] }];
1156
1266
  }
1157
1267
  return JSON.stringify({ hooks }, null, 2);
1158
1268
  }
@@ -1291,6 +1401,27 @@ async function main() {
1291
1401
  }
1292
1402
 
1293
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
+
1294
1425
  // Runs inside the coding agent on every hook — must be silent, quick,
1295
1426
  // and always exit 0 so it can never disturb the session. The two
1296
1427
  // deliberate exceptions hold the hook open while the phone answers:
@@ -1620,38 +1751,36 @@ async function main() {
1620
1751
  break;
1621
1752
  }
1622
1753
 
1623
- case 'install': {
1754
+ case 'unhook': {
1624
1755
  if (arg === 'all') {
1625
1756
  for (const key of Object.keys(TOOLS)) {
1626
- installTool(key, { skipMissing: true });
1757
+ uninstallTool(key, { skipMissing: true });
1627
1758
  }
1628
1759
  break;
1629
1760
  }
1630
1761
  if (!arg) {
1631
- 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(', ')}`);
1632
1763
  process.exit(1);
1633
1764
  }
1634
- installTool(arg);
1765
+ uninstallTool(arg);
1635
1766
  break;
1636
1767
  }
1637
1768
 
1638
- case 'uninstall': {
1639
- if (arg === 'all') {
1640
- for (const key of Object.keys(TOOLS)) {
1641
- uninstallTool(key, { skipMissing: true });
1642
- }
1643
- break;
1644
- }
1645
- if (!arg) {
1646
- 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.');
1647
1772
  process.exit(1);
1648
1773
  }
1649
- 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);
1650
1780
  break;
1651
- }
1652
1781
 
1653
1782
  case 'tools': {
1654
- console.log('Supported tools (carmoji install <tool>):');
1783
+ console.log('Supported tools (carmoji hook <tool>):');
1655
1784
  for (const [key, tool] of Object.entries(TOOLS)) {
1656
1785
  const where = key === 'codex' ? '$CODEX_HOME/hooks.json' : `~/${tool.config}`;
1657
1786
  const found = isToolPresent(key, tool) ? '' : ' (not detected)';
@@ -1667,16 +1796,23 @@ async function main() {
1667
1796
  else printHelp('config');
1668
1797
  break;
1669
1798
 
1670
- case 'install-codex-notify':
1799
+ case 'hook-codex-notify':
1800
+ installRuntime();
1671
1801
  installCodexNotify();
1672
1802
  break;
1673
1803
 
1674
1804
  // Hidden back-compat aliases — the app's setup sheet and old READMEs
1675
1805
  // print these spellings, so they must keep working.
1806
+ case 'install-codex-notify':
1807
+ installRuntime();
1808
+ installCodexNotify();
1809
+ break;
1676
1810
  case 'install-claude':
1811
+ installRuntime();
1677
1812
  installTool('claude');
1678
1813
  break;
1679
1814
  case 'install-codex':
1815
+ installRuntime();
1680
1816
  installTool('codex');
1681
1817
  break;
1682
1818
  case 'claude-config':
@@ -1696,21 +1832,24 @@ async function main() {
1696
1832
 
1697
1833
  function printClaudeConfig() {
1698
1834
  console.log('Merge this into the project’s .claude/settings.json (or ~/.claude/settings.json for everywhere):\n');
1699
- 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');
1700
1836
  console.log(claudeConfigSnippet());
1701
1837
  }
1702
1838
 
1703
1839
  function printCodexConfig() {
1704
1840
  console.log('Modern Codex reads hooks from $CODEX_HOME/hooks.json — prefer:');
1705
- console.log(' carmoji install codex\n');
1841
+ console.log(' carmoji hook codex\n');
1706
1842
  console.log('For older Codex versions, add this line to ~/.codex/config.toml:\n');
1707
- console.log(RUNNING_FROM_PACKAGE
1708
- ? 'notify = ["npx", "-y", "carmoji", "codex-notify"]'
1709
- : `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)}`);
1710
1849
  }
1711
1850
 
1712
1851
  function printGateConfig() {
1713
- console.log('Legacy/advanced: `carmoji install claude` now installs a PermissionRequest');
1852
+ console.log('Legacy/advanced: `carmoji hook claude` now installs a PermissionRequest');
1714
1853
  console.log('hook that answers real permission prompts from the phone by itself.');
1715
1854
  console.log('The PreToolUse gate below instead fires on EVERY matched tool call, even');
1716
1855
  console.log('allowlisted ones — useful only on older Claude Code versions without the');
@@ -1720,7 +1859,7 @@ function printGateConfig() {
1720
1859
  hooks: {
1721
1860
  PreToolUse: [{
1722
1861
  matcher: 'Bash',
1723
- hooks: [{ type: 'command', command: `${HOOK_COMMAND} --gate`, timeout: 60 }],
1862
+ hooks: [{ type: 'command', command: `${hookCommand()} --gate`, timeout: 60 }],
1724
1863
  }],
1725
1864
  },
1726
1865
  }, null, 2));
@@ -1733,11 +1872,12 @@ const USAGE = `carmoji bridge — talk to the CarMoji app on your phone
1733
1872
  pair <code> [ip[:port]] pair with a phone (code from its Code Buddy sheet)
1734
1873
  unpair [<name|ip[:port]>|all] forget paired devices
1735
1874
  discover list CarMoji devices on this Wi-Fi
1736
- install <tool>|all install hooks (tools: ${Object.keys(TOOLS).join(', ')})
1737
- 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
1738
1878
  tools show each tool's config path and whether it's present
1739
1879
  test <event> send a fake event; \`test approval\` pops the Allow/Deny card
1740
- hook entry point the agents' hooks invoke (reads JSON on stdin)
1880
+ hook [--source <tool>] event entry point used by installed hooks (reads stdin)
1741
1881
  config <claude|codex|gate> print a manual config snippet
1742
1882
  codex-notify legacy Codex notify entry point
1743
1883
 
@@ -1748,11 +1888,20 @@ accepted stdin JSON, \`carmoji test --help\` lists every test event.`;
1748
1888
  function printHelp(topic) {
1749
1889
  switch (topic) {
1750
1890
  case 'hook':
1751
- 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(', ')}
1752
1900
 
1753
- Entry point the coding agents' hooks invoke: reads ONE hook event as JSON on
1754
- stdin, forwards it to every paired phone, and exits 0 no matter what so it can
1755
- 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.
1756
1905
 
1757
1906
  Flags:
1758
1907
  --source <tool> which agent is calling (default: claude) — sets the wire
@@ -1821,34 +1970,36 @@ without a coding agent.
1821
1970
  case 'config':
1822
1971
  console.log(`carmoji config <claude|codex|gate>
1823
1972
 
1824
- claude the .claude/settings.json hooks snippet (what \`install claude\`
1973
+ claude the .claude/settings.json hooks snippet (what \`hook claude\`
1825
1974
  writes, including the ${PERMISSION_HOOK_TIMEOUT_S}s PermissionRequest hook)
1826
1975
  codex the legacy ~/.codex/config.toml notify line for Codex versions
1827
- without hooks.json (modern Codex: \`carmoji install codex\`)
1976
+ without hooks.json (modern Codex: \`carmoji hook codex\`)
1828
1977
  gate the legacy PreToolUse gate snippet — fires on every matched call;
1829
- superseded by the PermissionRequest hook \`install claude\` sets up`);
1978
+ superseded by the PermissionRequest hook \`hook claude\` sets up`);
1830
1979
  break;
1831
1980
 
1832
1981
  case 'install':
1833
- console.log(`carmoji install <tool>|all
1834
-
1835
- Writes CarMoji's hooks into the tool's own config (a .bak backup is saved
1836
- next to anything modified; reinstalling replaces earlier CarMoji entries).
1837
- \`all\` installs into every tool detected on this machine.
1838
-
1839
- Tools: ${Object.keys(TOOLS).join(', ')}
1840
- \`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`.');
1841
1983
  break;
1842
1984
 
1843
- case 'uninstall':
1844
- console.log(`carmoji uninstall <tool>|all
1985
+ case 'unhook':
1986
+ console.log(`carmoji unhook <tool>|all
1845
1987
 
1846
1988
  Removes every CarMoji entry from the tool's hook config, leaving other
1847
- 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.
1848
1991
 
1849
1992
  Tools: ${Object.keys(TOOLS).join(', ')}`);
1850
1993
  break;
1851
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
+
1852
2003
  default:
1853
2004
  console.log(USAGE);
1854
2005
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "carmoji",
3
- "version": "0.3.14",
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": {