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,417 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # Auto-detection logic for determining shell vs agent mode
4
+ # Shared across Bash 4+ and ZSH
5
+
6
+ # Cache for command -v lookups (avoids repeated PATH walks while typing)
7
+ LACY_CMD_CACHE_WORD=""
8
+ LACY_CMD_CACHE_RESULT=""
9
+
10
+ # Check if a word is a valid command, with single-entry cache
11
+ lacy_shell_is_valid_command() {
12
+ local word="$1"
13
+ if [[ "$word" == "$LACY_CMD_CACHE_WORD" ]]; then
14
+ return $LACY_CMD_CACHE_RESULT
15
+ fi
16
+ LACY_CMD_CACHE_WORD="$word"
17
+ if command -v "$word" &>/dev/null; then
18
+ LACY_CMD_CACHE_RESULT=0
19
+ else
20
+ LACY_CMD_CACHE_RESULT=1
21
+ fi
22
+ return $LACY_CMD_CACHE_RESULT
23
+ }
24
+
25
+ # Check if input starting with a valid command has natural language markers.
26
+ # Returns 0 (true) if at least one bare word after the first word is a strong
27
+ # NL marker. Used to flag reroute candidates — the reroute only fires when
28
+ # the command also fails, so this can be fairly aggressive.
29
+ lacy_shell_has_nl_markers() {
30
+ local input="$1"
31
+
32
+ # Bail if single word (no spaces)
33
+ [[ "$input" != *" "* ]] && return 1
34
+
35
+ # Bail if input contains shell operators — clearly shell syntax
36
+ local op
37
+ for op in "${LACY_SHELL_OPERATORS[@]}"; do
38
+ [[ "$input" == *"$op"* ]] && return 1
39
+ done
40
+
41
+ # Extract tokens after the first word
42
+ local rest="${input#* }"
43
+ local -a tokens
44
+ if [[ "$LACY_SHELL_TYPE" == "zsh" ]]; then
45
+ tokens=( ${=rest} )
46
+ else
47
+ # Bash: IFS word splitting
48
+ read -ra tokens <<< "$rest"
49
+ fi
50
+
51
+ # Filter to bare words only (skip flags, paths, numbers, variables)
52
+ local -a bare_words=()
53
+ local token lower_token
54
+ for token in "${tokens[@]}"; do
55
+ # Skip flags (-x, --flag)
56
+ [[ "$token" == -* ]] && continue
57
+ # Skip paths (/foo, ./bar, ~/dir)
58
+ [[ "$token" == /* || "$token" == ./* || "$token" == ~/* ]] && continue
59
+ # Skip pure numbers
60
+ [[ "$token" =~ ^[0-9]+$ ]] && continue
61
+ # Skip variables ($VAR, ${VAR})
62
+ [[ "$token" == \$* ]] && continue
63
+ lower_token=$(_lacy_lowercase "$token")
64
+ bare_words+=( "$lower_token" )
65
+ done
66
+
67
+ # Need at least 1 bare word after the first word
68
+ (( ${#bare_words[@]} < 1 )) && return 1
69
+
70
+ # Check for strong NL markers
71
+ local word marker
72
+ for word in "${bare_words[@]}"; do
73
+ for marker in "${LACY_NL_MARKERS[@]}"; do
74
+ [[ "$word" == "$marker" ]] && return 0
75
+ done
76
+ done
77
+
78
+ return 1
79
+ }
80
+
81
+ # Canonical detection function. Prints "neutral", "shell", or "agent".
82
+ # All detection flows (indicator, execution) must go through this function.
83
+ lacy_shell_classify_input() {
84
+ local input="$1"
85
+
86
+ # Trim leading whitespace (POSIX-compatible, no extendedglob)
87
+ input="${input#"${input%%[^[:space:]]*}"}"
88
+ # Trim trailing whitespace
89
+ input="${input%"${input##*[^[:space:]]}"}"
90
+
91
+ # Empty input - show mode color in shell/agent, neutral in auto
92
+ if [[ -z "$input" ]]; then
93
+ case "$LACY_SHELL_CURRENT_MODE" in
94
+ "shell") echo "shell" ;;
95
+ "agent") echo "agent" ;;
96
+ *) echo "neutral" ;;
97
+ esac
98
+ return
99
+ fi
100
+
101
+ # Emergency bypass prefix (!) = shell
102
+ if [[ "$input" == !* ]]; then
103
+ echo "shell"
104
+ return
105
+ fi
106
+
107
+ # Agent bypass prefix (@) = agent
108
+ if [[ "$input" == @* ]]; then
109
+ echo "agent"
110
+ return
111
+ fi
112
+
113
+ # In shell mode, everything goes to shell
114
+ if [[ "$LACY_SHELL_CURRENT_MODE" == "shell" ]]; then
115
+ echo "shell"
116
+ return
117
+ fi
118
+
119
+ # In agent mode, everything goes to agent
120
+ if [[ "$LACY_SHELL_CURRENT_MODE" == "agent" ]]; then
121
+ echo "agent"
122
+ return
123
+ fi
124
+
125
+ # Auto mode: check special cases and commands
126
+ # Extract first token respecting:
127
+ # - backslash-escaped spaces: /path/to/Google\ Chrome
128
+ # - double-quoted paths: "/Applications/Google Chrome.app/..."
129
+ # - single-quoted paths: '/Applications/Google Chrome.app/...'
130
+ local first_word first_word_cmd
131
+ if [[ "$input" == \"* ]]; then
132
+ # Double-quoted first token: extract up to closing quote
133
+ local _after="${input#\"}"
134
+ first_word="\"${_after%%\"*}\""
135
+ # Strip quotes for command -v lookup
136
+ first_word_cmd="${_after%%\"*}"
137
+ elif [[ "$input" == \'* ]]; then
138
+ # Single-quoted first token: extract up to closing quote
139
+ local _after="${input#\'}"
140
+ first_word="'${_after%%\'*}'"
141
+ first_word_cmd="${_after%%\'*}"
142
+ else
143
+ # Backslash-escaped spaces: use a variable for the placeholder so that
144
+ # $'\x01' is processed by ANSI-C quoting at assignment time. In ZSH,
145
+ # $'\x01' inside ${var//pattern/replacement} is NOT expanded — it is
146
+ # treated as the literal 6-char string $'\x01', breaking the round-trip.
147
+ local _lacy_bsp=$'\x01'
148
+ local _esc_input="${input//\\ /$_lacy_bsp}"
149
+ first_word="${_esc_input%% *}"
150
+ first_word="${first_word//$_lacy_bsp/\\ }"
151
+ # Un-escaped version for command -v lookups (backslash-space → space)
152
+ first_word_cmd="${first_word//\\ / }"
153
+ fi
154
+ local first_word_lower
155
+ first_word_lower=$(_lacy_lowercase "$first_word_cmd")
156
+
157
+ # Strip trailing punctuation for word-list lookups (e.g., "why?" → "why")
158
+ local first_word_stripped="$first_word_lower"
159
+ while [[ -n "$first_word_stripped" && "$first_word_stripped" == *[?.,\;:!] ]]; do
160
+ first_word_stripped="${first_word_stripped%?}"
161
+ done
162
+
163
+ # Layer 1a: Shell reserved words pass `command -v` but are never valid
164
+ # standalone commands. Route to agent. (see docs/NATURAL_LANGUAGE_DETECTION.md)
165
+ if _lacy_in_list "$first_word_stripped" "${LACY_SHELL_RESERVED_WORDS[@]}"; then
166
+ echo "agent"
167
+ return
168
+ fi
169
+
170
+ # Layer 1b: Common English words almost always route to agent.
171
+ # Exception: if the word is also a valid shell command AND the arguments
172
+ # look like shell syntax, defer to shell. Heuristic is conservative:
173
+ # only shell when operators are present OR there is at most one bare word
174
+ # argument (after flags/paths/numbers) that is not an NL marker.
175
+ # Examples: `which python` → shell, `yes | cmd` → shell
176
+ # `which version to use` → agent, `yes lets go` → agent
177
+ if _lacy_in_list "$first_word_stripped" "${LACY_AGENT_WORDS[@]}"; then
178
+ if lacy_shell_is_valid_command "$first_word_cmd"; then
179
+ # Shell operators anywhere → shell
180
+ local _op
181
+ for _op in "${LACY_SHELL_OPERATORS[@]}"; do
182
+ [[ "$input" == *"$_op"* ]] && { echo "shell"; return; }
183
+ done
184
+ # Count bare words (non-flag, non-path, non-number, non-variable)
185
+ if [[ "$input" == *" "* ]]; then
186
+ local _rest="${input#* }"
187
+ local -a _tokens
188
+ if [[ "$LACY_SHELL_TYPE" == "zsh" ]]; then
189
+ _tokens=( ${=_rest} )
190
+ else
191
+ read -ra _tokens <<< "$_rest"
192
+ fi
193
+ local -a _bare=()
194
+ local _tok _ltok
195
+ for _tok in "${_tokens[@]}"; do
196
+ [[ "$_tok" == -* ]] && continue
197
+ [[ "$_tok" == /* || "$_tok" == ./* || "$_tok" == ~/* ]] && continue
198
+ [[ "$_tok" =~ ^[0-9]+$ ]] && continue
199
+ [[ "$_tok" == \$* ]] && continue
200
+ _ltok=$(_lacy_lowercase "$_tok")
201
+ _bare+=( "$_ltok" )
202
+ done
203
+ # 0 bare words (flags only) → shell
204
+ if (( ${#_bare[@]} == 0 )); then
205
+ echo "shell"
206
+ return
207
+ fi
208
+ # Exactly 1 bare word that is not an NL marker → shell
209
+ if (( ${#_bare[@]} == 1 )); then
210
+ if ! _lacy_in_list "${_bare[${_LACY_ARR_OFFSET}]}" "${LACY_NL_MARKERS[@]}"; then
211
+ echo "shell"
212
+ return
213
+ fi
214
+ fi
215
+ # 2+ bare words, or the single bare word is an NL marker → agent
216
+ fi
217
+ fi
218
+ echo "agent"
219
+ return
220
+ fi
221
+
222
+ # Inline env var assignment: VAR=value command args
223
+ # Skip past any VAR=value prefixes to find the actual command
224
+ if [[ "$first_word" == *=* ]]; then
225
+ local -a _words
226
+ if [[ "$LACY_SHELL_TYPE" == "zsh" ]]; then
227
+ _words=( ${=input} )
228
+ else
229
+ read -ra _words <<< "$input"
230
+ fi
231
+ local _w
232
+ for _w in "${_words[@]}"; do
233
+ if [[ "$_w" == *=* ]]; then
234
+ continue
235
+ fi
236
+ # Found the actual command after env var(s)
237
+ if lacy_shell_is_valid_command "$_w"; then
238
+ echo "shell"
239
+ return
240
+ fi
241
+ break
242
+ done
243
+ fi
244
+
245
+ # Check if it's a valid command (cached)
246
+ if lacy_shell_is_valid_command "$first_word_cmd"; then
247
+ echo "shell"
248
+ return
249
+ fi
250
+
251
+ # Single word that's not a command = probably a typo -> shell
252
+ # Multiple words with non-command first word = natural language -> agent
253
+ # Check if there's anything after the first token
254
+ local _rest_after_first="${input#"$first_word"}"
255
+ _rest_after_first="${_rest_after_first#"${_rest_after_first%%[^[:space:]]*}"}"
256
+ if [[ -z "$_rest_after_first" ]]; then
257
+ echo "shell"
258
+ else
259
+ echo "agent"
260
+ fi
261
+ }
262
+
263
+ # Backward-compatible wrapper: returns 0 (agent) or 1 (shell/neutral)
264
+ lacy_shell_should_use_agent() {
265
+ local result
266
+ result=$(lacy_shell_classify_input "$1")
267
+ if [[ "$result" == "agent" ]]; then
268
+ return 0
269
+ else
270
+ return 1
271
+ fi
272
+ }
273
+
274
+ # Initialize detection cache (call at startup)
275
+ lacy_shell_init_detection_cache() {
276
+ LACY_CMD_CACHE_WORD=""
277
+ LACY_CMD_CACHE_RESULT=""
278
+ }
279
+
280
+ # Layer 2: Post-execution natural language detection.
281
+ # Analyzes a failed shell command's output to determine if the user
282
+ # typed natural language. Returns 0 (true) if NL detected, 1 otherwise.
283
+ # See docs/NATURAL_LANGUAGE_DETECTION.md for the full algorithm.
284
+ #
285
+ # Usage: lacy_shell_detect_natural_language "input" "output" exit_code
286
+ lacy_shell_detect_natural_language() {
287
+ local input="$1"
288
+ local output="$2"
289
+ local exit_code="$3"
290
+
291
+ # Only check failed commands
292
+ (( exit_code == 0 )) && return 1
293
+ [[ -z "$exit_code" ]] && return 1
294
+
295
+ # Count words
296
+ local -a words
297
+ if [[ "$LACY_SHELL_TYPE" == "zsh" ]]; then
298
+ words=( ${=input} )
299
+ else
300
+ read -ra words <<< "$input"
301
+ fi
302
+
303
+ # Single-word inputs are probably real commands
304
+ (( ${#words[@]} < 2 )) && return 1
305
+
306
+ # Criterion A: output must match at least one error pattern (case-insensitive)
307
+ local output_lower
308
+ output_lower=$(_lacy_lowercase "$output")
309
+ local pattern pattern_lower matched=false
310
+ for pattern in "${LACY_SHELL_ERROR_PATTERNS[@]}"; do
311
+ pattern_lower=$(_lacy_lowercase "$pattern")
312
+ if [[ "$output_lower" == *"$pattern_lower"* ]]; then
313
+ matched=true
314
+ break
315
+ fi
316
+ done
317
+ [[ "$matched" == false ]] && return 1
318
+
319
+ # Criterion B: check for natural language signal
320
+ local second_word
321
+ second_word=$(_lacy_lowercase "${words[$_LACY_ARR_OFFSET + 1]}")
322
+
323
+ # B1: second word is a natural language marker
324
+ if [[ -n "$second_word" ]] && _lacy_in_list "$second_word" "${LACY_NL_MARKERS[@]}"; then
325
+ return 0
326
+ fi
327
+
328
+ # B2: 4+ words and a parse/syntax error
329
+ if (( ${#words[@]} >= 4 )); then
330
+ if [[ "$output_lower" == *"parse error"* || "$output_lower" == *"syntax error"* || "$output_lower" == *"unexpected token"* ]]; then
331
+ return 0
332
+ fi
333
+ fi
334
+
335
+ return 1
336
+ }
337
+
338
+ # Test the detection logic (for debugging)
339
+ lacy_shell_test_detection() {
340
+ local test_cases=(
341
+ "ls -la"
342
+ "what files are in this directory?"
343
+ "git status"
344
+ "cd /home/user"
345
+ "npm install"
346
+ "rm file.txt"
347
+ "pwd"
348
+ "./run.sh"
349
+ "what is the meaning of life?"
350
+ "hello there"
351
+ "nonexistent_command foo"
352
+ " ls -la"
353
+ " what files"
354
+ " !rm /tmp/test"
355
+ "yes lets go"
356
+ "no I dont want that"
357
+ "yes"
358
+ "RUST_LOG=debug cargo run"
359
+ "FOO=bar BAZ=qux node index.js"
360
+ "CC=gcc make -j4"
361
+ # Agent words that are also valid commands — should use heuristics
362
+ "which python"
363
+ "which -a git"
364
+ "which version should I install"
365
+ "which"
366
+ "yes | apt-get install -y"
367
+ "nice -n 10 make"
368
+ "nice work"
369
+ "who"
370
+ "who root"
371
+ "who am I"
372
+ # Backslash-escaped spaces in paths
373
+ "/Applications/Google\\ Chrome.app/Contents/MacOS/Google\\ Chrome --remote-debugging-port=9222"
374
+ "./my\\ script.sh --flag"
375
+ # Quoted paths with spaces
376
+ '"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --remote-debugging-port=9222'
377
+ "'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' --flag"
378
+ '"/usr/local/bin/my tool"'
379
+ # @ agent bypass
380
+ "@ make sure the tests pass"
381
+ "@fix the bug in auth"
382
+ )
383
+
384
+ echo "Testing auto-detection logic:"
385
+ echo "============================="
386
+
387
+ local test_case result
388
+ for test_case in "${test_cases[@]}"; do
389
+ result=$(lacy_shell_classify_input "$test_case")
390
+ printf "%-40s -> %s\n" "$test_case" "$result"
391
+ done
392
+
393
+ echo ""
394
+ echo "Testing NL marker detection:"
395
+ echo "============================="
396
+
397
+ local nl_tests=(
398
+ "kill the process on localhost:3000"
399
+ "kill -9 my baby"
400
+ "kill -9 my baby girl"
401
+ "kill -9"
402
+ "echo the quick brown fox"
403
+ "echo hello | grep the"
404
+ "find my large files"
405
+ "make the tests pass"
406
+ "git push origin main"
407
+ "docker run -it ubuntu"
408
+ )
409
+
410
+ for test_case in "${nl_tests[@]}"; do
411
+ if lacy_shell_has_nl_markers "$test_case"; then
412
+ printf "%-40s -> nl_markers: YES\n" "$test_case"
413
+ else
414
+ printf "%-40s -> nl_markers: NO\n" "$test_case"
415
+ fi
416
+ done
417
+ }