@ariso-ai/ari-hooks 0.1.6 → 0.1.8
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 -4
- package/package.json +1 -1
- package/src/cli.js +10 -6
- package/src/hooks.js +25 -8
- package/src/init.js +138 -36
package/README.md
CHANGED
|
@@ -24,7 +24,11 @@ That single command:
|
|
|
24
24
|
|
|
25
25
|
1. Opens your browser to log in to Ari and mint an API token (stored in
|
|
26
26
|
`~/.ari-hooks/config.json`, `0600`). Only needed once per machine.
|
|
27
|
-
2.
|
|
27
|
+
2. Detects which coding agents you use — Claude Code, Codex, and Cursor — and
|
|
28
|
+
wires up the hooks for each one it finds (Claude Code by `~/.claude`, Codex
|
|
29
|
+
by `~/.codex`, Cursor by the environment variables it sets in its terminal).
|
|
30
|
+
If it can't tell, it defaults to Claude Code.
|
|
31
|
+
3. For Claude Code, adds three hooks to `./.claude/settings.json`:
|
|
28
32
|
- `SessionStart` — fetches Ari's top suggested tasks and shows them when
|
|
29
33
|
Claude Code boots; reply with a task number or name to run one
|
|
30
34
|
- `UserPromptSubmit` — records what you asked for
|
|
@@ -33,6 +37,21 @@ That single command:
|
|
|
33
37
|
|
|
34
38
|
Existing settings and hooks are preserved; running it again is a no-op.
|
|
35
39
|
|
|
40
|
+
### Codex
|
|
41
|
+
|
|
42
|
+
Codex reads hooks from `.codex/hooks.json` (project) or `~/.codex/hooks.json`
|
|
43
|
+
(machine-wide), using the same nested shape as Claude Code. When `ari-hooks
|
|
44
|
+
install` (or `init`) detects Codex, it writes the two activity-sharing hooks
|
|
45
|
+
there:
|
|
46
|
+
|
|
47
|
+
- `UserPromptSubmit` — records what you asked for
|
|
48
|
+
- `Stop` — Codex hands the final assistant text to the hook directly (as
|
|
49
|
+
`last_assistant_message`), and that request/outcome pair is sent to the Ari
|
|
50
|
+
API
|
|
51
|
+
|
|
52
|
+
Codex has no user-visible channel to render Ari's suggested-task list, so the
|
|
53
|
+
`SessionStart` task prompt is skipped there.
|
|
54
|
+
|
|
36
55
|
### Cursor
|
|
37
56
|
|
|
38
57
|
Cursor's agent doesn't read `.claude/settings.json` — it has its own hooks
|
|
@@ -48,14 +67,15 @@ writes the equivalent hooks there:
|
|
|
48
67
|
this event instead)
|
|
49
68
|
- `stop` — sends the request/outcome pair to the Ari API
|
|
50
69
|
|
|
51
|
-
`ari-hooks uninstall` cleans up
|
|
70
|
+
`ari-hooks uninstall` cleans up every file — Claude Code, Codex, and Cursor —
|
|
71
|
+
wherever it runs.
|
|
52
72
|
|
|
53
73
|
### Commands
|
|
54
74
|
|
|
55
75
|
| Command | What it does |
|
|
56
76
|
|---|---|
|
|
57
|
-
| `ari-hooks install` | Login (if needed) + set up hooks in the current folder |
|
|
58
|
-
| `ari-hooks uninstall` | Remove the hooks from `./.
|
|
77
|
+
| `ari-hooks install` | Login (if needed) + detect your agents and set up their hooks in the current folder |
|
|
78
|
+
| `ari-hooks uninstall` | Remove the hooks from the Claude Code, Codex (`./.codex/hooks.json`, `~/.codex/hooks.json`), and Cursor files |
|
|
59
79
|
| `ari-hooks login` | Browser login, stores the API token |
|
|
60
80
|
| `ari-hooks init` | Just add the hooks to `./.claude/settings.json` (no login) |
|
|
61
81
|
| `ari-hooks config` | Show configured URLs and login state |
|
|
@@ -88,3 +108,7 @@ precedence over the stored config:
|
|
|
88
108
|
logged to `~/.ari-hooks/error.log`.
|
|
89
109
|
- Only the request text and the final assistant message are sent — no tool
|
|
90
110
|
calls, diffs, or intermediate steps.
|
|
111
|
+
|
|
112
|
+
## Author
|
|
113
|
+
|
|
114
|
+
Max Heckel
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -6,13 +6,16 @@ import { loadConfig, setUrls, showConfig } from './config.js';
|
|
|
6
6
|
const USAGE = `ari-hooks — share your Claude Code activity with Ari
|
|
7
7
|
|
|
8
8
|
Usage:
|
|
9
|
-
ari-hooks install Log in (if needed) and set up hooks
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
.claude/settings.json
|
|
13
|
-
|
|
9
|
+
ari-hooks install Log in (if needed) and set up hooks. Detects which
|
|
10
|
+
coding agents you use (Claude Code, Codex, Cursor) and
|
|
11
|
+
wires up each one; asks whether to install for just this
|
|
12
|
+
repo (and then just for you — .claude/settings.local.json
|
|
13
|
+
— or everyone on the repo — .claude/settings.json) or
|
|
14
|
+
machine-wide. Codex hooks go in ./.codex/hooks.json (or
|
|
15
|
+
~/.codex/hooks.json), Cursor's in ./.cursor/hooks.json
|
|
14
16
|
ari-hooks uninstall Remove the hooks from ./.claude/settings.json,
|
|
15
17
|
./.claude/settings.local.json, ~/.claude/settings.json,
|
|
18
|
+
./.codex/hooks.json, ~/.codex/hooks.json,
|
|
16
19
|
and ./.cursor/hooks.json
|
|
17
20
|
ari-hooks login Log in via the browser and store an API token
|
|
18
21
|
ari-hooks init Just add the hooks (no login)
|
|
@@ -37,6 +40,7 @@ function parseFlags(args) {
|
|
|
37
40
|
if (args[i] === '--web-url') flags.webUrl = args[++i];
|
|
38
41
|
else if (args[i] === '--api-url') flags.apiUrl = args[++i];
|
|
39
42
|
else if (args[i] === '--reset-urls') flags.resetUrls = true;
|
|
43
|
+
else if (args[i] === '--agent') flags.agent = args[++i];
|
|
40
44
|
else rest.push(args[i]);
|
|
41
45
|
}
|
|
42
46
|
return { flags, rest };
|
|
@@ -84,7 +88,7 @@ export async function main(argv) {
|
|
|
84
88
|
uninstall();
|
|
85
89
|
return;
|
|
86
90
|
case 'hook':
|
|
87
|
-
await runHook(rest[1]);
|
|
91
|
+
await runHook(rest[1], flags.agent);
|
|
88
92
|
return;
|
|
89
93
|
case undefined:
|
|
90
94
|
// URL-only invocations (e.g. `ari-hooks --web-url ...`) are a
|
package/src/hooks.js
CHANGED
|
@@ -66,6 +66,21 @@ const sessionIdOf = (input) => input.session_id ?? input.conversation_id;
|
|
|
66
66
|
// Every Cursor hook payload carries cursor_version; Claude Code's never do.
|
|
67
67
|
const isCursorInput = (input) => typeof input.cursor_version === 'string';
|
|
68
68
|
|
|
69
|
+
// agent_type values, keyed by the --agent slug the hook command passes (init
|
|
70
|
+
// bakes it into each agent's config file).
|
|
71
|
+
const AGENT_TYPES = { claude: 'claude-code', codex: 'codex', cursor: 'cursor' };
|
|
72
|
+
|
|
73
|
+
// Which coding agent produced this turn. The --agent flag from the hook command
|
|
74
|
+
// is the reliable source; fall back to sniffing the payload for installs that
|
|
75
|
+
// predate the flag — Cursor stamps cursor_version, Claude Code sends a
|
|
76
|
+
// transcript_path, and Codex has neither (it hands us last_assistant_message).
|
|
77
|
+
function agentTypeOf(input, agent) {
|
|
78
|
+
if (AGENT_TYPES[agent]) return AGENT_TYPES[agent];
|
|
79
|
+
if (isCursorInput(input)) return 'cursor';
|
|
80
|
+
if (input.transcript_path) return 'claude-code';
|
|
81
|
+
return 'codex';
|
|
82
|
+
}
|
|
83
|
+
|
|
69
84
|
/**
|
|
70
85
|
* UserPromptSubmit (Claude Code) / beforeSubmitPrompt (Cursor): remember the
|
|
71
86
|
* prompt so the Stop hook can pair it with the turn's outcome. Both hosts
|
|
@@ -158,7 +173,7 @@ const clamp = (text) =>
|
|
|
158
173
|
* Stop: the turn is over — send the accumulated request(s) plus the final
|
|
159
174
|
* assistant message to Ari, then clear the per-session state.
|
|
160
175
|
*/
|
|
161
|
-
async function onStop(input) {
|
|
176
|
+
async function onStop(input, agent) {
|
|
162
177
|
// stop_hook_active means a stop hook already forced Claude to continue;
|
|
163
178
|
// the real end of the turn will fire another Stop event.
|
|
164
179
|
if (input.stop_hook_active) return;
|
|
@@ -168,11 +183,12 @@ async function onStop(input) {
|
|
|
168
183
|
const session = loadSession(sessionId);
|
|
169
184
|
if (session.prompts.length === 0) return;
|
|
170
185
|
|
|
171
|
-
// Cursor sessions get the outcome pushed to us via afterAgentResponse;
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
|
|
186
|
+
// Cursor sessions get the outcome pushed to us via afterAgentResponse; Codex
|
|
187
|
+
// hands us the final text directly on the Stop payload as
|
|
188
|
+
// last_assistant_message; Claude Code sessions read it from the transcript,
|
|
189
|
+
// waiting for the final assistant message to land there (on timeout, fall
|
|
190
|
+
// back to the last text we did find — best effort).
|
|
191
|
+
let outcome = session.outcome ?? input.last_assistant_message ?? null;
|
|
176
192
|
if (!outcome && input.transcript_path) {
|
|
177
193
|
const deadline = Date.now() + OUTCOME_SETTLE_TIMEOUT_MS;
|
|
178
194
|
for (;;) {
|
|
@@ -197,6 +213,7 @@ async function onStop(input) {
|
|
|
197
213
|
request: clamp(session.prompts.join('\n\n')),
|
|
198
214
|
outcome: clamp(outcome),
|
|
199
215
|
session_id: sessionId,
|
|
216
|
+
agent_type: agentTypeOf(input, agent),
|
|
200
217
|
// Cursor sends workspace_roots instead of cwd.
|
|
201
218
|
cwd: input.cwd ?? input.workspace_roots?.[0] ?? process.cwd(),
|
|
202
219
|
}),
|
|
@@ -317,7 +334,7 @@ async function onSessionStart(input) {
|
|
|
317
334
|
* user's Claude Code session: all failures are swallowed (logged to
|
|
318
335
|
* ~/.ari-hooks/error.log) and we always exit 0.
|
|
319
336
|
*/
|
|
320
|
-
export async function runHook(event) {
|
|
337
|
+
export async function runHook(event, agent) {
|
|
321
338
|
try {
|
|
322
339
|
const raw = await readStdin();
|
|
323
340
|
const input = raw ? JSON.parse(raw) : {};
|
|
@@ -326,7 +343,7 @@ export async function runHook(event) {
|
|
|
326
343
|
} else if (event === 'agent-response') {
|
|
327
344
|
await onAgentResponse(input);
|
|
328
345
|
} else if (event === 'stop') {
|
|
329
|
-
await onStop(input);
|
|
346
|
+
await onStop(input, agent);
|
|
330
347
|
} else if (event === 'session-start') {
|
|
331
348
|
await onSessionStart(input);
|
|
332
349
|
}
|
package/src/init.js
CHANGED
|
@@ -1,12 +1,28 @@
|
|
|
1
1
|
import { dirname, join } from 'node:path';
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { createInterface } from 'node:readline/promises';
|
|
5
5
|
|
|
6
|
+
// Each command carries an --agent flag so the Stop hook can report which coding
|
|
7
|
+
// agent produced the activity (agent_name) without having to guess from the
|
|
8
|
+
// payload — the config file it lives in is the reliable source of that fact.
|
|
6
9
|
const HOOK_EVENTS = {
|
|
7
|
-
SessionStart: 'ari-hooks hook session-start',
|
|
8
|
-
UserPromptSubmit: 'ari-hooks hook user-prompt-submit',
|
|
9
|
-
Stop: 'ari-hooks hook stop',
|
|
10
|
+
SessionStart: 'ari-hooks hook session-start --agent claude',
|
|
11
|
+
UserPromptSubmit: 'ari-hooks hook user-prompt-submit --agent claude',
|
|
12
|
+
Stop: 'ari-hooks hook stop --agent claude',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// Codex copied Claude Code's hooks design: the same { hooks: { Event: [{ hooks:
|
|
16
|
+
// [{ type, command }] }] } } shape and the same event names, read from
|
|
17
|
+
// .codex/hooks.json (or ~/.codex/hooks.json). We only wire up the
|
|
18
|
+
// activity-sharing pair: UserPromptSubmit remembers the prompt and Stop ships
|
|
19
|
+
// the outcome (Codex hands us the final text on the Stop payload as
|
|
20
|
+
// last_assistant_message — no transcript to parse; see onStop). We skip
|
|
21
|
+
// SessionStart because Codex has no user-visible channel to render the
|
|
22
|
+
// suggested-task list, so there is nothing to show.
|
|
23
|
+
const CODEX_HOOK_EVENTS = {
|
|
24
|
+
UserPromptSubmit: 'ari-hooks hook user-prompt-submit --agent codex',
|
|
25
|
+
Stop: 'ari-hooks hook stop --agent codex',
|
|
10
26
|
};
|
|
11
27
|
|
|
12
28
|
// Cursor's agent doesn't read .claude/settings.json — it has its own hooks
|
|
@@ -14,18 +30,70 @@ const HOOK_EVENTS = {
|
|
|
14
30
|
// format. Cursor's transcript is not the Claude Code JSONL our stop handler
|
|
15
31
|
// parses, so afterAgentResponse captures the final assistant text instead.
|
|
16
32
|
const CURSOR_HOOK_EVENTS = {
|
|
17
|
-
sessionStart: 'ari-hooks hook session-start',
|
|
18
|
-
beforeSubmitPrompt: 'ari-hooks hook user-prompt-submit',
|
|
19
|
-
afterAgentResponse: 'ari-hooks hook agent-response',
|
|
20
|
-
stop: 'ari-hooks hook stop',
|
|
33
|
+
sessionStart: 'ari-hooks hook session-start --agent cursor',
|
|
34
|
+
beforeSubmitPrompt: 'ari-hooks hook user-prompt-submit --agent cursor',
|
|
35
|
+
afterAgentResponse: 'ari-hooks hook agent-response --agent cursor',
|
|
36
|
+
stop: 'ari-hooks hook stop --agent cursor',
|
|
21
37
|
};
|
|
22
38
|
|
|
39
|
+
// Cursor's app install shows up in the env vars its integrated terminal
|
|
40
|
+
// inherits: the git-askpass helpers point into Cursor.app (macOS),
|
|
41
|
+
// AppData\Local\Programs\cursor (Windows), or a cursor install dir (Linux).
|
|
42
|
+
const CURSOR_PATH_VARS = [
|
|
43
|
+
'GIT_ASKPASS',
|
|
44
|
+
'VSCODE_GIT_ASKPASS_NODE',
|
|
45
|
+
'VSCODE_GIT_ASKPASS_MAIN',
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
// Cursor ships via ToDesktop, so its macOS bundle id is this opaque token
|
|
49
|
+
// rather than anything containing "cursor".
|
|
50
|
+
const CURSOR_BUNDLE_ID = 'com.todesktop.230313mzl4w4u92';
|
|
51
|
+
|
|
23
52
|
/**
|
|
24
|
-
* Cursor's
|
|
25
|
-
*
|
|
53
|
+
* Cursor's agent terminals export CURSOR_TRACE_ID and its CLI agent exports
|
|
54
|
+
* CURSOR_AGENT, but a regular integrated terminal in Cursor sets neither —
|
|
55
|
+
* it looks like VS Code (TERM_PROGRAM=vscode). There, the tells are the
|
|
56
|
+
* app's bundle id and the helper paths pointing into the Cursor install;
|
|
57
|
+
* plain VS Code and a bare shell match none of these.
|
|
26
58
|
*/
|
|
27
59
|
export const isCursor = (env = process.env) =>
|
|
28
|
-
Boolean(
|
|
60
|
+
Boolean(
|
|
61
|
+
env.CURSOR_TRACE_ID ||
|
|
62
|
+
env.CURSOR_AGENT ||
|
|
63
|
+
env.__CFBundleIdentifier === CURSOR_BUNDLE_ID ||
|
|
64
|
+
CURSOR_PATH_VARS.some((key) => /cursor/i.test(env[key] ?? ''))
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const dirExists = (path) => {
|
|
68
|
+
try {
|
|
69
|
+
return statSync(path).isDirectory();
|
|
70
|
+
} catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// Each agent keeps its config under a home directory whose presence is the
|
|
76
|
+
// tell that the user runs that agent; both honor the same env overrides the
|
|
77
|
+
// agents themselves do (CLAUDE_CONFIG_DIR, CODEX_HOME), which also lets tests
|
|
78
|
+
// point them at a scratch dir.
|
|
79
|
+
const claudeHome = (env) => env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude');
|
|
80
|
+
const codexHome = (env) => env.CODEX_HOME || join(homedir(), '.codex');
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Which coding agents does this user run? Claude Code and Codex are detected by
|
|
84
|
+
* their config directory; Cursor by the marks it leaves in an integrated
|
|
85
|
+
* terminal (see isCursor). Returns a Set so install can branch to one config
|
|
86
|
+
* path per detected agent. Falls back to Claude Code when nothing is found so a
|
|
87
|
+
* fresh machine (or CI) still gets the primary target wired up.
|
|
88
|
+
*/
|
|
89
|
+
export function detectAgents(env = process.env) {
|
|
90
|
+
const agents = new Set();
|
|
91
|
+
if (dirExists(claudeHome(env))) agents.add('claude');
|
|
92
|
+
if (dirExists(codexHome(env))) agents.add('codex');
|
|
93
|
+
if (isCursor(env)) agents.add('cursor');
|
|
94
|
+
if (agents.size === 0) agents.add('claude');
|
|
95
|
+
return agents;
|
|
96
|
+
}
|
|
29
97
|
|
|
30
98
|
function readJson(path) {
|
|
31
99
|
try {
|
|
@@ -51,19 +119,36 @@ const writeJson = (path, value) =>
|
|
|
51
119
|
*/
|
|
52
120
|
function claudeSettingsPath(scope, cwd, env) {
|
|
53
121
|
if (scope === 'user') {
|
|
54
|
-
return join(env
|
|
122
|
+
return join(claudeHome(env), 'settings.json');
|
|
55
123
|
}
|
|
56
124
|
const file = scope === 'local' ? 'settings.local.json' : 'settings.json';
|
|
57
125
|
return join(cwd, '.claude', file);
|
|
58
126
|
}
|
|
59
127
|
|
|
60
|
-
|
|
128
|
+
// Codex has one project file and one user file — no committed/private split
|
|
129
|
+
// like Claude Code's settings.local.json — so 'local' collapses onto the
|
|
130
|
+
// project file.
|
|
131
|
+
function codexHooksPath(scope, cwd, env) {
|
|
132
|
+
if (scope === 'user') {
|
|
133
|
+
return join(codexHome(env), 'hooks.json');
|
|
134
|
+
}
|
|
135
|
+
return join(cwd, '.codex', 'hooks.json');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Merge the ari-hooks commands into a settings file that uses the shared
|
|
140
|
+
* Claude Code / Codex nested hook shape ({ hooks: { Event: [{ hooks: [...] }] }
|
|
141
|
+
* }). Idempotent: an event that already has an ari-hooks command is left alone,
|
|
142
|
+
* everything else in the file is preserved. `label` tags the success line so
|
|
143
|
+
* the user can tell which agent a file belongs to.
|
|
144
|
+
*/
|
|
145
|
+
function initNestedHooks(settingsPath, events, label) {
|
|
61
146
|
const settings = readJson(settingsPath) ?? {};
|
|
62
147
|
|
|
63
148
|
settings.hooks ??= {};
|
|
64
149
|
let changed = false;
|
|
65
150
|
|
|
66
|
-
for (const [event, command] of Object.entries(
|
|
151
|
+
for (const [event, command] of Object.entries(events)) {
|
|
67
152
|
settings.hooks[event] ??= [];
|
|
68
153
|
const already = settings.hooks[event].some((matcher) =>
|
|
69
154
|
(matcher.hooks ?? []).some((h) => h.command?.includes('ari-hooks hook'))
|
|
@@ -82,10 +167,15 @@ function initClaude(settingsPath) {
|
|
|
82
167
|
|
|
83
168
|
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
84
169
|
writeJson(settingsPath, settings);
|
|
85
|
-
console.log(`✓ Ari hooks added to ${settingsPath}`);
|
|
170
|
+
console.log(`✓ Ari hooks added to ${settingsPath}${label ? ` (${label})` : ''}`);
|
|
86
171
|
return true;
|
|
87
172
|
}
|
|
88
173
|
|
|
174
|
+
const initClaude = (settingsPath) => initNestedHooks(settingsPath, HOOK_EVENTS);
|
|
175
|
+
|
|
176
|
+
const initCodex = (hooksPath) =>
|
|
177
|
+
initNestedHooks(hooksPath, CODEX_HOOK_EVENTS, 'Codex detected');
|
|
178
|
+
|
|
89
179
|
function initCursor(cwd) {
|
|
90
180
|
const cursorDir = join(cwd, '.cursor');
|
|
91
181
|
const hooksPath = join(cursorDir, 'hooks.json');
|
|
@@ -147,17 +237,22 @@ async function chooseClaudeScope() {
|
|
|
147
237
|
}
|
|
148
238
|
|
|
149
239
|
/**
|
|
150
|
-
* Merge the ari-hooks hook commands into
|
|
151
|
-
*
|
|
152
|
-
* existing ari-hooks entries
|
|
153
|
-
* are preserved. `scope` picks the
|
|
154
|
-
*
|
|
240
|
+
* Merge the ari-hooks hook commands into the config of every coding agent this
|
|
241
|
+
* user runs (see detectAgents): Claude Code settings, Codex hooks.json, and —
|
|
242
|
+
* inside Cursor — .cursor/hooks.json. Idempotent: existing ari-hooks entries
|
|
243
|
+
* are left alone, and unrelated hooks/settings are preserved. `scope` picks the
|
|
244
|
+
* Claude Code and Codex file (see claudeSettingsPath / codexHooksPath); the
|
|
245
|
+
* Cursor hooks file is always project-level.
|
|
155
246
|
*/
|
|
156
247
|
export function init(cwd = process.cwd(), env = process.env, scope = 'project') {
|
|
157
|
-
const
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
248
|
+
const agents = detectAgents(env);
|
|
249
|
+
const changed = [
|
|
250
|
+
agents.has('claude') && initClaude(claudeSettingsPath(scope, cwd, env)),
|
|
251
|
+
agents.has('codex') && initCodex(codexHooksPath(scope, cwd, env)),
|
|
252
|
+
agents.has('cursor') && initCursor(cwd),
|
|
253
|
+
];
|
|
254
|
+
|
|
255
|
+
if (!changed.some(Boolean)) return;
|
|
161
256
|
const where = scope === 'user' ? 'on this machine' : 'in this folder';
|
|
162
257
|
console.log(
|
|
163
258
|
`Agent sessions ${where} will now share each request and its outcome with Ari,`
|
|
@@ -166,11 +261,14 @@ export function init(cwd = process.cwd(), env = process.env, scope = 'project')
|
|
|
166
261
|
}
|
|
167
262
|
|
|
168
263
|
/**
|
|
169
|
-
* The `install` flavor of init:
|
|
170
|
-
*
|
|
171
|
-
* (CI, piped stdin) keep the old default of
|
|
264
|
+
* The `install` flavor of init: report which agents were detected, and — when
|
|
265
|
+
* attached to a terminal — ask where the hooks should live before writing them.
|
|
266
|
+
* Non-interactive runs (CI, piped stdin) keep the old default of the
|
|
267
|
+
* project-level files.
|
|
172
268
|
*/
|
|
173
269
|
export async function install(cwd = process.cwd(), env = process.env) {
|
|
270
|
+
const agents = detectAgents(env);
|
|
271
|
+
console.log(`Detected coding agent(s): ${[...agents].join(', ')}`);
|
|
174
272
|
const scope =
|
|
175
273
|
process.stdin.isTTY && process.stdout.isTTY
|
|
176
274
|
? await chooseClaudeScope()
|
|
@@ -178,7 +276,7 @@ export async function install(cwd = process.cwd(), env = process.env) {
|
|
|
178
276
|
init(cwd, env, scope);
|
|
179
277
|
}
|
|
180
278
|
|
|
181
|
-
function
|
|
279
|
+
function uninstallNestedHooks(settingsPath) {
|
|
182
280
|
if (!existsSync(settingsPath)) return false;
|
|
183
281
|
const settings = readJson(settingsPath);
|
|
184
282
|
|
|
@@ -235,20 +333,24 @@ function uninstallCursor(cwd) {
|
|
|
235
333
|
}
|
|
236
334
|
|
|
237
335
|
/**
|
|
238
|
-
* Remove the ari-hooks hook commands that init/install added to the
|
|
239
|
-
*
|
|
240
|
-
* ari-hooks entries are touched, everything else in the files is
|
|
241
|
-
* preserved. Cleans every location install can write to
|
|
242
|
-
*
|
|
243
|
-
*
|
|
336
|
+
* Remove the ari-hooks hook commands that init/install added to the Claude
|
|
337
|
+
* Code settings, Codex hooks.json, and Cursor hooks file. The inverse of init:
|
|
338
|
+
* only ari-hooks entries are touched, everything else in the files is
|
|
339
|
+
* preserved. Cleans every location install can write to — regardless of which
|
|
340
|
+
* agents are currently detected — so hooks don't linger wherever they were put:
|
|
341
|
+
* the project/local/user Claude settings, the project and user Codex hooks, and
|
|
342
|
+
* the Cursor hooks file.
|
|
244
343
|
*/
|
|
245
344
|
export function uninstall(cwd = process.cwd(), env = process.env) {
|
|
246
345
|
const removedClaude = ['project', 'local', 'user']
|
|
247
|
-
.map((scope) =>
|
|
346
|
+
.map((scope) => uninstallNestedHooks(claudeSettingsPath(scope, cwd, env)))
|
|
347
|
+
.some(Boolean);
|
|
348
|
+
const removedCodex = ['project', 'user']
|
|
349
|
+
.map((scope) => uninstallNestedHooks(codexHooksPath(scope, cwd, env)))
|
|
248
350
|
.some(Boolean);
|
|
249
351
|
const removedCursor = uninstallCursor(cwd);
|
|
250
352
|
|
|
251
|
-
if (!removedClaude && !removedCursor) {
|
|
353
|
+
if (!removedClaude && !removedCodex && !removedCursor) {
|
|
252
354
|
console.log('No Ari hooks found in this folder — nothing to remove.');
|
|
253
355
|
return;
|
|
254
356
|
}
|