diff --git a/bin/omarchy-cn-ai-launch b/bin/omarchy-cn-ai-launch new file mode 100755 index 00000000..91a02a6a --- /dev/null +++ b/bin/omarchy-cn-ai-launch @@ -0,0 +1,51 @@ +#!/bin/bash +# omarchy:summary=Launch the AI harness configured by a profile +# omarchy:args=[profile] +# omarchy:examples=omarchycn ai launch | omarchycn ai launch work + +set -euo pipefail + +source "$OMARCHY_PATH/cn/lib/ai.sh" + +profile="${1:-$(cn_ai_current_profile)}" + +if [[ ! -f $CN_AI_PROFILE_DIR/$profile.toml ]]; then + echo "No such profile: $profile" >&2 + exit 1 +fi + +harness=$(cn_ai_profile_field "$profile" harness) +provider=$(cn_ai_profile_field "$profile" provider) +model=$(cn_ai_profile_field "$profile" model) +command=$(cn_ai_harness_field "$harness" command) +config_method=$(cn_ai_harness_field "$harness" config_method) + +if ! command -v "$command" > /dev/null 2>&1; then + install=$(cn_ai_harness_field "$harness" install) + if [[ $install == doc:* ]]; then + echo "$harness 未安装,安装方法见官方文档: ${install#doc:}" >&2 + exit 1 + fi + echo "Installing $harness: $install" + $install +fi + +case "$config_method" in +env) + key=$(omarchy-cn-ai-secret get "$provider") + if [[ $harness == "codex" ]]; then + cn_ai_render_codex_config "$provider" "$model" "$key" + exec "$command" + fi + eval "$(cn_ai_render_env "$harness" "$provider" "$model" "$key")" + exec "$command" + ;; +native) + # Native harnesses (kimi-code, deep-code) manage their own auth + exec "$command" + ;; +*) + echo "Unknown config method: $config_method" >&2 + exit 1 + ;; +esac diff --git a/bin/omarchy-cn-ai-profile-create b/bin/omarchy-cn-ai-profile-create new file mode 100755 index 00000000..d0bf5f18 --- /dev/null +++ b/bin/omarchy-cn-ai-profile-create @@ -0,0 +1,57 @@ +#!/bin/bash +# omarchy:summary=Create an AI profile binding a harness, provider, and model +# omarchy:args= +# omarchy:examples=omarchycn ai profile create work claude-code deepseek default-coding + +set -euo pipefail + +source "$OMARCHY_PATH/cn/lib/ai.sh" + +name="${1:?usage: omarchycn ai profile create }" +harness="${2:?missing harness}" +provider="${3:?missing provider}" +model="${4:?missing model}" + +if [[ ! $name =~ ^[a-z0-9-]+$ ]]; then + echo "Profile name must be lowercase alphanumeric/dashes: $name" >&2 + exit 1 +fi + +cn_ai_harness_field "$harness" command > /dev/null || { + echo "Unknown harness: $harness (known: $(cn_ai_harness_ids | tr '\n' ' '))" >&2 + exit 1 +} +jq -e --arg p "$provider" '.providers[$p]' "$CN_AI_PROVIDERS" > /dev/null || { + echo "Unknown provider: $provider (known: $(cn_ai_provider_ids | tr '\n' ' '))" >&2 + exit 1 +} + +level=$(cn_ai_combo_level "$harness" "$provider") +if [[ $level == "unsupported" ]]; then + echo "Combo $harness x $provider is not in the compatibility matrix" >&2 + exit 1 +fi + +resolved=$(cn_ai_model_by_alias "$provider" "$model" 2>/dev/null || true) +if [[ -n $resolved ]]; then + model="$resolved" +elif ! cn_ai_models "$provider" | grep -qxF "$model"; then + echo "Unknown model for $provider: $model" >&2 + echo "Available: $(cn_ai_models "$provider" | tr '\n' ' ')" >&2 + exit 1 +fi + +mkdir -p "$CN_AI_PROFILE_DIR" +cat > "$CN_AI_PROFILE_DIR/$name.toml" < /dev/null 2>&1; then + echo "提示: 尚未存储 $provider 的 API Key,运行: omarchycn ai secret set $provider" +fi diff --git a/bin/omarchy-cn-ai-profile-list b/bin/omarchy-cn-ai-profile-list new file mode 100755 index 00000000..d1370f93 --- /dev/null +++ b/bin/omarchy-cn-ai-profile-list @@ -0,0 +1,30 @@ +#!/bin/bash +# omarchy:summary=List AI profiles and the current default +# omarchy:examples=omarchycn ai profile list + +set -euo pipefail + +source "$OMARCHY_PATH/cn/lib/ai.sh" + +current="" +if [[ -f $CN_AI_CURRENT ]]; then + current=$(<"$CN_AI_CURRENT") +fi + +found="false" +for f in "$CN_AI_PROFILE_DIR"/*.toml; do + [[ -f $f ]] || continue + found="true" + name="${f##*/}" + name="${name%.toml}" + marker=" " + [[ $name == "$current" ]] && marker="*" + printf '%s %-14s %s x %s x %s\n' "$marker" "$name" \ + "$(cn_ai_profile_field "$name" harness)" \ + "$(cn_ai_profile_field "$name" provider)" \ + "$(cn_ai_profile_field "$name" model)" +done + +if [[ $found == "false" ]]; then + echo "No profiles (create: omarchycn ai profile create )" +fi diff --git a/bin/omarchy-cn-ai-profile-use b/bin/omarchy-cn-ai-profile-use new file mode 100755 index 00000000..94c84201 --- /dev/null +++ b/bin/omarchy-cn-ai-profile-use @@ -0,0 +1,19 @@ +#!/bin/bash +# omarchy:summary=Set the default AI profile +# omarchy:args= +# omarchy:examples=omarchycn ai profile use work + +set -euo pipefail + +source "$OMARCHY_PATH/cn/lib/ai.sh" + +name="${1:?usage: omarchycn ai profile use }" + +if [[ ! -f $CN_AI_PROFILE_DIR/$name.toml ]]; then + echo "No such profile: $name (see: omarchycn ai profile list)" >&2 + exit 1 +fi + +mkdir -p "${CN_AI_CURRENT%/*}" +echo "$name" > "$CN_AI_CURRENT" +echo "Default AI profile: $name" diff --git a/cn/lib/ai.sh b/cn/lib/ai.sh new file mode 100644 index 00000000..b89a52e5 --- /dev/null +++ b/cn/lib/ai.sh @@ -0,0 +1,126 @@ +# shellcheck shell=bash +# Sourced helpers for omarchy-cn-ai-* commands + +CN_AI_PROVIDERS="$OMARCHY_PATH/cn/registry/ai-providers.json" +CN_AI_HARNESSES="$OMARCHY_PATH/cn/registry/ai-harnesses.json" +CN_AI_COMPAT="$OMARCHY_PATH/cn/registry/ai-compatibility.json" +CN_AI_PROFILE_DIR="$HOME/.config/omarchycn/ai/profiles" +CN_AI_CURRENT="$HOME/.config/omarchycn/ai/current" + +cn_ai_provider_ids() { jq -r '.providers | keys[]' "$CN_AI_PROVIDERS"; } +cn_ai_harness_ids() { jq -r '.harnesses | keys[]' "$CN_AI_HARNESSES"; } + +cn_ai_endpoint() { + jq -re --arg p "$1" --arg proto "$2" '.providers[$p].endpoints[$proto]' "$CN_AI_PROVIDERS" +} + +cn_ai_models() { + jq -r --arg p "$1" '.providers[$p].models[].id' "$CN_AI_PROVIDERS" +} + +cn_ai_model_by_alias() { + jq -re --arg p "$1" --arg a "$2" \ + '.providers[$p].models[] | select(.aliases // [] | index($a)) | .id' "$CN_AI_PROVIDERS" +} + +cn_ai_fast_model() { + cn_ai_model_by_alias "$1" fast 2>/dev/null || cn_ai_model_by_alias "$1" default-coding +} + +cn_ai_harness_field() { + jq -re --arg h "$1" --arg f "$2" '.harnesses[$h][$f]' "$CN_AI_HARNESSES" +} + +cn_ai_combo_level() { + jq -r --arg h "$1" --arg p "$2" '.combos[$h][$p].level // "unsupported"' "$CN_AI_COMPAT" +} + +# Profile files are flat key="value" lines +cn_ai_profile_field() { + local file="$CN_AI_PROFILE_DIR/$1.toml" key="$2" + grep -E "^$key = " "$file" | head -1 | cut -d\" -f2 +} + +cn_ai_current_profile() { + if [[ ! -f $CN_AI_CURRENT ]]; then + echo "No default AI profile set (run: omarchycn ai profile use )" >&2 + return 1 + fi + cat "$CN_AI_CURRENT" +} + +# Print export statements for the profile's harness x provider combo. +# Secrets are resolved at launch time only; nothing is written to disk. +cn_ai_render_env() { + local harness="$1" provider="$2" model="$3" key="$4" + local base fast + + case "$harness" in + claude-code) + base=$(cn_ai_endpoint "$provider" anthropic) + fast=$(cn_ai_fast_model "$provider") + printf 'export ANTHROPIC_BASE_URL=%q\n' "$base" + printf 'export ANTHROPIC_AUTH_TOKEN=%q\n' "$key" + printf 'export ANTHROPIC_MODEL=%q\n' "$model" + printf 'export ANTHROPIC_DEFAULT_SONNET_MODEL=%q\n' "$model" + printf 'export ANTHROPIC_DEFAULT_OPUS_MODEL=%q\n' "$model" + printf 'export ANTHROPIC_DEFAULT_HAIKU_MODEL=%q\n' "$fast" + ;; + opencode) + if [[ $provider != "deepseek" ]]; then + echo "opencode adapter renders only the deepseek combo (native provider)" >&2 + return 1 + fi + printf 'export DEEPSEEK_API_KEY=%q\n' "$key" + ;; + *) + echo "No env renderer for harness: $harness" >&2 + return 1 + ;; + esac +} + +# Write ~/.codex/config.toml provider block (official DeepSeek codex format) +cn_ai_render_codex_config() { + local provider="$1" model="$2" key="$3" + local base cfg="$HOME/.codex/config.toml" + + if [[ $provider != "deepseek" ]]; then + echo "codex adapter renders only the deepseek combo (official integration)" >&2 + return 1 + fi + base="https://api.deepseek.com/" + + mkdir -p "${cfg%/*}" + touch "$cfg" + if [[ -s $cfg ]]; then + cp "$cfg" "$cfg.omarchycn-bak-$(date +%Y%m%d-%H%M%S)" + fi + + # Top-level keys must precede the first [section]; provider table goes last. + # Strip our old blocks and any pre-section model keys, keep the rest intact. + awk ' + /^# OmarchyCN ai (model|provider) begin$/ { skip = 1; next } + /^# OmarchyCN ai (model|provider) end$/ { skip = 0; next } + skip { next } + /^\[/ { in_section = 1 } + !in_section && /^(model|model_provider) = / { next } + { print } + ' "$cfg" > "$cfg.omarchycn-tmp" + + { + echo "# OmarchyCN ai model begin" + echo "model = \"$model\"" + echo "model_provider = \"$provider\"" + echo "# OmarchyCN ai model end" + cat "$cfg.omarchycn-tmp" + echo "# OmarchyCN ai provider begin" + echo "[model_providers.$provider]" + echo "name = \"$provider\"" + echo "base_url = \"$base\"" + echo "experimental_bearer_token = \"$key\"" + echo "# OmarchyCN ai provider end" + } > "$cfg" + rm -f "$cfg.omarchycn-tmp" + chmod 600 "$cfg" +}