refactor: rename hooks docs and add tests
This commit is contained in:
@@ -7,7 +7,7 @@
|
|||||||
- Plugins: add Zalo Personal plugin (`@clawdbot/zalouser`) and unify channel directory for plugins. (#1032) — thanks @suminhthanh.
|
- Plugins: add Zalo Personal plugin (`@clawdbot/zalouser`) and unify channel directory for plugins. (#1032) — thanks @suminhthanh.
|
||||||
- Models: add Vercel AI Gateway auth choice + onboarding updates. (#1016) — thanks @timolins.
|
- Models: add Vercel AI Gateway auth choice + onboarding updates. (#1016) — thanks @timolins.
|
||||||
- Sessions: add `session.identityLinks` for cross-platform DM session linking. (#1033) — thanks @thewilloftheshadow.
|
- Sessions: add `session.identityLinks` for cross-platform DM session linking. (#1033) — thanks @thewilloftheshadow.
|
||||||
- Hooks: add internal hooks system with bundled hooks, CLI tooling, and docs. (#1028) — thanks @ThomsenDrake.
|
- Hooks: add hooks system with bundled hooks, CLI tooling, and docs. (#1028) — thanks @ThomsenDrake.
|
||||||
|
|
||||||
### Breaking
|
### Breaking
|
||||||
- **BREAKING:** Channel auth now prefers config over env for Discord/Telegram/Matrix (env is fallback only). (#1040) — thanks @thewilloftheshadow.
|
- **BREAKING:** Channel auth now prefers config over env for Discord/Telegram/Matrix (env is fallback only). (#1040) — thanks @thewilloftheshadow.
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
- **BREAKING:** Drop legacy `chatType: "room"` support; use `chatType: "channel"`.
|
- **BREAKING:** Drop legacy `chatType: "room"` support; use `chatType: "channel"`.
|
||||||
- **BREAKING:** remove legacy provider-specific target resolution fallbacks; target resolution is centralized with plugin hints + directory lookups.
|
- **BREAKING:** remove legacy provider-specific target resolution fallbacks; target resolution is centralized with plugin hints + directory lookups.
|
||||||
- **BREAKING:** drop legacy target normalization helpers; use outbound target normalization and resolver flows.
|
- **BREAKING:** drop legacy target normalization helpers; use outbound target normalization and resolver flows.
|
||||||
- **BREAKING:** `clawdbot hooks` is now `clawdbot webhooks`; internal hooks live under `clawdbot hooks`.
|
- **BREAKING:** `clawdbot hooks` is now `clawdbot webhooks`; hooks live under `clawdbot hooks`.
|
||||||
- **BREAKING:** `clawdbot plugins install <path>` now copies into `~/.clawdbot/extensions` (use `--link` to keep path-based loading).
|
- **BREAKING:** `clawdbot plugins install <path>` now copies into `~/.clawdbot/extensions` (use `--link` to keep path-based loading).
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
---
|
---
|
||||||
summary: "CLI reference for `clawdbot hooks` (internal hooks)"
|
summary: "CLI reference for `clawdbot hooks` (agent hooks)"
|
||||||
read_when:
|
read_when:
|
||||||
- You want to manage internal agent hooks
|
- You want to manage agent hooks
|
||||||
- You want to install or update internal hooks
|
- You want to install or update hooks
|
||||||
---
|
---
|
||||||
|
|
||||||
# `clawdbot hooks`
|
# `clawdbot hooks`
|
||||||
|
|
||||||
Manage internal agent hooks (event-driven automations for commands like `/new`, `/reset`, etc.).
|
Manage agent hooks (event-driven automations for commands like `/new`, `/reset`, etc.).
|
||||||
|
|
||||||
Related:
|
Related:
|
||||||
- Internal Hooks: [Internal Agent Hooks](/internal-hooks)
|
- Hooks: [Hooks](/hooks)
|
||||||
|
|
||||||
## List All Hooks
|
## List All Hooks
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ Related:
|
|||||||
clawdbot hooks list
|
clawdbot hooks list
|
||||||
```
|
```
|
||||||
|
|
||||||
List all discovered internal hooks from workspace, managed, and bundled directories.
|
List all discovered hooks from workspace, managed, and bundled directories.
|
||||||
|
|
||||||
**Options:**
|
**Options:**
|
||||||
- `--eligible`: Show only eligible hooks (requirements met)
|
- `--eligible`: Show only eligible hooks (requirements met)
|
||||||
@@ -28,7 +28,7 @@ List all discovered internal hooks from workspace, managed, and bundled director
|
|||||||
**Example output:**
|
**Example output:**
|
||||||
|
|
||||||
```
|
```
|
||||||
Internal Hooks (2/2 ready)
|
Hooks (2/2 ready)
|
||||||
|
|
||||||
Ready:
|
Ready:
|
||||||
📝 command-logger ✓ - Log all command events to a centralized audit file
|
📝 command-logger ✓ - Log all command events to a centralized audit file
|
||||||
@@ -82,7 +82,7 @@ Details:
|
|||||||
Source: clawdbot-bundled
|
Source: clawdbot-bundled
|
||||||
Path: /path/to/clawdbot/hooks/bundled/session-memory/HOOK.md
|
Path: /path/to/clawdbot/hooks/bundled/session-memory/HOOK.md
|
||||||
Handler: /path/to/clawdbot/hooks/bundled/session-memory/handler.ts
|
Handler: /path/to/clawdbot/hooks/bundled/session-memory/handler.ts
|
||||||
Homepage: https://docs.clawd.bot/internal-hooks#session-memory
|
Homepage: https://docs.clawd.bot/hooks#session-memory
|
||||||
Events: command:new
|
Events: command:new
|
||||||
|
|
||||||
Requirements:
|
Requirements:
|
||||||
@@ -103,7 +103,7 @@ Show summary of hook eligibility status (how many are ready vs. not ready).
|
|||||||
**Example output:**
|
**Example output:**
|
||||||
|
|
||||||
```
|
```
|
||||||
Internal Hooks Status
|
Hooks Status
|
||||||
|
|
||||||
Total hooks: 2
|
Total hooks: 2
|
||||||
Ready: 2
|
Ready: 2
|
||||||
@@ -228,7 +228,7 @@ clawdbot hooks enable session-memory
|
|||||||
|
|
||||||
**Output:** `~/clawd/memory/YYYY-MM-DD-slug.md`
|
**Output:** `~/clawd/memory/YYYY-MM-DD-slug.md`
|
||||||
|
|
||||||
**See:** [session-memory documentation](/internal-hooks#session-memory)
|
**See:** [session-memory documentation](/hooks#session-memory)
|
||||||
|
|
||||||
### command-logger
|
### command-logger
|
||||||
|
|
||||||
@@ -255,4 +255,4 @@ cat ~/.clawdbot/logs/commands.log | jq .
|
|||||||
grep '"action":"new"' ~/.clawdbot/logs/commands.log | jq .
|
grep '"action":"new"' ~/.clawdbot/logs/commands.log | jq .
|
||||||
```
|
```
|
||||||
|
|
||||||
**See:** [command-logger documentation](/internal-hooks#command-logger)
|
**See:** [command-logger documentation](/hooks#command-logger)
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
---
|
---
|
||||||
summary: "Internal agent hooks: event-driven automation for commands and lifecycle events"
|
summary: "Hooks: event-driven automation for commands and lifecycle events"
|
||||||
read_when:
|
read_when:
|
||||||
- You want event-driven automation for /new, /reset, /stop, and agent lifecycle events
|
- You want event-driven automation for /new, /reset, /stop, and agent lifecycle events
|
||||||
- You want to build, install, or debug internal hooks
|
- You want to build, install, or debug hooks
|
||||||
---
|
---
|
||||||
# Internal Agent Hooks
|
# Hooks
|
||||||
|
|
||||||
Internal hooks provide an extensible event-driven system for automating actions in response to agent commands and events. Hooks are automatically discovered from directories and can be managed via CLI commands, similar to how skills work in Clawdbot.
|
Hooks provide an extensible event-driven system for automating actions in response to agent commands and events. Hooks are automatically discovered from directories and can be managed via CLI commands, similar to how skills work in Clawdbot.
|
||||||
|
|
||||||
## Getting Oriented
|
## Getting Oriented
|
||||||
|
|
||||||
Hooks are small scripts that run when something happens. There are two kinds:
|
Hooks are small scripts that run when something happens. There are two kinds:
|
||||||
|
|
||||||
- **Internal hooks** (this page): run inside the Gateway when agent events fire, like `/new`, `/reset`, `/stop`, or lifecycle events.
|
- **Hooks** (this page): run inside the Gateway when agent events fire, like `/new`, `/reset`, `/stop`, or lifecycle events.
|
||||||
- **Web-based hooks**: external HTTP webhooks that let other systems trigger work in Clawdbot. See [Webhook Hooks](/automation/webhook) or use `clawdbot webhooks` for Gmail helper commands.
|
- **Webhooks**: external HTTP webhooks that let other systems trigger work in Clawdbot. See [Webhook Hooks](/automation/webhook) or use `clawdbot webhooks` for Gmail helper commands.
|
||||||
|
|
||||||
Common uses:
|
Common uses:
|
||||||
- Save a memory snapshot when you reset a session
|
- Save a memory snapshot when you reset a session
|
||||||
@@ -21,11 +21,11 @@ Common uses:
|
|||||||
- Trigger follow-up automation when a session starts or ends
|
- Trigger follow-up automation when a session starts or ends
|
||||||
- Write files into the agent workspace or call external APIs when events fire
|
- Write files into the agent workspace or call external APIs when events fire
|
||||||
|
|
||||||
If you can write a small TypeScript function, you can write an internal hook. Hooks are discovered automatically, and you enable or disable them via the CLI.
|
If you can write a small TypeScript function, you can write a hook. Hooks are discovered automatically, and you enable or disable them via the CLI.
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
The internal hooks system allows you to:
|
The hooks system allows you to:
|
||||||
- Save session context to memory when `/new` is issued
|
- Save session context to memory when `/new` is issued
|
||||||
- Log all commands for auditing
|
- Log all commands for auditing
|
||||||
- Trigger custom automations on agent lifecycle events
|
- Trigger custom automations on agent lifecycle events
|
||||||
@@ -120,7 +120,7 @@ The `HOOK.md` file contains metadata in YAML frontmatter plus Markdown documenta
|
|||||||
---
|
---
|
||||||
name: my-hook
|
name: my-hook
|
||||||
description: "Short description of what this hook does"
|
description: "Short description of what this hook does"
|
||||||
homepage: https://docs.clawd.bot/internal-hooks#my-hook
|
homepage: https://docs.clawd.bot/hooks#my-hook
|
||||||
metadata: {"clawdbot":{"emoji":"🔗","events":["command:new"],"requires":{"bins":["node"]}}}
|
metadata: {"clawdbot":{"emoji":"🔗","events":["command:new"],"requires":{"bins":["node"]}}}
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -162,12 +162,12 @@ The `metadata.clawdbot` object supports:
|
|||||||
|
|
||||||
### Handler Implementation
|
### Handler Implementation
|
||||||
|
|
||||||
The `handler.ts` file exports an `InternalHookHandler` function:
|
The `handler.ts` file exports a `HookHandler` function:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import type { InternalHookHandler } from '../../src/hooks/internal-hooks.js';
|
import type { HookHandler } from '../../src/hooks/hooks.js';
|
||||||
|
|
||||||
const myHandler: InternalHookHandler = async (event) => {
|
const myHandler: HookHandler = async (event) => {
|
||||||
// Only trigger on 'new' command
|
// Only trigger on 'new' command
|
||||||
if (event.type !== 'command' || event.action !== 'new') {
|
if (event.type !== 'command' || event.action !== 'new') {
|
||||||
return;
|
return;
|
||||||
@@ -260,9 +260,9 @@ This hook does something useful when you issue `/new`.
|
|||||||
### 4. Create handler.ts
|
### 4. Create handler.ts
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import type { InternalHookHandler } from '../../src/hooks/internal-hooks.js';
|
import type { HookHandler } from '../../src/hooks/hooks.js';
|
||||||
|
|
||||||
const handler: InternalHookHandler = async (event) => {
|
const handler: HookHandler = async (event) => {
|
||||||
if (event.type !== 'command' || event.action !== 'new') {
|
if (event.type !== 'command' || event.action !== 'new') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -505,12 +505,12 @@ Hooks run during command processing. Keep them lightweight:
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// ✓ Good - async work, returns immediately
|
// ✓ Good - async work, returns immediately
|
||||||
const handler: InternalHookHandler = async (event) => {
|
const handler: HookHandler = async (event) => {
|
||||||
void processInBackground(event); // Fire and forget
|
void processInBackground(event); // Fire and forget
|
||||||
};
|
};
|
||||||
|
|
||||||
// ✗ Bad - blocks command processing
|
// ✗ Bad - blocks command processing
|
||||||
const handler: InternalHookHandler = async (event) => {
|
const handler: HookHandler = async (event) => {
|
||||||
await slowDatabaseQuery(event);
|
await slowDatabaseQuery(event);
|
||||||
await evenSlowerAPICall(event);
|
await evenSlowerAPICall(event);
|
||||||
};
|
};
|
||||||
@@ -521,7 +521,7 @@ const handler: InternalHookHandler = async (event) => {
|
|||||||
Always wrap risky operations:
|
Always wrap risky operations:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const handler: InternalHookHandler = async (event) => {
|
const handler: HookHandler = async (event) => {
|
||||||
try {
|
try {
|
||||||
await riskyOperation(event);
|
await riskyOperation(event);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -536,7 +536,7 @@ const handler: InternalHookHandler = async (event) => {
|
|||||||
Return early if the event isn't relevant:
|
Return early if the event isn't relevant:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const handler: InternalHookHandler = async (event) => {
|
const handler: HookHandler = async (event) => {
|
||||||
// Only handle 'new' commands
|
// Only handle 'new' commands
|
||||||
if (event.type !== 'command' || event.action !== 'new') {
|
if (event.type !== 'command' || event.action !== 'new') {
|
||||||
return;
|
return;
|
||||||
@@ -584,7 +584,7 @@ clawdbot hooks list --verbose
|
|||||||
In your handler, log when it's called:
|
In your handler, log when it's called:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const handler: InternalHookHandler = async (event) => {
|
const handler: HookHandler = async (event) => {
|
||||||
console.log('[my-handler] Triggered:', event.type, event.action);
|
console.log('[my-handler] Triggered:', event.type, event.action);
|
||||||
// Your logic
|
// Your logic
|
||||||
};
|
};
|
||||||
@@ -620,11 +620,11 @@ Test your handlers in isolation:
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { test } from 'vitest';
|
import { test } from 'vitest';
|
||||||
import { createInternalHookEvent } from './src/hooks/internal-hooks.js';
|
import { createHookEvent } from './src/hooks/hooks.js';
|
||||||
import myHandler from './hooks/my-hook/handler.js';
|
import myHandler from './hooks/my-hook/handler.js';
|
||||||
|
|
||||||
test('my handler works', async () => {
|
test('my handler works', async () => {
|
||||||
const event = createInternalHookEvent('command', 'new', 'test-session', {
|
const event = createHookEvent('command', 'new', 'test-session', {
|
||||||
foo: 'bar'
|
foo: 'bar'
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -144,7 +144,7 @@ describe("handleCommands identity", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("handleCommands internal hooks", () => {
|
describe("handleCommands hooks", () => {
|
||||||
it("triggers hooks for /new with arguments", async () => {
|
it("triggers hooks for /new with arguments", async () => {
|
||||||
const cfg = {
|
const cfg = {
|
||||||
commands: { text: true },
|
commands: { text: true },
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ export type MsgContext = {
|
|||||||
*/
|
*/
|
||||||
OriginatingTo?: string;
|
OriginatingTo?: string;
|
||||||
/**
|
/**
|
||||||
* Messages from internal hooks to be included in the response.
|
* Messages from hooks to be included in the response.
|
||||||
* Used for hook confirmation messages like "Session context saved to memory".
|
* Used for hook confirmation messages like "Session context saved to memory".
|
||||||
*/
|
*/
|
||||||
HookMessages?: string[];
|
HookMessages?: string[];
|
||||||
|
|||||||
54
src/cli/hooks-cli.test.ts
Normal file
54
src/cli/hooks-cli.test.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import type { HookStatusReport } from "../hooks/hooks-status.js";
|
||||||
|
import { formatHooksCheck, formatHooksList } from "./hooks-cli.js";
|
||||||
|
|
||||||
|
const report: HookStatusReport = {
|
||||||
|
workspaceDir: "/tmp/workspace",
|
||||||
|
managedHooksDir: "/tmp/hooks",
|
||||||
|
hooks: [
|
||||||
|
{
|
||||||
|
name: "session-memory",
|
||||||
|
description: "Save session context to memory",
|
||||||
|
source: "clawdbot-bundled",
|
||||||
|
filePath: "/tmp/hooks/session-memory/HOOK.md",
|
||||||
|
baseDir: "/tmp/hooks/session-memory",
|
||||||
|
handlerPath: "/tmp/hooks/session-memory/handler.js",
|
||||||
|
hookKey: "session-memory",
|
||||||
|
emoji: "💾",
|
||||||
|
homepage: "https://docs.clawd.bot/hooks#session-memory",
|
||||||
|
events: ["command:new"],
|
||||||
|
always: false,
|
||||||
|
disabled: false,
|
||||||
|
eligible: true,
|
||||||
|
requirements: {
|
||||||
|
bins: [],
|
||||||
|
anyBins: [],
|
||||||
|
env: [],
|
||||||
|
config: [],
|
||||||
|
os: [],
|
||||||
|
},
|
||||||
|
missing: {
|
||||||
|
bins: [],
|
||||||
|
anyBins: [],
|
||||||
|
env: [],
|
||||||
|
config: [],
|
||||||
|
os: [],
|
||||||
|
},
|
||||||
|
configChecks: [],
|
||||||
|
install: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("hooks cli formatting", () => {
|
||||||
|
it("labels hooks list output", () => {
|
||||||
|
const output = formatHooksList(report, {});
|
||||||
|
expect(output).toContain("Hooks");
|
||||||
|
expect(output).not.toContain("Internal Hooks");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("labels hooks status output", () => {
|
||||||
|
const output = formatHooksCheck(report, {});
|
||||||
|
expect(output).toContain("Hooks Status");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -125,9 +125,7 @@ export function formatHooksList(report: HookStatusReport, opts: HooksListOptions
|
|||||||
const notEligible = hooks.filter((h) => !h.eligible);
|
const notEligible = hooks.filter((h) => !h.eligible);
|
||||||
|
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
lines.push(
|
lines.push(chalk.bold.cyan("Hooks") + chalk.gray(` (${eligible.length}/${hooks.length} ready)`));
|
||||||
chalk.bold.cyan("Internal Hooks") + chalk.gray(` (${eligible.length}/${hooks.length} ready)`),
|
|
||||||
);
|
|
||||||
lines.push("");
|
lines.push("");
|
||||||
|
|
||||||
if (eligible.length > 0) {
|
if (eligible.length > 0) {
|
||||||
@@ -273,7 +271,7 @@ export function formatHooksCheck(report: HookStatusReport, opts: HooksCheckOptio
|
|||||||
const notEligible = report.hooks.filter((h) => !h.eligible);
|
const notEligible = report.hooks.filter((h) => !h.eligible);
|
||||||
|
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
lines.push(chalk.bold.cyan("Internal Hooks Status"));
|
lines.push(chalk.bold.cyan("Hooks Status"));
|
||||||
lines.push("");
|
lines.push("");
|
||||||
lines.push(`Total hooks: ${report.hooks.length}`);
|
lines.push(`Total hooks: ${report.hooks.length}`);
|
||||||
lines.push(chalk.green(`Ready: ${eligible.length}`));
|
lines.push(chalk.green(`Ready: ${eligible.length}`));
|
||||||
@@ -373,7 +371,7 @@ export function registerHooksCli(program: Command): void {
|
|||||||
|
|
||||||
hooks
|
hooks
|
||||||
.command("list")
|
.command("list")
|
||||||
.description("List all internal hooks")
|
.description("List all hooks")
|
||||||
.option("--eligible", "Show only eligible hooks", false)
|
.option("--eligible", "Show only eligible hooks", false)
|
||||||
.option("--json", "Output as JSON", false)
|
.option("--json", "Output as JSON", false)
|
||||||
.option("-v, --verbose", "Show more details including missing requirements", false)
|
.option("-v, --verbose", "Show more details including missing requirements", false)
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ describe("onboard-hooks", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("setupInternalHooks", () => {
|
describe("setupInternalHooks", () => {
|
||||||
it("should enable internal hooks when user selects them", async () => {
|
it("should enable hooks when user selects them", async () => {
|
||||||
const { buildWorkspaceHookStatus } = await import("../hooks/hooks-status.js");
|
const { buildWorkspaceHookStatus } = await import("../hooks/hooks-status.js");
|
||||||
vi.mocked(buildWorkspaceHookStatus).mockReturnValue(createMockHookReport());
|
vi.mocked(buildWorkspaceHookStatus).mockReturnValue(createMockHookReport());
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ describe("onboard-hooks", () => {
|
|||||||
});
|
});
|
||||||
expect(prompter.note).toHaveBeenCalledTimes(2);
|
expect(prompter.note).toHaveBeenCalledTimes(2);
|
||||||
expect(prompter.multiselect).toHaveBeenCalledWith({
|
expect(prompter.multiselect).toHaveBeenCalledWith({
|
||||||
message: "Enable internal hooks?",
|
message: "Enable hooks?",
|
||||||
options: [
|
options: [
|
||||||
{ value: "__skip__", label: "Skip for now" },
|
{ value: "__skip__", label: "Skip for now" },
|
||||||
{
|
{
|
||||||
@@ -173,8 +173,8 @@ describe("onboard-hooks", () => {
|
|||||||
const noteCalls = (prompter.note as ReturnType<typeof vi.fn>).mock.calls;
|
const noteCalls = (prompter.note as ReturnType<typeof vi.fn>).mock.calls;
|
||||||
expect(noteCalls).toHaveLength(2);
|
expect(noteCalls).toHaveLength(2);
|
||||||
|
|
||||||
// First note should explain what internal hooks are
|
// First note should explain what hooks are
|
||||||
expect(noteCalls[0][0]).toContain("Internal hooks");
|
expect(noteCalls[0][0]).toContain("Hooks let you automate actions");
|
||||||
expect(noteCalls[0][0]).toContain("automate actions");
|
expect(noteCalls[0][0]).toContain("automate actions");
|
||||||
|
|
||||||
// Second note should confirm configuration
|
// Second note should confirm configuration
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ export async function setupInternalHooks(
|
|||||||
): Promise<ClawdbotConfig> {
|
): Promise<ClawdbotConfig> {
|
||||||
await prompter.note(
|
await prompter.note(
|
||||||
[
|
[
|
||||||
"Internal hooks let you automate actions when agent commands are issued.",
|
"Hooks let you automate actions when agent commands are issued.",
|
||||||
"Example: Save session context to memory when you issue /new.",
|
"Example: Save session context to memory when you issue /new.",
|
||||||
"",
|
"",
|
||||||
"Learn more: https://docs.clawd.bot/internal-hooks",
|
"Learn more: https://docs.clawd.bot/hooks",
|
||||||
].join("\n"),
|
].join("\n"),
|
||||||
"Internal Hooks",
|
"Hooks",
|
||||||
);
|
);
|
||||||
|
|
||||||
// Discover available hooks using the hook discovery system
|
// Discover available hooks using the hook discovery system
|
||||||
@@ -35,7 +35,7 @@ export async function setupInternalHooks(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const toEnable = await prompter.multiselect({
|
const toEnable = await prompter.multiselect({
|
||||||
message: "Enable internal hooks?",
|
message: "Enable hooks?",
|
||||||
options: [
|
options: [
|
||||||
{ value: "__skip__", label: "Skip for now" },
|
{ value: "__skip__", label: "Skip for now" },
|
||||||
...recommendedHooks.map((hook) => ({
|
...recommendedHooks.map((hook) => ({
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export type HookInstallRecord = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type InternalHooksConfig = {
|
export type InternalHooksConfig = {
|
||||||
/** Enable internal hooks system */
|
/** Enable hooks system */
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
/** Legacy: List of internal hook handlers to register (still supported) */
|
/** Legacy: List of internal hook handlers to register (still supported) */
|
||||||
handlers?: InternalHookHandlerConfig[];
|
handlers?: InternalHookHandlerConfig[];
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ export async function startGatewaySidecars(params: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
params.logHooks.error(`failed to load internal hooks: ${String(err)}`);
|
params.logHooks.error(`failed to load hooks: ${String(err)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Launch configured channels so gateway replies via the surface the message came from.
|
// Launch configured channels so gateway replies via the surface the message came from.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Bundled Internal Hooks
|
# Bundled Hooks
|
||||||
|
|
||||||
This directory contains internal hooks that ship with Clawdbot. These hooks are automatically discovered and can be enabled/disabled via CLI or configuration.
|
This directory contains hooks that ship with Clawdbot. These hooks are automatically discovered and can be enabled/disabled via CLI or configuration.
|
||||||
|
|
||||||
## Available Hooks
|
## Available Hooks
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ session-memory/
|
|||||||
---
|
---
|
||||||
name: my-hook
|
name: my-hook
|
||||||
description: "Short description"
|
description: "Short description"
|
||||||
homepage: https://docs.clawd.bot/hooks/my-hook
|
homepage: https://docs.clawd.bot/hooks#my-hook
|
||||||
metadata:
|
metadata:
|
||||||
{ "clawdbot": { "emoji": "🔗", "events": ["command:new"], "requires": { "bins": ["node"] } } }
|
{ "clawdbot": { "emoji": "🔗", "events": ["command:new"], "requires": { "bins": ["node"] } } }
|
||||||
---
|
---
|
||||||
@@ -161,9 +161,9 @@ interface InternalHookEvent {
|
|||||||
Example handler:
|
Example handler:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import type { InternalHookHandler } from "../../src/hooks/internal-hooks.js";
|
import type { HookHandler } from "../../src/hooks/hooks.js";
|
||||||
|
|
||||||
const myHandler: InternalHookHandler = async (event) => {
|
const myHandler: HookHandler = async (event) => {
|
||||||
if (event.type !== "command" || event.action !== "new") {
|
if (event.type !== "command" || event.action !== "new") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -190,4 +190,4 @@ Test your hooks by:
|
|||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
Full documentation: https://docs.clawd.bot/internal-hooks
|
Full documentation: https://docs.clawd.bot/hooks
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: command-logger
|
name: command-logger
|
||||||
description: "Log all command events to a centralized audit file"
|
description: "Log all command events to a centralized audit file"
|
||||||
homepage: https://docs.clawd.bot/internal-hooks#command-logger
|
homepage: https://docs.clawd.bot/hooks#command-logger
|
||||||
metadata:
|
metadata:
|
||||||
{
|
{
|
||||||
"clawdbot":
|
"clawdbot":
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Example internal hook handler: Log all commands to a file
|
* Example hook handler: Log all commands to a file
|
||||||
*
|
*
|
||||||
* This handler demonstrates how to create a hook that logs all command events
|
* This handler demonstrates how to create a hook that logs all command events
|
||||||
* to a centralized log file for audit/debugging purposes.
|
* to a centralized log file for audit/debugging purposes.
|
||||||
@@ -26,12 +26,12 @@
|
|||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import type { InternalHookHandler } from "../../internal-hooks.js";
|
import type { HookHandler } from "../../hooks.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log all command events to a file
|
* Log all command events to a file
|
||||||
*/
|
*/
|
||||||
const logCommand: InternalHookHandler = async (event) => {
|
const logCommand: HookHandler = async (event) => {
|
||||||
// Only trigger on command events
|
// Only trigger on command events
|
||||||
if (event.type !== "command") {
|
if (event.type !== "command") {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: session-memory
|
name: session-memory
|
||||||
description: "Save session context to memory when /new command is issued"
|
description: "Save session context to memory when /new command is issued"
|
||||||
homepage: https://docs.clawd.bot/internal-hooks#session-memory
|
homepage: https://docs.clawd.bot/hooks#session-memory
|
||||||
metadata:
|
metadata:
|
||||||
{
|
{
|
||||||
"clawdbot":
|
"clawdbot":
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import os from "node:os";
|
|||||||
import type { ClawdbotConfig } from "../../../config/config.js";
|
import type { ClawdbotConfig } from "../../../config/config.js";
|
||||||
import { resolveAgentWorkspaceDir } from "../../../agents/agent-scope.js";
|
import { resolveAgentWorkspaceDir } from "../../../agents/agent-scope.js";
|
||||||
import { resolveAgentIdFromSessionKey } from "../../../routing/session-key.js";
|
import { resolveAgentIdFromSessionKey } from "../../../routing/session-key.js";
|
||||||
import type { InternalHookHandler } from "../../internal-hooks.js";
|
import type { HookHandler } from "../../hooks.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read recent messages from session file for slug generation
|
* Read recent messages from session file for slug generation
|
||||||
@@ -57,7 +57,7 @@ async function getRecentSessionContent(sessionFilePath: string): Promise<string
|
|||||||
/**
|
/**
|
||||||
* Save session context to memory when /new command is triggered
|
* Save session context to memory when /new command is triggered
|
||||||
*/
|
*/
|
||||||
const saveSessionToMemory: InternalHookHandler = async (event) => {
|
const saveSessionToMemory: HookHandler = async (event) => {
|
||||||
// Only trigger on 'new' command
|
// Only trigger on 'new' command
|
||||||
if (event.type !== "command" || event.action !== "new") {
|
if (event.type !== "command" || event.action !== "new") {
|
||||||
return;
|
return;
|
||||||
|
|||||||
116
src/hooks/hooks-install.e2e.test.ts
Normal file
116
src/hooks/hooks-install.e2e.test.ts
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import fs from "node:fs/promises";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
const tempDirs: string[] = [];
|
||||||
|
|
||||||
|
async function makeTempDir() {
|
||||||
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-hooks-e2e-"));
|
||||||
|
tempDirs.push(dir);
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("hooks install (e2e)", () => {
|
||||||
|
let prevStateDir: string | undefined;
|
||||||
|
let prevBundledDir: string | undefined;
|
||||||
|
let workspaceDir: string;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const baseDir = await makeTempDir();
|
||||||
|
workspaceDir = path.join(baseDir, "workspace");
|
||||||
|
await fs.mkdir(workspaceDir, { recursive: true });
|
||||||
|
|
||||||
|
prevStateDir = process.env.CLAWDBOT_STATE_DIR;
|
||||||
|
prevBundledDir = process.env.CLAWDBOT_BUNDLED_HOOKS_DIR;
|
||||||
|
process.env.CLAWDBOT_STATE_DIR = path.join(baseDir, "state");
|
||||||
|
process.env.CLAWDBOT_BUNDLED_HOOKS_DIR = path.join(baseDir, "bundled-none");
|
||||||
|
vi.resetModules();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (prevStateDir === undefined) {
|
||||||
|
delete process.env.CLAWDBOT_STATE_DIR;
|
||||||
|
} else {
|
||||||
|
process.env.CLAWDBOT_STATE_DIR = prevStateDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prevBundledDir === undefined) {
|
||||||
|
delete process.env.CLAWDBOT_BUNDLED_HOOKS_DIR;
|
||||||
|
} else {
|
||||||
|
process.env.CLAWDBOT_BUNDLED_HOOKS_DIR = prevBundledDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
vi.resetModules();
|
||||||
|
for (const dir of tempDirs.splice(0)) {
|
||||||
|
try {
|
||||||
|
await fs.rm(dir, { recursive: true, force: true });
|
||||||
|
} catch {
|
||||||
|
// ignore cleanup failures
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("installs a hook pack and triggers the handler", async () => {
|
||||||
|
const baseDir = await makeTempDir();
|
||||||
|
const packDir = path.join(baseDir, "hook-pack");
|
||||||
|
const hookDir = path.join(packDir, "hooks", "hello-hook");
|
||||||
|
await fs.mkdir(hookDir, { recursive: true });
|
||||||
|
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(packDir, "package.json"),
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
name: "@acme/hello-hooks",
|
||||||
|
version: "0.0.0",
|
||||||
|
clawdbot: { hooks: ["./hooks/hello-hook"] },
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(hookDir, "HOOK.md"),
|
||||||
|
[
|
||||||
|
"---",
|
||||||
|
'name: "hello-hook"',
|
||||||
|
'description: "Test hook"',
|
||||||
|
'metadata: {"clawdbot":{"events":["command:new"]}}',
|
||||||
|
"---",
|
||||||
|
"",
|
||||||
|
"# Hello Hook",
|
||||||
|
"",
|
||||||
|
].join("\n"),
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(hookDir, "handler.js"),
|
||||||
|
"export default async function(event) { event.messages.push('hook-ok'); }\n",
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
|
||||||
|
const { installHooksFromPath } = await import("./install.js");
|
||||||
|
const installResult = await installHooksFromPath({ path: packDir });
|
||||||
|
expect(installResult.ok).toBe(true);
|
||||||
|
if (!installResult.ok) return;
|
||||||
|
|
||||||
|
const { clearInternalHooks, createInternalHookEvent, triggerInternalHook } = await import(
|
||||||
|
"./internal-hooks.js"
|
||||||
|
);
|
||||||
|
const { loadInternalHooks } = await import("./loader.js");
|
||||||
|
|
||||||
|
clearInternalHooks();
|
||||||
|
const loaded = await loadInternalHooks(
|
||||||
|
{ hooks: { internal: { enabled: true } } },
|
||||||
|
workspaceDir,
|
||||||
|
);
|
||||||
|
expect(loaded).toBe(1);
|
||||||
|
|
||||||
|
const event = createInternalHookEvent("command", "new", "test-session");
|
||||||
|
await triggerInternalHook(event);
|
||||||
|
expect(event.messages).toContain("hook-ok");
|
||||||
|
});
|
||||||
|
});
|
||||||
14
src/hooks/hooks.ts
Normal file
14
src/hooks/hooks.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
export * from "./internal-hooks.js";
|
||||||
|
|
||||||
|
export type HookEventType = import("./internal-hooks.js").InternalHookEventType;
|
||||||
|
export type HookEvent = import("./internal-hooks.js").InternalHookEvent;
|
||||||
|
export type HookHandler = import("./internal-hooks.js").InternalHookHandler;
|
||||||
|
|
||||||
|
export {
|
||||||
|
registerInternalHook as registerHook,
|
||||||
|
unregisterInternalHook as unregisterHook,
|
||||||
|
clearInternalHooks as clearHooks,
|
||||||
|
getRegisteredEventKeys as getRegisteredHookEventKeys,
|
||||||
|
triggerInternalHook as triggerHook,
|
||||||
|
createInternalHookEvent as createHookEvent,
|
||||||
|
} from "./internal-hooks.js";
|
||||||
@@ -3,6 +3,7 @@ import fs from "node:fs";
|
|||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import JSZip from "jszip";
|
import JSZip from "jszip";
|
||||||
|
import * as tar from "tar";
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
const tempDirs: string[] = [];
|
const tempDirs: string[] = [];
|
||||||
@@ -85,6 +86,54 @@ describe("installHooksFromArchive", () => {
|
|||||||
fs.existsSync(path.join(result.targetDir, "hooks", "zip-hook", "HOOK.md")),
|
fs.existsSync(path.join(result.targetDir, "hooks", "zip-hook", "HOOK.md")),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("installs hook packs from tar archives", async () => {
|
||||||
|
const stateDir = makeTempDir();
|
||||||
|
const workDir = makeTempDir();
|
||||||
|
const archivePath = path.join(workDir, "hooks.tar");
|
||||||
|
const pkgDir = path.join(workDir, "package");
|
||||||
|
|
||||||
|
fs.mkdirSync(path.join(pkgDir, "hooks", "tar-hook"), { recursive: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(pkgDir, "package.json"),
|
||||||
|
JSON.stringify({
|
||||||
|
name: "@clawdbot/tar-hooks",
|
||||||
|
version: "0.0.1",
|
||||||
|
clawdbot: { hooks: ["./hooks/tar-hook"] },
|
||||||
|
}),
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(pkgDir, "hooks", "tar-hook", "HOOK.md"),
|
||||||
|
[
|
||||||
|
"---",
|
||||||
|
"name: tar-hook",
|
||||||
|
"description: Tar hook",
|
||||||
|
"metadata: {\"clawdbot\":{\"events\":[\"command:new\"]}}",
|
||||||
|
"---",
|
||||||
|
"",
|
||||||
|
"# Tar Hook",
|
||||||
|
].join("\n"),
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(pkgDir, "hooks", "tar-hook", "handler.ts"),
|
||||||
|
"export default async () => {};\n",
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
await tar.c({ cwd: workDir, file: archivePath }, ["package"]);
|
||||||
|
|
||||||
|
const result = await withStateDir(stateDir, async () => {
|
||||||
|
const { installHooksFromArchive } = await import("./install.js");
|
||||||
|
return await installHooksFromArchive({ archivePath });
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.ok).toBe(true);
|
||||||
|
if (!result.ok) return;
|
||||||
|
expect(result.hookPackId).toBe("tar-hooks");
|
||||||
|
expect(result.hooks).toContain("tar-hook");
|
||||||
|
expect(result.targetDir).toBe(path.join(stateDir, "hooks", "tar-hooks"));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("installHooksFromPath", () => {
|
describe("installHooksFromPath", () => {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
type InternalHookEvent,
|
type InternalHookEvent,
|
||||||
} from "./internal-hooks.js";
|
} from "./internal-hooks.js";
|
||||||
|
|
||||||
describe("internal-hooks", () => {
|
describe("hooks", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
clearInternalHooks();
|
clearInternalHooks();
|
||||||
});
|
});
|
||||||
@@ -131,7 +131,7 @@ describe("internal-hooks", () => {
|
|||||||
expect(errorHandler).toHaveBeenCalled();
|
expect(errorHandler).toHaveBeenCalled();
|
||||||
expect(successHandler).toHaveBeenCalled();
|
expect(successHandler).toHaveBeenCalled();
|
||||||
expect(consoleError).toHaveBeenCalledWith(
|
expect(consoleError).toHaveBeenCalledWith(
|
||||||
expect.stringContaining("Internal hook error"),
|
expect.stringContaining("Hook error"),
|
||||||
expect.stringContaining("Handler failed"),
|
expect.stringContaining("Handler failed"),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Internal hook system for clawdbot agent events
|
* Hook system for clawdbot agent events
|
||||||
*
|
*
|
||||||
* Provides an extensible event-driven hook system for internal agent events
|
* Provides an extensible event-driven hook system for agent events
|
||||||
* like command processing, session lifecycle, etc.
|
* like command processing, session lifecycle, etc.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ export function getRegisteredEventKeys(): string[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Trigger an internal hook event
|
* Trigger a hook event
|
||||||
*
|
*
|
||||||
* Calls all handlers registered for:
|
* Calls all handlers registered for:
|
||||||
* 1. The general event type (e.g., 'command')
|
* 1. The general event type (e.g., 'command')
|
||||||
@@ -117,7 +117,7 @@ export async function triggerInternalHook(event: InternalHookEvent): Promise<voi
|
|||||||
await handler(event);
|
await handler(event);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(
|
console.error(
|
||||||
`Internal hook error [${event.type}:${event.action}]:`,
|
`Hook error [${event.type}:${event.action}]:`,
|
||||||
err instanceof Error ? err.message : String(err),
|
err instanceof Error ? err.message : String(err),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -125,7 +125,7 @@ export async function triggerInternalHook(event: InternalHookEvent): Promise<voi
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an internal hook event with common fields filled in
|
* Create a hook event with common fields filled in
|
||||||
*
|
*
|
||||||
* @param type - The event type
|
* @param type - The event type
|
||||||
* @param action - The action within that type
|
* @param action - The action within that type
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ describe("loader", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("loadInternalHooks", () => {
|
describe("loadInternalHooks", () => {
|
||||||
it("should return 0 when internal hooks are not enabled", async () => {
|
it("should return 0 when hooks are not enabled", async () => {
|
||||||
const cfg: ClawdbotConfig = {
|
const cfg: ClawdbotConfig = {
|
||||||
hooks: {
|
hooks: {
|
||||||
internal: {
|
internal: {
|
||||||
@@ -170,7 +170,7 @@ describe("loader", () => {
|
|||||||
const count = await loadInternalHooks(cfg, tmpDir);
|
const count = await loadInternalHooks(cfg, tmpDir);
|
||||||
expect(count).toBe(0);
|
expect(count).toBe(0);
|
||||||
expect(consoleError).toHaveBeenCalledWith(
|
expect(consoleError).toHaveBeenCalledWith(
|
||||||
expect.stringContaining("Failed to load internal hook handler"),
|
expect.stringContaining("Failed to load hook handler"),
|
||||||
expect.any(String),
|
expect.any(String),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Dynamic loader for internal hook handlers
|
* Dynamic loader for hook handlers
|
||||||
*
|
*
|
||||||
* Loads hook handlers from external modules based on configuration
|
* Loads hook handlers from external modules based on configuration
|
||||||
* and from directory-based discovery (bundled, managed, workspace)
|
* and from directory-based discovery (bundled, managed, workspace)
|
||||||
@@ -15,7 +15,7 @@ import { resolveHookConfig } from "./config.js";
|
|||||||
import { shouldIncludeHook } from "./config.js";
|
import { shouldIncludeHook } from "./config.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load and register all internal hook handlers
|
* Load and register all hook handlers
|
||||||
*
|
*
|
||||||
* Loads hooks from both:
|
* Loads hooks from both:
|
||||||
* 1. Directory-based discovery (bundled, managed, workspace)
|
* 1. Directory-based discovery (bundled, managed, workspace)
|
||||||
@@ -30,14 +30,14 @@ import { shouldIncludeHook } from "./config.js";
|
|||||||
* const config = await loadConfig();
|
* const config = await loadConfig();
|
||||||
* const workspaceDir = resolveAgentWorkspaceDir(config, agentId);
|
* const workspaceDir = resolveAgentWorkspaceDir(config, agentId);
|
||||||
* const count = await loadInternalHooks(config, workspaceDir);
|
* const count = await loadInternalHooks(config, workspaceDir);
|
||||||
* console.log(`Loaded ${count} internal hook handlers`);
|
* console.log(`Loaded ${count} hook handlers`);
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export async function loadInternalHooks(
|
export async function loadInternalHooks(
|
||||||
cfg: ClawdbotConfig,
|
cfg: ClawdbotConfig,
|
||||||
workspaceDir: string,
|
workspaceDir: string,
|
||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
// Check if internal hooks are enabled
|
// Check if hooks are enabled
|
||||||
if (!cfg.hooks?.internal?.enabled) {
|
if (!cfg.hooks?.internal?.enabled) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -71,7 +71,7 @@ export async function loadInternalHooks(
|
|||||||
|
|
||||||
if (typeof handler !== "function") {
|
if (typeof handler !== "function") {
|
||||||
console.error(
|
console.error(
|
||||||
`Internal hook error: Handler '${exportName}' from ${entry.hook.name} is not a function`,
|
`Hook error: Handler '${exportName}' from ${entry.hook.name} is not a function`,
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -80,7 +80,7 @@ export async function loadInternalHooks(
|
|||||||
const events = entry.clawdbot?.events ?? [];
|
const events = entry.clawdbot?.events ?? [];
|
||||||
if (events.length === 0) {
|
if (events.length === 0) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`Internal hook warning: Hook '${entry.hook.name}' has no events defined in metadata`,
|
`Hook warning: Hook '${entry.hook.name}' has no events defined in metadata`,
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -90,12 +90,12 @@ export async function loadInternalHooks(
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`Registered internal hook: ${entry.hook.name} -> ${events.join(", ")}${exportName !== "default" ? ` (export: ${exportName})` : ""}`,
|
`Registered hook: ${entry.hook.name} -> ${events.join(", ")}${exportName !== "default" ? ` (export: ${exportName})` : ""}`,
|
||||||
);
|
);
|
||||||
loadedCount++;
|
loadedCount++;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(
|
console.error(
|
||||||
`Failed to load internal hook ${entry.hook.name}:`,
|
`Failed to load hook ${entry.hook.name}:`,
|
||||||
err instanceof Error ? err.message : String(err),
|
err instanceof Error ? err.message : String(err),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -127,7 +127,7 @@ export async function loadInternalHooks(
|
|||||||
|
|
||||||
if (typeof handler !== "function") {
|
if (typeof handler !== "function") {
|
||||||
console.error(
|
console.error(
|
||||||
`Internal hook error: Handler '${exportName}' from ${modulePath} is not a function`,
|
`Hook error: Handler '${exportName}' from ${modulePath} is not a function`,
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -135,12 +135,12 @@ export async function loadInternalHooks(
|
|||||||
// Register the handler
|
// Register the handler
|
||||||
registerInternalHook(handlerConfig.event, handler as InternalHookHandler);
|
registerInternalHook(handlerConfig.event, handler as InternalHookHandler);
|
||||||
console.log(
|
console.log(
|
||||||
`Registered internal hook (legacy): ${handlerConfig.event} -> ${modulePath}${exportName !== "default" ? `#${exportName}` : ""}`,
|
`Registered hook (legacy): ${handlerConfig.event} -> ${modulePath}${exportName !== "default" ? `#${exportName}` : ""}`,
|
||||||
);
|
);
|
||||||
loadedCount++;
|
loadedCount++;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(
|
console.error(
|
||||||
`Failed to load internal hook handler from ${handlerConfig.module}:`,
|
`Failed to load hook handler from ${handlerConfig.module}:`,
|
||||||
err instanceof Error ? err.message : String(err),
|
err instanceof Error ? err.message : String(err),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
68
src/infra/archive.test.ts
Normal file
68
src/infra/archive.test.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import fs from "node:fs/promises";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import JSZip from "jszip";
|
||||||
|
import * as tar from "tar";
|
||||||
|
import { afterEach, describe, expect, it } from "vitest";
|
||||||
|
import { extractArchive, resolveArchiveKind, resolvePackedRootDir } from "./archive.js";
|
||||||
|
|
||||||
|
const tempDirs: string[] = [];
|
||||||
|
|
||||||
|
async function makeTempDir() {
|
||||||
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-archive-"));
|
||||||
|
tempDirs.push(dir);
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
for (const dir of tempDirs.splice(0)) {
|
||||||
|
try {
|
||||||
|
await fs.rm(dir, { recursive: true, force: true });
|
||||||
|
} catch {
|
||||||
|
// ignore cleanup failures
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("archive utils", () => {
|
||||||
|
it("detects archive kinds", () => {
|
||||||
|
expect(resolveArchiveKind("/tmp/file.zip")).toBe("zip");
|
||||||
|
expect(resolveArchiveKind("/tmp/file.tgz")).toBe("tar");
|
||||||
|
expect(resolveArchiveKind("/tmp/file.tar.gz")).toBe("tar");
|
||||||
|
expect(resolveArchiveKind("/tmp/file.tar")).toBe("tar");
|
||||||
|
expect(resolveArchiveKind("/tmp/file.txt")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("extracts zip archives", async () => {
|
||||||
|
const workDir = await makeTempDir();
|
||||||
|
const archivePath = path.join(workDir, "bundle.zip");
|
||||||
|
const extractDir = path.join(workDir, "extract");
|
||||||
|
|
||||||
|
const zip = new JSZip();
|
||||||
|
zip.file("package/hello.txt", "hi");
|
||||||
|
await fs.writeFile(archivePath, await zip.generateAsync({ type: "nodebuffer" }));
|
||||||
|
|
||||||
|
await fs.mkdir(extractDir, { recursive: true });
|
||||||
|
await extractArchive({ archivePath, destDir: extractDir, timeoutMs: 5_000 });
|
||||||
|
const rootDir = await resolvePackedRootDir(extractDir);
|
||||||
|
const content = await fs.readFile(path.join(rootDir, "hello.txt"), "utf-8");
|
||||||
|
expect(content).toBe("hi");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("extracts tar archives", async () => {
|
||||||
|
const workDir = await makeTempDir();
|
||||||
|
const archivePath = path.join(workDir, "bundle.tar");
|
||||||
|
const extractDir = path.join(workDir, "extract");
|
||||||
|
const packageDir = path.join(workDir, "package");
|
||||||
|
|
||||||
|
await fs.mkdir(packageDir, { recursive: true });
|
||||||
|
await fs.writeFile(path.join(packageDir, "hello.txt"), "yo");
|
||||||
|
await tar.c({ cwd: workDir, file: archivePath }, ["package"]);
|
||||||
|
|
||||||
|
await fs.mkdir(extractDir, { recursive: true });
|
||||||
|
await extractArchive({ archivePath, destDir: extractDir, timeoutMs: 5_000 });
|
||||||
|
const rootDir = await resolvePackedRootDir(extractDir);
|
||||||
|
const content = await fs.readFile(path.join(rootDir, "hello.txt"), "utf-8");
|
||||||
|
expect(content).toBe("yo");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -405,7 +405,7 @@ export async function runOnboardingWizard(
|
|||||||
nextConfig = await setupSkills(nextConfig, workspaceDir, runtime, prompter);
|
nextConfig = await setupSkills(nextConfig, workspaceDir, runtime, prompter);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup internal hooks (session memory on /new)
|
// Setup hooks (session memory on /new)
|
||||||
nextConfig = await setupInternalHooks(nextConfig, runtime, prompter);
|
nextConfig = await setupInternalHooks(nextConfig, runtime, prompter);
|
||||||
|
|
||||||
nextConfig = applyWizardMetadata(nextConfig, { command: "onboard", mode });
|
nextConfig = applyWizardMetadata(nextConfig, { command: "onboard", mode });
|
||||||
|
|||||||
Reference in New Issue
Block a user