carmoji 0.3.4 → 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 +292 -51
  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
@@ -9,15 +9,18 @@
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> install hooks for a tool (or `all`); see `tools`
13
- // 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
14
14
  // tools list supported tools and where their hooks live
15
15
  // hook hook entry point, all tools (reads hook JSON on stdin;
16
16
  // --source <tool>, --event <name>, --gate)
17
17
  // codex-notify legacy Codex notify entry point (event JSON as last arg)
18
- // test <name> send a test event: start|prompt|edit|read|run|heartbeat|done|error|permission|end
19
- // claude-config print the .claude/settings.json hooks snippet
20
- // 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.
21
24
 
22
25
  import {
23
26
  readFileSync, writeFileSync, mkdirSync,
@@ -279,6 +282,7 @@ function requestApprovalAll(devices, message, timeoutMs) {
279
282
  function summarizeToolInput(payload) {
280
283
  const input = payload.tool_input || {};
281
284
  const text = input.command || input.file_path || input.description
285
+ || input.questions?.[0]?.question
282
286
  || JSON.stringify(input);
283
287
  return String(text).replace(/\s+/g, ' ').slice(0, 140);
284
288
  }
@@ -548,7 +552,20 @@ function backupFile(path) {
548
552
  }
549
553
 
550
554
  const CLAUDE_HOOK_EVENTS = ['SessionStart', 'UserPromptSubmit', 'PreToolUse',
551
- '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']);
552
569
 
553
570
  // ---------------------------------------------------------------------------
554
571
  // Multi-tool hook installers (see EVENT_ALIASES for how their events map).
@@ -676,21 +693,25 @@ function installJsonHooks(key, tool) {
676
693
  for (const event of tool.events ?? CLAUDE_HOOK_EVENTS) {
677
694
  const entries = Array.isArray(hooks[event]) ? hooks[event] : [];
678
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);
679
700
  switch (tool.format) {
680
701
  case 'claude':
681
702
  entries.push({ matcher: '*',
682
- hooks: [{ type: 'command', command, timeout: timeout(5) }] });
703
+ hooks: [{ type: 'command', command, timeout: seconds }] });
683
704
  break;
684
705
  case 'codex':
685
706
  case 'nested':
686
- entries.push({ hooks: [{ type: 'command', command, timeout: timeout(5) }] });
707
+ entries.push({ hooks: [{ type: 'command', command, timeout: seconds }] });
687
708
  break;
688
709
  case 'flat':
689
710
  entries.push({ command });
690
711
  break;
691
712
  case 'trae':
692
713
  entries.push({ matcher: '*', loop_limit: 5,
693
- hooks: [{ type: 'command', command, timeout: timeout(5) }] });
714
+ hooks: [{ type: 'command', command, timeout: seconds }] });
694
715
  break;
695
716
  case 'copilot':
696
717
  entries.push({ type: 'command', bash: command, timeoutSec: 5 });
@@ -883,14 +904,20 @@ function installTool(key, { skipMissing = false } = {}) {
883
904
  installJsonHooks(key, tool);
884
905
  }
885
906
 
886
- function uninstallTool(key) {
907
+ function uninstallTool(key, { skipMissing = false } = {}) {
887
908
  const tool = TOOLS[key];
888
909
  if (!tool) {
889
910
  console.error(`Unknown tool "${key}". Known: ${Object.keys(TOOLS).join(', ')}`);
890
911
  process.exit(1);
891
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
+ };
892
918
  if (tool.format === 'cline') {
893
919
  const dir = join(homedir(), tool.config);
920
+ if (!existsSync(dir)) { nothing(); return; }
894
921
  for (const event of tool.events) {
895
922
  const path = join(dir, event);
896
923
  if (existsSync(path) && readFileSync(path, 'utf8').includes('carmoji')) {
@@ -902,15 +929,20 @@ function uninstallTool(key) {
902
929
  }
903
930
  if (tool.format === 'kimi') {
904
931
  const path = join(homedir(), tool.config);
905
- if (existsSync(path)) {
906
- writeConfig(path, removeKimiBlocks(readFileSync(path, 'utf8')) + '\n');
932
+ if (!existsSync(path) || !readFileSync(path, 'utf8').includes('carmoji')) {
933
+ nothing();
934
+ return;
907
935
  }
936
+ writeConfig(path, removeKimiBlocks(readFileSync(path, 'utf8')) + '\n');
908
937
  console.log(`Removed CarMoji hooks for ${tool.name}.`);
909
938
  return;
910
939
  }
911
940
  const path = key === 'codex' ? join(codexHome(), 'hooks.json')
912
941
  : join(homedir(), tool.config);
913
- if (!existsSync(path)) { console.log(`${tool.name}: nothing installed.`); return; }
942
+ if (!existsSync(path) || !readFileSync(path, 'utf8').includes('carmoji')) {
943
+ nothing();
944
+ return;
945
+ }
914
946
  const root = readJsonConfig(path);
915
947
  if (root.hooks && typeof root.hooks === 'object') {
916
948
  for (const [event, value] of Object.entries(root.hooks)) {
@@ -967,11 +999,10 @@ function installCodexNotify() {
967
999
  }
968
1000
 
969
1001
  function claudeConfigSnippet() {
970
- const entry = [{ hooks: [{ type: 'command', command: HOOK_COMMAND, timeout: 5 }] }];
971
1002
  const hooks = {};
972
- for (const name of ['SessionStart', 'UserPromptSubmit', 'PreToolUse',
973
- 'PostToolUse', 'Notification', 'Stop', 'SessionEnd']) {
974
- 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 }] }];
975
1006
  }
976
1007
  return JSON.stringify({ hooks }, null, 2);
977
1008
  }
@@ -979,6 +1010,17 @@ function claudeConfigSnippet() {
979
1010
  async function main() {
980
1011
  const [command, arg, ...rest] = process.argv.slice(2);
981
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
+
982
1024
  switch (command) {
983
1025
  case 'pair': {
984
1026
  if (!/^\d{4}$/.test(arg || '')) {
@@ -1100,7 +1142,11 @@ async function main() {
1100
1142
 
1101
1143
  case 'hook': {
1102
1144
  // Runs inside the coding agent on every hook — must be silent, quick,
1103
- // 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).
1104
1150
  // Flags: --source <tool> (default claude), --event <name> for tools
1105
1151
  // whose stdin lacks the event name, --gate for phone approvals.
1106
1152
  try {
@@ -1141,6 +1187,38 @@ async function main() {
1141
1187
  break;
1142
1188
  }
1143
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
+
1144
1222
  const event = eventFromHook(payload);
1145
1223
  if (!event) break;
1146
1224
  // host lets the phone attribute plan-window usage per machine and
@@ -1197,8 +1275,8 @@ async function main() {
1197
1275
 
1198
1276
  case 'test': {
1199
1277
  const event = TEST_EVENTS[arg];
1200
- if (!event) {
1201
- 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`);
1202
1280
  process.exit(1);
1203
1281
  }
1204
1282
  const devices = loadDevices();
@@ -1206,6 +1284,22 @@ async function main() {
1206
1284
  console.error('Not paired yet — run: carmoji pair <code>');
1207
1285
  process.exit(1);
1208
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
+ }
1209
1303
  const [session, tokens, project] = rest;
1210
1304
  const message = { source: 'test', session, host: hostname(), ...event };
1211
1305
  if (tokens !== undefined) message.tokens = Number(tokens);
@@ -1248,8 +1342,14 @@ async function main() {
1248
1342
  }
1249
1343
 
1250
1344
  case 'uninstall': {
1345
+ if (arg === 'all') {
1346
+ for (const key of Object.keys(TOOLS)) {
1347
+ uninstallTool(key, { skipMissing: true });
1348
+ }
1349
+ break;
1350
+ }
1251
1351
  if (!arg) {
1252
- console.log(`Usage: carmoji uninstall <tool>\nTools: ${Object.keys(TOOLS).join(', ')}`);
1352
+ console.log(`Usage: carmoji uninstall <tool>|all\nTools: ${Object.keys(TOOLS).join(', ')}`);
1253
1353
  process.exit(1);
1254
1354
  }
1255
1355
  uninstallTool(arg);
@@ -1266,50 +1366,191 @@ async function main() {
1266
1366
  break;
1267
1367
  }
1268
1368
 
1269
- case 'install-claude':
1270
- installClaude();
1271
- break;
1272
-
1273
- case 'install-codex':
1274
- 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');
1275
1374
  break;
1276
1375
 
1277
1376
  case 'install-codex-notify':
1278
1377
  installCodexNotify();
1279
1378
  break;
1280
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;
1281
1388
  case 'claude-config':
1282
- console.log('Merge this into the project’s .claude/settings.json (or ~/.claude/settings.json for everywhere):\n');
1283
- console.log('(Or let `carmoji install-claude` do it for you, with a backup.)\n');
1284
- console.log(claudeConfigSnippet());
1389
+ printClaudeConfig();
1285
1390
  break;
1286
-
1287
1391
  case 'codex-config':
1288
- console.log('Modern Codex reads hooks from $CODEX_HOME/hooks.json — prefer:');
1289
- console.log(' carmoji install codex\n');
1290
- console.log('For older Codex versions, add this line to ~/.codex/config.toml:\n');
1291
- console.log(RUNNING_FROM_PACKAGE
1292
- ? 'notify = ["npx", "-y", "carmoji", "codex-notify"]'
1293
- : `notify = ["node", "${SCRIPT_PATH}", "codex-notify"]`);
1392
+ printCodexConfig();
1294
1393
  break;
1295
-
1296
1394
  case 'gate-config':
1297
- console.log('Opt-in: answer Claude Code permission prompts from the phone.');
1298
- console.log('Merge into .claude/settings.json, adjusting the matcher to the tools you want gated:\n');
1299
- console.log(JSON.stringify({
1300
- hooks: {
1301
- PreToolUse: [{
1302
- matcher: 'Bash',
1303
- hooks: [{ type: 'command', command: `${HOOK_COMMAND} --gate`, timeout: 60 }],
1304
- }],
1305
- },
1306
- }, null, 2));
1307
- console.log('\nNotes: allow/deny from the phone settles the permission;');
1308
- 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(', ')}`);
1309
1550
  break;
1310
1551
 
1311
1552
  default:
1312
- 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);
1313
1554
  }
1314
1555
  }
1315
1556
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "carmoji",
3
- "version": "0.3.4",
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": {