jekyll-theme-zer0 1.27.0 → 1.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +812 -13
- data/_data/backlog.yml +356 -2
- data/_data/consumers.yml +252 -0
- data/_data/features.yml +234 -5
- data/_data/i18n/fr.yml +103 -0
- data/_data/i18n/manifest.yml +1529 -0
- data/_data/ingredient_densities.yml +122 -0
- data/_data/navigation/docs.yml +2 -0
- data/_data/navigation/main.yml +22 -0
- data/_data/recipe_courses.yml +64 -0
- data/_data/series.yml +19 -0
- data/_data/theme-manifest.yml +387 -277
- data/_data/ui-text.yml +8 -0
- data/_includes/README.md +36 -3
- data/_includes/analytics/google-tag-manager-body.html +11 -2
- data/_includes/analytics/google-tag-manager-head.html +16 -6
- data/_includes/analytics/posthog.html +33 -4
- data/_includes/components/abc-letter.html +43 -0
- data/_includes/components/background-customizer.html +2 -2
- data/_includes/components/book-card.html +42 -0
- data/_includes/components/book-nav.html +80 -0
- data/_includes/components/book-plate.html +31 -0
- data/_includes/components/book-toc.html +47 -0
- data/_includes/components/bookshelf.html +68 -0
- data/_includes/components/card-grid.html +60 -0
- data/_includes/components/cookie-consent.html +4 -4
- data/_includes/components/data-card.html +95 -0
- data/_includes/components/halfmoon.html +5 -1
- data/_includes/components/info-section.html +8 -3
- data/_includes/components/language-toggle.html +168 -21
- data/_includes/components/page-feedback.html +65 -2
- data/_includes/components/page-views-init.html +55 -0
- data/_includes/components/page-views.html +33 -0
- data/_includes/components/recipe-card.html +67 -0
- data/_includes/components/recipe-duration.html +50 -0
- data/_includes/components/recipe-grams.html +58 -0
- data/_includes/components/recipe-index.html +96 -0
- data/_includes/components/recipe-ingredients.html +90 -0
- data/_includes/components/recipe-meta.html +96 -0
- data/_includes/components/recipe-nutrition.html +57 -0
- data/_includes/components/recipe-qty.html +73 -0
- data/_includes/components/recipe-ratio.html +151 -0
- data/_includes/components/recipe-scaler.html +73 -0
- data/_includes/components/recipe-steps.html +86 -0
- data/_includes/components/recipe-temp.html +45 -0
- data/_includes/components/search-modal.html +2 -2
- data/_includes/components/shortcuts-modal.html +3 -0
- data/_includes/components/theme-controls-bar.html +10 -2
- data/_includes/components/theme-customizer.html +8 -2
- data/_includes/components/theme-info.html +8 -1
- data/_includes/content/jsonld-software.html +22 -3
- data/_includes/content/seo.html +9 -3
- data/_includes/core/color-mode-init.html +13 -4
- data/_includes/core/favicon.html +46 -0
- data/_includes/core/footer.html +18 -16
- data/_includes/core/head.html +21 -0
- data/_includes/core/header.html +7 -4
- data/_includes/custom/body-end.html +18 -0
- data/_includes/custom/body-start.html +17 -0
- data/_includes/custom/footer.html +18 -0
- data/_includes/custom/head.html +18 -0
- data/_includes/navigation/local-graph.html +28 -2
- data/_includes/navigation/nav-tree.html +3 -3
- data/_includes/navigation/section-sidebar.html +93 -11
- data/_includes/navigation/sidebar-config.html +21 -0
- data/_includes/navigation/sidebar-left.html +2 -1
- data/_includes/navigation/sidebar-nav.html +4 -0
- data/_includes/navigation/sidebar-pagetree.html +150 -0
- data/_includes/navigation/sidebar-right.html +2 -1
- data/_includes/navigation/unified-drawer.html +8 -2
- data/_includes/obsidian/full-graph.html +165 -136
- data/_includes/setup/wizard.html +173 -86
- data/_includes/stats/stats-metrics.html +2 -2
- data/_layouts/404.html +260 -0
- data/_layouts/README.md +4 -0
- data/_layouts/admin.html +2 -2
- data/_layouts/article.html +5 -0
- data/_layouts/book-abc.html +106 -0
- data/_layouts/book-story.html +91 -0
- data/_layouts/book.html +113 -0
- data/_layouts/collection.html +17 -5
- data/_layouts/cookbook.html +88 -0
- data/_layouts/default.html +4 -4
- data/_layouts/home.html +5 -2
- data/_layouts/landing.html +9 -0
- data/_layouts/news.html +155 -31
- data/_layouts/recipe.html +274 -0
- data/_layouts/root.html +38 -5
- data/_layouts/section.html +96 -25
- data/_sass/components/_book.scss +423 -0
- data/_sass/components/_callout.scss +1 -1
- data/_sass/components/_footer.scss +37 -1
- data/_sass/components/_page-views.scss +36 -0
- data/_sass/components/_recipe.scss +506 -0
- data/_sass/components/_setup-wizard.scss +235 -0
- data/_sass/components/_ui-enhancements.scss +6 -6
- data/_sass/core/_navbar.scss +263 -15
- data/_sass/core/_obsidian.scss +286 -6
- data/_sass/layouts/_landing.scss +2 -2
- data/_sass/theme/_backgrounds.scss +21 -8
- data/_sass/tokens/_color.scss +6 -0
- data/_sass/tokens/_index.scss +2 -0
- data/_sass/tokens/_radius.scss +21 -0
- data/_sass/tokens/_typography.scss +4 -0
- data/_sass/utilities/_focus.scss +14 -0
- data/assets/css/main.scss +4 -0
- data/assets/js/auto-hide-nav.js +5 -1
- data/assets/js/halfmoon.js +26 -0
- data/assets/js/modules/navigation/navbar.js +55 -0
- data/assets/js/obsidian-graph.js +702 -264
- data/assets/js/obsidian-local-graph.js +161 -54
- data/assets/js/page-views.js +372 -0
- data/assets/js/recipe-scaler.js +501 -0
- data/assets/js/search-modal.js +14 -1
- data/assets/js/setup-wizard.js +322 -20
- data/scripts/README.md +29 -0
- data/scripts/bin/audit-consumer +39 -7
- data/scripts/bin/giscus-discussions +213 -14
- data/scripts/bin/manifest +66 -16
- data/scripts/bin/validate +5 -1
- data/scripts/ci/agent_review_result.py +164 -0
- data/scripts/ci/test_agent_review_result.py +172 -0
- data/scripts/design-system-check.rb +170 -0
- data/scripts/install/README.md +47 -6
- data/scripts/install/ai/client.sh +302 -93
- data/scripts/install/ai/prompts/spec.schema.json +1 -1
- data/scripts/install/ai/wizard.sh +10 -5
- data/scripts/install/apply.sh +7 -3
- data/scripts/install/cli.sh +54 -4
- data/scripts/install/config.sh +167 -0
- data/scripts/install/doctor.sh +38 -0
- data/scripts/install/plan.sh +10 -0
- data/scripts/install/spec.sh +15 -7
- data/scripts/install/template.sh +4 -0
- data/scripts/lib/audit.sh +42 -2
- data/scripts/lint-liquid-raw.rb +137 -0
- data/scripts/propagate.rb +277 -0
- data/scripts/test/lib/run_tests.sh +2 -1
- data/scripts/test/lib/test_agent_review_result.sh +27 -0
- data/scripts/translate.rb +161 -15
- metadata +56 -2
|
@@ -1,52 +1,153 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# =============================================================================
|
|
3
|
-
# scripts/install/ai/client.sh —
|
|
3
|
+
# scripts/install/ai/client.sh — Multi-provider AI client
|
|
4
4
|
# =============================================================================
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
# - Enforces 30s timeout
|
|
8
|
-
# - Redacts Authorization header from logs
|
|
9
|
-
# - Returns raw JSON response body to stdout
|
|
5
|
+
# A thin curl/CLI wrapper that speaks to whichever AI provider is available.
|
|
6
|
+
# Three providers are supported, resolved in this order under `auto`:
|
|
10
7
|
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
8
|
+
# 1. claude-cli — the local, OAuth-authenticated `claude` CLI (Claude Code).
|
|
9
|
+
# Zero key handling: reuses the user's Claude Code session
|
|
10
|
+
# (`claude setup-token` / `claude login`). This is the
|
|
11
|
+
# flagship "Claude Code OAuth" integration.
|
|
12
|
+
# 2. anthropic — Anthropic Messages API over HTTPS. Auth resolves from:
|
|
13
|
+
# CLAUDE_CODE_OAUTH_TOKEN → OAuth bearer + beta header
|
|
14
|
+
# ANTHROPIC_API_KEY → x-api-key
|
|
15
|
+
# 3. openai — OpenAI-compatible /chat/completions (OpenAI, Azure, Ollama).
|
|
15
16
|
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
17
|
+
# Public API (stable — wizard.sh / suggest.sh / diagnose.sh depend on it):
|
|
18
|
+
# ai_client_available → 0 if a usable provider exists, else 1
|
|
19
|
+
# ai_client_provider → echo resolved provider slug (or "none")
|
|
20
|
+
# ai_client_model [PROVIDER] → echo model id for a provider ("" = default)
|
|
21
|
+
# ai_client_auth_source → echo human-readable auth description
|
|
22
|
+
# ai_client_chat SYS USER [SCHEMA] → print raw provider JSON to stdout
|
|
23
|
+
# ai_client_extract_text RESP → print assistant text (any provider shape)
|
|
18
24
|
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
25
|
+
# Selection & tuning (all optional, env or config-file driven):
|
|
26
|
+
# ZER0_AI_PROVIDER auto | claude-cli | anthropic | openai | none
|
|
27
|
+
# ZER0_AI_MODEL Override model id for the active provider
|
|
28
|
+
# ZER0_AI_MAX_TOKENS Output cap for the anthropic path (default 2048)
|
|
29
|
+
# ZER0_NO_AI=1 Global kill-switch (disables every provider)
|
|
30
|
+
# OPENAI_API_KEY / OPENAI_BASE_URL / OPENAI_MODEL (openai path)
|
|
31
|
+
# ANTHROPIC_API_KEY / ANTHROPIC_BASE_URL / ANTHROPIC_MODEL (anthropic path)
|
|
32
|
+
# CLAUDE_CODE_OAUTH_TOKEN (anthropic path, OAuth — takes precedence)
|
|
21
33
|
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
# Default: https://api.openai.com/v1
|
|
26
|
-
# OPENAI_MODEL Model name (default: gpt-4o-mini)
|
|
27
|
-
# ZER0_NO_AI Set to 1 to disable all AI calls (kill-switch)
|
|
34
|
+
# Contract: API keys are read from the environment ONLY — never logged, never
|
|
35
|
+
# accepted via flag, never written to disk. All network I/O is single-attempt
|
|
36
|
+
# with a 30s timeout and degrades gracefully (callers check the return code).
|
|
28
37
|
#
|
|
29
|
-
# Bash 3.2 compatible. No set -euo pipefail here.
|
|
38
|
+
# Bash 3.2 compatible. No `declare -A`, no `set -euo pipefail` here.
|
|
30
39
|
# =============================================================================
|
|
31
40
|
[[ -n "${_HAS_AI_CLIENT:-}" ]] && return 0
|
|
32
41
|
_HAS_AI_CLIENT=1
|
|
33
42
|
|
|
34
|
-
|
|
35
|
-
_AI_MODEL="${OPENAI_MODEL:-gpt-4o-mini}"
|
|
36
|
-
_AI_TIMEOUT=30
|
|
43
|
+
_AI_TIMEOUT="${ZER0_AI_TIMEOUT:-30}"
|
|
37
44
|
|
|
38
45
|
# ---------------------------------------------------------------------------
|
|
39
|
-
#
|
|
46
|
+
# Provider detection helpers
|
|
40
47
|
# ---------------------------------------------------------------------------
|
|
48
|
+
_ai_have_claude_cli() { command -v claude >/dev/null 2>&1; }
|
|
49
|
+
|
|
50
|
+
# _ai_provider_usable PROVIDER → 0 if that provider has what it needs.
|
|
51
|
+
_ai_provider_usable() {
|
|
52
|
+
case "$1" in
|
|
53
|
+
claude-cli) _ai_have_claude_cli ;;
|
|
54
|
+
anthropic) [[ -n "${CLAUDE_CODE_OAUTH_TOKEN:-}" || -n "${ANTHROPIC_API_KEY:-}" ]] ;;
|
|
55
|
+
openai) [[ -n "${OPENAI_API_KEY:-}" || -n "${OPENAI_BASE_URL:-}" ]] ;;
|
|
56
|
+
*) return 1 ;;
|
|
57
|
+
esac
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
# ai_client_provider → resolved provider slug. Honors an explicit choice
|
|
61
|
+
# (ZER0_AI_PROVIDER / SPEC_AI_PROVIDER); "auto"/"" picks the best available.
|
|
62
|
+
ai_client_provider() {
|
|
63
|
+
[[ "${ZER0_NO_AI:-0}" == "1" ]] && { echo "none"; return 0; }
|
|
64
|
+
|
|
65
|
+
local want="${ZER0_AI_PROVIDER:-${SPEC_AI_PROVIDER:-auto}}"
|
|
66
|
+
case "$want" in
|
|
67
|
+
claude|claude-cli|claude_cli) echo "claude-cli"; return 0 ;;
|
|
68
|
+
anthropic|claude-api) echo "anthropic"; return 0 ;;
|
|
69
|
+
openai|azure|ollama) echo "openai"; return 0 ;;
|
|
70
|
+
none|off|disabled) echo "none"; return 0 ;;
|
|
71
|
+
auto|"") ;; # fall through to auto-detect
|
|
72
|
+
*) log_warning "Unknown AI provider '$want' — falling back to auto" ;;
|
|
73
|
+
esac
|
|
74
|
+
|
|
75
|
+
if _ai_have_claude_cli; then echo "claude-cli"; return 0; fi
|
|
76
|
+
if [[ -n "${CLAUDE_CODE_OAUTH_TOKEN:-}" || -n "${ANTHROPIC_API_KEY:-}" ]]; then
|
|
77
|
+
echo "anthropic"; return 0
|
|
78
|
+
fi
|
|
79
|
+
if [[ -n "${OPENAI_API_KEY:-}" || -n "${OPENAI_BASE_URL:-}" ]]; then
|
|
80
|
+
echo "openai"; return 0
|
|
81
|
+
fi
|
|
82
|
+
echo "none"
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# ai_client_available → 0 if the resolved provider is actually usable.
|
|
41
86
|
ai_client_available() {
|
|
42
87
|
[[ "${ZER0_NO_AI:-0}" == "1" ]] && return 1
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
return 1
|
|
88
|
+
local p
|
|
89
|
+
p="$(ai_client_provider)"
|
|
90
|
+
[[ "$p" == "none" ]] && return 1
|
|
91
|
+
_ai_provider_usable "$p"
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# ai_client_model [PROVIDER] → model id ("" means "use the provider default").
|
|
95
|
+
ai_client_model() {
|
|
96
|
+
local p="${1:-$(ai_client_provider)}"
|
|
97
|
+
if [[ -n "${ZER0_AI_MODEL:-}" ]]; then echo "$ZER0_AI_MODEL"; return 0; fi
|
|
98
|
+
if [[ -n "${SPEC_AI_MODEL:-}" ]]; then echo "$SPEC_AI_MODEL"; return 0; fi
|
|
99
|
+
case "$p" in
|
|
100
|
+
anthropic) echo "${ANTHROPIC_MODEL:-claude-sonnet-5}" ;;
|
|
101
|
+
claude-cli) echo "${ANTHROPIC_MODEL:-}" ;; # empty → CLI's own default
|
|
102
|
+
*) echo "${OPENAI_MODEL:-gpt-4o-mini}" ;;
|
|
103
|
+
esac
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
# ai_client_auth_source → short description of how the active provider authenticates.
|
|
107
|
+
ai_client_auth_source() {
|
|
108
|
+
case "$(ai_client_provider)" in
|
|
109
|
+
claude-cli) echo "claude CLI session (OAuth)" ;;
|
|
110
|
+
anthropic)
|
|
111
|
+
if [[ -n "${CLAUDE_CODE_OAUTH_TOKEN:-}" ]]; then echo "CLAUDE_CODE_OAUTH_TOKEN (OAuth)"
|
|
112
|
+
else echo "ANTHROPIC_API_KEY"; fi ;;
|
|
113
|
+
openai)
|
|
114
|
+
if [[ -n "${OPENAI_API_KEY:-}" ]]; then echo "OPENAI_API_KEY"
|
|
115
|
+
else echo "OPENAI_BASE_URL (keyless)"; fi ;;
|
|
116
|
+
*) echo "none" ;;
|
|
117
|
+
esac
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
# ---------------------------------------------------------------------------
|
|
121
|
+
# JSON + sanitization helpers
|
|
122
|
+
# ---------------------------------------------------------------------------
|
|
123
|
+
# _ai_json_string STR → STR encoded as a JSON string literal (with quotes).
|
|
124
|
+
_ai_json_string() {
|
|
125
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
126
|
+
printf '%s' "$1" | python3 -c "import json,sys; print(json.dumps(sys.stdin.read()))"
|
|
127
|
+
elif command -v jq >/dev/null 2>&1; then
|
|
128
|
+
printf '%s' "$1" | jq -Rs .
|
|
129
|
+
else
|
|
130
|
+
printf '"%s"' "$(printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g' | tr '\n' '\001' | sed 's/\o001/\\n/g')"
|
|
131
|
+
fi
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
# _ai_sanitize STR → strip likely secrets/PII before a payload leaves the host.
|
|
135
|
+
# Reads from stdin. BSD/GNU-sed compatible. (Contract: sanitize before send.)
|
|
136
|
+
_ai_sanitize() {
|
|
137
|
+
local home_esc
|
|
138
|
+
home_esc="$(printf '%s' "${HOME:-/nonexistent}" | sed 's:/:\\/:g')"
|
|
139
|
+
sed \
|
|
140
|
+
-e "s/${home_esc}/~/g" \
|
|
141
|
+
-e 's/sk-ant-[A-Za-z0-9_-]\{12,\}/[REDACTED_ANTHROPIC_KEY]/g' \
|
|
142
|
+
-e 's/sk-[A-Za-z0-9_-]\{20,\}/[REDACTED_API_KEY]/g' \
|
|
143
|
+
-e 's/ghp_[A-Za-z0-9]\{20,\}/[REDACTED_GITHUB_TOKEN]/g' \
|
|
144
|
+
-e 's/github_pat_[A-Za-z0-9_]\{20,\}/[REDACTED_GITHUB_PAT]/g' \
|
|
145
|
+
-e 's/[A-Za-z0-9._%+-]\{1,\}@[A-Za-z0-9.-]\{1,\}\.[A-Za-z]\{2,\}/[REDACTED_EMAIL]/g'
|
|
46
146
|
}
|
|
47
147
|
|
|
48
148
|
# ---------------------------------------------------------------------------
|
|
49
149
|
# ai_client_chat SYSTEM_PROMPT USER_PROMPT [JSON_SCHEMA]
|
|
150
|
+
# Dispatches to the resolved provider. Prints raw provider JSON to stdout.
|
|
50
151
|
# ---------------------------------------------------------------------------
|
|
51
152
|
ai_client_chat() {
|
|
52
153
|
local system_prompt="$1"
|
|
@@ -54,111 +155,219 @@ ai_client_chat() {
|
|
|
54
155
|
local json_schema="${3:-}"
|
|
55
156
|
|
|
56
157
|
if ! ai_client_available; then
|
|
57
|
-
log_error "ai_client_chat: AI
|
|
158
|
+
log_error "ai_client_chat: no AI provider available (see 'install doctor')"
|
|
58
159
|
return 1
|
|
59
160
|
fi
|
|
60
161
|
|
|
61
|
-
|
|
62
|
-
|
|
162
|
+
# Sanitize the user-supplied context (never the system prompt — it's ours).
|
|
163
|
+
user_prompt="$(printf '%s' "$user_prompt" | _ai_sanitize)"
|
|
164
|
+
|
|
165
|
+
local provider
|
|
166
|
+
provider="$(ai_client_provider)"
|
|
167
|
+
log_debug "ai_client_chat: provider=${provider} model=$(ai_client_model "$provider")"
|
|
168
|
+
|
|
169
|
+
case "$provider" in
|
|
170
|
+
claude-cli) _ai_chat_claude_cli "$system_prompt" "$user_prompt" "$json_schema" ;;
|
|
171
|
+
anthropic) _ai_chat_anthropic "$system_prompt" "$user_prompt" "$json_schema" ;;
|
|
172
|
+
openai) _ai_chat_openai "$system_prompt" "$user_prompt" "$json_schema" ;;
|
|
173
|
+
*) log_error "ai_client_chat: unresolved provider '$provider'"; return 1 ;;
|
|
174
|
+
esac
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
# ---- claude-cli: shell out to the OAuth-authenticated Claude Code CLI --------
|
|
178
|
+
_ai_chat_claude_cli() {
|
|
179
|
+
local system_prompt="$1" user_prompt="$2"
|
|
180
|
+
local model prompt out
|
|
181
|
+
model="$(ai_client_model claude-cli)"
|
|
182
|
+
|
|
183
|
+
# Claude Code owns its own system prompt; append ours and put the request
|
|
184
|
+
# in the user turn. The whole thing is fed on stdin (no prompt on argv, so
|
|
185
|
+
# nothing sensitive lands in the process table).
|
|
186
|
+
prompt="$(printf '%s\n\n%s' "$system_prompt" "$user_prompt")"
|
|
187
|
+
|
|
188
|
+
local mflag
|
|
189
|
+
mflag=()
|
|
190
|
+
[[ -n "$model" ]] && mflag=(--model "$model")
|
|
191
|
+
|
|
192
|
+
if out="$(printf '%s' "$prompt" | claude -p --output-format json \
|
|
193
|
+
${mflag[@]+"${mflag[@]}"} 2>/dev/null)"; then
|
|
194
|
+
printf '%s\n' "$out"
|
|
195
|
+
return 0
|
|
196
|
+
fi
|
|
197
|
+
|
|
198
|
+
# Fallback: plain-text output → wrap so ai_client_extract_text can read it.
|
|
199
|
+
if out="$(printf '%s' "$prompt" | claude -p ${mflag[@]+"${mflag[@]}"} 2>/dev/null)"; then
|
|
200
|
+
printf '{"result":%s}\n' "$(_ai_json_string "$out")"
|
|
201
|
+
return 0
|
|
202
|
+
fi
|
|
203
|
+
|
|
204
|
+
log_error "ai_client_chat: claude CLI call failed (is it logged in? try 'claude login')"
|
|
205
|
+
return 1
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
# ---- anthropic: Anthropic Messages API (OAuth token or API key) --------------
|
|
209
|
+
_ai_chat_anthropic() {
|
|
210
|
+
local system_prompt="$1" user_prompt="$2"
|
|
211
|
+
local base model endpoint
|
|
212
|
+
base="${ANTHROPIC_BASE_URL:-https://api.anthropic.com}"
|
|
213
|
+
model="$(ai_client_model anthropic)"
|
|
214
|
+
endpoint="${base%/}/v1/messages"
|
|
215
|
+
|
|
216
|
+
local auth
|
|
217
|
+
auth=()
|
|
218
|
+
if [[ -n "${CLAUDE_CODE_OAUTH_TOKEN:-}" ]]; then
|
|
219
|
+
# OAuth tokens require the beta header and a Claude Code system identity.
|
|
220
|
+
auth=(-H "authorization: Bearer ${CLAUDE_CODE_OAUTH_TOKEN}" \
|
|
221
|
+
-H "anthropic-beta: oauth-2025-04-20")
|
|
222
|
+
system_prompt="$(printf '%s\n\n%s' \
|
|
223
|
+
"You are Claude Code, Anthropic's official CLI for Claude." "$system_prompt")"
|
|
224
|
+
elif [[ -n "${ANTHROPIC_API_KEY:-}" ]]; then
|
|
225
|
+
auth=(-H "x-api-key: ${ANTHROPIC_API_KEY}")
|
|
226
|
+
else
|
|
227
|
+
log_error "ai_client_chat: anthropic provider needs CLAUDE_CODE_OAUTH_TOKEN or ANTHROPIC_API_KEY"
|
|
228
|
+
return 1
|
|
229
|
+
fi
|
|
230
|
+
|
|
231
|
+
local sys_escaped user_escaped body
|
|
232
|
+
sys_escaped="$(_ai_json_string "$system_prompt")"
|
|
233
|
+
user_escaped="$(_ai_json_string "$user_prompt")"
|
|
234
|
+
body="$(cat <<JSON
|
|
235
|
+
{
|
|
236
|
+
"model": "${model}",
|
|
237
|
+
"max_tokens": ${ZER0_AI_MAX_TOKENS:-2048},
|
|
238
|
+
"system": ${sys_escaped},
|
|
239
|
+
"messages": [ {"role": "user", "content": ${user_escaped}} ]
|
|
240
|
+
}
|
|
241
|
+
JSON
|
|
242
|
+
)"
|
|
243
|
+
|
|
244
|
+
log_debug "ai_client_chat: POST ${endpoint} model=${model} (anthropic)"
|
|
245
|
+
local resp
|
|
246
|
+
resp="$(curl -fsSL --max-time "$_AI_TIMEOUT" \
|
|
247
|
+
-H "content-type: application/json" \
|
|
248
|
+
-H "anthropic-version: 2023-06-01" \
|
|
249
|
+
${auth[@]+"${auth[@]}"} \
|
|
250
|
+
-d "$body" "$endpoint" 2>&1)"
|
|
251
|
+
if [[ $? -ne 0 ]]; then
|
|
252
|
+
log_error "ai_client_chat: anthropic request failed"
|
|
253
|
+
return 1
|
|
254
|
+
fi
|
|
255
|
+
printf '%s\n' "$resp"
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
# ---- openai: OpenAI-compatible /chat/completions (unchanged behavior) --------
|
|
259
|
+
_ai_chat_openai() {
|
|
260
|
+
local system_prompt="$1" user_prompt="$2" json_schema="$3"
|
|
261
|
+
local base model endpoint api_key
|
|
262
|
+
base="${OPENAI_BASE_URL:-https://api.openai.com/v1}"
|
|
263
|
+
model="$(ai_client_model openai)"
|
|
264
|
+
endpoint="${base}/chat/completions"
|
|
265
|
+
api_key="${OPENAI_API_KEY:-}"
|
|
63
266
|
|
|
64
|
-
# Build request body — escape for JSON
|
|
65
267
|
local sys_escaped user_escaped
|
|
66
|
-
sys_escaped
|
|
67
|
-
|
|
68
|
-
user_escaped=$(printf '%s' "$user_prompt" | python3 -c "import json,sys; print(json.dumps(sys.stdin.read()))" 2>/dev/null \
|
|
69
|
-
|| printf '%s' "$user_prompt" | sed 's/\\/\\\\/g; s/"/\\"/g')
|
|
268
|
+
sys_escaped="$(_ai_json_string "$system_prompt")"
|
|
269
|
+
user_escaped="$(_ai_json_string "$user_prompt")"
|
|
70
270
|
|
|
71
271
|
local body
|
|
72
272
|
if [[ -n "$json_schema" ]]; then
|
|
73
|
-
|
|
74
|
-
body=$(cat <<JSON
|
|
273
|
+
body="$(cat <<JSON
|
|
75
274
|
{
|
|
76
|
-
"model": "${
|
|
77
|
-
"response_format": {
|
|
78
|
-
"type": "json_schema",
|
|
79
|
-
"json_schema": ${json_schema}
|
|
80
|
-
},
|
|
275
|
+
"model": "${model}",
|
|
276
|
+
"response_format": { "type": "json_schema", "json_schema": ${json_schema} },
|
|
81
277
|
"messages": [
|
|
82
278
|
{"role": "system", "content": ${sys_escaped}},
|
|
83
279
|
{"role": "user", "content": ${user_escaped}}
|
|
84
280
|
]
|
|
85
281
|
}
|
|
86
282
|
JSON
|
|
87
|
-
)
|
|
283
|
+
)"
|
|
88
284
|
else
|
|
89
|
-
body
|
|
285
|
+
body="$(cat <<JSON
|
|
90
286
|
{
|
|
91
|
-
"model": "${
|
|
287
|
+
"model": "${model}",
|
|
92
288
|
"messages": [
|
|
93
289
|
{"role": "system", "content": ${sys_escaped}},
|
|
94
290
|
{"role": "user", "content": ${user_escaped}}
|
|
95
291
|
]
|
|
96
292
|
}
|
|
97
293
|
JSON
|
|
98
|
-
)
|
|
294
|
+
)"
|
|
99
295
|
fi
|
|
100
296
|
|
|
101
|
-
log_debug "ai_client_chat: POST ${endpoint} model=${
|
|
102
|
-
|
|
297
|
+
log_debug "ai_client_chat: POST ${endpoint} model=${model} (openai)"
|
|
103
298
|
local resp
|
|
104
|
-
resp
|
|
105
|
-
--max-time "$_AI_TIMEOUT" \
|
|
299
|
+
resp="$(curl -fsSL --max-time "$_AI_TIMEOUT" \
|
|
106
300
|
-H "Content-Type: application/json" \
|
|
107
301
|
${api_key:+-H "Authorization: Bearer ${api_key}"} \
|
|
108
|
-
-d "$body"
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
local ret=$?
|
|
112
|
-
if [[ $ret -ne 0 ]]; then
|
|
113
|
-
log_error "ai_client_chat: HTTP request failed (exit $ret)"
|
|
302
|
+
-d "$body" "$endpoint" 2>&1)"
|
|
303
|
+
if [[ $? -ne 0 ]]; then
|
|
304
|
+
log_error "ai_client_chat: openai request failed"
|
|
114
305
|
return 1
|
|
115
306
|
fi
|
|
116
|
-
|
|
117
307
|
printf '%s\n' "$resp"
|
|
118
|
-
return 0
|
|
119
308
|
}
|
|
120
309
|
|
|
121
310
|
# ---------------------------------------------------------------------------
|
|
122
311
|
# ai_client_extract_text RESPONSE_JSON
|
|
123
|
-
#
|
|
312
|
+
# Normalizes across provider shapes and strips markdown code fences:
|
|
313
|
+
# openai → .choices[0].message.content
|
|
314
|
+
# anthropic → concatenated .content[].text
|
|
315
|
+
# claude → .result (claude -p --output-format json)
|
|
124
316
|
# ---------------------------------------------------------------------------
|
|
125
317
|
ai_client_extract_text() {
|
|
126
318
|
local resp="$1"
|
|
127
|
-
local text
|
|
319
|
+
local text=""
|
|
320
|
+
# Primary: jq. NOTE: test keys with plain truthiness (`if .choices`), NOT
|
|
321
|
+
# `// empty` — an empty stream in an `if` condition makes jq emit nothing.
|
|
128
322
|
if command -v jq >/dev/null 2>&1; then
|
|
129
|
-
text
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
323
|
+
text="$(printf '%s' "$resp" | jq -r '
|
|
324
|
+
if .choices then (.choices[0].message.content // "")
|
|
325
|
+
elif .content then ([.content[]? | select(.type=="text") | .text] | join(""))
|
|
326
|
+
elif (.result != null) then .result
|
|
327
|
+
else "" end
|
|
328
|
+
' 2>/dev/null)"
|
|
329
|
+
fi
|
|
330
|
+
# Secondary: python3 (handles the same three shapes robustly).
|
|
331
|
+
if [[ -z "$text" ]] && command -v python3 >/dev/null 2>&1; then
|
|
332
|
+
text="$(printf '%s' "$resp" | python3 -c '
|
|
333
|
+
import json, sys
|
|
334
|
+
try:
|
|
335
|
+
d = json.load(sys.stdin)
|
|
336
|
+
except Exception:
|
|
337
|
+
sys.exit(0)
|
|
338
|
+
if isinstance(d, dict):
|
|
339
|
+
if d.get("choices"):
|
|
340
|
+
print(d["choices"][0].get("message", {}).get("content", ""))
|
|
341
|
+
elif isinstance(d.get("content"), list):
|
|
342
|
+
print("".join(b.get("text", "") for b in d["content"]
|
|
343
|
+
if isinstance(b, dict) and b.get("type") == "text"))
|
|
344
|
+
elif d.get("result") is not None:
|
|
345
|
+
print(d["result"])
|
|
346
|
+
' 2>/dev/null)"
|
|
347
|
+
fi
|
|
348
|
+
# Last resort: awk scan to the first unescaped closing quote of the value.
|
|
349
|
+
if [[ -z "$text" ]]; then
|
|
350
|
+
text="$(printf '%s' "$resp" | awk '
|
|
351
|
+
function extract(s, i,c,prev,out) {
|
|
352
|
+
out=""; prev=""
|
|
353
|
+
for (i=1; i<=length(s); i++) {
|
|
354
|
+
c=substr(s,i,1)
|
|
355
|
+
if (c=="\"" && prev!="\\") return out
|
|
356
|
+
out=out c; prev=(prev=="\\")?"":c
|
|
357
|
+
}
|
|
358
|
+
return out
|
|
140
359
|
}
|
|
141
|
-
|
|
360
|
+
{
|
|
361
|
+
split("result content text", ks, " ")
|
|
362
|
+
for (k=1; k<=3; k++) {
|
|
363
|
+
if (match($0, "\"" ks[k] "\"[[:space:]]*:[[:space:]]*\"")) {
|
|
364
|
+
v=extract(substr($0, RSTART+RLENGTH))
|
|
365
|
+
gsub(/\\n/, "\n", v); gsub(/\\"/, "\"", v)
|
|
366
|
+
print v; exit
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
')"
|
|
142
371
|
fi
|
|
143
|
-
# Strip markdown code fences (```json, ```, etc.) — remove any line starting with ```
|
|
144
372
|
printf '%s\n' "$text" | sed '/^[[:space:]]*```/d'
|
|
145
373
|
}
|
|
146
|
-
|
|
147
|
-
# ---------------------------------------------------------------------------
|
|
148
|
-
# _ai_json_object_body SYSTEM_PROMPT USER_PROMPT
|
|
149
|
-
# Build a request body that forces json_object output (no schema needed)
|
|
150
|
-
# ---------------------------------------------------------------------------
|
|
151
|
-
_ai_json_object_body() {
|
|
152
|
-
local sys_escaped="$1"
|
|
153
|
-
local user_escaped="$2"
|
|
154
|
-
cat <<JSON
|
|
155
|
-
{
|
|
156
|
-
"model": "${_AI_MODEL}",
|
|
157
|
-
"response_format": {"type": "json_object"},
|
|
158
|
-
"messages": [
|
|
159
|
-
{"role": "system", "content": ${sys_escaped}},
|
|
160
|
-
{"role": "user", "content": ${user_escaped}}
|
|
161
|
-
]
|
|
162
|
-
}
|
|
163
|
-
JSON
|
|
164
|
-
}
|
|
@@ -119,7 +119,7 @@
|
|
|
119
119
|
"description": "Metadata about AI involvement. Written by ai/wizard.sh; informational only.",
|
|
120
120
|
"properties": {
|
|
121
121
|
"used": { "type": "boolean", "default": false },
|
|
122
|
-
"provider": { "type": "string", "default": "openai" },
|
|
122
|
+
"provider": { "type": "string", "default": "auto", "description": "Resolved AI provider: auto|claude-cli|anthropic|openai|none." },
|
|
123
123
|
"model": { "type": "string", "default": "" },
|
|
124
124
|
"tokens_estimated": { "type": "integer", "minimum": 0, "default": 0 },
|
|
125
125
|
"spec_hash": { "type": "string", "default": "", "description": "sha256 of the spec JSON (without the ai.spec_hash field itself)." }
|
|
@@ -38,7 +38,8 @@ ai_wizard_run() {
|
|
|
38
38
|
fi
|
|
39
39
|
|
|
40
40
|
if ! ai_client_available; then
|
|
41
|
-
log_error "AI wizard
|
|
41
|
+
log_error "AI wizard needs an AI provider — install the 'claude' CLI (Claude Code OAuth),"
|
|
42
|
+
log_error "or set CLAUDE_CODE_OAUTH_TOKEN / ANTHROPIC_API_KEY / OPENAI_API_KEY. Run 'install doctor'."
|
|
42
43
|
return 1
|
|
43
44
|
fi
|
|
44
45
|
|
|
@@ -79,8 +80,12 @@ Output ONLY the JSON object, no prose, no markdown fences.
|
|
|
79
80
|
PROMPT
|
|
80
81
|
)
|
|
81
82
|
|
|
83
|
+
local _ai_provider _ai_model
|
|
84
|
+
_ai_provider="$(ai_client_provider)"
|
|
85
|
+
_ai_model="$(ai_client_model "$_ai_provider")"
|
|
86
|
+
|
|
82
87
|
log_banner "AI Installation Wizard"
|
|
83
|
-
log_info "Connecting to AI
|
|
88
|
+
log_info "Connecting to AI (provider: ${_ai_provider}, auth: $(ai_client_auth_source)${_ai_model:+, model: ${_ai_model}})"
|
|
84
89
|
|
|
85
90
|
local resp
|
|
86
91
|
resp=$(ai_client_chat "$sys_prompt" "$user_prompt")
|
|
@@ -132,10 +137,10 @@ PROMPT
|
|
|
132
137
|
fi
|
|
133
138
|
# Apply platform defaults
|
|
134
139
|
plan_apply_platform
|
|
135
|
-
# Record AI metadata
|
|
140
|
+
# Record AI metadata (the provider that actually served this run)
|
|
136
141
|
SPEC_AI_USED=true
|
|
137
|
-
SPEC_AI_PROVIDER="$
|
|
138
|
-
SPEC_AI_MODEL="$
|
|
142
|
+
SPEC_AI_PROVIDER="$_ai_provider"
|
|
143
|
+
SPEC_AI_MODEL="$_ai_model"
|
|
139
144
|
export SPEC_AI_USED SPEC_AI_PROVIDER SPEC_AI_MODEL
|
|
140
145
|
# Write final spec
|
|
141
146
|
spec_write "$(spec_path "$target")"
|
data/scripts/install/apply.sh
CHANGED
|
@@ -140,10 +140,14 @@ apply_run() {
|
|
|
140
140
|
log_warning "Deploy task failed: $task (continuing)"
|
|
141
141
|
done
|
|
142
142
|
|
|
143
|
-
# Agent files
|
|
143
|
+
# Agent files — run once. Skip if `agents` already ran in the task loop
|
|
144
|
+
# above (SPEC_AGENTS being set does not mean it should run twice).
|
|
144
145
|
if [[ -n "${SPEC_AGENTS:-}" ]]; then
|
|
145
|
-
|
|
146
|
-
|
|
146
|
+
case " ${SPEC_TASKS} " in
|
|
147
|
+
*" agents "*) ;; # already applied as a task
|
|
148
|
+
*) apply_task "agents" "$target" || \
|
|
149
|
+
log_warning "Agents task failed (continuing)" ;;
|
|
150
|
+
esac
|
|
147
151
|
fi
|
|
148
152
|
|
|
149
153
|
if [[ -n "$failed_tasks" ]]; then
|
data/scripts/install/cli.sh
CHANGED
|
@@ -63,6 +63,7 @@ _cli_load_core() {
|
|
|
63
63
|
_cli_require "fs.sh"
|
|
64
64
|
_cli_require "template.sh"
|
|
65
65
|
_cli_require "prompt.sh"
|
|
66
|
+
_cli_require "config.sh"
|
|
66
67
|
_cli_require "spec.sh"
|
|
67
68
|
_cli_require "plan.sh"
|
|
68
69
|
_cli_require "apply.sh"
|
|
@@ -95,6 +96,9 @@ _cli_parse_flags() {
|
|
|
95
96
|
_FLAG_AGENTS=""
|
|
96
97
|
_FLAG_TASKS=""
|
|
97
98
|
_FLAG_AI=0
|
|
99
|
+
_FLAG_AI_PROVIDER=""
|
|
100
|
+
_FLAG_AI_MODEL=""
|
|
101
|
+
_FLAG_CONFIG=""
|
|
98
102
|
_FLAG_SPEC=""
|
|
99
103
|
_FLAG_SCRAPE_URL=""
|
|
100
104
|
_FLAG_SCRAPE_DEPTH=""
|
|
@@ -111,6 +115,7 @@ _cli_parse_flags() {
|
|
|
111
115
|
_FLAG_SITE_URL _FLAG_SITE_AUTHOR _FLAG_SITE_EMAIL \
|
|
112
116
|
_FLAG_GITHUB_USER _FLAG_GITHUB_REPO _FLAG_THEME_SOURCE \
|
|
113
117
|
_FLAG_DEPLOY _FLAG_AGENTS _FLAG_TASKS _FLAG_AI _FLAG_SPEC \
|
|
118
|
+
_FLAG_AI_PROVIDER _FLAG_AI_MODEL _FLAG_CONFIG \
|
|
114
119
|
_FLAG_SCRAPE_URL _FLAG_SCRAPE_DEPTH _FLAG_SCRAPE_MAX_PAGES
|
|
115
120
|
|
|
116
121
|
while [[ $# -gt 0 ]]; do
|
|
@@ -123,6 +128,14 @@ _cli_parse_flags() {
|
|
|
123
128
|
--skip-doctor) _FLAG_SKIP_DOCTOR=1 ;;
|
|
124
129
|
--verbose|-v) _FLAG_VERBOSE=1 ;;
|
|
125
130
|
--ai) _FLAG_AI=1 ;;
|
|
131
|
+
--yes|-y) _FLAG_AUTO_ACCEPT=1 ;;
|
|
132
|
+
--ai-provider) shift; _FLAG_AI_PROVIDER="${1:-}" ;;
|
|
133
|
+
--ai-provider=*) _FLAG_AI_PROVIDER="${1#--ai-provider=}" ;;
|
|
134
|
+
--ai-model) shift; _FLAG_AI_MODEL="${1:-}" ;;
|
|
135
|
+
--ai-model=*) _FLAG_AI_MODEL="${1#--ai-model=}" ;;
|
|
136
|
+
--config) shift; _FLAG_CONFIG="${1:-}" ;;
|
|
137
|
+
--config=*) _FLAG_CONFIG="${1#--config=}" ;;
|
|
138
|
+
--no-ai) _FLAG_AI_PROVIDER="none" ;;
|
|
126
139
|
--profile) shift; _FLAG_PROFILE="${1:-}" ;;
|
|
127
140
|
--profile=*) _FLAG_PROFILE="${1#--profile=}" ;;
|
|
128
141
|
--output) shift; _FLAG_OUTPUT="${1:-}" ;;
|
|
@@ -241,6 +254,22 @@ _cmd_wizard() {
|
|
|
241
254
|
fi
|
|
242
255
|
}
|
|
243
256
|
|
|
257
|
+
_cmd_suggest() {
|
|
258
|
+
# install suggest [TARGET_DIR] [GOAL...]
|
|
259
|
+
# Recommends a profile + deploy target from an optional goal description.
|
|
260
|
+
# Uses the resolved AI provider; falls back to rule-based defaults.
|
|
261
|
+
local target="${_CLI_POS_0:-$(pwd)}"
|
|
262
|
+
local goal="${_CLI_POS_1:-}"
|
|
263
|
+
local sug="${_CLI_DIR}/ai/suggest.sh"
|
|
264
|
+
if [[ ! -f "$sug" ]]; then
|
|
265
|
+
log_error "suggest: module not available: $sug"
|
|
266
|
+
return 1
|
|
267
|
+
fi
|
|
268
|
+
# shellcheck source=/dev/null
|
|
269
|
+
source "$sug"
|
|
270
|
+
ai_suggest_run "$target" "$goal"
|
|
271
|
+
}
|
|
272
|
+
|
|
244
273
|
_cmd_agents() {
|
|
245
274
|
local target="${_CLI_TARGET:-$(pwd)}"
|
|
246
275
|
local spec_file
|
|
@@ -299,8 +328,12 @@ _cmd_deploy() {
|
|
|
299
328
|
fi
|
|
300
329
|
|
|
301
330
|
# Caller explicitly asked for this deploy target — override spec defaults.
|
|
331
|
+
# Deploy is deploy-only: no base tasks and no agent rewrites (use
|
|
332
|
+
# `install agents` for those). SPEC_TASKS="" now serialises to [] rather
|
|
333
|
+
# than falling back to the full default list (which would clobber content).
|
|
302
334
|
SPEC_DEPLOY="$target_name"
|
|
303
|
-
SPEC_TASKS=""
|
|
335
|
+
SPEC_TASKS=""
|
|
336
|
+
SPEC_AGENTS=""
|
|
304
337
|
|
|
305
338
|
spec_write "$spec_file"
|
|
306
339
|
apply_run "$spec_file"
|
|
@@ -460,9 +493,10 @@ Usage:
|
|
|
460
493
|
Subcommands:
|
|
461
494
|
init Install from flags + profile (default)
|
|
462
495
|
wizard Interactive wizard (--ai for AI-driven spec)
|
|
496
|
+
suggest Recommend a profile + deploy target (AI or rule-based)
|
|
463
497
|
agents Install AI agent files only
|
|
464
498
|
deploy Configure deployment plugin(s)
|
|
465
|
-
doctor Run pre-install health checks
|
|
499
|
+
doctor Run pre-install health checks (incl. AI provider status)
|
|
466
500
|
scrape Crawl an existing site into ./.zer0/scrape (no install)
|
|
467
501
|
diagnose Diagnose an existing install (--ai for suggestions)
|
|
468
502
|
upgrade Re-apply spec to an existing install
|
|
@@ -494,7 +528,12 @@ Global options:
|
|
|
494
528
|
--skip-doctor Skip pre-install health checks
|
|
495
529
|
--verbose, -v Extra debug output
|
|
496
530
|
--output json|human Log format (default: human)
|
|
531
|
+
--yes, -y Auto-confirm all yes/no prompts (alias of --auto-accept)
|
|
532
|
+
--config FILE Load installer settings from a YAML config file
|
|
497
533
|
--ai Use AI assistant for wizard/diagnose
|
|
534
|
+
--ai-provider NAME AI provider: auto|claude-cli|anthropic|openai|none
|
|
535
|
+
--ai-model NAME Override the AI model id for the active provider
|
|
536
|
+
--no-ai Disable AI (equivalent to --ai-provider none)
|
|
498
537
|
--spec FILE Load spec from FILE instead of detecting
|
|
499
538
|
--scrape URL Import content from URL during init (adds 'scrape' task)
|
|
500
539
|
--scrape-depth N Max crawl depth (default: 2)
|
|
@@ -503,9 +542,12 @@ Global options:
|
|
|
503
542
|
Examples:
|
|
504
543
|
install init . --profile blog --site-title "My Blog"
|
|
505
544
|
install init ~/mysite --profile github-pages --github-user bamr87
|
|
506
|
-
install wizard . --ai
|
|
545
|
+
install wizard . --ai # uses Claude Code OAuth if available
|
|
546
|
+
install wizard . --ai --ai-provider anthropic
|
|
547
|
+
install suggest . "a docs site for a Rust CLI"
|
|
548
|
+
install init . --config zer0.install.yml
|
|
507
549
|
install agents . --copilot --claude
|
|
508
|
-
install doctor .
|
|
550
|
+
install doctor . # includes AI provider status
|
|
509
551
|
install diff .
|
|
510
552
|
install upgrade . --force
|
|
511
553
|
install plan . --profile docs
|
|
@@ -537,9 +579,17 @@ cli_main() {
|
|
|
537
579
|
[[ "$_FLAG_VERBOSE" == "1" ]] && _LOG_VERBOSE=1 && export _LOG_VERBOSE
|
|
538
580
|
[[ -n "$_FLAG_OUTPUT" ]] && _LOG_OUTPUT="$_FLAG_OUTPUT" && export _LOG_OUTPUT
|
|
539
581
|
|
|
582
|
+
# Propagate AI selection + config path to the environment so provider
|
|
583
|
+
# resolution and config discovery see them regardless of the spec flow
|
|
584
|
+
# (e.g. `doctor`, `suggest`, `wizard --ai`).
|
|
585
|
+
[[ -n "$_FLAG_AI_PROVIDER" ]] && export ZER0_AI_PROVIDER="$_FLAG_AI_PROVIDER"
|
|
586
|
+
[[ -n "$_FLAG_AI_MODEL" ]] && export ZER0_AI_MODEL="$_FLAG_AI_MODEL"
|
|
587
|
+
[[ -n "$_FLAG_CONFIG" ]] && export ZER0_CONFIG="$_FLAG_CONFIG"
|
|
588
|
+
|
|
540
589
|
case "$subcommand" in
|
|
541
590
|
init) _cmd_init ;;
|
|
542
591
|
wizard) _cmd_wizard ;;
|
|
592
|
+
suggest) _cmd_suggest ;;
|
|
543
593
|
agents) _cmd_agents ;;
|
|
544
594
|
deploy) _cmd_deploy ;;
|
|
545
595
|
doctor) _cmd_doctor ;;
|