-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathhook-execution.ts
More file actions
94 lines (84 loc) · 2.93 KB
/
Copy pathhook-execution.ts
File metadata and controls
94 lines (84 loc) · 2.93 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
/**
* Hook execution example.
*
* Registers a PreToolUse hook for the `Execute` tool and prints the hook
* lifecycle messages that arrive in the session stream.
*
* The hook lives in a throwaway project directory (`<tempdir>/.factory/hooks.json`)
* that is passed as the session `cwd` and deleted afterwards, so running this
* example never writes to your own `~/.factory` configuration. It does not
* isolate you from it: hook lists are concatenated across settings levels
* rather than replaced, so your own user-level `PreToolUse` hooks are appended
* to the one below and fire during this run whenever their matcher matches.
*
* `hooksDisabled` resolves first-defined-wins across settings levels, so the
* config below sets it explicitly rather than leaving it absent. An absent
* value would inherit a global `"hooksDisabled": true` from `~/.factory` and
* the hook would never run.
*
* See https://docs.factory.ai/cli/configuration/hooks-guide for the `/hooks`
* slash command, the other events, and security guidance.
*
* Usage:
* npx tsx examples/node/hook-execution.ts
*/
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import {
DroidMessageType,
ToolConfirmationOutcome,
createSession,
} from '@factory/droid-sdk/node';
const hookSettings = {
hooksDisabled: false,
PreToolUse: [
{
matcher: 'Execute',
hooks: [{ type: 'command', command: 'echo hook-from-sdk-example' }],
},
],
};
const projectDir = await mkdtemp(join(tmpdir(), 'droid-sdk-hook-project-'));
let hookCount = 0;
try {
await mkdir(join(projectDir, '.factory'), { recursive: true });
await writeFile(
join(projectDir, '.factory', 'hooks.json'),
JSON.stringify(hookSettings, null, 2)
);
const session = await createSession({
cwd: projectDir,
permissionHandler: () => ToolConfirmationOutcome.ProceedOnce,
});
try {
for await (const msg of session.stream(
'Run the shell command `echo hello` using the Execute tool.'
)) {
if (msg.type !== DroidMessageType.Hook) {
continue;
}
hookCount++;
if (msg.status === 'started') {
console.log(`[hook started] ${msg.eventName}: ${msg.command}`);
} else {
console.log(
`[hook ${msg.status}] exit code ${String(msg.exitCode)}: ${(msg.stdout ?? '').trim()}`
);
}
}
} finally {
await session.close();
}
} finally {
await rm(projectDir, { recursive: true, force: true });
}
// A silent zero-hook run would look identical to a passing one, so fail loudly.
if (hookCount === 0) {
throw new Error(
'No hook messages arrived: the registered PreToolUse hook never ran. ' +
'The most likely cause is that the droid answered this turn without ' +
'calling the Execute tool, so the `Execute` matcher never fired. ' +
'Re-run, or widen the matcher to the tool the droid actually used.'
);
}