@kuandotdev/indicator 0.1.1 → 0.1.3

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 (80) hide show
  1. package/README.md +2 -7
  2. package/package.json +4 -1
  3. package/project/.cursor/rules/agent-docs.mdc +16 -0
  4. package/project/.cursor/rules/api-credential-storage.mdc +16 -0
  5. package/project/.cursor/rules/pinescript-v6.mdc +16 -0
  6. package/project/.cursor/rules/stock-model-forecast.mdc +16 -0
  7. package/project/.cursor/rules/system-prompt-injection.mdc +16 -0
  8. package/project/.cursor/rules/system-prompt-updater.mdc +16 -0
  9. package/project/.cursor/rules/tradingview-stock-data.mdc +16 -0
  10. package/project/.env.example +44 -0
  11. package/project/.npm-packaged-project +1 -0
  12. package/project/.pi/APPEND_SYSTEM.md +338 -0
  13. package/project/.pi/settings.json +8 -0
  14. package/project/AGENTS.md +538 -0
  15. package/project/CLAUDE.md +538 -0
  16. package/project/GEMINI.md +538 -0
  17. package/project/Makefile +488 -0
  18. package/project/README.md +419 -0
  19. package/project/conda-env-active.sh +98 -0
  20. package/project/conda-env-deactive.sh +42 -0
  21. package/project/docs/agent-install.md +446 -0
  22. package/project/docs/agent-skill-directory.md +222 -0
  23. package/project/docs/integration.html +271 -0
  24. package/project/packages/indicator/README.md +39 -0
  25. package/project/packages/indicator/package.json +40 -0
  26. package/project/packages/indicator/scripts/build-project-snapshot.js +57 -0
  27. package/project/packages/indicator/src/cli.js +368 -0
  28. package/project/packages/tradingview-stock-data-skill/README.md +112 -0
  29. package/project/packages/tradingview-stock-data-skill/extensions/stock-prompt-injector.ts +121 -0
  30. package/project/packages/tradingview-stock-data-skill/package.json +35 -0
  31. package/project/packages/tradingview-stock-data-skill/scripts/postinstall.sh +73 -0
  32. package/project/packages/tradingview-stock-data-skill/skills/tradingview-stock-data/SKILL.md +241 -0
  33. package/project/pyproject.toml +68 -0
  34. package/project/screenshots/.gitkeep +0 -0
  35. package/project/scripts/indicators/example_rsi_bands.pine +27 -0
  36. package/project/scripts/indicators/tsla_levels.pine +57 -0
  37. package/project/skills/agent-docs/SKILL.md +56 -0
  38. package/project/skills/api-credential-storage/SKILL.md +83 -0
  39. package/project/skills/api-credential-storage/scripts/upsert_env.py +151 -0
  40. package/project/skills/pinescript-v6/SKILL.md +129 -0
  41. package/project/skills/pinescript-v6/reference/built-ins.md +219 -0
  42. package/project/skills/pinescript-v6/reference/templates/alert-webhook.pine +76 -0
  43. package/project/skills/pinescript-v6/reference/templates/indicator.pine +48 -0
  44. package/project/skills/pinescript-v6/reference/templates/strategy.pine +50 -0
  45. package/project/skills/pinescript-v6/reference/v5-to-v6-migration.md +102 -0
  46. package/project/skills/pinescript-v6/reference/v6-language.md +202 -0
  47. package/project/skills/stock-model-forecast/SKILL.md +192 -0
  48. package/project/skills/system-prompt-injection/CUSTOM_SYSTEM_PROMPT.md +333 -0
  49. package/project/skills/system-prompt-injection/DEFAULT_SYSTEM_PROMPT.md +327 -0
  50. package/project/skills/system-prompt-injection/SKILL.md +90 -0
  51. package/project/skills/system-prompt-injection/SYSTEM_PROMPT.md +23 -0
  52. package/project/skills/system-prompt-updater/SKILL.md +82 -0
  53. package/project/skills/system-prompt-updater/scripts/system_prompt_update.sh +106 -0
  54. package/project/skills/tradingview-stock-data/SKILL.md +272 -0
  55. package/project/src/tv_indicator/__init__.py +0 -0
  56. package/project/src/tv_indicator/browser/__init__.py +0 -0
  57. package/project/src/tv_indicator/browser/automation.py +541 -0
  58. package/project/src/tv_indicator/browser/selectors.py +70 -0
  59. package/project/src/tv_indicator/cli/__init__.py +0 -0
  60. package/project/src/tv_indicator/cli/browser_cmds.py +92 -0
  61. package/project/src/tv_indicator/cli/data_cmds.py +178 -0
  62. package/project/src/tv_indicator/cli/main.py +56 -0
  63. package/project/src/tv_indicator/cli/model_cmds.py +255 -0
  64. package/project/src/tv_indicator/cli/pine_cmds.py +140 -0
  65. package/project/src/tv_indicator/config.py +98 -0
  66. package/project/src/tv_indicator/data/__init__.py +0 -0
  67. package/project/src/tv_indicator/data/client.py +187 -0
  68. package/project/src/tv_indicator/data/screener.py +268 -0
  69. package/project/src/tv_indicator/mcp/__init__.py +0 -0
  70. package/project/src/tv_indicator/mcp/agent_server.py +398 -0
  71. package/project/src/tv_indicator/mcp/browser_server.py +133 -0
  72. package/project/src/tv_indicator/mcp/data_server.py +239 -0
  73. package/project/src/tv_indicator/model/__init__.py +19 -0
  74. package/project/src/tv_indicator/model/forecast.py +693 -0
  75. package/project/tools/import_agent_tools.sh +503 -0
  76. package/project/tools/install_skills.sh +673 -0
  77. package/project/tools/interactive_install.sh +917 -0
  78. package/project/tools/progress.sh +114 -0
  79. package/project/tools/uninstall_agent_tools.sh +373 -0
  80. package/src/cli.js +22 -25
@@ -0,0 +1,917 @@
1
+ #!/usr/bin/env bash
2
+ # interactive_install.sh — post-core-install wizard for new users.
3
+ # It optionally writes .env values, runs TradingView browser login, waits for
4
+ # session-check, then imports skills/context/MCP config for selected agents.
5
+
6
+ set -euo pipefail
7
+
8
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9
+ PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
10
+ CONDA_ENV="${CONDA_ENV:-tv-indicator}"
11
+ DEFAULT_PLATFORM="${TV_INDICATOR_AGENT_PLATFORM:-${PLATFORM:-auto}}"
12
+ INPUT_TIMEOUT_SECONDS="${TV_INSTALL_INPUT_TIMEOUT:-60}"
13
+ MAX_NO_INPUT_ATTEMPTS="${TV_INSTALL_MAX_NO_INPUT_ATTEMPTS:-3}"
14
+
15
+ cd "$PROJECT_ROOT"
16
+
17
+ log() { printf '%s\n' "$*"; }
18
+ warn() { printf 'WARN: %s\n' "$*" >&2; }
19
+
20
+ # Interactive only requires stdin to be a TTY. Stdout may be a pipe (e.g. when
21
+ # select_platforms is called via command substitution: $(select_platforms));
22
+ # the menu draws to stderr which still reaches the terminal in that case.
23
+ is_tty() { [[ -t 0 ]]; }
24
+
25
+ # Keep timeout knobs sane if users pass malformed env values.
26
+ case "$INPUT_TIMEOUT_SECONDS" in ''|*[!0-9]*) INPUT_TIMEOUT_SECONDS=60 ;; esac
27
+ case "$MAX_NO_INPUT_ATTEMPTS" in ''|*[!0-9]*) MAX_NO_INPUT_ATTEMPTS=3 ;; esac
28
+ (( INPUT_TIMEOUT_SECONDS < 1 )) && INPUT_TIMEOUT_SECONDS=60
29
+ (( MAX_NO_INPUT_ATTEMPTS < 1 )) && MAX_NO_INPUT_ATTEMPTS=3
30
+
31
+ abort_install_no_input() {
32
+ local context="${1:-installer prompt}"
33
+ printf '\n' > /dev/tty 2>/dev/null || true
34
+ warn "No input received for $context after $MAX_NO_INPUT_ATTEMPTS attempts. Aborting install."
35
+
36
+ if [[ "${TV_INSTALL_CLEANUP_ON_NO_INPUT:-1}" == "1" ]]; then
37
+ warn "Cleaning up data installed by this installer (managed agent config, browser sessions, conda env, .env)."
38
+ if command -v make >/dev/null 2>&1; then
39
+ MAKEFLAGS= make --no-print-directory uninstall-all KEEP_ENV=0 || \
40
+ warn "Automatic cleanup failed. Run 'make uninstall-all' manually."
41
+ else
42
+ warn "make not found; run './tools/uninstall_agent_tools.sh --platform all' and remove the conda env manually."
43
+ fi
44
+ else
45
+ warn "Cleanup skipped because TV_INSTALL_CLEANUP_ON_NO_INPUT=0."
46
+ fi
47
+
48
+ exit 130
49
+ }
50
+
51
+ warn_no_input_retry() {
52
+ local context="$1"
53
+ local attempt="$2"
54
+ warn "No input detected for $context ($attempt/$MAX_NO_INPUT_ATTEMPTS). Waiting again..."
55
+ }
56
+
57
+ ask_yes_no() {
58
+ local prompt="$1"
59
+ local default="${2:-n}"
60
+ local suffix answer oldstty junk drain_i attempts timed_out
61
+ if [[ "$default" == "y" ]]; then suffix="[Y/n]"; else suffix="[y/N]"; fi
62
+ attempts=0
63
+
64
+ while true; do
65
+ answer=""
66
+ timed_out=0
67
+
68
+ # Single-key prompt in interactive terminals: y/Y/n/N proceeds immediately;
69
+ # Enter still accepts the default. This avoids requiring an extra Enter after
70
+ # typing `n` or `N` during install.
71
+ if is_tty && [ -r /dev/tty ] && [ -w /dev/tty ] \
72
+ && oldstty="$(stty -g < /dev/tty 2>/dev/null)" \
73
+ && stty -echo -icanon time 0 min 1 < /dev/tty 2>/dev/null; then
74
+ printf '%s %s ' "$prompt" "$suffix" > /dev/tty
75
+ if ! IFS= read -r -s -n 1 -t "$INPUT_TIMEOUT_SECONDS" answer < /dev/tty; then
76
+ timed_out=1
77
+ answer=""
78
+ fi
79
+ if [[ "$answer" == $'\004' ]]; then
80
+ timed_out=1
81
+ answer=""
82
+ fi
83
+
84
+ # Drain an immediately buffered Enter (e.g. if the user typed n+Enter out
85
+ # of habit) so the next prompt does not consume the leftover newline.
86
+ if (( ! timed_out )); then
87
+ stty time 0 min 0 < /dev/tty 2>/dev/null || true
88
+ for drain_i in 1 2; do
89
+ if IFS= read -r -s -n 1 -t 0 junk < /dev/tty 2>/dev/null; then
90
+ case "$junk" in $'\r'|$'\n') : ;; *) break ;; esac
91
+ else
92
+ break
93
+ fi
94
+ done
95
+ fi
96
+
97
+ stty "$oldstty" < /dev/tty 2>/dev/null || true
98
+ case "$answer" in
99
+ $'\r'|$'\n'|"") printf '\n' > /dev/tty ;;
100
+ *) printf '%s\n' "$answer" > /dev/tty ;;
101
+ esac
102
+ else
103
+ if ! read -r -t "$INPUT_TIMEOUT_SECONDS" -p "$prompt $suffix " answer; then
104
+ timed_out=1
105
+ answer=""
106
+ printf '\n'
107
+ fi
108
+ fi
109
+
110
+ if (( timed_out )); then
111
+ ((attempts+=1))
112
+ if (( attempts >= MAX_NO_INPUT_ATTEMPTS )); then
113
+ abort_install_no_input "$prompt"
114
+ fi
115
+ warn_no_input_retry "$prompt" "$attempts"
116
+ continue
117
+ fi
118
+
119
+ answer="${answer:-$default}"
120
+ answer="$(printf '%s' "$answer" | tr '[:upper:]' '[:lower:]')"
121
+ case "$answer" in
122
+ y|yes) return 0 ;;
123
+ *) return 1 ;;
124
+ esac
125
+ done
126
+ }
127
+
128
+ ensure_env_file() {
129
+ if [[ ! -f .env ]]; then
130
+ if [[ -f .env.example ]]; then
131
+ cp .env.example .env
132
+ log "Created .env from .env.example"
133
+ else
134
+ touch .env
135
+ log "Created empty .env"
136
+ fi
137
+ fi
138
+ }
139
+
140
+ upsert_env() {
141
+ local key="$1"
142
+ local value="$2"
143
+ ensure_env_file
144
+ python3 - "$key" "$value" <<'PY'
145
+ import sys
146
+ from pathlib import Path
147
+ key, value = sys.argv[1], sys.argv[2]
148
+ path = Path(".env")
149
+ lines = path.read_text(encoding="utf-8").splitlines() if path.exists() else []
150
+ out = []
151
+ written = False
152
+ for line in lines:
153
+ if line.startswith(key + "="):
154
+ out.append(f"{key}={value}")
155
+ written = True
156
+ else:
157
+ out.append(line)
158
+ if not written:
159
+ if out and out[-1].strip():
160
+ out.append("")
161
+ out.append(f"{key}={value}")
162
+ path.write_text("\n".join(out) + "\n", encoding="utf-8")
163
+ PY
164
+ }
165
+
166
+ prompt_env_value() {
167
+ local key="$1"
168
+ local label="$2"
169
+ local secret="${3:-1}"
170
+ local value=""
171
+ local attempts=0
172
+ local timed_out=0
173
+
174
+ while true; do
175
+ value=""
176
+ timed_out=0
177
+ if [[ "$secret" == "1" ]]; then
178
+ if ! read -r -s -t "$INPUT_TIMEOUT_SECONDS" -p "$label ($key, Enter to skip): " value; then
179
+ timed_out=1
180
+ fi
181
+ printf '\n'
182
+ else
183
+ if ! read -r -t "$INPUT_TIMEOUT_SECONDS" -p "$label ($key, Enter to skip): " value; then
184
+ timed_out=1
185
+ printf '\n'
186
+ fi
187
+ fi
188
+
189
+ if (( timed_out )); then
190
+ ((attempts+=1))
191
+ if (( attempts >= MAX_NO_INPUT_ATTEMPTS )); then
192
+ abort_install_no_input "$key input"
193
+ fi
194
+ warn_no_input_retry "$key input" "$attempts"
195
+ continue
196
+ fi
197
+ break
198
+ done
199
+
200
+ if [[ -n "$value" ]]; then
201
+ upsert_env "$key" "$value"
202
+ log "Saved $key to .env"
203
+ fi
204
+ }
205
+
206
+ get_env_value() {
207
+ local key="$1"
208
+ [[ -f .env ]] || return 1
209
+ awk -F= -v key="$key" '$1 == key { print substr($0, length(key) + 2) }' .env | tail -n 1
210
+ }
211
+
212
+ normalize_language_code() {
213
+ local raw="$1"
214
+ raw="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]' | tr '-' '_' | tr -d '[:space:]')"
215
+ raw="${raw%%.*}"
216
+ raw="${raw%%@*}"
217
+ case "$raw" in
218
+ ""|c|posix) printf '\n' ;;
219
+ en|en_*|eng|english) printf 'en\n' ;;
220
+ zhtw|zh_tw|zh_hk|zh_mo|zh_hant|zh_hant_*|traditional|traditionalchinese|tc)
221
+ printf 'zhtw\n' ;;
222
+ zhcn|zh_cn|zh_sg|zh_hans|zh_hans_*|simplified|simplifiedchinese|sc|cn)
223
+ printf 'zhcn\n' ;;
224
+ zh) printf 'zhtw\n' ;;
225
+ ja|ja_*|jp|jp_*|japanese) printf 'jp\n' ;;
226
+ ko|ko_*|kr|kr_*|korean) printf 'ko\n' ;;
227
+ *_*) printf '%s\n' "${raw%%_*}" | sed 's/[^a-z0-9_-]//g' ;;
228
+ *)
229
+ printf '%s\n' "$raw" | sed 's/[^a-z0-9_-]//g'
230
+ ;;
231
+ esac
232
+ }
233
+
234
+ language_label() {
235
+ case "$1" in
236
+ en) printf 'English\n' ;;
237
+ zhtw) printf 'Traditional Chinese\n' ;;
238
+ zhcn) printf 'Simplified Chinese\n' ;;
239
+ jp) printf 'Japanese\n' ;;
240
+ ko) printf 'Korean\n' ;;
241
+ *) printf 'custom\n' ;;
242
+ esac
243
+ }
244
+
245
+ detect_terminal_language() {
246
+ local locale_value
247
+ locale_value="${LC_ALL:-${LC_MESSAGES:-${LANG:-}}}"
248
+ normalize_language_code "$locale_value"
249
+ }
250
+
251
+ default_language_code() {
252
+ local selected=""
253
+ if [[ -n "${TV_AGENT_LANGUAGE:-}" ]]; then
254
+ normalize_language_code "$TV_AGENT_LANGUAGE"
255
+ return
256
+ fi
257
+
258
+ selected="$(detect_terminal_language)"
259
+ if [[ -n "$selected" ]]; then
260
+ printf '%s\n' "$selected"
261
+ return
262
+ fi
263
+
264
+ selected="$(get_env_value TV_AGENT_LANGUAGE || true)"
265
+ selected="$(normalize_language_code "$selected")"
266
+ printf '%s\n' "${selected:-en}"
267
+ }
268
+
269
+ ensure_language_default() {
270
+ local selected
271
+ selected="$(default_language_code)"
272
+ [[ -z "$selected" ]] && selected="en"
273
+ upsert_env "TV_AGENT_LANGUAGE" "$selected"
274
+ export TV_AGENT_LANGUAGE="$selected"
275
+ }
276
+
277
+ prompt_language() {
278
+ local default answer selected attempts timed_out
279
+ default="$(default_language_code)"
280
+ [[ -z "$default" ]] && default="en"
281
+
282
+ attempts=0
283
+ while true; do
284
+ answer=""
285
+ timed_out=0
286
+ if [ -r /dev/tty ] && [ -w /dev/tty ]; then
287
+ printf 'Preferred agent output language (en English, zhtw Traditional Chinese, zhcn Simplified Chinese, jp Japanese, ko Korean) [%s]: ' "$default" > /dev/tty
288
+ if ! IFS= read -r -t "$INPUT_TIMEOUT_SECONDS" answer < /dev/tty; then
289
+ timed_out=1
290
+ printf '\n' > /dev/tty
291
+ fi
292
+ else
293
+ if ! read -r -t "$INPUT_TIMEOUT_SECONDS" -p "Preferred agent output language (en/zhtw/zhcn/jp/ko) [$default]: " answer; then
294
+ timed_out=1
295
+ printf '\n'
296
+ fi
297
+ fi
298
+
299
+ if (( timed_out )); then
300
+ ((attempts+=1))
301
+ if (( attempts >= MAX_NO_INPUT_ATTEMPTS )); then
302
+ abort_install_no_input "language selection"
303
+ fi
304
+ warn_no_input_retry "language selection" "$attempts"
305
+ continue
306
+ fi
307
+ break
308
+ done
309
+
310
+ selected="$(normalize_language_code "${answer:-$default}")"
311
+ [[ -z "$selected" ]] && selected="en"
312
+ upsert_env "TV_AGENT_LANGUAGE" "$selected"
313
+ export TV_AGENT_LANGUAGE="$selected"
314
+ log " → Language: $selected ($(language_label "$selected"))"
315
+ }
316
+
317
+ _PLATFORM_KEYS=(pi claude-desktop claude-code codex gemini cursor)
318
+ _PLATFORM_LABELS=(
319
+ "Pi"
320
+ "Claude Desktop"
321
+ "Claude Code"
322
+ "Codex / OpenCode / Aider"
323
+ "Gemini"
324
+ "Cursor"
325
+ )
326
+
327
+ # Arrow-key multi-select menu. Pure bash, no external deps. Works in bash 3.2+.
328
+ # Controls: ↑/↓ or k/j to move, SPACE to toggle, a = all, n = none, ENTER to confirm, q = quit.
329
+ # Menu drawing goes to stderr (>&2) so it survives $(...) command substitution
330
+ # which only captures stdout. Stdin (`read` builtin) is the terminal.
331
+ # Falls back to "all" if stdin is not a TTY.
332
+ # Sets the global _MENU_RESULT to a space-separated list of selected keys, or "skip".
333
+ arrow_multi_select() {
334
+ local header="$1"; shift
335
+ # IMPORTANT: bash 3.2 doesn't reliably treat `local -a a=() b=() c=()` as
336
+ # three array declarations on one line, so declare each on its own line.
337
+ local -a keys
338
+ local -a labels
339
+ local -a checked
340
+ keys=()
341
+ labels=()
342
+ checked=()
343
+
344
+ while (( $# > 0 )); do
345
+ keys+=("${1%%|*}")
346
+ labels+=("${1#*|}")
347
+ checked+=(0)
348
+ shift
349
+ done
350
+ local n=${#keys[@]}
351
+ local cursor=0
352
+
353
+ # Start with nothing checked. SPACE toggles one or more items; pressing
354
+ # ENTER always selects the highlighted item when nothing is checked.
355
+ local i
356
+ for ((i=0; i<n; i++)); do checked[$i]=0; done
357
+
358
+ # Non-TTY → signal fallback by leaving _MENU_RESULT empty.
359
+ if ! is_tty || [ ! -r /dev/tty ] || [ ! -w /dev/tty ]; then
360
+ _MENU_RESULT=""
361
+ return 1
362
+ fi
363
+
364
+ # Bind the menu directly to the controlling terminal. In `make` recipes and
365
+ # command substitutions, stdin/stderr can be captured or partially detached;
366
+ # /dev/tty keeps the arrow UI visible and makes reads reliable.
367
+ if ! exec 3</dev/tty 4>/dev/tty; then
368
+ _MENU_RESULT=""
369
+ return 1
370
+ fi
371
+
372
+ # Probe: can we actually save/restore the TTY state? Some terminal/shell
373
+ # combinations report -t 0 as true but then error on stty operations
374
+ # (e.g. read returning "Input/output error" on FD 0). Detect that here
375
+ # and bail out before drawing anything.
376
+ local _stty_saved
377
+ if ! _stty_saved="$(stty -g <&3 2>/dev/null)"; then
378
+ exec 3<&- 4>&- 2>/dev/null || true
379
+ _MENU_RESULT=""
380
+ return 1
381
+ fi
382
+ if ! stty -echo -icanon time 0 min 1 <&3 2>/dev/null; then
383
+ exec 3<&- 4>&- 2>/dev/null || true
384
+ _MENU_RESULT=""
385
+ return 1
386
+ fi
387
+
388
+ # Disable set -e for the menu's body so a single read failure doesn't kill
389
+ # the whole wizard — we want to fall back to the numeric prompt instead.
390
+ set +e
391
+
392
+ # Keep menu input/output bound to the controlling terminal so the UI paints
393
+ # immediately even when launched from `make`.
394
+ _arrow_menu_cleanup() {
395
+ # RETURN traps persist in bash until explicitly cleared. Clear them first
396
+ # so cleanup only runs for this menu, not for later function returns.
397
+ trap - RETURN
398
+ trap - INT TERM
399
+ if [[ -n "${_stty_saved:-}" ]]; then
400
+ stty "$_stty_saved" < /dev/tty 2>/dev/null || true
401
+ fi
402
+ printf '\033[?25h' > /dev/tty 2>/dev/null || true
403
+ # Close only our menu fds. Do not use `exec ... 2>/dev/null` here; with
404
+ # exec and no command, that would permanently redirect the script stderr.
405
+ exec 3<&- || true
406
+ exec 4>&- || true
407
+ set -e
408
+ }
409
+ trap '_arrow_menu_cleanup' RETURN
410
+ trap '_arrow_menu_cleanup; exit 130' INT TERM
411
+
412
+ printf '\033[?25l' >&4
413
+
414
+ # Do not try to drain pre-buffered keystrokes here. On macOS/bash 3.2 under
415
+ # `make`, `read -n1` can block even after `stty min 0`, which makes the
416
+ # installer appear to hang before the menu is drawn.
417
+ stty time 0 min 1 <&3 2>/dev/null
418
+
419
+ printf '\n%s\n' "$header" >&4
420
+ printf ' ↑/↓ or j/k to move · SPACE to toggle · a = all · n = none · q = skip · ENTER = confirm/current\n\n' >&4
421
+
422
+ local marker prefix key rest first_draw idle_attempts
423
+ first_draw=1
424
+ idle_attempts=0
425
+ while true; do
426
+ if (( first_draw )); then
427
+ first_draw=0
428
+ else
429
+ # Redraw in-place. CSI save/restore cursor is not reliable in every
430
+ # terminal/tmux/make combination, so move back over the fixed-height
431
+ # list and clear from there before repainting.
432
+ printf '\033[%dA' "$n" >&4
433
+ fi
434
+ printf '\033[J' >&4
435
+ for ((i=0; i<n; i++)); do
436
+ marker="[ ]"
437
+ [[ "${checked[$i]}" == "1" ]] && marker="[x]"
438
+ prefix=" "
439
+ [[ $i -eq $cursor ]] && prefix=" ❯ "
440
+ printf '\r\033[2K%s%s %s\n' "$prefix" "$marker" "${labels[$i]}" >&4
441
+ done
442
+
443
+ key=""
444
+ rest=""
445
+ # If the user provides no input for the timeout window, retry up to the
446
+ # global limit, then abort and clean up installed artifacts. Other read
447
+ # failures still fall back to the numeric prompt.
448
+ if ! IFS= read -rsn1 -t "$INPUT_TIMEOUT_SECONDS" key <&3 2>/dev/null; then
449
+ ((idle_attempts+=1))
450
+ if (( idle_attempts >= MAX_NO_INPUT_ATTEMPTS )); then
451
+ _arrow_menu_cleanup
452
+ abort_install_no_input "platform selection"
453
+ fi
454
+ printf '\nWARN: No input detected for platform selection (%d/%d). Waiting again...\n' \
455
+ "$idle_attempts" "$MAX_NO_INPUT_ATTEMPTS" >&4
456
+ first_draw=1
457
+ continue
458
+ fi
459
+ if [[ "$key" == $'\004' ]]; then
460
+ ((idle_attempts+=1))
461
+ if (( idle_attempts >= MAX_NO_INPUT_ATTEMPTS )); then
462
+ _arrow_menu_cleanup
463
+ abort_install_no_input "platform selection"
464
+ fi
465
+ printf '\nWARN: No input detected for platform selection (%d/%d). Waiting again...\n' \
466
+ "$idle_attempts" "$MAX_NO_INPUT_ATTEMPTS" >&4
467
+ first_draw=1
468
+ continue
469
+ fi
470
+ idle_attempts=0
471
+ case "$key" in
472
+ $'\x1b')
473
+ stty time 1 min 0 <&3 2>/dev/null
474
+ IFS= read -rsn2 rest <&3 2>/dev/null || rest=""
475
+ stty time 0 min 1 <&3 2>/dev/null
476
+ case "$rest" in
477
+ "[A") (( cursor > 0 )) && ((cursor--)) ;;
478
+ "[B") (( cursor < n-1 )) && ((cursor++)) ;;
479
+ esac
480
+ ;;
481
+ k|K) (( cursor > 0 )) && ((cursor--)) ;;
482
+ j|J) (( cursor < n-1 )) && ((cursor++)) ;;
483
+ ' ') checked[$cursor]=$(( 1 - checked[$cursor] )) ;;
484
+ a|A) for ((i=0; i<n; i++)); do checked[$i]=1; done ;;
485
+ n|N) for ((i=0; i<n; i++)); do checked[$i]=0; done ;;
486
+ q|Q) _MENU_RESULT="skip"; printf '\n' >&4; return 0 ;;
487
+ ''|$'\n'|$'\r') break ;;
488
+ esac
489
+ done
490
+
491
+ local out=""
492
+ local any=0
493
+ for ((i=0; i<n; i++)); do
494
+ if [[ "${checked[$i]}" == "1" ]]; then
495
+ out+="${keys[$i]} "
496
+ any=1
497
+ fi
498
+ done
499
+ printf '\n' >&4
500
+ if (( ! any )); then
501
+ _MENU_RESULT="${keys[$cursor]}"
502
+ else
503
+ _MENU_RESULT="${out% }"
504
+ fi
505
+ }
506
+
507
+ select_platforms_numeric_fallback() {
508
+ # Plain prompt that reads/writes directly from the controlling terminal.
509
+ # This avoids bash `select` + command-substitution visibility issues under
510
+ # `make`, and works when the arrow-key UI is not supported.
511
+ local tty_in="/dev/tty"
512
+ local tty_out="/dev/tty"
513
+ if [ ! -r "$tty_in" ] || [ ! -w "$tty_out" ]; then
514
+ _PLATFORM_RESULT="all"
515
+ return
516
+ fi
517
+
518
+ local idle_attempts=0
519
+ while true; do
520
+ cat > "$tty_out" <<'MENU'
521
+
522
+ Select agent platforms to configure:
523
+ 1) All — Pi, Claude Desktop, Claude Code, Codex, Gemini, Cursor (recommended)
524
+ 2) Claude Code only
525
+ 3) Claude Desktop only
526
+ 4) Pi only
527
+ 5) Codex / OpenCode / Aider only
528
+ 6) Gemini only
529
+ 7) Cursor only
530
+ 8) Custom — type a comma-separated list
531
+ 9) Skip — don't configure any agents now
532
+
533
+ MENU
534
+ printf 'Your choice (1-9, default 1): ' > "$tty_out"
535
+ local reply=""
536
+ if ! IFS= read -r -t "$INPUT_TIMEOUT_SECONDS" reply < "$tty_in"; then
537
+ ((idle_attempts+=1))
538
+ if (( idle_attempts >= MAX_NO_INPUT_ATTEMPTS )); then
539
+ abort_install_no_input "platform selection"
540
+ fi
541
+ warn_no_input_retry "platform selection" "$idle_attempts"
542
+ continue
543
+ fi
544
+ idle_attempts=0
545
+ reply="${reply:-1}"
546
+
547
+ case "$reply" in
548
+ 1) _PLATFORM_RESULT="all"; return ;;
549
+ 2) _PLATFORM_RESULT="claude-code"; return ;;
550
+ 3) _PLATFORM_RESULT="claude-desktop"; return ;;
551
+ 4) _PLATFORM_RESULT="pi"; return ;;
552
+ 5) _PLATFORM_RESULT="codex"; return ;;
553
+ 6) _PLATFORM_RESULT="gemini"; return ;;
554
+ 7) _PLATFORM_RESULT="cursor"; return ;;
555
+ 8)
556
+ local custom=""
557
+ local custom_attempts=0
558
+ while true; do
559
+ printf '\nEnter platforms separated by commas.\n' > "$tty_out"
560
+ printf 'Available: pi, claude-desktop, claude-code, codex, gemini, cursor\n' > "$tty_out"
561
+ printf 'Platforms: ' > "$tty_out"
562
+ if IFS= read -r -t "$INPUT_TIMEOUT_SECONDS" custom < "$tty_in"; then
563
+ break
564
+ fi
565
+ ((custom_attempts+=1))
566
+ if (( custom_attempts >= MAX_NO_INPUT_ATTEMPTS )); then
567
+ abort_install_no_input "custom platform entry"
568
+ fi
569
+ warn_no_input_retry "custom platform entry" "$custom_attempts"
570
+ done
571
+ # Normalize: trim whitespace, allow space-separated input too.
572
+ custom="${custom//,/ }"
573
+ local token token_lc out=""
574
+ for token in $custom; do
575
+ token_lc="$(printf '%s' "$token" | tr '[:upper:]' '[:lower:]')"
576
+ case "$token_lc" in
577
+ pi|claude-desktop|claude-code|codex|gemini|cursor|all|skip)
578
+ out+="${out:+,}$token_lc" ;;
579
+ claude|code) out+="${out:+,}claude-code" ;;
580
+ desktop) out+="${out:+,}claude-desktop" ;;
581
+ opencode|aider) out+="${out:+,}codex" ;;
582
+ *) warn "Ignoring unknown platform: $token" ;;
583
+ esac
584
+ done
585
+ _PLATFORM_RESULT="${out:-all}"
586
+ return ;;
587
+ 9) _PLATFORM_RESULT="skip"; return ;;
588
+ *) printf 'Invalid choice: %s — pick a number 1 through 9.\n' "$reply" > "$tty_out" ;;
589
+ esac
590
+ done
591
+ }
592
+
593
+ # Top-level platform selector.
594
+ #
595
+ # Default: arrow-key multi-select menu (↑/↓ or j/k, SPACE to toggle, ENTER).
596
+ # Fallback: TV_INSTALL_MENU=plain make install → numeric prompt
597
+ # (always works; use if the arrow menu misbehaves in your terminal).
598
+ # Sets _PLATFORM_RESULT instead of echoing, so the menu is not hidden inside
599
+ # command substitution.
600
+ select_platforms() {
601
+ _PLATFORM_RESULT=""
602
+ if [[ "$DEFAULT_PLATFORM" != "auto" && -n "$DEFAULT_PLATFORM" ]]; then
603
+ _PLATFORM_RESULT="$DEFAULT_PLATFORM"
604
+ return
605
+ fi
606
+
607
+ # No controlling terminal at all (CI, piped install) → default to all.
608
+ if [ ! -r /dev/tty ] || [ ! -w /dev/tty ]; then
609
+ _PLATFORM_RESULT="all"
610
+ return
611
+ fi
612
+
613
+ if [[ "${TV_INSTALL_MENU:-arrow}" == "plain" ]]; then
614
+ select_platforms_numeric_fallback
615
+ return
616
+ fi
617
+
618
+ local _MENU_RESULT=""
619
+ # `|| true` so `set -e` doesn't kill the wizard if the arrow menu bails out
620
+ # (e.g. terminal doesn't support raw mode). The fallback handles it below.
621
+ arrow_multi_select \
622
+ "Select agent platforms to configure:" \
623
+ "pi|Pi" \
624
+ "claude-desktop|Claude Desktop" \
625
+ "claude-code|Claude Code" \
626
+ "codex|Codex / OpenCode / Aider" \
627
+ "gemini|Gemini" \
628
+ "cursor|Cursor" || true
629
+
630
+ if [[ -z "$_MENU_RESULT" ]]; then
631
+ warn "Arrow-key menu unavailable in this terminal — switching to numeric prompt."
632
+ warn "(Tip: TV_INSTALL_MENU=plain make install skips the arrow menu directly.)"
633
+ select_platforms_numeric_fallback
634
+ return
635
+ fi
636
+
637
+ if [[ "$_MENU_RESULT" == "skip" ]]; then
638
+ _PLATFORM_RESULT="skip"
639
+ return
640
+ fi
641
+ _PLATFORM_RESULT="${_MENU_RESULT// /,}"
642
+ }
643
+
644
+ tv_session_ok() {
645
+ command -v conda >/dev/null 2>&1 || return 1
646
+ local out
647
+ out="$(conda run -n "$CONDA_ENV" --no-capture-output python -m tv_indicator.cli.main browser session-check 2>/dev/null || true)"
648
+ grep -Eiq 'signed_in:[[:space:]]*true|signed_in[[:space:]]+true' <<<"$out"
649
+ }
650
+
651
+ print_tv_login_status() {
652
+ if ! command -v conda >/dev/null 2>&1; then
653
+ warn "conda not found; cannot check TradingView login status."
654
+ return 0
655
+ fi
656
+
657
+ if ! conda run -n "$CONDA_ENV" --live-stream --no-capture-output \
658
+ python -m tv_indicator.cli.main browser session-check; then
659
+ warn "TradingView login status check failed. Retry with 'make session-check'."
660
+ fi
661
+ }
662
+
663
+ ensure_playwright_chromium() {
664
+ # Returns 0 if Chromium is installed for Playwright, 1 otherwise.
665
+ conda run -n "$CONDA_ENV" --no-capture-output python -c \
666
+ "import sys; from pathlib import Path; from playwright._impl._driver import compute_driver_executable; \
667
+ sys.exit(0 if any(Path(p).exists() for p in (Path.home()/'Library/Caches/ms-playwright', Path.home()/'.cache/ms-playwright')) else 1)" \
668
+ >/dev/null 2>&1
669
+ }
670
+
671
+ install_playwright_chromium_if_missing() {
672
+ if ensure_playwright_chromium; then
673
+ return 0
674
+ fi
675
+ warn "Playwright Chromium not found. Installing it now (one-time, ~150 MB download)..."
676
+ if conda run -n "$CONDA_ENV" --live-stream --no-capture-output python -m playwright install chromium; then
677
+ log "Chromium installed ✓"
678
+ return 0
679
+ else
680
+ warn "Could not install Chromium automatically. Run 'make install-playwright' and try login again."
681
+ return 1
682
+ fi
683
+ }
684
+
685
+ run_tv_login() {
686
+ if ! command -v conda >/dev/null 2>&1; then
687
+ warn "conda not found; skipping TradingView browser login."
688
+ return 0
689
+ fi
690
+
691
+ if tv_session_ok; then
692
+ log " → TradingView session already signed in ✓; skipping browser login."
693
+ return 0
694
+ fi
695
+
696
+ if ! install_playwright_chromium_if_missing; then
697
+ return 0
698
+ fi
699
+
700
+ log ""
701
+ log " Opening TradingView login in Playwright."
702
+ log " → A Chromium window will open. Complete the login (or 2FA) in that window."
703
+ log " → The window closes automatically once login is detected."
704
+ log " → If the window doesn't appear, check behind other apps (macOS Mission Control)."
705
+ log ""
706
+
707
+ # TV_BROWSER_HEADLESS may be set to "true" in .env or from a parent shell —
708
+ # the login flow forces a visible window regardless.
709
+ if env -u TV_BROWSER_HEADLESS \
710
+ conda run -n "$CONDA_ENV" --live-stream --no-capture-output \
711
+ env TV_BROWSER_HEADLESS=false python -m tv_indicator.cli.main browser login; then
712
+ log " → Login captured. Verifying session..."
713
+ else
714
+ warn " → Login did not complete. You can run 'make login' later."
715
+ return 0
716
+ fi
717
+
718
+ conda run -n "$CONDA_ENV" --live-stream --no-capture-output python -m tv_indicator.cli.main browser session-check || \
719
+ warn " → Session check failed. Retry with 'make login' && 'make session-check'."
720
+ }
721
+
722
+ run_agent_import() {
723
+ local platforms="$1"
724
+ [[ "$platforms" == "skip" ]] && { log "Skipping agent import."; return 0; }
725
+ if [[ -x tools/progress.sh ]]; then
726
+ tools/progress.sh \
727
+ --success "Agent tools imported for: $platforms ✓" \
728
+ --failure "Agent tool import failed ✗" \
729
+ "Importing agent tools for: $platforms" \
730
+ -- bash tools/import_agent_tools.sh --platform "$platforms"
731
+ else
732
+ log "Importing agent tools for: $platforms"
733
+ bash tools/import_agent_tools.sh --platform "$platforms"
734
+ fi
735
+ }
736
+
737
+ _add_unique_platform() {
738
+ local current="$1"
739
+ local item="$2"
740
+ case " $current " in
741
+ *" $item "*) printf '%s\n' "$current" ;;
742
+ *) printf '%s\n' "${current:+$current }$item" ;;
743
+ esac
744
+ }
745
+
746
+ _expand_platforms_for_examples() {
747
+ local raw="$1"
748
+ raw="${raw//,/ }"
749
+ local selected=""
750
+ local token mapped
751
+ for token in $raw; do
752
+ case "$token" in
753
+ all|auto)
754
+ for mapped in pi claude-desktop claude-code codex gemini cursor; do
755
+ selected="$(_add_unique_platform "$selected" "$mapped")"
756
+ done
757
+ ;;
758
+ pi|claude-desktop|claude-code|codex|gemini|cursor)
759
+ selected="$(_add_unique_platform "$selected" "$token")" ;;
760
+ claude) selected="$(_add_unique_platform "$selected" "claude-code")" ;;
761
+ desktop) selected="$(_add_unique_platform "$selected" "claude-desktop")" ;;
762
+ opencode|aider) selected="$(_add_unique_platform "$selected" "codex")" ;;
763
+ skip|"") ;;
764
+ esac
765
+ done
766
+ printf '%s\n' "$selected"
767
+ }
768
+
769
+ print_usage_examples() {
770
+ local platforms="$1"
771
+ log ""
772
+ log "Usage examples for selected platform(s):"
773
+
774
+ if [[ "$platforms" == "skip" ]]; then
775
+ log " No agent platform was configured. Import later with:"
776
+ log " make agent-import PLATFORM=pi # or: all, claude-code, cursor, ..."
777
+ return
778
+ fi
779
+
780
+ local selected
781
+ selected="$(_expand_platforms_for_examples "$platforms")"
782
+ if [[ -z "$selected" ]]; then
783
+ log " Run: make agent-import PLATFORM=pi # or: all, claude-code, cursor, ..."
784
+ return
785
+ fi
786
+
787
+ local p
788
+ for p in $selected; do
789
+ case "$p" in
790
+ pi)
791
+ log " Pi:"
792
+ log " /reload # inside Pi, or restart Pi"
793
+ log " Ask: analyze NASDAQ:AAPL"
794
+ ;;
795
+ claude-desktop)
796
+ log " Claude Desktop:"
797
+ log " Quit and reopen Claude Desktop"
798
+ log " Ask: analyze NASDAQ:AAPL"
799
+ ;;
800
+ claude-code)
801
+ log " Claude Code:"
802
+ log " claude # run from this repo root"
803
+ log " Ask: analyze NASDAQ:AAPL"
804
+ ;;
805
+ codex)
806
+ log " Codex / OpenCode / Aider:"
807
+ log " Restart the agent/client, then open this repo"
808
+ log " Ask: analyze NASDAQ:AAPL"
809
+ ;;
810
+ gemini)
811
+ log " Gemini CLI:"
812
+ log " gemini # run from this repo root"
813
+ log " Ask: analyze NASDAQ:AAPL"
814
+ ;;
815
+ cursor)
816
+ log " Cursor:"
817
+ log " Reload Window, then check Cursor MCP settings"
818
+ log " Ask: analyze NASDAQ:AAPL"
819
+ ;;
820
+ esac
821
+ done
822
+
823
+ log " CLI checks:"
824
+ log " make status"
825
+ log " make session-check"
826
+ log " make model-status"
827
+ log " make model-setup # verify model API env"
828
+ log " make model-forecast SYMBOL=NASDAQ:TSLA MODEL_JSON=1"
829
+ log " make ohlcv SYMBOL=NASDAQ:AAPL BARS=50"
830
+ log " Analysis artifacts:"
831
+ log " temp/{YYYYMMDD_HHMMSS}/{TARGET_ID}/analyzed_result.md"
832
+ }
833
+
834
+ noninteractive_setup() {
835
+ ensure_env_file
836
+ ensure_language_default
837
+ local platform="${TV_INDICATOR_AGENT_PLATFORM:-$DEFAULT_PLATFORM}"
838
+ # In non-interactive package/CI installs, avoid unexpectedly touching global
839
+ # Claude Desktop config. Users can override with TV_INDICATOR_AGENT_PLATFORM=all.
840
+ [[ "$platform" == "auto" ]] && platform="pi"
841
+ run_agent_import "$platform"
842
+ }
843
+
844
+ main() {
845
+ if [[ "${TV_INDICATOR_SETUP:-1}" == "0" ]]; then
846
+ ensure_env_file
847
+ ensure_language_default
848
+ log "TV_INDICATOR_SETUP=0; skipping interactive agent setup."
849
+ return 0
850
+ fi
851
+
852
+ if ! is_tty; then
853
+ log "Non-interactive shell detected; running default agent import only."
854
+ noninteractive_setup
855
+ return 0
856
+ fi
857
+
858
+ log ""
859
+ log "=== Indicator — interactive setup ==="
860
+ ensure_env_file
861
+
862
+ log ""
863
+ log "Step 1/4 — preferred agent output language:"
864
+ prompt_language
865
+
866
+ log ""
867
+ log "Step 2/4 — choose which agent platforms to configure:"
868
+ local platforms
869
+ select_platforms
870
+ platforms="$_PLATFORM_RESULT"
871
+ if [[ "$platforms" == "skip" ]]; then
872
+ log " → Skipping agent import."
873
+ else
874
+ log " → Selected: $platforms"
875
+ fi
876
+
877
+ log ""
878
+ log "Step 3/4 — optional TradingView cookies and AI API keys:"
879
+ if [[ "${TV_INSTALL_PROMPT_ENV:-0}" =~ ^(1|true|TRUE|yes|YES)$ ]]; then
880
+ if ask_yes_no "Store optional API/session values in .env now?" n; then
881
+ log "Press Enter to skip any value. Secret values are hidden while typing."
882
+ prompt_env_value "TV_SESSIONID" "TradingView sessionid cookie" 1
883
+ prompt_env_value "TV_SESSIONID_SIGN" "TradingView sessionid_sign cookie" 1
884
+ prompt_env_value "TV_USERNAME" "TradingView username/label" 0
885
+ prompt_env_value "ANTHROPIC_API_KEY" "Anthropic API key" 1
886
+ prompt_env_value "OPENAI_API_KEY" "OpenAI API key" 1
887
+ prompt_env_value "GEMINI_API_KEY" "Gemini API key" 1
888
+ prompt_env_value "GOOGLE_API_KEY" "Google API key" 1
889
+ else
890
+ log "Skipped .env API/session input."
891
+ fi
892
+ else
893
+ log " → Skipped by default; no API/session answer is required during install."
894
+ log " → Store values later with: python3 skills/api-credential-storage/scripts/upsert_env.py OPENAI_API_KEY --stdin"
895
+ log " → To enable install-time prompts, rerun with: TV_INSTALL_PROMPT_ENV=1 make install"
896
+ fi
897
+
898
+ log ""
899
+ log "Step 4/4 — TradingView browser login:"
900
+ if tv_session_ok; then
901
+ log " → Already signed in ✓; no browser login needed."
902
+ elif ask_yes_no "Run TradingView browser login now?" y; then
903
+ run_tv_login
904
+ else
905
+ log " → Skipped browser login. You can run 'make login' later."
906
+ fi
907
+
908
+ log ""
909
+ log "Importing agent tools..."
910
+ run_agent_import "$platforms"
911
+
912
+ log ""
913
+ log "Setup complete. Restart/reload selected agents to use the generated tools and prompts."
914
+ print_usage_examples "$platforms"
915
+ }
916
+
917
+ main