diff --git a/CHANGELOG.md b/CHANGELOG.md index c33225c51..dd2fda4ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ - **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:** 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 plugins install ` now copies into `~/.clawdbot/extensions` (use `--link` to keep path-based loading). ### Changes - Tools: improve `web_fetch` extraction using Readability (with fallback). @@ -37,6 +39,8 @@ - Docs: add `/help` hub, Node/npm PATH guide, and expand directory CLI docs. - Config: support env var substitution in config values. (#1044) — thanks @sebslight. - Health: add per-agent session summaries and account-level health details, and allow selective probes. (#1047) — thanks @gumadeiras. +- Hooks: add hook pack installs (npm/path/zip/tar) with `clawdbot.hooks` manifests and `clawdbot hooks install/update`. +- Plugins: add zip installs and `--link` to avoid copying local paths. ### Fixes - Sub-agents: normalize announce delivery origin + queue bucketing by accountId to keep multi-account routing stable. (#1061, #1058) — thanks @adam91holt. diff --git a/docs/automation/gmail-pubsub.md b/docs/automation/gmail-pubsub.md index 3c74672d4..94feba3d7 100644 --- a/docs/automation/gmail-pubsub.md +++ b/docs/automation/gmail-pubsub.md @@ -92,13 +92,13 @@ under `hooks.transformsDir` (see [Webhooks](/automation/webhook)). Use the Clawdbot helper to wire everything together (installs deps on macOS via brew): ```bash -clawdbot hooks gmail setup \ +clawdbot webhooks gmail setup \ --account clawdbot@gmail.com ``` Defaults: - Uses Tailscale Funnel for the public push endpoint. -- Writes `hooks.gmail` config for `clawdbot hooks gmail run`. +- Writes `hooks.gmail` config for `clawdbot webhooks gmail run`. - Enables the Gmail hook preset (`hooks.presets: ["gmail"]`). Path note: when `tailscale.mode` is enabled, Clawdbot automatically sets @@ -124,7 +124,7 @@ Gateway auto-start (recommended): Manual daemon (starts `gog gmail watch serve` + auto-renew): ```bash -clawdbot hooks gmail run +clawdbot webhooks gmail run ``` ## One-time setup @@ -191,7 +191,7 @@ Notes: - `--hook-url` points to Clawdbot `/hooks/gmail` (mapped; isolated run + summary to main). - `--include-body` and `--max-bytes` control the body snippet sent to Clawdbot. -Recommended: `clawdbot hooks gmail run` wraps the same flow and auto-renews the watch. +Recommended: `clawdbot webhooks gmail run` wraps the same flow and auto-renews the watch. ## Expose the handler (advanced, unsupported) diff --git a/docs/automation/webhook.md b/docs/automation/webhook.md index 00734f0d0..f2a62b4e3 100644 --- a/docs/automation/webhook.md +++ b/docs/automation/webhook.md @@ -96,7 +96,7 @@ Mapping options (summary): - TS transforms require a TS loader (e.g. `bun` or `tsx`) or precompiled `.js` at runtime. - Set `deliver: true` + `channel`/`to` on mappings to route replies to a chat surface (`channel` defaults to `last` and falls back to WhatsApp). -- `clawdbot hooks gmail setup` writes `hooks.gmail` config for `clawdbot hooks gmail run`. +- `clawdbot webhooks gmail setup` writes `hooks.gmail` config for `clawdbot webhooks gmail run`. See [Gmail Pub/Sub](/automation/gmail-pubsub) for the full Gmail watch flow. ## Responses diff --git a/docs/cli/hooks.md b/docs/cli/hooks.md index 6824d1880..14f333366 100644 --- a/docs/cli/hooks.md +++ b/docs/cli/hooks.md @@ -1,28 +1,21 @@ --- -summary: "CLI reference for `clawdbot hooks` (internal hooks + Gmail Pub/Sub + webhook helpers)" +summary: "CLI reference for `clawdbot hooks` (internal hooks)" read_when: - You want to manage internal agent hooks - - You want to wire Gmail Pub/Sub events into Clawdbot hooks - - You want to run the gog watch service and renew loop + - You want to install or update internal hooks --- # `clawdbot hooks` -Webhook helpers and hook-based integrations. +Manage internal agent hooks (event-driven automations for commands like `/new`, `/reset`, etc.). Related: - Internal Hooks: [Internal Agent Hooks](/internal-hooks) -- Webhooks: [Webhook](/automation/webhook) -- Gmail Pub/Sub: [Gmail Pub/Sub](/automation/gmail-pubsub) -## Internal Hooks - -Manage internal agent hooks (event-driven automations for commands like `/new`, `/reset`, etc.). - -### List All Hooks +## List All Hooks ```bash -clawdbot hooks internal list +clawdbot hooks list ``` List all discovered internal hooks from workspace, managed, and bundled directories. @@ -45,7 +38,7 @@ Ready: **Example (verbose):** ```bash -clawdbot hooks internal list --verbose +clawdbot hooks list --verbose ``` Shows missing requirements for ineligible hooks. @@ -53,15 +46,15 @@ Shows missing requirements for ineligible hooks. **Example (JSON):** ```bash -clawdbot hooks internal list --json +clawdbot hooks list --json ``` Returns structured JSON for programmatic use. -### Get Hook Information +## Get Hook Information ```bash -clawdbot hooks internal info +clawdbot hooks info ``` Show detailed information about a specific hook. @@ -75,7 +68,7 @@ Show detailed information about a specific hook. **Example:** ```bash -clawdbot hooks internal info session-memory +clawdbot hooks info session-memory ``` **Output:** @@ -96,10 +89,10 @@ Requirements: Config: ✓ workspace.dir ``` -### Check Hooks Eligibility +## Check Hooks Eligibility ```bash -clawdbot hooks internal check +clawdbot hooks check ``` Show summary of hook eligibility status (how many are ready vs. not ready). @@ -117,10 +110,10 @@ Ready: 2 Not ready: 0 ``` -### Enable a Hook +## Enable a Hook ```bash -clawdbot hooks internal enable +clawdbot hooks enable ``` Enable a specific hook by adding it to your config (`~/.clawdbot/config.json`). @@ -131,7 +124,7 @@ Enable a specific hook by adding it to your config (`~/.clawdbot/config.json`). **Example:** ```bash -clawdbot hooks internal enable session-memory +clawdbot hooks enable session-memory ``` **Output:** @@ -148,10 +141,10 @@ clawdbot hooks internal enable session-memory **After enabling:** - Restart the gateway so hooks reload (menu bar app restart on macOS, or restart your gateway process in dev). -### Disable a Hook +## Disable a Hook ```bash -clawdbot hooks internal disable +clawdbot hooks disable ``` Disable a specific hook by updating your config. @@ -162,7 +155,7 @@ Disable a specific hook by updating your config. **Example:** ```bash -clawdbot hooks internal disable command-logger +clawdbot hooks disable command-logger ``` **Output:** @@ -174,6 +167,53 @@ clawdbot hooks internal disable command-logger **After disabling:** - Restart the gateway so hooks reload +## Install Hooks + +```bash +clawdbot hooks install +``` + +Install a hook pack from a local folder/archive or npm. + +**What it does:** +- Copies the hook pack into `~/.clawdbot/hooks/` +- Enables the installed hooks in `hooks.internal.entries.*` +- Records the install under `hooks.internal.installs` + +**Options:** +- `-l, --link`: Link a local directory instead of copying (adds it to `hooks.internal.load.extraDirs`) + +**Supported archives:** `.zip`, `.tgz`, `.tar.gz`, `.tar` + +**Examples:** + +```bash +# Local directory +clawdbot hooks install ./my-hook-pack + +# Local archive +clawdbot hooks install ./my-hook-pack.zip + +# NPM package +clawdbot hooks install @clawdbot/my-hook-pack + +# Link a local directory without copying +clawdbot hooks install -l ./my-hook-pack +``` + +## Update Hooks + +```bash +clawdbot hooks update +clawdbot hooks update --all +``` + +Update installed hook packs (npm installs only). + +**Options:** +- `--all`: Update all tracked hook packs +- `--dry-run`: Show what would change without writing + ## Bundled Hooks ### session-memory @@ -183,7 +223,7 @@ Saves session context to memory when you issue `/new`. **Enable:** ```bash -clawdbot hooks internal enable session-memory +clawdbot hooks enable session-memory ``` **Output:** `~/clawd/memory/YYYY-MM-DD-slug.md` @@ -197,7 +237,7 @@ Logs all command events to a centralized audit file. **Enable:** ```bash -clawdbot hooks internal enable command-logger +clawdbot hooks enable command-logger ``` **Output:** `~/.clawdbot/logs/commands.log` @@ -216,12 +256,3 @@ grep '"action":"new"' ~/.clawdbot/logs/commands.log | jq . ``` **See:** [command-logger documentation](/internal-hooks#command-logger) - -## Gmail - -```bash -clawdbot hooks gmail setup --account you@example.com -clawdbot hooks gmail run -``` - -See [Gmail Pub/Sub documentation](/automation/gmail-pubsub) for details. diff --git a/docs/cli/index.md b/docs/cli/index.md index 27eb5e797..9e9b5ee54 100644 --- a/docs/cli/index.md +++ b/docs/cli/index.md @@ -40,6 +40,7 @@ This page describes the current CLI behavior. If commands change, update this do - [`dns`](/cli/dns) - [`docs`](/cli/docs) - [`hooks`](/cli/hooks) +- [`webhooks`](/cli/webhooks) - [`pairing`](/cli/pairing) - [`plugins`](/cli/plugins) (plugin commands) - [`channels`](/cli/channels) @@ -212,6 +213,14 @@ clawdbot [--dev] [--profile ] console pdf hooks + list + info + check + enable + disable + install + update + webhooks gmail setup|run pairing list @@ -414,12 +423,12 @@ Subcommands: - `pairing list [--json]` - `pairing approve [--notify]` -### `hooks gmail` +### `webhooks gmail` Gmail Pub/Sub hook setup + runner. See [/automation/gmail-pubsub](/automation/gmail-pubsub). Subcommands: -- `hooks gmail setup` (requires `--account `; supports `--project`, `--topic`, `--subscription`, `--label`, `--hook-url`, `--hook-token`, `--push-token`, `--bind`, `--port`, `--path`, `--include-body`, `--max-bytes`, `--renew-minutes`, `--tailscale`, `--tailscale-path`, `--tailscale-target`, `--push-endpoint`, `--json`) -- `hooks gmail run` (runtime overrides for the same flags) +- `webhooks gmail setup` (requires `--account `; supports `--project`, `--topic`, `--subscription`, `--label`, `--hook-url`, `--hook-token`, `--push-token`, `--bind`, `--port`, `--path`, `--include-body`, `--max-bytes`, `--renew-minutes`, `--tailscale`, `--tailscale-path`, `--tailscale-target`, `--push-endpoint`, `--json`) +- `webhooks gmail run` (runtime overrides for the same flags) ### `dns setup` Wide-area discovery DNS helper (CoreDNS + Tailscale). See [/gateway/discovery](/gateway/discovery). diff --git a/docs/cli/plugins.md b/docs/cli/plugins.md index c9d00c6f0..09fdf5e79 100644 --- a/docs/cli/plugins.md +++ b/docs/cli/plugins.md @@ -28,11 +28,19 @@ clawdbot plugins update --all ### Install ```bash -clawdbot plugins install +clawdbot plugins install ``` Security note: treat plugin installs like running code. Prefer pinned versions. +Supported archives: `.zip`, `.tgz`, `.tar.gz`, `.tar`. + +Use `--link` to avoid copying a local directory (adds to `plugins.load.paths`): + +```bash +clawdbot plugins install -l ./my-plugin +``` + ### Update ```bash diff --git a/docs/cli/webhooks.md b/docs/cli/webhooks.md new file mode 100644 index 000000000..efbf20fa0 --- /dev/null +++ b/docs/cli/webhooks.md @@ -0,0 +1,23 @@ +--- +summary: "CLI reference for `clawdbot webhooks` (webhook helpers + Gmail Pub/Sub)" +read_when: + - You want to wire Gmail Pub/Sub events into Clawdbot + - You want webhook helper commands +--- + +# `clawdbot webhooks` + +Webhook helpers and integrations (Gmail Pub/Sub, webhook helpers). + +Related: +- Webhooks: [Webhook](/automation/webhook) +- Gmail Pub/Sub: [Gmail Pub/Sub](/automation/gmail-pubsub) + +## Gmail + +```bash +clawdbot webhooks gmail setup --account you@example.com +clawdbot webhooks gmail run +``` + +See [Gmail Pub/Sub documentation](/automation/gmail-pubsub) for details. diff --git a/docs/gateway/configuration.md b/docs/gateway/configuration.md index e8e37bd21..e9fe9b6e7 100644 --- a/docs/gateway/configuration.md +++ b/docs/gateway/configuration.md @@ -2757,7 +2757,7 @@ Mapping notes: - If there is no prior delivery route, set `channel` + `to` explicitly (required for Telegram/Discord/Slack/Signal/iMessage/MS Teams). - `model` overrides the LLM for this hook run (`provider/model` or alias; must be allowed if `agents.defaults.models` is set). -Gmail helper config (used by `clawdbot hooks gmail setup` / `run`): +Gmail helper config (used by `clawdbot webhooks gmail setup` / `run`): ```json5 { diff --git a/docs/internal-hooks.md b/docs/internal-hooks.md index ff3dd47e1..0800d2c1a 100644 --- a/docs/internal-hooks.md +++ b/docs/internal-hooks.md @@ -13,7 +13,7 @@ Internal hooks provide an extensible event-driven system for automating actions 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. -- **Web-based hooks**: external HTTP webhooks that let other systems trigger work in Clawdbot. See [Webhook Hooks](/automation/webhook). +- **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. Common uses: - Save a memory snapshot when you reset a session @@ -43,25 +43,25 @@ Clawdbot ships with two bundled hooks that are automatically discovered: List available hooks: ```bash -clawdbot hooks internal list +clawdbot hooks list ``` Enable a hook: ```bash -clawdbot hooks internal enable session-memory +clawdbot hooks enable session-memory ``` Check hook status: ```bash -clawdbot hooks internal check +clawdbot hooks check ``` Get detailed information: ```bash -clawdbot hooks internal info session-memory +clawdbot hooks info session-memory ``` ### Onboarding @@ -76,6 +76,8 @@ Hooks are automatically discovered from three directories (in order of precedenc 2. **Managed hooks**: `~/.clawdbot/hooks/` (user-installed, shared across workspaces) 3. **Bundled hooks**: `/dist/hooks/bundled/` (shipped with Clawdbot) +Managed hook directories can be either a **single hook** or a **hook pack** (package directory). + Each hook is a directory containing: ``` @@ -84,6 +86,30 @@ my-hook/ └── handler.ts # Handler implementation ``` +## Hook Packs (npm/archives) + +Hook packs are standard npm packages that export one or more hooks via `clawdbot.hooks` in +`package.json`. Install them with: + +```bash +clawdbot hooks install +``` + +Example `package.json`: + +```json +{ + "name": "@acme/my-hooks", + "version": "0.1.0", + "clawdbot": { + "hooks": ["./hooks/my-hook", "./hooks/other-hook"] + } +} +``` + +Each entry points to a hook directory containing `HOOK.md` and `handler.ts` (or `index.ts`). +Hook packs can ship dependencies; they will be installed under `~/.clawdbot/hooks/`. + ## Hook Structure ### HOOK.md Format @@ -252,10 +278,10 @@ export default handler; ```bash # Verify hook is discovered -clawdbot hooks internal list +clawdbot hooks list # Enable it -clawdbot hooks internal enable my-hook +clawdbot hooks enable my-hook # Restart your gateway process (menu bar app restart on macOS, or restart your dev process) @@ -349,46 +375,46 @@ The old config format still works for backwards compatibility: ```bash # List all hooks -clawdbot hooks internal list +clawdbot hooks list # Show only eligible hooks -clawdbot hooks internal list --eligible +clawdbot hooks list --eligible # Verbose output (show missing requirements) -clawdbot hooks internal list --verbose +clawdbot hooks list --verbose # JSON output -clawdbot hooks internal list --json +clawdbot hooks list --json ``` ### Hook Information ```bash # Show detailed info about a hook -clawdbot hooks internal info session-memory +clawdbot hooks info session-memory # JSON output -clawdbot hooks internal info session-memory --json +clawdbot hooks info session-memory --json ``` ### Check Eligibility ```bash # Show eligibility summary -clawdbot hooks internal check +clawdbot hooks check # JSON output -clawdbot hooks internal check --json +clawdbot hooks check --json ``` ### Enable/Disable ```bash # Enable a hook -clawdbot hooks internal enable session-memory +clawdbot hooks enable session-memory # Disable a hook -clawdbot hooks internal disable command-logger +clawdbot hooks disable command-logger ``` ## Bundled Hooks @@ -427,7 +453,7 @@ Saves session context to memory when you issue `/new`. **Enable**: ```bash -clawdbot hooks internal enable session-memory +clawdbot hooks enable session-memory ``` ### command-logger @@ -468,7 +494,7 @@ grep '"action":"new"' ~/.clawdbot/logs/commands.log | jq . **Enable**: ```bash -clawdbot hooks internal enable command-logger +clawdbot hooks enable command-logger ``` ## Best Practices @@ -550,7 +576,7 @@ Registered hook: command-logger -> command List all discovered hooks: ```bash -clawdbot hooks internal list --verbose +clawdbot hooks list --verbose ``` ### Check Registration @@ -569,7 +595,7 @@ const handler: InternalHookHandler = async (event) => { Check why a hook isn't eligible: ```bash -clawdbot hooks internal info my-hook +clawdbot hooks info my-hook ``` Look for missing requirements in the output. @@ -618,7 +644,7 @@ test('my handler works', async () => { - **`src/hooks/config.ts`**: Eligibility checking - **`src/hooks/hooks-status.ts`**: Status reporting - **`src/hooks/loader.ts`**: Dynamic module loader -- **`src/cli/hooks-internal-cli.ts`**: CLI commands +- **`src/cli/hooks-cli.ts`**: CLI commands - **`src/gateway/server-startup.ts`**: Loads hooks at gateway start - **`src/auto-reply/reply/commands-core.ts`**: Triggers command events @@ -672,7 +698,7 @@ Session reset 3. List all discovered hooks: ```bash - clawdbot hooks internal list + clawdbot hooks list ``` ### Hook Not Eligible @@ -680,7 +706,7 @@ Session reset Check requirements: ```bash -clawdbot hooks internal info my-hook +clawdbot hooks info my-hook ``` Look for missing: @@ -693,7 +719,7 @@ Look for missing: 1. Verify hook is enabled: ```bash - clawdbot hooks internal list + clawdbot hooks list # Should show ✓ next to enabled hooks ``` @@ -772,7 +798,7 @@ node -e "import('./path/to/handler.ts').then(console.log)" 4. Verify and restart your gateway process: ```bash - clawdbot hooks internal list + clawdbot hooks list # Should show: 🎯 my-hook ✓ ``` @@ -785,7 +811,7 @@ node -e "import('./path/to/handler.ts').then(console.log)" ## See Also -- [CLI Reference: hooks internal](/cli/hooks) +- [CLI Reference: hooks](/cli/hooks) - [Bundled Hooks README](https://github.com/clawdbot/clawdbot/tree/main/src/hooks/bundled) - [Webhook Hooks](/automation/webhook) - [Configuration](/gateway/configuration#hooks) diff --git a/docs/plugin.md b/docs/plugin.md index 11d204439..facd7c2c8 100644 --- a/docs/plugin.md +++ b/docs/plugin.md @@ -157,9 +157,11 @@ export default { ```bash clawdbot plugins list clawdbot plugins info -clawdbot plugins install # add a local file/dir to plugins.load.paths +clawdbot plugins install # copy a local file/dir into ~/.clawdbot/extensions/ clawdbot plugins install ./extensions/voice-call # relative path ok -clawdbot plugins install ./plugin.tgz # install from a local tarball +clawdbot plugins install ./plugin.tgz # install from a local tarball +clawdbot plugins install ./plugin.zip # install from a local zip +clawdbot plugins install -l ./extensions/voice-call # link (no copy) for dev clawdbot plugins install @clawdbot/voice-call # install from npm clawdbot plugins update clawdbot plugins update --all diff --git a/docs/start/onboarding.md b/docs/start/onboarding.md index 0bc94fa62..72db7ece5 100644 --- a/docs/start/onboarding.md +++ b/docs/start/onboarding.md @@ -87,7 +87,7 @@ On the first agent run, Clawdbot bootstraps a workspace (default `~/clawd`): Gmail Pub/Sub setup is currently a manual step. Use: ```bash -clawdbot hooks gmail setup --account you@gmail.com +clawdbot webhooks gmail setup --account you@gmail.com ``` See [/automation/gmail-pubsub](/automation/gmail-pubsub) for details. diff --git a/package.json b/package.json index 91466b4a9..686379e9e 100644 --- a/package.json +++ b/package.json @@ -164,6 +164,7 @@ "hono": "4.11.4", "jiti": "^2.6.1", "json5": "^2.2.3", + "jszip": "^3.10.1", "linkedom": "^0.18.12", "long": "5.3.2", "markdown-it": "^14.1.0", @@ -195,7 +196,6 @@ "@types/ws": "^8.18.1", "@vitest/coverage-v8": "^4.0.16", "docx-preview": "^0.3.7", - "jszip": "^3.10.1", "lit": "^3.3.2", "lucide": "^0.562.0", "ollama": "^0.6.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f208ac41b..ff3a95c13 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -106,6 +106,9 @@ importers: json5: specifier: ^2.2.3 version: 2.2.3 + jszip: + specifier: ^3.10.1 + version: 3.10.1 linkedom: specifier: ^0.18.12 version: 0.18.12 @@ -185,9 +188,6 @@ importers: docx-preview: specifier: ^0.3.7 version: 0.3.7 - jszip: - specifier: ^3.10.1 - version: 3.10.1 lit: specifier: ^3.3.2 version: 3.3.2 diff --git a/src/cli/hooks-cli.ts b/src/cli/hooks-cli.ts index 1f55b1698..8c31eaa49 100644 --- a/src/cli/hooks-cli.ts +++ b/src/cli/hooks-cli.ts @@ -1,176 +1,775 @@ +import fs from "node:fs"; +import fsp from "node:fs/promises"; +import path from "node:path"; +import chalk from "chalk"; import type { Command } from "commander"; - -import { danger } from "../globals.js"; +import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js"; +import type { ClawdbotConfig } from "../config/config.js"; +import { resolveArchiveKind } from "../infra/archive.js"; import { - DEFAULT_GMAIL_LABEL, - DEFAULT_GMAIL_MAX_BYTES, - DEFAULT_GMAIL_RENEW_MINUTES, - DEFAULT_GMAIL_SERVE_BIND, - DEFAULT_GMAIL_SERVE_PATH, - DEFAULT_GMAIL_SERVE_PORT, - DEFAULT_GMAIL_SUBSCRIPTION, - DEFAULT_GMAIL_TOPIC, -} from "../hooks/gmail.js"; -import { - type GmailRunOptions, - type GmailSetupOptions, - runGmailService, - runGmailSetup, -} from "../hooks/gmail-ops.js"; + buildWorkspaceHookStatus, + type HookStatusEntry, + type HookStatusReport, +} from "../hooks/hooks-status.js"; +import { loadConfig, writeConfigFile } from "../config/io.js"; +import { installHooksFromNpmSpec, installHooksFromPath, resolveHookInstallDir } from "../hooks/install.js"; +import { recordHookInstall } from "../hooks/installs.js"; import { defaultRuntime } from "../runtime.js"; import { formatDocsLink } from "../terminal/links.js"; import { theme } from "../terminal/theme.js"; -import { registerInternalHooksSubcommands } from "./hooks-internal-cli.js"; +import { resolveUserPath } from "../utils.js"; -export function registerHooksCli(program: Command) { +export type HooksListOptions = { + json?: boolean; + eligible?: boolean; + verbose?: boolean; +}; + +export type HookInfoOptions = { + json?: boolean; +}; + +export type HooksCheckOptions = { + json?: boolean; +}; + +export type HooksUpdateOptions = { + all?: boolean; + dryRun?: boolean; +}; + +/** + * Format a single hook for display in the list + */ +function formatHookLine(hook: HookStatusEntry, verbose = false): string { + const emoji = hook.emoji ?? "🔗"; + const status = hook.eligible + ? chalk.green("✓") + : hook.disabled + ? chalk.yellow("disabled") + : chalk.red("missing reqs"); + + const name = hook.eligible ? chalk.white(hook.name) : chalk.gray(hook.name); + + const desc = chalk.gray( + hook.description.length > 50 ? `${hook.description.slice(0, 47)}...` : hook.description, + ); + + if (verbose) { + const missing: string[] = []; + if (hook.missing.bins.length > 0) { + missing.push(`bins: ${hook.missing.bins.join(", ")}`); + } + if (hook.missing.anyBins.length > 0) { + missing.push(`anyBins: ${hook.missing.anyBins.join(", ")}`); + } + if (hook.missing.env.length > 0) { + missing.push(`env: ${hook.missing.env.join(", ")}`); + } + if (hook.missing.config.length > 0) { + missing.push(`config: ${hook.missing.config.join(", ")}`); + } + if (hook.missing.os.length > 0) { + missing.push(`os: ${hook.missing.os.join(", ")}`); + } + const missingStr = missing.length > 0 ? chalk.red(` [${missing.join("; ")}]`) : ""; + return `${emoji} ${name} ${status}${missingStr}\n ${desc}`; + } + + return `${emoji} ${name} ${status} - ${desc}`; +} + +async function readInstalledPackageVersion(dir: string): Promise { + try { + const raw = await fsp.readFile(path.join(dir, "package.json"), "utf-8"); + const parsed = JSON.parse(raw) as { version?: unknown }; + return typeof parsed.version === "string" ? parsed.version : undefined; + } catch { + return undefined; + } +} + +/** + * Format the hooks list output + */ +export function formatHooksList(report: HookStatusReport, opts: HooksListOptions): string { + const hooks = opts.eligible ? report.hooks.filter((h) => h.eligible) : report.hooks; + + if (opts.json) { + const jsonReport = { + workspaceDir: report.workspaceDir, + managedHooksDir: report.managedHooksDir, + hooks: hooks.map((h) => ({ + name: h.name, + description: h.description, + emoji: h.emoji, + eligible: h.eligible, + disabled: h.disabled, + source: h.source, + events: h.events, + homepage: h.homepage, + missing: h.missing, + })), + }; + return JSON.stringify(jsonReport, null, 2); + } + + if (hooks.length === 0) { + const message = opts.eligible + ? "No eligible hooks found. Run `clawdbot hooks list` to see all hooks." + : "No hooks found."; + return message; + } + + const eligible = hooks.filter((h) => h.eligible); + 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(""); + + if (eligible.length > 0) { + lines.push(chalk.bold.green("Ready:")); + for (const hook of eligible) { + lines.push(` ${formatHookLine(hook, opts.verbose)}`); + } + } + + if (notEligible.length > 0 && !opts.eligible) { + if (eligible.length > 0) lines.push(""); + lines.push(chalk.bold.yellow("Not ready:")); + for (const hook of notEligible) { + lines.push(` ${formatHookLine(hook, opts.verbose)}`); + } + } + + return lines.join("\n"); +} + +/** + * Format detailed info for a single hook + */ +export function formatHookInfo( + report: HookStatusReport, + hookName: string, + opts: HookInfoOptions, +): string { + const hook = report.hooks.find((h) => h.name === hookName || h.hookKey === hookName); + + if (!hook) { + if (opts.json) { + return JSON.stringify({ error: "not found", hook: hookName }, null, 2); + } + return `Hook "${hookName}" not found. Run \`clawdbot hooks list\` to see available hooks.`; + } + + if (opts.json) { + return JSON.stringify(hook, null, 2); + } + + const lines: string[] = []; + const emoji = hook.emoji ?? "🔗"; + const status = hook.eligible + ? chalk.green("✓ Ready") + : hook.disabled + ? chalk.yellow("⏸ Disabled") + : chalk.red("✗ Missing requirements"); + + lines.push(`${emoji} ${chalk.bold.cyan(hook.name)} ${status}`); + lines.push(""); + lines.push(chalk.white(hook.description)); + lines.push(""); + + // Details + lines.push(chalk.bold("Details:")); + lines.push(` Source: ${hook.source}`); + lines.push(` Path: ${chalk.gray(hook.filePath)}`); + lines.push(` Handler: ${chalk.gray(hook.handlerPath)}`); + if (hook.homepage) { + lines.push(` Homepage: ${chalk.blue(hook.homepage)}`); + } + if (hook.events.length > 0) { + lines.push(` Events: ${hook.events.join(", ")}`); + } + + // Requirements + const hasRequirements = + hook.requirements.bins.length > 0 || + hook.requirements.anyBins.length > 0 || + hook.requirements.env.length > 0 || + hook.requirements.config.length > 0 || + hook.requirements.os.length > 0; + + if (hasRequirements) { + lines.push(""); + lines.push(chalk.bold("Requirements:")); + if (hook.requirements.bins.length > 0) { + const binsStatus = hook.requirements.bins.map((bin) => { + const missing = hook.missing.bins.includes(bin); + return missing ? chalk.red(`✗ ${bin}`) : chalk.green(`✓ ${bin}`); + }); + lines.push(` Binaries: ${binsStatus.join(", ")}`); + } + if (hook.requirements.anyBins.length > 0) { + const anyBinsStatus = + hook.missing.anyBins.length > 0 + ? chalk.red(`✗ (any of: ${hook.requirements.anyBins.join(", ")})`) + : chalk.green(`✓ (any of: ${hook.requirements.anyBins.join(", ")})`); + lines.push(` Any binary: ${anyBinsStatus}`); + } + if (hook.requirements.env.length > 0) { + const envStatus = hook.requirements.env.map((env) => { + const missing = hook.missing.env.includes(env); + return missing ? chalk.red(`✗ ${env}`) : chalk.green(`✓ ${env}`); + }); + lines.push(` Environment: ${envStatus.join(", ")}`); + } + if (hook.requirements.config.length > 0) { + const configStatus = hook.configChecks.map((check) => { + return check.satisfied ? chalk.green(`✓ ${check.path}`) : chalk.red(`✗ ${check.path}`); + }); + lines.push(` Config: ${configStatus.join(", ")}`); + } + if (hook.requirements.os.length > 0) { + const osStatus = + hook.missing.os.length > 0 + ? chalk.red(`✗ (${hook.requirements.os.join(", ")})`) + : chalk.green(`✓ (${hook.requirements.os.join(", ")})`); + lines.push(` OS: ${osStatus}`); + } + } + + return lines.join("\n"); +} + +/** + * Format check output + */ +export function formatHooksCheck(report: HookStatusReport, opts: HooksCheckOptions): string { + if (opts.json) { + const eligible = report.hooks.filter((h) => h.eligible); + const notEligible = report.hooks.filter((h) => !h.eligible); + return JSON.stringify( + { + total: report.hooks.length, + eligible: eligible.length, + notEligible: notEligible.length, + hooks: { + eligible: eligible.map((h) => h.name), + notEligible: notEligible.map((h) => ({ + name: h.name, + missing: h.missing, + })), + }, + }, + null, + 2, + ); + } + + const eligible = report.hooks.filter((h) => h.eligible); + const notEligible = report.hooks.filter((h) => !h.eligible); + + const lines: string[] = []; + lines.push(chalk.bold.cyan("Internal Hooks Status")); + lines.push(""); + lines.push(`Total hooks: ${report.hooks.length}`); + lines.push(chalk.green(`Ready: ${eligible.length}`)); + lines.push(chalk.yellow(`Not ready: ${notEligible.length}`)); + + if (notEligible.length > 0) { + lines.push(""); + lines.push(chalk.bold.yellow("Hooks not ready:")); + for (const hook of notEligible) { + const reasons = []; + if (hook.disabled) reasons.push("disabled"); + if (hook.missing.bins.length > 0) reasons.push(`bins: ${hook.missing.bins.join(", ")}`); + if (hook.missing.anyBins.length > 0) + reasons.push(`anyBins: ${hook.missing.anyBins.join(", ")}`); + if (hook.missing.env.length > 0) reasons.push(`env: ${hook.missing.env.join(", ")}`); + if (hook.missing.config.length > 0) reasons.push(`config: ${hook.missing.config.join(", ")}`); + if (hook.missing.os.length > 0) reasons.push(`os: ${hook.missing.os.join(", ")}`); + lines.push(` ${hook.emoji ?? "🔗"} ${hook.name} - ${reasons.join("; ")}`); + } + } + + return lines.join("\n"); +} + +export async function enableHook(hookName: string): Promise { + const config = loadConfig(); + const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config)); + const report = buildWorkspaceHookStatus(workspaceDir, { config }); + const hook = report.hooks.find((h) => h.name === hookName); + + if (!hook) { + throw new Error(`Hook "${hookName}" not found`); + } + + if (!hook.eligible) { + throw new Error(`Hook "${hookName}" is not eligible (missing requirements)`); + } + + // Update config + const entries = { ...config.hooks?.internal?.entries }; + entries[hookName] = { ...entries[hookName], enabled: true }; + + const nextConfig = { + ...config, + hooks: { + ...config.hooks, + internal: { + ...config.hooks?.internal, + enabled: true, + entries, + }, + }, + }; + + await writeConfigFile(nextConfig); + console.log(`${chalk.green("✓")} Enabled hook: ${hook.emoji ?? "🔗"} ${hookName}`); +} + +export async function disableHook(hookName: string): Promise { + const config = loadConfig(); + const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config)); + const report = buildWorkspaceHookStatus(workspaceDir, { config }); + const hook = report.hooks.find((h) => h.name === hookName); + + if (!hook) { + throw new Error(`Hook "${hookName}" not found`); + } + + // Update config + const entries = { ...config.hooks?.internal?.entries }; + entries[hookName] = { ...entries[hookName], enabled: false }; + + const nextConfig = { + ...config, + hooks: { + ...config.hooks, + internal: { + ...config.hooks?.internal, + entries, + }, + }, + }; + + await writeConfigFile(nextConfig); + console.log(`${chalk.yellow("⏸")} Disabled hook: ${hook.emoji ?? "🔗"} ${hookName}`); +} + +export function registerHooksCli(program: Command): void { const hooks = program .command("hooks") - .description("Webhook helpers and hook-based integrations") + .description("Manage internal agent hooks") .addHelpText( "after", () => `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/hooks", "docs.clawd.bot/cli/hooks")}\n`, ); - // Register internal hooks management subcommands - registerInternalHooksSubcommands(hooks); - - const gmail = hooks.command("gmail").description("Gmail Pub/Sub hooks (via gogcli)"); - - gmail - .command("setup") - .description("Configure Gmail watch + Pub/Sub + Clawdbot hooks") - .requiredOption("--account ", "Gmail account to watch") - .option("--project ", "GCP project id (OAuth client owner)") - .option("--topic ", "Pub/Sub topic name", DEFAULT_GMAIL_TOPIC) - .option("--subscription ", "Pub/Sub subscription name", DEFAULT_GMAIL_SUBSCRIPTION) - .option("--label