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.
Files changed (111) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +558 -5
  3. data/README.md +10 -27
  4. data/_data/authors.yml +4 -3
  5. data/_data/backlog.yml +28 -0
  6. data/_data/consumers.yml +147 -0
  7. data/_data/features.yml +142 -23
  8. data/_data/i18n/fr.yml +98 -0
  9. data/_data/i18n/languages.yml +36 -0
  10. data/_data/i18n/manifest.yml +1502 -0
  11. data/_data/navigation/docs.yml +2 -0
  12. data/_data/navigation/main.yml +6 -0
  13. data/_data/series.yml +19 -0
  14. data/_data/theme-manifest.yml +348 -268
  15. data/_data/ui-text.yml +36 -246
  16. data/_includes/README.md +25 -2
  17. data/_includes/analytics/google-tag-manager-body.html +11 -2
  18. data/_includes/analytics/google-tag-manager-head.html +16 -6
  19. data/_includes/analytics/posthog.html +33 -4
  20. data/_includes/components/abc-letter.html +43 -0
  21. data/_includes/components/author-avatar-url.html +4 -2
  22. data/_includes/components/book-card.html +42 -0
  23. data/_includes/components/book-nav.html +80 -0
  24. data/_includes/components/book-plate.html +31 -0
  25. data/_includes/components/book-toc.html +47 -0
  26. data/_includes/components/bookshelf.html +68 -0
  27. data/_includes/components/card-grid.html +60 -0
  28. data/_includes/components/data-card.html +95 -0
  29. data/_includes/components/env-switcher.html +3 -1
  30. data/_includes/components/halfmoon.html +5 -1
  31. data/_includes/components/language-toggle.html +81 -0
  32. data/_includes/components/page-feedback.html +65 -2
  33. data/_includes/components/search-modal.html +2 -2
  34. data/_includes/components/shortcuts-modal.html +1 -1
  35. data/_includes/components/theme-controls-bar.html +10 -2
  36. data/_includes/components/theme-customizer.html +8 -2
  37. data/_includes/components/translation-notice.html +27 -0
  38. data/_includes/content/intro.html +16 -15
  39. data/_includes/content/seo.html +9 -3
  40. data/_includes/core/color-mode-init.html +13 -4
  41. data/_includes/core/favicon.html +46 -0
  42. data/_includes/core/footer.html +9 -4
  43. data/_includes/core/head.html +18 -0
  44. data/_includes/core/header.html +9 -4
  45. data/_includes/core/hreflang.html +33 -0
  46. data/_includes/core/i18n.html +36 -0
  47. data/_includes/custom/body-end.html +18 -0
  48. data/_includes/custom/body-start.html +17 -0
  49. data/_includes/custom/footer.html +18 -0
  50. data/_includes/custom/head.html +18 -0
  51. data/_includes/navigation/breadcrumbs.html +1 -1
  52. data/_includes/navigation/local-graph.html +28 -2
  53. data/_includes/navigation/nav-tree.html +3 -3
  54. data/_includes/navigation/navbar.html +5 -4
  55. data/_includes/navigation/sidebar-config.html +21 -0
  56. data/_includes/navigation/sidebar-nav.html +4 -0
  57. data/_includes/navigation/sidebar-pagetree.html +150 -0
  58. data/_includes/navigation/sidebar-right.html +3 -2
  59. data/_includes/navigation/unified-drawer.html +9 -3
  60. data/_includes/obsidian/full-graph.html +165 -136
  61. data/_layouts/404.html +260 -0
  62. data/_layouts/README.md +2 -0
  63. data/_layouts/article.html +7 -1
  64. data/_layouts/book-abc.html +106 -0
  65. data/_layouts/book-story.html +91 -0
  66. data/_layouts/book.html +113 -0
  67. data/_layouts/collection.html +17 -5
  68. data/_layouts/default.html +5 -3
  69. data/_layouts/home.html +5 -2
  70. data/_layouts/landing.html +9 -0
  71. data/_layouts/news.html +159 -33
  72. data/_layouts/root.html +29 -9
  73. data/_layouts/section.html +61 -17
  74. data/_sass/components/_book.scss +423 -0
  75. data/_sass/core/_navbar.scss +13 -0
  76. data/_sass/core/_obsidian.scss +295 -7
  77. data/_sass/layouts/_navbar-extras.scss +6 -1
  78. data/_sass/theme/_backgrounds.scss +21 -8
  79. data/assets/css/main.scss +1 -0
  80. data/assets/js/auto-hide-nav.js +5 -1
  81. data/assets/js/halfmoon.js +26 -0
  82. data/assets/js/obsidian-graph.js +707 -265
  83. data/assets/js/obsidian-local-graph.js +161 -54
  84. data/assets/js/search-modal.js +4 -1
  85. data/scripts/README.md +20 -26
  86. data/scripts/bin/audit-consumer +1 -1
  87. data/scripts/bin/manifest +31 -5
  88. data/scripts/bin/sync-plugins +0 -1
  89. data/scripts/bin/validate +5 -1
  90. data/scripts/dev/rasterize-svg.js +65 -0
  91. data/scripts/features/generate-preview-images +49 -1390
  92. data/scripts/features/install-preview-generator +55 -33
  93. data/scripts/install/README.md +55 -25
  94. data/scripts/install/ai/client.sh +302 -93
  95. data/scripts/install/ai/prompts/spec.schema.json +1 -1
  96. data/scripts/install/ai/prompts/wizard.system.md +8 -17
  97. data/scripts/install/ai/wizard.sh +10 -5
  98. data/scripts/install/apply.sh +7 -3
  99. data/scripts/install/cli.sh +54 -4
  100. data/scripts/install/config.sh +167 -0
  101. data/scripts/install/doctor.sh +38 -0
  102. data/scripts/install/plan.sh +10 -0
  103. data/scripts/install/spec.sh +15 -7
  104. data/scripts/install/template.sh +4 -0
  105. data/scripts/lib/README.md +1 -5
  106. data/scripts/lib/install/deploy/README.md +3 -9
  107. data/scripts/lib/preview_generator.py +2261 -1341
  108. data/scripts/propagate.rb +277 -0
  109. data/scripts/translate.rb +1204 -0
  110. metadata +34 -3
  111. data/_plugins/preview_image_generator.rb +0 -351
@@ -1,52 +1,153 @@
1
1
  #!/bin/bash
2
2
  # =============================================================================
3
- # scripts/install/ai/client.sh — HTTP client for AI providers
3
+ # scripts/install/ai/client.sh — Multi-provider AI client
4
4
  # =============================================================================
5
- # Thin curl wrapper that:
6
- # - Targets OpenAI-compatible APIs (OpenAI, Azure OpenAI, Ollama)
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
- # Provides:
12
- # ai_client_chat SYSTEM_PROMPT USER_PROMPT [JSON_SCHEMA]
13
- # → print raw API JSON response to stdout
14
- # → return 0 on success, 1 on error
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
- # ai_client_extract_text RESPONSE_JSON
17
- # → print the assistant's text content from a chat response
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
- # ai_client_available
20
- # → return 0 if OPENAI_API_KEY or OPENAI_BASE_URL is set, 1 otherwise
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
- # Environment:
23
- # OPENAI_API_KEY Required for openai.com
24
- # OPENAI_BASE_URL Override base URL (Azure OpenAI / Ollama compatible)
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
- _AI_BASE_URL="${OPENAI_BASE_URL:-https://api.openai.com/v1}"
35
- _AI_MODEL="${OPENAI_MODEL:-gpt-4o-mini}"
36
- _AI_TIMEOUT=30
43
+ _AI_TIMEOUT="${ZER0_AI_TIMEOUT:-30}"
37
44
 
38
45
  # ---------------------------------------------------------------------------
39
- # ai_client_available
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
- [[ -n "${OPENAI_API_KEY:-}" ]] && return 0
44
- [[ -n "${OPENAI_BASE_URL:-}" ]] && return 0
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 not available (set OPENAI_API_KEY or OPENAI_BASE_URL)"
158
+ log_error "ai_client_chat: no AI provider available (see 'install doctor')"
58
159
  return 1
59
160
  fi
60
161
 
61
- local api_key="${OPENAI_API_KEY:-}"
62
- local endpoint="${_AI_BASE_URL}/chat/completions"
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=$(printf '%s' "$system_prompt" | python3 -c "import json,sys; print(json.dumps(sys.stdin.read()))" 2>/dev/null \
67
- || printf '%s' "$system_prompt" | sed 's/\\/\\\\/g; s/"/\\"/g; s/$/\\n/g' | tr -d '\n' | sed 's/\\n$//')
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
- # Structured output (function calling)
74
- body=$(cat <<JSON
273
+ body="$(cat <<JSON
75
274
  {
76
- "model": "${_AI_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=$(cat <<JSON
285
+ body="$(cat <<JSON
90
286
  {
91
- "model": "${_AI_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=${_AI_MODEL}"
102
-
297
+ log_debug "ai_client_chat: POST ${endpoint} model=${model} (openai)"
103
298
  local resp
104
- resp=$(curl -fsSL \
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
- "$endpoint" 2>&1)
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
- # Extracts assistant text and strips markdown code fences if present.
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=$(printf '%s' "$resp" | jq -r '.choices[0].message.content // ""')
130
- else
131
- # Minimal awk extraction
132
- text=$(printf '%s' "$resp" | awk '
133
- /"content"[[:space:]]*:/ {
134
- line = $0
135
- sub(/.*"content"[[:space:]]*:[[:space:]]*"/, "", line)
136
- sub(/"[^"]*$/, "", line)
137
- gsub(/\\n/, "\n", line)
138
- print line
139
- exit
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
- fences (no triple-backticks). The first character of your reply MUST be
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
- the user prompt — copy them through unchanged. Only fill in fields the
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
- a complete `options` object (all 5 required keys: `dry_run`, `force`,
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
- default and move on. Do not ask follow-up questions in non-interactive
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 requires OPENAI_API_KEY or OPENAI_BASE_URL"
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... (model: ${OPENAI_MODEL:-gpt-4o-mini})"
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="${OPENAI_PROVIDER:-openai}"
138
- SPEC_AI_MODEL="${OPENAI_MODEL:-gpt-4o-mini}"
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")"
@@ -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
- apply_task "agents" "$target" || \
146
- log_warning "Agents task failed (continuing)"
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