carmoji 0.3.14 → 0.3.16
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 +28 -17
- package/carmoji.js +218 -67
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -49,23 +49,25 @@ approval.
|
|
|
49
49
|
To get reactions in every project:
|
|
50
50
|
|
|
51
51
|
```sh
|
|
52
|
-
npx carmoji
|
|
52
|
+
npx carmoji hook claude
|
|
53
53
|
```
|
|
54
54
|
|
|
55
55
|
merges the hooks into `~/.claude/settings.json`, preserving everything
|
|
56
|
-
already there (a backup is saved to `settings.json.bak` first, reruns
|
|
57
|
-
|
|
56
|
+
already there (a backup is saved to `settings.json.bak` first, reruns replace
|
|
57
|
+
only CarMoji-owned entries, and a file that doesn't parse is refused untouched). Prefer to do
|
|
58
58
|
it by hand? `npx carmoji config claude` prints the snippet instead. Every
|
|
59
|
-
command answers `--help`; `npx carmoji hook --help` documents
|
|
60
|
-
|
|
59
|
+
command answers `--help`; `npx carmoji hook --help` documents both setup and
|
|
60
|
+
the JSON event mode used by installed hooks. `npx carmoji unhook <tool>|all`
|
|
61
|
+
removes tool integrations again.
|
|
61
62
|
|
|
62
63
|
## All the other tools
|
|
63
64
|
|
|
64
65
|
```sh
|
|
65
66
|
npx carmoji tools # what's supported, what's detected
|
|
66
|
-
npx carmoji
|
|
67
|
-
npx carmoji
|
|
68
|
-
npx carmoji
|
|
67
|
+
npx carmoji hook codex # one tool…
|
|
68
|
+
npx carmoji hook all # …or hooks for everything found on this machine
|
|
69
|
+
npx carmoji unhook <tool> # remove one tool's integration
|
|
70
|
+
npx carmoji uninstall # remove ~/.config/carmoji/carmoji.js
|
|
69
71
|
```
|
|
70
72
|
|
|
71
73
|
| Tool | Hooks written to | Notes |
|
|
@@ -85,12 +87,22 @@ npx carmoji uninstall <tool>
|
|
|
85
87
|
|
|
86
88
|
Everything merges non-destructively: a backup is written next to each
|
|
87
89
|
config, reruns are idempotent, only carmoji-owned entries are ever
|
|
88
|
-
removed, and unparseable files are refused untouched. `
|
|
90
|
+
removed, and unparseable files are refused untouched. `hook all` skips
|
|
89
91
|
tools that aren't on the machine.
|
|
90
92
|
|
|
91
|
-
|
|
93
|
+
Every `carmoji hook <tool>|all` also refreshes a persistent hook runtime
|
|
94
|
+
at `~/.config/carmoji/carmoji.js`, with its Node package metadata and
|
|
95
|
+
dependencies beside it. Installed hooks call that file directly with
|
|
96
|
+
`node`; they do not run `npx`, contact npm, or depend on npx's temporary
|
|
97
|
+
cache during an agent session. Run `npx carmoji hook …` again whenever
|
|
98
|
+
you want to update both the runtime and the selected tool's hooks. Pairing
|
|
99
|
+
state in the same directory is preserved. `npx carmoji uninstall` removes
|
|
100
|
+
the runtime script but preserves pairing state and tool configs; use
|
|
101
|
+
`npx carmoji unhook <tool>|all` for those configs.
|
|
102
|
+
|
|
103
|
+
Codex requires new or changed command hooks to be trusted. After hooking,
|
|
92
104
|
open `/hooks` in Codex, review the CarMoji commands, and trust them. The
|
|
93
|
-
|
|
105
|
+
setup command removes a direct legacy CarMoji `notify` command, or removes only
|
|
94
106
|
CarMoji when another notifier has it chained via `--previous-notify`.
|
|
95
107
|
|
|
96
108
|
All of them funnel through the same `carmoji hook` entry point
|
|
@@ -99,7 +111,7 @@ whose payloads lack one), so the pet reacts identically no matter which
|
|
|
99
111
|
agent is working — each with its own colored dot on the dashboard.
|
|
100
112
|
|
|
101
113
|
Older Codex versions without hooks support can still use
|
|
102
|
-
`npx carmoji
|
|
114
|
+
`npx carmoji hook-codex-notify`, which sets the `notify` program in
|
|
103
115
|
`~/.codex/config.toml` (turn completions only). OpenCode users running the
|
|
104
116
|
community **omo** plugin get events for free — omo fires your Claude Code
|
|
105
117
|
hooks, which already carry carmoji.
|
|
@@ -155,7 +167,7 @@ Recomputed at turn boundaries with a 5-minute cache so hooks stay fast.
|
|
|
155
167
|
|
|
156
168
|
## Answer permissions from the phone (Claude Code and Codex)
|
|
157
169
|
|
|
158
|
-
`carmoji
|
|
170
|
+
`carmoji hook claude` and `carmoji hook codex` hook each agent's
|
|
159
171
|
`PermissionRequest` event, which fires **only when a permission dialog is
|
|
160
172
|
about to appear** —
|
|
161
173
|
allowlisted tools and auto-accepted edits never trigger it, so nothing
|
|
@@ -185,8 +197,7 @@ matcher (e.g. `Bash`). Unlike `PermissionRequest`, that variant fires on
|
|
|
185
197
|
- The app runs an `NWListener` WebSocket server on port 8737, advertised
|
|
186
198
|
via Bonjour as `_carmoji._tcp`. LAN only; every frame must carry the
|
|
187
199
|
pairing code.
|
|
188
|
-
-
|
|
189
|
-
stdin, maps it to
|
|
190
|
-
|
|
191
|
-
unpaired it exits immediately.
|
|
200
|
+
- `~/.config/carmoji/carmoji.js hook` is invoked by the coding agent's hooks
|
|
201
|
+
with event JSON on stdin, maps it to wire events, gives each delivery batch
|
|
202
|
+
a ≤0.9 s budget, and always exits 0. When unpaired it exits immediately.
|
|
192
203
|
- Pairing state lives in `~/.config/carmoji/config.json`.
|
package/carmoji.js
CHANGED
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
// whose Code Buddy sheet shows <code>
|
|
10
10
|
// (pairing is additive — events fan out to every device)
|
|
11
11
|
// unpair [device] forget a paired device by name or ip[:port], or `all`
|
|
12
|
-
//
|
|
13
|
-
//
|
|
12
|
+
// hook <tool>|all install hooks for a tool (or every detected one)
|
|
13
|
+
// unhook <tool>|all remove CarMoji hooks from a tool's config
|
|
14
|
+
// uninstall remove the persistent hook runtime
|
|
14
15
|
// tools list supported tools and where their hooks live
|
|
15
16
|
// hook hook entry point, all tools (reads hook JSON on stdin;
|
|
16
17
|
// --source <tool>, --event <name>, --gate)
|
|
@@ -25,26 +26,125 @@
|
|
|
25
26
|
import {
|
|
26
27
|
readFileSync, writeFileSync, mkdirSync,
|
|
27
28
|
statSync, openSync, readSync, closeSync,
|
|
28
|
-
copyFileSync, existsSync, readdirSync, unlinkSync, realpathSync,
|
|
29
|
+
copyFileSync, cpSync, existsSync, readdirSync, unlinkSync, realpathSync,
|
|
30
|
+
renameSync,
|
|
29
31
|
} from 'node:fs';
|
|
30
32
|
import { spawn } from 'node:child_process';
|
|
33
|
+
import { createRequire } from 'node:module';
|
|
31
34
|
import { homedir, hostname } from 'node:os';
|
|
32
35
|
import { join, dirname, basename } from 'node:path';
|
|
33
36
|
import { fileURLToPath } from 'node:url';
|
|
34
37
|
|
|
35
38
|
const CONFIG_PATH = join(homedir(), '.config', 'carmoji', 'config.json');
|
|
39
|
+
const RUNTIME_DIR = dirname(CONFIG_PATH);
|
|
40
|
+
const RUNTIME_SCRIPT_PATH = join(RUNTIME_DIR, 'carmoji.js');
|
|
41
|
+
const RUNTIME_PACKAGE_PATH = join(RUNTIME_DIR, 'package.json');
|
|
42
|
+
const RUNTIME_NODE_MODULES = join(RUNTIME_DIR, 'node_modules');
|
|
36
43
|
const SCRIPT_PATH = fileURLToPath(import.meta.url);
|
|
37
|
-
|
|
38
|
-
// printed configs must go through `npx` rather than an absolute path.
|
|
44
|
+
const REQUIRE = createRequire(import.meta.url);
|
|
39
45
|
const RUNNING_FROM_PACKAGE = SCRIPT_PATH.includes('node_modules')
|
|
40
46
|
|| SCRIPT_PATH.includes('_npx');
|
|
41
|
-
const HOOK_COMMAND = RUNNING_FROM_PACKAGE
|
|
42
|
-
? 'npx -y carmoji hook'
|
|
43
|
-
: `node "${SCRIPT_PATH}" hook`;
|
|
44
47
|
// Never make the user's coding session wait on us.
|
|
45
48
|
const SEND_TIMEOUT_MS = 900;
|
|
46
49
|
const DISCOVER_TIMEOUT_MS = 10_000;
|
|
47
50
|
|
|
51
|
+
function nodeCommand(path, subcommand) {
|
|
52
|
+
return `node ${JSON.stringify(path)} ${subcommand}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function runtimeHookCommand() {
|
|
56
|
+
return nodeCommand(RUNTIME_SCRIPT_PATH, 'hook');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Manual config snippets prefer the installed runtime, but remain useful on
|
|
60
|
+
* a fresh machine before the first `hook <tool>` command. */
|
|
61
|
+
function hookCommand() {
|
|
62
|
+
if (existsSync(RUNTIME_SCRIPT_PATH)) return runtimeHookCommand();
|
|
63
|
+
return RUNNING_FROM_PACKAGE
|
|
64
|
+
? 'npx -y carmoji hook'
|
|
65
|
+
: nodeCommand(SCRIPT_PATH, 'hook');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Find the npm installation containing CarMoji's runtime dependencies. */
|
|
69
|
+
function dependencyRoot() {
|
|
70
|
+
let path = dirname(REQUIRE.resolve('ws'));
|
|
71
|
+
while (dirname(path) !== path) {
|
|
72
|
+
if (basename(path) === 'node_modules') return path;
|
|
73
|
+
path = dirname(path);
|
|
74
|
+
}
|
|
75
|
+
throw new Error('could not locate CarMoji dependencies');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function samePath(left, right) {
|
|
79
|
+
try {
|
|
80
|
+
return realpathSync(left) === realpathSync(right);
|
|
81
|
+
} catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Replace one runtime file only after its complete new copy is on disk. */
|
|
87
|
+
function installRuntimeFile(source, destination, fallback) {
|
|
88
|
+
if (samePath(source, destination)) return;
|
|
89
|
+
const temporary = `${destination}.tmp-${process.pid}`;
|
|
90
|
+
try {
|
|
91
|
+
if (source && existsSync(source)) copyFileSync(source, temporary);
|
|
92
|
+
else writeFileSync(temporary, fallback);
|
|
93
|
+
renameSync(temporary, destination);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
try { unlinkSync(temporary); } catch { /* nothing staged */ }
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Install a network-independent hook runtime. `npx carmoji hook …` may run
|
|
102
|
+
* from an ephemeral npm cache, so every setup refreshes the script, its ESM
|
|
103
|
+
* package marker, and the dependencies beside the user's persistent config.
|
|
104
|
+
* Pairing state and transcript caches in the same directory are untouched.
|
|
105
|
+
*/
|
|
106
|
+
function installRuntime() {
|
|
107
|
+
mkdirSync(RUNTIME_DIR, { recursive: true });
|
|
108
|
+
const existed = existsSync(RUNTIME_SCRIPT_PATH);
|
|
109
|
+
try {
|
|
110
|
+
const sourceModules = dependencyRoot();
|
|
111
|
+
if (!samePath(sourceModules, RUNTIME_NODE_MODULES)) {
|
|
112
|
+
cpSync(sourceModules, RUNTIME_NODE_MODULES, {
|
|
113
|
+
recursive: true,
|
|
114
|
+
force: true,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
const sourcePackage = join(dirname(SCRIPT_PATH), 'package.json');
|
|
118
|
+
installRuntimeFile(sourcePackage, RUNTIME_PACKAGE_PATH,
|
|
119
|
+
'{"name":"carmoji-runtime","private":true,"type":"module"}\n');
|
|
120
|
+
// Install the entry point last: an old script can use updated dependencies,
|
|
121
|
+
// while a new script must never become visible before its dependencies do.
|
|
122
|
+
installRuntimeFile(SCRIPT_PATH, RUNTIME_SCRIPT_PATH);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
console.error(`Could not install the CarMoji hook runtime in ${RUNTIME_DIR}: ${error.message}`);
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
console.log(`${existed ? 'Updated' : 'Installed'} CarMoji hook runtime at ${RUNTIME_SCRIPT_PATH}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Remove only the executable runtime. Pairing state, caches, and staged
|
|
131
|
+
* dependencies stay in place so a later `hook <tool>` can restore it without
|
|
132
|
+
* erasing the user's devices. Tool configs are managed separately by unhook. */
|
|
133
|
+
function uninstallRuntime() {
|
|
134
|
+
if (!existsSync(RUNTIME_SCRIPT_PATH)) {
|
|
135
|
+
console.log(`CarMoji hook runtime is not installed at ${RUNTIME_SCRIPT_PATH}`);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
unlinkSync(RUNTIME_SCRIPT_PATH);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
console.error(`Could not remove ${RUNTIME_SCRIPT_PATH}: ${error.message}`);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
console.log(`Removed CarMoji hook runtime from ${RUNTIME_SCRIPT_PATH}`);
|
|
145
|
+
console.log('Run `carmoji unhook <tool>|all` separately to remove tool configuration.');
|
|
146
|
+
}
|
|
147
|
+
|
|
48
148
|
function loadConfig() {
|
|
49
149
|
try {
|
|
50
150
|
return JSON.parse(readFileSync(CONFIG_PATH, 'utf8'));
|
|
@@ -532,8 +632,19 @@ function eventFromHook(payload) {
|
|
|
532
632
|
case 'PostToolUse': return { event: 'tool_end', tool };
|
|
533
633
|
case 'PostToolUseFailure':
|
|
534
634
|
case 'Error': return { event: 'error' };
|
|
535
|
-
case 'PermissionRequest':
|
|
536
|
-
case 'Notification':
|
|
635
|
+
case 'PermissionRequest': return { event: 'permission', tool };
|
|
636
|
+
case 'Notification': {
|
|
637
|
+
// Claude Code routes several nudges through this one hook. Only a
|
|
638
|
+
// real permission prompt deserves the phone's pleading face; the
|
|
639
|
+
// "waiting for your input" idle reminder lands a minute after a
|
|
640
|
+
// *finished* reply, and mapping it to permission flipped "done"
|
|
641
|
+
// into "blocked". Anything not clearly a permission stays a bare
|
|
642
|
+
// heartbeat.
|
|
643
|
+
const note = firstString(payload.message, payload.detail) ?? '';
|
|
644
|
+
return /permission|approv/i.test(note)
|
|
645
|
+
? { event: 'permission', tool }
|
|
646
|
+
: { event: 'tool_end' };
|
|
647
|
+
}
|
|
537
648
|
case 'Stop': return { event: 'turn_done' };
|
|
538
649
|
case 'SessionEnd': return { event: 'session_end' };
|
|
539
650
|
// Liveness only: keep the session fresh without a face reaction.
|
|
@@ -738,7 +849,7 @@ const CURSOR_STYLE_EVENTS = [
|
|
|
738
849
|
];
|
|
739
850
|
|
|
740
851
|
/**
|
|
741
|
-
* Every tool `carmoji
|
|
852
|
+
* Every tool `carmoji hook <name>` knows. `source` defaults to the key;
|
|
742
853
|
* `timeoutMs` marks tools whose hook timeouts are milliseconds; `eventFlag`
|
|
743
854
|
* marks tools whose stdin lacks the event name (the command carries
|
|
744
855
|
* `--event <name>`); `requiresDir` gates installation on the tool actually
|
|
@@ -827,7 +938,7 @@ function installJsonHooks(key, tool) {
|
|
|
827
938
|
const hooks = (root.hooks && typeof root.hooks === 'object' && !Array.isArray(root.hooks))
|
|
828
939
|
? root.hooks : {};
|
|
829
940
|
const source = tool.source ?? key;
|
|
830
|
-
const base = `${
|
|
941
|
+
const base = `${runtimeHookCommand()} --source ${source}`;
|
|
831
942
|
const timeout = (seconds) => (tool.timeoutMs ? seconds * 1000 : seconds);
|
|
832
943
|
|
|
833
944
|
// Remove every old CarMoji entry first, including events no longer in this
|
|
@@ -904,7 +1015,7 @@ function installKimiHooks(key, tool) {
|
|
|
904
1015
|
const path = join(homedir(), tool.config);
|
|
905
1016
|
let contents = existsSync(path) ? readFileSync(path, 'utf8') : '';
|
|
906
1017
|
contents = removeKimiBlocks(contents);
|
|
907
|
-
const command = `${
|
|
1018
|
+
const command = `${runtimeHookCommand()} --source kimi`.replace(/"/g, '\\"');
|
|
908
1019
|
const blocks = tool.events.map((event) => {
|
|
909
1020
|
const toolEvent = ['PreToolUse', 'PostToolUse', 'PostToolUseFailure'].includes(event);
|
|
910
1021
|
return `[[hooks]]\nevent = "${event}"\ncommand = "${command}"\ntimeout = 5`
|
|
@@ -924,7 +1035,7 @@ function installClineHooks(key, tool) {
|
|
|
924
1035
|
mkdirSync(dir, { recursive: true });
|
|
925
1036
|
const script = '#!/bin/sh\n'
|
|
926
1037
|
+ '# carmoji hook — forwards Cline events to the CarMoji pet\n'
|
|
927
|
-
+ `exec ${
|
|
1038
|
+
+ `exec ${runtimeHookCommand()} --source cline\n`;
|
|
928
1039
|
for (const event of tool.events) {
|
|
929
1040
|
const path = join(dir, event);
|
|
930
1041
|
if (existsSync(path) && !readFileSync(path, 'utf8').includes('carmoji')) {
|
|
@@ -1060,7 +1171,7 @@ function uninstallTool(key, { skipMissing = false } = {}) {
|
|
|
1060
1171
|
console.error(`Unknown tool "${key}". Known: ${Object.keys(TOOLS).join(', ')}`);
|
|
1061
1172
|
process.exit(1);
|
|
1062
1173
|
}
|
|
1063
|
-
// `
|
|
1174
|
+
// `unhook all` sweeps every known tool; stay quiet about the ones that
|
|
1064
1175
|
// were never installed instead of printing 14 "nothing installed" lines.
|
|
1065
1176
|
const nothing = () => {
|
|
1066
1177
|
if (!skipMissing) console.log(`${tool.name}: nothing installed.`);
|
|
@@ -1108,9 +1219,8 @@ function uninstallTool(key, { skipMissing = false } = {}) {
|
|
|
1108
1219
|
|
|
1109
1220
|
/**
|
|
1110
1221
|
* Merge the CarMoji hooks into ~/.claude/settings.json, keeping a backup.
|
|
1111
|
-
* Existing carmoji entries are replaced, not skipped, so
|
|
1112
|
-
*
|
|
1113
|
-
* always leaves the config pointing at whichever one ran the install.
|
|
1222
|
+
* Existing carmoji entries are replaced, not skipped, so every reinstall
|
|
1223
|
+
* points at the freshly staged runtime in ~/.config/carmoji.
|
|
1114
1224
|
*/
|
|
1115
1225
|
function installClaude() {
|
|
1116
1226
|
installJsonHooks('claude', TOOLS.claude);
|
|
@@ -1121,9 +1231,9 @@ function installClaude() {
|
|
|
1121
1231
|
* Codex versions without hooks.json support), keeping a backup. */
|
|
1122
1232
|
function installCodexNotify() {
|
|
1123
1233
|
const path = join(codexHome(), 'config.toml');
|
|
1124
|
-
const notifyLine =
|
|
1125
|
-
|
|
1126
|
-
|
|
1234
|
+
const notifyLine = `notify = ${JSON.stringify([
|
|
1235
|
+
'node', RUNTIME_SCRIPT_PATH, 'codex-notify',
|
|
1236
|
+
])}`;
|
|
1127
1237
|
|
|
1128
1238
|
let text = existsSync(path) ? readFileSync(path, 'utf8') : '';
|
|
1129
1239
|
const lines = text.split('\n');
|
|
@@ -1152,7 +1262,7 @@ function claudeConfigSnippet() {
|
|
|
1152
1262
|
const hooks = {};
|
|
1153
1263
|
for (const name of CLAUDE_HOOK_EVENTS) {
|
|
1154
1264
|
const timeout = name === 'PermissionRequest' ? PERMISSION_HOOK_TIMEOUT_S : 5;
|
|
1155
|
-
hooks[name] = [{ hooks: [{ type: 'command', command:
|
|
1265
|
+
hooks[name] = [{ hooks: [{ type: 'command', command: hookCommand(), timeout }] }];
|
|
1156
1266
|
}
|
|
1157
1267
|
return JSON.stringify({ hooks }, null, 2);
|
|
1158
1268
|
}
|
|
@@ -1291,6 +1401,27 @@ async function main() {
|
|
|
1291
1401
|
}
|
|
1292
1402
|
|
|
1293
1403
|
case 'hook': {
|
|
1404
|
+
// Setup mode overloads the event entry point deliberately:
|
|
1405
|
+
// carmoji hook codex writes Codex's hook config
|
|
1406
|
+
// carmoji hook --source codex receives one event on stdin
|
|
1407
|
+
// Installed hooks always start with a flag, so the two paths cannot
|
|
1408
|
+
// be confused.
|
|
1409
|
+
if (arg && !arg.startsWith('-')) {
|
|
1410
|
+
if (arg !== 'all' && !TOOLS[arg]) {
|
|
1411
|
+
console.error(`Unknown tool "${arg}". Known: ${Object.keys(TOOLS).join(', ')}`);
|
|
1412
|
+
process.exit(1);
|
|
1413
|
+
}
|
|
1414
|
+
installRuntime();
|
|
1415
|
+
if (arg === 'all') {
|
|
1416
|
+
for (const key of Object.keys(TOOLS)) {
|
|
1417
|
+
installTool(key, { skipMissing: true });
|
|
1418
|
+
}
|
|
1419
|
+
break;
|
|
1420
|
+
}
|
|
1421
|
+
installTool(arg);
|
|
1422
|
+
break;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1294
1425
|
// Runs inside the coding agent on every hook — must be silent, quick,
|
|
1295
1426
|
// and always exit 0 so it can never disturb the session. The two
|
|
1296
1427
|
// deliberate exceptions hold the hook open while the phone answers:
|
|
@@ -1620,38 +1751,36 @@ async function main() {
|
|
|
1620
1751
|
break;
|
|
1621
1752
|
}
|
|
1622
1753
|
|
|
1623
|
-
case '
|
|
1754
|
+
case 'unhook': {
|
|
1624
1755
|
if (arg === 'all') {
|
|
1625
1756
|
for (const key of Object.keys(TOOLS)) {
|
|
1626
|
-
|
|
1757
|
+
uninstallTool(key, { skipMissing: true });
|
|
1627
1758
|
}
|
|
1628
1759
|
break;
|
|
1629
1760
|
}
|
|
1630
1761
|
if (!arg) {
|
|
1631
|
-
console.log(`Usage: carmoji
|
|
1762
|
+
console.log(`Usage: carmoji unhook <tool>|all\nTools: ${Object.keys(TOOLS).join(', ')}`);
|
|
1632
1763
|
process.exit(1);
|
|
1633
1764
|
}
|
|
1634
|
-
|
|
1765
|
+
uninstallTool(arg);
|
|
1635
1766
|
break;
|
|
1636
1767
|
}
|
|
1637
1768
|
|
|
1638
|
-
case 'uninstall':
|
|
1639
|
-
if (arg
|
|
1640
|
-
|
|
1641
|
-
uninstallTool(key, { skipMissing: true });
|
|
1642
|
-
}
|
|
1643
|
-
break;
|
|
1644
|
-
}
|
|
1645
|
-
if (!arg) {
|
|
1646
|
-
console.log(`Usage: carmoji uninstall <tool>|all\nTools: ${Object.keys(TOOLS).join(', ')}`);
|
|
1769
|
+
case 'uninstall':
|
|
1770
|
+
if (arg) {
|
|
1771
|
+
console.error('`carmoji uninstall` takes no arguments. Use `carmoji unhook <tool>|all` to remove hooks.');
|
|
1647
1772
|
process.exit(1);
|
|
1648
1773
|
}
|
|
1649
|
-
|
|
1774
|
+
uninstallRuntime();
|
|
1775
|
+
break;
|
|
1776
|
+
|
|
1777
|
+
case 'install':
|
|
1778
|
+
console.error('`carmoji install <tool>|all` was renamed to `carmoji hook <tool>|all`.');
|
|
1779
|
+
process.exit(1);
|
|
1650
1780
|
break;
|
|
1651
|
-
}
|
|
1652
1781
|
|
|
1653
1782
|
case 'tools': {
|
|
1654
|
-
console.log('Supported tools (carmoji
|
|
1783
|
+
console.log('Supported tools (carmoji hook <tool>):');
|
|
1655
1784
|
for (const [key, tool] of Object.entries(TOOLS)) {
|
|
1656
1785
|
const where = key === 'codex' ? '$CODEX_HOME/hooks.json' : `~/${tool.config}`;
|
|
1657
1786
|
const found = isToolPresent(key, tool) ? '' : ' (not detected)';
|
|
@@ -1667,16 +1796,23 @@ async function main() {
|
|
|
1667
1796
|
else printHelp('config');
|
|
1668
1797
|
break;
|
|
1669
1798
|
|
|
1670
|
-
case '
|
|
1799
|
+
case 'hook-codex-notify':
|
|
1800
|
+
installRuntime();
|
|
1671
1801
|
installCodexNotify();
|
|
1672
1802
|
break;
|
|
1673
1803
|
|
|
1674
1804
|
// Hidden back-compat aliases — the app's setup sheet and old READMEs
|
|
1675
1805
|
// print these spellings, so they must keep working.
|
|
1806
|
+
case 'install-codex-notify':
|
|
1807
|
+
installRuntime();
|
|
1808
|
+
installCodexNotify();
|
|
1809
|
+
break;
|
|
1676
1810
|
case 'install-claude':
|
|
1811
|
+
installRuntime();
|
|
1677
1812
|
installTool('claude');
|
|
1678
1813
|
break;
|
|
1679
1814
|
case 'install-codex':
|
|
1815
|
+
installRuntime();
|
|
1680
1816
|
installTool('codex');
|
|
1681
1817
|
break;
|
|
1682
1818
|
case 'claude-config':
|
|
@@ -1696,21 +1832,24 @@ async function main() {
|
|
|
1696
1832
|
|
|
1697
1833
|
function printClaudeConfig() {
|
|
1698
1834
|
console.log('Merge this into the project’s .claude/settings.json (or ~/.claude/settings.json for everywhere):\n');
|
|
1699
|
-
console.log('(Or let `carmoji
|
|
1835
|
+
console.log('(Or let `carmoji hook claude` do it for you, with a backup.)\n');
|
|
1700
1836
|
console.log(claudeConfigSnippet());
|
|
1701
1837
|
}
|
|
1702
1838
|
|
|
1703
1839
|
function printCodexConfig() {
|
|
1704
1840
|
console.log('Modern Codex reads hooks from $CODEX_HOME/hooks.json — prefer:');
|
|
1705
|
-
console.log(' carmoji
|
|
1841
|
+
console.log(' carmoji hook codex\n');
|
|
1706
1842
|
console.log('For older Codex versions, add this line to ~/.codex/config.toml:\n');
|
|
1707
|
-
|
|
1708
|
-
? '
|
|
1709
|
-
:
|
|
1843
|
+
const program = existsSync(RUNTIME_SCRIPT_PATH)
|
|
1844
|
+
? ['node', RUNTIME_SCRIPT_PATH, 'codex-notify']
|
|
1845
|
+
: RUNNING_FROM_PACKAGE
|
|
1846
|
+
? ['npx', '-y', 'carmoji', 'codex-notify']
|
|
1847
|
+
: ['node', SCRIPT_PATH, 'codex-notify'];
|
|
1848
|
+
console.log(`notify = ${JSON.stringify(program)}`);
|
|
1710
1849
|
}
|
|
1711
1850
|
|
|
1712
1851
|
function printGateConfig() {
|
|
1713
|
-
console.log('Legacy/advanced: `carmoji
|
|
1852
|
+
console.log('Legacy/advanced: `carmoji hook claude` now installs a PermissionRequest');
|
|
1714
1853
|
console.log('hook that answers real permission prompts from the phone by itself.');
|
|
1715
1854
|
console.log('The PreToolUse gate below instead fires on EVERY matched tool call, even');
|
|
1716
1855
|
console.log('allowlisted ones — useful only on older Claude Code versions without the');
|
|
@@ -1720,7 +1859,7 @@ function printGateConfig() {
|
|
|
1720
1859
|
hooks: {
|
|
1721
1860
|
PreToolUse: [{
|
|
1722
1861
|
matcher: 'Bash',
|
|
1723
|
-
hooks: [{ type: 'command', command: `${
|
|
1862
|
+
hooks: [{ type: 'command', command: `${hookCommand()} --gate`, timeout: 60 }],
|
|
1724
1863
|
}],
|
|
1725
1864
|
},
|
|
1726
1865
|
}, null, 2));
|
|
@@ -1733,11 +1872,12 @@ const USAGE = `carmoji bridge — talk to the CarMoji app on your phone
|
|
|
1733
1872
|
pair <code> [ip[:port]] pair with a phone (code from its Code Buddy sheet)
|
|
1734
1873
|
unpair [<name|ip[:port]>|all] forget paired devices
|
|
1735
1874
|
discover list CarMoji devices on this Wi-Fi
|
|
1736
|
-
|
|
1737
|
-
|
|
1875
|
+
hook <tool>|all install/update hooks (tools: ${Object.keys(TOOLS).join(', ')})
|
|
1876
|
+
unhook <tool>|all remove CarMoji hooks from a tool's config
|
|
1877
|
+
uninstall remove ~/.config/carmoji/carmoji.js
|
|
1738
1878
|
tools show each tool's config path and whether it's present
|
|
1739
1879
|
test <event> send a fake event; \`test approval\` pops the Allow/Deny card
|
|
1740
|
-
hook
|
|
1880
|
+
hook [--source <tool>] event entry point used by installed hooks (reads stdin)
|
|
1741
1881
|
config <claude|codex|gate> print a manual config snippet
|
|
1742
1882
|
codex-notify legacy Codex notify entry point
|
|
1743
1883
|
|
|
@@ -1748,11 +1888,20 @@ accepted stdin JSON, \`carmoji test --help\` lists every test event.`;
|
|
|
1748
1888
|
function printHelp(topic) {
|
|
1749
1889
|
switch (topic) {
|
|
1750
1890
|
case 'hook':
|
|
1751
|
-
console.log(`carmoji hook
|
|
1891
|
+
console.log(`carmoji hook <tool>|all
|
|
1892
|
+
carmoji hook [--source <tool>] [--event <name>] [--gate]
|
|
1893
|
+
|
|
1894
|
+
With a tool name, installs or updates the network-independent runtime at
|
|
1895
|
+
~/.config/carmoji/carmoji.js and writes that tool's hook configuration.
|
|
1896
|
+
The \`all\` target hooks every supported tool detected on this machine. Re-running the
|
|
1897
|
+
command updates the runtime and replaces earlier CarMoji-owned entries.
|
|
1898
|
+
|
|
1899
|
+
Tools: ${Object.keys(TOOLS).join(', ')}
|
|
1752
1900
|
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
never disturb a session.
|
|
1901
|
+
Without a tool name, this is the event entry point used by those installed
|
|
1902
|
+
hooks. It reads ONE hook event as JSON on stdin, forwards it to every paired
|
|
1903
|
+
phone, and exits 0 no matter what so it can never disturb a session.
|
|
1904
|
+
Unpaired event delivery = immediate no-op.
|
|
1756
1905
|
|
|
1757
1906
|
Flags:
|
|
1758
1907
|
--source <tool> which agent is calling (default: claude) — sets the wire
|
|
@@ -1821,34 +1970,36 @@ without a coding agent.
|
|
|
1821
1970
|
case 'config':
|
|
1822
1971
|
console.log(`carmoji config <claude|codex|gate>
|
|
1823
1972
|
|
|
1824
|
-
claude the .claude/settings.json hooks snippet (what \`
|
|
1973
|
+
claude the .claude/settings.json hooks snippet (what \`hook claude\`
|
|
1825
1974
|
writes, including the ${PERMISSION_HOOK_TIMEOUT_S}s PermissionRequest hook)
|
|
1826
1975
|
codex the legacy ~/.codex/config.toml notify line for Codex versions
|
|
1827
|
-
without hooks.json (modern Codex: \`carmoji
|
|
1976
|
+
without hooks.json (modern Codex: \`carmoji hook codex\`)
|
|
1828
1977
|
gate the legacy PreToolUse gate snippet — fires on every matched call;
|
|
1829
|
-
superseded by the PermissionRequest hook \`
|
|
1978
|
+
superseded by the PermissionRequest hook \`hook claude\` sets up`);
|
|
1830
1979
|
break;
|
|
1831
1980
|
|
|
1832
1981
|
case 'install':
|
|
1833
|
-
console.log(`carmoji install <tool>|all
|
|
1834
|
-
|
|
1835
|
-
Writes CarMoji's hooks into the tool's own config (a .bak backup is saved
|
|
1836
|
-
next to anything modified; reinstalling replaces earlier CarMoji entries).
|
|
1837
|
-
\`all\` installs into every tool detected on this machine.
|
|
1838
|
-
|
|
1839
|
-
Tools: ${Object.keys(TOOLS).join(', ')}
|
|
1840
|
-
\`carmoji tools\` shows each one's config path and whether it's present.`);
|
|
1982
|
+
console.log('`carmoji install <tool>|all` was renamed to `carmoji hook <tool>|all`.');
|
|
1841
1983
|
break;
|
|
1842
1984
|
|
|
1843
|
-
case '
|
|
1844
|
-
console.log(`carmoji
|
|
1985
|
+
case 'unhook':
|
|
1986
|
+
console.log(`carmoji unhook <tool>|all
|
|
1845
1987
|
|
|
1846
1988
|
Removes every CarMoji entry from the tool's hook config, leaving other
|
|
1847
|
-
hooks untouched. \`all\` sweeps every known tool quietly.
|
|
1989
|
+
hooks untouched. \`all\` sweeps every known tool quietly. It does not remove
|
|
1990
|
+
the persistent runtime; use \`carmoji uninstall\` for that.
|
|
1848
1991
|
|
|
1849
1992
|
Tools: ${Object.keys(TOOLS).join(', ')}`);
|
|
1850
1993
|
break;
|
|
1851
1994
|
|
|
1995
|
+
case 'uninstall':
|
|
1996
|
+
console.log(`carmoji uninstall
|
|
1997
|
+
|
|
1998
|
+
Removes ~/.config/carmoji/carmoji.js. Pairing state and caches are preserved.
|
|
1999
|
+
This does not edit any tool config; run \`carmoji unhook <tool>|all\` first if
|
|
2000
|
+
you also want to remove installed hooks.`);
|
|
2001
|
+
break;
|
|
2002
|
+
|
|
1852
2003
|
default:
|
|
1853
2004
|
console.log(USAGE);
|
|
1854
2005
|
}
|
package/package.json
CHANGED