From d5d62dfd77dbe6b87c7a5b06606935bc09b96677 Mon Sep 17 00:00:00 2001 From: Flood Sung Date: Sat, 25 Apr 2026 08:32:09 +0000 Subject: [PATCH 1/7] feat(installer): allow custom install directory via --dir / -Dir flag install.sh and install.ps1 previously hardcoded the install path to $HOME/metabot. Add a CLI flag (and matching PowerShell parameter) plus an interactive prompt so users can install MetaBot anywhere. - Priority: --dir / -Dir > METABOT_HOME env var > prompt > default. - Tilde expansion + absolute-path validation; refuses to clobber $HOME / system roots. - Persists METABOT_HOME to ~/.bashrc / ~/.zshrc (Linux/macOS) or user-level env (Windows) when non-default, so the mm/mb/metabot CLIs can locate the install in new shells. --- CHANGELOG.md | 2 + README.md | 2 + README_EN.md | 2 + install.ps1 | 93 +++++++++++++++++++++++++++++++++++++++++- install.sh | 111 ++++++++++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 206 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe819c94..3147ee22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file. ## [Unreleased] ### Added +- `install.sh`: `--dir ` / `-d ` flag to customize the install directory (priority: `--dir` > `METABOT_HOME` env > interactive prompt > `~/metabot`). Non-default paths are persisted to `~/.bashrc` / `~/.zshrc` so the `mb`/`mm`/`metabot` CLIs find the install in new shells. +- `install.ps1`: matching `-Dir ` parameter on Windows; non-default paths persisted via user-level `METABOT_HOME` environment variable. - CONTRIBUTING.md with development setup guide - GitHub Actions CI workflow (Node.js 20/22 build + type check) - Issue templates for bug reports and feature requests diff --git a/README.md b/README.md index f7683e02..00da9b6c 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,8 @@ curl -fsSL https://raw.githubusercontent.com/xvirobotics/metabot/main/install.sh 安装器引导一切:工作目录 → **引擎选择(Claude / Kimi / Codex)** → 订阅登录 → IM 平台 → PM2 自动启动。**5 分钟上手。** +> 自定义安装目录(默认 `~/metabot`):`curl ... | bash -s -- --dir /opt/metabot`,或 `METABOT_HOME=/opt/metabot bash install.sh`。Windows: `.\install.ps1 -Dir C:\opt\metabot`。 + --- ## 三引擎:Claude Code ✕ Kimi Code ✕ Codex CLI 并列一等支持 diff --git a/README_EN.md b/README_EN.md index e19da3e4..aca8f322 100644 --- a/README_EN.md +++ b/README_EN.md @@ -42,6 +42,8 @@ curl -fsSL https://raw.githubusercontent.com/xvirobotics/metabot/main/install.sh The installer walks you through everything: working directory → **engine choice (Claude / Kimi / Codex)** → subscription login → IM platform → auto-start with PM2. **5 minutes to get started.** +> Custom install directory (default `~/metabot`): `curl ... | bash -s -- --dir /opt/metabot`, or `METABOT_HOME=/opt/metabot bash install.sh`. Windows: `.\install.ps1 -Dir C:\opt\metabot`. + --- ## Multi-Engine: Claude Code, Kimi Code, and Codex CLI diff --git a/install.ps1 b/install.ps1 index 8d1dd72b..d96888f8 100644 --- a/install.ps1 +++ b/install.ps1 @@ -1,14 +1,49 @@ # MetaBot Installer for Windows PowerShell -# Usage: irm https://raw.githubusercontent.com/xvirobotics/metabot/main/install.ps1 | iex +# Usage: +# irm https://raw.githubusercontent.com/xvirobotics/metabot/main/install.ps1 | iex +# .\install.ps1 -Dir C:\opt\metabot +# $env:METABOT_HOME = "C:\opt\metabot"; irm | iex #Requires -Version 5.1 +[CmdletBinding()] +param( + [Alias('d', 'InstallDir')] + [string]$Dir = "", + + [switch]$Help +) + $ErrorActionPreference = "Stop" +if ($Help) { + @" +MetaBot Installer (Windows) + +Usage: + .\install.ps1 [-Dir ] + irm | iex # uses default ($env:USERPROFILE\metabot) or $env:METABOT_HOME + +Parameters: + -Dir, -d Install MetaBot to . + Priority: -Dir > `$env:METABOT_HOME > interactive prompt. + Default: `$env:USERPROFILE\metabot + -Help Show this help and exit. + +Examples: + .\install.ps1 + .\install.ps1 -Dir C:\opt\metabot + `$env:METABOT_HOME = "C:\opt\metabot"; irm | iex +"@ | Write-Host + exit 0 +} + # ============================================================================ # Configuration defaults # ============================================================================ $MetabotRepo = if ($env:METABOT_REPO) { $env:METABOT_REPO } else { "https://github.com/xvirobotics/metabot.git" } -$MetabotHome = if ($env:METABOT_HOME) { $env:METABOT_HOME } else { Join-Path $env:USERPROFILE "metabot" } +# $MetabotHome is resolved later (Phase 0.5) — priority: -Dir > env > prompt > default. +$DefaultMetabotHome = Join-Path $env:USERPROFILE "metabot" +$MetabotHome = $null # ============================================================================ # Helper functions (colors via Write-Host -ForegroundColor) @@ -104,6 +139,51 @@ if ($PSVer.Major -lt 5 -or ($PSVer.Major -eq 5 -and $PSVer.Minor -lt 1)) { exit 1 } +# ============================================================================ +# Phase 0.5: Resolve install directory +# Priority: -Dir parameter > $env:METABOT_HOME > interactive prompt > default. +# ============================================================================ +Write-Step "Phase 0.5: Choose install directory" + +if ($Dir) { + $MetabotHome = $Dir + Write-Info "Using install directory from -Dir: $MetabotHome" +} elseif ($env:METABOT_HOME) { + $MetabotHome = $env:METABOT_HOME + Write-Info "Using install directory from `$env:METABOT_HOME: $MetabotHome" +} else { + Write-Host "" + Write-Host "Where should MetaBot be installed?" -ForegroundColor White + Write-Host " (Override later with -Dir or `$env:METABOT_HOME.)" + $MetabotHome = Read-Input "Install directory" $DefaultMetabotHome +} + +# Expand a leading ~ to $env:USERPROFILE. +if ($MetabotHome.StartsWith("~")) { + $MetabotHome = Join-Path $env:USERPROFILE ($MetabotHome.Substring(1).TrimStart('\','/')) +} + +# Require a rooted path so all later $MetabotHome references are unambiguous. +if (-not [System.IO.Path]::IsPathRooted($MetabotHome)) { + Write-Err "Install path must be absolute, got: $MetabotHome" + exit 1 +} + +# Refuse a few obviously-bad targets that would clobber the user's profile or a system root. +$normalized = $MetabotHome.TrimEnd('\','/') +$forbidden = @( + $env:USERPROFILE.TrimEnd('\','/'), + $env:SystemDrive, # e.g. "C:" + (Join-Path $env:SystemDrive 'Users').TrimEnd('\','/'), + (Join-Path $env:SystemDrive 'Windows').TrimEnd('\','/') +) | ForEach-Object { $_.TrimEnd('\','/') } +if ($forbidden -contains $normalized -or $normalized -eq '') { + Write-Err "Refusing to install directly into $MetabotHome — pick a dedicated subdirectory." + exit 1 +} + +Write-Success "Install directory: $MetabotHome" + # ============================================================================ # Phase 1: Check prerequisites # ============================================================================ @@ -633,6 +713,15 @@ if ($HasBash) { Write-Warn "Install Git for Windows (https://git-scm.com) to enable CLI tools." } +# Persist METABOT_HOME for non-default install paths so the CLI tools +# (mm/mb/metabot) can find the install in new shell sessions. The CLIs all +# fall back to ~/metabot, so we only need to persist when it differs. +if ($MetabotHome -ne $DefaultMetabotHome) { + [System.Environment]::SetEnvironmentVariable("METABOT_HOME", $MetabotHome, "User") + $env:METABOT_HOME = $MetabotHome + Write-Info "Persisted METABOT_HOME=$MetabotHome to user environment" +} + # ============================================================================ # Phase 7: MetaMemory # ============================================================================ diff --git a/install.sh b/install.sh index 9ed9f744..62e9cf53 100755 --- a/install.sh +++ b/install.sh @@ -1,6 +1,9 @@ #!/usr/bin/env bash # MetaBot Installer -# Usage: curl -fsSL https://raw.githubusercontent.com/xvirobotics/metabot/main/install.sh | bash +# Usage: +# curl -fsSL https://raw.githubusercontent.com/xvirobotics/metabot/main/install.sh | bash +# curl -fsSL https://raw.githubusercontent.com/xvirobotics/metabot/main/install.sh | bash -s -- --dir /opt/metabot +# METABOT_HOME=/opt/metabot bash install.sh set -euo pipefail # ============================================================================ @@ -13,10 +16,59 @@ else TTY=/dev/stdin fi +# ============================================================================ +# Parse CLI arguments +# ============================================================================ +INSTALL_DIR_ARG="" +print_usage() { + cat <<'USAGE' +MetaBot Installer + +Usage: + bash install.sh [OPTIONS] + curl -fsSL | bash -s -- [OPTIONS] + +Options: + -d, --dir Install MetaBot to . + Priority: --dir > METABOT_HOME env var > interactive prompt. + Default: $HOME/metabot + -h, --help Show this help and exit. + +Examples: + bash install.sh + bash install.sh --dir /opt/metabot + bash install.sh -d ~/projects/metabot + METABOT_HOME=/opt/metabot bash install.sh +USAGE +} + +while [[ $# -gt 0 ]]; do + case "$1" in + -d|--dir) + [[ $# -ge 2 ]] || { echo "Error: $1 requires a path argument" >&2; exit 1; } + INSTALL_DIR_ARG="$2" + shift 2 + ;; + --dir=*) + INSTALL_DIR_ARG="${1#--dir=}" + shift + ;; + -h|--help) + print_usage + exit 0 + ;; + *) + echo "Warning: unknown argument '$1'" >&2 + shift + ;; + esac +done + # ============================================================================ # Configuration defaults # ============================================================================ -METABOT_HOME="${METABOT_HOME:-$HOME/metabot}" +# METABOT_HOME is resolved later (Phase 0.5) — priority: --dir > env var > prompt > default. +DEFAULT_METABOT_HOME="$HOME/metabot" METABOT_REPO="${METABOT_REPO:-https://github.com/xvirobotics/metabot.git}" # ============================================================================ @@ -131,6 +183,43 @@ sed_i() { fi } +# ============================================================================ +# Phase 0.5: Resolve install directory +# Priority: --dir CLI arg > METABOT_HOME env var > interactive prompt > default. +# ============================================================================ +step "Phase 0.5: Choose install directory" + +if [[ -n "$INSTALL_DIR_ARG" ]]; then + METABOT_HOME="$INSTALL_DIR_ARG" + info "Using install directory from --dir: $METABOT_HOME" +elif [[ -n "${METABOT_HOME:-}" ]]; then + info "Using install directory from METABOT_HOME env: $METABOT_HOME" +else + echo "" + echo -e "${BOLD}Where should MetaBot be installed?${NC}" + echo " (You can override later with the METABOT_HOME env var or --dir flag.)" + prompt_input METABOT_HOME "Install directory" "$DEFAULT_METABOT_HOME" +fi + +# Expand a leading ~ to $HOME (avoids eval; safe with spaces). +METABOT_HOME="${METABOT_HOME/#\~/$HOME}" + +# Require an absolute path so all later $METABOT_HOME references are unambiguous. +if [[ "$METABOT_HOME" != /* ]]; then + error "Install path must be absolute, got: $METABOT_HOME" + exit 1 +fi + +# Refuse a few obviously-bad targets that would clobber the user's home or root. +case "$METABOT_HOME" in + /|/root|/home|/Users|"$HOME") + error "Refusing to install directly into $METABOT_HOME — pick a dedicated subdirectory." + exit 1 + ;; +esac + +success "Install directory: $METABOT_HOME" + # ============================================================================ # Phase 1: Check prerequisites # ============================================================================ @@ -1001,6 +1090,24 @@ if ! echo "$PATH" | grep -q "$LOCAL_BIN"; then fi success "mm/mb/metabot CLI tools installed to $LOCAL_BIN" +# Persist METABOT_HOME for non-default install paths so the CLI tools +# (mm/mb/metabot) can find the install in new shell sessions. The CLIs all +# fall back to $HOME/metabot, so we only need to export when it differs. +if [[ "$METABOT_HOME" != "$DEFAULT_METABOT_HOME" ]]; then + for rc_file in "$HOME/.bashrc" "$HOME/.zshrc" "$HOME/.profile"; do + [[ -f "$rc_file" ]] || continue + # Drop any prior export to keep this idempotent across re-runs. + if grep -q '^export METABOT_HOME=' "$rc_file" 2>/dev/null; then + sed_i '/^export METABOT_HOME=/d' "$rc_file" + fi + done + echo "export METABOT_HOME=\"$METABOT_HOME\"" >> "$HOME/.bashrc" + if [[ -f "$HOME/.zshrc" ]]; then + echo "export METABOT_HOME=\"$METABOT_HOME\"" >> "$HOME/.zshrc" + fi + info "Persisted METABOT_HOME=$METABOT_HOME to shell rc files" +fi + # ============================================================================ # Phase 8: Build + Start MetaBot with PM2 # ============================================================================ From c80258c0f888798c3beccf0e0517e8fa9f770a8b Mon Sep 17 00:00:00 2001 From: Flood Sung Date: Fri, 1 May 2026 07:18:09 +0000 Subject: [PATCH 2/7] fix(codex): show model metadata in cards --- src/engines/claude/stream-processor.ts | 6 ++ src/engines/codex/executor.ts | 103 ++++++++++++++++++++++++- tests/codex-build-args.test.ts | 29 ++++++- 3 files changed, 134 insertions(+), 4 deletions(-) diff --git a/src/engines/claude/stream-processor.ts b/src/engines/claude/stream-processor.ts index 65a1c4f4..ab4843ea 100644 --- a/src/engines/claude/stream-processor.ts +++ b/src/engines/claude/stream-processor.ts @@ -91,6 +91,9 @@ export class StreamProcessor { toolCalls: [...this.toolCalls], costUsd: this.costUsd, durationMs: this.durationMs, + model: this._model, + totalTokens: this._totalTokens, + contextWindow: this._contextWindow, pendingQuestion: this._pendingQuestions[0] || undefined, backgroundEvents: this._backgroundEvents.size > 0 ? [...this._backgroundEvents.values()] @@ -384,6 +387,9 @@ export class StreamProcessor { toolCalls: [...this.toolCalls], costUsd: this.costUsd, durationMs: this.durationMs, + model: this._model, + totalTokens: this._totalTokens, + contextWindow: this._contextWindow, pendingQuestion: this._pendingQuestions[0] || undefined, backgroundEvents: this._backgroundEvents.size > 0 ? [...this._backgroundEvents.values()] diff --git a/src/engines/codex/executor.ts b/src/engines/codex/executor.ts index c12ba6cf..ded879a5 100644 --- a/src/engines/codex/executor.ts +++ b/src/engines/codex/executor.ts @@ -1,4 +1,7 @@ import { execSync, spawn, type ChildProcess } from 'node:child_process'; +import { existsSync, readFileSync } from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; import type { BotConfigBase, CodexBotConfig } from '../../config.js'; import type { Logger } from '../../utils/logger.js'; import { AsyncQueue } from '../../utils/async-queue.js'; @@ -15,6 +18,7 @@ import { } from './jsonl-translator.js'; const isWindows = process.platform === 'win32'; +const FALLBACK_CODEX_CONTEXT_WINDOW = 272000; function resolveCodexPath(): string { if (process.env.CODEX_EXECUTABLE_PATH) return process.env.CODEX_EXECUTABLE_PATH; @@ -22,12 +26,104 @@ function resolveCodexPath(): string { const cmd = isWindows ? 'where codex' : 'which codex'; return execSync(cmd, { encoding: 'utf-8' }).trim().split(/\r?\n/)[0]; } catch { - return isWindows ? 'codex' : '/usr/local/bin/codex'; + if (!isWindows) { + for (const candidate of ['/usr/local/bin/codex', '/usr/bin/codex', '/opt/homebrew/bin/codex']) { + if (existsSync(candidate)) return candidate; + } + } + return 'codex'; } } const CODEX_EXECUTABLE = resolveCodexPath(); +interface CodexModelMetadata { + model?: string; + contextWindow?: number; +} + +export function resolveCodexModelMetadata(codexConfig: CodexBotConfig, requestedModel?: string): CodexModelMetadata { + const model = requestedModel + || codexConfig.model + || codexConfig.displayModel + || readCodexConfigModel(codexConfig.profile) + || readDefaultModelFromCache(); + return { + model, + contextWindow: codexConfig.contextWindow ?? readContextWindowFromCache(model) ?? (model ? FALLBACK_CODEX_CONTEXT_WINDOW : undefined), + }; +} + +function readCodexConfigModel(profile?: string): string | undefined { + const configPath = process.env.CODEX_HOME + ? path.join(process.env.CODEX_HOME, 'config.toml') + : path.join(os.homedir(), '.codex', 'config.toml'); + try { + const text = readFileSync(configPath, 'utf-8'); + const profileModel = profile ? readTomlSectionValue(text, `profiles.${profile}`, 'model') : undefined; + return profileModel ?? readTomlTopLevelValue(text, 'model'); + } catch { + return undefined; + } +} + +function readDefaultModelFromCache(): string | undefined { + return readModelsCache()?.models?.find((m) => m.slug)?.slug; +} + +function readContextWindowFromCache(model: string | undefined): number | undefined { + if (!model) return undefined; + const found = readModelsCache()?.models?.find((m) => m.slug === model); + return found?.context_window ?? found?.max_context_window; +} + +function readModelsCache(): { models?: Array<{ slug?: string; context_window?: number; max_context_window?: number }> } | undefined { + const cachePath = process.env.CODEX_HOME + ? path.join(process.env.CODEX_HOME, 'models_cache.json') + : path.join(os.homedir(), '.codex', 'models_cache.json'); + try { + return JSON.parse(readFileSync(cachePath, 'utf-8')) as { models?: Array<{ slug?: string; context_window?: number; max_context_window?: number }> }; + } catch { + return undefined; + } +} + +function readTomlTopLevelValue(text: string, key: string): string | undefined { + for (const line of text.split(/\r?\n/)) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + if (trimmed.startsWith('[')) return undefined; + const value = parseTomlStringAssignment(trimmed, key); + if (value) return value; + } + return undefined; +} + +function readTomlSectionValue(text: string, section: string, key: string): string | undefined { + let inSection = false; + for (const line of text.split(/\r?\n/)) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + const header = trimmed.match(/^\[([^\]]+)\]$/); + if (header) { + inSection = header[1] === section; + continue; + } + if (!inSection) continue; + const value = parseTomlStringAssignment(trimmed, key); + if (value) return value; + } + return undefined; +} + +function parseTomlStringAssignment(line: string, key: string): string | undefined { + const match = line.match(new RegExp(`^${key}\\s*=\\s*(.+?)(?:\\s+#.*)?$`)); + if (!match) return undefined; + const raw = match[1].trim(); + const quoted = raw.match(/^["'](.+)["']$/); + return quoted ? quoted[1] : raw; +} + /** * Build the argv array for `codex exec`. Exported for unit testing. * Values are passed as discrete argv entries (never through a shell), so @@ -75,11 +171,12 @@ export class CodexExecutor { const { prompt, cwd, sessionId, abortController, outputsDir, apiContext } = options; const codexConfig = this.config.codex ?? {}; const model = options.model ?? codexConfig.model; + const modelMetadata = resolveCodexModelMetadata(codexConfig, model); const fullPrompt = this.buildPromptWithContext(prompt, outputsDir, apiContext); const queue = new AsyncQueue(); const state = createCodexTranslatorState({ - model: model || codexConfig.displayModel, - contextWindow: codexConfig.contextWindow, + model: modelMetadata.model, + contextWindow: modelMetadata.contextWindow, }); const args = buildCodexArgs(codexConfig, cwd, fullPrompt, sessionId, model); const startTime = Date.now(); diff --git a/tests/codex-build-args.test.ts b/tests/codex-build-args.test.ts index c5dd38a1..aeb35d55 100644 --- a/tests/codex-build-args.test.ts +++ b/tests/codex-build-args.test.ts @@ -1,5 +1,8 @@ import { describe, expect, it } from 'vitest'; -import { buildCodexArgs } from '../src/engines/codex/executor.js'; +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { buildCodexArgs, resolveCodexModelMetadata } from '../src/engines/codex/executor.js'; import type { CodexBotConfig } from '../src/config.js'; describe('buildCodexArgs', () => { @@ -70,4 +73,28 @@ describe('buildCodexArgs', () => { const args = buildCodexArgs({}, cwd, evil, undefined, undefined); expect(args[args.length - 1]).toBe(evil); }); + + it('infers Codex display model and context from CODEX_HOME files', () => { + const priorCodexHome = process.env.CODEX_HOME; + const dir = mkdtempSync(join(tmpdir(), 'metabot-codex-')); + try { + process.env.CODEX_HOME = dir; + writeFileSync(join(dir, 'config.toml'), 'model = "gpt-test"\n'); + writeFileSync(join(dir, 'models_cache.json'), JSON.stringify({ + models: [ + { slug: 'gpt-test', context_window: 123456 }, + { slug: 'gpt-other', context_window: 999 }, + ], + })); + + expect(resolveCodexModelMetadata({})).toEqual({ + model: 'gpt-test', + contextWindow: 123456, + }); + } finally { + if (priorCodexHome === undefined) delete process.env.CODEX_HOME; + else process.env.CODEX_HOME = priorCodexHome; + rmSync(dir, { recursive: true, force: true }); + } + }); }); From b48ead49c6db75eae413f2f56c8d0397a98c31f5 Mon Sep 17 00:00:00 2001 From: Flood Sung Date: Fri, 1 May 2026 07:31:33 +0000 Subject: [PATCH 3/7] fix(codex): mirror skills and avoid bwrap sandbox --- bin/mb | 33 +++++++++--- bin/metabot | 35 +++++++++---- src/api/routes/skill-hub-routes.ts | 6 ++- src/api/skills-installer.ts | 78 +++++++++++++++++------------ src/bridge/message-bridge.ts | 22 +++++--- src/engines/codex/executor.ts | 2 +- src/skills/metaskill/SKILL.md | 4 +- src/skills/metaskill/flows/agent.md | 5 +- src/skills/metaskill/flows/skill.md | 18 ++++--- src/skills/metaskill/flows/team.md | 29 ++++++----- src/workspace/CLAUDE.md | 8 +-- tests/codex-build-args.test.ts | 4 +- tests/message-bridge.test.ts | 16 +++++- tests/skills-installer.test.ts | 55 ++++++++++++++++++++ 14 files changed, 228 insertions(+), 87 deletions(-) create mode 100644 tests/skills-installer.test.ts diff --git a/bin/mb b/bin/mb index 2de3de8b..e19e387f 100755 --- a/bin/mb +++ b/bin/mb @@ -351,7 +351,8 @@ print(json.dumps({'botName': sys.argv[1], 'source': sys.argv[2]})) echo "==> Updating skills..." SKILLS_DIR="$HOME/.claude/skills" - mkdir -p "$SKILLS_DIR" + CODEX_SKILLS_DIR="$HOME/.codex/skills" + mkdir -p "$SKILLS_DIR" "$CODEX_SKILLS_DIR" for skill in metaskill metamemory metabot voice phone-call skill-hub; do case "$skill" in metaskill) src="$METABOT_SRC/src/skills/metaskill" ;; @@ -363,8 +364,10 @@ print(json.dumps({'botName': sys.argv[1], 'source': sys.argv[2]})) *) src="" ;; esac if [[ -n "$src" && -d "$src" ]]; then - mkdir -p "$SKILLS_DIR/$skill" - cp -r "$src/." "$SKILLS_DIR/$skill/" + for dst_root in "$SKILLS_DIR" "$CODEX_SKILLS_DIR"; do + mkdir -p "$dst_root/$skill" + cp -r "$src/." "$dst_root/$skill/" + done echo " Updated: $skill" fi done @@ -373,6 +376,9 @@ print(json.dumps({'botName': sys.argv[1], 'source': sys.argv[2]})) rm -rf "$SKILLS_DIR/feishu-doc" echo " Removed legacy: feishu-doc" fi + if [[ -d "$CODEX_SKILLS_DIR/feishu-doc" ]]; then + rm -rf "$CODEX_SKILLS_DIR/feishu-doc" + fi # Check if lark-cli skills were previously installed (opt-in via install.sh) has_lark_skills=false @@ -389,19 +395,24 @@ print(json.dumps({'botName': sys.argv[1], 'source': sys.argv[2]})) " 2>/dev/null || true) if [[ -n "${work_dir:-}" ]]; then ws_skills_dir="$work_dir/.claude/skills" - mkdir -p "$ws_skills_dir" + ws_codex_skills_dir="$work_dir/.codex/skills" + mkdir -p "$ws_skills_dir" "$ws_codex_skills_dir" for skill in metaskill metamemory metabot voice phone-call skill-hub; do if [[ -d "$SKILLS_DIR/$skill" ]]; then - mkdir -p "$ws_skills_dir/$skill" - cp -r "$SKILLS_DIR/$skill/." "$ws_skills_dir/$skill/" + for dst_root in "$ws_skills_dir" "$ws_codex_skills_dir"; do + mkdir -p "$dst_root/$skill" + cp -r "$SKILLS_DIR/$skill/." "$dst_root/$skill/" + done fi done # Copy lark-cli skills if previously installed if [[ "${has_lark_skills:-false}" == "true" ]]; then for lark_skill in lark-base lark-calendar lark-contact lark-doc lark-drive lark-event lark-im lark-mail lark-minutes lark-openapi-explorer lark-shared lark-sheets lark-skill-maker lark-task lark-vc lark-whiteboard lark-wiki lark-workflow-meeting-summary lark-workflow-standup-report; do if [[ -d "$SKILLS_DIR/$lark_skill" ]]; then - mkdir -p "$ws_skills_dir/$lark_skill" - cp -r "$SKILLS_DIR/$lark_skill/." "$ws_skills_dir/$lark_skill/" + for dst_root in "$ws_skills_dir" "$ws_codex_skills_dir"; do + mkdir -p "$dst_root/$lark_skill" + cp -r "$SKILLS_DIR/$lark_skill/." "$dst_root/$lark_skill/" + done fi done fi @@ -409,8 +420,14 @@ print(json.dumps({'botName': sys.argv[1], 'source': sys.argv[2]})) if [[ -d "$ws_skills_dir/feishu-doc" ]]; then rm -rf "$ws_skills_dir/feishu-doc" fi + if [[ -d "$ws_codex_skills_dir/feishu-doc" ]]; then + rm -rf "$ws_codex_skills_dir/feishu-doc" + fi if [[ -f "$METABOT_SRC/src/workspace/CLAUDE.md" ]]; then cp "$METABOT_SRC/src/workspace/CLAUDE.md" "$work_dir/CLAUDE.md" + if [[ ! -e "$work_dir/AGENTS.md" ]]; then + cp "$METABOT_SRC/src/workspace/CLAUDE.md" "$work_dir/AGENTS.md" + fi fi fi fi diff --git a/bin/metabot b/bin/metabot index 2e3e6eb8..67a4796b 100755 --- a/bin/metabot +++ b/bin/metabot @@ -62,9 +62,10 @@ cmd_update() { success "CLI tools updated" fi - # Update skills in ~/.claude/skills + # Update skills in ~/.claude/skills and ~/.codex/skills SKILLS_DIR="$HOME/.claude/skills" - mkdir -p "$SKILLS_DIR" + CODEX_SKILLS_DIR="$HOME/.codex/skills" + mkdir -p "$SKILLS_DIR" "$CODEX_SKILLS_DIR" info "Updating skills..." local src="" for skill in metaskill metamemory metabot voice skill-hub; do @@ -77,8 +78,10 @@ cmd_update() { *) src="" ;; esac if [[ -n "$src" && -d "$src" ]]; then - mkdir -p "$SKILLS_DIR/$skill" - cp -r "$src/." "$SKILLS_DIR/$skill/" + for dst_root in "$SKILLS_DIR" "$CODEX_SKILLS_DIR"; do + mkdir -p "$dst_root/$skill" + cp -r "$src/." "$dst_root/$skill/" + done fi done # Clean up legacy feishu-doc skill @@ -86,6 +89,9 @@ cmd_update() { rm -rf "$SKILLS_DIR/feishu-doc" info "Removed legacy feishu-doc skill" fi + if [[ -d "$CODEX_SKILLS_DIR/feishu-doc" ]]; then + rm -rf "$CODEX_SKILLS_DIR/feishu-doc" + fi # Update lark-cli skills only if previously installed (opt-in via install.sh) local has_lark_skills=false @@ -104,21 +110,26 @@ cmd_update() { " 2>/dev/null || true) if [[ -n "$work_dir" ]]; then local ws_skills_dir="$work_dir/.claude/skills" - mkdir -p "$ws_skills_dir" + local ws_codex_skills_dir="$work_dir/.codex/skills" + mkdir -p "$ws_skills_dir" "$ws_codex_skills_dir" # Copy common skills for skill in metaskill metamemory metabot voice skill-hub; do if [[ -d "$SKILLS_DIR/$skill" ]]; then - mkdir -p "$ws_skills_dir/$skill" - cp -r "$SKILLS_DIR/$skill/." "$ws_skills_dir/$skill/" + for dst_root in "$ws_skills_dir" "$ws_codex_skills_dir"; do + mkdir -p "$dst_root/$skill" + cp -r "$SKILLS_DIR/$skill/." "$dst_root/$skill/" + done fi done # Copy lark-cli skills if previously installed if [[ "$has_lark_skills" == "true" ]]; then for lark_skill in lark-base lark-calendar lark-contact lark-doc lark-drive lark-event lark-im lark-mail lark-minutes lark-openapi-explorer lark-shared lark-sheets lark-skill-maker lark-task lark-vc lark-whiteboard lark-wiki lark-workflow-meeting-summary lark-workflow-standup-report; do if [[ -d "$SKILLS_DIR/$lark_skill" ]]; then - mkdir -p "$ws_skills_dir/$lark_skill" - cp -r "$SKILLS_DIR/$lark_skill/." "$ws_skills_dir/$lark_skill/" + for dst_root in "$ws_skills_dir" "$ws_codex_skills_dir"; do + mkdir -p "$dst_root/$lark_skill" + cp -r "$SKILLS_DIR/$lark_skill/." "$dst_root/$lark_skill/" + done fi done fi @@ -126,9 +137,15 @@ cmd_update() { if [[ -d "$ws_skills_dir/feishu-doc" ]]; then rm -rf "$ws_skills_dir/feishu-doc" fi + if [[ -d "$ws_codex_skills_dir/feishu-doc" ]]; then + rm -rf "$ws_codex_skills_dir/feishu-doc" + fi # Update CLAUDE.md if [[ -f "$METABOT_HOME/src/workspace/CLAUDE.md" ]]; then cp "$METABOT_HOME/src/workspace/CLAUDE.md" "$work_dir/CLAUDE.md" + if [[ ! -e "$work_dir/AGENTS.md" ]]; then + cp "$METABOT_HOME/src/workspace/CLAUDE.md" "$work_dir/AGENTS.md" + fi fi success "Workspace skills updated" fi diff --git a/src/api/routes/skill-hub-routes.ts b/src/api/routes/skill-hub-routes.ts index dd5fbf0b..5ba00bd5 100644 --- a/src/api/routes/skill-hub-routes.ts +++ b/src/api/routes/skill-hub-routes.ts @@ -56,7 +56,11 @@ export async function handleSkillHubRoutes( return true; } - const skillDir = path.join(bot.config.claude.defaultWorkingDirectory, '.claude', 'skills', skillName); + const skillDir = [ + path.join(bot.config.claude.defaultWorkingDirectory, '.claude', 'skills', skillName), + path.join(bot.config.claude.defaultWorkingDirectory, '.codex', 'skills', skillName), + ].find((candidate) => fs.existsSync(path.join(candidate, 'SKILL.md'))) + ?? path.join(bot.config.claude.defaultWorkingDirectory, '.claude', 'skills', skillName); const skillMdPath = path.join(skillDir, 'SKILL.md'); if (!fs.existsSync(skillMdPath)) { jsonResponse(res, 404, { error: `Skill not found at ${skillMdPath}` }); diff --git a/src/api/skills-installer.ts b/src/api/skills-installer.ts index 06a2ee03..4fc8d458 100644 --- a/src/api/skills-installer.ts +++ b/src/api/skills-installer.ts @@ -29,7 +29,10 @@ export interface InstallSkillsOptions { export function installSkillsToWorkDir(workDir: string, logger: Logger, options?: InstallSkillsOptions): void { const userSkillsDir = path.join(os.homedir(), '.claude', 'skills'); - const destSkillsDir = path.join(workDir, '.claude', 'skills'); + const destSkillDirs = [ + path.join(workDir, '.claude', 'skills'), + path.join(workDir, '.codex', 'skills'), + ]; const skillNames = options?.platform === 'feishu' ? [...COMMON_SKILLS, ...LARK_CLI_SKILLS] @@ -43,10 +46,12 @@ export function installSkillsToWorkDir(workDir: string, logger: Logger, options? continue; } - const dest = path.join(destSkillsDir, skill); - fs.mkdirSync(dest, { recursive: true }); - fs.cpSync(src, dest, { recursive: true }); - logger.info({ skill, src, dest }, 'Skill installed to working directory'); + for (const destSkillsDir of destSkillDirs) { + const dest = path.join(destSkillsDir, skill); + fs.mkdirSync(dest, { recursive: true }); + fs.cpSync(src, dest, { recursive: true }); + logger.info({ skill, src, dest }, 'Skill installed to working directory'); + } } // For Feishu bots, ensure lark-cli is configured @@ -54,23 +59,7 @@ export function installSkillsToWorkDir(workDir: string, logger: Logger, options? ensureLarkCliConfig(options.feishuAppId, options.feishuAppSecret, logger); } - // Deploy workspace CLAUDE.md if not already present - const destClaudeMd = path.join(workDir, 'CLAUDE.md'); - if (!fs.existsSync(destClaudeMd)) { - const thisFile = url.fileURLToPath(import.meta.url); - const thisDir = path.dirname(thisFile); - // Try src/workspace/CLAUDE.md (tsx) or dist/workspace/CLAUDE.md (compiled) - for (const candidate of [ - path.join(thisDir, '..', 'workspace', 'CLAUDE.md'), - path.join(thisDir, '..', '..', 'src', 'workspace', 'CLAUDE.md'), - ]) { - if (fs.existsSync(candidate)) { - fs.copyFileSync(candidate, destClaudeMd); - logger.info({ dest: destClaudeMd }, 'CLAUDE.md deployed to working directory'); - break; - } - } - } + deployWorkspaceInstructions(workDir, logger); } /** @@ -114,19 +103,44 @@ export function installSkillFromHub( referencesTar: Buffer | undefined, logger: Logger, ): void { - const destDir = path.join(workDir, '.claude', 'skills', skillName); - fs.mkdirSync(destDir, { recursive: true }); - fs.writeFileSync(path.join(destDir, 'SKILL.md'), skillMd, 'utf-8'); - - if (referencesTar && referencesTar.length > 0) { - try { - execSync(`tar xf - -C "${destDir}"`, { input: referencesTar, stdio: ['pipe', 'pipe', 'pipe'], timeout: 30_000 }); - } catch (err: any) { - logger.warn({ err: err.message, skillName }, 'Failed to extract references tar'); + const destDirs = [ + path.join(workDir, '.claude', 'skills', skillName), + path.join(workDir, '.codex', 'skills', skillName), + ]; + + for (const destDir of destDirs) { + fs.mkdirSync(destDir, { recursive: true }); + fs.writeFileSync(path.join(destDir, 'SKILL.md'), skillMd, 'utf-8'); + + if (referencesTar && referencesTar.length > 0) { + try { + execSync(`tar xf - -C "${destDir}"`, { input: referencesTar, stdio: ['pipe', 'pipe', 'pipe'], timeout: 30_000 }); + } catch (err: any) { + logger.warn({ err: err.message, skillName, destDir }, 'Failed to extract references tar'); + } } + + logger.info({ skillName, dest: destDir }, 'Skill installed from Hub'); } +} - logger.info({ skillName, dest: destDir }, 'Skill installed from Hub'); +function deployWorkspaceInstructions(workDir: string, logger: Logger): void { + const thisFile = url.fileURLToPath(import.meta.url); + const thisDir = path.dirname(thisFile); + for (const candidate of [ + path.join(thisDir, '..', 'workspace', 'CLAUDE.md'), + path.join(thisDir, '..', '..', 'src', 'workspace', 'CLAUDE.md'), + ]) { + if (!fs.existsSync(candidate)) continue; + + for (const fileName of ['CLAUDE.md', 'AGENTS.md']) { + const dest = path.join(workDir, fileName); + if (fs.existsSync(dest)) continue; + fs.copyFileSync(candidate, dest); + logger.info({ dest }, `${fileName} deployed to working directory`); + } + break; + } } /** Locate the lark-cli executable. */ diff --git a/src/bridge/message-bridge.ts b/src/bridge/message-bridge.ts index a164c906..e7dcf8c1 100644 --- a/src/bridge/message-bridge.ts +++ b/src/bridge/message-bridge.ts @@ -591,22 +591,24 @@ export class MessageBridge { const session = this.sessionManager.getSession(chatId); const cwd = session.workingDirectory; const abortController = new AbortController(); + const activeEngine = session.engine ?? resolveEngineName(this.config); + const enginePromptText = normalizePromptForEngine(text, activeEngine); // Prepare downloads directory (bot-isolated) const downloadsDir = this.config.claude.downloadsDir; fs.mkdirSync(downloadsDir, { recursive: true }); // Handle image download if present - let prompt = text; + let prompt = enginePromptText; let imagePath: string | undefined; let filePath: string | undefined; if (imageKey) { imagePath = path.join(downloadsDir, `${imageKey}.png`); const ok = await this.sender.downloadImage(msgId, imageKey, imagePath); if (ok) { - prompt = `${text}\n\n[Image saved at: ${imagePath}]\nPlease use the Read tool to read and analyze this image file.`; + prompt = `${enginePromptText}\n\n[Image saved at: ${imagePath}]\nPlease use the Read tool to read and analyze this image file.`; } else { - prompt = `${text}\n\n(Note: Failed to download the image)`; + prompt = `${enginePromptText}\n\n(Note: Failed to download the image)`; } } @@ -615,9 +617,9 @@ export class MessageBridge { filePath = path.join(downloadsDir, `${fileKey}_${fileName}`); const ok = await this.sender.downloadFile(msgId, fileKey, filePath); if (ok) { - prompt = `${text}\n\n[File saved at: ${filePath}]\nPlease use the Read tool (for text/code files, images, PDFs) or Bash tool (for other formats) to read and analyze this file.`; + prompt = `${enginePromptText}\n\n[File saved at: ${filePath}]\nPlease use the Read tool (for text/code files, images, PDFs) or Bash tool (for other formats) to read and analyze this file.`; } else { - prompt = `${text}\n\n(Note: Failed to download the file)`; + prompt = `${enginePromptText}\n\n(Note: Failed to download the file)`; } } @@ -1517,8 +1519,16 @@ export function isStaleSessionError(errorMessage?: string): boolean { return /no conversation found|conversation not found|session id|invalid session|multiple.*tool_result.*blocks|each tool_use must have a single result/i.test(errorMessage); } +export function normalizePromptForEngine(text: string, engine: EngineName): string { + if (engine !== 'codex') return text; + const match = text.match(/^\/([A-Za-z0-9][A-Za-z0-9_-]*)([\s\S]*)$/); + if (!match) return text; + const suffix = match[2] ?? ''; + if (suffix && !/^\s/.test(suffix)) return text; + return `$${match[1]}${suffix}`; +} + export function isContextOverflowError(errorMessage?: string): boolean { if (!errorMessage) return false; return /context.window.exceeds.limit|context.length.exceeded|context.too.long|max.context.length|token.limit.exceeded|maximum.context/i.test(errorMessage); } - diff --git a/src/engines/codex/executor.ts b/src/engines/codex/executor.ts index ded879a5..cbe0a396 100644 --- a/src/engines/codex/executor.ts +++ b/src/engines/codex/executor.ts @@ -144,7 +144,7 @@ export function buildCodexArgs( args.push('--dangerously-bypass-approvals-and-sandbox'); } else { args.push('-a', codexConfig.approvalPolicy ?? 'never'); - args.push('--sandbox', codexConfig.sandbox ?? 'workspace-write'); + args.push('--sandbox', codexConfig.sandbox ?? 'danger-full-access'); } args.push('-C', cwd); diff --git a/src/skills/metaskill/SKILL.md b/src/skills/metaskill/SKILL.md index d0341dcd..6e1180de 100644 --- a/src/skills/metaskill/SKILL.md +++ b/src/skills/metaskill/SKILL.md @@ -1,6 +1,6 @@ --- name: metaskill -description: "The meta-skill: create AI agent teams, individual agents, or custom skills for any project. Use when the user wants to generate a complete .claude/ agent team, create a single agent, or create a single skill." +description: "The meta-skill: create AI agent teams, individual agents, or custom skills for any project. Use when the user wants to generate a complete agent team, create a single agent, or create a single skill for Claude Code, Kimi, or Codex." user-invocable: true disable-model-invocation: false context: fork @@ -17,7 +17,7 @@ You are an elite AI agent architect. You can create complete agent teams, indivi Working directory: !`pwd` Existing subdirectories: !`ls -d */ 2>/dev/null | head -20 || echo "empty directory"` -Skill base: !`for d in "$HOME/.claude/skills/metaskill" ".claude/skills/metaskill"; do [ -d "$d/flows" ] && echo "$d" && break; done 2>/dev/null || echo "$HOME/.claude/skills/metaskill"` +Skill base: !`for d in ".codex/skills/metaskill" ".claude/skills/metaskill" "$HOME/.codex/skills/metaskill" "$HOME/.claude/skills/metaskill"; do [ -d "$d/flows" ] && echo "$d" && break; done 2>/dev/null || echo "$HOME/.codex/skills/metaskill"` ## Step 1: Detect Intent diff --git a/src/skills/metaskill/flows/agent.md b/src/skills/metaskill/flows/agent.md index 8b6464e2..c7e8a010 100644 --- a/src/skills/metaskill/flows/agent.md +++ b/src/skills/metaskill/flows/agent.md @@ -1,6 +1,6 @@ # Flow: Create Single Agent -You are an elite AI agent architect specializing in crafting high-performance Claude Code subagent configurations. Your task is to create a well-designed agent based on the user's request. +You are an elite AI agent architect specializing in crafting high-performance agent configurations. Your task is to create a well-designed agent based on the user's request. ## Process @@ -19,6 +19,7 @@ Also check for existing agents to avoid conflicts: ``` Glob(".claude/agents/*.md") Glob("~/.claude/agents/*.md") +Read("AGENTS.md") — if it exists ``` ### Step 2: Determine Scope @@ -129,4 +130,4 @@ After writing the file, confirm the file path and briefly explain how to use the ### Engine Compatibility Note -`.claude/agents/*.md` files are discovered only by the **Claude engine**. Under the **Kimi engine**, subagent definitions in this directory are not loaded — Kimi only ships with the builtin `default`/`okabe` subagents. If the target bot uses `engine: "kimi"` in `bots.json`, this agent will have no effect; the main session must handle its role inline. Tell the user this when the bot is Kimi-backed. +`.claude/agents/*.md` files are discovered only by the **Claude engine**. Under the **Kimi** and **Codex** engines, subagent definitions in this directory are not loaded. For Codex compatibility, also add or update an `AGENTS.md` section that describes when the main Codex session should assume this role inline. Tell the user that the standalone agent file only takes effect under Claude, while `AGENTS.md` carries the guidance for Codex/Kimi. diff --git a/src/skills/metaskill/flows/skill.md b/src/skills/metaskill/flows/skill.md index e958b889..5e06e7c3 100644 --- a/src/skills/metaskill/flows/skill.md +++ b/src/skills/metaskill/flows/skill.md @@ -1,6 +1,6 @@ # Flow: Create Single Skill -You are a Claude Code skill designer. Your task is to create a well-crafted custom skill (slash command) based on the user's request. +You are an AI agent skill designer. Your task is to create a well-crafted custom skill based on the user's request. The skill should work in Claude Code/Kimi (`.claude/skills`) and Codex (`.codex/skills`) when possible. ## Process @@ -19,6 +19,8 @@ Also check for existing skills to avoid naming conflicts: ``` Glob(".claude/skills/*/SKILL.md") Glob("~/.claude/skills/*/SKILL.md") +Glob(".codex/skills/*/SKILL.md") +Glob("~/.codex/skills/*/SKILL.md") ``` ### Step 2: Determine Scope and Behavior @@ -26,8 +28,8 @@ Glob("~/.claude/skills/*/SKILL.md") Ask the user (if not clear from their request): 1. **Where to save:** - - **Project-level** (`.claude/skills//SKILL.md`) — specific to this project - - **User-level** (`~/.claude/skills//SKILL.md`) — available across all projects + - **Project-level** (`.claude/skills//SKILL.md` and `.codex/skills//SKILL.md`) — specific to this project, compatible with Claude/Kimi/Codex + - **User-level** (`~/.claude/skills//SKILL.md` and `~/.codex/skills//SKILL.md`) — available across all projects for both engines 2. **Invocation model:** - **User-invocable + auto-invocable** (default) — appears in `/` menu AND Claude can auto-trigger it @@ -105,13 +107,17 @@ Provide a structured report with severity levels. ### Step 6: Write the File -Create the directory and write the SKILL.md file to the chosen path: +Create the directory and write the SKILL.md file to the chosen paths: ``` -~/.claude/skills//SKILL.md (user-level) -.claude/skills//SKILL.md (project-level) +~/.claude/skills//SKILL.md (user-level, Claude/Kimi) +~/.codex/skills//SKILL.md (user-level, Codex) +.claude/skills//SKILL.md (project-level, Claude/Kimi) +.codex/skills//SKILL.md (project-level, Codex) ``` +For Codex, keep `name` and `description` accurate; Codex uses those fields for discovery and may ignore Claude-specific fields like `allowed-tools`, `context`, or `user-invocable`. Prefer portable instructions in the Markdown body over engine-specific frontmatter. + ### Key Principles - Keep skills **focused** — one skill, one purpose. diff --git a/src/skills/metaskill/flows/team.md b/src/skills/metaskill/flows/team.md index 0cc0cc32..05e7e1cc 100644 --- a/src/skills/metaskill/flows/team.md +++ b/src/skills/metaskill/flows/team.md @@ -17,7 +17,8 @@ If the user's request is empty or unclear, use `AskUserQuestion` to ask the user **All files are created inside this project folder:** ``` / -├── CLAUDE.md # Orchestration hub (YOU are the tech lead) +├── CLAUDE.md # Claude/Kimi orchestration hub (YOU are the tech lead) +├── AGENTS.md # Codex orchestration hub (same content or symlink) ├── .mcp.json # MCP server config └── .claude/ ├── agents/ @@ -38,24 +39,26 @@ cd git init ``` -### Engine Compatibility (Claude ↔ Kimi) +### Engine Compatibility (Claude ↔ Kimi ↔ Codex) -MetaBot supports two engines: **Claude Code** and **Kimi**. The scaffolded project should be usable by either engine. Key differences you must account for: +MetaBot supports three engines: **Claude Code**, **Kimi**, and **Codex**. The scaffolded project should be usable by any engine. Key differences you must account for: -| Feature | Claude | Kimi | -|---------|--------|------| -| Orchestration doc | `CLAUDE.md` | `AGENTS.md` (symlink `CLAUDE.md` → `AGENTS.md`) | -| `.claude/skills/` | ✅ auto-discovered | ✅ auto-discovered (brand fallback) | -| `.claude/agents/*.md` | ✅ auto-discovered | ❌ not loaded (Kimi has only builtin `default`/`okabe`) | -| MCP config | `.mcp.json` (project-level) | `~/.kimi/mcp.json` (user-level) | +| Feature | Claude | Kimi | Codex | +|---------|--------|------|-------| +| Orchestration doc | `CLAUDE.md` | `AGENTS.md` (symlink or copy from `CLAUDE.md`) | `AGENTS.md` | +| Skills | `.claude/skills/` | `.claude/skills/` | `.codex/skills/` | +| `.claude/agents/*.md` | ✅ auto-discovered | ❌ not loaded (builtin agents only) | ❌ not loaded | +| MCP config | `.mcp.json` (project-level) | `~/.kimi/mcp.json` (user-level) | Codex config / MCP setup | **What to do at the end of Phase 2 (before verification):** ```bash -# Create AGENTS.md symlink so Kimi reads the same CLAUDE.md +# Create AGENTS.md symlink so Kimi/Codex read the same orchestration guide [ -f CLAUDE.md ] && [ ! -e AGENTS.md ] && ln -s CLAUDE.md AGENTS.md +# Mirror project skills for Codex +if [ -d .claude/skills ]; then mkdir -p .codex && cp -R .claude/skills .codex/skills; fi ``` -Note in the final summary that subagents under `.claude/agents/` only take effect under the Claude engine. Users who run this team on a Kimi-backed bot should expect the orchestrator (CLAUDE.md/AGENTS.md) to do the work inline rather than delegating. +Note in the final summary that subagents under `.claude/agents/` only take effect under the Claude engine. Users who run this team on a Kimi- or Codex-backed bot should expect the orchestrator (`AGENTS.md`) to do the work inline rather than delegating to project subagents. All subsequent paths in Phase 2-4 are **relative to this project folder**. You MUST `cd` into the project folder before creating any files. @@ -127,9 +130,9 @@ After all searches, write a structured summary (in your thinking, not as a file) Based on your research findings combined with the embedded patterns below, create all files **inside the project folder** created in Step 0. Make sure you are `cd`'d into the project folder before writing any files. Create them in this order. -### File 1: `/CLAUDE.md` +### File 1: `/CLAUDE.md` and `/AGENTS.md` -Write a comprehensive `CLAUDE.md` that serves as the orchestration hub. Structure: +Write a comprehensive `CLAUDE.md` that serves as the orchestration hub. Then create `AGENTS.md` as a symlink or copy of the same content so Codex reads it. Structure: ```markdown # CLAUDE.md diff --git a/src/workspace/CLAUDE.md b/src/workspace/CLAUDE.md index 5c53163e..5c52e26b 100644 --- a/src/workspace/CLAUDE.md +++ b/src/workspace/CLAUDE.md @@ -1,6 +1,6 @@ # MetaBot Workspace -This workspace is managed by **MetaBot** — an AI assistant accessible via Feishu/Telegram that runs the Claude Code or Kimi agent engine with full tool access. The bot's engine is configured per-bot in `bots.json` (`engine: "claude" | "kimi"`). +This workspace is managed by **MetaBot** — an AI assistant accessible via Feishu/Telegram that runs the Claude Code, Kimi, or Codex agent engine with full tool access. The bot's engine is configured per-bot in `bots.json` (`engine: "claude" | "kimi" | "codex"`). ## Available Skills @@ -8,9 +8,9 @@ This workspace is managed by **MetaBot** — an AI assistant accessible via Feis Create AI agent teams, individual agents, or custom skills for any project. ``` -/metaskill ios app → generates full .claude/ agent team +/metaskill ios app → generates a portable agent team /metaskill a security agent → creates a single agent -/metaskill a deploy skill → creates a custom slash command +/metaskill a deploy skill → creates a custom skill ``` ### /metamemory — Shared Knowledge Store @@ -50,7 +50,7 @@ lark-cli calendar +agenda --as user # View calendar lark-cli base records list ... # Query bitable ``` -19 AI Agent Skills are installed (lark-doc, lark-im, lark-calendar, lark-sheets, lark-base, lark-task, lark-drive, lark-mail, lark-wiki, etc.) providing structured guidance for each domain. +19 AI Agent Skills are installed (lark-doc, lark-im, lark-calendar, lark-sheets, lark-base, lark-task, lark-drive, lark-mail, lark-wiki, etc.) providing structured guidance for each domain. Claude/Kimi discover these under `.claude/skills`; Codex discovers the mirrored copies under `.codex/skills`. ## Guidelines diff --git a/tests/codex-build-args.test.ts b/tests/codex-build-args.test.ts index aeb35d55..30334de6 100644 --- a/tests/codex-build-args.test.ts +++ b/tests/codex-build-args.test.ts @@ -9,11 +9,11 @@ describe('buildCodexArgs', () => { const cwd = '/work/proj'; const prompt = 'run pwd'; - it('defaults approval policy to "never" and sandbox to "workspace-write"', () => { + it('defaults approval policy to "never" and sandbox to "danger-full-access"', () => { const args = buildCodexArgs({}, cwd, prompt, undefined, undefined); expect(args).toEqual([ '-a', 'never', - '--sandbox', 'workspace-write', + '--sandbox', 'danger-full-access', '-C', cwd, 'exec', '--json', '--color', 'never', '--skip-git-repo-check', prompt, ]); diff --git a/tests/message-bridge.test.ts b/tests/message-bridge.test.ts index 757dee9f..5dacc0ef 100644 --- a/tests/message-bridge.test.ts +++ b/tests/message-bridge.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { isStaleSessionError } from '../src/bridge/message-bridge.js'; +import { isStaleSessionError, normalizePromptForEngine } from '../src/bridge/message-bridge.js'; describe('isStaleSessionError', () => { it('matches the GitHub issue error text', () => { @@ -31,3 +31,17 @@ describe('isStaleSessionError', () => { expect(isStaleSessionError(undefined)).toBe(false); }); }); + +describe('normalizePromptForEngine', () => { + it('converts slash skill invocations to Codex explicit skill syntax', () => { + expect(normalizePromptForEngine('/metaskill ios app', 'codex')).toBe('$metaskill ios app'); + expect(normalizePromptForEngine('/skill-name', 'codex')).toBe('$skill-name'); + }); + + it('leaves non-Codex and non-skill prompts unchanged', () => { + expect(normalizePromptForEngine('/metaskill ios app', 'claude')).toBe('/metaskill ios app'); + expect(normalizePromptForEngine('/metaskill ios app', 'kimi')).toBe('/metaskill ios app'); + expect(normalizePromptForEngine('hello /metaskill', 'codex')).toBe('hello /metaskill'); + expect(normalizePromptForEngine('/bad/path', 'codex')).toBe('/bad/path'); + }); +}); diff --git a/tests/skills-installer.test.ts b/tests/skills-installer.test.ts new file mode 100644 index 00000000..1dded704 --- /dev/null +++ b/tests/skills-installer.test.ts @@ -0,0 +1,55 @@ +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { mkdirSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { afterEach, describe, expect, it } from 'vitest'; +import { installSkillFromHub, installSkillsToWorkDir } from '../src/api/skills-installer.js'; + +const logger = { + debug: () => {}, + info: () => {}, + warn: () => {}, +} as any; + +let cleanupDirs: string[] = []; + +afterEach(() => { + for (const dir of cleanupDirs) rmSync(dir, { recursive: true, force: true }); + cleanupDirs = []; +}); + +function tempDir(prefix: string): string { + const dir = mkdtempSync(join(tmpdir(), prefix)); + cleanupDirs.push(dir); + return dir; +} + +describe('skills installer', () => { + it('installs hub skills to both Claude and Codex project skill directories', () => { + const workDir = tempDir('metabot-work-'); + installSkillFromHub(workDir, 'demo-skill', '---\nname: demo-skill\ndescription: Demo\n---\n', undefined, logger); + + expect(readFileSync(join(workDir, '.claude/skills/demo-skill/SKILL.md'), 'utf-8')).toContain('demo-skill'); + expect(readFileSync(join(workDir, '.codex/skills/demo-skill/SKILL.md'), 'utf-8')).toContain('demo-skill'); + }); + + it('mirrors user skills into Claude and Codex project directories and deploys AGENTS.md', () => { + const priorHome = process.env.HOME; + const home = tempDir('metabot-home-'); + const workDir = tempDir('metabot-work-'); + try { + process.env.HOME = home; + mkdirSync(join(home, '.claude/skills/metaskill'), { recursive: true }); + writeFileSync(join(home, '.claude/skills/metaskill/SKILL.md'), '---\nname: metaskill\ndescription: Meta\n---\n'); + + installSkillsToWorkDir(workDir, logger); + + expect(readFileSync(join(workDir, '.claude/skills/metaskill/SKILL.md'), 'utf-8')).toContain('metaskill'); + expect(readFileSync(join(workDir, '.codex/skills/metaskill/SKILL.md'), 'utf-8')).toContain('metaskill'); + expect(readFileSync(join(workDir, 'AGENTS.md'), 'utf-8')).toContain('MetaBot Workspace'); + } finally { + if (priorHome === undefined) delete process.env.HOME; + else process.env.HOME = priorHome; + } + }); +}); From 22cb7112cf6e436270176166e3e0373d02047ec4 Mon Sep 17 00:00:00 2001 From: Flood Sung Date: Fri, 1 May 2026 07:33:24 +0000 Subject: [PATCH 4/7] fix(codex): tolerate agents deployment failures --- src/api/skills-installer.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/api/skills-installer.ts b/src/api/skills-installer.ts index 4fc8d458..020dd32e 100644 --- a/src/api/skills-installer.ts +++ b/src/api/skills-installer.ts @@ -127,22 +127,29 @@ export function installSkillFromHub( function deployWorkspaceInstructions(workDir: string, logger: Logger): void { const thisFile = url.fileURLToPath(import.meta.url); const thisDir = path.dirname(thisFile); + const existingClaudeMd = path.join(workDir, 'CLAUDE.md'); for (const candidate of [ path.join(thisDir, '..', 'workspace', 'CLAUDE.md'), path.join(thisDir, '..', '..', 'src', 'workspace', 'CLAUDE.md'), ]) { if (!fs.existsSync(candidate)) continue; - for (const fileName of ['CLAUDE.md', 'AGENTS.md']) { - const dest = path.join(workDir, fileName); - if (fs.existsSync(dest)) continue; - fs.copyFileSync(candidate, dest); - logger.info({ dest }, `${fileName} deployed to working directory`); - } + copyInstructionFile(candidate, existingClaudeMd, 'CLAUDE.md', logger); + copyInstructionFile(fs.existsSync(existingClaudeMd) ? existingClaudeMd : candidate, path.join(workDir, 'AGENTS.md'), 'AGENTS.md', logger); break; } } +function copyInstructionFile(src: string, dest: string, fileName: string, logger: Logger): void { + if (fs.existsSync(dest)) return; + try { + fs.copyFileSync(src, dest); + logger.info({ dest }, `${fileName} deployed to working directory`); + } catch (err: any) { + logger.warn({ err: err.message, src, dest }, `Failed to deploy ${fileName}`); + } +} + /** Locate the lark-cli executable. */ function findLarkCli(): string | null { const candidates = [ From 37661547407920ac9621f8f6aff82cfe7554db9e Mon Sep 17 00:00:00 2001 From: Flood Sung Date: Fri, 1 May 2026 07:35:38 +0000 Subject: [PATCH 5/7] fix(codex): install bundled skills when user cache is empty --- src/api/skills-installer.ts | 34 ++++++++++++++++++++++++++++++++-- tests/skills-installer.test.ts | 7 +++---- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/api/skills-installer.ts b/src/api/skills-installer.ts index 020dd32e..291b6282 100644 --- a/src/api/skills-installer.ts +++ b/src/api/skills-installer.ts @@ -39,9 +39,11 @@ export function installSkillsToWorkDir(workDir: string, logger: Logger, options? : COMMON_SKILLS; for (const skill of skillNames) { - const src = path.join(userSkillsDir, skill); + const src = fs.existsSync(path.join(userSkillsDir, skill)) + ? path.join(userSkillsDir, skill) + : bundledSkillSource(skill); - if (!fs.existsSync(src)) { + if (!src || !fs.existsSync(src)) { logger.debug({ skill }, 'Skill source not found, skipping'); continue; } @@ -140,6 +142,34 @@ function deployWorkspaceInstructions(workDir: string, logger: Logger): void { } } +function bundledSkillSource(skill: string): string | undefined { + const thisFile = url.fileURLToPath(import.meta.url); + const thisDir = path.dirname(thisFile); + const candidatesBySkill: Record = { + metaskill: [ + path.join(thisDir, '..', 'skills', 'metaskill'), + path.join(thisDir, '..', '..', 'src', 'skills', 'metaskill'), + ], + metamemory: [ + path.join(thisDir, '..', 'memory', 'skill'), + path.join(thisDir, '..', '..', 'src', 'memory', 'skill'), + ], + metabot: [ + path.join(thisDir, '..', 'skills', 'metabot'), + path.join(thisDir, '..', '..', 'src', 'skills', 'metabot'), + ], + voice: [ + path.join(thisDir, '..', 'skills', 'voice'), + path.join(thisDir, '..', '..', 'src', 'skills', 'voice'), + ], + 'skill-hub': [ + path.join(thisDir, '..', 'skills', 'skill-hub'), + path.join(thisDir, '..', '..', 'src', 'skills', 'skill-hub'), + ], + }; + return candidatesBySkill[skill]?.find((candidate) => fs.existsSync(candidate)); +} + function copyInstructionFile(src: string, dest: string, fileName: string, logger: Logger): void { if (fs.existsSync(dest)) return; try { diff --git a/tests/skills-installer.test.ts b/tests/skills-installer.test.ts index 1dded704..fbd1ac9d 100644 --- a/tests/skills-installer.test.ts +++ b/tests/skills-installer.test.ts @@ -1,4 +1,4 @@ -import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { mkdtempSync, readFileSync, rmSync } from 'node:fs'; import { mkdirSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -33,14 +33,13 @@ describe('skills installer', () => { expect(readFileSync(join(workDir, '.codex/skills/demo-skill/SKILL.md'), 'utf-8')).toContain('demo-skill'); }); - it('mirrors user skills into Claude and Codex project directories and deploys AGENTS.md', () => { + it('mirrors bundled skills into Claude and Codex project directories and deploys AGENTS.md', () => { const priorHome = process.env.HOME; const home = tempDir('metabot-home-'); const workDir = tempDir('metabot-work-'); try { process.env.HOME = home; - mkdirSync(join(home, '.claude/skills/metaskill'), { recursive: true }); - writeFileSync(join(home, '.claude/skills/metaskill/SKILL.md'), '---\nname: metaskill\ndescription: Meta\n---\n'); + mkdirSync(join(home, '.claude/skills'), { recursive: true }); installSkillsToWorkDir(workDir, logger); From c1ec430c511ab6d4e2a1007b64d0d42fd26987f3 Mon Sep 17 00:00:00 2001 From: Flood Sung Date: Fri, 1 May 2026 07:41:31 +0000 Subject: [PATCH 6/7] docs: explain Codex skill migration --- README.md | 33 ++++++++++++++++++++++++++++----- README_EN.md | 33 ++++++++++++++++++++++++++++----- 2 files changed, 56 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 00da9b6c..e6e63ebb 100644 --- a/README.md +++ b/README.md @@ -55,9 +55,9 @@ MetaBot 不是只绑定一家 — 三大顶级 AI 编码 Agent 都内置原生 | **订阅直连** | ✅ `claude login` OAuth | ✅ `kimi login` | ✅ `codex login`,走 ChatGPT 订阅 | | **API Key 兜底** | ✅ `ANTHROPIC_API_KEY` / 第三方 Anthropic 兼容端 | ✅ Moonshot API Key | ✅ `OPENAI_API_KEY` / Codex profile | | **上下文窗口** | 200k(Opus/Sonnet 可选 1M) | 256k(kimi-for-coding) | 400k(gpt-5.x-codex) | -| **工具能力** | Read/Write/Edit/Bash/Glob/Grep/WebSearch/MCP | 同上(Kimi CLI 原生 + `.claude/skills/` 自动发现) | Codex CLI 原生 sandbox + shell 工具链 | -| **自主运行模式** | `bypassPermissions` | `yoloMode`(等价) | `--dangerously-bypass-approvals-and-sandbox` | -| **子 Agent** | `.claude/agents/*.md` 自动加载 | 仅内置 `default` / `okabe` | 暂不支持子 Agent | +| **工具能力** | Read/Write/Edit/Bash/Glob/Grep/WebSearch/MCP | 同上(Kimi CLI 原生 + `.claude/skills/` 自动发现) | Codex CLI 原生工具链 + `.codex/skills/` 自动发现 | +| **自主运行模式** | `bypassPermissions` | `yoloMode`(等价) | 默认 `--sandbox danger-full-access`,避免无 user namespace 环境下的 `bwrap` 失败 | +| **子 Agent** | `.claude/agents/*.md` 自动加载 | 仅内置 `default` / `okabe` | 暂不支持项目子 Agent;把角色/路由写进 `AGENTS.md` | | **工作区说明** | `CLAUDE.md` | `AGENTS.md`(安装器自动建软链) | `AGENTS.md`(Codex 官方约定) | **配置只需一行** — 每个 Bot 独立选引擎: @@ -67,7 +67,29 @@ MetaBot 不是只绑定一家 — 三大顶级 AI 编码 Agent 都内置原生 { "name": "vegeta", "engine": "codex", "codex": { "model": "gpt-5.4-codex" } } ``` -Codex 支持通过本机 `codex exec --json` CLI 接入,并使用 `codex exec resume` 续接聊天会话。启动 MetaBot 前,请先执行 `codex login` 或配置好 Codex API key/profile。 +Codex 支持通过本机 `codex exec --json` CLI 接入,并使用 `codex exec resume` 续接聊天会话。启动 MetaBot 前,请先执行 `codex login` 或配置好 Codex API key/profile。MetaBot 会把 `/metaskill ...` 等飞书 slash skill 调用转成 Codex 的 `$metaskill ...` 显式技能调用。 + +### Codex 迁移:复用 `.claude` 配置 + +Claude/Kimi 和 Codex 的发现路径不同。MetaBot 安装、更新和 Skill Hub 安装时会自动镜像内置 skills: + +| 内容 | Claude / Kimi | Codex | +|------|---------------|-------| +| 工作区说明 | `CLAUDE.md` | `AGENTS.md` | +| Skills | `.claude/skills//SKILL.md` | `.codex/skills//SKILL.md` | +| 子 Agent | `.claude/agents/*.md` | 不自动加载;迁移为 `AGENTS.md` 里的角色/路由说明 | + +已有项目可以直接让 Codex 帮你迁移: + +```text +/model codex +请根据当前项目的 .claude 配置,为 Codex 创建对应的 .codex/skills 和 AGENTS.md: +- 把 .claude/skills/* 镜像到 .codex/skills/* +- 根据 CLAUDE.md 生成或更新 AGENTS.md +- 如果存在 .claude/agents/*.md,把这些 subagent 的角色、路由表和工作流整合进 AGENTS.md +``` + +如果你的宿主机禁用了 unprivileged user namespace,Codex CLI 的 `workspace-write` sandbox 可能在命令执行前报 `bwrap: No permissions to create a new namespace`。MetaBot 的 Codex 默认改用 `danger-full-access` 避开这个问题;需要更强隔离时可以通过 `CODEX_SANDBOX` 或 `codex.sandbox` 显式覆盖。 前端 Bot 用 Claude、后端 Bot 用 Kimi?完全可以。Agent 总线让它们互相委派任务,对面跑什么引擎对调用方透明。 @@ -153,7 +175,7 @@ MetaBot 支持 4 种方式与你的 Agent 团队交互: | 组件 | 一句话说明 | |------|-----------| | **三引擎内核** | 每个 Bot 独立选 Claude Code / Kimi Code / Codex CLI — 完整工具链(Read/Write/Edit/Bash/Glob/Grep/WebSearch/MCP),自主模式运行 | -| **MetaSkill** | Agent 工厂。`/metaskill` 一键生成 `.claude/` Agent 团队(orchestrator + 专家 + reviewer) | +| **MetaSkill** | Agent 工厂。`/metaskill` 一键生成可迁移的 Agent 团队(`CLAUDE.md` / `AGENTS.md` + skills) | | **MetaMemory** | 内嵌 SQLite 知识库,全文搜索,Web UI,变更自动同步到飞书知识库 | | **IM Bridge** | 飞书、Telegram、微信(含手机端)对话任意 Agent,流式卡片 + 工具调用追踪 | | **Agent 总线** | Agent 通过 `mb talk` 互相对话,运行时创建/删除 Bot | @@ -382,6 +404,7 @@ MetaBot 以 `bypassPermissions` 模式运行 Claude Code — 无交互式确认 | `/help` | 帮助 | > **模型切换**:每个会话可独立设置模型。在模型名后加 `[1m]` 可启用 1M 上下文窗口(仅 Opus 4.7/4.6、Sonnet 4.6 支持),例如 `/model claude-opus-4-7[1m]`。OAuth/Pro-Max 登录用户 SDK 会丢弃 beta flag,`[1m]` 后缀是唯一可靠的 1M 开启方式。 +> **Codex Skill 调用**:飞书里仍然可以发 `/metaskill ...`。当当前会话是 Codex 引擎时,MetaBot 会自动转换为 Codex 识别的 `$metaskill ...`。
API 参考 diff --git a/README_EN.md b/README_EN.md index aca8f322..1fc44578 100644 --- a/README_EN.md +++ b/README_EN.md @@ -55,9 +55,9 @@ MetaBot isn't locked to one vendor — all three top AI coding agents ship with | **Subscription login** | ✅ `claude login` OAuth | ✅ `kimi login` | ✅ `codex login` — uses your ChatGPT subscription | | **API key fallback** | ✅ `ANTHROPIC_API_KEY` or third-party Anthropic-compat endpoints | ✅ Moonshot API key | ✅ `OPENAI_API_KEY` / Codex profile | | **Context window** | 200k (1M optional on Opus/Sonnet) | 256k (kimi-for-coding) | 400k (gpt-5.x-codex) | -| **Tools** | Read/Write/Edit/Bash/Glob/Grep/WebSearch/MCP | Same (Kimi CLI builtin + `.claude/skills/` auto-discovery) | Codex CLI native sandbox + shell toolchain | -| **Autonomous mode** | `bypassPermissions` | `yoloMode` (equivalent) | `--dangerously-bypass-approvals-and-sandbox` | -| **Subagents** | `.claude/agents/*.md` auto-loaded | Builtin `default` / `okabe` only | Subagents not supported yet | +| **Tools** | Read/Write/Edit/Bash/Glob/Grep/WebSearch/MCP | Same (Kimi CLI builtin + `.claude/skills/` auto-discovery) | Codex CLI native toolchain + `.codex/skills/` auto-discovery | +| **Autonomous mode** | `bypassPermissions` | `yoloMode` (equivalent) | Defaults to `--sandbox danger-full-access` to avoid `bwrap` failures on hosts without user namespaces | +| **Subagents** | `.claude/agents/*.md` auto-loaded | Builtin `default` / `okabe` only | Project subagents are not auto-loaded; put role routing in `AGENTS.md` | | **Workspace doc** | `CLAUDE.md` | `AGENTS.md` (installer creates the symlink) | `AGENTS.md` (Codex convention) | **One line of config** — each bot picks its engine: @@ -67,7 +67,29 @@ MetaBot isn't locked to one vendor — all three top AI coding agents ship with { "name": "vegeta", "engine": "codex", "codex": { "model": "gpt-5.4-codex" } } ``` -Codex support uses the local `codex exec --json` CLI and resumes chat sessions with `codex exec resume`. Authenticate once with `codex login` (or configure your Codex API key/profile) before starting MetaBot. +Codex support uses the local `codex exec --json` CLI and resumes chat sessions with `codex exec resume`. Authenticate once with `codex login` (or configure your Codex API key/profile) before starting MetaBot. MetaBot translates Feishu slash-skill invocations like `/metaskill ...` into Codex's explicit `$metaskill ...` skill syntax. + +### Codex Migration: Reuse `.claude` Config + +Claude/Kimi and Codex use different discovery paths. MetaBot mirrors bundled skills during install/update and Skill Hub installs: + +| Content | Claude / Kimi | Codex | +|---------|---------------|-------| +| Workspace instructions | `CLAUDE.md` | `AGENTS.md` | +| Skills | `.claude/skills//SKILL.md` | `.codex/skills//SKILL.md` | +| Subagents | `.claude/agents/*.md` | Not auto-loaded; migrate roles/routes into `AGENTS.md` | + +For an existing project, ask Codex to migrate it: + +```text +/model codex +Use the current project's .claude config to create Codex-compatible .codex/skills and AGENTS.md: +- mirror .claude/skills/* into .codex/skills/* +- generate or update AGENTS.md from CLAUDE.md +- if .claude/agents/*.md exists, merge those subagent roles, routing tables, and workflows into AGENTS.md +``` + +If the host disables unprivileged user namespaces, Codex CLI's `workspace-write` sandbox can fail before commands run with `bwrap: No permissions to create a new namespace`. MetaBot defaults Codex to `danger-full-access` to avoid that failure; set `CODEX_SANDBOX` or `codex.sandbox` explicitly if you want stricter isolation. Run your frontend bot on Claude and your backend bot on Kimi? Totally fine. The Agent Bus lets them delegate to each other — the calling bot doesn't need to know which engine is on the other side. @@ -149,7 +171,7 @@ Full-featured browser-based chat interface. Access at `https://your-server/web/` | Component | Description | |-----------|-------------| | **Triple Engine Kernel** | Each bot independently chooses Claude Code / Kimi Code / Codex CLI — full tool stack (Read/Write/Edit/Bash/Glob/Grep/WebSearch/MCP) in autonomous mode | -| **MetaSkill** | Agent factory. `/metaskill` generates a complete `.claude/` agent team (orchestrator + specialists + reviewer) | +| **MetaSkill** | Agent factory. `/metaskill` generates portable agent teams (`CLAUDE.md` / `AGENTS.md` + skills) | | **MetaMemory** | Embedded SQLite knowledge store with full-text search, Web UI, auto-syncs to Feishu Wiki | | **IM Bridge** | Chat with any agent from Feishu, Telegram, or WeChat (including mobile). Streaming cards + tool call tracking | | **Agent Bus** | Agents talk to each other via `mb talk`. Create/remove bots at runtime | @@ -383,6 +405,7 @@ MetaBot runs Claude Code in `bypassPermissions` mode — no interactive approval | `/help` | Show help | > **Model switching**: Each session can pick its own model. Append `[1m]` to the model name to enable the 1M context window (only Opus 4.7/4.6 and Sonnet 4.6 support it), e.g. `/model claude-opus-4-7[1m]`. OAuth/Pro-Max users must use this suffix — the SDK silently drops beta headers under that auth mode. +> **Codex skills**: In Feishu you can still send `/metaskill ...`. When the session is on Codex, MetaBot converts it to Codex's `$metaskill ...` form.
API Reference From 9e70579a37b5983522997d71171b18f9e9207a39 Mon Sep 17 00:00:00 2001 From: Flood Sung Date: Thu, 14 May 2026 09:06:33 +0000 Subject: [PATCH 7/7] docs: document /goal loops and Agent Teams - README_EN.md / README.md: add Core Components rows for Persistent Sessions & Goal Loops and Agent Teams, new Example Prompts sections, and /goal row in the Chat Commands table. - docs/features/goal-loops.{md,zh.md}: new feature page covering Claude-native Stop hook + fast-model evaluator and MetaBot's persistent-executor role. - docs/features/agent-teams.{md,zh.md}: new feature page describing the runtime team experience; honest about current Team-panel limitations (SDK hooks not firing yet, activity surfaces in the agent activity card). - docs/usage/chat-commands.{md,zh.md}: add /goal row with link to the new feature page. - mkdocs.yml: add both pages to the en and zh nav blocks under Features, adjacent to MetaSkill. --- README.md | 18 +++++++++++ README_EN.md | 18 +++++++++++ docs/features/agent-teams.md | 52 ++++++++++++++++++++++++++++++ docs/features/agent-teams.zh.md | 51 ++++++++++++++++++++++++++++++ docs/features/goal-loops.md | 56 +++++++++++++++++++++++++++++++++ docs/features/goal-loops.zh.md | 56 +++++++++++++++++++++++++++++++++ docs/usage/chat-commands.md | 1 + docs/usage/chat-commands.zh.md | 1 + mkdocs.yml | 4 +++ 9 files changed, 257 insertions(+) create mode 100644 docs/features/agent-teams.md create mode 100644 docs/features/agent-teams.zh.md create mode 100644 docs/features/goal-loops.md create mode 100644 docs/features/goal-loops.zh.md diff --git a/README.md b/README.md index e6e63ebb..70854e80 100644 --- a/README.md +++ b/README.md @@ -175,6 +175,8 @@ MetaBot 支持 4 种方式与你的 Agent 团队交互: | 组件 | 一句话说明 | |------|-----------| | **三引擎内核** | 每个 Bot 独立选 Claude Code / Kimi Code / Codex CLI — 完整工具链(Read/Write/Edit/Bash/Glob/Grep/WebSearch/MCP),自主模式运行 | +| **常驻会话与目标循环** | 每个会话一个常驻 Claude 进程 — `/goal` 让 Agent 在多轮之间持续自驱直到目标达成;团队成员和后台任务跨轮存活 | +| **Agent 团队(运行时)** | 主导 Agent 并行派遣专家队友,互相路由任务、汇总结果 —— 全部在一个飞书会话中完成 | | **MetaSkill** | Agent 工厂。`/metaskill` 一键生成可迁移的 Agent 团队(`CLAUDE.md` / `AGENTS.md` + skills) | | **MetaMemory** | 内嵌 SQLite 知识库,全文搜索,Web UI,变更自动同步到飞书知识库 | | **IM Bridge** | 飞书、Telegram、微信(含手机端)对话任意 Agent,流式卡片 + 工具调用追踪 | @@ -230,6 +232,21 @@ MetaBot 支持 4 种方式与你的 Agent 团队交互: 我需要一个前端专家、一个后端 API 专家、一个 code reviewer。 ``` +### Agent 团队 — 运行时协作 + +``` +你来当主导工程师。并行派出一个前端专家和一个后端专家: +前端负责 React UI 改造,后端加上新的 /api/reports 接口, +你负责 review 两边的 PR,全部通过后再合并。 +``` + +### 目标循环 + +``` +/goal PR #123 的 CI 全绿、部署成功。 +每 10 分钟检查一次,搞定后告诉我。 +``` + ### 定时任务 ``` @@ -396,6 +413,7 @@ MetaBot 以 `bypassPermissions` 模式运行 Claude Code — 无交互式确认 | `/reset` | 清除会话 | | `/stop` | 中止当前任务 | | `/status` | 查看会话状态(含当前模型) | +| `/goal <条件>` | 设置目标,Agent 跨多轮持续推进直到达成。`/goal clear` 停止 | | `/model` | 查看当前模型;`/model list` 查看可用模型;`/model ` 切换;`/model reset` 恢复默认 | | `/memory list` | 浏览知识库目录 | | `/memory search 关键词` | 搜索知识库 | diff --git a/README_EN.md b/README_EN.md index 1fc44578..a4bbf5d1 100644 --- a/README_EN.md +++ b/README_EN.md @@ -171,6 +171,8 @@ Full-featured browser-based chat interface. Access at `https://your-server/web/` | Component | Description | |-----------|-------------| | **Triple Engine Kernel** | Each bot independently chooses Claude Code / Kimi Code / Codex CLI — full tool stack (Read/Write/Edit/Bash/Glob/Grep/WebSearch/MCP) in autonomous mode | +| **Persistent Sessions & Goal Loops** | One Claude process per chat — `/goal` keeps the agent auto-driving across turns until a condition is met; teammates and background tasks survive between turns | +| **Agent Teams** | A lead agent spawns specialist teammates in parallel, routes tasks between them, and aggregates results — all in one Feishu chat | | **MetaSkill** | Agent factory. `/metaskill` generates portable agent teams (`CLAUDE.md` / `AGENTS.md` + skills) | | **MetaMemory** | Embedded SQLite knowledge store with full-text search, Web UI, auto-syncs to Feishu Wiki | | **IM Bridge** | Chat with any agent from Feishu, Telegram, or WeChat (including mobile). Streaming cards + tool call tracking | @@ -227,6 +229,21 @@ Search MetaMemory for our API design conventions. I need a frontend specialist, a backend API specialist, and a code reviewer. ``` +### Agent Teams — Runtime + +``` +Act as a lead engineer. Spawn a frontend specialist and a backend specialist +in parallel: the frontend handles the React UI changes, the backend adds the +new /api/reports endpoint, and you review both PRs before merging. +``` + +### Goal Loops + +``` +/goal The CI for PR #123 is green and the deploy completes successfully. +Check every 10 minutes and report back when done. +``` + ### Scheduling ``` @@ -397,6 +414,7 @@ MetaBot runs Claude Code in `bypassPermissions` mode — no interactive approval | `/reset` | Clear session | | `/stop` | Abort current task | | `/status` | Session info (includes current model) | +| `/goal ` | Set a goal the agent keeps pursuing across turns. `/goal clear` to stop | | `/model` | Show current engine/model; `/model list` — available engines/models; `/model claude`, `/model kimi`, or `/model codex` — switch engine; `/model ` — set model; `/model reset` — restore default | | `/memory list` | Browse knowledge tree | | `/memory search ` | Search knowledge base | diff --git a/docs/features/agent-teams.md b/docs/features/agent-teams.md new file mode 100644 index 00000000..e7817df9 --- /dev/null +++ b/docs/features/agent-teams.md @@ -0,0 +1,52 @@ +# Agent Teams + +A lead agent spawns specialist teammates in parallel, routes tasks between them, and aggregates results — all inside a single Feishu chat. + +## What It Does + +Agent Teams is the **runtime** team experience inside a single chat session: + +- A **lead agent** receives your request and decides what specialists are needed. +- The lead spawns **teammates** (frontend, backend, reviewer, …) via the `Agent` tool. Teammates run as separate sub-agents under the same chat process. +- Teammates can be addressed across turns: the lead routes tasks via `SendMessage`, asks teammates to report back, and aggregates the results. +- Everything happens in one Feishu chat — you talk to the lead, the lead talks to the team. + +This is the runtime counterpart to [MetaSkill](metaskill.md): MetaSkill *generates* an agent team configuration (CLAUDE.md / AGENTS.md + skills), while Agent Teams *runs* it. + +## Usage + +Prompt the lead agent to spawn teammates. You don't need a special command — just describe the team and the work: + +``` +Act as a lead engineer. Spawn a frontend specialist and a backend specialist +in parallel: the frontend handles the React UI changes, the backend adds the +new /api/reports endpoint, and you review both PRs before merging. +``` + +``` +Spawn a researcher and a writer teammate. The researcher gathers everything +we have on competitor X's pricing strategy from MetaMemory and the web. +The writer turns it into a one-pager. Hand off when done. +``` + +If your bot already has a generated team (via `/metaskill`), the orchestrator agent in that team is your lead — just describe the goal. + +## How It Works + +- **Persistent process per chat.** Teammates spawned in turn 1 are still addressable in turn N hours later, because each chat has one long-lived Claude process (see [How it works in CLAUDE.md](https://github.com/xvirobotics/metabot/blob/main/CLAUDE.md#persistent-claude-process-per-chat-stage-4--opt-in)). Without this, every turn would spawn a fresh subprocess and tear down all teammates. +- **Agent tool spawns teammates.** The lead uses Claude's native `Agent` tool with a `team_name=` parameter to start a teammate. Teammates inherit the same working directory and tools. +- **Cross-agent messaging.** Teammates and the lead use `SendMessage` to exchange messages. Replies are queued and delivered when the recipient is ready. +- **Background activity surfacing.** Teammate progress between user turns shows up as a coalesced "Agent activity" card in Feishu (30-second debounce, so you don't get spammed during fast back-and-forth). + +## Current Limitations + +- **Team panel UX is coming soon.** A dedicated `🧑‍🤝‍🧑 Team` panel showing each teammate with a working/idle status icon and a shared task list is implemented in the card renderer, but the upstream SDK hooks that populate it (`TaskCreated` / `TaskCompleted` / `TeammateIdle`) do not fire reliably yet. Today, teammate activity surfaces in the existing **agent activity card** during the run. +- **Claude engine only.** Teammate spawning relies on Claude's native `Agent` tool. Kimi and Codex bots don't support Agent Teams yet. +- **One lead per chat.** The chat session has one process, so one lead agent. Use separate Feishu chats (or [peers](peers.md)) to run multiple independent teams. +- **Budget is shared.** All teammates run inside the same chat's token budget. Heavy parallel work counts against `maxBudgetUsd`. + +## See Also + +- [MetaSkill](metaskill.md) — generate a team configuration before running it +- [Goal Loops](goal-loops.md) — give the team a multi-turn objective +- [Peers](peers.md) — run teams on separate MetaBot instances and route between them diff --git a/docs/features/agent-teams.zh.md b/docs/features/agent-teams.zh.md new file mode 100644 index 00000000..0d8ac033 --- /dev/null +++ b/docs/features/agent-teams.zh.md @@ -0,0 +1,51 @@ +# Agent 团队 + +主导 Agent 并行派遣专家队友,互相路由任务、汇总结果 —— 全部在一个飞书会话中完成。 + +## 功能 + +Agent 团队是单个会话内的**运行时**团队体验: + +- **主导 Agent**接到你的需求,决定需要哪些专家。 +- 主导 Agent 通过 `Agent` 工具派遣**队友**(前端、后端、Reviewer 等)。队友以子 Agent 形式运行在同一个会话进程下。 +- 队友跨多轮可寻址:主导 Agent 用 `SendMessage` 路由任务、请队友汇报,并汇总结果。 +- 所有交互都在一个飞书会话中 —— 你和主导 Agent 对话,主导 Agent 协调团队。 + +这是 [MetaSkill](metaskill.md) 的运行时对应:MetaSkill *生成* Agent 团队的配置(CLAUDE.md / AGENTS.md + skills),Agent 团队负责*运行*它。 + +## 用法 + +让主导 Agent 派出队友即可,不需要特殊命令 —— 把团队和工作描述出来: + +``` +你来当主导工程师。并行派出一个前端专家和一个后端专家: +前端负责 React UI 改造,后端加上新的 /api/reports 接口, +你负责 review 两边的 PR,全部通过后再合并。 +``` + +``` +派出一个研究员和一个写手队友。研究员从 MetaMemory 和网络 +收集竞品 X 的定价策略;写手整理成一页纸。完成后交付。 +``` + +如果你的 Bot 已经通过 `/metaskill` 生成了团队,里面的 orchestrator 就是主导 Agent —— 直接描述目标即可。 + +## 工作原理 + +- **每个会话一个常驻进程。**第 1 轮派出的队友,几小时后第 N 轮仍可寻址。原因是每个会话有一个长生命周期的 Claude 进程(见 [CLAUDE.md 中的说明](https://github.com/xvirobotics/metabot/blob/main/CLAUDE.md#persistent-claude-process-per-chat-stage-4--opt-in))。如果没有这个机制,每轮都重新启动子进程,所有队友会被销毁。 +- **Agent 工具派遣队友。**主导 Agent 用 Claude 原生的 `Agent` 工具加 `team_name=` 参数派出队友。队友继承同样的工作目录和工具集。 +- **跨 Agent 消息。**队友和主导 Agent 通过 `SendMessage` 交换消息。回复会排队,接收方就绪后投递。 +- **后台活动展示。**用户轮之间的队友进度,会在飞书以合并后的"Agent activity"卡片呈现(30 秒去抖,避免快速交互时刷屏)。 + +## 当前限制 + +- **专属团队面板即将上线。**专门的 `🧑‍🤝‍🧑 Team` 面板(队友列表带忙/闲图标 + 共享任务列表)渲染逻辑已经写好,但上游 SDK 的 hooks(`TaskCreated` / `TaskCompleted` / `TeammateIdle`)目前还不稳定触发。当前队友活动展示在运行期的**Agent activity 卡片**中。 +- **仅 Claude 引擎。**队友派遣依赖 Claude 原生 `Agent` 工具,Kimi 和 Codex Bot 暂不支持 Agent 团队。 +- **每个会话一个主导 Agent。**单会话单进程,只能有一个主导 Agent。需要并行运行多个独立团队时,用不同的飞书会话(或 [peers](peers.md))。 +- **预算共享。**所有队友共享同一个会话的 token 预算,重并行任务会快速计入 `maxBudgetUsd`。 + +## 相关 + +- [MetaSkill](metaskill.md) — 先用它生成团队配置,再运行 +- [目标循环](goal-loops.md) — 给团队一个跨多轮的目标 +- [Peers 联邦](peers.md) — 把团队跑在不同 MetaBot 实例上,跨实例路由 diff --git a/docs/features/goal-loops.md b/docs/features/goal-loops.md new file mode 100644 index 00000000..d0ec07a7 --- /dev/null +++ b/docs/features/goal-loops.md @@ -0,0 +1,56 @@ +# Goal Loops + +Set a goal condition; MetaBot keeps Claude working across turns until the goal is met. + +## What It Does + +`/goal` lets you hand Claude an objective rather than a single instruction. The agent keeps pursuing the goal across **multiple turns** — checking, retrying, waiting on external state — and reports back when the condition is satisfied (or when you tell it to stop). + +The Feishu card shows a persistent `🎯 Goal: ` badge across turns, so you always know what the agent is chasing. + +## Usage + +Send `/goal` followed by the condition you want satisfied: + +``` +/goal The CI for PR #123 is green and the deploy completes successfully. +Check every 10 minutes and report back when done. +``` + +``` +/goal All open Linear tickets in the INGEST project are either resolved +or assigned to a human. Recheck every 30 minutes. +``` + +Other forms: + +| Command | Effect | +|---------|--------| +| `/goal ` | Set or replace the active goal | +| `/goal` | Query the current goal (no mutation) | +| `/goal clear` (or `stop` / `off` / `reset` / `none` / `cancel`) | Clear the active goal | + +## How It Works + +`/goal` is a **Claude Code native command** — the loop machinery lives inside Claude Code itself: + +1. Claude registers a session-scoped **Stop hook** when the goal is set. +2. When a turn finishes, the Stop hook runs a fast-model evaluator against the goal condition. +3. If the goal is **not yet met**, the evaluator queues another turn automatically. If it **is** met, the loop ends and Claude reports the result. + +MetaBot's contribution is the runtime that makes this work over Feishu: + +- The **persistent Claude process per chat** (one long-lived SDK session per `chatId`) is what keeps the Stop hook alive between user turns. Without it, every turn spawned a fresh subprocess and killed any in-flight hooks. This runs by default — no configuration needed. +- The Feishu card mirrors the goal condition into a persistent badge so the user can see what's being pursued. + +## Limits + +- Auto-driven turns count toward the bot's token budget (`maxBudgetUsd`) and turn limit (`maxTurns`) just like manual turns. +- One active goal per chat session. Setting a new goal replaces the previous one. +- Use `/stop` to abort the current turn; use `/goal clear` to stop the loop entirely. +- Goals are scoped to a single chat (`chatId`); they don't persist after `/reset`. + +## See Also + +- [Agent Teams](agent-teams.md) — combine a goal with parallel teammates +- [Chat Commands](../usage/chat-commands.md) — full command reference diff --git a/docs/features/goal-loops.zh.md b/docs/features/goal-loops.zh.md new file mode 100644 index 00000000..75dc86e9 --- /dev/null +++ b/docs/features/goal-loops.zh.md @@ -0,0 +1,56 @@ +# 目标循环(Goal Loops) + +设一个目标,MetaBot 让 Claude 跨多轮持续推进,直到目标达成。 + +## 功能 + +`/goal` 让你把"目标"交给 Claude,而不是单条指令。Agent 会**跨多轮**自动驱动:检查、重试、等待外部状态,达成条件时回来汇报(或者你主动叫停)。 + +飞书卡片会在多轮之间持续显示 `🎯 Goal: ` 徽标,你随时知道 Agent 在追什么。 + +## 用法 + +发 `/goal` 加目标条件: + +``` +/goal PR #123 的 CI 全绿、部署成功。 +每 10 分钟检查一次,搞定后告诉我。 +``` + +``` +/goal Linear INGEST 项目下所有 open ticket 都已解决或分派给人。 +每 30 分钟复查一次。 +``` + +其他形式: + +| 命令 | 效果 | +|------|------| +| `/goal <条件>` | 设置或替换当前目标 | +| `/goal` | 查询当前目标(不修改) | +| `/goal clear`(或 `stop` / `off` / `reset` / `none` / `cancel`) | 清除当前目标 | + +## 工作原理 + +`/goal` 是 **Claude Code 原生命令**,循环机制本身跑在 Claude Code 内部: + +1. 设置目标时,Claude 注册一个会话级 **Stop hook**。 +2. 每轮结束时,Stop hook 调用快速模型评估目标是否达成。 +3. **未达成**则自动排队下一轮;**已达成**则结束循环并汇报结果。 + +MetaBot 的贡献是让这套机制能跨飞书消息工作: + +- **每个会话一个常驻 Claude 进程**(一个长生命周期的 SDK 会话 per `chatId`),这是 Stop hook 跨用户轮存活的前提。没有它的话,每轮都重新启动子进程,hook 会被杀掉。该机制默认开启,无需配置。 +- 飞书卡片把目标条件镜像成持久徽标,让用户看到 Agent 在追什么。 + +## 限制 + +- 自动驱动的轮次和手动轮次一样,计入 Bot 的 token 预算(`maxBudgetUsd`)和轮次上限(`maxTurns`)。 +- 每个会话只能有一个活跃目标。再次设置会替换原目标。 +- `/stop` 中止当前轮次;`/goal clear` 完全停止循环。 +- 目标作用域是单个会话(`chatId`),`/reset` 后不保留。 + +## 相关 + +- [Agent 团队](agent-teams.md) — 把目标和并行队友组合使用 +- [聊天命令](../usage/chat-commands.md) — 完整命令参考 diff --git a/docs/usage/chat-commands.md b/docs/usage/chat-commands.md index 74ed7cd1..8a1899d5 100644 --- a/docs/usage/chat-commands.md +++ b/docs/usage/chat-commands.md @@ -9,6 +9,7 @@ Commands you can send to MetaBot in Feishu or Telegram. | `/reset` | Clear session — starts a fresh conversation | | `/stop` | Abort the currently running task | | `/status` | Show session info (session ID, working directory) | +| `/goal ` | Set a goal the agent keeps pursuing across turns. `/goal clear` to stop. See [Goal Loops](../features/goal-loops.md) | | `/memory list` | Browse MetaMemory knowledge tree | | `/memory search ` | Search MetaMemory knowledge base | | `/sync` | Trigger MetaMemory → Feishu Wiki sync | diff --git a/docs/usage/chat-commands.zh.md b/docs/usage/chat-commands.zh.md index 33f0deb5..2f8c9412 100644 --- a/docs/usage/chat-commands.zh.md +++ b/docs/usage/chat-commands.zh.md @@ -9,6 +9,7 @@ | `/reset` | 清除会话 — 开始全新对话 | | `/stop` | 中止当前任务 | | `/status` | 查看会话信息(会话 ID、工作目录) | +| `/goal <条件>` | 设置目标,Agent 跨多轮持续推进直到达成。`/goal clear` 停止。参见 [目标循环](../features/goal-loops.md) | | `/memory list` | 浏览 MetaMemory 知识库目录 | | `/memory search 关键词` | 搜索 MetaMemory 知识库 | | `/sync` | 触发 MetaMemory → 飞书知识库同步 | diff --git a/mkdocs.yml b/mkdocs.yml index 5f3c6d63..0d9b55e6 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -73,6 +73,8 @@ plugins: - 安全: concepts/security.md - 功能: - MetaSkill: features/metaskill.md + - 目标循环: features/goal-loops.md + - Agent 团队: features/agent-teams.md - MetaMemory: features/metamemory.md - Web UI: features/web-ui.md - Peers 联邦: features/peers.md @@ -125,6 +127,8 @@ nav: - Security: concepts/security.md - Features: - MetaSkill: features/metaskill.md + - Goal Loops: features/goal-loops.md + - Agent Teams: features/agent-teams.md - MetaMemory: features/metamemory.md - Web UI: features/web-ui.md - Peers Federation: features/peers.md