@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 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 two hooks to `./.claude/settings.json`:
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ariso-ai/ari-hooks",
3
- "version": "0.1.1",
3
+ "version": "0.1.4",
4
4
  "description": "Set up Claude Code hooks that share your requests and their outcomes with Ari",
5
5
  "type": "module",
6
6
  "bin": {
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 URL, and wait for the
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://127.0.0.1');
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
- authUrl.searchParams.set(
73
- 'callback',
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...');