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,328 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Generate frames for Lacy Shell UGC SHORT video (15s).
4
+ Quick hook → single demo → CTA. Maximum shareability.
5
+ Vertical 1080x1920 for TikTok/Reels/Shorts.
6
+ """
7
+
8
+ import os
9
+ import math
10
+ from PIL import Image, ImageDraw, ImageFont
11
+
12
+ W, H = 1080, 1920
13
+ FPS = 30
14
+ OUT_DIR = "/tmp/ugc-video/frames-short"
15
+ os.makedirs(OUT_DIR, exist_ok=True)
16
+
17
+ # Colors
18
+ BG = (13, 13, 15)
19
+ TERM_BG = (22, 22, 28)
20
+ WHITE = (235, 235, 240)
21
+ GRAY = (130, 130, 145)
22
+ DIM = (80, 80, 95)
23
+ GREEN = (52, 211, 153)
24
+ MAGENTA = (216, 100, 240)
25
+ BLUE = (96, 165, 250)
26
+ YELLOW = (250, 204, 21)
27
+
28
+ def load_font(size, bold=False):
29
+ paths = ["/opt/X11/share/system_fonts/Menlo.ttc", "/System/Library/Fonts/Menlo.ttc"]
30
+ for p in paths:
31
+ try:
32
+ return ImageFont.truetype(p, size, index=1 if bold else 0)
33
+ except:
34
+ try:
35
+ return ImageFont.truetype(p, size)
36
+ except:
37
+ continue
38
+ return ImageFont.load_default()
39
+
40
+ def load_sans(size, bold=False):
41
+ for p in ["/System/Library/Fonts/SFNS.ttf"]:
42
+ try:
43
+ return ImageFont.truetype(p, size, index=2 if bold else 0)
44
+ except:
45
+ try:
46
+ return ImageFont.truetype(p, size)
47
+ except:
48
+ continue
49
+ return load_font(size, bold)
50
+
51
+ FONT_BIG = load_sans(72, True)
52
+ FONT_MED = load_sans(48, True)
53
+ FONT_SM = load_sans(32)
54
+ FONT_MONO = load_font(30)
55
+ FONT_MONO_SM = load_font(24)
56
+ FONT_MONO_LG = load_font(36, True)
57
+
58
+ def ease_out(t): return 1 - (1 - min(1, max(0, t))) ** 3
59
+ def ease_in_out(t): return 3 * t * t - 2 * t * t * t if t < 1 else 1
60
+
61
+ def lerp(c1, c2, t):
62
+ t = max(0, min(1, t))
63
+ return tuple(int(c1[i] + (c2[i] - c1[i]) * t) for i in range(3))
64
+
65
+ def rounded_rect(draw, xy, r, fill):
66
+ x0, y0, x1, y1 = xy
67
+ draw.rectangle([x0+r, y0, x1-r, y1], fill=fill)
68
+ draw.rectangle([x0, y0+r, x1, y1-r], fill=fill)
69
+ draw.pieslice([x0, y0, x0+2*r, y0+2*r], 180, 270, fill=fill)
70
+ draw.pieslice([x1-2*r, y0, x1, y0+2*r], 270, 360, fill=fill)
71
+ draw.pieslice([x0, y1-2*r, x0+2*r, y1], 90, 180, fill=fill)
72
+ draw.pieslice([x1-2*r, y1-2*r, x1, y1], 0, 90, fill=fill)
73
+
74
+ def tw(draw, text, font):
75
+ return draw.textlength(text, font=font)
76
+
77
+ def center_text(draw, y, text, font, color):
78
+ w = tw(draw, text, font)
79
+ draw.text(((W - w) / 2, y), text, fill=color, font=font)
80
+
81
+ # === Scenes ===
82
+
83
+ def scene_hook(f, total):
84
+ """0-2s: Bold hook text"""
85
+ img = Image.new("RGB", (W, H), BG)
86
+ draw = ImageDraw.Draw(img)
87
+ t = f / total
88
+
89
+ # "Stop" - pops in
90
+ if t < 0.5:
91
+ s_t = ease_out(t / 0.15)
92
+ c = lerp(BG, WHITE, s_t)
93
+ center_text(draw, H//2 - 180, "Stop", FONT_BIG, c)
94
+
95
+ # "copy-pasting" - slides in
96
+ if t > 0.12:
97
+ c_t = ease_out((t - 0.12) / 0.15)
98
+ c = lerp(BG, WHITE, c_t)
99
+ center_text(draw, H//2 - 80, "copy-pasting", FONT_BIG, c)
100
+
101
+ # "into ChatGPT" — in yellow
102
+ if t > 0.25:
103
+ g_t = ease_out((t - 0.25) / 0.15)
104
+ c = lerp(BG, YELLOW, g_t)
105
+ center_text(draw, H//2 + 20, "into ChatGPT.", FONT_BIG, c)
106
+
107
+ # Subtext
108
+ if t > 0.55:
109
+ sub_t = ease_out((t - 0.55) / 0.2)
110
+ c = lerp(BG, GRAY, sub_t)
111
+ center_text(draw, H//2 + 160, "Just talk to your shell.", FONT_MED, c)
112
+
113
+ return img
114
+
115
+
116
+ def scene_demo(f, total):
117
+ """2-10s: Terminal demo — type command, then type NL query"""
118
+ img = Image.new("RGB", (W, H), BG)
119
+ draw = ImageDraw.Draw(img)
120
+ t = f / total
121
+
122
+ margin = 50
123
+ term_top = 250
124
+ term_h = 1100
125
+
126
+ # Terminal window
127
+ rounded_rect(draw, (margin, term_top, W-margin, term_top+term_h), 16, TERM_BG)
128
+ # Title bar
129
+ rounded_rect(draw, (margin, term_top, W-margin, term_top+44), 16, (35, 35, 45))
130
+ draw.rectangle([margin, term_top+30, W-margin, term_top+44], fill=(35, 35, 45))
131
+ draw.ellipse([margin+18, term_top+14, margin+32, term_top+28], fill=(255, 95, 87))
132
+ draw.ellipse([margin+42, term_top+14, margin+56, term_top+28], fill=(255, 189, 46))
133
+ draw.ellipse([margin+66, term_top+14, margin+80, term_top+28], fill=(39, 201, 63))
134
+
135
+ title = "lacy ~"
136
+ ttw = tw(draw, title, FONT_MONO_SM)
137
+ draw.text(((W-ttw)/2, term_top+12), title, fill=GRAY, font=FONT_MONO_SM)
138
+
139
+ cx, cy = margin + 24, term_top + 65
140
+
141
+ # Phase 1 (0-0.35): Type "git status" → green indicator → shell output
142
+ cmd1 = "git status"
143
+ type1_t = min(1, t / 0.2)
144
+ chars1 = int(len(cmd1) * ease_in_out(type1_t))
145
+
146
+ # Indicator
147
+ if chars1 > 0:
148
+ ind_c = GREEN
149
+ else:
150
+ ind_c = GRAY
151
+ draw.ellipse([cx-2, cy+8, cx+12, cy+22], fill=ind_c)
152
+
153
+ prompt_text = "~ "
154
+ draw.text((cx + 20, cy), prompt_text, fill=GREEN, font=FONT_MONO)
155
+ pw = tw(draw, prompt_text, FONT_MONO)
156
+ draw.text((cx + 20 + pw, cy), cmd1[:chars1], fill=WHITE, font=FONT_MONO)
157
+
158
+ # Cursor for cmd1
159
+ if t < 0.25:
160
+ cw = tw(draw, cmd1[:chars1], FONT_MONO)
161
+ if (f % 15) < 10 or type1_t < 1:
162
+ bbox = FONT_MONO.getbbox("M")
163
+ draw.rectangle([cx+20+pw+cw, cy-1, cx+20+pw+cw+(bbox[2]-bbox[0]), cy+bbox[3]-bbox[1]+1], fill=WHITE)
164
+
165
+ # Shell output for git status
166
+ if t > 0.25:
167
+ out_t = min(1, (t - 0.25) / 0.08)
168
+ lines = [
169
+ ("On branch main", WHITE),
170
+ ("Changes not staged:", YELLOW),
171
+ (" modified: src/app.ts", (255, 130, 100)),
172
+ (" modified: src/utils.ts", (255, 130, 100)),
173
+ ]
174
+ n_show = int(len(lines) * ease_out(out_t))
175
+ for i in range(n_show):
176
+ draw.text((cx + 20, cy + 44 + i * 36), lines[i][0], fill=lines[i][1], font=FONT_MONO_SM)
177
+
178
+ # Green "SHELL" label
179
+ if out_t > 0.5:
180
+ lb_t = ease_out((out_t - 0.5) / 0.5)
181
+ lc = lerp(BG, GREEN, lb_t)
182
+ lbl = "→ Shell"
183
+ lw = tw(draw, lbl, FONT_SM)
184
+ draw.text((W - margin - lw - 20, cy + 2), lbl, fill=lc, font=FONT_SM)
185
+
186
+ # Phase 2 (0.4-0.75): Type NL query → magenta indicator → AI response
187
+ line2_y = cy + 230
188
+ if t > 0.38:
189
+ t2 = (t - 0.38) / 0.35
190
+ cmd2 = "explain what changed and why"
191
+ chars2 = int(len(cmd2) * min(1, ease_in_out(t2)))
192
+
193
+ # Magenta indicator (transitions from green)
194
+ if chars2 > 5:
195
+ ind2_c = MAGENTA
196
+ elif chars2 > 0:
197
+ blend = chars2 / 5
198
+ ind2_c = lerp(GREEN, MAGENTA, blend)
199
+ else:
200
+ ind2_c = GRAY
201
+ draw.ellipse([cx-2, line2_y+8, cx+12, line2_y+22], fill=ind2_c)
202
+
203
+ draw.text((cx+20, line2_y), "~ ", fill=GREEN, font=FONT_MONO)
204
+ draw.text((cx+20+pw, line2_y), cmd2[:chars2], fill=WHITE, font=FONT_MONO)
205
+
206
+ # Cursor
207
+ if t2 < 1:
208
+ cw2 = tw(draw, cmd2[:chars2], FONT_MONO)
209
+ if (f % 15) < 10:
210
+ bbox = FONT_MONO.getbbox("M")
211
+ draw.rectangle([cx+20+pw+cw2, line2_y-1, cx+20+pw+cw2+(bbox[2]-bbox[0]), line2_y+bbox[3]-bbox[1]+1], fill=WHITE)
212
+
213
+ # Magenta "AGENT" label
214
+ if t2 > 0.5:
215
+ lb2_t = ease_out((t2 - 0.5) / 0.5)
216
+ lc2 = lerp(BG, MAGENTA, lb2_t)
217
+ draw.text((W - margin - tw(draw, "→ Agent", FONT_SM) - 20, line2_y + 2), "→ Agent", fill=lc2, font=FONT_SM)
218
+
219
+ # AI response
220
+ if t > 0.78:
221
+ ai_t = (t - 0.78) / 0.2
222
+ ai_lines = [
223
+ "The changes in src/app.ts add a",
224
+ "new authentication middleware that",
225
+ "validates JWT tokens before routing.",
226
+ "",
227
+ "src/utils.ts was updated to export",
228
+ "a new `verifyToken()` helper that",
229
+ "the middleware depends on.",
230
+ ]
231
+ n_ai = int(len(ai_lines) * min(1, ease_out(ai_t * 1.3)))
232
+ for i in range(n_ai):
233
+ lc_ai = lerp(TERM_BG, MAGENTA, ease_out(ai_t))
234
+ draw.text((cx + 20, line2_y + 44 + i * 36), ai_lines[i], fill=lc_ai, font=FONT_MONO_SM)
235
+
236
+ # Mode badge
237
+ rounded_rect(draw, (W-margin-90, term_top+term_h-42, W-margin-10, term_top+term_h-14), 6, (30, 30, 42))
238
+ draw.text((W-margin-82, term_top+term_h-40), "AUTO", fill=BLUE, font=FONT_MONO_SM)
239
+
240
+ # Top label
241
+ if t < 0.38:
242
+ lbl_text = "Commands run in your shell"
243
+ lbl_c = GREEN
244
+ else:
245
+ lbl_text = "Questions go to AI"
246
+ lbl_c = MAGENTA
247
+ ltw = tw(draw, lbl_text, FONT_SM)
248
+ center_text(draw, 170, lbl_text, FONT_SM, lbl_c)
249
+
250
+ return img
251
+
252
+
253
+ def scene_cta(f, total):
254
+ """10-15s: CTA"""
255
+ img = Image.new("RGB", (W, H), BG)
256
+ draw = ImageDraw.Draw(img)
257
+ t = f / total
258
+
259
+ # Brand
260
+ b_t = ease_out(t / 0.2)
261
+ center_text(draw, H//2 - 220, "lacy.sh", FONT_BIG, lerp(BG, WHITE, b_t))
262
+
263
+ # Tagline
264
+ if t > 0.1:
265
+ tg_t = ease_out((t - 0.1) / 0.2)
266
+ center_text(draw, H//2 - 120, "Talk to your shell.", FONT_MED, lerp(BG, GRAY, tg_t))
267
+
268
+ # Install box
269
+ if t > 0.25:
270
+ bx_t = ease_out((t - 0.25) / 0.2)
271
+ install = "curl -fsSL lacy.sh/install | bash"
272
+ iw = tw(draw, install, FONT_MONO_SM) + 50
273
+ bx = (W - iw) / 2
274
+ by = H//2 - 20
275
+ rounded_rect(draw, (bx-2, by-2, bx+iw+2, by+52), 12, lerp(BG, MAGENTA, bx_t * 0.4))
276
+ rounded_rect(draw, (bx, by, bx+iw, by+50), 10, lerp(BG, (30, 30, 42), bx_t))
277
+ draw.text((bx+25, by+12), install, fill=lerp(BG, WHITE, bx_t), font=FONT_MONO_SM)
278
+
279
+ # "or" + npx
280
+ if t > 0.4:
281
+ or_t = ease_out((t - 0.4) / 0.15)
282
+ center_text(draw, H//2 + 60, "or", FONT_SM, lerp(BG, DIM, or_t))
283
+ center_text(draw, H//2 + 105, "npx lacy", FONT_MONO, lerp(BG, GRAY, or_t))
284
+
285
+ # Features
286
+ if t > 0.55:
287
+ feats = ["ZSH & Bash", "macOS · Linux · WSL", "Works with Claude, Gemini, Codex..."]
288
+ for i, feat in enumerate(feats):
289
+ ft = ease_out((t - 0.55 - i * 0.06) / 0.15)
290
+ if ft > 0:
291
+ center_text(draw, H//2 + 200 + i * 50, feat, FONT_SM, lerp(BG, DIM, ft))
292
+
293
+ # Colored dots
294
+ if t > 0.7:
295
+ dt = ease_out((t - 0.7) / 0.15)
296
+ for color, ox in [(GREEN, -60), (MAGENTA, 0), (BLUE, 60)]:
297
+ dc = lerp(BG, color, dt)
298
+ draw.ellipse([W//2+ox-6, H//2+380, W//2+ox+6, H//2+392], fill=dc)
299
+
300
+ return img
301
+
302
+
303
+ # === Generate ===
304
+
305
+ def generate():
306
+ scenes = [
307
+ (scene_hook, 2.0), # 0-2s
308
+ (scene_demo, 8.0), # 2-10s
309
+ (scene_cta, 5.0), # 10-15s
310
+ ]
311
+
312
+ frame_num = 0
313
+ total_frames = int(sum(d for _, d in scenes) * FPS)
314
+ print(f"Generating {total_frames} frames ({total_frames/FPS:.1f}s at {FPS}fps)")
315
+
316
+ for i, (fn, dur) in enumerate(scenes):
317
+ n = int(dur * FPS)
318
+ print(f" Scene {i+1}/{len(scenes)}: {fn.__name__} ({n} frames)")
319
+ for f in range(n):
320
+ img = fn(f, n)
321
+ img.save(os.path.join(OUT_DIR, f"frame_{frame_num:05d}.png"))
322
+ frame_num += 1
323
+
324
+ print(f"Done! {frame_num} frames")
325
+
326
+
327
+ if __name__ == "__main__":
328
+ generate()