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.
- package/.claude/settings.local.json +26 -0
- package/.github/FUNDING.yml +3 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +49 -0
- package/.github/ISSUE_TEMPLATE/config.yml +5 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +28 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +17 -0
- package/.github/SECURITY.md +32 -0
- package/.github/assets/logo-horizontal-dark.png +0 -0
- package/.github/assets/logo-horizontal-dark.svg +17 -0
- package/.github/assets/logo-horizontal.png +0 -0
- package/.github/assets/logo-horizontal.svg +17 -0
- package/.github/assets/logo.png +0 -0
- package/.github/assets/logo.svg +12 -0
- package/.github/assets/social-preview.png +0 -0
- package/.github/assets/social-preview.svg +50 -0
- package/.github/dependabot.yml +21 -0
- package/.github/workflows/ci.yml +80 -0
- package/.github/workflows/dependabot-auto-merge.yml +32 -0
- package/CHANGELOG.md +366 -0
- package/CLAUDE.md +340 -0
- package/CONTRIBUTING.md +141 -0
- package/LICENSE +110 -0
- package/README.md +201 -31
- package/RELEASING.md +148 -0
- package/STYLE.md +202 -0
- package/assets/hero.jpeg +0 -0
- package/assets/mode-indicators.jpeg +0 -0
- package/assets/real-time-indicator.jpeg +0 -0
- package/assets/supported-tools.jpeg +0 -0
- package/bin/lacy +1028 -0
- package/docs/ADDING-BACKENDS.md +124 -0
- package/docs/DEVTO-ARTICLE.md +94 -0
- package/docs/DOCS.md +68 -0
- package/docs/GROWTH-STRATEGY.md +119 -0
- package/docs/HN-RESPONSES.md +122 -0
- package/docs/LAUNCH-COPY-FINAL.md +105 -0
- package/docs/MARKETING.md +411 -0
- package/docs/NATURAL_LANGUAGE_DETECTION.md +204 -0
- package/docs/UGC_VIDEO_SCRIPT.md +114 -0
- package/docs/articles/devto-how-i-made-my-terminal-understand-english.md +117 -0
- package/docs/demo-color-transition.gif +0 -0
- package/docs/demo-full.gif +0 -0
- package/docs/demo-indicator.gif +0 -0
- package/docs/launch-thread-may6.sh +158 -0
- package/docs/videos/README.md +189 -0
- package/docs/videos/generate_frames.py +510 -0
- package/docs/videos/generate_frames_v2.py +729 -0
- package/docs/videos/generate_short.py +328 -0
- package/docs/videos/generate_short_v2.py +526 -0
- package/docs/videos/lacy-shell-demo-v2.mp4 +0 -0
- package/docs/videos/lacy-shell-demo.mp4 +0 -0
- package/docs/videos/lacy-shell-short-v2.mp4 +0 -0
- package/docs/videos/lacy-shell-short.mp4 +0 -0
- package/install.sh +1009 -0
- package/lacy.plugin.bash +75 -0
- package/lacy.plugin.fish +43 -0
- package/lacy.plugin.zsh +65 -0
- package/lib/animations.zsh +3 -0
- package/lib/bash/completions.bash +40 -0
- package/lib/bash/execute.bash +233 -0
- package/lib/bash/init.bash +40 -0
- package/lib/bash/keybindings.bash +134 -0
- package/lib/bash/prompt.bash +85 -0
- package/lib/commands/info.sh +25 -0
- package/lib/config.zsh +3 -0
- package/lib/constants.zsh +3 -0
- package/lib/core/animations.sh +271 -0
- package/lib/core/commands.sh +297 -0
- package/lib/core/config.sh +340 -0
- package/lib/core/constants.sh +366 -0
- package/lib/core/context.sh +260 -0
- package/lib/core/detection.sh +417 -0
- package/lib/core/mcp.sh +741 -0
- package/lib/core/modes.sh +123 -0
- package/lib/core/preheat.sh +496 -0
- package/lib/core/spinner.sh +174 -0
- package/lib/core/telemetry.sh +99 -0
- package/lib/detection.zsh +3 -0
- package/lib/execute.zsh +3 -0
- package/lib/fish/config.fish +66 -0
- package/lib/fish/detection.fish +90 -0
- package/lib/fish/execute.fish +105 -0
- package/lib/fish/keybindings.fish +42 -0
- package/lib/fish/prompt.fish +30 -0
- package/lib/keybindings.zsh +3 -0
- package/lib/mcp.zsh +3 -0
- package/lib/modes.zsh +3 -0
- package/lib/preheat.zsh +3 -0
- package/lib/prompt.zsh +3 -0
- package/lib/spinner.zsh +3 -0
- package/lib/zsh/completions.zsh +60 -0
- package/lib/zsh/execute.zsh +294 -0
- package/lib/zsh/init.zsh +26 -0
- package/lib/zsh/keybindings.zsh +551 -0
- package/lib/zsh/prompt.zsh +90 -0
- package/package.json +42 -27
- package/packages/lacy/README.md +61 -0
- package/packages/lacy/commands/info.sh +25 -0
- package/{index.mjs → packages/lacy/index.mjs} +247 -20
- package/packages/lacy/package-lock.json +71 -0
- package/packages/lacy/package.json +42 -0
- package/script/release.ts +487 -0
- package/squirrel.toml +36 -0
- package/tests/test_bash.bash +163 -0
- package/tests/test_core.sh +607 -0
- package/tests/test_gemini.sh +119 -0
- package/tests/test_gemini_mcp.sh +126 -0
- package/tests/test_preheat_server.zsh +446 -0
- 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
|
+
}
|