@ariso-ai/ari-hooks 0.1.1 → 0.1.4
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 +6 -2
- package/package.json +1 -1
- package/src/hooks.js +86 -0
- package/src/init.js +3 -1
- package/src/login.js +8 -8
package/README.md
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Shares your Claude Code activity with [Ari](https://ariso.ai): after every
|
|
4
4
|
Claude Code turn, the hooks send your request and the final outcome (not the
|
|
5
|
-
intermediate steps) to Ari.
|
|
5
|
+
intermediate steps) to Ari. When a session starts, Ari suggests the top
|
|
6
|
+
things Claude can take care of for you right now — pick one and Claude runs
|
|
7
|
+
it.
|
|
6
8
|
|
|
7
9
|
## Install
|
|
8
10
|
|
|
@@ -22,7 +24,9 @@ That single command:
|
|
|
22
24
|
|
|
23
25
|
1. Opens your browser to log in to Ari and mint an API token (stored in
|
|
24
26
|
`~/.ari-hooks/config.json`, `0600`). Only needed once per machine.
|
|
25
|
-
2. Adds
|
|
27
|
+
2. Adds three hooks to `./.claude/settings.json`:
|
|
28
|
+
- `SessionStart` — fetches Ari's top suggested tasks and shows them when
|
|
29
|
+
Claude Code boots; reply with a task number or name to run one
|
|
26
30
|
- `UserPromptSubmit` — records what you asked for
|
|
27
31
|
- `Stop` — reads the final assistant message from the transcript and sends
|
|
28
32
|
the request/outcome pair to the Ari API
|
package/package.json
CHANGED
package/src/hooks.js
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
mkdirSync,
|
|
4
4
|
readFileSync,
|
|
5
5
|
writeFileSync,
|
|
6
|
+
writeSync,
|
|
6
7
|
rmSync,
|
|
7
8
|
appendFileSync,
|
|
8
9
|
} from 'node:fs';
|
|
@@ -128,6 +129,89 @@ async function onStop(input) {
|
|
|
128
129
|
rmSync(sessionPath(input.session_id), { force: true });
|
|
129
130
|
}
|
|
130
131
|
|
|
132
|
+
const MAX_TASKS = 3;
|
|
133
|
+
const MAX_TASK_NAME_LENGTH = 200;
|
|
134
|
+
|
|
135
|
+
const oneLine = (text) =>
|
|
136
|
+
text.replace(/\s+/g, ' ').trim().slice(0, MAX_TASK_NAME_LENGTH);
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* SessionStart: ask Ari for the top tasks Claude can take care of right now
|
|
140
|
+
* and surface them at boot — a visible list for the user (systemMessage)
|
|
141
|
+
* plus the full prompts for Claude (additionalContext) so it can run
|
|
142
|
+
* whichever one the user picks.
|
|
143
|
+
*/
|
|
144
|
+
async function onSessionStart(input) {
|
|
145
|
+
// Compaction restarts the session mid-conversation; the tasks were
|
|
146
|
+
// already offered, so don't show (or inject) them again.
|
|
147
|
+
if (input.source === 'compact') return;
|
|
148
|
+
|
|
149
|
+
const config = loadConfig();
|
|
150
|
+
if (!config.token) return;
|
|
151
|
+
|
|
152
|
+
const response = await fetch(new URL('/agent-tasks', getApiUrl(config)), {
|
|
153
|
+
headers: { Authorization: `Bearer ${config.token}` },
|
|
154
|
+
signal: AbortSignal.timeout(SEND_TIMEOUT_MS),
|
|
155
|
+
});
|
|
156
|
+
if (!response.ok) {
|
|
157
|
+
throw new Error(`GET /agent-tasks failed: ${response.status}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const body = await response.json();
|
|
161
|
+
// The API wraps the list ({ tasks: [...] }); accept a bare array too.
|
|
162
|
+
const list = Array.isArray(body) ? body : Array.isArray(body?.tasks) ? body.tasks : [];
|
|
163
|
+
const tasks = list
|
|
164
|
+
.filter(
|
|
165
|
+
(t) =>
|
|
166
|
+
t &&
|
|
167
|
+
typeof t.taskName === 'string' &&
|
|
168
|
+
t.taskName.trim() &&
|
|
169
|
+
typeof t.prompt === 'string' &&
|
|
170
|
+
t.prompt.trim()
|
|
171
|
+
)
|
|
172
|
+
.slice(0, MAX_TASKS);
|
|
173
|
+
if (tasks.length === 0) return;
|
|
174
|
+
|
|
175
|
+
// Claude Code renders systemMessage with ANSI intact; the leading \n
|
|
176
|
+
// pushes our block below the fixed "SessionStart:<source> says:" prefix.
|
|
177
|
+
const BOLD = '\x1b[1m';
|
|
178
|
+
const CYAN = '\x1b[36m';
|
|
179
|
+
const GREY = '\x1b[37m';
|
|
180
|
+
const RESET = '\x1b[0m';
|
|
181
|
+
const visibleList = tasks
|
|
182
|
+
.map((t, i) => ` ${BOLD}${i + 1}.${RESET} ${oneLine(t.taskName)}`)
|
|
183
|
+
.join('\n');
|
|
184
|
+
const systemMessage =
|
|
185
|
+
`\n${BOLD}${CYAN}✻ Ari — things Claude can take care of for you right now${RESET}\n` +
|
|
186
|
+
`${visibleList}\n` +
|
|
187
|
+
`${GREY}Reply "run task 1" (or the task name) to start one.${RESET}`;
|
|
188
|
+
|
|
189
|
+
const additionalContext =
|
|
190
|
+
`The user has Ari connected via ari-hooks. At session start the user was ` +
|
|
191
|
+
`shown this list of suggested tasks:\n\n` +
|
|
192
|
+
tasks
|
|
193
|
+
.map(
|
|
194
|
+
(t, i) =>
|
|
195
|
+
`Task ${i + 1}: ${oneLine(t.taskName)}\nPrompt: ${clamp(t.prompt)}`
|
|
196
|
+
)
|
|
197
|
+
.join('\n\n') +
|
|
198
|
+
`\n\nIf the user asks to run one of these tasks (by number or name), ` +
|
|
199
|
+
`carry out that task's prompt as if the user had typed it. Do not start ` +
|
|
200
|
+
`any of these tasks unless the user asks.`;
|
|
201
|
+
|
|
202
|
+
// writeSync: process.exit(0) in runHook would race an async stdout write.
|
|
203
|
+
writeSync(
|
|
204
|
+
1,
|
|
205
|
+
JSON.stringify({
|
|
206
|
+
systemMessage,
|
|
207
|
+
hookSpecificOutput: {
|
|
208
|
+
hookEventName: 'SessionStart',
|
|
209
|
+
additionalContext,
|
|
210
|
+
},
|
|
211
|
+
}) + '\n'
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
131
215
|
/**
|
|
132
216
|
* Entry point for `ari-hooks hook <event>`. Hooks must never break the
|
|
133
217
|
* user's Claude Code session: all failures are swallowed (logged to
|
|
@@ -141,6 +225,8 @@ export async function runHook(event) {
|
|
|
141
225
|
await onUserPromptSubmit(input);
|
|
142
226
|
} else if (event === 'stop') {
|
|
143
227
|
await onStop(input);
|
|
228
|
+
} else if (event === 'session-start') {
|
|
229
|
+
await onSessionStart(input);
|
|
144
230
|
}
|
|
145
231
|
} catch (err) {
|
|
146
232
|
logError(err);
|
package/src/init.js
CHANGED
|
@@ -2,6 +2,7 @@ import { join } from 'node:path';
|
|
|
2
2
|
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
3
|
|
|
4
4
|
const HOOK_EVENTS = {
|
|
5
|
+
SessionStart: 'ari-hooks hook session-start',
|
|
5
6
|
UserPromptSubmit: 'ari-hooks hook user-prompt-submit',
|
|
6
7
|
Stop: 'ari-hooks hook stop',
|
|
7
8
|
};
|
|
@@ -50,6 +51,7 @@ export function init(cwd = process.cwd()) {
|
|
|
50
51
|
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
51
52
|
console.log(`✓ Ari hooks added to ${settingsPath}`);
|
|
52
53
|
console.log(
|
|
53
|
-
'Claude Code sessions in this folder will now share each request and its outcome with Ari
|
|
54
|
+
'Claude Code sessions in this folder will now share each request and its outcome with Ari,'
|
|
54
55
|
);
|
|
56
|
+
console.log('and show suggested Ari tasks when a session starts.');
|
|
55
57
|
}
|
package/src/login.js
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
const LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
|
|
13
13
|
|
|
14
14
|
const SUCCESS_HTML = `<!doctype html>
|
|
15
|
-
<html><head><title>Ari Hooks</title></head>
|
|
15
|
+
<html><head><meta charset="utf-8"><title>Ari Hooks</title></head>
|
|
16
16
|
<body style="font-family: sans-serif; text-align: center; padding-top: 4rem;">
|
|
17
17
|
<h2>✓ Logged in</h2>
|
|
18
18
|
<p>The Ari Hooks CLI received your token. You can close this window.</p>
|
|
@@ -36,7 +36,7 @@ function openBrowser(url) {
|
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* Browser login: start a one-shot loopback HTTP server, send the user to
|
|
39
|
-
* the web app's /cli-auth page with our callback
|
|
39
|
+
* the web app's /cli-auth page with our callback port, and wait for the
|
|
40
40
|
* page to redirect back with a freshly minted API token.
|
|
41
41
|
*/
|
|
42
42
|
export async function login() {
|
|
@@ -45,7 +45,7 @@ export async function login() {
|
|
|
45
45
|
|
|
46
46
|
const token = await new Promise((resolve, reject) => {
|
|
47
47
|
const server = createServer((req, res) => {
|
|
48
|
-
const url = new URL(req.url, 'http://
|
|
48
|
+
const url = new URL(req.url, 'http://localhost');
|
|
49
49
|
if (url.pathname !== '/callback') {
|
|
50
50
|
res.writeHead(404).end();
|
|
51
51
|
return;
|
|
@@ -59,20 +59,20 @@ export async function login() {
|
|
|
59
59
|
res.writeHead(400).end('Missing token.');
|
|
60
60
|
return;
|
|
61
61
|
}
|
|
62
|
-
res.writeHead(200, { 'Content-Type': 'text/html' }).end(SUCCESS_HTML);
|
|
62
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }).end(SUCCESS_HTML);
|
|
63
63
|
// Let the response flush before tearing the server down.
|
|
64
64
|
setTimeout(() => server.close(), 100);
|
|
65
65
|
resolve(received);
|
|
66
66
|
});
|
|
67
67
|
|
|
68
68
|
server.on('error', reject);
|
|
69
|
+
// Bind to 127.0.0.1 explicitly — the web app reconstructs the callback
|
|
70
|
+
// as http://127.0.0.1:<port>/callback from callback_port.
|
|
69
71
|
server.listen(0, '127.0.0.1', () => {
|
|
70
72
|
const { port } = server.address();
|
|
71
73
|
const authUrl = new URL('/cli-auth', getWebUrl(config));
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
`http://127.0.0.1:${port}/callback`
|
|
75
|
-
);
|
|
74
|
+
// Pass only the port; the full callback URL trips the WAF.
|
|
75
|
+
authUrl.searchParams.set('callback_port', String(port));
|
|
76
76
|
authUrl.searchParams.set('state', state);
|
|
77
77
|
|
|
78
78
|
console.log('Opening your browser to log in to Ari...');
|