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
package/lib/core/mcp.sh
ADDED
|
@@ -0,0 +1,741 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
# Agent query functions for Lacy Shell
|
|
4
|
+
# Routes queries to configured AI CLI tools
|
|
5
|
+
# Shared across Bash 4+ and ZSH
|
|
6
|
+
|
|
7
|
+
# ============================================================================
|
|
8
|
+
# JSON Extraction Helpers
|
|
9
|
+
# ============================================================================
|
|
10
|
+
|
|
11
|
+
# Extract a value from JSON using the best available tool (jq > python3 > grep).
|
|
12
|
+
# For top-level fields: _lacy_json_get "$json" "field_name"
|
|
13
|
+
# Returns the field value on stdout, or empty string if not found.
|
|
14
|
+
_lacy_json_get() {
|
|
15
|
+
local json="$1"
|
|
16
|
+
local field="$2"
|
|
17
|
+
|
|
18
|
+
if command -v jq >/dev/null 2>&1; then
|
|
19
|
+
printf '%s\n' "$json" | jq -r --arg f "$field" '.[$f] // empty' 2>/dev/null
|
|
20
|
+
elif command -v python3 >/dev/null 2>&1; then
|
|
21
|
+
printf '%s\n' "$json" | python3 -c "
|
|
22
|
+
import json, sys
|
|
23
|
+
try:
|
|
24
|
+
d = json.loads(sys.stdin.read())
|
|
25
|
+
v = d.get('$field')
|
|
26
|
+
if v is not None:
|
|
27
|
+
print(v if isinstance(v, str) else json.dumps(v))
|
|
28
|
+
except: pass" 2>/dev/null
|
|
29
|
+
else
|
|
30
|
+
# Grep fallback — handles simple "key": "value" and "key": true/false/number
|
|
31
|
+
local val
|
|
32
|
+
val=$(printf '%s' "$json" | grep -o "\"${field}\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | head -1 | sed "s/\"${field}\"[[:space:]]*:[[:space:]]*\"//" | sed 's/"$//')
|
|
33
|
+
if [[ -n "$val" ]]; then
|
|
34
|
+
printf '%s' "$val"
|
|
35
|
+
else
|
|
36
|
+
# Try unquoted values (booleans, numbers)
|
|
37
|
+
printf '%s' "$json" | grep -o "\"${field}\"[[:space:]]*:[[:space:]]*[^,}\"]*" | head -1 | sed "s/\"${field}\"[[:space:]]*:[[:space:]]*//" | tr -d ' '
|
|
38
|
+
fi
|
|
39
|
+
fi
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# Run an arbitrary query expression against JSON (jq syntax, python3 fallback).
|
|
43
|
+
# Usage: _lacy_json_query "$json" '.choices[0].message.content'
|
|
44
|
+
# The second argument is a jq expression. A python3 equivalent is auto-generated
|
|
45
|
+
# for common patterns: .a.b.c and .a[N].b.c
|
|
46
|
+
# Returns empty string if the query fails or tools are unavailable.
|
|
47
|
+
_lacy_json_query() {
|
|
48
|
+
local json="$1"
|
|
49
|
+
local expr="$2"
|
|
50
|
+
|
|
51
|
+
if command -v jq >/dev/null 2>&1; then
|
|
52
|
+
printf '%s\n' "$json" | jq -r "$expr // empty" 2>/dev/null
|
|
53
|
+
elif command -v python3 >/dev/null 2>&1; then
|
|
54
|
+
printf '%s\n' "$json" | python3 -c "
|
|
55
|
+
import json, sys, re
|
|
56
|
+
try:
|
|
57
|
+
d = json.loads(sys.stdin.read())
|
|
58
|
+
# Parse jq-like expression: .key[0].key2
|
|
59
|
+
parts = re.findall(r'\.(\w+)|\[(\d+)\]', '''$expr''')
|
|
60
|
+
obj = d
|
|
61
|
+
for key, idx in parts:
|
|
62
|
+
if key:
|
|
63
|
+
obj = obj[key]
|
|
64
|
+
else:
|
|
65
|
+
obj = obj[int(idx)]
|
|
66
|
+
if obj is not None:
|
|
67
|
+
print(obj if isinstance(obj, str) else json.dumps(obj))
|
|
68
|
+
except: pass" 2>/dev/null
|
|
69
|
+
else
|
|
70
|
+
# No structured parser available — return empty
|
|
71
|
+
return 1
|
|
72
|
+
fi
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
# ============================================================================
|
|
76
|
+
# Markdown Rendering
|
|
77
|
+
# ============================================================================
|
|
78
|
+
|
|
79
|
+
# Cached renderer (set on first call)
|
|
80
|
+
_LACY_MD_RENDERER=""
|
|
81
|
+
|
|
82
|
+
# Render markdown for terminal display.
|
|
83
|
+
# Uses glow if available, otherwise a basic sed fallback for bold/headers/code.
|
|
84
|
+
# Usage: _lacy_render_markdown "$text"
|
|
85
|
+
_lacy_render_markdown() {
|
|
86
|
+
local text="$1"
|
|
87
|
+
[[ -z "$text" ]] && return
|
|
88
|
+
|
|
89
|
+
# Auto-detect on first call
|
|
90
|
+
if [[ -z "$_LACY_MD_RENDERER" ]]; then
|
|
91
|
+
if command -v glow >/dev/null 2>&1; then
|
|
92
|
+
_LACY_MD_RENDERER="glow"
|
|
93
|
+
else
|
|
94
|
+
_LACY_MD_RENDERER="basic"
|
|
95
|
+
fi
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
case "$_LACY_MD_RENDERER" in
|
|
99
|
+
glow) printf '%s\n' "$text" | glow -s dark ;;
|
|
100
|
+
basic) _lacy_render_markdown_basic "$text" ;;
|
|
101
|
+
esac
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# Minimal markdown rendering via sed — bold, headers, inline code, rules.
|
|
105
|
+
# Uses literal escape chars (via $'') for BSD/GNU sed portability.
|
|
106
|
+
_lacy_render_markdown_basic() {
|
|
107
|
+
local bold=$'\e[1m' nobold=$'\e[22m'
|
|
108
|
+
local cyan=$'\e[36m' reset=$'\e[0m'
|
|
109
|
+
local dim=$'\e[38;5;238m'
|
|
110
|
+
local cols; cols=$(tput cols 2>/dev/null || echo 80)
|
|
111
|
+
local hr="${dim}$(printf '%*s' "$cols" | tr ' ' '─')${reset}"
|
|
112
|
+
|
|
113
|
+
printf '%s\n' "$1" | sed -E \
|
|
114
|
+
-e "s/^#{1,6}[[:space:]]+(.*)/${bold}\1${nobold}/" \
|
|
115
|
+
-e "s/^---*$/${hr}/" \
|
|
116
|
+
-e "s/^\*\*\*.*$/${hr}/" \
|
|
117
|
+
-e "s/\*\*([^*]*)\*\*/${bold}\1${nobold}/g" \
|
|
118
|
+
-e "s/\`([^\`]*)\`/${cyan}\1${reset}/g"
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
# ============================================================================
|
|
122
|
+
# Tool Command Execution
|
|
123
|
+
# ============================================================================
|
|
124
|
+
|
|
125
|
+
# Run a tool command safely — splits command string into array to avoid eval.
|
|
126
|
+
# Usage: _lacy_run_tool_cmd "cmd string" "query"
|
|
127
|
+
_lacy_run_tool_cmd() {
|
|
128
|
+
local cmd_str="$1"
|
|
129
|
+
local query="$2"
|
|
130
|
+
local -a cmd_parts
|
|
131
|
+
if [[ "$LACY_SHELL_TYPE" == "zsh" ]]; then
|
|
132
|
+
cmd_parts=( ${=cmd_str} )
|
|
133
|
+
else
|
|
134
|
+
read -ra cmd_parts <<< "$cmd_str"
|
|
135
|
+
fi
|
|
136
|
+
"${cmd_parts[@]}" "$query"
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
# Internal helper to build and run a Gemini query with session context and spinner.
|
|
140
|
+
# Returns 0 on success, non-zero on error. Outputs tool response to stdout.
|
|
141
|
+
# NOTE: Uses LACY_GEMINI_SESSION_ID which is managed in lib/core/preheat.sh.
|
|
142
|
+
_lacy_gemini_query_exec() {
|
|
143
|
+
local query="$1"
|
|
144
|
+
local gemini_cmd
|
|
145
|
+
gemini_cmd=$(lacy_preheat_gemini_build_cmd)
|
|
146
|
+
|
|
147
|
+
# Only include context on the first message of a session (when ID is empty)
|
|
148
|
+
local gemini_query
|
|
149
|
+
if [[ -z "$LACY_GEMINI_SESSION_ID" ]]; then
|
|
150
|
+
local _gemini_ctx
|
|
151
|
+
_gemini_ctx="${LACY_GEMINI_CONTEXT//\{cwd\}/$(pwd 2>/dev/null)}"
|
|
152
|
+
gemini_query="$_gemini_ctx $query"
|
|
153
|
+
else
|
|
154
|
+
gemini_query="$query"
|
|
155
|
+
fi
|
|
156
|
+
|
|
157
|
+
if [[ -t 0 ]]; then
|
|
158
|
+
_lacy_run_tool_cmd "$gemini_cmd" "$gemini_query" </dev/tty 2>/dev/null
|
|
159
|
+
else
|
|
160
|
+
_lacy_run_tool_cmd "$gemini_cmd" "$gemini_query" 2>/dev/null
|
|
161
|
+
fi
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
# Tool registry — function-based for maximum portability
|
|
165
|
+
# Usage: cmd=$(lacy_tool_cmd <tool_name>)
|
|
166
|
+
lacy_tool_cmd() {
|
|
167
|
+
case "$1" in
|
|
168
|
+
lash) echo "lash run -c" ;;
|
|
169
|
+
claude) echo "claude -p" ;;
|
|
170
|
+
opencode) echo "opencode run -c" ;;
|
|
171
|
+
gemini) echo "gemini -p" ;;
|
|
172
|
+
codex) echo "codex exec resume --last" ;;
|
|
173
|
+
hermes) echo "hermes chat -q" ;;
|
|
174
|
+
copilot) echo "copilot -p" ;;
|
|
175
|
+
goose) echo "goose run -t" ;;
|
|
176
|
+
amp) echo "amp -x" ;;
|
|
177
|
+
aider) echo "aider --no-auto-commits --message" ;;
|
|
178
|
+
*) echo "" ;;
|
|
179
|
+
esac
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
# Active tool (set during install or via config)
|
|
183
|
+
: "${LACY_ACTIVE_TOOL:=""}"
|
|
184
|
+
|
|
185
|
+
# Last resume command (set after each successful agent query)
|
|
186
|
+
LACY_LAST_RESUME_CMD=""
|
|
187
|
+
|
|
188
|
+
# Resume command registry — returns the command to resume a conversation
|
|
189
|
+
# Usage: cmd=$(lacy_resume_cmd <tool_name>)
|
|
190
|
+
lacy_resume_cmd() {
|
|
191
|
+
case "$1" in
|
|
192
|
+
claude)
|
|
193
|
+
[[ -n "$LACY_PREHEAT_CLAUDE_SESSION_ID" ]] && \
|
|
194
|
+
echo "claude --resume $LACY_PREHEAT_CLAUDE_SESSION_ID"
|
|
195
|
+
;;
|
|
196
|
+
lash)
|
|
197
|
+
[[ -n "$LACY_PREHEAT_SERVER_SESSION_ID" ]] && \
|
|
198
|
+
echo "lash --session $LACY_PREHEAT_SERVER_SESSION_ID"
|
|
199
|
+
;;
|
|
200
|
+
opencode)
|
|
201
|
+
[[ -n "$LACY_PREHEAT_SERVER_SESSION_ID" ]] && \
|
|
202
|
+
echo "opencode --session $LACY_PREHEAT_SERVER_SESSION_ID"
|
|
203
|
+
;;
|
|
204
|
+
gemini)
|
|
205
|
+
[[ -n "$LACY_GEMINI_SESSION_ID" ]] && \
|
|
206
|
+
echo "gemini --resume $LACY_GEMINI_SESSION_ID"
|
|
207
|
+
;;
|
|
208
|
+
codex) echo "codex exec resume --last" ;;
|
|
209
|
+
hermes) echo "hermes --continue" ;;
|
|
210
|
+
copilot) echo "copilot --resume" ;;
|
|
211
|
+
goose) echo "goose session resume" ;;
|
|
212
|
+
amp) echo "amp --continue" ;;
|
|
213
|
+
esac
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
# Print resume hint after successful agent query
|
|
217
|
+
# Usage: _lacy_print_resume_hint <tool_name>
|
|
218
|
+
_lacy_print_resume_hint() {
|
|
219
|
+
local tool="$1"
|
|
220
|
+
local resume_cmd
|
|
221
|
+
resume_cmd=$(lacy_resume_cmd "$tool")
|
|
222
|
+
|
|
223
|
+
if [[ -n "$resume_cmd" ]]; then
|
|
224
|
+
LACY_LAST_RESUME_CMD="$resume_cmd"
|
|
225
|
+
lacy_print_color 238 "$resume_cmd"
|
|
226
|
+
# Persist for cross-shell resume (lacy /resume in a new shell)
|
|
227
|
+
_lacy_save_last_session
|
|
228
|
+
fi
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
# Format tool error output — detects JSON error blobs and prints a clean message.
|
|
232
|
+
# Returns 0 if an error was detected and formatted, 1 if output is not a tool error.
|
|
233
|
+
# Usage: lacy_format_tool_error "$output" "$tool_name"
|
|
234
|
+
lacy_format_tool_error() {
|
|
235
|
+
local output="$1"
|
|
236
|
+
local tool="${2:-agent}"
|
|
237
|
+
|
|
238
|
+
# Quick check: does it look like JSON with an error?
|
|
239
|
+
[[ "$output" == "{"* ]] || return 1
|
|
240
|
+
|
|
241
|
+
local is_error="" result_text=""
|
|
242
|
+
is_error=$(_lacy_json_get "$output" "is_error")
|
|
243
|
+
result_text=$(_lacy_json_get "$output" "result")
|
|
244
|
+
|
|
245
|
+
[[ "$is_error" == "true" ]] || return 1
|
|
246
|
+
|
|
247
|
+
# We have an error — format it nicely
|
|
248
|
+
local red=196
|
|
249
|
+
local dim=238
|
|
250
|
+
local yellow=220
|
|
251
|
+
|
|
252
|
+
echo ""
|
|
253
|
+
lacy_print_color "$red" " Error from ${tool}"
|
|
254
|
+
echo ""
|
|
255
|
+
if [[ -n "$result_text" ]]; then
|
|
256
|
+
# Split on " · " delimiter that Claude uses
|
|
257
|
+
local IFS_BAK="$IFS"
|
|
258
|
+
local msg="$result_text"
|
|
259
|
+
local main_msg="" hint_msg=""
|
|
260
|
+
if [[ "$msg" == *" · "* ]]; then
|
|
261
|
+
main_msg="${msg%% · *}"
|
|
262
|
+
hint_msg="${msg#* · }"
|
|
263
|
+
else
|
|
264
|
+
main_msg="$msg"
|
|
265
|
+
fi
|
|
266
|
+
lacy_print_color "$yellow" " ${main_msg}"
|
|
267
|
+
if [[ -n "$hint_msg" ]]; then
|
|
268
|
+
echo ""
|
|
269
|
+
lacy_print_color "$dim" " ${hint_msg}"
|
|
270
|
+
fi
|
|
271
|
+
else
|
|
272
|
+
lacy_print_color "$yellow" " The agent returned an error (no details available)"
|
|
273
|
+
fi
|
|
274
|
+
echo ""
|
|
275
|
+
return 0
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
# Strip non-JSON leading lines from captured output.
|
|
279
|
+
# Agent CLIs (e.g. claude) emit startup/build text to stderr which gets
|
|
280
|
+
# merged into stdout by 2>&1. This strips everything before the first '{'.
|
|
281
|
+
_lacy_strip_leading_noise() {
|
|
282
|
+
local output="$1"
|
|
283
|
+
while [[ -n "$output" && "$output" != "{"* ]]; do
|
|
284
|
+
# Remove everything up to and including the first newline
|
|
285
|
+
local rest="${output#*$'\n'}"
|
|
286
|
+
# If no newline found, the whole string is noise
|
|
287
|
+
[[ "$rest" == "$output" ]] && output="" && break
|
|
288
|
+
output="$rest"
|
|
289
|
+
done
|
|
290
|
+
printf '%s' "$output"
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
# Normalize claude JSON output — handles startup noise, JSON arrays, and NDJSON.
|
|
294
|
+
# Claude --output-format json wraps all events in a JSON array: [{init},{assistant},{result}]
|
|
295
|
+
# This extracts the last element (the result object) so downstream parsing works.
|
|
296
|
+
_lacy_claude_normalize_output() {
|
|
297
|
+
local output="$1"
|
|
298
|
+
|
|
299
|
+
# Strip non-JSON leading lines (agent startup text on stderr merged via 2>&1)
|
|
300
|
+
local stripped="$output"
|
|
301
|
+
while [[ -n "$stripped" && "$stripped" != "{"* && "$stripped" != "["* ]]; do
|
|
302
|
+
local rest="${stripped#*$'\n'}"
|
|
303
|
+
[[ "$rest" == "$stripped" ]] && stripped="" && break
|
|
304
|
+
stripped="$rest"
|
|
305
|
+
done
|
|
306
|
+
[[ -z "$stripped" ]] && stripped="$output"
|
|
307
|
+
|
|
308
|
+
# JSON array — extract the last element (the result object)
|
|
309
|
+
if [[ "$stripped" == "["* ]]; then
|
|
310
|
+
local last_obj=""
|
|
311
|
+
if command -v jq >/dev/null 2>&1; then
|
|
312
|
+
last_obj=$(printf '%s\n' "$stripped" | jq -c '.[-1]' 2>/dev/null)
|
|
313
|
+
elif command -v python3 >/dev/null 2>&1; then
|
|
314
|
+
last_obj=$(printf '%s\n' "$stripped" | python3 -c "
|
|
315
|
+
import json, sys
|
|
316
|
+
try:
|
|
317
|
+
d = json.loads(sys.stdin.read())
|
|
318
|
+
if isinstance(d, list): print(json.dumps(d[-1]))
|
|
319
|
+
except: pass" 2>/dev/null)
|
|
320
|
+
fi
|
|
321
|
+
[[ -n "$last_obj" ]] && stripped="$last_obj"
|
|
322
|
+
fi
|
|
323
|
+
|
|
324
|
+
printf '%s' "$stripped"
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
# Append a query entry to the query log (rotates at ~1000 lines or ~1 MB).
|
|
328
|
+
# Usage: _lacy_log_query "tool_name" "query_text"
|
|
329
|
+
_lacy_log_query() {
|
|
330
|
+
local tool="$1"
|
|
331
|
+
local query="$2"
|
|
332
|
+
local log_dir="${LACY_SHELL_HOME}/logs"
|
|
333
|
+
local log_file="${log_dir}/queries.log"
|
|
334
|
+
|
|
335
|
+
mkdir -p "$log_dir" 2>/dev/null || return 0
|
|
336
|
+
|
|
337
|
+
local ts
|
|
338
|
+
ts=$(date '+%Y-%m-%dT%H:%M:%S' 2>/dev/null || echo "unknown")
|
|
339
|
+
local escaped_query="${query//$'\n'/\\n}"
|
|
340
|
+
printf '%s\t%s\t%s\n' "$ts" "$tool" "$escaped_query" >> "$log_file" 2>/dev/null || true
|
|
341
|
+
|
|
342
|
+
# Rotate: keep last 1000 lines if file is large
|
|
343
|
+
local size
|
|
344
|
+
if [[ -f "$log_file" ]]; then
|
|
345
|
+
size=$(wc -c < "$log_file" 2>/dev/null || echo 0)
|
|
346
|
+
if (( size > 1048576 )); then
|
|
347
|
+
local tmp
|
|
348
|
+
tmp=$(mktemp) && tail -n 1000 "$log_file" > "$tmp" && mv "$tmp" "$log_file" 2>/dev/null || true
|
|
349
|
+
fi
|
|
350
|
+
fi
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
# Send query to AI agent (configurable tool or fallback)
|
|
354
|
+
lacy_shell_query_agent() {
|
|
355
|
+
local query="$1"
|
|
356
|
+
local tool="${LACY_ACTIVE_TOOL}"
|
|
357
|
+
|
|
358
|
+
# Prepend delta-based terminal context (cwd, git, exit code, recent commands).
|
|
359
|
+
# Only includes what changed since the last query — zero overhead when idle.
|
|
360
|
+
# Uses result variable (not subshell) so state resets propagate.
|
|
361
|
+
_lacy_build_query_context "$query"
|
|
362
|
+
query="$_LACY_CTX_RESULT"
|
|
363
|
+
|
|
364
|
+
# Auto-detect if not set
|
|
365
|
+
local _auto_detected=false
|
|
366
|
+
if [[ -z "$tool" ]]; then
|
|
367
|
+
local t
|
|
368
|
+
for t in "${LACY_TOOL_LIST[@]}"; do
|
|
369
|
+
if command -v "$t" >/dev/null 2>&1; then
|
|
370
|
+
tool="$t"
|
|
371
|
+
_auto_detected=true
|
|
372
|
+
break
|
|
373
|
+
fi
|
|
374
|
+
done
|
|
375
|
+
fi
|
|
376
|
+
|
|
377
|
+
# If still no tool, try API fallback
|
|
378
|
+
if [[ -z "$tool" ]]; then
|
|
379
|
+
if lacy_shell_check_api_keys; then
|
|
380
|
+
local temp_file
|
|
381
|
+
temp_file=$(mktemp)
|
|
382
|
+
cat > "$temp_file" << EOF
|
|
383
|
+
Query: $query
|
|
384
|
+
EOF
|
|
385
|
+
echo ""
|
|
386
|
+
lacy_start_spinner
|
|
387
|
+
lacy_shell_send_to_ai_streaming "$temp_file" "$query"
|
|
388
|
+
local exit_code=$?
|
|
389
|
+
lacy_stop_spinner
|
|
390
|
+
rm -f "$temp_file"
|
|
391
|
+
echo ""
|
|
392
|
+
return $exit_code
|
|
393
|
+
fi
|
|
394
|
+
|
|
395
|
+
echo ""
|
|
396
|
+
printf '\e[38;5;196m No AI tool detected.\e[0m Lacy needs an AI CLI to handle queries.\n'
|
|
397
|
+
echo ""
|
|
398
|
+
printf '\e[1m Supported tools:\e[0m\n'
|
|
399
|
+
echo ""
|
|
400
|
+
printf ' \e[38;5;34m%-12s\e[0m %s\n' "lash" "npm install -g lashcode (recommended)"
|
|
401
|
+
printf ' \e[38;5;238m%-12s\e[0m %s\n' "claude" "brew install claude"
|
|
402
|
+
printf ' \e[38;5;238m%-12s\e[0m %s\n' "opencode" "brew install opencode"
|
|
403
|
+
printf ' \e[38;5;238m%-12s\e[0m %s\n' "gemini" "brew install gemini"
|
|
404
|
+
printf ' \e[38;5;238m%-12s\e[0m %s\n' "codex" "npm install -g @openai/codex"
|
|
405
|
+
printf ' \e[38;5;238m%-12s\e[0m %s\n' "hermes" "curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash"
|
|
406
|
+
printf ' \e[38;5;238m%-12s\e[0m %s\n' "copilot" "gh extension install github/gh-copilot"
|
|
407
|
+
printf ' \e[38;5;238m%-12s\e[0m %s\n' "goose" "brew install goose"
|
|
408
|
+
printf ' \e[38;5;238m%-12s\e[0m %s\n' "amp" "npm install -g @sourcegraph/amp"
|
|
409
|
+
printf ' \e[38;5;238m%-12s\e[0m %s\n' "aider" "pipx install aider-chat"
|
|
410
|
+
echo ""
|
|
411
|
+
printf ' \e[38;5;75mThen run:\e[0m lacy setup\n'
|
|
412
|
+
printf ' \e[38;5;75mDocs:\e[0m %s\n' "$LACY_DOCS_URL"
|
|
413
|
+
echo ""
|
|
414
|
+
|
|
415
|
+
# Offer to install lash interactively if terminal is available
|
|
416
|
+
local can_prompt=false
|
|
417
|
+
if [[ -t 0 ]]; then
|
|
418
|
+
can_prompt=true
|
|
419
|
+
elif [[ -c /dev/tty ]]; then
|
|
420
|
+
can_prompt=true
|
|
421
|
+
fi
|
|
422
|
+
|
|
423
|
+
if [[ "$can_prompt" == true ]]; then
|
|
424
|
+
local install_now=""
|
|
425
|
+
printf ' Install \e[38;5;34mlash\e[0m now? (AI coding agent — lash.lacy.sh)\n'
|
|
426
|
+
echo ""
|
|
427
|
+
if [[ -t 0 ]]; then
|
|
428
|
+
read -p " [Y/n]: " install_now
|
|
429
|
+
else
|
|
430
|
+
read -p " [Y/n]: " install_now < /dev/tty 2>/dev/null || install_now="n"
|
|
431
|
+
fi
|
|
432
|
+
|
|
433
|
+
if [[ ! "$install_now" =~ ^[Nn]$ ]]; then
|
|
434
|
+
echo ""
|
|
435
|
+
if command -v npm >/dev/null 2>&1; then
|
|
436
|
+
echo " Installing lash..."
|
|
437
|
+
if npm install -g lashcode; then
|
|
438
|
+
echo ""
|
|
439
|
+
printf ' \e[38;5;34m✓\e[0m lash installed! Re-running your query...\n'
|
|
440
|
+
echo ""
|
|
441
|
+
tool="lash"
|
|
442
|
+
else
|
|
443
|
+
echo ""
|
|
444
|
+
printf ' \e[38;5;196m✗\e[0m Installation failed. Try manually: npm install -g lashcode\n'
|
|
445
|
+
return 1
|
|
446
|
+
fi
|
|
447
|
+
elif command -v brew >/dev/null 2>&1; then
|
|
448
|
+
echo " Installing lash..."
|
|
449
|
+
if brew tap lacymorrow/tap && brew install lash; then
|
|
450
|
+
echo ""
|
|
451
|
+
printf ' \e[38;5;34m✓\e[0m lash installed! Re-running your query...\n'
|
|
452
|
+
echo ""
|
|
453
|
+
tool="lash"
|
|
454
|
+
else
|
|
455
|
+
echo ""
|
|
456
|
+
printf ' \e[38;5;196m✗\e[0m Installation failed. Try manually: brew install lacymorrow/tap/lash\n'
|
|
457
|
+
return 1
|
|
458
|
+
fi
|
|
459
|
+
else
|
|
460
|
+
printf ' \e[38;5;196m✗\e[0m Neither npm nor brew found. Install one, then run:\n'
|
|
461
|
+
echo " npm install -g lashcode"
|
|
462
|
+
return 1
|
|
463
|
+
fi
|
|
464
|
+
else
|
|
465
|
+
return 1
|
|
466
|
+
fi
|
|
467
|
+
else
|
|
468
|
+
return 1
|
|
469
|
+
fi
|
|
470
|
+
fi
|
|
471
|
+
|
|
472
|
+
local cmd
|
|
473
|
+
if [[ "$tool" == "custom" ]]; then
|
|
474
|
+
if [[ -z "$LACY_CUSTOM_TOOL_CMD" ]]; then
|
|
475
|
+
echo "Error: custom tool selected but no command configured."
|
|
476
|
+
echo "Set one with: tool set custom \"your-command -flags\""
|
|
477
|
+
echo "Or add to ~/.lacy/config.yaml:"
|
|
478
|
+
echo " agent_tools:"
|
|
479
|
+
echo " active: custom"
|
|
480
|
+
echo " custom_command: \"your-command -flags\""
|
|
481
|
+
return 1
|
|
482
|
+
fi
|
|
483
|
+
cmd="$LACY_CUSTOM_TOOL_CMD"
|
|
484
|
+
else
|
|
485
|
+
cmd=$(lacy_tool_cmd "$tool")
|
|
486
|
+
fi
|
|
487
|
+
|
|
488
|
+
# Log the query (tool name + input text, not the AI response)
|
|
489
|
+
_lacy_log_query "$tool" "$query"
|
|
490
|
+
|
|
491
|
+
# Show which tool was auto-detected
|
|
492
|
+
if [[ "$_auto_detected" == true ]]; then
|
|
493
|
+
lacy_print_color 238 " Using $tool (auto-detected)"
|
|
494
|
+
fi
|
|
495
|
+
|
|
496
|
+
# === Preheat: lash/opencode background server ===
|
|
497
|
+
if [[ "$tool" == "lash" || "$tool" == "opencode" ]]; then
|
|
498
|
+
if lacy_preheat_server_is_healthy || lacy_preheat_server_start "$tool"; then
|
|
499
|
+
echo ""
|
|
500
|
+
lacy_start_spinner
|
|
501
|
+
local server_result
|
|
502
|
+
server_result=$(lacy_preheat_server_query "$query")
|
|
503
|
+
local exit_code=$?
|
|
504
|
+
lacy_stop_spinner
|
|
505
|
+
# Restore session ID from file (lost in subshell)
|
|
506
|
+
lacy_preheat_server_restore_session
|
|
507
|
+
if [[ $exit_code -eq 0 && -n "$server_result" ]]; then
|
|
508
|
+
while [[ "$server_result" == $'\n'* ]]; do server_result="${server_result#$'\n'}"; done
|
|
509
|
+
_lacy_render_markdown "$server_result"
|
|
510
|
+
_lacy_print_resume_hint "$tool"
|
|
511
|
+
echo ""
|
|
512
|
+
return 0
|
|
513
|
+
fi
|
|
514
|
+
# Server query failed — fall through to single-shot
|
|
515
|
+
fi
|
|
516
|
+
fi
|
|
517
|
+
|
|
518
|
+
# === Preheat: claude session reuse ===
|
|
519
|
+
if [[ "$tool" == "claude" ]]; then
|
|
520
|
+
local claude_cmd
|
|
521
|
+
claude_cmd=$(lacy_preheat_claude_build_cmd)
|
|
522
|
+
echo ""
|
|
523
|
+
lacy_start_spinner
|
|
524
|
+
local json_output
|
|
525
|
+
json_output=$(unset CLAUDECODE; _lacy_run_tool_cmd "$claude_cmd" "$query" </dev/tty 2>&1)
|
|
526
|
+
local exit_code=$?
|
|
527
|
+
lacy_stop_spinner
|
|
528
|
+
|
|
529
|
+
# Normalize: strip noise, extract last element from JSON array
|
|
530
|
+
json_output=$(_lacy_claude_normalize_output "$json_output")
|
|
531
|
+
|
|
532
|
+
if [[ $exit_code -eq 0 ]]; then
|
|
533
|
+
# Check for structured errors (e.g. invalid API key)
|
|
534
|
+
if lacy_format_tool_error "$json_output" "$tool"; then
|
|
535
|
+
return 1
|
|
536
|
+
fi
|
|
537
|
+
local result_text
|
|
538
|
+
result_text=$(lacy_preheat_claude_extract_result "$json_output")
|
|
539
|
+
while [[ "$result_text" == $'\n'* ]]; do result_text="${result_text#$'\n'}"; done
|
|
540
|
+
if [[ -n "$result_text" ]]; then
|
|
541
|
+
_lacy_render_markdown "$result_text"
|
|
542
|
+
else
|
|
543
|
+
printf '%s\n' "$json_output"
|
|
544
|
+
fi
|
|
545
|
+
lacy_preheat_claude_capture_session "$json_output"
|
|
546
|
+
_lacy_print_resume_hint "$tool"
|
|
547
|
+
echo ""
|
|
548
|
+
return 0
|
|
549
|
+
elif [[ -n "$LACY_PREHEAT_CLAUDE_SESSION_ID" ]]; then
|
|
550
|
+
lacy_preheat_claude_reset_session
|
|
551
|
+
claude_cmd=$(lacy_preheat_claude_build_cmd)
|
|
552
|
+
lacy_start_spinner
|
|
553
|
+
json_output=$(unset CLAUDECODE; _lacy_run_tool_cmd "$claude_cmd" "$query" </dev/tty 2>&1)
|
|
554
|
+
exit_code=$?
|
|
555
|
+
lacy_stop_spinner
|
|
556
|
+
|
|
557
|
+
# Normalize: strip noise, extract last element from JSON array
|
|
558
|
+
json_output=$(_lacy_claude_normalize_output "$json_output")
|
|
559
|
+
|
|
560
|
+
# Check for structured errors before processing
|
|
561
|
+
if lacy_format_tool_error "$json_output" "$tool"; then
|
|
562
|
+
return 1
|
|
563
|
+
fi
|
|
564
|
+
|
|
565
|
+
if [[ $exit_code -eq 0 ]]; then
|
|
566
|
+
local result_text
|
|
567
|
+
result_text=$(lacy_preheat_claude_extract_result "$json_output")
|
|
568
|
+
while [[ "$result_text" == $'\n'* ]]; do result_text="${result_text#$'\n'}"; done
|
|
569
|
+
if [[ -n "$result_text" ]]; then
|
|
570
|
+
_lacy_render_markdown "$result_text"
|
|
571
|
+
else
|
|
572
|
+
printf '%s\n' "$json_output"
|
|
573
|
+
fi
|
|
574
|
+
lacy_preheat_claude_capture_session "$json_output"
|
|
575
|
+
_lacy_print_resume_hint "$tool"
|
|
576
|
+
echo ""
|
|
577
|
+
return 0
|
|
578
|
+
fi
|
|
579
|
+
lacy_format_tool_error "$json_output" "$tool" || printf '%s\n' "$json_output"
|
|
580
|
+
echo ""
|
|
581
|
+
return $exit_code
|
|
582
|
+
else
|
|
583
|
+
lacy_format_tool_error "$json_output" "$tool" || printf '%s\n' "$json_output"
|
|
584
|
+
echo ""
|
|
585
|
+
return $exit_code
|
|
586
|
+
fi
|
|
587
|
+
fi
|
|
588
|
+
|
|
589
|
+
# === Gemini session reuse ===
|
|
590
|
+
if [[ "$tool" == "gemini" ]]; then
|
|
591
|
+
echo ""
|
|
592
|
+
local json_output
|
|
593
|
+
lacy_start_spinner
|
|
594
|
+
json_output=$(_lacy_gemini_query_exec "$query")
|
|
595
|
+
local exit_code=$?
|
|
596
|
+
lacy_stop_spinner
|
|
597
|
+
# Restore session ID lost in subshell
|
|
598
|
+
lacy_preheat_gemini_restore_session
|
|
599
|
+
|
|
600
|
+
if [[ $exit_code -ne 0 && -n "$LACY_GEMINI_SESSION_ID" ]]; then
|
|
601
|
+
# --resume failed (session expired/missing) — retry without it
|
|
602
|
+
lacy_preheat_gemini_reset_session
|
|
603
|
+
lacy_start_spinner
|
|
604
|
+
json_output=$(_lacy_gemini_query_exec "$query")
|
|
605
|
+
exit_code=$?
|
|
606
|
+
lacy_stop_spinner
|
|
607
|
+
fi
|
|
608
|
+
|
|
609
|
+
if [[ $exit_code -eq 0 ]]; then
|
|
610
|
+
local result_text
|
|
611
|
+
result_text=$(lacy_preheat_gemini_extract_result "$json_output")
|
|
612
|
+
while [[ "$result_text" == $'\n'* ]]; do result_text="${result_text#$'\n'}"; done
|
|
613
|
+
if [[ -n "$result_text" ]]; then
|
|
614
|
+
_lacy_render_markdown "$result_text"
|
|
615
|
+
else
|
|
616
|
+
printf '%s\n' "$json_output"
|
|
617
|
+
fi
|
|
618
|
+
lacy_preheat_gemini_capture_session "$json_output"
|
|
619
|
+
_lacy_print_resume_hint "$tool"
|
|
620
|
+
fi
|
|
621
|
+
echo ""
|
|
622
|
+
return $exit_code
|
|
623
|
+
fi
|
|
624
|
+
|
|
625
|
+
# === Generic path (codex, custom, and fallback) ===
|
|
626
|
+
echo ""
|
|
627
|
+
lacy_start_spinner
|
|
628
|
+
_lacy_run_tool_cmd "$cmd" "$query" </dev/tty 2>&1 | {
|
|
629
|
+
local _spinner_killed=false
|
|
630
|
+
local _full_output=""
|
|
631
|
+
local _line_count=0
|
|
632
|
+
while IFS= read -r line; do
|
|
633
|
+
# Skip agent startup noise (e.g. "> build · big-pickle", "exit_code=0")
|
|
634
|
+
[[ "$line" =~ ^'> '[a-z]+' · ' ]] && continue
|
|
635
|
+
[[ "$line" =~ ^exit_code= ]] && continue
|
|
636
|
+
if ! $_spinner_killed; then
|
|
637
|
+
if [[ -n "$LACY_SPINNER_PID" ]] && kill -0 "$LACY_SPINNER_PID" 2>/dev/null; then
|
|
638
|
+
kill "$LACY_SPINNER_PID" 2>/dev/null
|
|
639
|
+
sleep "$LACY_TERMINAL_FLUSH_DELAY"
|
|
640
|
+
printf '\e[2K\r\e[?25h\e[?7h'
|
|
641
|
+
fi
|
|
642
|
+
_spinner_killed=true
|
|
643
|
+
fi
|
|
644
|
+
_full_output+="$line"
|
|
645
|
+
(( _line_count++ ))
|
|
646
|
+
# Only buffer first line to check for JSON errors
|
|
647
|
+
if (( _line_count > 1 )); then
|
|
648
|
+
# Multi-line output — not a JSON error blob, flush everything
|
|
649
|
+
if [[ $_line_count -eq 2 ]]; then
|
|
650
|
+
printf '%s\n' "$_full_output"
|
|
651
|
+
fi
|
|
652
|
+
printf '%s\n' "$line"
|
|
653
|
+
fi
|
|
654
|
+
done
|
|
655
|
+
if ! $_spinner_killed && [[ -n "$LACY_SPINNER_PID" ]]; then
|
|
656
|
+
kill "$LACY_SPINNER_PID" 2>/dev/null
|
|
657
|
+
sleep "$LACY_TERMINAL_FLUSH_DELAY"
|
|
658
|
+
printf '\e[2K\r\e[?25h\e[?7h'
|
|
659
|
+
fi
|
|
660
|
+
# Single-line output — check if it's a JSON error
|
|
661
|
+
if (( _line_count <= 1 )); then
|
|
662
|
+
lacy_format_tool_error "$_full_output" "$tool" || printf '%s\n' "$_full_output"
|
|
663
|
+
fi
|
|
664
|
+
}
|
|
665
|
+
local exit_code
|
|
666
|
+
if [[ "$LACY_SHELL_TYPE" == "zsh" ]]; then
|
|
667
|
+
exit_code=${pipestatus[1]}
|
|
668
|
+
else
|
|
669
|
+
exit_code=${PIPESTATUS[0]}
|
|
670
|
+
fi
|
|
671
|
+
lacy_stop_spinner
|
|
672
|
+
if [[ $exit_code -eq 0 ]]; then
|
|
673
|
+
_lacy_print_resume_hint "$tool"
|
|
674
|
+
fi
|
|
675
|
+
echo ""
|
|
676
|
+
return $exit_code
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
# Check if API keys are configured (used by mcp.sh)
|
|
680
|
+
lacy_shell_check_api_keys() {
|
|
681
|
+
[[ -n "$LACY_SHELL_API_OPENAI" || -n "$LACY_SHELL_API_ANTHROPIC" || -n "$OPENAI_API_KEY" || -n "$ANTHROPIC_API_KEY" ]]
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
# ============================================================================
|
|
685
|
+
# Direct API Fallback (when no CLI tool installed)
|
|
686
|
+
# ============================================================================
|
|
687
|
+
|
|
688
|
+
lacy_shell_send_to_ai_streaming() {
|
|
689
|
+
local input_file="$1"
|
|
690
|
+
local query="$2"
|
|
691
|
+
|
|
692
|
+
local provider="${LACY_SHELL_PROVIDER:-$LACY_SHELL_DEFAULT_PROVIDER}"
|
|
693
|
+
local api_key_openai="${LACY_SHELL_API_OPENAI:-$OPENAI_API_KEY}"
|
|
694
|
+
local api_key_anthropic="${LACY_SHELL_API_ANTHROPIC:-$ANTHROPIC_API_KEY}"
|
|
695
|
+
|
|
696
|
+
if [[ "$provider" == "anthropic" && -n "$api_key_anthropic" ]]; then
|
|
697
|
+
lacy_shell_query_anthropic "$input_file" "$api_key_anthropic"
|
|
698
|
+
elif [[ -n "$api_key_openai" ]]; then
|
|
699
|
+
lacy_shell_query_openai "$input_file" "$api_key_openai"
|
|
700
|
+
elif [[ -n "$api_key_anthropic" ]]; then
|
|
701
|
+
lacy_shell_query_anthropic "$input_file" "$api_key_anthropic"
|
|
702
|
+
else
|
|
703
|
+
echo "Error: No API keys configured"
|
|
704
|
+
return 1
|
|
705
|
+
fi
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
lacy_shell_query_openai() {
|
|
709
|
+
local input_file="$1"
|
|
710
|
+
local api_key="$2"
|
|
711
|
+
local content
|
|
712
|
+
content=$(_lacy_json_escape_str "$(cat "$input_file")")
|
|
713
|
+
|
|
714
|
+
local response
|
|
715
|
+
response=$(curl -s -H "Content-Type: application/json" \
|
|
716
|
+
-H "Authorization: Bearer $api_key" \
|
|
717
|
+
-d "{\"model\":\"${LACY_API_MODEL_OPENAI}\",\"messages\":[{\"role\":\"user\",\"content\":\"$content\"}],\"max_tokens\":1500}" \
|
|
718
|
+
"$LACY_API_URL_OPENAI")
|
|
719
|
+
|
|
720
|
+
_lacy_json_query "$response" '.choices[0].message.content'
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
lacy_shell_query_anthropic() {
|
|
724
|
+
local input_file="$1"
|
|
725
|
+
local api_key="$2"
|
|
726
|
+
local content
|
|
727
|
+
content=$(_lacy_json_escape_str "$(cat "$input_file")")
|
|
728
|
+
|
|
729
|
+
local response
|
|
730
|
+
response=$(curl -s -H "Content-Type: application/json" \
|
|
731
|
+
-H "x-api-key: $api_key" \
|
|
732
|
+
-H "anthropic-version: 2023-06-01" \
|
|
733
|
+
-d "{\"model\":\"${LACY_API_MODEL_ANTHROPIC}\",\"max_tokens\":1500,\"messages\":[{\"role\":\"user\",\"content\":\"$content\"}]}" \
|
|
734
|
+
"$LACY_API_URL_ANTHROPIC")
|
|
735
|
+
|
|
736
|
+
_lacy_json_query "$response" '.content[0].text'
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
# Stub for MCP init (no-op, lash handles MCP)
|
|
740
|
+
lacy_shell_init_mcp() { :; }
|
|
741
|
+
lacy_shell_cleanup_mcp() { :; }
|