From 5599603bdb18ba13ad51c8906a590abce1d23c44 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 15 Jan 2026 09:07:14 +0000 Subject: [PATCH] feat: offer local plugin install in git checkouts --- docs/channels/matrix.md | 23 ++++++++++++++----- docs/plugin.md | 6 +++++ .../onboarding/plugin-install.test.ts | 10 ++++++-- src/commands/onboarding/plugin-install.ts | 22 ++++++++++++++++-- 4 files changed, 51 insertions(+), 10 deletions(-) diff --git a/docs/channels/matrix.md b/docs/channels/matrix.md index 0b7c731c1..add7d4930 100644 --- a/docs/channels/matrix.md +++ b/docs/channels/matrix.md @@ -9,15 +9,26 @@ Status: supported via plugin (matrix-js-sdk). Direct messages, rooms, threads, m ## Plugin required Matrix ships as a plugin and is not bundled with the core install. -- Install via CLI: `clawdbot plugins install @clawdbot/matrix` -- Or select **Matrix** during onboarding and confirm the install prompt -- Details: [Plugins](/plugin) + +Install via CLI (npm registry): +```bash +clawdbot plugins install @clawdbot/matrix +``` + +Local checkout (when running from a git repo): +```bash +clawdbot plugins install ./extensions/matrix +``` + +If you choose Matrix during configure/onboarding and a git checkout is detected, +Clawdbot will offer the local install path automatically. + +Details: [Plugins](/plugin) ## Quick setup (beginner) 1) Install the Matrix plugin: - - From a source checkout: `clawdbot plugins install ./extensions/matrix` - - From npm (if published): `clawdbot plugins install @clawdbot/matrix` - - Or pick **Matrix** in onboarding and confirm the install prompt + - From npm: `clawdbot plugins install @clawdbot/matrix` + - From a local checkout: `clawdbot plugins install ./extensions/matrix` 2) Configure credentials: - Env: `MATRIX_HOMESERVER`, `MATRIX_USER_ID`, `MATRIX_ACCESS_TOKEN` (or `MATRIX_PASSWORD`) - Or config: `channels.matrix.*` diff --git a/docs/plugin.md b/docs/plugin.md index 78a203d4d..4be535625 100644 --- a/docs/plugin.md +++ b/docs/plugin.md @@ -33,6 +33,12 @@ clawdbot plugins install @clawdbot/voice-call See [Voice Call](/plugins/voice-call) for a concrete example plugin. +## Available plugins (official) + +- [Voice Call](/plugins/voice-call) — `@clawdbot/voice-call` +- [Matrix](/channels/matrix) — `@clawdbot/matrix` +- [Zalo](/channels/zalo) — `@clawdbot/zalo` + Clawdbot plugins are **TypeScript modules** loaded at runtime via jiti. They can register: diff --git a/src/commands/onboarding/plugin-install.test.ts b/src/commands/onboarding/plugin-install.test.ts index f31012f42..790a79eed 100644 --- a/src/commands/onboarding/plugin-install.test.ts +++ b/src/commands/onboarding/plugin-install.test.ts @@ -79,7 +79,10 @@ describe("ensureOnboardingPluginInstalled", () => { select: vi.fn(async () => "local") as WizardPrompter["select"], }); const cfg: ClawdbotConfig = {}; - vi.mocked(fs.existsSync).mockReturnValue(true); + vi.mocked(fs.existsSync).mockImplementation((value) => { + const raw = String(value); + return raw.endsWith(`${path.sep}.git`) || raw.endsWith(`${path.sep}extensions${path.sep}zalo`); + }); const result = await ensureOnboardingPluginInstalled({ cfg, @@ -104,7 +107,10 @@ describe("ensureOnboardingPluginInstalled", () => { confirm, }); const cfg: ClawdbotConfig = {}; - vi.mocked(fs.existsSync).mockReturnValue(true); + vi.mocked(fs.existsSync).mockImplementation((value) => { + const raw = String(value); + return raw.endsWith(`${path.sep}.git`) || raw.endsWith(`${path.sep}extensions${path.sep}zalo`); + }); installPluginFromNpmSpec.mockResolvedValue({ ok: false, error: "nope", diff --git a/src/commands/onboarding/plugin-install.ts b/src/commands/onboarding/plugin-install.ts index 041f0b10e..d4e31de2c 100644 --- a/src/commands/onboarding/plugin-install.ts +++ b/src/commands/onboarding/plugin-install.ts @@ -16,7 +16,24 @@ type InstallResult = { installed: boolean; }; -function resolveLocalPath(entry: ChannelPluginCatalogEntry, workspaceDir?: string): string | null { +function hasGitWorkspace(workspaceDir?: string): boolean { + const candidates = new Set(); + candidates.add(path.join(process.cwd(), ".git")); + if (workspaceDir && workspaceDir !== process.cwd()) { + candidates.add(path.join(workspaceDir, ".git")); + } + for (const candidate of candidates) { + if (fs.existsSync(candidate)) return true; + } + return false; +} + +function resolveLocalPath( + entry: ChannelPluginCatalogEntry, + workspaceDir: string | undefined, + allowLocal: boolean, +): string | null { + if (!allowLocal) return null; const raw = entry.install.localPath?.trim(); if (!raw) return null; const candidates = new Set(); @@ -113,7 +130,8 @@ export async function ensureOnboardingPluginInstalled(params: { }): Promise { const { entry, prompter, runtime, workspaceDir } = params; let next = params.cfg; - const localPath = resolveLocalPath(entry, workspaceDir); + const allowLocal = hasGitWorkspace(workspaceDir); + const localPath = resolveLocalPath(entry, workspaceDir, allowLocal); const choice = await promptInstallChoice({ entry, localPath,