@ariso-ai/ari-hooks 0.1.4 → 0.1.5
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 +1 -0
- package/package.json +1 -1
- package/src/cli.js +5 -1
- package/src/hooks.js +64 -14
- package/src/init.js +56 -1
package/README.md
CHANGED
|
@@ -38,6 +38,7 @@ Existing settings and hooks are preserved; running it again is a no-op.
|
|
|
38
38
|
| Command | What it does |
|
|
39
39
|
|---|---|
|
|
40
40
|
| `ari-hooks install` | Login (if needed) + set up hooks in the current folder |
|
|
41
|
+
| `ari-hooks uninstall` | Remove the hooks from `./.claude/settings.json` |
|
|
41
42
|
| `ari-hooks login` | Browser login, stores the API token |
|
|
42
43
|
| `ari-hooks init` | Just add the hooks to `./.claude/settings.json` (no login) |
|
|
43
44
|
| `ari-hooks config` | Show configured URLs and login state |
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { login, logout, status } from './login.js';
|
|
2
|
-
import { init } from './init.js';
|
|
2
|
+
import { init, uninstall } from './init.js';
|
|
3
3
|
import { runHook } from './hooks.js';
|
|
4
4
|
import { loadConfig, setUrls, showConfig } from './config.js';
|
|
5
5
|
|
|
@@ -7,6 +7,7 @@ const USAGE = `ari-hooks — share your Claude Code activity with Ari
|
|
|
7
7
|
|
|
8
8
|
Usage:
|
|
9
9
|
ari-hooks install Log in (if needed) and set up hooks in the current folder
|
|
10
|
+
ari-hooks uninstall Remove the hooks from ./.claude/settings.json
|
|
10
11
|
ari-hooks login Log in via the browser and store an API token
|
|
11
12
|
ari-hooks init Just add the hooks to ./.claude/settings.json (no login)
|
|
12
13
|
ari-hooks config Show the configured URLs and login state
|
|
@@ -73,6 +74,9 @@ export async function main(argv) {
|
|
|
73
74
|
case 'init':
|
|
74
75
|
init();
|
|
75
76
|
return;
|
|
77
|
+
case 'uninstall':
|
|
78
|
+
uninstall();
|
|
79
|
+
return;
|
|
76
80
|
case 'hook':
|
|
77
81
|
await runHook(rest[1]);
|
|
78
82
|
return;
|
package/src/hooks.js
CHANGED
|
@@ -11,6 +11,14 @@ import { configDir, loadConfig, getApiUrl } from './config.js';
|
|
|
11
11
|
|
|
12
12
|
const MAX_TEXT_LENGTH = 100_000;
|
|
13
13
|
const SEND_TIMEOUT_MS = 15_000;
|
|
14
|
+
// Claude Code can fire Stop while the final assistant message is still being
|
|
15
|
+
// flushed to the transcript; poll until the tail settles (or give up).
|
|
16
|
+
const OUTCOME_POLL_INTERVAL_MS = 150;
|
|
17
|
+
const OUTCOME_SETTLE_TIMEOUT_MS = Number(
|
|
18
|
+
process.env.ARI_HOOKS_SETTLE_TIMEOUT_MS ?? 5_000
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
14
22
|
|
|
15
23
|
const sessionsDir = () => join(configDir(), 'sessions');
|
|
16
24
|
const sessionPath = (sessionId) =>
|
|
@@ -61,29 +69,62 @@ async function onUserPromptSubmit(input) {
|
|
|
61
69
|
saveSession(input.session_id, session);
|
|
62
70
|
}
|
|
63
71
|
|
|
72
|
+
function assistantText(entry) {
|
|
73
|
+
if (entry.type !== 'assistant' || !Array.isArray(entry.message?.content)) {
|
|
74
|
+
return '';
|
|
75
|
+
}
|
|
76
|
+
return entry.message.content
|
|
77
|
+
.filter((block) => block.type === 'text' && block.text)
|
|
78
|
+
.map((block) => block.text)
|
|
79
|
+
.join('\n')
|
|
80
|
+
.trim();
|
|
81
|
+
}
|
|
82
|
+
|
|
64
83
|
/**
|
|
65
84
|
* Pull the final assistant text out of the transcript (JSONL). This is the
|
|
66
85
|
* "outcome" — we deliberately skip the intermediate steps/tool calls.
|
|
86
|
+
*
|
|
87
|
+
* `settled` reports whether the exchange actually ends in assistant text.
|
|
88
|
+
* When the transcript instead ends at a tool call/result or a half-written
|
|
89
|
+
* line, the final message hasn't been flushed yet and `text` is only the
|
|
90
|
+
* last narration before a tool ran — the caller should re-read rather than
|
|
91
|
+
* ship that as the outcome.
|
|
67
92
|
*/
|
|
68
93
|
function extractOutcome(transcriptPath) {
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
94
|
+
const entries = [];
|
|
95
|
+
let tailPartial = false;
|
|
96
|
+
for (const line of readFileSync(transcriptPath, 'utf8').split('\n')) {
|
|
97
|
+
if (!line.trim()) continue;
|
|
73
98
|
try {
|
|
74
|
-
|
|
99
|
+
entries.push(JSON.parse(line));
|
|
100
|
+
tailPartial = false;
|
|
75
101
|
} catch {
|
|
102
|
+
tailPartial = true; // a line still being written
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let settled = tailPartial ? false : null;
|
|
107
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
108
|
+
const entry = entries[i];
|
|
109
|
+
// Bookkeeping entries (system, attachment, last-prompt, …) may trail
|
|
110
|
+
// the exchange; they say nothing about whether it is complete.
|
|
111
|
+
if (entry.type !== 'assistant' && entry.type !== 'user') continue;
|
|
112
|
+
let text = assistantText(entry);
|
|
113
|
+
if (!text) {
|
|
114
|
+
// A tool call/result with nothing after it: mid-turn.
|
|
115
|
+
settled ??= false;
|
|
76
116
|
continue;
|
|
77
117
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
118
|
+
// The message may span several JSONL entries (one per content block);
|
|
119
|
+
// stitch earlier blocks of the same message back on.
|
|
120
|
+
const id = entry.message?.id;
|
|
121
|
+
for (let j = i - 1; id && j >= 0 && entries[j].message?.id === id; j--) {
|
|
122
|
+
const earlier = assistantText(entries[j]);
|
|
123
|
+
if (earlier) text = `${earlier}\n${text}`;
|
|
124
|
+
}
|
|
125
|
+
return { text, settled: settled ?? true };
|
|
85
126
|
}
|
|
86
|
-
return null;
|
|
127
|
+
return { text: null, settled: false };
|
|
87
128
|
}
|
|
88
129
|
|
|
89
130
|
const clamp = (text) =>
|
|
@@ -102,7 +143,16 @@ async function onStop(input) {
|
|
|
102
143
|
const session = loadSession(input.session_id);
|
|
103
144
|
if (session.prompts.length === 0) return;
|
|
104
145
|
|
|
105
|
-
|
|
146
|
+
// Wait for the final assistant message to land in the transcript; on
|
|
147
|
+
// timeout fall back to the last text we did find (best effort).
|
|
148
|
+
const deadline = Date.now() + OUTCOME_SETTLE_TIMEOUT_MS;
|
|
149
|
+
let outcome;
|
|
150
|
+
for (;;) {
|
|
151
|
+
const { text, settled } = extractOutcome(input.transcript_path);
|
|
152
|
+
outcome = text;
|
|
153
|
+
if (settled || Date.now() >= deadline) break;
|
|
154
|
+
await sleep(OUTCOME_POLL_INTERVAL_MS);
|
|
155
|
+
}
|
|
106
156
|
if (!outcome) return;
|
|
107
157
|
|
|
108
158
|
const config = loadConfig();
|
package/src/init.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
|
-
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
3
|
|
|
4
4
|
const HOOK_EVENTS = {
|
|
5
5
|
SessionStart: 'ari-hooks hook session-start',
|
|
@@ -55,3 +55,58 @@ export function init(cwd = process.cwd()) {
|
|
|
55
55
|
);
|
|
56
56
|
console.log('and show suggested Ari tasks when a session starts.');
|
|
57
57
|
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Remove the ari-hooks hook commands that init/install added to the
|
|
61
|
+
* project's Claude Code settings. The inverse of init: only ari-hooks
|
|
62
|
+
* entries are touched, everything else in the file is preserved.
|
|
63
|
+
*/
|
|
64
|
+
export function uninstall(cwd = process.cwd()) {
|
|
65
|
+
const settingsPath = join(cwd, '.claude', 'settings.json');
|
|
66
|
+
|
|
67
|
+
if (!existsSync(settingsPath)) {
|
|
68
|
+
console.log(`No Claude Code settings found at ${settingsPath} — nothing to remove.`);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let settings;
|
|
73
|
+
try {
|
|
74
|
+
settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
|
|
75
|
+
} catch {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`${settingsPath} exists but is not valid JSON — fix or remove it, then re-run.`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const isOurs = (h) => h.command?.includes('ari-hooks hook');
|
|
82
|
+
let changed = false;
|
|
83
|
+
|
|
84
|
+
for (const [event, matchers] of Object.entries(settings.hooks ?? {})) {
|
|
85
|
+
if (!Array.isArray(matchers)) continue;
|
|
86
|
+
const kept = matchers
|
|
87
|
+
.map((matcher) => {
|
|
88
|
+
if (!(matcher.hooks ?? []).some(isOurs)) return matcher;
|
|
89
|
+
changed = true;
|
|
90
|
+
const rest = matcher.hooks.filter((h) => !isOurs(h));
|
|
91
|
+
return rest.length > 0 ? { ...matcher, hooks: rest } : null;
|
|
92
|
+
})
|
|
93
|
+
.filter(Boolean);
|
|
94
|
+
if (kept.length > 0) settings.hooks[event] = kept;
|
|
95
|
+
else delete settings.hooks[event];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!changed) {
|
|
99
|
+
console.log(`No Ari hooks found in ${settingsPath} — nothing to remove.`);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (settings.hooks && Object.keys(settings.hooks).length === 0) {
|
|
104
|
+
delete settings.hooks;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
108
|
+
console.log(`✓ Ari hooks removed from ${settingsPath}`);
|
|
109
|
+
console.log(
|
|
110
|
+
'Claude Code sessions in this folder will no longer share activity with Ari.'
|
|
111
|
+
);
|
|
112
|
+
}
|