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,123 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # Mode management for Lacy Shell
4
+ # Shared across Bash 4+ and ZSH
5
+
6
+ # Available modes
7
+ LACY_SHELL_MODES=("shell" "agent" "auto")
8
+ LACY_SHELL_CURRENT_MODE="auto"
9
+
10
+ # Mode description helper (replaces associative array for portability)
11
+ lacy_mode_description() {
12
+ case "$1" in
13
+ shell) echo "Normal shell execution" ;;
14
+ agent) echo "AI agent assistance via MCP" ;;
15
+ auto) echo "Try shell commands first, fallback to AI agent" ;;
16
+ *) echo "Unknown mode" ;;
17
+ esac
18
+ }
19
+
20
+ # Set mode
21
+ lacy_shell_set_mode() {
22
+ local new_mode="$1"
23
+
24
+ if ! _lacy_in_list "$new_mode" "${LACY_SHELL_MODES[@]}"; then
25
+ echo "Invalid mode: $new_mode. Available modes: ${LACY_SHELL_MODES[*]}"
26
+ return 1
27
+ fi
28
+
29
+ LACY_SHELL_CURRENT_MODE="$new_mode"
30
+ lacy_shell_save_mode "$new_mode"
31
+
32
+ if type lacy_shell_update_prompt &>/dev/null; then
33
+ lacy_shell_update_prompt
34
+ fi
35
+ }
36
+
37
+ # Get current mode
38
+ lacy_shell_get_mode() {
39
+ echo "$LACY_SHELL_CURRENT_MODE"
40
+ }
41
+
42
+ # Toggle between modes
43
+ lacy_shell_toggle_mode() {
44
+ local current_index=0
45
+ local i count
46
+
47
+ count=${#LACY_SHELL_MODES[@]}
48
+
49
+ if [[ "$LACY_SHELL_TYPE" == "zsh" ]]; then
50
+ # ZSH: 1-based indexing
51
+ for (( i = 1; i <= count; i++ )); do
52
+ if [[ "${LACY_SHELL_MODES[$i]}" == "$LACY_SHELL_CURRENT_MODE" ]]; then
53
+ current_index=$i
54
+ break
55
+ fi
56
+ done
57
+ local next_index=$(( (current_index % count) + 1 ))
58
+ lacy_shell_set_mode "${LACY_SHELL_MODES[$next_index]}"
59
+ else
60
+ # Bash: 0-based indexing
61
+ for (( i = 0; i < count; i++ )); do
62
+ if [[ "${LACY_SHELL_MODES[$i]}" == "$LACY_SHELL_CURRENT_MODE" ]]; then
63
+ current_index=$i
64
+ break
65
+ fi
66
+ done
67
+ local next_index=$(( (current_index + 1) % count ))
68
+ lacy_shell_set_mode "${LACY_SHELL_MODES[$next_index]}"
69
+ fi
70
+ }
71
+
72
+ # Direct mode switches
73
+ lacy_shell_agent_mode() {
74
+ lacy_shell_set_mode "agent"
75
+ }
76
+
77
+ lacy_shell_shell_mode() {
78
+ lacy_shell_set_mode "shell"
79
+ }
80
+
81
+ lacy_shell_auto_mode() {
82
+ lacy_shell_set_mode "auto"
83
+ }
84
+
85
+ # Mode persistence
86
+ lacy_shell_save_mode() {
87
+ mkdir -p "$(dirname "$LACY_SHELL_MODE_FILE")"
88
+ echo "$1" > "$LACY_SHELL_MODE_FILE"
89
+ }
90
+
91
+ lacy_shell_init_mode() {
92
+ if [[ -f "$LACY_SHELL_MODE_FILE" ]]; then
93
+ local saved_mode
94
+ saved_mode=$(cat "$LACY_SHELL_MODE_FILE" 2>/dev/null)
95
+ if _lacy_in_list "$saved_mode" "${LACY_SHELL_MODES[@]}"; then
96
+ LACY_SHELL_CURRENT_MODE="$saved_mode"
97
+ else
98
+ LACY_SHELL_CURRENT_MODE="$LACY_SHELL_DEFAULT_MODE"
99
+ fi
100
+ else
101
+ LACY_SHELL_CURRENT_MODE="$LACY_SHELL_DEFAULT_MODE"
102
+ fi
103
+ }
104
+
105
+ # Show mode status
106
+ lacy_shell_mode_status() {
107
+ echo ""
108
+ echo -n "Current mode: "
109
+ case "$LACY_SHELL_CURRENT_MODE" in
110
+ "shell") lacy_print_color "$LACY_COLOR_SHELL" "SHELL" ;;
111
+ "agent") lacy_print_color "$LACY_COLOR_AGENT" "AGENT" ;;
112
+ "auto") lacy_print_color "$LACY_COLOR_AUTO" "AUTO" ;;
113
+ *) lacy_print_color "$LACY_COLOR_NEUTRAL" "unknown" ;;
114
+ esac
115
+ echo ""
116
+ echo "Description: $(lacy_mode_description "$LACY_SHELL_CURRENT_MODE")"
117
+ echo ""
118
+ echo "Colors:"
119
+ lacy_print_color "$LACY_COLOR_SHELL" " ${LACY_INDICATOR_CHAR} Green = shell command"
120
+ lacy_print_color "$LACY_COLOR_AGENT" " ${LACY_INDICATOR_CHAR} Magenta = agent query"
121
+ lacy_print_color "$LACY_COLOR_AUTO" " ${LACY_INDICATOR_CHAR} Blue = auto mode"
122
+ echo ""
123
+ }
@@ -0,0 +1,496 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # Agent preheating for Lacy Shell
4
+ # - Background server for lash/opencode (eliminates cold-start)
5
+ # - Session reuse for claude (conversation continuity)
6
+ # Shared across Bash 4+ and ZSH
7
+
8
+ # === State ===
9
+ LACY_PREHEAT_SERVER_PID=""
10
+ LACY_PREHEAT_SERVER_PASSWORD=""
11
+ LACY_PREHEAT_SERVER_PID_FILE="$LACY_SHELL_HOME/.server.pid"
12
+ LACY_PREHEAT_SERVER_SESSION_ID=""
13
+ # Per-shell session files (using PID to ensure fresh session per window/tab)
14
+ LACY_PREHEAT_SERVER_SESSION_FILE="$LACY_SHELL_HOME/.server_session_id_$$"
15
+ LACY_PREHEAT_CLAUDE_SESSION_ID=""
16
+ LACY_PREHEAT_SESSION_FILE="$LACY_SHELL_HOME/.claude_session_id_$$"
17
+ # Global last-session file (not PID-specific) — enables cross-shell resume
18
+ LACY_LAST_SESSION_FILE="$LACY_SHELL_HOME/.last_session"
19
+
20
+ # ============================================================================
21
+ # Background Server (lash + opencode)
22
+ # ============================================================================
23
+
24
+ # Start background server for lash or opencode
25
+ lacy_preheat_server_start() {
26
+ local tool="$1"
27
+
28
+ # Already running?
29
+ if lacy_preheat_server_is_healthy; then
30
+ return 0
31
+ fi
32
+
33
+ # Clean up stale PID from previous session
34
+ lacy_preheat_server_stop 2>/dev/null
35
+
36
+ # Generate random password for this session
37
+ LACY_PREHEAT_SERVER_PASSWORD=$(LC_ALL=C tr -dc 'A-Za-z0-9' </dev/urandom 2>/dev/null | head -c 32 || date +%s%N)
38
+
39
+ # Start server in background (suppress all job notifications)
40
+ # Redirect stdin from /dev/null so the background server doesn't compete
41
+ # with foreground processes (lash, vim, etc.) for terminal input.
42
+ _lacy_jobctl_off
43
+ "$tool" serve --port "$LACY_PREHEAT_SERVER_PORT" </dev/null >/dev/null 2>&1 &
44
+ LACY_PREHEAT_SERVER_PID=$!
45
+ disown 2>/dev/null
46
+ _lacy_jobctl_on
47
+
48
+ # Save PID to file for crash recovery
49
+ echo "$LACY_PREHEAT_SERVER_PID" > "$LACY_PREHEAT_SERVER_PID_FILE"
50
+
51
+ # Wait for server to become healthy (up to 3 seconds)
52
+ local attempts=0
53
+ while (( attempts < LACY_HEALTH_CHECK_ATTEMPTS )); do
54
+ if lacy_preheat_server_is_healthy; then
55
+ return 0
56
+ fi
57
+ sleep "$LACY_HEALTH_CHECK_INTERVAL"
58
+ (( attempts++ ))
59
+ done
60
+
61
+ # Failed to start — clean up
62
+ lacy_preheat_server_stop 2>/dev/null
63
+ return 1
64
+ }
65
+
66
+ # Start async health check in background
67
+ lacy_preheat_server_check_async() {
68
+ # Cancel any existing check
69
+ [[ -n "$LACY_PREHEAT_HEALTH_CHECK_PID" ]] && kill "$LACY_PREHEAT_HEALTH_CHECK_PID" 2>/dev/null
70
+
71
+ # Skip if we already have a fresh cache
72
+ if [[ "$LACY_PREHEAT_HEALTH_CACHE" == true ]] && [[ -f "$LACY_SHELL_HEALTH_CACHE_FILE" ]] && \
73
+ [[ $(find "$LACY_SHELL_HEALTH_CACHE_FILE" -mmin -1 2>/dev/null) ]]; then
74
+ return 0
75
+ fi
76
+
77
+ {
78
+ local pid="$LACY_PREHEAT_SERVER_PID"
79
+ if [[ -z "$pid" ]]; then
80
+ if [[ -f "$LACY_PREHEAT_SERVER_PID_FILE" ]]; then
81
+ pid=$(cat "$LACY_PREHEAT_SERVER_PID_FILE" 2>/dev/null)
82
+ fi
83
+ [[ -z "$pid" ]] && echo "1" > "$LACY_SHELL_HEALTH_CACHE_FILE" && return
84
+ fi
85
+
86
+ kill -0 "$pid" 2>/dev/null || { echo "1" > "$LACY_SHELL_HEALTH_CACHE_FILE" && return; }
87
+
88
+ if curl -sf --max-time "$LACY_HEALTH_CHECK_TIMEOUT_ASYNC" "http://localhost:${LACY_PREHEAT_SERVER_PORT}/global/health" >/dev/null 2>&1; then
89
+ echo "0" > "$LACY_SHELL_HEALTH_CACHE_FILE"
90
+ else
91
+ echo "1" > "$LACY_SHELL_HEALTH_CACHE_FILE"
92
+ fi
93
+ } &
94
+ LACY_PREHEAT_HEALTH_CHECK_PID=$!
95
+ LACY_PREHEAT_HEALTH_CACHE=true
96
+ }
97
+
98
+ # Check if server is alive and responding
99
+ lacy_preheat_server_is_healthy() {
100
+ # First check cache for instant response
101
+ if [[ "$LACY_PREHEAT_HEALTH_CACHE" == true ]] && [[ -f "$LACY_SHELL_HEALTH_CACHE_FILE" ]]; then
102
+ local result
103
+ result=$(cat "$LACY_SHELL_HEALTH_CACHE_FILE" 2>/dev/null || echo "1")
104
+ [[ "$result" == "0" ]] && return 0
105
+ fi
106
+
107
+ # Fallback: synchronous check
108
+ if [[ -z "$LACY_PREHEAT_SERVER_PID" ]]; then
109
+ if [[ -f "$LACY_PREHEAT_SERVER_PID_FILE" ]]; then
110
+ LACY_PREHEAT_SERVER_PID=$(cat "$LACY_PREHEAT_SERVER_PID_FILE" 2>/dev/null)
111
+ fi
112
+ [[ -z "$LACY_PREHEAT_SERVER_PID" ]] && return 1
113
+ fi
114
+
115
+ kill -0 "$LACY_PREHEAT_SERVER_PID" 2>/dev/null || return 1
116
+
117
+ curl -sf --max-time "$LACY_HEALTH_CHECK_TIMEOUT_SYNC" "http://localhost:${LACY_PREHEAT_SERVER_PORT}/global/health" >/dev/null 2>&1
118
+ }
119
+
120
+ # Internal helper to create a new server session (lash/opencode)
121
+ _lacy_preheat_server_create_session() {
122
+ if ! lacy_preheat_server_is_healthy; then
123
+ return 1
124
+ fi
125
+
126
+ local _session_dir session_json
127
+ _session_dir=$(pwd 2>/dev/null)
128
+ if session_json=$(curl -sf --max-time "$LACY_SESSION_CREATE_TIMEOUT" \
129
+ -X POST \
130
+ -H "Content-Type: application/json" \
131
+ -H "x-opencode-directory: ${_session_dir}" \
132
+ -d '{}' \
133
+ "http://localhost:${LACY_PREHEAT_SERVER_PORT}/session" 2>/dev/null); then
134
+ LACY_PREHEAT_SERVER_SESSION_ID=$(_lacy_json_get "$session_json" "id")
135
+ if [[ -n "$LACY_PREHEAT_SERVER_SESSION_ID" ]]; then
136
+ echo "$LACY_PREHEAT_SERVER_SESSION_ID" > "$LACY_PREHEAT_SERVER_SESSION_FILE"
137
+ return 0
138
+ fi
139
+ fi
140
+ return 1
141
+ }
142
+
143
+ # Send query to background server via REST API
144
+ lacy_preheat_server_query() {
145
+ local query="$1"
146
+
147
+ if [[ -z "$LACY_PREHEAT_SERVER_SESSION_ID" ]]; then
148
+ _lacy_preheat_server_create_session || return 1
149
+ fi
150
+
151
+ local escaped_query
152
+ escaped_query=$(_lacy_json_escape_str "$query")
153
+
154
+ # Pass the current working directory on every message request.
155
+ # lash/opencode wraps each request in Instance.provide({ directory }) so
156
+ # per-message directory takes effect even on an existing session — this
157
+ # preserves conversation continuity while keeping CWD always accurate.
158
+ local _msg_dir
159
+ _msg_dir=$(pwd 2>/dev/null)
160
+
161
+ local response
162
+ response=$(curl -sf --max-time "$LACY_SESSION_MESSAGE_TIMEOUT" \
163
+ -X POST \
164
+ -H "Content-Type: application/json" \
165
+ -H "x-opencode-directory: ${_msg_dir}" \
166
+ -d "{\"parts\": [{\"type\": \"text\", \"text\": \"${escaped_query}\"}]}" \
167
+ "http://localhost:${LACY_PREHEAT_SERVER_PORT}/session/${LACY_PREHEAT_SERVER_SESSION_ID}/message" 2>/dev/null)
168
+
169
+ local exit_code=$?
170
+ if [[ $exit_code -ne 0 ]]; then
171
+ LACY_PREHEAT_SERVER_SESSION_ID=""
172
+ rm -f "$LACY_PREHEAT_SERVER_SESSION_FILE"
173
+ return 1
174
+ fi
175
+
176
+ if command -v jq >/dev/null 2>&1; then
177
+ printf '%s\n' "$response" | jq -r '
178
+ if type == "array" then
179
+ [.[] | select(.role == "assistant") | .parts[]? | select(.type == "text") | .text] | last // empty
180
+ elif .parts then
181
+ [.parts[] | select(.type == "text") | .text] | join("\n") // empty
182
+ else
183
+ .result // .content // .text // .response // .message // empty
184
+ end' 2>/dev/null
185
+ elif command -v python3 >/dev/null 2>&1; then
186
+ printf '%s\n' "$response" | python3 -c "
187
+ import json, sys
188
+ data = sys.stdin.read().strip()
189
+ for line in reversed(data.split('\n')):
190
+ line = line.strip()
191
+ if not line: continue
192
+ try:
193
+ obj = json.loads(line)
194
+ if isinstance(obj, list):
195
+ for msg in reversed(obj):
196
+ if msg.get('role') == 'assistant':
197
+ texts = [p['text'] for p in msg.get('parts', []) if p.get('type') == 'text']
198
+ if texts: print('\n'.join(texts)); sys.exit(0)
199
+ elif isinstance(obj, dict):
200
+ parts = obj.get('parts', [])
201
+ texts = [p['text'] for p in parts if p.get('type') == 'text']
202
+ if texts: print('\n'.join(texts)); sys.exit(0)
203
+ for key in ('result', 'content', 'text', 'response', 'message'):
204
+ val = obj.get(key)
205
+ if val and isinstance(val, str): print(val); sys.exit(0)
206
+ except (json.JSONDecodeError, KeyError, TypeError): continue
207
+ print(data)" 2>/dev/null
208
+ else
209
+ printf '%s' "$response" | sed 's/.*"text"[[:space:]]*:[[:space:]]*"//' | sed 's/"[[:space:]]*[,}\]].*//' | sed 's/\\n/\'$'\n''/g; s/\\"/"/g; s/\\\\/\\/g'
210
+ fi
211
+ }
212
+
213
+ # Stop background server and clean up
214
+ lacy_preheat_server_stop() {
215
+ if [[ -n "$LACY_PREHEAT_SERVER_PID" ]]; then
216
+ kill "$LACY_PREHEAT_SERVER_PID" 2>/dev/null
217
+ wait "$LACY_PREHEAT_SERVER_PID" 2>/dev/null
218
+ LACY_PREHEAT_SERVER_PID=""
219
+ fi
220
+
221
+ if [[ -f "$LACY_PREHEAT_SERVER_PID_FILE" ]]; then
222
+ local file_pid
223
+ file_pid=$(cat "$LACY_PREHEAT_SERVER_PID_FILE" 2>/dev/null)
224
+ if [[ -n "$file_pid" ]]; then
225
+ kill "$file_pid" 2>/dev/null
226
+ wait "$file_pid" 2>/dev/null
227
+ fi
228
+ rm -f "$LACY_PREHEAT_SERVER_PID_FILE"
229
+ fi
230
+
231
+ LACY_PREHEAT_SERVER_PASSWORD=""
232
+ LACY_PREHEAT_SERVER_SESSION_ID=""
233
+ rm -f "$LACY_PREHEAT_SERVER_SESSION_FILE"
234
+ }
235
+
236
+ # Restore server session ID from file (survives subshell boundary)
237
+ lacy_preheat_server_restore_session() {
238
+ if [[ -z "$LACY_PREHEAT_SERVER_SESSION_ID" && -f "$LACY_PREHEAT_SERVER_SESSION_FILE" ]]; then
239
+ LACY_PREHEAT_SERVER_SESSION_ID=$(cat "$LACY_PREHEAT_SERVER_SESSION_FILE" 2>/dev/null)
240
+ fi
241
+ }
242
+
243
+ # ============================================================================
244
+ # Generic Session Reuse
245
+ # ============================================================================
246
+
247
+ # Internal helper to restore a session ID from a file
248
+ _lacy_session_restore() {
249
+ local file="$1"
250
+ local var_name="$2"
251
+ if [[ -f "$file" ]]; then
252
+ local _val
253
+ _val=$(cat "$file" 2>/dev/null)
254
+ printf -v "$var_name" '%s' "$_val"
255
+ fi
256
+ }
257
+
258
+ # Internal helper to build a tool command with optional session resume
259
+ _lacy_session_build_cmd() {
260
+ local tool="$1"
261
+ local session_id="$2"
262
+ local file="$3"
263
+ local var_name="$4"
264
+
265
+ # Ensure we have the latest session ID from the file (subshell workaround)
266
+ if [[ -z "$session_id" ]]; then
267
+ _lacy_session_restore "$file" "$var_name"
268
+ if [[ "$LACY_SHELL_TYPE" == "zsh" ]]; then
269
+ session_id="${(P)var_name}"
270
+ else
271
+ session_id="${!var_name}"
272
+ fi
273
+ fi
274
+
275
+ local parts="${tool}"
276
+ [[ -n "$session_id" ]] && parts+=" --resume ${session_id}"
277
+ if [[ "$tool" == "claude" ]]; then
278
+ parts+=" --output-format json"
279
+ fi
280
+ parts+=" -p"
281
+ echo "$parts"
282
+ }
283
+
284
+ # Internal helper to capture a session ID from JSON response and persist it
285
+ _lacy_session_capture() {
286
+ local json="$1"
287
+ local file="$2"
288
+ local var_name="$3"
289
+ local key_name="${4:-session_id}"
290
+ local session_id
291
+ session_id=$(_lacy_json_get "$json" "$key_name")
292
+
293
+ if [[ -n "$session_id" ]]; then
294
+ printf -v "$var_name" '%s' "$session_id"
295
+ echo "$session_id" > "$file"
296
+ fi
297
+ }
298
+
299
+ # Internal helper to reset a session
300
+ _lacy_session_reset() {
301
+ local file="$1"
302
+ local var_name="$2"
303
+ printf -v "$var_name" '%s' ""
304
+ rm -f "$file"
305
+ }
306
+
307
+ # ============================================================================
308
+ # Claude Session Reuse
309
+ # ============================================================================
310
+
311
+ lacy_preheat_claude_restore_session() {
312
+ _lacy_session_restore "$LACY_PREHEAT_SESSION_FILE" "LACY_PREHEAT_CLAUDE_SESSION_ID"
313
+ }
314
+
315
+ lacy_preheat_claude_build_cmd() {
316
+ _lacy_session_build_cmd "claude" "$LACY_PREHEAT_CLAUDE_SESSION_ID" "$LACY_PREHEAT_SESSION_FILE" "LACY_PREHEAT_CLAUDE_SESSION_ID"
317
+ }
318
+
319
+ lacy_preheat_claude_capture_session() {
320
+ _lacy_session_capture "$1" "$LACY_PREHEAT_SESSION_FILE" "LACY_PREHEAT_CLAUDE_SESSION_ID" "session_id"
321
+ }
322
+
323
+ lacy_preheat_claude_extract_result() {
324
+ _lacy_json_get "$1" "result"
325
+ }
326
+
327
+ lacy_preheat_claude_reset_session() {
328
+ _lacy_session_reset "$LACY_PREHEAT_SESSION_FILE" "LACY_PREHEAT_CLAUDE_SESSION_ID"
329
+ }
330
+
331
+ # ============================================================================
332
+ # Gemini Session Reuse
333
+ # ============================================================================
334
+
335
+ LACY_GEMINI_SESSION_ID=""
336
+ LACY_GEMINI_SESSION_ID_FILE="$LACY_SHELL_HOME/.gemini_session_id_$$"
337
+
338
+ lacy_preheat_gemini_restore_session() {
339
+ _lacy_session_restore "$LACY_GEMINI_SESSION_ID_FILE" "LACY_GEMINI_SESSION_ID"
340
+ }
341
+
342
+ lacy_preheat_gemini_build_cmd() {
343
+ _lacy_session_build_cmd "gemini" "$LACY_GEMINI_SESSION_ID" "$LACY_GEMINI_SESSION_ID_FILE" "LACY_GEMINI_SESSION_ID"
344
+ }
345
+
346
+ lacy_preheat_gemini_capture_session() {
347
+ _lacy_session_capture "$1" "$LACY_GEMINI_SESSION_ID_FILE" "LACY_GEMINI_SESSION_ID" "session_id"
348
+ }
349
+
350
+ lacy_preheat_gemini_extract_result() {
351
+ _lacy_json_get "$1" "response"
352
+ }
353
+
354
+ lacy_preheat_gemini_reset_session() {
355
+ _lacy_session_reset "$LACY_GEMINI_SESSION_ID_FILE" "LACY_GEMINI_SESSION_ID"
356
+ }
357
+
358
+ # ============================================================================
359
+ # Session Commands (new / resume)
360
+ # ============================================================================
361
+
362
+ # Return the active tool name: LACY_ACTIVE_TOOL if set, else first installed tool found.
363
+ _lacy_get_current_tool() {
364
+ if [[ -n "${LACY_ACTIVE_TOOL:-}" ]]; then
365
+ echo "$LACY_ACTIVE_TOOL"
366
+ return
367
+ fi
368
+ local t
369
+ for t in "${LACY_TOOL_LIST[@]}"; do
370
+ command -v "$t" >/dev/null 2>&1 && { echo "$t"; return; }
371
+ done
372
+ }
373
+
374
+ # Persist current session state to global file for cross-shell resume.
375
+ # Called after each successful agent query via _lacy_print_resume_hint.
376
+ _lacy_save_last_session() {
377
+ local tool
378
+ tool=$(_lacy_get_current_tool)
379
+
380
+ local session_id=""
381
+ case "$tool" in
382
+ lash|opencode) session_id="$LACY_PREHEAT_SERVER_SESSION_ID" ;;
383
+ claude) session_id="$LACY_PREHEAT_CLAUDE_SESSION_ID" ;;
384
+ gemini) session_id="$LACY_GEMINI_SESSION_ID" ;;
385
+ codex|hermes|copilot|goose|amp) session_id="default" ;;
386
+ esac
387
+
388
+ [[ -n "$session_id" && -n "$tool" ]] || return 0
389
+ printf '%s\n%s\n' "$tool" "$session_id" > "$LACY_LAST_SESSION_FILE"
390
+ }
391
+
392
+ # Clear all session state and start a fresh context.
393
+ # For server-based tools (lash/opencode), eagerly creates a new session (blocking).
394
+ lacy_session_new() {
395
+ # Persist current session before clearing (enables cross-shell resume)
396
+ _lacy_save_last_session
397
+
398
+ # Reset all per-session state
399
+ lacy_preheat_claude_reset_session
400
+ lacy_preheat_gemini_reset_session
401
+ LACY_PREHEAT_SERVER_SESSION_ID=""
402
+ rm -f "$LACY_PREHEAT_SERVER_SESSION_FILE"
403
+
404
+ # Reset terminal context so the next query sends full context
405
+ _lacy_ctx_reset
406
+
407
+ # For server-based tools, pre-create a new session now (blocking)
408
+ local tool
409
+ tool=$(_lacy_get_current_tool)
410
+
411
+ if [[ "$tool" == "lash" || "$tool" == "opencode" ]]; then
412
+ lacy_start_spinner
413
+ _lacy_preheat_server_create_session
414
+ lacy_stop_spinner
415
+ fi
416
+
417
+ echo ""
418
+ lacy_print_color 34 " New session started"
419
+ echo ""
420
+ }
421
+
422
+ # Resume the last saved session in the current shell.
423
+ # Reads from LACY_LAST_SESSION_FILE — written after every successful query.
424
+ lacy_session_resume() {
425
+ local saved_tool="" saved_id=""
426
+ if [[ -f "$LACY_LAST_SESSION_FILE" ]]; then
427
+ { read -r saved_tool; read -r saved_id; } < "$LACY_LAST_SESSION_FILE"
428
+ fi
429
+
430
+ if [[ -z "$saved_tool" || -z "$saved_id" ]]; then
431
+ echo ""
432
+ lacy_print_color 238 " No previous session to resume"
433
+ echo ""
434
+ return 1
435
+ fi
436
+
437
+ # Swap: save current session before overwriting (so it's resumable in turn)
438
+ _lacy_save_last_session
439
+
440
+ # Switch active tool to match the saved session
441
+ local current_tool
442
+ current_tool=$(_lacy_get_current_tool)
443
+ if [[ -n "$current_tool" && "$current_tool" != "$saved_tool" ]]; then
444
+ LACY_ACTIVE_TOOL="$saved_tool"
445
+ export LACY_ACTIVE_TOOL
446
+ fi
447
+
448
+ # Load saved session into current shell state
449
+ case "$saved_tool" in
450
+ lash|opencode)
451
+ LACY_PREHEAT_SERVER_SESSION_ID="$saved_id"
452
+ echo "$saved_id" > "$LACY_PREHEAT_SERVER_SESSION_FILE"
453
+ ;;
454
+ claude)
455
+ LACY_PREHEAT_CLAUDE_SESSION_ID="$saved_id"
456
+ echo "$saved_id" > "$LACY_PREHEAT_SESSION_FILE"
457
+ ;;
458
+ gemini)
459
+ LACY_GEMINI_SESSION_ID="$saved_id"
460
+ echo "$saved_id" > "$LACY_GEMINI_SESSION_ID_FILE"
461
+ ;;
462
+ codex|hermes|copilot|goose|amp)
463
+ ;;
464
+ esac
465
+
466
+ echo ""
467
+ lacy_print_color 34 " Resumed $saved_tool session"
468
+ lacy_print_color 238 " $saved_id"
469
+ echo ""
470
+ }
471
+
472
+ # ============================================================================
473
+ # Lifecycle
474
+ # ============================================================================
475
+
476
+ lacy_preheat_init() {
477
+ # Per-shell session files ensure a fresh session on every new shell start.
478
+ # We no longer need to restore here because the PID-specific file won't exist yet.
479
+
480
+ if [[ "$LACY_PREHEAT_EAGER" == "true" ]]; then
481
+ local tool="${LACY_ACTIVE_TOOL}"
482
+
483
+ if [[ "$tool" == "lash" || "$tool" == "opencode" ]]; then
484
+ _lacy_jobctl_off
485
+ lacy_preheat_server_start "$tool" &
486
+ disown 2>/dev/null
487
+ _lacy_jobctl_on
488
+ fi
489
+ fi
490
+ }
491
+
492
+ lacy_preheat_cleanup() {
493
+ lacy_preheat_server_stop
494
+ rm -f "$LACY_PREHEAT_SESSION_FILE" \
495
+ "$LACY_GEMINI_SESSION_ID_FILE"
496
+ }