refactor: rename hooks docs and add tests
This commit is contained in:
@@ -144,7 +144,7 @@ describe("handleCommands identity", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("handleCommands internal hooks", () => {
|
||||
describe("handleCommands hooks", () => {
|
||||
it("triggers hooks for /new with arguments", async () => {
|
||||
const cfg = {
|
||||
commands: { text: true },
|
||||
|
||||
@@ -96,7 +96,7 @@ export type MsgContext = {
|
||||
*/
|
||||
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".
|
||||
*/
|
||||
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 lines: string[] = [];
|
||||
lines.push(
|
||||
chalk.bold.cyan("Internal Hooks") + chalk.gray(` (${eligible.length}/${hooks.length} ready)`),
|
||||
);
|
||||
lines.push(chalk.bold.cyan("Hooks") + chalk.gray(` (${eligible.length}/${hooks.length} ready)`));
|
||||
lines.push("");
|
||||
|
||||
if (eligible.length > 0) {
|
||||
@@ -273,7 +271,7 @@ export function formatHooksCheck(report: HookStatusReport, opts: HooksCheckOptio
|
||||
const notEligible = report.hooks.filter((h) => !h.eligible);
|
||||
|
||||
const lines: string[] = [];
|
||||
lines.push(chalk.bold.cyan("Internal Hooks Status"));
|
||||
lines.push(chalk.bold.cyan("Hooks Status"));
|
||||
lines.push("");
|
||||
lines.push(`Total hooks: ${report.hooks.length}`);
|
||||
lines.push(chalk.green(`Ready: ${eligible.length}`));
|
||||
@@ -373,7 +371,7 @@ export function registerHooksCli(program: Command): void {
|
||||
|
||||
hooks
|
||||
.command("list")
|
||||
.description("List all internal hooks")
|
||||
.description("List all hooks")
|
||||
.option("--eligible", "Show only eligible hooks", false)
|
||||
.option("--json", "Output as JSON", false)
|
||||
.option("-v, --verbose", "Show more details including missing requirements", false)
|
||||
|
||||
@@ -59,7 +59,7 @@ describe("onboard-hooks", () => {
|
||||
});
|
||||
|
||||
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");
|
||||
vi.mocked(buildWorkspaceHookStatus).mockReturnValue(createMockHookReport());
|
||||
|
||||
@@ -75,7 +75,7 @@ describe("onboard-hooks", () => {
|
||||
});
|
||||
expect(prompter.note).toHaveBeenCalledTimes(2);
|
||||
expect(prompter.multiselect).toHaveBeenCalledWith({
|
||||
message: "Enable internal hooks?",
|
||||
message: "Enable hooks?",
|
||||
options: [
|
||||
{ value: "__skip__", label: "Skip for now" },
|
||||
{
|
||||
@@ -173,8 +173,8 @@ describe("onboard-hooks", () => {
|
||||
const noteCalls = (prompter.note as ReturnType<typeof vi.fn>).mock.calls;
|
||||
expect(noteCalls).toHaveLength(2);
|
||||
|
||||
// First note should explain what internal hooks are
|
||||
expect(noteCalls[0][0]).toContain("Internal hooks");
|
||||
// First note should explain what hooks are
|
||||
expect(noteCalls[0][0]).toContain("Hooks let you automate actions");
|
||||
expect(noteCalls[0][0]).toContain("automate actions");
|
||||
|
||||
// Second note should confirm configuration
|
||||
|
||||
@@ -11,12 +11,12 @@ export async function setupInternalHooks(
|
||||
): Promise<ClawdbotConfig> {
|
||||
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.",
|
||||
"",
|
||||
"Learn more: https://docs.clawd.bot/internal-hooks",
|
||||
"Learn more: https://docs.clawd.bot/hooks",
|
||||
].join("\n"),
|
||||
"Internal Hooks",
|
||||
"Hooks",
|
||||
);
|
||||
|
||||
// Discover available hooks using the hook discovery system
|
||||
@@ -35,7 +35,7 @@ export async function setupInternalHooks(
|
||||
}
|
||||
|
||||
const toEnable = await prompter.multiselect({
|
||||
message: "Enable internal hooks?",
|
||||
message: "Enable hooks?",
|
||||
options: [
|
||||
{ value: "__skip__", label: "Skip for now" },
|
||||
...recommendedHooks.map((hook) => ({
|
||||
|
||||
@@ -90,7 +90,7 @@ export type HookInstallRecord = {
|
||||
};
|
||||
|
||||
export type InternalHooksConfig = {
|
||||
/** Enable internal hooks system */
|
||||
/** Enable hooks system */
|
||||
enabled?: boolean;
|
||||
/** Legacy: List of internal hook handlers to register (still supported) */
|
||||
handlers?: InternalHookHandlerConfig[];
|
||||
|
||||
@@ -103,7 +103,7 @@ export async function startGatewaySidecars(params: {
|
||||
);
|
||||
}
|
||||
} 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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -53,7 +53,7 @@ session-memory/
|
||||
---
|
||||
name: my-hook
|
||||
description: "Short description"
|
||||
homepage: https://docs.clawd.bot/hooks/my-hook
|
||||
homepage: https://docs.clawd.bot/hooks#my-hook
|
||||
metadata:
|
||||
{ "clawdbot": { "emoji": "🔗", "events": ["command:new"], "requires": { "bins": ["node"] } } }
|
||||
---
|
||||
@@ -161,9 +161,9 @@ interface InternalHookEvent {
|
||||
Example handler:
|
||||
|
||||
```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") {
|
||||
return;
|
||||
}
|
||||
@@ -190,4 +190,4 @@ Test your hooks by:
|
||||
|
||||
## Documentation
|
||||
|
||||
Full documentation: https://docs.clawd.bot/internal-hooks
|
||||
Full documentation: https://docs.clawd.bot/hooks
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: command-logger
|
||||
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:
|
||||
{
|
||||
"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
|
||||
* to a centralized log file for audit/debugging purposes.
|
||||
@@ -26,12 +26,12 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
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
|
||||
*/
|
||||
const logCommand: InternalHookHandler = async (event) => {
|
||||
const logCommand: HookHandler = async (event) => {
|
||||
// Only trigger on command events
|
||||
if (event.type !== "command") {
|
||||
return;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: session-memory
|
||||
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:
|
||||
{
|
||||
"clawdbot":
|
||||
|
||||
@@ -11,7 +11,7 @@ import os from "node:os";
|
||||
import type { ClawdbotConfig } from "../../../config/config.js";
|
||||
import { resolveAgentWorkspaceDir } from "../../../agents/agent-scope.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
|
||||
@@ -57,7 +57,7 @@ async function getRecentSessionContent(sessionFilePath: string): Promise<string
|
||||
/**
|
||||
* 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
|
||||
if (event.type !== "command" || event.action !== "new") {
|
||||
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 path from "node:path";
|
||||
import JSZip from "jszip";
|
||||
import * as tar from "tar";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
@@ -85,6 +86,54 @@ describe("installHooksFromArchive", () => {
|
||||
fs.existsSync(path.join(result.targetDir, "hooks", "zip-hook", "HOOK.md")),
|
||||
).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", () => {
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
type InternalHookEvent,
|
||||
} from "./internal-hooks.js";
|
||||
|
||||
describe("internal-hooks", () => {
|
||||
describe("hooks", () => {
|
||||
beforeEach(() => {
|
||||
clearInternalHooks();
|
||||
});
|
||||
@@ -131,7 +131,7 @@ describe("internal-hooks", () => {
|
||||
expect(errorHandler).toHaveBeenCalled();
|
||||
expect(successHandler).toHaveBeenCalled();
|
||||
expect(consoleError).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Internal hook error"),
|
||||
expect.stringContaining("Hook error"),
|
||||
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.
|
||||
*/
|
||||
|
||||
@@ -91,7 +91,7 @@ export function getRegisteredEventKeys(): string[] {
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger an internal hook event
|
||||
* Trigger a hook event
|
||||
*
|
||||
* Calls all handlers registered for:
|
||||
* 1. The general event type (e.g., 'command')
|
||||
@@ -117,7 +117,7 @@ export async function triggerInternalHook(event: InternalHookEvent): Promise<voi
|
||||
await handler(event);
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`Internal hook error [${event.type}:${event.action}]:`,
|
||||
`Hook error [${event.type}:${event.action}]:`,
|
||||
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 action - The action within that type
|
||||
|
||||
@@ -43,7 +43,7 @@ describe("loader", () => {
|
||||
});
|
||||
|
||||
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 = {
|
||||
hooks: {
|
||||
internal: {
|
||||
@@ -170,7 +170,7 @@ describe("loader", () => {
|
||||
const count = await loadInternalHooks(cfg, tmpDir);
|
||||
expect(count).toBe(0);
|
||||
expect(consoleError).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Failed to load internal hook handler"),
|
||||
expect.stringContaining("Failed to load hook handler"),
|
||||
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
|
||||
* and from directory-based discovery (bundled, managed, workspace)
|
||||
@@ -15,7 +15,7 @@ import { resolveHookConfig } 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:
|
||||
* 1. Directory-based discovery (bundled, managed, workspace)
|
||||
@@ -30,14 +30,14 @@ import { shouldIncludeHook } from "./config.js";
|
||||
* const config = await loadConfig();
|
||||
* const workspaceDir = resolveAgentWorkspaceDir(config, agentId);
|
||||
* const count = await loadInternalHooks(config, workspaceDir);
|
||||
* console.log(`Loaded ${count} internal hook handlers`);
|
||||
* console.log(`Loaded ${count} hook handlers`);
|
||||
* ```
|
||||
*/
|
||||
export async function loadInternalHooks(
|
||||
cfg: ClawdbotConfig,
|
||||
workspaceDir: string,
|
||||
): Promise<number> {
|
||||
// Check if internal hooks are enabled
|
||||
// Check if hooks are enabled
|
||||
if (!cfg.hooks?.internal?.enabled) {
|
||||
return 0;
|
||||
}
|
||||
@@ -71,7 +71,7 @@ export async function loadInternalHooks(
|
||||
|
||||
if (typeof handler !== "function") {
|
||||
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;
|
||||
}
|
||||
@@ -80,7 +80,7 @@ export async function loadInternalHooks(
|
||||
const events = entry.clawdbot?.events ?? [];
|
||||
if (events.length === 0) {
|
||||
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;
|
||||
}
|
||||
@@ -90,12 +90,12 @@ export async function loadInternalHooks(
|
||||
}
|
||||
|
||||
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++;
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`Failed to load internal hook ${entry.hook.name}:`,
|
||||
`Failed to load hook ${entry.hook.name}:`,
|
||||
err instanceof Error ? err.message : String(err),
|
||||
);
|
||||
}
|
||||
@@ -127,7 +127,7 @@ export async function loadInternalHooks(
|
||||
|
||||
if (typeof handler !== "function") {
|
||||
console.error(
|
||||
`Internal hook error: Handler '${exportName}' from ${modulePath} is not a function`,
|
||||
`Hook error: Handler '${exportName}' from ${modulePath} is not a function`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
@@ -135,12 +135,12 @@ export async function loadInternalHooks(
|
||||
// Register the handler
|
||||
registerInternalHook(handlerConfig.event, handler as InternalHookHandler);
|
||||
console.log(
|
||||
`Registered internal hook (legacy): ${handlerConfig.event} -> ${modulePath}${exportName !== "default" ? `#${exportName}` : ""}`,
|
||||
`Registered hook (legacy): ${handlerConfig.event} -> ${modulePath}${exportName !== "default" ? `#${exportName}` : ""}`,
|
||||
);
|
||||
loadedCount++;
|
||||
} catch (err) {
|
||||
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),
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// Setup internal hooks (session memory on /new)
|
||||
// Setup hooks (session memory on /new)
|
||||
nextConfig = await setupInternalHooks(nextConfig, runtime, prompter);
|
||||
|
||||
nextConfig = applyWizardMetadata(nextConfig, { command: "onboard", mode });
|
||||
|
||||
Reference in New Issue
Block a user