carmoji 0.3.15 → 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 +205 -65
- 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'));
|
|
@@ -749,7 +849,7 @@ const CURSOR_STYLE_EVENTS = [
|
|
|
749
849
|
];
|
|
750
850
|
|
|
751
851
|
/**
|
|
752
|
-
* Every tool `carmoji
|
|
852
|
+
* Every tool `carmoji hook <name>` knows. `source` defaults to the key;
|
|
753
853
|
* `timeoutMs` marks tools whose hook timeouts are milliseconds; `eventFlag`
|
|
754
854
|
* marks tools whose stdin lacks the event name (the command carries
|
|
755
855
|
* `--event <name>`); `requiresDir` gates installation on the tool actually
|
|
@@ -838,7 +938,7 @@ function installJsonHooks(key, tool) {
|
|
|
838
938
|
const hooks = (root.hooks && typeof root.hooks === 'object' && !Array.isArray(root.hooks))
|
|
839
939
|
? root.hooks : {};
|
|
840
940
|
const source = tool.source ?? key;
|
|
841
|
-
const base = `${
|
|
941
|
+
const base = `${runtimeHookCommand()} --source ${source}`;
|
|
842
942
|
const timeout = (seconds) => (tool.timeoutMs ? seconds * 1000 : seconds);
|
|
843
943
|
|
|
844
944
|
// Remove every old CarMoji entry first, including events no longer in this
|
|
@@ -915,7 +1015,7 @@ function installKimiHooks(key, tool) {
|
|
|
915
1015
|
const path = join(homedir(), tool.config);
|
|
916
1016
|
let contents = existsSync(path) ? readFileSync(path, 'utf8') : '';
|
|
917
1017
|
contents = removeKimiBlocks(contents);
|
|
918
|
-
const command = `${
|
|
1018
|
+
const command = `${runtimeHookCommand()} --source kimi`.replace(/"/g, '\\"');
|
|
919
1019
|
const blocks = tool.events.map((event) => {
|
|
920
1020
|
const toolEvent = ['PreToolUse', 'PostToolUse', 'PostToolUseFailure'].includes(event);
|
|
921
1021
|
return `[[hooks]]\nevent = "${event}"\ncommand = "${command}"\ntimeout = 5`
|
|
@@ -935,7 +1035,7 @@ function installClineHooks(key, tool) {
|
|
|
935
1035
|
mkdirSync(dir, { recursive: true });
|
|
936
1036
|
const script = '#!/bin/sh\n'
|
|
937
1037
|
+ '# carmoji hook — forwards Cline events to the CarMoji pet\n'
|
|
938
|
-
+ `exec ${
|
|
1038
|
+
+ `exec ${runtimeHookCommand()} --source cline\n`;
|
|
939
1039
|
for (const event of tool.events) {
|
|
940
1040
|
const path = join(dir, event);
|
|
941
1041
|
if (existsSync(path) && !readFileSync(path, 'utf8').includes('carmoji')) {
|
|
@@ -1071,7 +1171,7 @@ function uninstallTool(key, { skipMissing = false } = {}) {
|
|
|
1071
1171
|
console.error(`Unknown tool "${key}". Known: ${Object.keys(TOOLS).join(', ')}`);
|
|
1072
1172
|
process.exit(1);
|
|
1073
1173
|
}
|
|
1074
|
-
// `
|
|
1174
|
+
// `unhook all` sweeps every known tool; stay quiet about the ones that
|
|
1075
1175
|
// were never installed instead of printing 14 "nothing installed" lines.
|
|
1076
1176
|
const nothing = () => {
|
|
1077
1177
|
if (!skipMissing) console.log(`${tool.name}: nothing installed.`);
|
|
@@ -1119,9 +1219,8 @@ function uninstallTool(key, { skipMissing = false } = {}) {
|
|
|
1119
1219
|
|
|
1120
1220
|
/**
|
|
1121
1221
|
* Merge the CarMoji hooks into ~/.claude/settings.json, keeping a backup.
|
|
1122
|
-
* Existing carmoji entries are replaced, not skipped, so
|
|
1123
|
-
*
|
|
1124
|
-
* 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.
|
|
1125
1224
|
*/
|
|
1126
1225
|
function installClaude() {
|
|
1127
1226
|
installJsonHooks('claude', TOOLS.claude);
|
|
@@ -1132,9 +1231,9 @@ function installClaude() {
|
|
|
1132
1231
|
* Codex versions without hooks.json support), keeping a backup. */
|
|
1133
1232
|
function installCodexNotify() {
|
|
1134
1233
|
const path = join(codexHome(), 'config.toml');
|
|
1135
|
-
const notifyLine =
|
|
1136
|
-
|
|
1137
|
-
|
|
1234
|
+
const notifyLine = `notify = ${JSON.stringify([
|
|
1235
|
+
'node', RUNTIME_SCRIPT_PATH, 'codex-notify',
|
|
1236
|
+
])}`;
|
|
1138
1237
|
|
|
1139
1238
|
let text = existsSync(path) ? readFileSync(path, 'utf8') : '';
|
|
1140
1239
|
const lines = text.split('\n');
|
|
@@ -1163,7 +1262,7 @@ function claudeConfigSnippet() {
|
|
|
1163
1262
|
const hooks = {};
|
|
1164
1263
|
for (const name of CLAUDE_HOOK_EVENTS) {
|
|
1165
1264
|
const timeout = name === 'PermissionRequest' ? PERMISSION_HOOK_TIMEOUT_S : 5;
|
|
1166
|
-
hooks[name] = [{ hooks: [{ type: 'command', command:
|
|
1265
|
+
hooks[name] = [{ hooks: [{ type: 'command', command: hookCommand(), timeout }] }];
|
|
1167
1266
|
}
|
|
1168
1267
|
return JSON.stringify({ hooks }, null, 2);
|
|
1169
1268
|
}
|
|
@@ -1302,6 +1401,27 @@ async function main() {
|
|
|
1302
1401
|
}
|
|
1303
1402
|
|
|
1304
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
|
+
|
|
1305
1425
|
// Runs inside the coding agent on every hook — must be silent, quick,
|
|
1306
1426
|
// and always exit 0 so it can never disturb the session. The two
|
|
1307
1427
|
// deliberate exceptions hold the hook open while the phone answers:
|
|
@@ -1631,38 +1751,36 @@ async function main() {
|
|
|
1631
1751
|
break;
|
|
1632
1752
|
}
|
|
1633
1753
|
|
|
1634
|
-
case '
|
|
1754
|
+
case 'unhook': {
|
|
1635
1755
|
if (arg === 'all') {
|
|
1636
1756
|
for (const key of Object.keys(TOOLS)) {
|
|
1637
|
-
|
|
1757
|
+
uninstallTool(key, { skipMissing: true });
|
|
1638
1758
|
}
|
|
1639
1759
|
break;
|
|
1640
1760
|
}
|
|
1641
1761
|
if (!arg) {
|
|
1642
|
-
console.log(`Usage: carmoji
|
|
1762
|
+
console.log(`Usage: carmoji unhook <tool>|all\nTools: ${Object.keys(TOOLS).join(', ')}`);
|
|
1643
1763
|
process.exit(1);
|
|
1644
1764
|
}
|
|
1645
|
-
|
|
1765
|
+
uninstallTool(arg);
|
|
1646
1766
|
break;
|
|
1647
1767
|
}
|
|
1648
1768
|
|
|
1649
|
-
case 'uninstall':
|
|
1650
|
-
if (arg
|
|
1651
|
-
|
|
1652
|
-
uninstallTool(key, { skipMissing: true });
|
|
1653
|
-
}
|
|
1654
|
-
break;
|
|
1655
|
-
}
|
|
1656
|
-
if (!arg) {
|
|
1657
|
-
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.');
|
|
1658
1772
|
process.exit(1);
|
|
1659
1773
|
}
|
|
1660
|
-
|
|
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);
|
|
1661
1780
|
break;
|
|
1662
|
-
}
|
|
1663
1781
|
|
|
1664
1782
|
case 'tools': {
|
|
1665
|
-
console.log('Supported tools (carmoji
|
|
1783
|
+
console.log('Supported tools (carmoji hook <tool>):');
|
|
1666
1784
|
for (const [key, tool] of Object.entries(TOOLS)) {
|
|
1667
1785
|
const where = key === 'codex' ? '$CODEX_HOME/hooks.json' : `~/${tool.config}`;
|
|
1668
1786
|
const found = isToolPresent(key, tool) ? '' : ' (not detected)';
|
|
@@ -1678,16 +1796,23 @@ async function main() {
|
|
|
1678
1796
|
else printHelp('config');
|
|
1679
1797
|
break;
|
|
1680
1798
|
|
|
1681
|
-
case '
|
|
1799
|
+
case 'hook-codex-notify':
|
|
1800
|
+
installRuntime();
|
|
1682
1801
|
installCodexNotify();
|
|
1683
1802
|
break;
|
|
1684
1803
|
|
|
1685
1804
|
// Hidden back-compat aliases — the app's setup sheet and old READMEs
|
|
1686
1805
|
// print these spellings, so they must keep working.
|
|
1806
|
+
case 'install-codex-notify':
|
|
1807
|
+
installRuntime();
|
|
1808
|
+
installCodexNotify();
|
|
1809
|
+
break;
|
|
1687
1810
|
case 'install-claude':
|
|
1811
|
+
installRuntime();
|
|
1688
1812
|
installTool('claude');
|
|
1689
1813
|
break;
|
|
1690
1814
|
case 'install-codex':
|
|
1815
|
+
installRuntime();
|
|
1691
1816
|
installTool('codex');
|
|
1692
1817
|
break;
|
|
1693
1818
|
case 'claude-config':
|
|
@@ -1707,21 +1832,24 @@ async function main() {
|
|
|
1707
1832
|
|
|
1708
1833
|
function printClaudeConfig() {
|
|
1709
1834
|
console.log('Merge this into the project’s .claude/settings.json (or ~/.claude/settings.json for everywhere):\n');
|
|
1710
|
-
console.log('(Or let `carmoji
|
|
1835
|
+
console.log('(Or let `carmoji hook claude` do it for you, with a backup.)\n');
|
|
1711
1836
|
console.log(claudeConfigSnippet());
|
|
1712
1837
|
}
|
|
1713
1838
|
|
|
1714
1839
|
function printCodexConfig() {
|
|
1715
1840
|
console.log('Modern Codex reads hooks from $CODEX_HOME/hooks.json — prefer:');
|
|
1716
|
-
console.log(' carmoji
|
|
1841
|
+
console.log(' carmoji hook codex\n');
|
|
1717
1842
|
console.log('For older Codex versions, add this line to ~/.codex/config.toml:\n');
|
|
1718
|
-
|
|
1719
|
-
? '
|
|
1720
|
-
:
|
|
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)}`);
|
|
1721
1849
|
}
|
|
1722
1850
|
|
|
1723
1851
|
function printGateConfig() {
|
|
1724
|
-
console.log('Legacy/advanced: `carmoji
|
|
1852
|
+
console.log('Legacy/advanced: `carmoji hook claude` now installs a PermissionRequest');
|
|
1725
1853
|
console.log('hook that answers real permission prompts from the phone by itself.');
|
|
1726
1854
|
console.log('The PreToolUse gate below instead fires on EVERY matched tool call, even');
|
|
1727
1855
|
console.log('allowlisted ones — useful only on older Claude Code versions without the');
|
|
@@ -1731,7 +1859,7 @@ function printGateConfig() {
|
|
|
1731
1859
|
hooks: {
|
|
1732
1860
|
PreToolUse: [{
|
|
1733
1861
|
matcher: 'Bash',
|
|
1734
|
-
hooks: [{ type: 'command', command: `${
|
|
1862
|
+
hooks: [{ type: 'command', command: `${hookCommand()} --gate`, timeout: 60 }],
|
|
1735
1863
|
}],
|
|
1736
1864
|
},
|
|
1737
1865
|
}, null, 2));
|
|
@@ -1744,11 +1872,12 @@ const USAGE = `carmoji bridge — talk to the CarMoji app on your phone
|
|
|
1744
1872
|
pair <code> [ip[:port]] pair with a phone (code from its Code Buddy sheet)
|
|
1745
1873
|
unpair [<name|ip[:port]>|all] forget paired devices
|
|
1746
1874
|
discover list CarMoji devices on this Wi-Fi
|
|
1747
|
-
|
|
1748
|
-
|
|
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
|
|
1749
1878
|
tools show each tool's config path and whether it's present
|
|
1750
1879
|
test <event> send a fake event; \`test approval\` pops the Allow/Deny card
|
|
1751
|
-
hook
|
|
1880
|
+
hook [--source <tool>] event entry point used by installed hooks (reads stdin)
|
|
1752
1881
|
config <claude|codex|gate> print a manual config snippet
|
|
1753
1882
|
codex-notify legacy Codex notify entry point
|
|
1754
1883
|
|
|
@@ -1759,11 +1888,20 @@ accepted stdin JSON, \`carmoji test --help\` lists every test event.`;
|
|
|
1759
1888
|
function printHelp(topic) {
|
|
1760
1889
|
switch (topic) {
|
|
1761
1890
|
case 'hook':
|
|
1762
|
-
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(', ')}
|
|
1763
1900
|
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
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.
|
|
1767
1905
|
|
|
1768
1906
|
Flags:
|
|
1769
1907
|
--source <tool> which agent is calling (default: claude) — sets the wire
|
|
@@ -1832,34 +1970,36 @@ without a coding agent.
|
|
|
1832
1970
|
case 'config':
|
|
1833
1971
|
console.log(`carmoji config <claude|codex|gate>
|
|
1834
1972
|
|
|
1835
|
-
claude the .claude/settings.json hooks snippet (what \`
|
|
1973
|
+
claude the .claude/settings.json hooks snippet (what \`hook claude\`
|
|
1836
1974
|
writes, including the ${PERMISSION_HOOK_TIMEOUT_S}s PermissionRequest hook)
|
|
1837
1975
|
codex the legacy ~/.codex/config.toml notify line for Codex versions
|
|
1838
|
-
without hooks.json (modern Codex: \`carmoji
|
|
1976
|
+
without hooks.json (modern Codex: \`carmoji hook codex\`)
|
|
1839
1977
|
gate the legacy PreToolUse gate snippet — fires on every matched call;
|
|
1840
|
-
superseded by the PermissionRequest hook \`
|
|
1978
|
+
superseded by the PermissionRequest hook \`hook claude\` sets up`);
|
|
1841
1979
|
break;
|
|
1842
1980
|
|
|
1843
1981
|
case 'install':
|
|
1844
|
-
console.log(`carmoji install <tool>|all
|
|
1845
|
-
|
|
1846
|
-
Writes CarMoji's hooks into the tool's own config (a .bak backup is saved
|
|
1847
|
-
next to anything modified; reinstalling replaces earlier CarMoji entries).
|
|
1848
|
-
\`all\` installs into every tool detected on this machine.
|
|
1849
|
-
|
|
1850
|
-
Tools: ${Object.keys(TOOLS).join(', ')}
|
|
1851
|
-
\`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`.');
|
|
1852
1983
|
break;
|
|
1853
1984
|
|
|
1854
|
-
case '
|
|
1855
|
-
console.log(`carmoji
|
|
1985
|
+
case 'unhook':
|
|
1986
|
+
console.log(`carmoji unhook <tool>|all
|
|
1856
1987
|
|
|
1857
1988
|
Removes every CarMoji entry from the tool's hook config, leaving other
|
|
1858
|
-
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.
|
|
1859
1991
|
|
|
1860
1992
|
Tools: ${Object.keys(TOOLS).join(', ')}`);
|
|
1861
1993
|
break;
|
|
1862
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
|
+
|
|
1863
2003
|
default:
|
|
1864
2004
|
console.log(USAGE);
|
|
1865
2005
|
}
|
package/package.json
CHANGED