carmoji 0.3.15 → 0.3.17
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 +233 -66
- 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'));
|
|
@@ -690,6 +790,19 @@ const TEST_EVENTS = {
|
|
|
690
790
|
error: { event: 'error' },
|
|
691
791
|
permission: { event: 'permission' },
|
|
692
792
|
end: { event: 'session_end' },
|
|
793
|
+
gamepad: { event: 'standby_activity', activity: 'gamepad' },
|
|
794
|
+
tinkering: { event: 'standby_activity', activity: 'tinkering' },
|
|
795
|
+
wheel: { event: 'standby_activity', activity: 'wheel' },
|
|
796
|
+
puzzle: { event: 'standby_activity', activity: 'puzzle' },
|
|
797
|
+
plant: { event: 'standby_activity', activity: 'plant' },
|
|
798
|
+
blocks: { event: 'standby_activity', activity: 'blocks' },
|
|
799
|
+
plane: { event: 'standby_activity', activity: 'plane' },
|
|
800
|
+
yarn: { event: 'standby_activity', activity: 'yarn' },
|
|
801
|
+
sketch: { event: 'standby_activity', activity: 'sketch' },
|
|
802
|
+
bubbles: { event: 'standby_activity', activity: 'bubbles' },
|
|
803
|
+
snack: { event: 'standby_activity', activity: 'snack' },
|
|
804
|
+
phone: { event: 'standby_activity', activity: 'phone' },
|
|
805
|
+
piano: { event: 'standby_activity', activity: 'piano' },
|
|
693
806
|
};
|
|
694
807
|
|
|
695
808
|
/** Copy `path` to `path.bak` before modifying it; returns the backup path. */
|
|
@@ -749,7 +862,7 @@ const CURSOR_STYLE_EVENTS = [
|
|
|
749
862
|
];
|
|
750
863
|
|
|
751
864
|
/**
|
|
752
|
-
* Every tool `carmoji
|
|
865
|
+
* Every tool `carmoji hook <name>` knows. `source` defaults to the key;
|
|
753
866
|
* `timeoutMs` marks tools whose hook timeouts are milliseconds; `eventFlag`
|
|
754
867
|
* marks tools whose stdin lacks the event name (the command carries
|
|
755
868
|
* `--event <name>`); `requiresDir` gates installation on the tool actually
|
|
@@ -838,7 +951,7 @@ function installJsonHooks(key, tool) {
|
|
|
838
951
|
const hooks = (root.hooks && typeof root.hooks === 'object' && !Array.isArray(root.hooks))
|
|
839
952
|
? root.hooks : {};
|
|
840
953
|
const source = tool.source ?? key;
|
|
841
|
-
const base = `${
|
|
954
|
+
const base = `${runtimeHookCommand()} --source ${source}`;
|
|
842
955
|
const timeout = (seconds) => (tool.timeoutMs ? seconds * 1000 : seconds);
|
|
843
956
|
|
|
844
957
|
// Remove every old CarMoji entry first, including events no longer in this
|
|
@@ -915,7 +1028,7 @@ function installKimiHooks(key, tool) {
|
|
|
915
1028
|
const path = join(homedir(), tool.config);
|
|
916
1029
|
let contents = existsSync(path) ? readFileSync(path, 'utf8') : '';
|
|
917
1030
|
contents = removeKimiBlocks(contents);
|
|
918
|
-
const command = `${
|
|
1031
|
+
const command = `${runtimeHookCommand()} --source kimi`.replace(/"/g, '\\"');
|
|
919
1032
|
const blocks = tool.events.map((event) => {
|
|
920
1033
|
const toolEvent = ['PreToolUse', 'PostToolUse', 'PostToolUseFailure'].includes(event);
|
|
921
1034
|
return `[[hooks]]\nevent = "${event}"\ncommand = "${command}"\ntimeout = 5`
|
|
@@ -935,7 +1048,7 @@ function installClineHooks(key, tool) {
|
|
|
935
1048
|
mkdirSync(dir, { recursive: true });
|
|
936
1049
|
const script = '#!/bin/sh\n'
|
|
937
1050
|
+ '# carmoji hook — forwards Cline events to the CarMoji pet\n'
|
|
938
|
-
+ `exec ${
|
|
1051
|
+
+ `exec ${runtimeHookCommand()} --source cline\n`;
|
|
939
1052
|
for (const event of tool.events) {
|
|
940
1053
|
const path = join(dir, event);
|
|
941
1054
|
if (existsSync(path) && !readFileSync(path, 'utf8').includes('carmoji')) {
|
|
@@ -1071,7 +1184,7 @@ function uninstallTool(key, { skipMissing = false } = {}) {
|
|
|
1071
1184
|
console.error(`Unknown tool "${key}". Known: ${Object.keys(TOOLS).join(', ')}`);
|
|
1072
1185
|
process.exit(1);
|
|
1073
1186
|
}
|
|
1074
|
-
// `
|
|
1187
|
+
// `unhook all` sweeps every known tool; stay quiet about the ones that
|
|
1075
1188
|
// were never installed instead of printing 14 "nothing installed" lines.
|
|
1076
1189
|
const nothing = () => {
|
|
1077
1190
|
if (!skipMissing) console.log(`${tool.name}: nothing installed.`);
|
|
@@ -1119,9 +1232,8 @@ function uninstallTool(key, { skipMissing = false } = {}) {
|
|
|
1119
1232
|
|
|
1120
1233
|
/**
|
|
1121
1234
|
* 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.
|
|
1235
|
+
* Existing carmoji entries are replaced, not skipped, so every reinstall
|
|
1236
|
+
* points at the freshly staged runtime in ~/.config/carmoji.
|
|
1125
1237
|
*/
|
|
1126
1238
|
function installClaude() {
|
|
1127
1239
|
installJsonHooks('claude', TOOLS.claude);
|
|
@@ -1132,9 +1244,9 @@ function installClaude() {
|
|
|
1132
1244
|
* Codex versions without hooks.json support), keeping a backup. */
|
|
1133
1245
|
function installCodexNotify() {
|
|
1134
1246
|
const path = join(codexHome(), 'config.toml');
|
|
1135
|
-
const notifyLine =
|
|
1136
|
-
|
|
1137
|
-
|
|
1247
|
+
const notifyLine = `notify = ${JSON.stringify([
|
|
1248
|
+
'node', RUNTIME_SCRIPT_PATH, 'codex-notify',
|
|
1249
|
+
])}`;
|
|
1138
1250
|
|
|
1139
1251
|
let text = existsSync(path) ? readFileSync(path, 'utf8') : '';
|
|
1140
1252
|
const lines = text.split('\n');
|
|
@@ -1163,7 +1275,7 @@ function claudeConfigSnippet() {
|
|
|
1163
1275
|
const hooks = {};
|
|
1164
1276
|
for (const name of CLAUDE_HOOK_EVENTS) {
|
|
1165
1277
|
const timeout = name === 'PermissionRequest' ? PERMISSION_HOOK_TIMEOUT_S : 5;
|
|
1166
|
-
hooks[name] = [{ hooks: [{ type: 'command', command:
|
|
1278
|
+
hooks[name] = [{ hooks: [{ type: 'command', command: hookCommand(), timeout }] }];
|
|
1167
1279
|
}
|
|
1168
1280
|
return JSON.stringify({ hooks }, null, 2);
|
|
1169
1281
|
}
|
|
@@ -1302,6 +1414,27 @@ async function main() {
|
|
|
1302
1414
|
}
|
|
1303
1415
|
|
|
1304
1416
|
case 'hook': {
|
|
1417
|
+
// Setup mode overloads the event entry point deliberately:
|
|
1418
|
+
// carmoji hook codex writes Codex's hook config
|
|
1419
|
+
// carmoji hook --source codex receives one event on stdin
|
|
1420
|
+
// Installed hooks always start with a flag, so the two paths cannot
|
|
1421
|
+
// be confused.
|
|
1422
|
+
if (arg && !arg.startsWith('-')) {
|
|
1423
|
+
if (arg !== 'all' && !TOOLS[arg]) {
|
|
1424
|
+
console.error(`Unknown tool "${arg}". Known: ${Object.keys(TOOLS).join(', ')}`);
|
|
1425
|
+
process.exit(1);
|
|
1426
|
+
}
|
|
1427
|
+
installRuntime();
|
|
1428
|
+
if (arg === 'all') {
|
|
1429
|
+
for (const key of Object.keys(TOOLS)) {
|
|
1430
|
+
installTool(key, { skipMissing: true });
|
|
1431
|
+
}
|
|
1432
|
+
break;
|
|
1433
|
+
}
|
|
1434
|
+
installTool(arg);
|
|
1435
|
+
break;
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1305
1438
|
// Runs inside the coding agent on every hook — must be silent, quick,
|
|
1306
1439
|
// and always exit 0 so it can never disturb the session. The two
|
|
1307
1440
|
// deliberate exceptions hold the hook open while the phone answers:
|
|
@@ -1631,38 +1764,36 @@ async function main() {
|
|
|
1631
1764
|
break;
|
|
1632
1765
|
}
|
|
1633
1766
|
|
|
1634
|
-
case '
|
|
1767
|
+
case 'unhook': {
|
|
1635
1768
|
if (arg === 'all') {
|
|
1636
1769
|
for (const key of Object.keys(TOOLS)) {
|
|
1637
|
-
|
|
1770
|
+
uninstallTool(key, { skipMissing: true });
|
|
1638
1771
|
}
|
|
1639
1772
|
break;
|
|
1640
1773
|
}
|
|
1641
1774
|
if (!arg) {
|
|
1642
|
-
console.log(`Usage: carmoji
|
|
1775
|
+
console.log(`Usage: carmoji unhook <tool>|all\nTools: ${Object.keys(TOOLS).join(', ')}`);
|
|
1643
1776
|
process.exit(1);
|
|
1644
1777
|
}
|
|
1645
|
-
|
|
1778
|
+
uninstallTool(arg);
|
|
1646
1779
|
break;
|
|
1647
1780
|
}
|
|
1648
1781
|
|
|
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(', ')}`);
|
|
1782
|
+
case 'uninstall':
|
|
1783
|
+
if (arg) {
|
|
1784
|
+
console.error('`carmoji uninstall` takes no arguments. Use `carmoji unhook <tool>|all` to remove hooks.');
|
|
1658
1785
|
process.exit(1);
|
|
1659
1786
|
}
|
|
1660
|
-
|
|
1787
|
+
uninstallRuntime();
|
|
1788
|
+
break;
|
|
1789
|
+
|
|
1790
|
+
case 'install':
|
|
1791
|
+
console.error('`carmoji install <tool>|all` was renamed to `carmoji hook <tool>|all`.');
|
|
1792
|
+
process.exit(1);
|
|
1661
1793
|
break;
|
|
1662
|
-
}
|
|
1663
1794
|
|
|
1664
1795
|
case 'tools': {
|
|
1665
|
-
console.log('Supported tools (carmoji
|
|
1796
|
+
console.log('Supported tools (carmoji hook <tool>):');
|
|
1666
1797
|
for (const [key, tool] of Object.entries(TOOLS)) {
|
|
1667
1798
|
const where = key === 'codex' ? '$CODEX_HOME/hooks.json' : `~/${tool.config}`;
|
|
1668
1799
|
const found = isToolPresent(key, tool) ? '' : ' (not detected)';
|
|
@@ -1678,16 +1809,23 @@ async function main() {
|
|
|
1678
1809
|
else printHelp('config');
|
|
1679
1810
|
break;
|
|
1680
1811
|
|
|
1681
|
-
case '
|
|
1812
|
+
case 'hook-codex-notify':
|
|
1813
|
+
installRuntime();
|
|
1682
1814
|
installCodexNotify();
|
|
1683
1815
|
break;
|
|
1684
1816
|
|
|
1685
1817
|
// Hidden back-compat aliases — the app's setup sheet and old READMEs
|
|
1686
1818
|
// print these spellings, so they must keep working.
|
|
1819
|
+
case 'install-codex-notify':
|
|
1820
|
+
installRuntime();
|
|
1821
|
+
installCodexNotify();
|
|
1822
|
+
break;
|
|
1687
1823
|
case 'install-claude':
|
|
1824
|
+
installRuntime();
|
|
1688
1825
|
installTool('claude');
|
|
1689
1826
|
break;
|
|
1690
1827
|
case 'install-codex':
|
|
1828
|
+
installRuntime();
|
|
1691
1829
|
installTool('codex');
|
|
1692
1830
|
break;
|
|
1693
1831
|
case 'claude-config':
|
|
@@ -1707,21 +1845,24 @@ async function main() {
|
|
|
1707
1845
|
|
|
1708
1846
|
function printClaudeConfig() {
|
|
1709
1847
|
console.log('Merge this into the project’s .claude/settings.json (or ~/.claude/settings.json for everywhere):\n');
|
|
1710
|
-
console.log('(Or let `carmoji
|
|
1848
|
+
console.log('(Or let `carmoji hook claude` do it for you, with a backup.)\n');
|
|
1711
1849
|
console.log(claudeConfigSnippet());
|
|
1712
1850
|
}
|
|
1713
1851
|
|
|
1714
1852
|
function printCodexConfig() {
|
|
1715
1853
|
console.log('Modern Codex reads hooks from $CODEX_HOME/hooks.json — prefer:');
|
|
1716
|
-
console.log(' carmoji
|
|
1854
|
+
console.log(' carmoji hook codex\n');
|
|
1717
1855
|
console.log('For older Codex versions, add this line to ~/.codex/config.toml:\n');
|
|
1718
|
-
|
|
1719
|
-
? '
|
|
1720
|
-
:
|
|
1856
|
+
const program = existsSync(RUNTIME_SCRIPT_PATH)
|
|
1857
|
+
? ['node', RUNTIME_SCRIPT_PATH, 'codex-notify']
|
|
1858
|
+
: RUNNING_FROM_PACKAGE
|
|
1859
|
+
? ['npx', '-y', 'carmoji', 'codex-notify']
|
|
1860
|
+
: ['node', SCRIPT_PATH, 'codex-notify'];
|
|
1861
|
+
console.log(`notify = ${JSON.stringify(program)}`);
|
|
1721
1862
|
}
|
|
1722
1863
|
|
|
1723
1864
|
function printGateConfig() {
|
|
1724
|
-
console.log('Legacy/advanced: `carmoji
|
|
1865
|
+
console.log('Legacy/advanced: `carmoji hook claude` now installs a PermissionRequest');
|
|
1725
1866
|
console.log('hook that answers real permission prompts from the phone by itself.');
|
|
1726
1867
|
console.log('The PreToolUse gate below instead fires on EVERY matched tool call, even');
|
|
1727
1868
|
console.log('allowlisted ones — useful only on older Claude Code versions without the');
|
|
@@ -1731,7 +1872,7 @@ function printGateConfig() {
|
|
|
1731
1872
|
hooks: {
|
|
1732
1873
|
PreToolUse: [{
|
|
1733
1874
|
matcher: 'Bash',
|
|
1734
|
-
hooks: [{ type: 'command', command: `${
|
|
1875
|
+
hooks: [{ type: 'command', command: `${hookCommand()} --gate`, timeout: 60 }],
|
|
1735
1876
|
}],
|
|
1736
1877
|
},
|
|
1737
1878
|
}, null, 2));
|
|
@@ -1744,11 +1885,12 @@ const USAGE = `carmoji bridge — talk to the CarMoji app on your phone
|
|
|
1744
1885
|
pair <code> [ip[:port]] pair with a phone (code from its Code Buddy sheet)
|
|
1745
1886
|
unpair [<name|ip[:port]>|all] forget paired devices
|
|
1746
1887
|
discover list CarMoji devices on this Wi-Fi
|
|
1747
|
-
|
|
1748
|
-
|
|
1888
|
+
hook <tool>|all install/update hooks (tools: ${Object.keys(TOOLS).join(', ')})
|
|
1889
|
+
unhook <tool>|all remove CarMoji hooks from a tool's config
|
|
1890
|
+
uninstall remove ~/.config/carmoji/carmoji.js
|
|
1749
1891
|
tools show each tool's config path and whether it's present
|
|
1750
1892
|
test <event> send a fake event; \`test approval\` pops the Allow/Deny card
|
|
1751
|
-
hook
|
|
1893
|
+
hook [--source <tool>] event entry point used by installed hooks (reads stdin)
|
|
1752
1894
|
config <claude|codex|gate> print a manual config snippet
|
|
1753
1895
|
codex-notify legacy Codex notify entry point
|
|
1754
1896
|
|
|
@@ -1759,11 +1901,20 @@ accepted stdin JSON, \`carmoji test --help\` lists every test event.`;
|
|
|
1759
1901
|
function printHelp(topic) {
|
|
1760
1902
|
switch (topic) {
|
|
1761
1903
|
case 'hook':
|
|
1762
|
-
console.log(`carmoji hook
|
|
1904
|
+
console.log(`carmoji hook <tool>|all
|
|
1905
|
+
carmoji hook [--source <tool>] [--event <name>] [--gate]
|
|
1906
|
+
|
|
1907
|
+
With a tool name, installs or updates the network-independent runtime at
|
|
1908
|
+
~/.config/carmoji/carmoji.js and writes that tool's hook configuration.
|
|
1909
|
+
The \`all\` target hooks every supported tool detected on this machine. Re-running the
|
|
1910
|
+
command updates the runtime and replaces earlier CarMoji-owned entries.
|
|
1911
|
+
|
|
1912
|
+
Tools: ${Object.keys(TOOLS).join(', ')}
|
|
1763
1913
|
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
never disturb a session.
|
|
1914
|
+
Without a tool name, this is the event entry point used by those installed
|
|
1915
|
+
hooks. It reads ONE hook event as JSON on stdin, forwards it to every paired
|
|
1916
|
+
phone, and exits 0 no matter what so it can never disturb a session.
|
|
1917
|
+
Unpaired event delivery = immediate no-op.
|
|
1767
1918
|
|
|
1768
1919
|
Flags:
|
|
1769
1920
|
--source <tool> which agent is calling (default: claude) — sets the wire
|
|
@@ -1808,7 +1959,8 @@ Try it:
|
|
|
1808
1959
|
console.log(`carmoji test <event> [session] [tokens] [project]
|
|
1809
1960
|
|
|
1810
1961
|
Sends a fake event to every paired phone so you can watch the reactions
|
|
1811
|
-
without a coding agent.
|
|
1962
|
+
without a coding agent. Leisure activities appear in StandBy, stopped Drive,
|
|
1963
|
+
or idle Code Buddy; if one is already playing, the request plays next.
|
|
1812
1964
|
|
|
1813
1965
|
start wakes up, excited (session_start)
|
|
1814
1966
|
prompt perks up and listens (prompt)
|
|
@@ -1820,6 +1972,19 @@ without a coding agent.
|
|
|
1820
1972
|
error dizzy comic collapse (error)
|
|
1821
1973
|
permission pleading + floating "?" (permission)
|
|
1822
1974
|
end contented sigh (session_end)
|
|
1975
|
+
gamepad leisure gamepad activity
|
|
1976
|
+
tinkering leisure terminal-tinkering activity
|
|
1977
|
+
wheel steering-wheel preview (never chosen at random)
|
|
1978
|
+
puzzle leisure puzzle cube activity
|
|
1979
|
+
plant leisure plant-care activity
|
|
1980
|
+
blocks leisure building-block activity
|
|
1981
|
+
plane leisure paper-plane activity
|
|
1982
|
+
yarn leisure yarn-ball activity
|
|
1983
|
+
sketch leisure sketch-pad activity
|
|
1984
|
+
bubbles leisure bubble-wand activity
|
|
1985
|
+
snack leisure snack-time activity
|
|
1986
|
+
phone leisure pear-phone activity
|
|
1987
|
+
piano leisure piano activity
|
|
1823
1988
|
approval pops the Allow/Deny card, waits ${APPROVAL_WAIT_MS / 1000}s, prints your tap
|
|
1824
1989
|
question pops an option-picker card, waits ${APPROVAL_WAIT_MS / 1000}s, prints your pick
|
|
1825
1990
|
|
|
@@ -1832,34 +1997,36 @@ without a coding agent.
|
|
|
1832
1997
|
case 'config':
|
|
1833
1998
|
console.log(`carmoji config <claude|codex|gate>
|
|
1834
1999
|
|
|
1835
|
-
claude the .claude/settings.json hooks snippet (what \`
|
|
2000
|
+
claude the .claude/settings.json hooks snippet (what \`hook claude\`
|
|
1836
2001
|
writes, including the ${PERMISSION_HOOK_TIMEOUT_S}s PermissionRequest hook)
|
|
1837
2002
|
codex the legacy ~/.codex/config.toml notify line for Codex versions
|
|
1838
|
-
without hooks.json (modern Codex: \`carmoji
|
|
2003
|
+
without hooks.json (modern Codex: \`carmoji hook codex\`)
|
|
1839
2004
|
gate the legacy PreToolUse gate snippet — fires on every matched call;
|
|
1840
|
-
superseded by the PermissionRequest hook \`
|
|
2005
|
+
superseded by the PermissionRequest hook \`hook claude\` sets up`);
|
|
1841
2006
|
break;
|
|
1842
2007
|
|
|
1843
2008
|
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.`);
|
|
2009
|
+
console.log('`carmoji install <tool>|all` was renamed to `carmoji hook <tool>|all`.');
|
|
1852
2010
|
break;
|
|
1853
2011
|
|
|
1854
|
-
case '
|
|
1855
|
-
console.log(`carmoji
|
|
2012
|
+
case 'unhook':
|
|
2013
|
+
console.log(`carmoji unhook <tool>|all
|
|
1856
2014
|
|
|
1857
2015
|
Removes every CarMoji entry from the tool's hook config, leaving other
|
|
1858
|
-
hooks untouched. \`all\` sweeps every known tool quietly.
|
|
2016
|
+
hooks untouched. \`all\` sweeps every known tool quietly. It does not remove
|
|
2017
|
+
the persistent runtime; use \`carmoji uninstall\` for that.
|
|
1859
2018
|
|
|
1860
2019
|
Tools: ${Object.keys(TOOLS).join(', ')}`);
|
|
1861
2020
|
break;
|
|
1862
2021
|
|
|
2022
|
+
case 'uninstall':
|
|
2023
|
+
console.log(`carmoji uninstall
|
|
2024
|
+
|
|
2025
|
+
Removes ~/.config/carmoji/carmoji.js. Pairing state and caches are preserved.
|
|
2026
|
+
This does not edit any tool config; run \`carmoji unhook <tool>|all\` first if
|
|
2027
|
+
you also want to remove installed hooks.`);
|
|
2028
|
+
break;
|
|
2029
|
+
|
|
1863
2030
|
default:
|
|
1864
2031
|
console.log(USAGE);
|
|
1865
2032
|
}
|
package/package.json
CHANGED