jekyll-theme-zer0 1.26.0 → 1.28.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 +558 -5
- data/README.md +10 -27
- data/_data/authors.yml +4 -3
- data/_data/backlog.yml +28 -0
- data/_data/consumers.yml +147 -0
- data/_data/features.yml +142 -23
- data/_data/i18n/fr.yml +98 -0
- data/_data/i18n/languages.yml +36 -0
- data/_data/i18n/manifest.yml +1502 -0
- data/_data/navigation/docs.yml +2 -0
- data/_data/navigation/main.yml +6 -0
- data/_data/series.yml +19 -0
- data/_data/theme-manifest.yml +348 -268
- data/_data/ui-text.yml +36 -246
- data/_includes/README.md +25 -2
- 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/author-avatar-url.html +4 -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/data-card.html +95 -0
- data/_includes/components/env-switcher.html +3 -1
- data/_includes/components/halfmoon.html +5 -1
- data/_includes/components/language-toggle.html +81 -0
- data/_includes/components/page-feedback.html +65 -2
- data/_includes/components/search-modal.html +2 -2
- data/_includes/components/shortcuts-modal.html +1 -1
- data/_includes/components/theme-controls-bar.html +10 -2
- data/_includes/components/theme-customizer.html +8 -2
- data/_includes/components/translation-notice.html +27 -0
- data/_includes/content/intro.html +16 -15
- 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 +9 -4
- data/_includes/core/head.html +18 -0
- data/_includes/core/header.html +9 -4
- data/_includes/core/hreflang.html +33 -0
- data/_includes/core/i18n.html +36 -0
- 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/breadcrumbs.html +1 -1
- data/_includes/navigation/local-graph.html +28 -2
- data/_includes/navigation/nav-tree.html +3 -3
- data/_includes/navigation/navbar.html +5 -4
- data/_includes/navigation/sidebar-config.html +21 -0
- data/_includes/navigation/sidebar-nav.html +4 -0
- data/_includes/navigation/sidebar-pagetree.html +150 -0
- data/_includes/navigation/sidebar-right.html +3 -2
- data/_includes/navigation/unified-drawer.html +9 -3
- data/_includes/obsidian/full-graph.html +165 -136
- data/_layouts/404.html +260 -0
- data/_layouts/README.md +2 -0
- data/_layouts/article.html +7 -1
- 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/default.html +5 -3
- data/_layouts/home.html +5 -2
- data/_layouts/landing.html +9 -0
- data/_layouts/news.html +159 -33
- data/_layouts/root.html +29 -9
- data/_layouts/section.html +61 -17
- data/_sass/components/_book.scss +423 -0
- data/_sass/core/_navbar.scss +13 -0
- data/_sass/core/_obsidian.scss +295 -7
- data/_sass/layouts/_navbar-extras.scss +6 -1
- data/_sass/theme/_backgrounds.scss +21 -8
- data/assets/css/main.scss +1 -0
- data/assets/js/auto-hide-nav.js +5 -1
- data/assets/js/halfmoon.js +26 -0
- data/assets/js/obsidian-graph.js +707 -265
- data/assets/js/obsidian-local-graph.js +161 -54
- data/assets/js/search-modal.js +4 -1
- data/scripts/README.md +20 -26
- data/scripts/bin/audit-consumer +1 -1
- data/scripts/bin/manifest +31 -5
- data/scripts/bin/sync-plugins +0 -1
- data/scripts/bin/validate +5 -1
- data/scripts/dev/rasterize-svg.js +65 -0
- data/scripts/features/generate-preview-images +49 -1390
- data/scripts/features/install-preview-generator +55 -33
- data/scripts/install/README.md +55 -25
- data/scripts/install/ai/client.sh +302 -93
- data/scripts/install/ai/prompts/spec.schema.json +1 -1
- data/scripts/install/ai/prompts/wizard.system.md +8 -17
- 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/README.md +1 -5
- data/scripts/lib/install/deploy/README.md +3 -9
- data/scripts/lib/preview_generator.py +2261 -1341
- data/scripts/propagate.rb +277 -0
- data/scripts/translate.rb +1204 -0
- metadata +34 -3
- data/_plugins/preview_image_generator.rb +0 -351
|
@@ -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)." }
|
|
@@ -1,31 +1,24 @@
|
|
|
1
1
|
# zer0-mistakes AI Installation Wizard
|
|
2
2
|
|
|
3
|
-
You are the **zer0-mistakes installer wizard**. You convert the user's intent
|
|
4
|
-
into a single, valid **install spec JSON** that conforms to the schema the
|
|
5
|
-
caller will provide in the user prompt.
|
|
3
|
+
You are the **zer0-mistakes installer wizard**. You convert the user's intent into a single, valid **install spec JSON** that conforms to the schema the caller will provide in the user prompt.
|
|
6
4
|
|
|
7
|
-
You NEVER write files yourself. You ONLY emit a JSON spec. The installer's
|
|
8
|
-
`apply.sh` is the sole writer.
|
|
5
|
+
You NEVER write files yourself. You ONLY emit a JSON spec. The installer's `apply.sh` is the sole writer.
|
|
9
6
|
|
|
10
7
|
---
|
|
11
8
|
|
|
12
9
|
## Hard constraints (do not violate)
|
|
13
10
|
|
|
14
11
|
1. **Output ONLY one JSON object.** No prose. No explanations. No markdown
|
|
15
|
-
|
|
16
|
-
`{` and the last MUST be `}`.
|
|
12
|
+
fences (no triple-backticks). The first character of your reply MUST be `{` and the last MUST be `}`.
|
|
17
13
|
2. **Conform exactly to the provided schema.** All `required` keys must be
|
|
18
14
|
present. All enums must match. `additionalProperties: false` is enforced.
|
|
19
15
|
3. **Honor every value the user already provided** in the "Key info" block of
|
|
20
|
-
|
|
21
|
-
user left as `not set`.
|
|
16
|
+
the user prompt — copy them through unchanged. Only fill in fields the user left as `not set`.
|
|
22
17
|
4. **Always include `schema_version: "1"`**, the supplied `target_dir`, and
|
|
23
|
-
|
|
24
|
-
`backup`, `non_interactive`, `output`).
|
|
18
|
+
a complete `options` object (all 5 required keys: `dry_run`, `force`, `backup`, `non_interactive`, `output`).
|
|
25
19
|
5. **`tasks` MUST be non-empty and end with `"marker"`.**
|
|
26
20
|
6. **Be decisive.** If information is ambiguous, pick the most reasonable
|
|
27
|
-
|
|
28
|
-
mode.
|
|
21
|
+
default and move on. Do not ask follow-up questions in non-interactive mode.
|
|
29
22
|
|
|
30
23
|
---
|
|
31
24
|
|
|
@@ -49,8 +42,7 @@ If a flag in "Key info" already names a profile, USE IT — do not second-guess.
|
|
|
49
42
|
|
|
50
43
|
## Task defaults (per profile)
|
|
51
44
|
|
|
52
|
-
If you are unsure which tasks to enable, use these defaults. `marker` is
|
|
53
|
-
always last.
|
|
45
|
+
If you are unsure which tasks to enable, use these defaults. `marker` is always last.
|
|
54
46
|
|
|
55
47
|
- `minimal`: `["config", "gemfile", "gitignore", "marker"]`
|
|
56
48
|
- `default`: `["config", "gemfile", "docker", "pages", "nav", "data", "gitignore", "readme", "agents", "marker"]`
|
|
@@ -102,8 +94,7 @@ Populate `agents` (array, may be empty) based on signals:
|
|
|
102
94
|
|
|
103
95
|
## Required output shape (example)
|
|
104
96
|
|
|
105
|
-
The example below is illustrative. Adapt every value to the user's request.
|
|
106
|
-
The exact field set is enforced by the schema in the user prompt.
|
|
97
|
+
The example below is illustrative. Adapt every value to the user's request. The exact field set is enforced by the schema in the user prompt.
|
|
107
98
|
|
|
108
99
|
```
|
|
109
100
|
{
|
|
@@ -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
|