carmoji 0.3.3 → 0.3.5

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 +27 -21
  2. package/carmoji.js +324 -64
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -46,18 +46,18 @@ approval.
46
46
 
47
47
  ## Claude Code
48
48
 
49
- Coding in *this* repo works out of the box — the hooks ship in
50
- [.claude/settings.json](../.claude/settings.json). To get reactions in
51
- every project:
49
+ To get reactions in every project:
52
50
 
53
51
  ```sh
54
- npx carmoji install-claude
52
+ npx carmoji install claude
55
53
  ```
56
54
 
57
55
  merges the hooks into `~/.claude/settings.json`, preserving everything
58
56
  already there (a backup is saved to `settings.json.bak` first, reruns are
59
57
  no-ops, and a file that doesn't parse is refused untouched). Prefer to do
60
- it by hand? `npx carmoji claude-config` prints the snippet instead.
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.
61
61
 
62
62
  ## All the other tools
63
63
 
@@ -111,6 +111,7 @@ npx carmoji test start # wakes up, excited
111
111
  npx carmoji test edit # focused typing face
112
112
  npx carmoji test run # nervous side-eye
113
113
  npx carmoji test permission # pleading + floating "?"
114
+ npx carmoji test approval # Allow/Deny card; waits and prints your tap
114
115
  npx carmoji test done # celebration with hearts
115
116
  npx carmoji test error # dizzy collapse
116
117
  npx carmoji test end # contented sigh
@@ -152,22 +153,27 @@ cache-write tokens; cache reads excluded). It's your real consumption, not
152
153
  an official percentage of plan quota — there's no public API for that.
153
154
  Recomputed at turn boundaries with a 5-minute cache so hooks stay fast.
154
155
 
155
- ## Answer permissions from the phone (Claude Code, opt-in)
156
-
157
- ```sh
158
- npx carmoji gate-config
159
- ```
160
-
161
- prints a `PreToolUse` hook snippet with a tool matcher (e.g. `Bash`). With
162
- it installed, whenever Claude wants to run a matched tool the phone chimes
163
- (a doorbell sound used by nothing else), shows big pleading eyes, and pops
164
- an **Allow / Deny** card with the command text your tap settles the
165
- permission on the computer via the hook's official decision output. No
166
- answer within ~55 s falls through to the normal terminal prompt.
167
-
168
- Honest limits: this decides *tool permissions* only hooks expose no way
169
- to answer AskUserQuestion option pickers — and the Codex integration currently
170
- treats `PermissionRequest` as display-only.
156
+ ## Answer permissions from the phone (Claude Code)
157
+
158
+ `carmoji install claude` hooks Claude Code's `PermissionRequest` event,
159
+ which fires **only when a permission dialog is about to appear** —
160
+ allowlisted tools and auto-accepted edits never trigger it, so nothing
161
+ ever slows down a call that would have run anyway. When it fires, the
162
+ phone chimes (a doorbell sound used by nothing else), shows big pleading
163
+ eyes, and pops an **Allow / Deny** card with the command text — your tap
164
+ settles the prompt on the computer via the hook's official decision
165
+ output. No answer within ~55 s falls through to the normal terminal
166
+ prompt (`npx carmoji test approval` exercises the whole loop).
167
+
168
+ Honest limits: this decides *tool permissions* only — AskUserQuestion
169
+ option pickers show as a pleading toast but must be answered in the
170
+ terminal — and the Codex integration currently treats `PermissionRequest`
171
+ as display-only (answering from the phone is planned next).
172
+
173
+ Older Claude Code versions without the `PermissionRequest` hook can use
174
+ `npx carmoji config gate` instead: a legacy `PreToolUse` gate with a tool
175
+ matcher (e.g. `Bash`). Unlike `PermissionRequest`, that variant fires on
176
+ *every* matched call, allowlisted or not, so keep its matcher narrow.
171
177
 
172
178
  ## How it works
173
179
 
package/carmoji.js CHANGED
@@ -5,18 +5,22 @@
5
5
  // address + pairing code in ~/.config/carmoji/config.json.
6
6
  //
7
7
  // Commands:
8
- // pair <code> discover the phone on this Wi-Fi and pair with it
8
+ // pair <code> discover phones on this Wi-Fi and pair with the one
9
+ // whose Code Buddy sheet shows <code>
9
10
  // (pairing is additive — events fan out to every device)
10
11
  // unpair [device] forget a paired device by name or ip[:port], or `all`
11
- // install <tool> install hooks for a tool (or `all`); see `tools`
12
- // uninstall <tool> remove CarMoji hooks from a tool's config
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
13
14
  // tools list supported tools and where their hooks live
14
15
  // hook hook entry point, all tools (reads hook JSON on stdin;
15
16
  // --source <tool>, --event <name>, --gate)
16
17
  // codex-notify legacy Codex notify entry point (event JSON as last arg)
17
- // test <name> send a test event: start|prompt|edit|read|run|heartbeat|done|error|permission|end
18
- // claude-config print the .claude/settings.json hooks snippet
19
- // codex-config print the ~/.codex/config.toml notify line (legacy)
18
+ // test <name> send a test event; `test approval` pops the Allow/Deny card
19
+ // config <what> print a manual config snippet: claude | codex | gate
20
+ //
21
+ // Every command answers --help; `carmoji hook --help` documents the stdin
22
+ // JSON it accepts. Old command spellings (install-claude, claude-config, …)
23
+ // keep working as hidden aliases because shipped app versions print them.
20
24
 
21
25
  import {
22
26
  readFileSync, writeFileSync, mkdirSync,
@@ -170,8 +174,8 @@ async function askTTY(question) {
170
174
  }
171
175
 
172
176
  /** Let the user pick between several discovered phones. */
173
- async function chooseDevice(devices) {
174
- console.log('Multiple CarMoji devices found:');
177
+ async function chooseDevice(devices, heading = 'Multiple CarMoji devices found:') {
178
+ console.log(heading);
175
179
  devices.forEach((d, i) => console.log(` ${i + 1}. ${d.name} — ${d.host}:${d.port}`));
176
180
  if (!process.stdin.isTTY) {
177
181
  console.error('\nRe-run with the address of the one you want: carmoji pair <code> <ip[:port]>');
@@ -278,6 +282,7 @@ function requestApprovalAll(devices, message, timeoutMs) {
278
282
  function summarizeToolInput(payload) {
279
283
  const input = payload.tool_input || {};
280
284
  const text = input.command || input.file_path || input.description
285
+ || input.questions?.[0]?.question
281
286
  || JSON.stringify(input);
282
287
  return String(text).replace(/\s+/g, ' ').slice(0, 140);
283
288
  }
@@ -547,7 +552,20 @@ function backupFile(path) {
547
552
  }
548
553
 
549
554
  const CLAUDE_HOOK_EVENTS = ['SessionStart', 'UserPromptSubmit', 'PreToolUse',
550
- 'PostToolUse', 'Notification', 'Stop', 'SessionEnd'];
555
+ 'PermissionRequest', 'PostToolUse', 'Notification',
556
+ 'Stop', 'SessionEnd'];
557
+
558
+ // PermissionRequest fires only when a permission dialog is about to appear,
559
+ // and the dialog waits for the hook — so the phone gets a bounded window to
560
+ // answer, then silence falls through to the normal terminal prompt. The
561
+ // installed hook timeout leaves headroom over the bridge's wait.
562
+ const APPROVAL_WAIT_MS = 55_000;
563
+ const PERMISSION_HOOK_TIMEOUT_S = 75;
564
+
565
+ /** Sources whose PermissionRequest hook accepts a decision back via
566
+ * `hookSpecificOutput.decision` (Claude Code's schema). Codex is the next
567
+ * candidate once its hooks are verified to honor the reply. */
568
+ const ANSWERABLE_PERMISSION_SOURCES = new Set(['claude']);
551
569
 
552
570
  // ---------------------------------------------------------------------------
553
571
  // Multi-tool hook installers (see EVENT_ALIASES for how their events map).
@@ -675,21 +693,25 @@ function installJsonHooks(key, tool) {
675
693
  for (const event of tool.events ?? CLAUDE_HOOK_EVENTS) {
676
694
  const entries = Array.isArray(hooks[event]) ? hooks[event] : [];
677
695
  const command = tool.eventFlag ? `${base} --event ${event}` : base;
696
+ // PermissionRequest blocks while the phone shows Allow/Deny; everything
697
+ // else must stay too quick to ever be felt in the session.
698
+ const seconds = timeout(event === 'PermissionRequest'
699
+ ? PERMISSION_HOOK_TIMEOUT_S : 5);
678
700
  switch (tool.format) {
679
701
  case 'claude':
680
702
  entries.push({ matcher: '*',
681
- hooks: [{ type: 'command', command, timeout: timeout(5) }] });
703
+ hooks: [{ type: 'command', command, timeout: seconds }] });
682
704
  break;
683
705
  case 'codex':
684
706
  case 'nested':
685
- entries.push({ hooks: [{ type: 'command', command, timeout: timeout(5) }] });
707
+ entries.push({ hooks: [{ type: 'command', command, timeout: seconds }] });
686
708
  break;
687
709
  case 'flat':
688
710
  entries.push({ command });
689
711
  break;
690
712
  case 'trae':
691
713
  entries.push({ matcher: '*', loop_limit: 5,
692
- hooks: [{ type: 'command', command, timeout: timeout(5) }] });
714
+ hooks: [{ type: 'command', command, timeout: seconds }] });
693
715
  break;
694
716
  case 'copilot':
695
717
  entries.push({ type: 'command', bash: command, timeoutSec: 5 });
@@ -882,14 +904,20 @@ function installTool(key, { skipMissing = false } = {}) {
882
904
  installJsonHooks(key, tool);
883
905
  }
884
906
 
885
- function uninstallTool(key) {
907
+ function uninstallTool(key, { skipMissing = false } = {}) {
886
908
  const tool = TOOLS[key];
887
909
  if (!tool) {
888
910
  console.error(`Unknown tool "${key}". Known: ${Object.keys(TOOLS).join(', ')}`);
889
911
  process.exit(1);
890
912
  }
913
+ // `uninstall all` sweeps every known tool; stay quiet about the ones that
914
+ // were never installed instead of printing 14 "nothing installed" lines.
915
+ const nothing = () => {
916
+ if (!skipMissing) console.log(`${tool.name}: nothing installed.`);
917
+ };
891
918
  if (tool.format === 'cline') {
892
919
  const dir = join(homedir(), tool.config);
920
+ if (!existsSync(dir)) { nothing(); return; }
893
921
  for (const event of tool.events) {
894
922
  const path = join(dir, event);
895
923
  if (existsSync(path) && readFileSync(path, 'utf8').includes('carmoji')) {
@@ -901,15 +929,20 @@ function uninstallTool(key) {
901
929
  }
902
930
  if (tool.format === 'kimi') {
903
931
  const path = join(homedir(), tool.config);
904
- if (existsSync(path)) {
905
- writeConfig(path, removeKimiBlocks(readFileSync(path, 'utf8')) + '\n');
932
+ if (!existsSync(path) || !readFileSync(path, 'utf8').includes('carmoji')) {
933
+ nothing();
934
+ return;
906
935
  }
936
+ writeConfig(path, removeKimiBlocks(readFileSync(path, 'utf8')) + '\n');
907
937
  console.log(`Removed CarMoji hooks for ${tool.name}.`);
908
938
  return;
909
939
  }
910
940
  const path = key === 'codex' ? join(codexHome(), 'hooks.json')
911
941
  : join(homedir(), tool.config);
912
- if (!existsSync(path)) { console.log(`${tool.name}: nothing installed.`); return; }
942
+ if (!existsSync(path) || !readFileSync(path, 'utf8').includes('carmoji')) {
943
+ nothing();
944
+ return;
945
+ }
913
946
  const root = readJsonConfig(path);
914
947
  if (root.hooks && typeof root.hooks === 'object') {
915
948
  for (const [event, value] of Object.entries(root.hooks)) {
@@ -966,11 +999,10 @@ function installCodexNotify() {
966
999
  }
967
1000
 
968
1001
  function claudeConfigSnippet() {
969
- const entry = [{ hooks: [{ type: 'command', command: HOOK_COMMAND, timeout: 5 }] }];
970
1002
  const hooks = {};
971
- for (const name of ['SessionStart', 'UserPromptSubmit', 'PreToolUse',
972
- 'PostToolUse', 'Notification', 'Stop', 'SessionEnd']) {
973
- hooks[name] = entry;
1003
+ for (const name of CLAUDE_HOOK_EVENTS) {
1004
+ const timeout = name === 'PermissionRequest' ? PERMISSION_HOOK_TIMEOUT_S : 5;
1005
+ hooks[name] = [{ hooks: [{ type: 'command', command: HOOK_COMMAND, timeout }] }];
974
1006
  }
975
1007
  return JSON.stringify({ hooks }, null, 2);
976
1008
  }
@@ -978,6 +1010,17 @@ function claudeConfigSnippet() {
978
1010
  async function main() {
979
1011
  const [command, arg, ...rest] = process.argv.slice(2);
980
1012
 
1013
+ // `carmoji help [command]`, `carmoji --help`, or `--help` anywhere.
1014
+ // Real agent hooks never pass --help, so this can't shadow the hook path.
1015
+ if (command === 'help' || command === '--help' || command === '-h') {
1016
+ printHelp(arg);
1017
+ return;
1018
+ }
1019
+ if (arg === '--help' || rest.includes('--help')) {
1020
+ printHelp(command);
1021
+ return;
1022
+ }
1023
+
981
1024
  switch (command) {
982
1025
  case 'pair': {
983
1026
  if (!/^\d{4}$/.test(arg || '')) {
@@ -987,10 +1030,18 @@ async function main() {
987
1030
  let target;
988
1031
  const manual = rest[0];
989
1032
  if (manual) {
990
- // Manual addressing for networks with several phones (the app's
991
- // Code Buddy sheet shows the address) or when mDNS is blocked.
1033
+ // Manual addressing (the app's Code Buddy sheet shows the address)
1034
+ // for networks where mDNS is blocked.
992
1035
  const [host, port] = manual.split(':');
993
1036
  target = { host, port: Number(port) || 8737, name: host };
1037
+ const result = await send({ ...target, code: arg },
1038
+ { source: 'bridge', event: 'turn_done' });
1039
+ if (result !== 'ok') {
1040
+ console.error(result === 'bad-code'
1041
+ ? `"${target.name}" (${target.host}:${target.port}) rejected the pairing code — check that device’s Code Buddy sheet.`
1042
+ : `Found "${target.name}" at ${target.host}:${target.port} but couldn’t talk to it (${result}).`);
1043
+ process.exit(1);
1044
+ }
994
1045
  } else {
995
1046
  console.log('Looking for CarMoji on this network…');
996
1047
  const devices = await discoverAll();
@@ -999,17 +1050,27 @@ async function main() {
999
1050
  console.error('You can also pair by address: carmoji pair <code> <ip[:port]>');
1000
1051
  process.exit(1);
1001
1052
  }
1002
- target = devices.length === 1 ? devices[0] : await chooseDevice(devices);
1053
+ // The pairing code identifies the phone: probe every discovered
1054
+ // device with it and pair with whichever accepts — no picking from
1055
+ // a list when several phones share the network.
1056
+ const results = await Promise.all(devices.map((d) =>
1057
+ send({ ...d, code: arg }, { source: 'bridge', event: 'turn_done' })));
1058
+ const accepted = devices.filter((_, i) => results[i] === 'ok');
1059
+ if (accepted.length === 0) {
1060
+ console.error(results.includes('bad-code')
1061
+ ? `No device on this network accepted code ${arg} — check the Code Buddy sheet on the phone you want:`
1062
+ : 'Found CarMoji but couldn’t talk to it:');
1063
+ devices.forEach((d, i) =>
1064
+ console.error(` ${deviceLabel(d)}: ${results[i]}`));
1065
+ process.exit(1);
1066
+ }
1067
+ // Two phones drew the same 4-digit code — let the user break the
1068
+ // tie instead of guessing.
1069
+ target = accepted.length === 1 ? accepted[0]
1070
+ : await chooseDevice(accepted, 'Several devices accepted this pairing code:');
1003
1071
  }
1004
1072
  const device = { name: target.name, host: target.host,
1005
1073
  port: target.port, code: arg };
1006
- const result = await send(device, { source: 'bridge', event: 'turn_done' });
1007
- if (result !== 'ok') {
1008
- console.error(result === 'bad-code'
1009
- ? `"${target.name}" (${target.host}:${target.port}) rejected the pairing code — check that device’s Code Buddy sheet. (Wrong phone? Pass its address: carmoji pair <code> <ip[:port]>)`
1010
- : `Found "${target.name}" at ${target.host}:${target.port} but couldn’t talk to it (${result}).`);
1011
- process.exit(1);
1012
- }
1013
1074
  // Pairing the same device again just refreshes its code; other
1014
1075
  // devices stay paired unless the user says otherwise here.
1015
1076
  const others = loadDevices().filter(
@@ -1081,7 +1142,11 @@ async function main() {
1081
1142
 
1082
1143
  case 'hook': {
1083
1144
  // Runs inside the coding agent on every hook — must be silent, quick,
1084
- // and always exit 0 so it can never disturb the session.
1145
+ // and always exit 0 so it can never disturb the session. The two
1146
+ // deliberate exceptions hold the hook open while the phone answers:
1147
+ // PermissionRequest (fires only when a permission dialog was about to
1148
+ // block the session anyway) and the legacy --gate (blocks every
1149
+ // matched PreToolUse).
1085
1150
  // Flags: --source <tool> (default claude), --event <name> for tools
1086
1151
  // whose stdin lacks the event name, --gate for phone approvals.
1087
1152
  try {
@@ -1122,6 +1187,38 @@ async function main() {
1122
1187
  break;
1123
1188
  }
1124
1189
 
1190
+ // PermissionRequest fires only when a permission dialog is about to
1191
+ // appear (allowlisted tools never trigger it), so blocking here —
1192
+ // unlike the PreToolUse gate — never delays a call that would have
1193
+ // run anyway. A phone tap settles the prompt; silence falls through
1194
+ // to the terminal dialog. AskUserQuestion also arrives as a
1195
+ // PermissionRequest, but the phone has no option picker, so it drops
1196
+ // to the display-only path below.
1197
+ const canonicalEvent = EVENT_ALIASES[payload.hook_event_name]
1198
+ ?? payload.hook_event_name;
1199
+ if (canonicalEvent === 'PermissionRequest'
1200
+ && ANSWERABLE_PERMISSION_SOURCES.has(source)
1201
+ && payload.tool_name !== 'AskUserQuestion') {
1202
+ const decision = await requestApprovalAll(devices, {
1203
+ source,
1204
+ session: payload.session_id,
1205
+ host: hostname(),
1206
+ event: 'approval_request',
1207
+ tool: payload.tool_name,
1208
+ detail: summarizeToolInput(payload),
1209
+ project: payload.cwd ? basename(payload.cwd) : undefined,
1210
+ }, APPROVAL_WAIT_MS);
1211
+ if (decision) {
1212
+ console.log(JSON.stringify({
1213
+ hookSpecificOutput: {
1214
+ hookEventName: 'PermissionRequest',
1215
+ decision: { behavior: decision },
1216
+ },
1217
+ }));
1218
+ }
1219
+ break;
1220
+ }
1221
+
1125
1222
  const event = eventFromHook(payload);
1126
1223
  if (!event) break;
1127
1224
  // host lets the phone attribute plan-window usage per machine and
@@ -1178,8 +1275,8 @@ async function main() {
1178
1275
 
1179
1276
  case 'test': {
1180
1277
  const event = TEST_EVENTS[arg];
1181
- if (!event) {
1182
- console.error(`Usage: carmoji test <${Object.keys(TEST_EVENTS).join('|')}> [session] [tokens]`);
1278
+ if (!event && arg !== 'approval') {
1279
+ console.error(`Usage: carmoji test <${Object.keys(TEST_EVENTS).join('|')}|approval> [session] [tokens] [project] — details: carmoji test --help`);
1183
1280
  process.exit(1);
1184
1281
  }
1185
1282
  const devices = loadDevices();
@@ -1187,6 +1284,22 @@ async function main() {
1187
1284
  console.error('Not paired yet — run: carmoji pair <code>');
1188
1285
  process.exit(1);
1189
1286
  }
1287
+ if (arg === 'approval') {
1288
+ // Exercises the blocking PermissionRequest flow end to end.
1289
+ console.log(`Approval card sent — tap Allow or Deny on the phone (${APPROVAL_WAIT_MS / 1000}s)…`);
1290
+ const decision = await requestApprovalAll(devices, {
1291
+ source: 'test',
1292
+ session: rest[0],
1293
+ host: hostname(),
1294
+ event: 'approval_request',
1295
+ tool: 'Bash',
1296
+ detail: 'carmoji test approval — tap Allow or Deny',
1297
+ }, APPROVAL_WAIT_MS);
1298
+ console.log(decision
1299
+ ? `Decision: ${decision}`
1300
+ : 'No answer — in a real session Claude Code would fall through to the terminal prompt.');
1301
+ process.exit(decision ? 0 : 1);
1302
+ }
1190
1303
  const [session, tokens, project] = rest;
1191
1304
  const message = { source: 'test', session, host: hostname(), ...event };
1192
1305
  if (tokens !== undefined) message.tokens = Number(tokens);
@@ -1229,8 +1342,14 @@ async function main() {
1229
1342
  }
1230
1343
 
1231
1344
  case 'uninstall': {
1345
+ if (arg === 'all') {
1346
+ for (const key of Object.keys(TOOLS)) {
1347
+ uninstallTool(key, { skipMissing: true });
1348
+ }
1349
+ break;
1350
+ }
1232
1351
  if (!arg) {
1233
- console.log(`Usage: carmoji uninstall <tool>\nTools: ${Object.keys(TOOLS).join(', ')}`);
1352
+ console.log(`Usage: carmoji uninstall <tool>|all\nTools: ${Object.keys(TOOLS).join(', ')}`);
1234
1353
  process.exit(1);
1235
1354
  }
1236
1355
  uninstallTool(arg);
@@ -1247,50 +1366,191 @@ async function main() {
1247
1366
  break;
1248
1367
  }
1249
1368
 
1250
- case 'install-claude':
1251
- installClaude();
1252
- break;
1253
-
1254
- case 'install-codex':
1255
- installCodexHooks();
1369
+ case 'config':
1370
+ if (arg === 'claude') printClaudeConfig();
1371
+ else if (arg === 'codex') printCodexConfig();
1372
+ else if (arg === 'gate') printGateConfig();
1373
+ else printHelp('config');
1256
1374
  break;
1257
1375
 
1258
1376
  case 'install-codex-notify':
1259
1377
  installCodexNotify();
1260
1378
  break;
1261
1379
 
1380
+ // Hidden back-compat aliases — the app's setup sheet and old READMEs
1381
+ // print these spellings, so they must keep working.
1382
+ case 'install-claude':
1383
+ installTool('claude');
1384
+ break;
1385
+ case 'install-codex':
1386
+ installTool('codex');
1387
+ break;
1262
1388
  case 'claude-config':
1263
- console.log('Merge this into the project’s .claude/settings.json (or ~/.claude/settings.json for everywhere):\n');
1264
- console.log('(Or let `carmoji install-claude` do it for you, with a backup.)\n');
1265
- console.log(claudeConfigSnippet());
1389
+ printClaudeConfig();
1266
1390
  break;
1267
-
1268
1391
  case 'codex-config':
1269
- console.log('Modern Codex reads hooks from $CODEX_HOME/hooks.json — prefer:');
1270
- console.log(' carmoji install codex\n');
1271
- console.log('For older Codex versions, add this line to ~/.codex/config.toml:\n');
1272
- console.log(RUNNING_FROM_PACKAGE
1273
- ? 'notify = ["npx", "-y", "carmoji", "codex-notify"]'
1274
- : `notify = ["node", "${SCRIPT_PATH}", "codex-notify"]`);
1392
+ printCodexConfig();
1275
1393
  break;
1276
-
1277
1394
  case 'gate-config':
1278
- console.log('Opt-in: answer Claude Code permission prompts from the phone.');
1279
- console.log('Merge into .claude/settings.json, adjusting the matcher to the tools you want gated:\n');
1280
- console.log(JSON.stringify({
1281
- hooks: {
1282
- PreToolUse: [{
1283
- matcher: 'Bash',
1284
- hooks: [{ type: 'command', command: `${HOOK_COMMAND} --gate`, timeout: 60 }],
1285
- }],
1286
- },
1287
- }, null, 2));
1288
- console.log('\nNotes: allow/deny from the phone settles the permission;');
1289
- console.log('no answer within ~55s falls through to the terminal prompt as usual.');
1395
+ printGateConfig();
1396
+ break;
1397
+
1398
+ default:
1399
+ printHelp();
1400
+ }
1401
+ }
1402
+
1403
+ function printClaudeConfig() {
1404
+ console.log('Merge this into the project’s .claude/settings.json (or ~/.claude/settings.json for everywhere):\n');
1405
+ console.log('(Or let `carmoji install claude` do it for you, with a backup.)\n');
1406
+ console.log(claudeConfigSnippet());
1407
+ }
1408
+
1409
+ function printCodexConfig() {
1410
+ console.log('Modern Codex reads hooks from $CODEX_HOME/hooks.json — prefer:');
1411
+ console.log(' carmoji install codex\n');
1412
+ console.log('For older Codex versions, add this line to ~/.codex/config.toml:\n');
1413
+ console.log(RUNNING_FROM_PACKAGE
1414
+ ? 'notify = ["npx", "-y", "carmoji", "codex-notify"]'
1415
+ : `notify = ["node", "${SCRIPT_PATH}", "codex-notify"]`);
1416
+ }
1417
+
1418
+ function printGateConfig() {
1419
+ console.log('Legacy/advanced: `carmoji install claude` now installs a PermissionRequest');
1420
+ console.log('hook that answers real permission prompts from the phone by itself.');
1421
+ console.log('The PreToolUse gate below instead fires on EVERY matched tool call, even');
1422
+ console.log('allowlisted ones — useful only on older Claude Code versions without the');
1423
+ console.log('PermissionRequest hook, or to force-gate specific tools.');
1424
+ console.log('Merge into .claude/settings.json, adjusting the matcher to the tools you want gated:\n');
1425
+ console.log(JSON.stringify({
1426
+ hooks: {
1427
+ PreToolUse: [{
1428
+ matcher: 'Bash',
1429
+ hooks: [{ type: 'command', command: `${HOOK_COMMAND} --gate`, timeout: 60 }],
1430
+ }],
1431
+ },
1432
+ }, null, 2));
1433
+ console.log('\nNotes: allow/deny from the phone settles the permission;');
1434
+ console.log('no answer within ~55s falls through to the terminal prompt as usual.');
1435
+ }
1436
+
1437
+ const USAGE = `carmoji bridge — talk to the CarMoji app on your phone
1438
+
1439
+ pair <code> [ip[:port]] pair with a phone (code from its Code Buddy sheet)
1440
+ unpair [<name|ip[:port]>|all] forget paired devices
1441
+ discover list CarMoji devices on this Wi-Fi
1442
+ install <tool>|all install hooks (tools: ${Object.keys(TOOLS).join(', ')})
1443
+ uninstall <tool>|all remove CarMoji hooks from a tool's config
1444
+ tools show each tool's config path and whether it's present
1445
+ test <event> send a fake event; \`test approval\` pops the Allow/Deny card
1446
+ hook entry point the agents' hooks invoke (reads JSON on stdin)
1447
+ config <claude|codex|gate> print a manual config snippet
1448
+ codex-notify legacy Codex notify entry point
1449
+
1450
+ Run a command with --help for details — \`carmoji hook --help\` documents the
1451
+ accepted stdin JSON, \`carmoji test --help\` lists every test event.`;
1452
+
1453
+ /** Detailed per-command help; anything without a dedicated page gets USAGE. */
1454
+ function printHelp(topic) {
1455
+ switch (topic) {
1456
+ case 'hook':
1457
+ console.log(`carmoji hook [--source <tool>] [--event <name>] [--gate]
1458
+
1459
+ Entry point the coding agents' hooks invoke: reads ONE hook event as JSON on
1460
+ stdin, forwards it to every paired phone, and exits 0 no matter what so it can
1461
+ never disturb a session. Unpaired = immediate no-op.
1462
+
1463
+ Flags:
1464
+ --source <tool> which agent is calling (default: claude) — sets the wire
1465
+ \`source\` and enables tool-specific payload handling
1466
+ --event <name> event name for tools whose stdin JSON lacks one
1467
+ (Gemini, Cursor, Trae, Copilot)
1468
+ --gate legacy PreToolUse gate — see \`carmoji config gate\`
1469
+
1470
+ Accepted stdin JSON — Claude Code's field names, with aliases folded in:
1471
+ hook_event_name or hookEventName / eventName / event / hookName / --event
1472
+ session_id or sessionId / conversation_id / conversationId / taskId
1473
+ tool_name or toolName / (Cline) preToolUse.toolName
1474
+ tool_input or toolArgs (JSON string) / (Cline) preToolUse.parameters
1475
+ cwd project folder shown on the phone (default: process cwd)
1476
+ message / prompt human snippet for prompt & permission toasts
1477
+ transcript_path lets the bridge tail token usage for the tok/s readout
1478
+
1479
+ Canonical events: ${CLAUDE_HOOK_EVENTS.join(', ')},
1480
+ PostToolUseFailure, SubagentStart/Stop, PreCompact/PostCompact — plus each
1481
+ tool's dialect (beforeShellExecution, TaskComplete, permission_request, …);
1482
+ see EVENT_ALIASES in carmoji.js for the full mapping. Unknown events are
1483
+ silently ignored.
1484
+
1485
+ Blocking: a Claude Code PermissionRequest (any tool except AskUserQuestion)
1486
+ holds the hook open up to ${APPROVAL_WAIT_MS / 1000}s while the phone shows Allow/Deny, then
1487
+ prints the decision for the agent:
1488
+ {"hookSpecificOutput":{"hookEventName":"PermissionRequest",
1489
+ "decision":{"behavior":"allow"}}}
1490
+ No tap → no output → the normal terminal prompt appears.
1491
+
1492
+ Try it:
1493
+ echo '{"hook_event_name":"PreToolUse","session_id":"s1","tool_name":"Edit"}' \\
1494
+ | carmoji hook`);
1495
+ break;
1496
+
1497
+ case 'test':
1498
+ console.log(`carmoji test <event> [session] [tokens] [project]
1499
+
1500
+ Sends a fake event to every paired phone so you can watch the reactions
1501
+ without a coding agent.
1502
+
1503
+ start wakes up, excited (session_start)
1504
+ prompt perks up and listens (prompt)
1505
+ edit focused typing face (tool_start Edit)
1506
+ read pores over a document (tool_start Read)
1507
+ run nervous side-eye (tool_start Bash)
1508
+ heartbeat no reaction; session liveness + token sample (tool_end)
1509
+ done celebration with hearts (turn_done)
1510
+ error dizzy comic collapse (error)
1511
+ permission pleading + floating "?" (permission)
1512
+ end contented sigh (session_end)
1513
+ approval pops the Allow/Deny card, waits ${APPROVAL_WAIT_MS / 1000}s, prints your tap
1514
+
1515
+ [session] session key — use two different values to simulate several
1516
+ agents at once
1517
+ [tokens] cumulative output-token count for the tok/s readout
1518
+ [project] project name shown under the dashboard robot`);
1519
+ break;
1520
+
1521
+ case 'config':
1522
+ console.log(`carmoji config <claude|codex|gate>
1523
+
1524
+ claude the .claude/settings.json hooks snippet (what \`install claude\`
1525
+ writes, including the ${PERMISSION_HOOK_TIMEOUT_S}s PermissionRequest hook)
1526
+ codex the legacy ~/.codex/config.toml notify line for Codex versions
1527
+ without hooks.json (modern Codex: \`carmoji install codex\`)
1528
+ gate the legacy PreToolUse gate snippet — fires on every matched call;
1529
+ superseded by the PermissionRequest hook \`install claude\` sets up`);
1530
+ break;
1531
+
1532
+ case 'install':
1533
+ console.log(`carmoji install <tool>|all
1534
+
1535
+ Writes CarMoji's hooks into the tool's own config (a .bak backup is saved
1536
+ next to anything modified; reinstalling replaces earlier CarMoji entries).
1537
+ \`all\` installs into every tool detected on this machine.
1538
+
1539
+ Tools: ${Object.keys(TOOLS).join(', ')}
1540
+ \`carmoji tools\` shows each one's config path and whether it's present.`);
1541
+ break;
1542
+
1543
+ case 'uninstall':
1544
+ console.log(`carmoji uninstall <tool>|all
1545
+
1546
+ Removes every CarMoji entry from the tool's hook config, leaving other
1547
+ hooks untouched. \`all\` sweeps every known tool quietly.
1548
+
1549
+ Tools: ${Object.keys(TOOLS).join(', ')}`);
1290
1550
  break;
1291
1551
 
1292
1552
  default:
1293
- console.log('carmoji bridge — usage: pair <code> [ip[:port]] | unpair [<name|ip[:port]>|all] | discover | install <tool>|all | uninstall <tool> | tools | test <event> | hook [--source <tool>] [--event <name>] [--gate] | codex-notify | claude-config | codex-config | gate-config');
1553
+ console.log(USAGE);
1294
1554
  }
1295
1555
  }
1296
1556
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "carmoji",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
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": {