carmoji 0.3.15 → 0.3.17

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 +233 -66
  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'));
@@ -690,6 +790,19 @@ const TEST_EVENTS = {
690
790
  error: { event: 'error' },
691
791
  permission: { event: 'permission' },
692
792
  end: { event: 'session_end' },
793
+ gamepad: { event: 'standby_activity', activity: 'gamepad' },
794
+ tinkering: { event: 'standby_activity', activity: 'tinkering' },
795
+ wheel: { event: 'standby_activity', activity: 'wheel' },
796
+ puzzle: { event: 'standby_activity', activity: 'puzzle' },
797
+ plant: { event: 'standby_activity', activity: 'plant' },
798
+ blocks: { event: 'standby_activity', activity: 'blocks' },
799
+ plane: { event: 'standby_activity', activity: 'plane' },
800
+ yarn: { event: 'standby_activity', activity: 'yarn' },
801
+ sketch: { event: 'standby_activity', activity: 'sketch' },
802
+ bubbles: { event: 'standby_activity', activity: 'bubbles' },
803
+ snack: { event: 'standby_activity', activity: 'snack' },
804
+ phone: { event: 'standby_activity', activity: 'phone' },
805
+ piano: { event: 'standby_activity', activity: 'piano' },
693
806
  };
694
807
 
695
808
  /** Copy `path` to `path.bak` before modifying it; returns the backup path. */
@@ -749,7 +862,7 @@ const CURSOR_STYLE_EVENTS = [
749
862
  ];
750
863
 
751
864
  /**
752
- * Every tool `carmoji install <name>` knows. `source` defaults to the key;
865
+ * Every tool `carmoji hook <name>` knows. `source` defaults to the key;
753
866
  * `timeoutMs` marks tools whose hook timeouts are milliseconds; `eventFlag`
754
867
  * marks tools whose stdin lacks the event name (the command carries
755
868
  * `--event <name>`); `requiresDir` gates installation on the tool actually
@@ -838,7 +951,7 @@ function installJsonHooks(key, tool) {
838
951
  const hooks = (root.hooks && typeof root.hooks === 'object' && !Array.isArray(root.hooks))
839
952
  ? root.hooks : {};
840
953
  const source = tool.source ?? key;
841
- const base = `${HOOK_COMMAND} --source ${source}`;
954
+ const base = `${runtimeHookCommand()} --source ${source}`;
842
955
  const timeout = (seconds) => (tool.timeoutMs ? seconds * 1000 : seconds);
843
956
 
844
957
  // Remove every old CarMoji entry first, including events no longer in this
@@ -915,7 +1028,7 @@ function installKimiHooks(key, tool) {
915
1028
  const path = join(homedir(), tool.config);
916
1029
  let contents = existsSync(path) ? readFileSync(path, 'utf8') : '';
917
1030
  contents = removeKimiBlocks(contents);
918
- const command = `${HOOK_COMMAND} --source kimi`.replace(/"/g, '\\"');
1031
+ const command = `${runtimeHookCommand()} --source kimi`.replace(/"/g, '\\"');
919
1032
  const blocks = tool.events.map((event) => {
920
1033
  const toolEvent = ['PreToolUse', 'PostToolUse', 'PostToolUseFailure'].includes(event);
921
1034
  return `[[hooks]]\nevent = "${event}"\ncommand = "${command}"\ntimeout = 5`
@@ -935,7 +1048,7 @@ function installClineHooks(key, tool) {
935
1048
  mkdirSync(dir, { recursive: true });
936
1049
  const script = '#!/bin/sh\n'
937
1050
  + '# carmoji hook — forwards Cline events to the CarMoji pet\n'
938
- + `exec ${HOOK_COMMAND} --source cline\n`;
1051
+ + `exec ${runtimeHookCommand()} --source cline\n`;
939
1052
  for (const event of tool.events) {
940
1053
  const path = join(dir, event);
941
1054
  if (existsSync(path) && !readFileSync(path, 'utf8').includes('carmoji')) {
@@ -1071,7 +1184,7 @@ function uninstallTool(key, { skipMissing = false } = {}) {
1071
1184
  console.error(`Unknown tool "${key}". Known: ${Object.keys(TOOLS).join(', ')}`);
1072
1185
  process.exit(1);
1073
1186
  }
1074
- // `uninstall all` sweeps every known tool; stay quiet about the ones that
1187
+ // `unhook all` sweeps every known tool; stay quiet about the ones that
1075
1188
  // were never installed instead of printing 14 "nothing installed" lines.
1076
1189
  const nothing = () => {
1077
1190
  if (!skipMissing) console.log(`${tool.name}: nothing installed.`);
@@ -1119,9 +1232,8 @@ function uninstallTool(key, { skipMissing = false } = {}) {
1119
1232
 
1120
1233
  /**
1121
1234
  * 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.
1235
+ * Existing carmoji entries are replaced, not skipped, so every reinstall
1236
+ * points at the freshly staged runtime in ~/.config/carmoji.
1125
1237
  */
1126
1238
  function installClaude() {
1127
1239
  installJsonHooks('claude', TOOLS.claude);
@@ -1132,9 +1244,9 @@ function installClaude() {
1132
1244
  * Codex versions without hooks.json support), keeping a backup. */
1133
1245
  function installCodexNotify() {
1134
1246
  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"]`;
1247
+ const notifyLine = `notify = ${JSON.stringify([
1248
+ 'node', RUNTIME_SCRIPT_PATH, 'codex-notify',
1249
+ ])}`;
1138
1250
 
1139
1251
  let text = existsSync(path) ? readFileSync(path, 'utf8') : '';
1140
1252
  const lines = text.split('\n');
@@ -1163,7 +1275,7 @@ function claudeConfigSnippet() {
1163
1275
  const hooks = {};
1164
1276
  for (const name of CLAUDE_HOOK_EVENTS) {
1165
1277
  const timeout = name === 'PermissionRequest' ? PERMISSION_HOOK_TIMEOUT_S : 5;
1166
- hooks[name] = [{ hooks: [{ type: 'command', command: HOOK_COMMAND, timeout }] }];
1278
+ hooks[name] = [{ hooks: [{ type: 'command', command: hookCommand(), timeout }] }];
1167
1279
  }
1168
1280
  return JSON.stringify({ hooks }, null, 2);
1169
1281
  }
@@ -1302,6 +1414,27 @@ async function main() {
1302
1414
  }
1303
1415
 
1304
1416
  case 'hook': {
1417
+ // Setup mode overloads the event entry point deliberately:
1418
+ // carmoji hook codex writes Codex's hook config
1419
+ // carmoji hook --source codex receives one event on stdin
1420
+ // Installed hooks always start with a flag, so the two paths cannot
1421
+ // be confused.
1422
+ if (arg && !arg.startsWith('-')) {
1423
+ if (arg !== 'all' && !TOOLS[arg]) {
1424
+ console.error(`Unknown tool "${arg}". Known: ${Object.keys(TOOLS).join(', ')}`);
1425
+ process.exit(1);
1426
+ }
1427
+ installRuntime();
1428
+ if (arg === 'all') {
1429
+ for (const key of Object.keys(TOOLS)) {
1430
+ installTool(key, { skipMissing: true });
1431
+ }
1432
+ break;
1433
+ }
1434
+ installTool(arg);
1435
+ break;
1436
+ }
1437
+
1305
1438
  // Runs inside the coding agent on every hook — must be silent, quick,
1306
1439
  // and always exit 0 so it can never disturb the session. The two
1307
1440
  // deliberate exceptions hold the hook open while the phone answers:
@@ -1631,38 +1764,36 @@ async function main() {
1631
1764
  break;
1632
1765
  }
1633
1766
 
1634
- case 'install': {
1767
+ case 'unhook': {
1635
1768
  if (arg === 'all') {
1636
1769
  for (const key of Object.keys(TOOLS)) {
1637
- installTool(key, { skipMissing: true });
1770
+ uninstallTool(key, { skipMissing: true });
1638
1771
  }
1639
1772
  break;
1640
1773
  }
1641
1774
  if (!arg) {
1642
- console.log(`Usage: carmoji install <tool>|all\nTools: ${Object.keys(TOOLS).join(', ')}`);
1775
+ console.log(`Usage: carmoji unhook <tool>|all\nTools: ${Object.keys(TOOLS).join(', ')}`);
1643
1776
  process.exit(1);
1644
1777
  }
1645
- installTool(arg);
1778
+ uninstallTool(arg);
1646
1779
  break;
1647
1780
  }
1648
1781
 
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(', ')}`);
1782
+ case 'uninstall':
1783
+ if (arg) {
1784
+ console.error('`carmoji uninstall` takes no arguments. Use `carmoji unhook <tool>|all` to remove hooks.');
1658
1785
  process.exit(1);
1659
1786
  }
1660
- uninstallTool(arg);
1787
+ uninstallRuntime();
1788
+ break;
1789
+
1790
+ case 'install':
1791
+ console.error('`carmoji install <tool>|all` was renamed to `carmoji hook <tool>|all`.');
1792
+ process.exit(1);
1661
1793
  break;
1662
- }
1663
1794
 
1664
1795
  case 'tools': {
1665
- console.log('Supported tools (carmoji install <tool>):');
1796
+ console.log('Supported tools (carmoji hook <tool>):');
1666
1797
  for (const [key, tool] of Object.entries(TOOLS)) {
1667
1798
  const where = key === 'codex' ? '$CODEX_HOME/hooks.json' : `~/${tool.config}`;
1668
1799
  const found = isToolPresent(key, tool) ? '' : ' (not detected)';
@@ -1678,16 +1809,23 @@ async function main() {
1678
1809
  else printHelp('config');
1679
1810
  break;
1680
1811
 
1681
- case 'install-codex-notify':
1812
+ case 'hook-codex-notify':
1813
+ installRuntime();
1682
1814
  installCodexNotify();
1683
1815
  break;
1684
1816
 
1685
1817
  // Hidden back-compat aliases — the app's setup sheet and old READMEs
1686
1818
  // print these spellings, so they must keep working.
1819
+ case 'install-codex-notify':
1820
+ installRuntime();
1821
+ installCodexNotify();
1822
+ break;
1687
1823
  case 'install-claude':
1824
+ installRuntime();
1688
1825
  installTool('claude');
1689
1826
  break;
1690
1827
  case 'install-codex':
1828
+ installRuntime();
1691
1829
  installTool('codex');
1692
1830
  break;
1693
1831
  case 'claude-config':
@@ -1707,21 +1845,24 @@ async function main() {
1707
1845
 
1708
1846
  function printClaudeConfig() {
1709
1847
  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');
1848
+ console.log('(Or let `carmoji hook claude` do it for you, with a backup.)\n');
1711
1849
  console.log(claudeConfigSnippet());
1712
1850
  }
1713
1851
 
1714
1852
  function printCodexConfig() {
1715
1853
  console.log('Modern Codex reads hooks from $CODEX_HOME/hooks.json — prefer:');
1716
- console.log(' carmoji install codex\n');
1854
+ console.log(' carmoji hook codex\n');
1717
1855
  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"]`);
1856
+ const program = existsSync(RUNTIME_SCRIPT_PATH)
1857
+ ? ['node', RUNTIME_SCRIPT_PATH, 'codex-notify']
1858
+ : RUNNING_FROM_PACKAGE
1859
+ ? ['npx', '-y', 'carmoji', 'codex-notify']
1860
+ : ['node', SCRIPT_PATH, 'codex-notify'];
1861
+ console.log(`notify = ${JSON.stringify(program)}`);
1721
1862
  }
1722
1863
 
1723
1864
  function printGateConfig() {
1724
- console.log('Legacy/advanced: `carmoji install claude` now installs a PermissionRequest');
1865
+ console.log('Legacy/advanced: `carmoji hook claude` now installs a PermissionRequest');
1725
1866
  console.log('hook that answers real permission prompts from the phone by itself.');
1726
1867
  console.log('The PreToolUse gate below instead fires on EVERY matched tool call, even');
1727
1868
  console.log('allowlisted ones — useful only on older Claude Code versions without the');
@@ -1731,7 +1872,7 @@ function printGateConfig() {
1731
1872
  hooks: {
1732
1873
  PreToolUse: [{
1733
1874
  matcher: 'Bash',
1734
- hooks: [{ type: 'command', command: `${HOOK_COMMAND} --gate`, timeout: 60 }],
1875
+ hooks: [{ type: 'command', command: `${hookCommand()} --gate`, timeout: 60 }],
1735
1876
  }],
1736
1877
  },
1737
1878
  }, null, 2));
@@ -1744,11 +1885,12 @@ const USAGE = `carmoji bridge — talk to the CarMoji app on your phone
1744
1885
  pair <code> [ip[:port]] pair with a phone (code from its Code Buddy sheet)
1745
1886
  unpair [<name|ip[:port]>|all] forget paired devices
1746
1887
  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
1888
+ hook <tool>|all install/update hooks (tools: ${Object.keys(TOOLS).join(', ')})
1889
+ unhook <tool>|all remove CarMoji hooks from a tool's config
1890
+ uninstall remove ~/.config/carmoji/carmoji.js
1749
1891
  tools show each tool's config path and whether it's present
1750
1892
  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)
1893
+ hook [--source <tool>] event entry point used by installed hooks (reads stdin)
1752
1894
  config <claude|codex|gate> print a manual config snippet
1753
1895
  codex-notify legacy Codex notify entry point
1754
1896
 
@@ -1759,11 +1901,20 @@ accepted stdin JSON, \`carmoji test --help\` lists every test event.`;
1759
1901
  function printHelp(topic) {
1760
1902
  switch (topic) {
1761
1903
  case 'hook':
1762
- console.log(`carmoji hook [--source <tool>] [--event <name>] [--gate]
1904
+ console.log(`carmoji hook <tool>|all
1905
+ carmoji hook [--source <tool>] [--event <name>] [--gate]
1906
+
1907
+ With a tool name, installs or updates the network-independent runtime at
1908
+ ~/.config/carmoji/carmoji.js and writes that tool's hook configuration.
1909
+ The \`all\` target hooks every supported tool detected on this machine. Re-running the
1910
+ command updates the runtime and replaces earlier CarMoji-owned entries.
1911
+
1912
+ Tools: ${Object.keys(TOOLS).join(', ')}
1763
1913
 
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.
1914
+ Without a tool name, this is the event entry point used by those installed
1915
+ hooks. It reads ONE hook event as JSON on stdin, forwards it to every paired
1916
+ phone, and exits 0 no matter what so it can never disturb a session.
1917
+ Unpaired event delivery = immediate no-op.
1767
1918
 
1768
1919
  Flags:
1769
1920
  --source <tool> which agent is calling (default: claude) — sets the wire
@@ -1808,7 +1959,8 @@ Try it:
1808
1959
  console.log(`carmoji test <event> [session] [tokens] [project]
1809
1960
 
1810
1961
  Sends a fake event to every paired phone so you can watch the reactions
1811
- without a coding agent.
1962
+ without a coding agent. Leisure activities appear in StandBy, stopped Drive,
1963
+ or idle Code Buddy; if one is already playing, the request plays next.
1812
1964
 
1813
1965
  start wakes up, excited (session_start)
1814
1966
  prompt perks up and listens (prompt)
@@ -1820,6 +1972,19 @@ without a coding agent.
1820
1972
  error dizzy comic collapse (error)
1821
1973
  permission pleading + floating "?" (permission)
1822
1974
  end contented sigh (session_end)
1975
+ gamepad leisure gamepad activity
1976
+ tinkering leisure terminal-tinkering activity
1977
+ wheel steering-wheel preview (never chosen at random)
1978
+ puzzle leisure puzzle cube activity
1979
+ plant leisure plant-care activity
1980
+ blocks leisure building-block activity
1981
+ plane leisure paper-plane activity
1982
+ yarn leisure yarn-ball activity
1983
+ sketch leisure sketch-pad activity
1984
+ bubbles leisure bubble-wand activity
1985
+ snack leisure snack-time activity
1986
+ phone leisure pear-phone activity
1987
+ piano leisure piano activity
1823
1988
  approval pops the Allow/Deny card, waits ${APPROVAL_WAIT_MS / 1000}s, prints your tap
1824
1989
  question pops an option-picker card, waits ${APPROVAL_WAIT_MS / 1000}s, prints your pick
1825
1990
 
@@ -1832,34 +1997,36 @@ without a coding agent.
1832
1997
  case 'config':
1833
1998
  console.log(`carmoji config <claude|codex|gate>
1834
1999
 
1835
- claude the .claude/settings.json hooks snippet (what \`install claude\`
2000
+ claude the .claude/settings.json hooks snippet (what \`hook claude\`
1836
2001
  writes, including the ${PERMISSION_HOOK_TIMEOUT_S}s PermissionRequest hook)
1837
2002
  codex the legacy ~/.codex/config.toml notify line for Codex versions
1838
- without hooks.json (modern Codex: \`carmoji install codex\`)
2003
+ without hooks.json (modern Codex: \`carmoji hook codex\`)
1839
2004
  gate the legacy PreToolUse gate snippet — fires on every matched call;
1840
- superseded by the PermissionRequest hook \`install claude\` sets up`);
2005
+ superseded by the PermissionRequest hook \`hook claude\` sets up`);
1841
2006
  break;
1842
2007
 
1843
2008
  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.`);
2009
+ console.log('`carmoji install <tool>|all` was renamed to `carmoji hook <tool>|all`.');
1852
2010
  break;
1853
2011
 
1854
- case 'uninstall':
1855
- console.log(`carmoji uninstall <tool>|all
2012
+ case 'unhook':
2013
+ console.log(`carmoji unhook <tool>|all
1856
2014
 
1857
2015
  Removes every CarMoji entry from the tool's hook config, leaving other
1858
- hooks untouched. \`all\` sweeps every known tool quietly.
2016
+ hooks untouched. \`all\` sweeps every known tool quietly. It does not remove
2017
+ the persistent runtime; use \`carmoji uninstall\` for that.
1859
2018
 
1860
2019
  Tools: ${Object.keys(TOOLS).join(', ')}`);
1861
2020
  break;
1862
2021
 
2022
+ case 'uninstall':
2023
+ console.log(`carmoji uninstall
2024
+
2025
+ Removes ~/.config/carmoji/carmoji.js. Pairing state and caches are preserved.
2026
+ This does not edit any tool config; run \`carmoji unhook <tool>|all\` first if
2027
+ you also want to remove installed hooks.`);
2028
+ break;
2029
+
1863
2030
  default:
1864
2031
  console.log(USAGE);
1865
2032
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "carmoji",
3
- "version": "0.3.15",
3
+ "version": "0.3.17",
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": {