lacy 1.8.11 → 1.8.13

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 (109) hide show
  1. package/.claude/settings.local.json +26 -0
  2. package/.github/FUNDING.yml +3 -0
  3. package/.github/ISSUE_TEMPLATE/bug_report.yml +49 -0
  4. package/.github/ISSUE_TEMPLATE/config.yml +5 -0
  5. package/.github/ISSUE_TEMPLATE/feature_request.yml +28 -0
  6. package/.github/PULL_REQUEST_TEMPLATE.md +17 -0
  7. package/.github/SECURITY.md +32 -0
  8. package/.github/assets/logo-horizontal-dark.png +0 -0
  9. package/.github/assets/logo-horizontal-dark.svg +17 -0
  10. package/.github/assets/logo-horizontal.png +0 -0
  11. package/.github/assets/logo-horizontal.svg +17 -0
  12. package/.github/assets/logo.png +0 -0
  13. package/.github/assets/logo.svg +12 -0
  14. package/.github/assets/social-preview.png +0 -0
  15. package/.github/assets/social-preview.svg +50 -0
  16. package/.github/dependabot.yml +21 -0
  17. package/.github/workflows/ci.yml +80 -0
  18. package/.github/workflows/dependabot-auto-merge.yml +32 -0
  19. package/CHANGELOG.md +366 -0
  20. package/CLAUDE.md +340 -0
  21. package/CONTRIBUTING.md +141 -0
  22. package/LICENSE +110 -0
  23. package/README.md +201 -31
  24. package/RELEASING.md +148 -0
  25. package/STYLE.md +202 -0
  26. package/assets/hero.jpeg +0 -0
  27. package/assets/mode-indicators.jpeg +0 -0
  28. package/assets/real-time-indicator.jpeg +0 -0
  29. package/assets/supported-tools.jpeg +0 -0
  30. package/bin/lacy +1028 -0
  31. package/docs/ADDING-BACKENDS.md +124 -0
  32. package/docs/DEVTO-ARTICLE.md +94 -0
  33. package/docs/DOCS.md +68 -0
  34. package/docs/GROWTH-STRATEGY.md +119 -0
  35. package/docs/HN-RESPONSES.md +122 -0
  36. package/docs/LAUNCH-COPY-FINAL.md +105 -0
  37. package/docs/MARKETING.md +411 -0
  38. package/docs/NATURAL_LANGUAGE_DETECTION.md +204 -0
  39. package/docs/UGC_VIDEO_SCRIPT.md +114 -0
  40. package/docs/articles/devto-how-i-made-my-terminal-understand-english.md +117 -0
  41. package/docs/demo-color-transition.gif +0 -0
  42. package/docs/demo-full.gif +0 -0
  43. package/docs/demo-indicator.gif +0 -0
  44. package/docs/launch-thread-may6.sh +158 -0
  45. package/docs/videos/README.md +189 -0
  46. package/docs/videos/generate_frames.py +510 -0
  47. package/docs/videos/generate_frames_v2.py +729 -0
  48. package/docs/videos/generate_short.py +328 -0
  49. package/docs/videos/generate_short_v2.py +526 -0
  50. package/docs/videos/lacy-shell-demo-v2.mp4 +0 -0
  51. package/docs/videos/lacy-shell-demo.mp4 +0 -0
  52. package/docs/videos/lacy-shell-short-v2.mp4 +0 -0
  53. package/docs/videos/lacy-shell-short.mp4 +0 -0
  54. package/install.sh +1009 -0
  55. package/lacy.plugin.bash +75 -0
  56. package/lacy.plugin.fish +43 -0
  57. package/lacy.plugin.zsh +65 -0
  58. package/lib/animations.zsh +3 -0
  59. package/lib/bash/completions.bash +40 -0
  60. package/lib/bash/execute.bash +233 -0
  61. package/lib/bash/init.bash +40 -0
  62. package/lib/bash/keybindings.bash +134 -0
  63. package/lib/bash/prompt.bash +85 -0
  64. package/lib/commands/info.sh +25 -0
  65. package/lib/config.zsh +3 -0
  66. package/lib/constants.zsh +3 -0
  67. package/lib/core/animations.sh +271 -0
  68. package/lib/core/commands.sh +297 -0
  69. package/lib/core/config.sh +340 -0
  70. package/lib/core/constants.sh +366 -0
  71. package/lib/core/context.sh +260 -0
  72. package/lib/core/detection.sh +417 -0
  73. package/lib/core/mcp.sh +741 -0
  74. package/lib/core/modes.sh +123 -0
  75. package/lib/core/preheat.sh +496 -0
  76. package/lib/core/spinner.sh +174 -0
  77. package/lib/core/telemetry.sh +99 -0
  78. package/lib/detection.zsh +3 -0
  79. package/lib/execute.zsh +3 -0
  80. package/lib/fish/config.fish +66 -0
  81. package/lib/fish/detection.fish +90 -0
  82. package/lib/fish/execute.fish +105 -0
  83. package/lib/fish/keybindings.fish +42 -0
  84. package/lib/fish/prompt.fish +30 -0
  85. package/lib/keybindings.zsh +3 -0
  86. package/lib/mcp.zsh +3 -0
  87. package/lib/modes.zsh +3 -0
  88. package/lib/preheat.zsh +3 -0
  89. package/lib/prompt.zsh +3 -0
  90. package/lib/spinner.zsh +3 -0
  91. package/lib/zsh/completions.zsh +60 -0
  92. package/lib/zsh/execute.zsh +294 -0
  93. package/lib/zsh/init.zsh +26 -0
  94. package/lib/zsh/keybindings.zsh +551 -0
  95. package/lib/zsh/prompt.zsh +90 -0
  96. package/package.json +42 -27
  97. package/packages/lacy/README.md +61 -0
  98. package/packages/lacy/commands/info.sh +25 -0
  99. package/{index.mjs → packages/lacy/index.mjs} +247 -20
  100. package/packages/lacy/package-lock.json +71 -0
  101. package/packages/lacy/package.json +42 -0
  102. package/script/release.ts +487 -0
  103. package/squirrel.toml +36 -0
  104. package/tests/test_bash.bash +163 -0
  105. package/tests/test_core.sh +607 -0
  106. package/tests/test_gemini.sh +119 -0
  107. package/tests/test_gemini_mcp.sh +126 -0
  108. package/tests/test_preheat_server.zsh +446 -0
  109. package/uninstall.sh +52 -0
@@ -0,0 +1,366 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # Lacy Shell Constants — shared across Bash 4+ and ZSH
4
+ # Sourced by lib/zsh/init.zsh and lib/bash/init.bash
5
+
6
+ # === Runtime State ===
7
+ LACY_SHELL_ENABLED=true
8
+ LACY_SHELL_DEFER_QUIT=false
9
+
10
+ # === Shell Type (set by entry point before sourcing) ===
11
+ # LACY_SHELL_TYPE — "zsh" or "bash"
12
+ # _LACY_ARR_OFFSET — 1 for ZSH (1-based), 0 for Bash (0-based)
13
+
14
+ # === Paths ===
15
+ : "${LACY_SHELL_HOME:="${HOME}/.lacy"}"
16
+
17
+ : "${LACY_SHELL_CONFIG_FILE:="${LACY_SHELL_HOME}/config.yaml"}"
18
+
19
+ : "${LACY_SHELL_MODE_FILE:="${LACY_SHELL_HOME}/current_mode"}"
20
+
21
+ : "${LACY_SHELL_CONVERSATION_FILE:="${LACY_SHELL_HOME}/conversation.log"}"
22
+
23
+ : "${LACY_SHELL_MCP_DIR:="${LACY_SHELL_HOME}/mcp"}"
24
+
25
+ # === Defaults ===
26
+ : "${LACY_SHELL_DEFAULT_MODE:="auto"}"
27
+
28
+ : "${LACY_SHELL_DEFAULT_INDICATOR_STYLE:="top"}"
29
+
30
+ : "${LACY_SHELL_DEFAULT_CONFIDENCE_THRESHOLD:="0.7"}"
31
+
32
+ : "${LACY_SHELL_DEFAULT_PROVIDER:="openai"}"
33
+
34
+ : "${LACY_SHELL_DEFAULT_MODEL:="gpt-4o-mini"}"
35
+
36
+ # === Timeouts (in milliseconds) ===
37
+ : "${LACY_SHELL_EXIT_TIMEOUT_MS:=1000}"
38
+
39
+ : "${LACY_SHELL_EXIT_TIMEOUT_SEC:="1.0"}"
40
+
41
+ : "${LACY_SHELL_MESSAGE_DURATION_SEC:="1.0"}"
42
+
43
+ : "${LACY_SHELL_MCP_TIMEOUT_SEC:=5}"
44
+
45
+ # === UI ===
46
+ : "${LACY_SHELL_TOP_BAR_HEIGHT:=1}"
47
+
48
+ # === Preheat ===
49
+ : "${LACY_PREHEAT_EAGER:="false"}"
50
+ : "${LACY_PREHEAT_SERVER_PORT:="4096"}"
51
+
52
+ # === Colors (256-color palette) ===
53
+ LACY_COLOR_SHELL=34 # Green - shell commands
54
+ LACY_COLOR_AGENT=200 # Magenta - agent queries
55
+ LACY_COLOR_AUTO=75 # Blue - auto mode
56
+ LACY_COLOR_NEUTRAL=238 # Dark gray - neutral/dim
57
+ LACY_COLOR_SHIMMER=(255 219 213 200 141) # Spinner shimmer gradient
58
+
59
+ # === Detection ===
60
+ # (LACY_HARD_AGENT_INDICATORS removed — replaced by LACY_AGENT_WORDS below)
61
+
62
+ # Shell reserved words — pass `command -v` but are never valid standalone commands.
63
+ # Used by Layer 1 of natural language detection (see docs/NATURAL_LANGUAGE_DETECTION.md).
64
+ LACY_SHELL_RESERVED_WORDS=("do" "done" "then" "else" "elif" "fi" "esac" "in" "select" "function" "coproc" "{" "}" "!" "[[")
65
+
66
+ # Agent words — common English words that always route to agent, even as
67
+ # single-word input. Some (yes, nice, cancel) exist as real commands but are
68
+ # almost never typed standalone intentionally.
69
+ # Kept in sync with lash plugin/shell-mode/command-check.ts AGENT_WORDS.
70
+ LACY_AGENT_WORDS=(
71
+ # affirmations
72
+ "yes" "yeah" "yep" "yup" "sure" "ok" "okay" "alright"
73
+ "absolutely" "definitely" "certainly" "indeed" "correct" "right" "exactly"
74
+ "perfect" "agreed" "affirmative" "totally" "clearly" "obviously" "lgtm"
75
+ "roger" "understood" "acknowledged" "gotcha"
76
+ # negations
77
+ "no" "nope" "nah" "never" "wrong" "disagree" "nay" "meh"
78
+ # gratitude
79
+ "thanks" "thank" "thx" "ty" "cheers" "appreciated" "kudos" "congrats" "bravo"
80
+ # reactions
81
+ "great" "good" "nice" "cool" "awesome" "amazing" "wonderful" "brilliant"
82
+ "excellent" "fantastic" "sweet" "neat" "beautiful" "gorgeous" "impressive"
83
+ "incredible" "outstanding" "superb" "marvelous" "magnificent" "stellar"
84
+ "phenomenal" "terrific" "splendid" "fine" "solid" "dope" "sick" "fire" "lit" "rad" "legit"
85
+ "noice" "yay" "hooray" "woah"
86
+ # greetings/closings
87
+ "hey" "hi" "hello" "howdy" "sup" "yo" "bye" "goodbye" "cya" "later"
88
+ # conversational
89
+ "please" "sorry" "pardon" "hmm" "huh" "wow" "whoa" "oops" "ugh" "yikes"
90
+ "damn" "dang" "shoot" "welp" "well" "anyway" "anyways" "regardless"
91
+ "meanwhile" "honestly" "basically" "literally" "actually" "really"
92
+ "seriously" "hopefully" "unfortunately" "apparently"
93
+ "supposedly" "probably" "maybe" "perhaps" "possibly"
94
+ "sheesh" "geez" "oof" "ouch" "bummer" "duh"
95
+ # internet/chat shorthand
96
+ "lol" "haha" "heh" "omg" "wtf" "idk" "fyi" "btw" "imho" "imo" "tbh" "pls" "plz"
97
+ # action/intent
98
+ "stop" "hold" "pause" "cancel" "abort" "skip" "continue" "proceed"
99
+ "next" "again" "redo" "undo" "retry"
100
+ "explain" "elaborate" "clarify" "summarize" "describe" "show" "tell"
101
+ "suggest" "recommend" "consider" "imagine" "suppose"
102
+ # question words
103
+ "why" "how" "what" "when" "where" "who" "which" "whom" "whose"
104
+ "can" "could" "would" "should" "will" "shall" "may" "might" "must"
105
+ "does" "did" "is" "are" "was" "were" "has" "have" "had"
106
+ # programming verbs used conversationally (not valid commands on common systems)
107
+ "refactor" "optimize" "scaffold" "debug" "deploy" "implement"
108
+ "migrate" "lint" "render" "integrate" "iterate"
109
+ "diagnose" "troubleshoot" "hotfix" "rollback" "revert"
110
+ )
111
+
112
+ # Natural language markers — common English words unusual as shell arguments.
113
+ # Used by has_nl_markers (reroute candidates) and Layer 2 detection.
114
+ # Kept in sync with lash plugin/shell-mode/natural-language.ts.
115
+ LACY_NL_MARKERS=(
116
+ # articles/determiners
117
+ "a" "an" "the" "this" "that" "these" "those" "my" "our" "your" "its" "their" "his" "her"
118
+ # pronouns
119
+ "i" "we" "you" "it" "they" "me" "us" "him" "her" "them"
120
+ "myself" "yourself" "itself" "ourselves" "themselves"
121
+ # prepositions
122
+ "to" "of" "about" "with" "from" "for" "into" "through" "between" "after" "before"
123
+ "during" "without" "within" "against" "above" "below" "under" "upon" "across"
124
+ "toward" "towards" "beside" "besides" "beyond" "except" "inside" "outside"
125
+ "behind" "near" "among" "along" "around"
126
+ # conjunctions
127
+ "and" "but" "or" "so" "because" "since" "although" "though" "unless" "while"
128
+ "whereas" "whether" "however" "therefore" "moreover" "furthermore"
129
+ "nevertheless" "otherwise" "instead"
130
+ # verbs
131
+ "is" "are" "was" "were" "be" "been" "being" "have" "has" "had" "having"
132
+ "can" "could" "would" "should" "will" "shall" "may" "might" "must" "need" "want"
133
+ "know" "think" "believe" "understand" "remember" "forget" "seem" "appear"
134
+ "look" "feel" "sound" "mean" "try" "keep" "let" "begin" "start" "stop"
135
+ "continue" "happen" "work" "run" "give" "take" "bring" "send" "put" "get"
136
+ "got" "went" "going" "done" "doing" "made" "making"
137
+ # adverbs
138
+ "not" "already" "also" "just" "still" "even" "really" "actually" "probably" "maybe"
139
+ "perhaps" "always" "never" "sometimes" "often" "usually" "only" "very" "too"
140
+ "enough" "quite" "rather" "pretty" "almost" "nearly" "completely" "entirely"
141
+ "definitely" "certainly" "obviously" "clearly" "honestly" "basically" "literally"
142
+ "seriously" "hopefully" "unfortunately" "apparently" "absolutely" "simply" "merely"
143
+ "exactly" "roughly"
144
+ # question words
145
+ "how" "what" "when" "where" "why" "who" "which" "whom" "whose"
146
+ # other common sentence words
147
+ "if" "there" "here" "all" "any" "some" "every" "no" "each"
148
+ "does" "do" "did" "sure" "out" "up" "down" "ahead" "back" "over" "away" "off"
149
+ "on" "now" "then" "again" "once" "twice" "first" "last" "next"
150
+ "new" "old" "same" "other" "another" "both" "either" "neither"
151
+ "much" "many" "more" "most" "less" "least" "few" "several" "own" "such" "whole" "entire"
152
+ # conversational/reactions
153
+ "please" "thanks" "thank" "sorry" "yes" "yeah" "yep" "ok" "okay" "alright"
154
+ "right" "correct" "wrong" "perfect" "great" "good" "nice" "cool" "awesome"
155
+ "amazing" "wonderful" "excellent" "fantastic" "brilliant" "fine"
156
+ "terrible" "horrible" "awful" "bad" "worse" "worst" "better" "best"
157
+ # indefinite pronouns
158
+ "anyone" "someone" "everyone" "anything" "something" "everything"
159
+ "nobody" "nothing" "nowhere" "wherever" "whatever" "whoever" "whenever" "however"
160
+ # common nouns used in conversation
161
+ "way" "thing" "things" "stuff" "part" "place" "point" "fact"
162
+ "issue" "problem" "question" "answer" "idea" "reason" "example"
163
+ "change" "error" "bug" "fix" "feature" "code" "file" "files" "repo" "project" "app" "test" "tests"
164
+ )
165
+
166
+ # Error patterns that suggest the shell tried to interpret natural language.
167
+ # Case-insensitive matching. Used by Layer 2 detection.
168
+ # Kept in sync with lash plugin/shell-mode/natural-language.ts.
169
+ LACY_SHELL_ERROR_PATTERNS=(
170
+ "parse error"
171
+ "syntax error"
172
+ "unexpected token"
173
+ "unexpected end of file"
174
+ "command not found"
175
+ "no such file or directory"
176
+ "invalid option"
177
+ "unrecognized option"
178
+ "illegal option"
179
+ "unknown option"
180
+ "no rule to make target"
181
+ "unknown primary or operator"
182
+ "missing argument to"
183
+ "invalid regular expression"
184
+ "is not a git command"
185
+ "unknown command"
186
+ "no such command"
187
+ )
188
+
189
+ LACY_SHELL_OPERATORS=('|' '&&' '||' ';' '>')
190
+
191
+ # === Interrupt State (shared by ZSH + Bash keybindings) ===
192
+ LACY_SHELL_LAST_INTERRUPT_TIME=0
193
+ LACY_SHELL_QUITTING=false
194
+ LACY_SHELL_INPUT_TYPE=""
195
+
196
+ # === UI ===
197
+ LACY_INDICATOR_CHAR="▌"
198
+ LACY_SPINNER_FRAMES='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'
199
+ : "${LACY_SPINNER_STYLE:="random"}"
200
+ LACY_SPINNER_TEXT='Thinking'
201
+
202
+ # === Tool List (canonical order for detection and display) ===
203
+ LACY_TOOL_LIST=(lash claude opencode gemini codex hermes copilot goose amp aider)
204
+
205
+ # === URLs ===
206
+ LACY_DOCS_URL="https://lacy.sh/docs"
207
+
208
+ # === User-Facing Messages ===
209
+ LACY_MSG_QUIT="Exiting Lacy Shell..."
210
+ LACY_MSG_CTRL_C_HINT="Press Ctrl-C again to quit"
211
+ LACY_MSG_NO_TOOL=" No AI tool found. Install one to get started:"
212
+ LACY_MSG_INSTALL_HINT=" npm install -g lashcode (lash — recommended)"
213
+ LACY_MSG_INSTALL_HINT2=" brew install claude (Claude Code CLI)"
214
+ LACY_MSG_INSTALL_HINT3=" brew install opencode (OpenCode CLI)"
215
+ LACY_MSG_INSTALL_HINT4=" npm install -g @openai/codex"
216
+ LACY_MSG_CONFIGURE_HINT=" Run \`lacy setup\` to configure after installing."
217
+ LACY_MSG_RECOVERY_TOOL=" Try: tool set <name> Switch to a different tool"
218
+ LACY_MSG_RECOVERY_ASK=' ask "your query" Send directly to agent'
219
+ LACY_MSG_RECOVERY_DOCTOR=" lacy doctor Diagnose issues"
220
+ LACY_MSG_CONVERSATION_CLEARED="Conversation history cleared"
221
+ LACY_MSG_NO_CONVERSATION="No conversation history found"
222
+
223
+ # Mode descriptions
224
+ LACY_MSG_MODE_SHELL="SHELL mode - all commands execute directly"
225
+ LACY_MSG_MODE_AGENT="AGENT mode - all input goes to AI"
226
+ LACY_MSG_MODE_AUTO="AUTO mode - smart detection"
227
+ LACY_MSG_MODE_SHELL_SHORT="SHELL mode"
228
+ LACY_MSG_MODE_AGENT_SHORT="AGENT mode"
229
+ LACY_MSG_MODE_AUTO_SHORT="AUTO mode"
230
+ LACY_MSG_COLOR_SHELL="Green = shell command"
231
+ LACY_MSG_COLOR_AGENT="Magenta = agent query"
232
+
233
+ # === Gemini ===
234
+ LACY_GEMINI_CONTEXT="[Context: headless mode (-p). Available tools: grep_search, cli_help, read_file. Shell execution (run_shell_command) is NOT available — answer from context instead. cwd: {cwd}]"
235
+
236
+ # === API URLs ===
237
+ LACY_API_URL_OPENAI="https://api.openai.com/v1/chat/completions"
238
+ LACY_API_URL_ANTHROPIC="https://api.anthropic.com/v1/messages"
239
+
240
+ # === timing (seconds) ===
241
+ LACY_HEALTH_CHECK_TIMEOUT_SYNC=0.3
242
+ LACY_HEALTH_CHECK_TIMEOUT_ASYNC=0.5
243
+
244
+ LACY_SPINNER_FRAME_DELAY=0.05
245
+ LACY_TERMINAL_FLUSH_DELAY=0.02
246
+ LACY_HEALTH_CHECK_ATTEMPTS=30
247
+ LACY_HEALTH_CHECK_INTERVAL=0.1
248
+ LACY_SESSION_CREATE_TIMEOUT=10
249
+ LACY_SESSION_MESSAGE_TIMEOUT=120
250
+
251
+ # === Thresholds ===
252
+ LACY_SIGNAL_EXIT_THRESHOLD=128 # Exit codes >= this are signal-based
253
+
254
+ # === API Models (fallback only) ===
255
+ LACY_API_MODEL_OPENAI="gpt-4o-mini"
256
+ LACY_API_MODEL_ANTHROPIC="claude-3-5-sonnet-20241022"
257
+
258
+ # === Dangerous Commands ===
259
+ LACY_DANGEROUS_PATTERNS=("rm -rf" "sudo rm" "mkfs" "dd if=" ">" "truncate")
260
+
261
+ # === Performance Optimization: Caching ===
262
+ LACY_CONFIG_CACHE_VALID=false
263
+ declare -A LACY_CACHED_CONFIG 2>/dev/null || true
264
+ : "${LACY_SHELL_CONFIG_CACHE_FILE:="${LACY_SHELL_HOME}/.config_cache"}"
265
+
266
+ # Async health check cache
267
+ LACY_PREHEAT_HEALTH_CACHE=false
268
+ LACY_PREHEAT_HEALTH_CHECK_PID=""
269
+ : "${LACY_SHELL_HEALTH_CACHE_FILE:="${LACY_SHELL_HOME}/.health_cache"}"
270
+
271
+ # === Portable Helpers ===
272
+
273
+ # Print colored text — dispatches to shell-appropriate method
274
+ # Usage: lacy_print_color <color_code> <text>
275
+ lacy_print_color() {
276
+ local color="$1"
277
+ shift
278
+ if [[ "$LACY_SHELL_TYPE" == "zsh" ]]; then
279
+ print -P "%F{${color}}$*%f"
280
+ else
281
+ printf '\e[38;5;%dm%s\e[0m\n' "$color" "$*"
282
+ fi
283
+ }
284
+
285
+ # Print colored text without trailing newline
286
+ # Usage: lacy_print_color_n <color_code> <text>
287
+ lacy_print_color_n() {
288
+ local color="$1"
289
+ shift
290
+ if [[ "$LACY_SHELL_TYPE" == "zsh" ]]; then
291
+ print -Pn "%F{${color}}$*%f"
292
+ else
293
+ printf '\e[38;5;%dm%s\e[0m' "$color" "$*"
294
+ fi
295
+ }
296
+
297
+ # Check if a value is in a list (portable array membership)
298
+ # Usage: _lacy_in_list "value" "item1" "item2" ...
299
+ _lacy_in_list() {
300
+ local needle="$1"
301
+ shift
302
+ local item
303
+ for item in "$@"; do
304
+ [[ "$item" == "$needle" ]] && return 0
305
+ done
306
+ return 1
307
+ }
308
+
309
+ # Portable lowercase — works in Bash 4+, ZSH, and falls back to tr
310
+ # Usage: result=$(_lacy_lowercase "STRING")
311
+ _lacy_lowercase() {
312
+ if [[ "$LACY_SHELL_TYPE" == "zsh" ]]; then
313
+ echo "${1:l}"
314
+ elif (( BASH_VERSINFO[0] >= 4 )) 2>/dev/null; then
315
+ echo "${1,,}"
316
+ else
317
+ # Bash 3 fallback (macOS default) — only used in test/core contexts
318
+ echo "$1" | tr '[:upper:]' '[:lower:]'
319
+ fi
320
+ }
321
+
322
+ # Portable pipe status — get exit code of first command in pipeline
323
+ # Must be called immediately after a pipeline
324
+ # Usage: local exit_code=$(_lacy_pipe_status)
325
+ _lacy_pipe_status() {
326
+ if [[ "$LACY_SHELL_TYPE" == "zsh" ]]; then
327
+ echo "${pipestatus[1]}"
328
+ else
329
+ echo "${PIPESTATUS[0]}"
330
+ fi
331
+ }
332
+
333
+ # Portable job control off/on
334
+ _lacy_jobctl_off() {
335
+ if [[ "$LACY_SHELL_TYPE" == "zsh" ]]; then
336
+ [[ -o monitor ]] && LACY_JOBCTL_WAS_SET=1 || LACY_JOBCTL_WAS_SET=""
337
+ setopt NO_MONITOR 2>/dev/null
338
+ else
339
+ LACY_JOBCTL_WAS_SET=""
340
+ case "$-" in *m*) LACY_JOBCTL_WAS_SET=1 ;; esac
341
+ set +m 2>/dev/null
342
+ fi
343
+ }
344
+
345
+ _lacy_jobctl_on() {
346
+ if [[ -n "$LACY_JOBCTL_WAS_SET" ]]; then
347
+ if [[ "$LACY_SHELL_TYPE" == "zsh" ]]; then
348
+ setopt MONITOR 2>/dev/null
349
+ else
350
+ set -m 2>/dev/null
351
+ fi
352
+ LACY_JOBCTL_WAS_SET=""
353
+ fi
354
+ }
355
+
356
+ # Escape a string for safe JSON embedding — handles \, ", and control chars.
357
+ # Usage: escaped=$(_lacy_json_escape_str "$value")
358
+ _lacy_json_escape_str() {
359
+ local s="$1"
360
+ s="${s//\\/\\\\}" # \ → \\
361
+ s="${s//\"/\\\"}" # " → \"
362
+ s="${s//$'\n'/\\n}" # newline → \n
363
+ s="${s//$'\r'/\\r}" # carriage return → \r
364
+ s="${s//$'\t'/\\t}" # tab → \t
365
+ printf '%s' "$s"
366
+ }
@@ -0,0 +1,260 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # Terminal context for agent queries — delta-based, token-efficient
4
+ # Only sends what changed since the last agent query.
5
+ # Shared across Bash 4+ and ZSH.
6
+
7
+ # === State: Delta Tracking ===
8
+ _LACY_CTX_LAST_CWD=""
9
+ _LACY_CTX_LAST_GIT=""
10
+ _LACY_CTX_CMDS_SINCE_QUERY=0
11
+ _LACY_CTX_LAST_EXIT_CODE=0
12
+ _LACY_CTX_REAL_CMD=false
13
+
14
+ # Command ring buffer — explicit array avoids agent queries leaking from fc/history
15
+ _LACY_CTX_CMD_BUFFER=()
16
+ _LACY_CTX_CMD_BUFFER_MAX=10
17
+
18
+ # === State: Terminal Output Capture ===
19
+ _LACY_CTX_TERMINAL_CAPTURE_CMD="" # Detected at load time; empty = unsupported
20
+ _LACY_CTX_OUTPUT_ENABLED=true # Toggled via config (context.output)
21
+ _LACY_CTX_OUTPUT_MAX_LINES=50 # Configurable cap (context.output_lines)
22
+
23
+ # ============================================================================
24
+ # Terminal Detection (called once at source time)
25
+ # ============================================================================
26
+
27
+ # Detect terminal/multiplexer API for screen capture.
28
+ # Sets _LACY_CTX_TERMINAL_CAPTURE_CMD to a command string (or function name),
29
+ # or empty if unsupported. Checked once at source time.
30
+ #
31
+ # Priority: tmux > screen > iTerm2 > Terminal.app
32
+ # Multiplexers are checked first because terminal emulator APIs return wrong
33
+ # content when running inside a multiplexer.
34
+ _lacy_ctx_detect_terminal() {
35
+ _LACY_CTX_TERMINAL_CAPTURE_CMD=""
36
+
37
+ # 1. tmux: native pane capture (takes priority over terminal APIs)
38
+ if [[ -n "${TMUX:-}" ]]; then
39
+ if command -v tmux >/dev/null 2>&1; then
40
+ _LACY_CTX_TERMINAL_CAPTURE_CMD="tmux capture-pane -p"
41
+ return
42
+ fi
43
+ fi
44
+
45
+ # 2. screen: hardcopy to temp file
46
+ if [[ -n "${STY:-}" ]]; then
47
+ if command -v screen >/dev/null 2>&1; then
48
+ _LACY_CTX_TERMINAL_CAPTURE_CMD="_lacy_ctx_screen_capture"
49
+ return
50
+ fi
51
+ fi
52
+
53
+ # 3-4. macOS: AppleScript for iTerm2 and Terminal.app
54
+ if [[ "$(uname -s 2>/dev/null)" == "Darwin" ]]; then
55
+ if [[ "${TERM_PROGRAM:-}" == "iTerm.app" ]]; then
56
+ _LACY_CTX_TERMINAL_CAPTURE_CMD="_lacy_ctx_iterm2_capture"
57
+ return
58
+ fi
59
+ if [[ "${TERM_PROGRAM:-}" == "Apple_Terminal" ]]; then
60
+ _LACY_CTX_TERMINAL_CAPTURE_CMD="_lacy_ctx_terminal_app_capture"
61
+ return
62
+ fi
63
+ fi
64
+ }
65
+
66
+ # ============================================================================
67
+ # Capture Helpers (multi-step captures that can't be a single command)
68
+ # ============================================================================
69
+
70
+ # screen: hardcopy writes to a file, not stdout
71
+ _lacy_ctx_screen_capture() {
72
+ local tmpfile
73
+ tmpfile=$(mktemp) || return
74
+ screen -X hardcopy "$tmpfile" 2>/dev/null || { rm -f "$tmpfile"; return; }
75
+ cat "$tmpfile" 2>/dev/null
76
+ rm -f "$tmpfile" 2>/dev/null
77
+ }
78
+
79
+ # iTerm2: AppleScript to get current session contents
80
+ _lacy_ctx_iterm2_capture() {
81
+ osascript -e 'tell application "iTerm2" to tell current session of current window to get contents' 2>/dev/null
82
+ }
83
+
84
+ # Terminal.app: AppleScript to get current tab contents
85
+ _lacy_ctx_terminal_app_capture() {
86
+ osascript -e 'tell application "Terminal" to get contents of selected tab of front window' 2>/dev/null
87
+ }
88
+
89
+ # ============================================================================
90
+ # Screen Capture (called lazily at query time)
91
+ # ============================================================================
92
+
93
+ # Capture visible terminal screen text, stripped of ANSI escapes.
94
+ # Returns captured text on stdout, or empty if unavailable/disabled.
95
+ _lacy_ctx_capture_screen() {
96
+ [[ "$_LACY_CTX_OUTPUT_ENABLED" != true ]] && return
97
+ [[ -z "$_LACY_CTX_TERMINAL_CAPTURE_CMD" ]] && return
98
+
99
+ local raw_output
100
+ raw_output=$(eval "$_LACY_CTX_TERMINAL_CAPTURE_CMD" 2>/dev/null) || return
101
+
102
+ # Strip ANSI escape sequences (SGR, cursor movement, etc.)
103
+ local cleaned
104
+ cleaned=$(printf '%s\n' "$raw_output" | sed $'s/\x1b\[[0-9;]*[a-zA-Z]//g')
105
+
106
+ # Remove trailing blank lines
107
+ while [[ "$cleaned" == *$'\n' ]]; do
108
+ cleaned="${cleaned%$'\n'}"
109
+ done
110
+
111
+ [[ -z "$cleaned" ]] && return
112
+
113
+ # Truncate from the top, keeping the last N lines (errors are at the bottom)
114
+ local max_lines="${_LACY_CTX_OUTPUT_MAX_LINES:-50}"
115
+ if (( max_lines > 0 )); then
116
+ local line_count
117
+ line_count=$(printf '%s\n' "$cleaned" | wc -l)
118
+ if (( line_count > max_lines )); then
119
+ cleaned=$(printf '%s\n' "$cleaned" | tail -n "$max_lines")
120
+ fi
121
+ fi
122
+
123
+ printf '%s' "$cleaned"
124
+ }
125
+
126
+ # ============================================================================
127
+ # Hooks (called from accept-line and precmd)
128
+ # ============================================================================
129
+
130
+ # Called from accept-line when routing input to the shell.
131
+ # Records the command text for inclusion in the next agent query context.
132
+ # Usage: _lacy_ctx_mark_command "$BUFFER" (ZSH)
133
+ # _lacy_ctx_mark_command "$READLINE_LINE" (Bash)
134
+ _lacy_ctx_mark_command() {
135
+ local cmd="$1"
136
+ _LACY_CTX_REAL_CMD=true
137
+
138
+ # Append to ring buffer, trim to max size
139
+ _LACY_CTX_CMD_BUFFER+=("$cmd")
140
+ if (( ${#_LACY_CTX_CMD_BUFFER[@]} > _LACY_CTX_CMD_BUFFER_MAX )); then
141
+ _LACY_CTX_CMD_BUFFER=("${_LACY_CTX_CMD_BUFFER[@]: -$_LACY_CTX_CMD_BUFFER_MAX}")
142
+ fi
143
+ }
144
+
145
+ # Called from precmd hooks. Captures exit code for real shell commands only.
146
+ # Usage: _lacy_ctx_on_precmd $last_exit
147
+ _lacy_ctx_on_precmd() {
148
+ local exit_code="$1"
149
+ if [[ "$_LACY_CTX_REAL_CMD" == true ]]; then
150
+ _LACY_CTX_LAST_EXIT_CODE=$exit_code
151
+ (( _LACY_CTX_CMDS_SINCE_QUERY++ ))
152
+ _LACY_CTX_REAL_CMD=false
153
+ fi
154
+ }
155
+
156
+ # ============================================================================
157
+ # Context Builder (called at query time)
158
+ # ============================================================================
159
+
160
+ # Build delta-based context and prepend to query.
161
+ # Sets _LACY_CTX_RESULT to the enriched query (avoids subshell so state resets
162
+ # propagate to the parent). If nothing changed, result is the bare query.
163
+ # Format: [cwd: /path] [git: branch] [exit: 1] [recent: cmd1 | cmd2] query
164
+ # With output: ...context header...\n[terminal-output]\n...\n[/terminal-output]\nquery
165
+ # Usage: _lacy_build_query_context "$query"; query="$_LACY_CTX_RESULT"
166
+ _LACY_CTX_RESULT=""
167
+
168
+ _lacy_build_query_context() {
169
+ local query="$1"
170
+ local ctx=""
171
+
172
+ # --- CWD (only if changed) ---
173
+ local cwd="${PWD}"
174
+ if [[ "$cwd" != "$_LACY_CTX_LAST_CWD" ]]; then
175
+ ctx+="[cwd: ${cwd}] "
176
+ _LACY_CTX_LAST_CWD="$cwd"
177
+ fi
178
+
179
+ # --- Git branch (only if changed) ---
180
+ local git_branch=""
181
+ if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
182
+ git_branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
183
+ # Detached HEAD returns literal "HEAD" — fall back to short hash
184
+ if [[ "$git_branch" == "HEAD" ]]; then
185
+ git_branch=$(git rev-parse --short HEAD 2>/dev/null)
186
+ fi
187
+ fi
188
+ if [[ "$git_branch" != "$_LACY_CTX_LAST_GIT" ]]; then
189
+ if [[ -n "$git_branch" ]]; then
190
+ ctx+="[git: ${git_branch}] "
191
+ fi
192
+ _LACY_CTX_LAST_GIT="$git_branch"
193
+ fi
194
+
195
+ # --- Last exit code (only if non-zero AND a command ran since last query) ---
196
+ if (( _LACY_CTX_CMDS_SINCE_QUERY > 0 && _LACY_CTX_LAST_EXIT_CODE != 0 )); then
197
+ ctx+="[exit: ${_LACY_CTX_LAST_EXIT_CODE}] "
198
+ fi
199
+
200
+ # --- Recent commands since last query ---
201
+ if (( _LACY_CTX_CMDS_SINCE_QUERY > 0 )) && [[ ${#_LACY_CTX_CMD_BUFFER[@]} -gt 0 ]]; then
202
+ local cmds=""
203
+ local cmd
204
+ for cmd in "${_LACY_CTX_CMD_BUFFER[@]}"; do
205
+ # Truncate long commands to keep context compact
206
+ if (( ${#cmd} > 80 )); then
207
+ cmd="${cmd:0:77}..."
208
+ fi
209
+ if [[ -n "$cmds" ]]; then
210
+ cmds+=" | $cmd"
211
+ else
212
+ cmds="$cmd"
213
+ fi
214
+ done
215
+ ctx+="[recent: ${cmds}] "
216
+ fi
217
+
218
+ # --- Terminal screen output (lazy capture, only if commands ran) ---
219
+ local screen_output=""
220
+ if (( _LACY_CTX_CMDS_SINCE_QUERY > 0 )); then
221
+ screen_output=$(_lacy_ctx_capture_screen)
222
+ fi
223
+
224
+ # --- Reset counters ---
225
+ _LACY_CTX_CMDS_SINCE_QUERY=0
226
+ _LACY_CTX_LAST_EXIT_CODE=0
227
+ _LACY_CTX_CMD_BUFFER=()
228
+
229
+ # --- Set result ---
230
+ if [[ -n "$screen_output" ]]; then
231
+ _LACY_CTX_RESULT="${ctx}
232
+ [terminal-output]
233
+ ${screen_output}
234
+ [/terminal-output]
235
+ ${query}"
236
+ else
237
+ _LACY_CTX_RESULT="${ctx}${query}"
238
+ fi
239
+ }
240
+
241
+ # ============================================================================
242
+ # Reset (called on /new session)
243
+ # ============================================================================
244
+
245
+ # Clear all context state so the next query sends full context.
246
+ # Does NOT reset terminal detection or config — those are session-lifetime.
247
+ _lacy_ctx_reset() {
248
+ _LACY_CTX_LAST_CWD=""
249
+ _LACY_CTX_LAST_GIT=""
250
+ _LACY_CTX_CMDS_SINCE_QUERY=0
251
+ _LACY_CTX_LAST_EXIT_CODE=0
252
+ _LACY_CTX_REAL_CMD=false
253
+ _LACY_CTX_CMD_BUFFER=()
254
+ }
255
+
256
+ # ============================================================================
257
+ # Init (runs once when sourced)
258
+ # ============================================================================
259
+
260
+ _lacy_ctx_detect_terminal