feat: add docs search command
This commit is contained in:
@@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
- Gateway/CLI: add daemon runtime selection (Node recommended; Bun optional) and document WhatsApp/Baileys Bun WebSocket instability on reconnect.
|
- Gateway/CLI: add daemon runtime selection (Node recommended; Bun optional) and document WhatsApp/Baileys Bun WebSocket instability on reconnect.
|
||||||
|
- CLI: add `clawdbot docs` live docs search with pretty output.
|
||||||
- Agent: treat compaction retry AbortError as a fallback trigger without swallowing non-abort errors. Thanks @erikpr1994 for PR #341.
|
- Agent: treat compaction retry AbortError as a fallback trigger without swallowing non-abort errors. Thanks @erikpr1994 for PR #341.
|
||||||
- Sub-agents: allow `sessions_spawn` model overrides and error on invalid models. Thanks @azade-c for PR #298.
|
- Sub-agents: allow `sessions_spawn` model overrides and error on invalid models. Thanks @azade-c for PR #298.
|
||||||
- Heartbeat: default interval 30m; clarified default prompt usage and HEARTBEAT.md template behavior.
|
- Heartbeat: default interval 30m; clarified default prompt usage and HEARTBEAT.md template behavior.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ summary: "Frequently asked questions about Clawdbot setup, configuration, and us
|
|||||||
---
|
---
|
||||||
# FAQ 🦞
|
# FAQ 🦞
|
||||||
|
|
||||||
Common questions from the community. For detailed configuration, see [Configuration](/configuration).
|
Common questions from the community. For detailed configuration, see [Configuration](/gateway/configuration).
|
||||||
|
|
||||||
## Installation & Setup
|
## Installation & Setup
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ wsl --install
|
|||||||
|
|
||||||
Then open Ubuntu and run the normal Getting Started steps.
|
Then open Ubuntu and run the normal Getting Started steps.
|
||||||
|
|
||||||
Full guide: [Windows (WSL2)](/windows)
|
Full guide: [Windows (WSL2)](/platforms/windows)
|
||||||
|
|
||||||
### How do I install on Linux without Homebrew?
|
### How do I install on Linux without Homebrew?
|
||||||
|
|
||||||
@@ -111,6 +111,16 @@ pnpm clawdbot doctor
|
|||||||
|
|
||||||
It checks your config, skills status, and gateway health. It can also restart the gateway daemon if needed.
|
It checks your config, skills status, and gateway health. It can also restart the gateway daemon if needed.
|
||||||
|
|
||||||
|
### How do I search the docs quickly?
|
||||||
|
|
||||||
|
Use the CLI docs search (live docs):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot docs "gateway lock"
|
||||||
|
```
|
||||||
|
|
||||||
|
The first run will fetch the helper CLIs if they are missing.
|
||||||
|
|
||||||
### Terminal onboarding vs macOS app?
|
### Terminal onboarding vs macOS app?
|
||||||
|
|
||||||
**Use terminal onboarding** (`pnpm clawdbot onboard`) — it's more stable right now.
|
**Use terminal onboarding** (`pnpm clawdbot onboard`) — it's more stable right now.
|
||||||
@@ -319,7 +329,7 @@ Per-group activation can be changed by the owner:
|
|||||||
- `/activation mention` — respond only when mentioned (default)
|
- `/activation mention` — respond only when mentioned (default)
|
||||||
- `/activation always` — respond to all messages
|
- `/activation always` — respond to all messages
|
||||||
|
|
||||||
See [Groups](/groups) for details.
|
See [Groups](/concepts/groups) for details.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -356,7 +366,7 @@ cat ~/.clawdbot/clawdbot.json | grep workspace
|
|||||||
- **Telegram** — Via Bot API (grammY).
|
- **Telegram** — Via Bot API (grammY).
|
||||||
- **Discord** — Bot integration.
|
- **Discord** — Bot integration.
|
||||||
- **iMessage** — Via `imsg` CLI (macOS only).
|
- **iMessage** — Via `imsg` CLI (macOS only).
|
||||||
- **Signal** — Via `signal-cli` (see [Signal](/signal)).
|
- **Signal** — Via `signal-cli` (see [Signal](/providers/signal)).
|
||||||
- **WebChat** — Browser-based chat UI.
|
- **WebChat** — Browser-based chat UI.
|
||||||
|
|
||||||
### Discord: Bot works in channels but not DMs?
|
### Discord: Bot works in channels but not DMs?
|
||||||
@@ -599,7 +609,7 @@ Quick reference (send these in chat):
|
|||||||
|
|
||||||
Slash commands are owner-only (gated by `whatsapp.allowFrom` and command authorization on other surfaces).
|
Slash commands are owner-only (gated by `whatsapp.allowFrom` and command authorization on other surfaces).
|
||||||
Commands are only recognized when the entire message is the command (slash required; no plain-text aliases).
|
Commands are only recognized when the entire message is the command (slash required; no plain-text aliases).
|
||||||
Full list + config: [Slash commands](/slash-commands)
|
Full list + config: [Slash commands](/tools/slash-commands)
|
||||||
|
|
||||||
### How do I switch models on the fly?
|
### How do I switch models on the fly?
|
||||||
|
|
||||||
|
|||||||
19
src/cli/docs-cli.ts
Normal file
19
src/cli/docs-cli.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import type { Command } from "commander";
|
||||||
|
|
||||||
|
import { docsSearchCommand } from "../commands/docs.js";
|
||||||
|
import { defaultRuntime } from "../runtime.js";
|
||||||
|
|
||||||
|
export function registerDocsCli(program: Command) {
|
||||||
|
program
|
||||||
|
.command("docs")
|
||||||
|
.description("Search the live Clawdbot docs")
|
||||||
|
.argument("[query...]", "Search query")
|
||||||
|
.action(async (queryParts: string[]) => {
|
||||||
|
try {
|
||||||
|
await docsSearchCommand(queryParts, defaultRuntime);
|
||||||
|
} catch (err) {
|
||||||
|
defaultRuntime.error(String(err));
|
||||||
|
defaultRuntime.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ import { registerBrowserCli } from "./browser-cli.js";
|
|||||||
import { registerCanvasCli } from "./canvas-cli.js";
|
import { registerCanvasCli } from "./canvas-cli.js";
|
||||||
import { registerCronCli } from "./cron-cli.js";
|
import { registerCronCli } from "./cron-cli.js";
|
||||||
import { createDefaultDeps } from "./deps.js";
|
import { createDefaultDeps } from "./deps.js";
|
||||||
|
import { registerDocsCli } from "./docs-cli.js";
|
||||||
import { registerDnsCli } from "./dns-cli.js";
|
import { registerDnsCli } from "./dns-cli.js";
|
||||||
import { registerGatewayCli } from "./gateway-cli.js";
|
import { registerGatewayCli } from "./gateway-cli.js";
|
||||||
import { registerHooksCli } from "./hooks-cli.js";
|
import { registerHooksCli } from "./hooks-cli.js";
|
||||||
@@ -543,6 +544,7 @@ Examples:
|
|||||||
registerTuiCli(program);
|
registerTuiCli(program);
|
||||||
registerCronCli(program);
|
registerCronCli(program);
|
||||||
registerDnsCli(program);
|
registerDnsCli(program);
|
||||||
|
registerDocsCli(program);
|
||||||
registerHooksCli(program);
|
registerHooksCli(program);
|
||||||
registerPairingCli(program);
|
registerPairingCli(program);
|
||||||
registerTelegramCli(program);
|
registerTelegramCli(program);
|
||||||
|
|||||||
169
src/commands/docs.ts
Normal file
169
src/commands/docs.ts
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
import { hasBinary } from "../agents/skills.js";
|
||||||
|
import { runCommandWithTimeout } from "../process/exec.js";
|
||||||
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
|
|
||||||
|
const SEARCH_TOOL = "https://docs.clawd.bot/mcp.SearchClawdbot";
|
||||||
|
const SEARCH_TIMEOUT_MS = 30_000;
|
||||||
|
const RENDER_TIMEOUT_MS = 10_000;
|
||||||
|
const DEFAULT_SNIPPET_MAX = 220;
|
||||||
|
|
||||||
|
type DocResult = {
|
||||||
|
title: string;
|
||||||
|
link: string;
|
||||||
|
snippet?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type NodeRunner = {
|
||||||
|
cmd: string;
|
||||||
|
args: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type ToolRunOptions = {
|
||||||
|
input?: string;
|
||||||
|
timeoutMs?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function resolveNodeRunner(): NodeRunner {
|
||||||
|
if (hasBinary("pnpm")) return { cmd: "pnpm", args: ["dlx"] };
|
||||||
|
if (hasBinary("npx")) return { cmd: "npx", args: ["-y"] };
|
||||||
|
throw new Error("Missing pnpm or npx; install a Node package runner.");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runNodeTool(
|
||||||
|
tool: string,
|
||||||
|
toolArgs: string[],
|
||||||
|
options: ToolRunOptions = {},
|
||||||
|
) {
|
||||||
|
const runner = resolveNodeRunner();
|
||||||
|
const argv = [runner.cmd, ...runner.args, tool, ...toolArgs];
|
||||||
|
return await runCommandWithTimeout(argv, {
|
||||||
|
timeoutMs: options.timeoutMs ?? SEARCH_TIMEOUT_MS,
|
||||||
|
input: options.input,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runTool(
|
||||||
|
tool: string,
|
||||||
|
toolArgs: string[],
|
||||||
|
options: ToolRunOptions = {},
|
||||||
|
) {
|
||||||
|
if (hasBinary(tool)) {
|
||||||
|
return await runCommandWithTimeout([tool, ...toolArgs], {
|
||||||
|
timeoutMs: options.timeoutMs ?? SEARCH_TIMEOUT_MS,
|
||||||
|
input: options.input,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return await runNodeTool(tool, toolArgs, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractLine(lines: string[], prefix: string): string | undefined {
|
||||||
|
const line = lines.find((value) => value.startsWith(prefix));
|
||||||
|
if (!line) return undefined;
|
||||||
|
return line.slice(prefix.length).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeSnippet(raw: string | undefined, fallback: string): string {
|
||||||
|
const base = raw && raw.trim().length > 0 ? raw : fallback;
|
||||||
|
const cleaned = base.replace(/\s+/g, " ").trim();
|
||||||
|
if (!cleaned) return "";
|
||||||
|
if (cleaned.length <= DEFAULT_SNIPPET_MAX) return cleaned;
|
||||||
|
return `${cleaned.slice(0, DEFAULT_SNIPPET_MAX - 3)}...`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function firstParagraph(text: string): string {
|
||||||
|
const parts = text
|
||||||
|
.split(/\n\s*\n/)
|
||||||
|
.map((chunk) => chunk.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
return parts[0] ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSearchOutput(raw: string): DocResult[] {
|
||||||
|
const normalized = raw.replace(/\r/g, "");
|
||||||
|
const blocks = normalized
|
||||||
|
.split(/\n(?=Title: )/g)
|
||||||
|
.map((chunk) => chunk.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const results: DocResult[] = [];
|
||||||
|
for (const block of blocks) {
|
||||||
|
const lines = block.split("\n");
|
||||||
|
const title = extractLine(lines, "Title:");
|
||||||
|
const link = extractLine(lines, "Link:");
|
||||||
|
if (!title || !link) continue;
|
||||||
|
const content = extractLine(lines, "Content:");
|
||||||
|
const contentIndex = lines.findIndex((line) => line.startsWith("Content:"));
|
||||||
|
const body =
|
||||||
|
contentIndex >= 0 ? lines.slice(contentIndex + 1).join("\n").trim() : "";
|
||||||
|
const snippet = normalizeSnippet(content, firstParagraph(body));
|
||||||
|
results.push({ title, link, snippet: snippet || undefined });
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeMarkdown(text: string): string {
|
||||||
|
return text.replace(/[\[\]()]/g, "\\$&");
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildMarkdown(query: string, results: DocResult[]): string {
|
||||||
|
const lines: string[] = [`# Docs search: ${escapeMarkdown(query)}`, ""];
|
||||||
|
if (results.length === 0) {
|
||||||
|
lines.push("_No results._");
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
for (const item of results) {
|
||||||
|
const title = escapeMarkdown(item.title);
|
||||||
|
const snippet = item.snippet ? escapeMarkdown(item.snippet) : "";
|
||||||
|
const suffix = snippet ? ` - ${snippet}` : "";
|
||||||
|
lines.push(`- [${title}](${item.link})${suffix}`);
|
||||||
|
}
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderMarkdown(markdown: string, runtime: RuntimeEnv) {
|
||||||
|
const width = process.stdout.columns ?? 0;
|
||||||
|
const args = width > 0 ? ["--width", String(width)] : [];
|
||||||
|
try {
|
||||||
|
const res = await runTool("markdansi", args, {
|
||||||
|
timeoutMs: RENDER_TIMEOUT_MS,
|
||||||
|
input: markdown,
|
||||||
|
});
|
||||||
|
if (res.code === 0 && res.stdout.trim()) {
|
||||||
|
runtime.log(res.stdout.trimEnd());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Fall back to plain Markdown if renderer fails or cannot be installed.
|
||||||
|
}
|
||||||
|
runtime.log(markdown.trimEnd());
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function docsSearchCommand(
|
||||||
|
queryParts: string[],
|
||||||
|
runtime: RuntimeEnv,
|
||||||
|
) {
|
||||||
|
const query = queryParts.join(" ").trim();
|
||||||
|
if (!query) {
|
||||||
|
runtime.log("Docs: https://docs.clawd.bot/");
|
||||||
|
runtime.log('Search: clawdbot docs "your query"');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = JSON.stringify({ query });
|
||||||
|
const res = await runTool(
|
||||||
|
"mcporter",
|
||||||
|
["call", SEARCH_TOOL, "--args", payload, "--output", "text"],
|
||||||
|
{ timeoutMs: SEARCH_TIMEOUT_MS },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.code !== 0) {
|
||||||
|
const err = res.stderr.trim() || res.stdout.trim() || `exit ${res.code}`;
|
||||||
|
runtime.error(`Docs search failed: ${err}`);
|
||||||
|
runtime.exit(1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = parseSearchOutput(res.stdout);
|
||||||
|
const markdown = buildMarkdown(query, results);
|
||||||
|
await renderMarkdown(markdown, runtime);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user