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.
- package/README.md +27 -21
- package/carmoji.js +324 -64
- 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
|
@@ -5,18 +5,22 @@
|
|
|
5
5
|
// address + pairing code in ~/.config/carmoji/config.json.
|
|
6
6
|
//
|
|
7
7
|
// Commands:
|
|
8
|
-
// pair <code> discover
|
|
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
|
|
12
|
-
// 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
|
|
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
|
|
18
|
-
//
|
|
19
|
-
//
|
|
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(
|
|
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
|
-
'
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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)
|
|
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
|
|
972
|
-
|
|
973
|
-
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 }] }];
|
|
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
|
|
991
|
-
//
|
|
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
|
-
|
|
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
|
|
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 '
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
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(
|
|
1553
|
+
console.log(USAGE);
|
|
1294
1554
|
}
|
|
1295
1555
|
}
|
|
1296
1556
|
|