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.
- package/README.md +27 -21
- package/carmoji.js +292 -51
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,18 +46,18 @@ approval.
|
|
|
46
46
|
|
|
47
47
|
## Claude Code
|
|
48
48
|
|
|
49
|
-
|
|
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
|
|
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
|
|
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
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
|
13
|
-
// uninstall <tool
|
|
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
|
|
19
|
-
//
|
|
20
|
-
//
|
|
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
|
-
'
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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)
|
|
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
|
|
973
|
-
|
|
974
|
-
hooks[name] =
|
|
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
|
|
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 '
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
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(
|
|
1553
|
+
console.log(USAGE);
|
|
1313
1554
|
}
|
|
1314
1555
|
}
|
|
1315
1556
|
|