karnel-termux 4.15.2 → 4.16.0

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 (77) hide show
  1. package/README.md +10 -4
  2. package/docs/CHANGELOG.md +19 -0
  3. package/docs/cli/README.md +0 -1
  4. package/karnel/RELEASE_COMMIT +1 -1
  5. package/karnel/cli/commands/agent.sh +841 -0
  6. package/karnel/cli/commands/ia.sh +24 -0
  7. package/karnel/cli/commands/list.sh +2 -0
  8. package/karnel/cli/commands/update.sh +1 -1
  9. package/karnel/cli/karnel.sh +29 -17
  10. package/karnel/tools/ai/all.sh +2 -0
  11. package/karnel/tools/ai/ampcode/bin/amp.proot +22 -0
  12. package/karnel/tools/ai/antigravity-cli/bin/agy.proot +22 -0
  13. package/karnel/tools/ai/cactus/bin/cactus +30 -0
  14. package/karnel/tools/ai/cactus/bin/cactus.glibc +31 -0
  15. package/karnel/tools/ai/cactus/bin/cactus.launcher.py +57 -0
  16. package/karnel/tools/ai/cactus/bin/cactus.proot +33 -0
  17. package/karnel/tools/ai/cactus/install.sh +288 -9
  18. package/karnel/tools/ai/cactus-needle/README.md +120 -0
  19. package/karnel/tools/ai/cactus-needle/bin/needle +30 -0
  20. package/karnel/tools/ai/cactus-needle/bin/needle.glibc +18 -0
  21. package/karnel/tools/ai/cactus-needle/bin/needle.proot +24 -0
  22. package/karnel/tools/ai/cactus-needle/install.sh +371 -0
  23. package/karnel/tools/ai/claude-code/bin/claude.proot +22 -0
  24. package/karnel/tools/ai/cline/bin/cline.proot +28 -0
  25. package/karnel/tools/ai/cline/install.sh +87 -34
  26. package/karnel/tools/ai/codegraph/bin/codegraph +1 -1
  27. package/karnel/tools/ai/command-code/install.sh +21 -3
  28. package/karnel/tools/ai/ctx7/install.sh +1 -1
  29. package/karnel/tools/ai/cursor-cli/bin/cursor.proot +33 -0
  30. package/karnel/tools/ai/cursor-cli/install.sh +107 -31
  31. package/karnel/tools/ai/droid/bin/droid +27 -0
  32. package/karnel/tools/ai/droid/bin/droid.proot +22 -0
  33. package/karnel/tools/ai/droid/helper/droid_helper.c +58 -0
  34. package/karnel/tools/ai/droid/install.sh +381 -79
  35. package/karnel/tools/ai/engram/install.sh +47 -13
  36. package/karnel/tools/ai/gentle-ai/install.sh +43 -5
  37. package/karnel/tools/ai/goose/bin/goose.proot +22 -0
  38. package/karnel/tools/ai/goose/install.sh +90 -8
  39. package/karnel/tools/ai/hermes-agent/install.sh +11 -2
  40. package/karnel/tools/ai/hugging-face/install.sh +1 -1
  41. package/karnel/tools/ai/keelcode/bin/keelcode.proot +23 -0
  42. package/karnel/tools/ai/keelcode/install.sh +95 -18
  43. package/karnel/tools/ai/kilocode-cli/bin/kilocode +4 -4
  44. package/karnel/tools/ai/kilocode-cli/bin/kilocode.proot +22 -0
  45. package/karnel/tools/ai/kilocode-cli/install.sh +19 -1
  46. package/karnel/tools/ai/kimchi-code/bin/kimchi.proot +22 -0
  47. package/karnel/tools/ai/kimchi-code/install.sh +399 -83
  48. package/karnel/tools/ai/kiro/install.sh +57 -14
  49. package/karnel/tools/ai/mimocode/bin/mimo.proot +22 -0
  50. package/karnel/tools/ai/mistral-vibe/install.sh +4 -1
  51. package/karnel/tools/ai/odysseus/install.sh +247 -55
  52. package/karnel/tools/ai/oh-my-pi/bin/omp.proot +22 -0
  53. package/karnel/tools/ai/omni-route/install.sh +63 -50
  54. package/karnel/tools/ai/openclaw/install.sh +4 -1
  55. package/karnel/tools/ai/opencode/bin/opencode.proot +22 -0
  56. package/karnel/tools/ai/openspec/install.sh +1 -1
  57. package/karnel/tools/ai/qoder/README.md +1 -1
  58. package/karnel/tools/ai/qoder/bin/qodercli.proot +22 -0
  59. package/karnel/tools/ai/walkie/README.md +84 -0
  60. package/karnel/tools/ai/walkie/bin/walkie +29 -0
  61. package/karnel/tools/ai/walkie/install.sh +660 -0
  62. package/karnel/tools/ai/walkie/patches/network-interfaces.js +71 -0
  63. package/karnel/tools/ai/walkie/patches/patch-agents.js +191 -0
  64. package/karnel/tools/ai/walkie/patches/patch-members.js +169 -0
  65. package/karnel/tools/ai/walkie/patches/patch-mention-only.js +89 -0
  66. package/karnel/tools/editor/nvchad/README.md +4 -4
  67. package/karnel/tools/editor/nvchad/install.sh +3 -3
  68. package/karnel/utils/agent_actions.sh +1421 -0
  69. package/karnel/utils/agent_llm.sh +304 -0
  70. package/karnel/utils/agent_markdown.sh +254 -0
  71. package/karnel/utils/bootstrap.sh +3 -2
  72. package/karnel/utils/log.sh +6 -0
  73. package/karnel/utils/walkie.sh +29 -0
  74. package/package.json +4 -1
  75. package/scripts/check-package.js +5 -2
  76. package/scripts/completion.bash +4 -4
  77. package/scripts/completion.zsh +4 -3
@@ -0,0 +1,841 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # ============================================================
4
+ # karnel agent — AI assistant (ask) & bash-backed task agent (run)
5
+ #
6
+ # karnel agent ask "prompt" assistant, streamed (markdown TUI)
7
+ # karnel agent run "prompt" task agent: the LLM answers in
8
+ # markdown; bash parses the answer
9
+ # into files & commands and does the
10
+ # work (commands need y/N approval)
11
+ #
12
+ # Everything in this command is plain bash; the "AI" only DECIDES
13
+ # and DESCRIBES. bash EXECUTES. In the REPLs (and -p prompts on a
14
+ # TTY) typing `@query` opens an fzf picker to attach a file's
15
+ # contents to your message.
16
+ # ============================================================
17
+
18
+ import "@/utils/log"
19
+ import "@/utils/colors"
20
+ import "@/utils/agent_llm"
21
+ import "@/utils/agent_actions"
22
+ import "@/utils/agent_markdown"
23
+
24
+ # ------------------------------------------------------------
25
+ # agent_help
26
+ # ------------------------------------------------------------
27
+ agent_help() {
28
+ echo
29
+ box "Karnel Agent — Local AI Assistant & Task Agent"
30
+ echo
31
+ log_info "Usage: karnel agent <ask|run|config> [options]"
32
+ echo
33
+ separator_section "Subcommands"
34
+ echo
35
+ printf " ${D_CYAN}%-11s${D_NC} %s\n" "ask" "Assistant (prompt → LLM → colored markdown answer)"
36
+ printf " ${D_CYAN}%-11s${D_NC} %s\n" "run" "Agent (prompt → LLM markdown → bash creates files / runs commands)"
37
+ printf " ${D_CYAN}%-11s${D_NC} %s\n" "config" "Show or edit saved settings"
38
+ printf " ${D_CYAN}%-11s${D_NC} %s\n" "status" "Show endpoint/model status"
39
+ echo
40
+ separator_section "Options (ask & run)"
41
+ echo
42
+ printf " ${D_CYAN}%-22s${D_NC} %s\n" "-p, --prompt <text>" "Task/question (omit for interactive shell)"
43
+ printf " ${D_CYAN}%-22s${D_NC} %s\n" "-m, --model <name>" "Model id (default: $AGENT_MODEL)"
44
+ printf " ${D_CYAN}%-22s${D_NC} %s\n" "-u, --endpoint <url>" "OpenAI-compatible endpoint (default: $AGENT_ENDPOINT)"
45
+ printf " ${D_CYAN}%-22s${D_NC} %s\n" "-t, --temperature <n>" "Sampling temperature (default: $AGENT_TEMPERATURE)"
46
+ printf " ${D_CYAN}%-22s${D_NC} %s\n" "--max-tokens <n>" "Max output tokens (default: $AGENT_MAX_TOKENS)"
47
+ printf " ${D_CYAN}%-22s${D_NC} %s\n" "-w, --workspace <dir>" "Agent working dir (run mode, default: \$PWD)"
48
+ printf " ${D_CYAN}%-22s${D_NC} %s\n" "-n, --max-iterations <n>" "Agent loop limit (default: $AGENT_MAX_ITERATIONS)"
49
+ printf " ${D_CYAN}%-22s${D_NC} %s\n" "-y, --yes" "Auto-approve commands (skip y/N prompt)"
50
+ printf " ${D_CYAN}%-22s${D_NC} %s\n" "--plan" "Plan mode (read-only): no file writes, write commands blocked"
51
+ printf " ${D_CYAN}%-22s${D_NC} %s\n" "--build" "Build mode (default): files and commands applied"
52
+ echo
53
+ separator_section "Files & commands"
54
+ echo
55
+ list_item "Type ${D_CYAN}@name${D_NC} in a message to attach a file's contents (live fzf picker while typing)"
56
+ list_item "Start a message with ${D_CYAN}!${D_NC} for shell mode (e.g. ${D_CYAN}!git status${D_NC}) — the prompt turns green ${D_CYAN}Shell${D_NC} and the output is added to the agent's context"
57
+ list_item "Commands from the model run only after your ${D_CYAN}y/N${D_NC} confirmation (${D_CYAN}-y${D_NC} auto-approves)"
58
+ list_item "Press ${D_CYAN}ESC ESC${D_NC} at any prompt to cancel the agent"
59
+ list_item "The interactive REPL remembers the conversation and shows ${D_CYAN}[context % · elapsed]${D_NC} after each answer/task"
60
+ list_item "Dictate your prompt with ${D_CYAN}/voice${D_NC} (Termux:API)"
61
+ list_item "If the model server is down, the agent starts ${D_CYAN}cactus${D_NC} in the background (logs → ${D_CYAN}~/.cache/karnel/karnel-agent.log${D_NC}) and stops it when you leave (${D_CYAN}/exit${D_NC} or ${D_CYAN}Ctrl+C${D_NC})"
62
+ echo
63
+ separator_section "Interactive slash commands"
64
+ echo
65
+ printf " ${D_CYAN}%-16s${D_NC} %s\n" "/help" "Show this help"
66
+ printf " ${D_CYAN}%-16s${D_NC} %s\n" "/model <name>" "Switch model"
67
+ printf " ${D_CYAN}%-16s${D_NC} %s\n" "/endpoint <url>" "Switch endpoint"
68
+ printf " ${D_CYAN}%-16s${D_NC} %s\n" "/temp <n>" "Set temperature"
69
+ printf " ${D_CYAN}%-16s${D_NC} %s\n" "/max <n>" "Set max tokens"
70
+ printf " ${D_CYAN}%-16s${D_NC} %s\n" "/workspace <dir>" "Set agent workspace (run)"
71
+ printf " ${D_CYAN}%-16s${D_NC} %s\n" "/voice" "Dictate with Termux:API"
72
+ printf " ${D_CYAN}%-16s${D_NC} %s\n" "/clear" "Reset conversation"
73
+ printf " ${D_CYAN}%-16s${D_NC} %s\n" "/history" "Show conversation history"
74
+ printf " ${D_CYAN}%-16s${D_NC} %s\n" "/status" "Show current settings"
75
+ printf " ${D_CYAN}%-16s${D_NC} %s\n" "/plan" "Plan mode (read-only) — next task only inspects and proposes"
76
+ printf " ${D_CYAN}%-16s${D_NC} %s\n" "/build" "Build mode — next task applies files and commands"
77
+ printf " ${D_CYAN}%-16s${D_NC} %s\n" "/exit" "Leave the interactive shell"
78
+ echo
79
+ separator_section "Examples"
80
+ echo
81
+ printf " ${D_CYAN}karnel agent ask -p \"Explain rsync\"${D_NC}\n"
82
+ printf " ${D_CYAN}karnel agent run -p \"create a backup script\"${D_NC}\n"
83
+ printf " ${D_CYAN}karnel agent run -p \"...\" -m gemma-4-e2b-it-cq4 -u http://127.0.0.1:8000/v1${D_NC}\n"
84
+ printf " ${D_CYAN}karnel agent ask${D_NC} # interactive chat\n"
85
+ echo
86
+ }
87
+
88
+ # ------------------------------------------------------------
89
+ # agent_main — dispatcher
90
+ # ------------------------------------------------------------
91
+ agent_main() {
92
+ local cmd="$1"
93
+ shift || true
94
+
95
+ agent_config_load
96
+
97
+ case "$cmd" in
98
+ ask | chat)
99
+ agent_ask "$@"
100
+ ;;
101
+ run | exec)
102
+ agent_run "$@"
103
+ ;;
104
+ config | settings)
105
+ agent_config_cmd "$@"
106
+ ;;
107
+ status | info)
108
+ agent_status
109
+ ;;
110
+ "" | --help | -h | help)
111
+ agent_help
112
+ ;;
113
+ *)
114
+ log_error "Unknown subcommand: ${D_CYAN}$cmd${D_NC}"
115
+ echo
116
+ agent_help
117
+ exit 1
118
+ ;;
119
+ esac
120
+ }
121
+
122
+ # ------------------------------------------------------------
123
+ # agent_status_line — compact one-liner of current settings
124
+ # ------------------------------------------------------------
125
+ agent_status_line() {
126
+ printf ' %b%-10s%b : %s\n' "$D_CYAN" "Model" "$NC" "$AGENT_MODEL"
127
+ printf ' %b%-10s%b : %s\n' "$D_CYAN" "Endpoint" "$NC" "$AGENT_ENDPOINT"
128
+ printf ' %b%-10s%b : %s\n' "$D_CYAN" "Temp" "$NC" "$AGENT_TEMPERATURE"
129
+ printf ' %b%-10s%b : %s\n' "$D_CYAN" "Max tokens" "$NC" "$AGENT_MAX_TOKENS"
130
+ printf ' %b%-10s%b : %s\n' "$D_CYAN" "Context" "$NC" "$AGENT_CONTEXT_WINDOW tokens"
131
+ # PLAN/BUILD modes are a run-mode concept; hide them in ask
132
+ [[ "$AGENT_REPL_MODE" != "ask" ]] && printf ' %b%-10s%b : %s\n' "$D_CYAN" "Mode" "$NC" "$([[ $AGENT_PLAN_MODE == 1 ]] && echo 'PLAN (read-only)' || echo 'BUILD (full access)')"
133
+ printf ' %b%-10s%b : %s\n' "$D_CYAN" "Workspace" "$NC" "$AGENT_WORKSPACE"
134
+ }
135
+
136
+ # ------------------------------------------------------------
137
+ # agent_status — full status with server check
138
+ # ------------------------------------------------------------
139
+ agent_status() {
140
+ separator_section "Karnel Agent — Status"
141
+ echo
142
+ agent_status_line
143
+ echo
144
+ separator_section "Server"
145
+ echo
146
+ if agent_check_server; then
147
+ log_success "Endpoint reachable: ${D_CYAN}$AGENT_ENDPOINT${D_NC}"
148
+ local ms
149
+ ms=$(agent_models_list 2>/dev/null)
150
+ echo
151
+ log_info "Available models:"
152
+ echo "$ms" | while IFS= read -r mid; do
153
+ list_item "$mid"
154
+ done
155
+ else
156
+ log_error "Endpoint unreachable"
157
+ fi
158
+ echo
159
+ separator_section "Config file"
160
+ echo
161
+ list_item "$AGENT_CONF_FILE"
162
+ echo
163
+ }
164
+
165
+ # ------------------------------------------------------------
166
+ # agent_config_help
167
+ # ------------------------------------------------------------
168
+ agent_config_help() {
169
+ log_info "Usage: karnel agent config [key] [value]"
170
+ echo
171
+ separator_section "Settings"
172
+ echo
173
+ printf " ${D_CYAN}%-14s${D_NC} %s\n" "model" "Model id (default: gemma-4-e2b-it-cq4)"
174
+ printf " ${D_CYAN}%-14s${D_NC} %s\n" "endpoint" "OpenAI-compatible URL"
175
+ printf " ${D_CYAN}%-14s${D_NC} %s\n" "temperature" "Sampling temperature"
176
+ printf " ${D_CYAN}%-14s${D_NC} %s\n" "max_tokens" "Max output tokens"
177
+ printf " ${D_CYAN}%-14s${D_NC} %s\n" "max_iterations" "Agent loop limit"
178
+ printf " ${D_CYAN}%-14s${D_NC} %s\n" "confirm_commands" "Ask y/N before running commands (1/0)"
179
+ printf " ${D_CYAN}%-14s${D_NC} %s\n" "context_window" "Model context window in tokens (default: 8192)"
180
+ printf " ${D_CYAN}%-14s${D_NC} %s\n" "server_command" "Command that starts the model server in the background"
181
+ printf " ${D_CYAN}%-14s${D_NC} %s\n" "workspace" "Default agent working dir"
182
+ echo
183
+ log_tip "Run ${D_CYAN}karnel agent config${D_NC} with no arguments to show current settings."
184
+ }
185
+
186
+ # ------------------------------------------------------------
187
+ # agent_config_cmd
188
+ # ------------------------------------------------------------
189
+ agent_config_cmd() {
190
+ local key="$1"
191
+ shift || true
192
+
193
+ if [[ -z "$key" || "$key" == "--help" || "$key" == "-h" ]]; then
194
+ echo
195
+ separator_section "Current Config"
196
+ echo
197
+ agent_status_line
198
+ echo
199
+ separator_section "Config file"
200
+ echo
201
+ list_item "Saved at: ${D_CYAN}$AGENT_CONF_FILE${D_NC}"
202
+ echo
203
+ agent_config_help
204
+ echo
205
+ return 0
206
+ fi
207
+
208
+ local value="$*"
209
+ if [[ -z "$value" ]]; then
210
+ log_error "Missing value for: $key"
211
+ echo
212
+ agent_config_help
213
+ return 1
214
+ fi
215
+ agent_config_set "$key" "$value"
216
+ log_success "Saved $key = ${D_CYAN}$value${D_NC}"
217
+ }
218
+ # ------------------------------------------------------------
219
+ # Slash-command help
220
+ # ------------------------------------------------------------
221
+ agent_repl_help() {
222
+ local mode="${1:-$AGENT_REPL_MODE}"
223
+ separator_section "Slash commands"
224
+ echo
225
+ printf " ${D_CYAN}%-14s${D_NC} %s\n" "/model <name>" "Switch model"
226
+ printf " ${D_CYAN}%-14s${D_NC} %s\n" "/endpoint <url>" "Switch endpoint"
227
+ printf " ${D_CYAN}%-14s${D_NC} %s\n" "/temp <n>" "Set temperature"
228
+ printf " ${D_CYAN}%-14s${D_NC} %s\n" "/max <n>" "Set max tokens"
229
+ printf " ${D_CYAN}%-14s${D_NC} %s\n" "/workspace <dir>" "Set agent workspace (run)"
230
+ printf " ${D_CYAN}%-14s${D_NC} %s\n" "/voice" "Dictate with Termux:API (editable prompt)"
231
+ printf " ${D_CYAN}%-14s${D_NC} %s\n" "/clear" "Reset conversation"
232
+ printf " ${D_CYAN}%-14s${D_NC} %s\n" "/history" "Show conversation history"
233
+ printf " ${D_CYAN}%-14s${D_NC} %s\n" "/status" "Show current settings"
234
+ if [[ "$mode" != "ask" ]]; then
235
+ printf " ${D_CYAN}%-14s${D_NC} %s\n" "/plan" "Plan mode (read-only) — next task only inspects and proposes"
236
+ printf " ${D_CYAN}%-14s${D_NC} %s\n" "/build" "Build mode — next task applies files and commands"
237
+ fi
238
+ printf " ${D_CYAN}%-14s${D_NC} %s\n" "/exit" "Leave the interactive shell"
239
+ echo
240
+ }
241
+
242
+ # ------------------------------------------------------------
243
+ # agent_parse_args — unified flag parser for ask/run
244
+ # -p|--prompt <text> -m|--model <id> -u|--endpoint <url>
245
+ # -t|--temperature <n> --max-tokens <n>
246
+ # -w|--workspace <dir> -n|--max-iterations <n>
247
+ # Any positional argument is treated as the prompt.
248
+ # ------------------------------------------------------------
249
+ agent_parse_args() {
250
+ AGENT_PROMPT=""
251
+ AGENT_SHOW_HELP=0
252
+ AGENT_YES=0
253
+ local prev=""
254
+ for arg in "$@"; do
255
+ case "$prev" in
256
+ -p | --prompt) AGENT_PROMPT="$arg"; prev=""; continue ;;
257
+ -m | --model) AGENT_MODEL="$arg"; prev=""; continue ;;
258
+ -u | --endpoint) AGENT_ENDPOINT="$arg"; prev=""; continue ;;
259
+ -t | --temperature) AGENT_TEMPERATURE="$arg"; prev=""; continue ;;
260
+ -w | --workspace) AGENT_WORKSPACE="$arg"; prev=""; continue ;;
261
+ -n | --max-iterations) AGENT_MAX_ITERATIONS="$arg"; prev=""; continue ;;
262
+ --max-tokens) AGENT_MAX_TOKENS="$arg"; prev=""; continue ;;
263
+ esac
264
+ case "$arg" in
265
+ -p=* | --prompt=*) AGENT_PROMPT="${arg#*=}" ;;
266
+ -m=* | --model=*) AGENT_MODEL="${arg#*=}" ;;
267
+ -u=* | --endpoint=*) AGENT_ENDPOINT="${arg#*=}" ;;
268
+ -t=* | --temperature=*) AGENT_TEMPERATURE="${arg#*=}" ;;
269
+ -w=* | --workspace=*) AGENT_WORKSPACE="${arg#*=}" ;;
270
+ -n=* | --max-iterations=*) AGENT_MAX_ITERATIONS="${arg#*=}" ;;
271
+ --max-tokens=*) AGENT_MAX_TOKENS="${arg#*=}" ;;
272
+ -p | --prompt | -m | --model | -u | --endpoint | -t | --temperature | -w | --workspace | -n | --max-iterations | --max-tokens) prev="$arg" ;;
273
+ --plan) AGENT_PLAN_MODE=1 ;;
274
+ --build) AGENT_PLAN_MODE=0 ;;
275
+ -y | --yes) AGENT_YES=1 ;;
276
+ -h | --help) AGENT_SHOW_HELP=1 ;;
277
+ -*) log_warn "Unknown option: $arg" ;;
278
+ *) [[ -z "$AGENT_PROMPT" ]] && AGENT_PROMPT="$arg" ;;
279
+ esac
280
+ done
281
+ }
282
+ readonly AGENT_ASK_SYSTEM="You are a concise chat assistant. Reply in the SAME LANGUAGE the user writes in. Be direct and practical; use markdown, and put code in fenced blocks. <attached_file path=\"...\">...</attached_file> blocks hold real file contents (action=\"no-read\" = path only). <compacted_summary>...</compacted_summary> blocks are condensed summaries of earlier turns — treat them as ground truth."
283
+
284
+ # ------------------------------------------------------------
285
+ # agent_show_answer <response> — render markdown
286
+ # ------------------------------------------------------------
287
+ agent_show_answer() {
288
+ local resp="$1"
289
+ md_render "$resp"
290
+ }
291
+
292
+ # ------------------------------------------------------------
293
+ # agent_strip_meta_blocks <text> — remove <compacted_summary>
294
+ # blocks that the model sometimes echoes back into its answer.
295
+ # ------------------------------------------------------------
296
+ agent_strip_meta_blocks() {
297
+ local text="$1"
298
+ printf '%s' "$text" | sed -e 's/<compacted_summary>.*<\/compacted_summary>//g' \
299
+ -e '/<compacted_summary>/,/<\/compacted_summary>/d'
300
+ }
301
+
302
+ # ------------------------------------------------------------
303
+ # agent_clean_output <text> — post-process a model answer with
304
+ # pure bash so the system prompts stay short: drop echoed
305
+ # <compacted_summary> blocks and translate the LaTeX math the
306
+ # model loves to emit ($\rightarrow$, \Rightarrow, ...) into
307
+ # unicode arrows so they render instead of showing as raw text.
308
+ # ------------------------------------------------------------
309
+ agent_clean_output() {
310
+ local text="$1"
311
+ text=$(agent_strip_meta_blocks "$text")
312
+ text=$(printf '%s' "$text" | sed \
313
+ -e 's/\$\\rightarrow\$/→/g' \
314
+ -e 's/\$\\Rightarrow\$/⇒/g' \
315
+ -e 's/\$\\leftarrow\$/←/g' \
316
+ -e 's/\$\\Leftarrow\$/⇐/g' \
317
+ -e 's/\$\\to\$/→/g' \
318
+ -e 's/\$\\leftarrow\$/←/g' \
319
+ -e 's/\\rightarrow/→/g' \
320
+ -e 's/\\Rightarrow/⇒/g' \
321
+ -e 's/\\leftarrow/←/g' \
322
+ -e 's/\\Leftarrow/⇐/g' \
323
+ -e 's/\\to\b/→/g')
324
+ printf '%s' "$text"
325
+ }
326
+
327
+ # ------------------------------------------------------------
328
+ # agent_response_has_summary <text> — true if the model's answer
329
+ # already ends with a substantial prose summary AFTER its last
330
+ # action (file/command) block. Lets the run loop finish in one
331
+ # iteration when the model did the work AND wrote the summary,
332
+ # instead of forcing a redundant "final summary" iteration.
333
+ # ------------------------------------------------------------
334
+ agent_response_has_summary() {
335
+ local text="$1"
336
+ local trail prose
337
+ trail=$(printf '%s\n' "$text" | awk '
338
+ BEGIN { in_fence=0; last=0 }
339
+ /^[[:space:]]*```/ { in_fence = !in_fence; last=NR; next }
340
+ !in_fence && /^[[:space:]]*\$[[:space:]]/ { last=NR; next }
341
+ { a[NR]=$0 }
342
+ END { for (i=last+1; i<=NR; i++) print a[i] }
343
+ ')
344
+ prose=$(printf '%s\n' "$trail" | sed '/^[[:space:]]*#/d' | tr '\n' ' ')
345
+ prose=$(printf '%s\n' "$prose" | sed -E 's/[[:space:]]+/ /g; s/^[[:space:]]*//; s/[[:space:]]*$//')
346
+ [[ "${#prose}" -ge 40 ]]
347
+ }
348
+
349
+ # ------------------------------------------------------------
350
+ # agent_ask_once <prompt> — single-shot assistant answer
351
+ # ------------------------------------------------------------
352
+ agent_ask_once() {
353
+ local prompt="$1"
354
+ prompt=$(agent_prepare_prompt "$prompt")
355
+ if ! agent_check_server; then echo; return 1; fi
356
+
357
+ local history outfile resp
358
+ history=$(agent_history_new)
359
+ history=$(agent_history_system "$history" "$AGENT_ASK_SYSTEM")
360
+ history=$(agent_history_add "$history" user "$prompt")
361
+
362
+ echo
363
+ box "Ask"
364
+ echo
365
+
366
+ outfile=$(agent_md_dir)/ask_stream.txt
367
+ AGENT_ABORT=0
368
+ if agent_exec_loading "Asking ${D_CYAN}$AGENT_MODEL${D_NC}…" agent_chat_text_to "$history" "$outfile"; then
369
+ resp=$(agent_clean_output "$(cat "$outfile")")
370
+ echo
371
+ if [[ -z "$resp" ]]; then
372
+ log_warn "Empty response from the model"
373
+ else
374
+ agent_show_answer "$resp"
375
+ fi
376
+ elif (( AGENT_ABORT )); then
377
+ echo
378
+ else
379
+ log_error "Request failed (is the server running?)"
380
+ fi
381
+ echo
382
+ }
383
+
384
+ # ------------------------------------------------------------
385
+ # agent_repl_handle <line> — common slash-command handler.
386
+ # Returns 0 (continue), 1 (exit). Uses global AGENT_REPL_HISTORY.
387
+ # ------------------------------------------------------------
388
+ agent_repl_handle() {
389
+ local line="$1"
390
+ local mode="${2:-$AGENT_REPL_MODE}"
391
+ case "$line" in
392
+ /help | help)
393
+ agent_repl_help "$mode"
394
+ return 0
395
+ ;;
396
+ /exit | /quit | /q | exit | quit)
397
+ return 1
398
+ ;;
399
+ /clear)
400
+ AGENT_REPL_HISTORY=$(agent_history_new)
401
+ AGENT_REPL_STEPS=0
402
+ log_success "Conversation cleared"
403
+ echo
404
+ return 0
405
+ ;;
406
+ /status)
407
+ echo
408
+ agent_status_line
409
+ echo
410
+ return 0
411
+ ;;
412
+ /plan | /build)
413
+ if [[ "$mode" == "ask" ]]; then
414
+ echo
415
+ log_warn "Plan/Build modes only exist in ${D_CYAN}karnel agent run${D_NC} — ask is a plain chat"
416
+ echo
417
+ return 0
418
+ fi
419
+ if [[ "$line" == "/plan" ]]; then
420
+ AGENT_PLAN_MODE=1
421
+ echo
422
+ log_success "Mode → ${D_CYAN}PLAN (read-only)${D_NC} — file blocks and write commands will be ignored"
423
+ list_item "Next task will only run read-only commands and produce a plan"
424
+ else
425
+ AGENT_PLAN_MODE=0
426
+ echo
427
+ log_success "Mode → ${D_CYAN}BUILD (full access)${D_NC} — files and commands will be applied"
428
+ fi
429
+ echo
430
+ return 0
431
+ ;;
432
+ /voice)
433
+ echo
434
+ printf ' %sListening… speak now, then stop the dialog when done%s\n' "$D_CYAN" "$NC"
435
+ echo
436
+ if agent_voice_capture; then
437
+ AGENT_REPL_PREFILL="$AGENT_VOICE_TEXT"
438
+ log_success "Transcribed — edit it below, then press Enter to send:"
439
+ printf ' %s%s%s\n' "$D_GREEN" "$AGENT_VOICE_TEXT" "$NC"
440
+ fi
441
+ echo
442
+ return 0
443
+ ;;
444
+ /model*)
445
+ local m="${line#/model }"
446
+ m="${m#/model}"
447
+ m=$(echo "$m" | xargs)
448
+ if [[ -z "$m" ]]; then
449
+ log_info "Available models:"
450
+ agent_models_list 2>/dev/null | sed 's/^/ /' || log_warn "Cannot list models (check endpoint)"
451
+ agent_read_input "Model name" m
452
+ fi
453
+ if [[ -z "$m" ]]; then return 0; fi
454
+ AGENT_MODEL="$m"
455
+ log_success "Model → ${D_CYAN}$AGENT_MODEL${D_NC}"
456
+ return 0
457
+ ;;
458
+ /endpoint*)
459
+ local e="${line#/endpoint }"
460
+ e="${e#/endpoint}"
461
+ e=$(echo "$e" | xargs)
462
+ if [[ -z "$e" ]]; then agent_read_input "Endpoint URL (e.g. http://127.0.0.1:8000/v1)" e; fi
463
+ if [[ -z "$e" ]]; then return 0; fi
464
+ AGENT_ENDPOINT="$e"
465
+ log_success "Endpoint → ${D_CYAN}$AGENT_ENDPOINT${D_NC}"
466
+ return 0
467
+ ;;
468
+ /temp*)
469
+ local t="${line#/temp }"
470
+ t="${t#/temp}"
471
+ t=$(echo "$t" | xargs)
472
+ if [[ -z "$t" ]]; then agent_read_input "Temperature (0-2)" t; fi
473
+ if [[ -n "$t" ]]; then AGENT_TEMPERATURE="$t"; log_success "Temperature → ${D_CYAN}$AGENT_TEMPERATURE${D_NC}"; fi
474
+ return 0
475
+ ;;
476
+ /max*)
477
+ local mx="${line#/max }"
478
+ mx="${mx#/max}"
479
+ mx=$(echo "$mx" | xargs)
480
+ if [[ -z "$mx" ]]; then agent_read_input "Max tokens" mx; fi
481
+ if [[ -n "$mx" ]]; then AGENT_MAX_TOKENS="$mx"; log_success "Max tokens → ${D_CYAN}$AGENT_MAX_TOKENS${D_NC}"; fi
482
+ return 0
483
+ ;;
484
+ /workspace*)
485
+ local ws="${line#/workspace }"
486
+ ws="${ws#/workspace}"
487
+ ws=$(echo "$ws" | xargs)
488
+ if [[ -z "$ws" ]]; then agent_read_input "Workspace directory (absolute path)" ws; fi
489
+ if [[ -n "$ws" ]]; then
490
+ mkdir -p "$ws"
491
+ AGENT_WORKSPACE="$ws"
492
+ log_success "Workspace → ${D_CYAN}$AGENT_WORKSPACE${D_NC}"
493
+ fi
494
+ return 0
495
+ ;;
496
+ /history)
497
+ echo
498
+ if [[ "$(echo "$AGENT_REPL_HISTORY" | jq 'length')" == "0" ]]; then
499
+ log_warn "No history yet"
500
+ else
501
+ echo "$AGENT_REPL_HISTORY" | jq -r '.[] | " [" + .role + "] " + (.content | .[0:120] | gsub("[\r\n]"; " "))' | sed 's/^/ /'
502
+ fi
503
+ echo
504
+ return 0
505
+ ;;
506
+ *)
507
+ return 2
508
+ ;;
509
+ esac
510
+ }
511
+ # ------------------------------------------------------------
512
+ # agent_ask — entry point
513
+ # ------------------------------------------------------------
514
+ agent_ask() {
515
+ agent_parse_args "$@"
516
+ if (( AGENT_SHOW_HELP )); then
517
+ agent_help
518
+ return 0
519
+ fi
520
+ if [[ -n "$AGENT_PROMPT" ]]; then
521
+ # traps before ensure: Ctrl+C during the startup wait also stops it
522
+ trap 'agent_server_stop; exit 130' INT
523
+ trap 'agent_server_stop' EXIT
524
+ agent_server_ensure
525
+ agent_ask_once "$AGENT_PROMPT"
526
+ agent_server_stop
527
+ trap - INT EXIT
528
+ else
529
+ agent_ask_repl
530
+ fi
531
+ }
532
+
533
+ # ------------------------------------------------------------
534
+ # agent_ask_repl — interactive chat with streaming
535
+ # ------------------------------------------------------------
536
+ agent_ask_repl() {
537
+ AGENT_REPL_HISTORY=$(agent_history_new)
538
+ AGENT_REPL_STEPS=0
539
+ AGENT_REPL_MODE=ask
540
+
541
+ # traps must exist before the server starts, so Ctrl+C during the
542
+ # startup wait also stops a server we just launched
543
+ trap 'agent_server_stop; exit 130' INT
544
+ trap 'agent_server_stop' EXIT
545
+
546
+ # start the model server first (if down): shows the starting message
547
+ # and waits for it with a loading spinner, then clear the screen so
548
+ # the welcome banner starts clean.
549
+ agent_server_ensure
550
+ clear
551
+
552
+ agent_banner
553
+ separator_section "Karnel Agent — Ask"
554
+ echo
555
+ agent_status_line
556
+ echo
557
+ list_item "Type ${D_CYAN}/help${D_NC} to see all commands"
558
+ echo
559
+
560
+ agent_repl_bind
561
+
562
+ local line outfile resp _ems
563
+ while true; do
564
+ if ! agent_repl_read; then
565
+ break
566
+ fi
567
+ line="$AGENT_REPL_LINE"
568
+ [[ -z "$line" ]] && continue
569
+
570
+ agent_repl_handle "$line" "$AGENT_REPL_MODE"
571
+ local hc=$?
572
+ if (( hc == 1 )); then
573
+ break
574
+ elif (( hc == 0 )); then
575
+ continue
576
+ fi
577
+
578
+ # shell mode: !command runs it and feeds the output to the agent
579
+ if [[ "$line" == '!'* ]]; then
580
+ agent_shell_cmd "$line"
581
+ continue
582
+ fi
583
+
584
+ # actual message — expand @file references
585
+ line=$(agent_prepare_prompt "$line")
586
+ if ! agent_check_server; then echo; continue; fi
587
+ AGENT_REPL_HISTORY=$(agent_history_add "$AGENT_REPL_HISTORY" user "$line")
588
+ AGENT_REPL_HISTORY=$(agent_history_ensure_system "$AGENT_REPL_HISTORY" "$AGENT_ASK_SYSTEM")
589
+ AGENT_REPL_HISTORY=$(agent_history_manage "$AGENT_REPL_HISTORY" "$AGENT_CONTEXT_WINDOW")
590
+
591
+ echo
592
+ outfile=$(agent_md_dir)/ask_stream.txt
593
+ agent_timer_start
594
+ AGENT_ABORT=0
595
+ if agent_exec_loading "Asking ${D_CYAN}$AGENT_MODEL${D_NC}…" agent_chat_text_to "$AGENT_REPL_HISTORY" "$outfile"; then
596
+ _ems=$(agent_timer_ms)
597
+ resp=$(agent_clean_output "$(cat "$outfile")")
598
+ echo
599
+ if [[ -n "$resp" ]]; then
600
+ agent_show_answer "$resp"
601
+ AGENT_REPL_HISTORY=$(agent_history_add "$AGENT_REPL_HISTORY" assistant "$resp")
602
+ printf ' %s[context %s]%s\n' "$D_GRAY" "$(agent_context_footer "$AGENT_REPL_HISTORY" "$_ems")" "$NC"
603
+ fi
604
+ elif (( AGENT_ABORT )); then
605
+ echo
606
+ continue
607
+ else
608
+ log_error "Request failed (is the server running?)"
609
+ echo
610
+ fi
611
+ done
612
+ agent_repl_unbind
613
+ agent_server_stop
614
+ trap - INT EXIT
615
+ echo
616
+ if (( AGENT_CANCEL )); then
617
+ log_warn "Aborted — you pressed ESC ESC"
618
+ echo
619
+ fi
620
+ }
621
+
622
+ # ------------------------------------------------------------
623
+ # agent_run — entry point
624
+ # ------------------------------------------------------------
625
+ agent_run() {
626
+ agent_parse_args "$@"
627
+ if (( AGENT_SHOW_HELP )); then
628
+ agent_help
629
+ return 0
630
+ fi
631
+ AGENT_ACTIONS_WORKSPACE="$AGENT_WORKSPACE"
632
+ mkdir -p "$AGENT_WORKSPACE" 2>/dev/null
633
+
634
+ if [[ -n "$AGENT_PROMPT" ]]; then
635
+ # traps before ensure: Ctrl+C during the startup wait also stops it
636
+ trap 'agent_server_stop; exit 130' INT
637
+ trap 'agent_server_stop' EXIT
638
+ agent_server_ensure
639
+ AGENT_PROMPT=$(agent_prepare_prompt "$AGENT_PROMPT")
640
+ agent_run_loop "$AGENT_PROMPT"
641
+ agent_server_stop
642
+ trap - INT EXIT
643
+ else
644
+ agent_run_repl
645
+ fi
646
+ }
647
+
648
+ # ------------------------------------------------------------
649
+ # agent_run_repl — interactive: every message is a new task
650
+ # ------------------------------------------------------------
651
+ agent_run_repl() {
652
+ AGENT_REPL_HISTORY=$(agent_history_new)
653
+ AGENT_REPL_STEPS=0
654
+ AGENT_REPL_MODE=run
655
+
656
+ # traps must exist before the server starts, so Ctrl+C during the
657
+ # startup wait also stops a server we just launched
658
+ trap 'agent_server_stop; exit 130' INT
659
+ trap 'agent_server_stop' EXIT
660
+
661
+ # start the model server first (if down): shows the starting message
662
+ # and waits for it with a loading spinner, then clear the screen so
663
+ # the welcome banner starts clean.
664
+ agent_server_ensure
665
+ clear
666
+
667
+ agent_banner
668
+ separator_section "Karnel Agent — Run ($([[ $AGENT_PLAN_MODE == 1 ]] && echo 'PLAN · read-only' || echo 'BUILD mode'))"
669
+ echo
670
+ agent_status_line
671
+ echo
672
+ list_item "Type ${D_CYAN}/help${D_NC} to see all commands"
673
+ echo
674
+
675
+ agent_repl_bind
676
+
677
+ local line
678
+ while true; do
679
+ if ! agent_repl_read; then
680
+ break
681
+ fi
682
+ line="$AGENT_REPL_LINE"
683
+ [[ -z "$line" ]] && continue
684
+
685
+ agent_repl_handle "$line" "$AGENT_REPL_MODE"
686
+ local hc=$?
687
+ if (( hc == 1 )); then
688
+ break
689
+ elif (( hc == 0 )); then
690
+ continue
691
+ fi
692
+
693
+ # shell mode: !command runs it and feeds the output to the agent
694
+ if [[ "$line" == '!'* ]]; then
695
+ agent_shell_cmd "$line"
696
+ continue
697
+ fi
698
+
699
+ # expand @file references
700
+ line=$(agent_prepare_prompt "$line")
701
+ if ! agent_check_server; then echo; continue; fi
702
+ AGENT_ACTIONS_WORKSPACE="$AGENT_WORKSPACE"
703
+ agent_run_loop "$line" "$AGENT_REPL_HISTORY"
704
+ done
705
+ agent_repl_unbind
706
+ agent_server_stop
707
+ trap - INT EXIT
708
+ echo
709
+ if (( AGENT_CANCEL )); then
710
+ log_warn "Aborted — you pressed ESC ESC"
711
+ echo
712
+ fi
713
+ }
714
+
715
+ # ------------------------------------------------------------
716
+ # agent_run_loop <prompt> [history] — the karnel agent loop:
717
+ # LLM answers in markdown → bash parses files & commands →
718
+ # bash creates the files / runs the approved commands →
719
+ # results go back to the LLM until it writes a final summary
720
+ # ------------------------------------------------------------
721
+ agent_run_loop() {
722
+ local user_prompt="$1"
723
+ local shared_history="${2:-}"
724
+ local system history
725
+ system=$(agent_system_prompt "$AGENT_WORKSPACE")
726
+ if [[ -n "$shared_history" && "$shared_history" != "[]" ]]; then
727
+ history="$shared_history"
728
+ else
729
+ history=$(agent_history_new)
730
+ fi
731
+ # always (re)set the front system prompt to the CURRENT mode's prompt
732
+ history=$(agent_history_ensure_system "$history" "$system")
733
+ history=$(agent_history_add "$history" user "$user_prompt")
734
+
735
+ AGENT_ACTIONS_WORKSPACE="$AGENT_WORKSPACE"
736
+ AGENT_ABORT=0
737
+ agent_timer_start
738
+ # fresh read-dedup registry per task
739
+ : >"$(agent_md_dir)/ran_commands.txt"
740
+
741
+ local iter=0 step=0 resp outfile parsed nfiles ncmds results rfile _ems
742
+ while (( iter < AGENT_MAX_ITERATIONS )); do
743
+ step=$((iter + 1))
744
+ printf ' %s[%s/%s]%s %sthinking…%s\n' "$D_GRAY" "$step" "$AGENT_MAX_ITERATIONS" "$NC" "$D_PURPLE" "$NC"
745
+
746
+ outfile=$(agent_md_dir)/run_stream.txt
747
+ if agent_exec_loading " model reasoning…" agent_chat_text_to "$history" "$outfile"; then
748
+ resp=$(agent_clean_output "$(cat "$outfile")")
749
+ elif (( AGENT_ABORT )); then
750
+ break
751
+ else
752
+ log_error "Request failed (is the server running?)"
753
+ break
754
+ fi
755
+ if [[ -z "$resp" ]]; then
756
+ log_error "Empty response from the model (is the server running?)"
757
+ break
758
+ fi
759
+
760
+ # visual feedback: render the full markdown answer on screen
761
+ echo
762
+ md_render "$resp"
763
+
764
+ parsed=$(agent_parse_response "$resp")
765
+ read -r nfiles ncmds <<<"$parsed"
766
+
767
+ # no files and no commands → this is the final answer
768
+ if (( nfiles == 0 && ncmds == 0 )); then
769
+ history=$(agent_history_add "$history" assistant "$resp")
770
+ break
771
+ fi
772
+
773
+ history=$(agent_history_add "$history" assistant "$resp")
774
+
775
+ AGENT_FILES_WRITTEN=0
776
+ if (( nfiles > 0 )); then
777
+ if (( AGENT_PLAN_MODE )); then
778
+ # PLAN mode: never write; just tell the model what was skipped
779
+ echo
780
+ separator_section "Plan mode — files skipped"
781
+ printf ' %s↳ %s ## File: block(s) NOT written (read-only mode)%s\n' "$D_GRAY" "$nfiles" "$NC"
782
+ AGENT_FILES_WRITTEN=0
783
+ else
784
+ agent_apply_files
785
+ # file writes invalidate earlier reads: allow re-reading to verify
786
+ (( AGENT_FILES_WRITTEN > 0 )) && : >"$(agent_md_dir)/ran_commands.txt"
787
+ fi
788
+ fi
789
+
790
+ results=""
791
+ if (( ncmds > 0 )); then
792
+ echo
793
+ separator_section "Running commands"
794
+ agent_execute_commands
795
+ if (( AGENT_ABORT )); then
796
+ break
797
+ fi
798
+ rfile="$(agent_md_dir)/command_results.txt"
799
+ [[ -s "$rfile" ]] && results=$(cat "$rfile")
800
+ fi
801
+
802
+ # The model already wrote its final summary in this response and
803
+ # there is no command output left to fold in — the task is done.
804
+ # Only files were written (or nothing at all), so forcing another
805
+ # iteration just to repeat the summary would be wasteful.
806
+ if (( ! AGENT_PLAN_MODE )) && [[ -z "$results" ]] && agent_response_has_summary "$resp"; then
807
+ break
808
+ fi
809
+
810
+ if [[ -n "$results" ]]; then
811
+ history=$(agent_history_add "$history" user "$results")
812
+ fi
813
+
814
+ if (( AGENT_PLAN_MODE )); then
815
+ # plan mode: keep the model from trying to apply changes
816
+ if (( nfiles > 0 )); then
817
+ history=$(agent_history_add "$history" user "<system>PLAN mode is active (read-only): your ## File: blocks were NOT written and write commands were blocked. Only read-only commands ran. Now present your plan (files to create or edit with their proposed content, commands to run). Do NOT emit more file blocks or write commands until the user switches to BUILD mode (type /build).</system>")
818
+ elif [[ -z "$results" ]]; then
819
+ history=$(agent_history_add "$history" user "<system>PLAN mode is active (read-only). Keep inspecting with read-only command blocks if needed; when ready, write your analysis and plan as a final summary with NO file or command blocks.</system>")
820
+ fi
821
+ elif [[ -z "$results" ]]; then
822
+ if (( AGENT_FILES_WRITTEN > 0 )); then
823
+ history=$(agent_history_add "$history" user "<system>Files were created or updated. Verify them with a command block if needed, then write your final summary (markdown) with no file or command blocks.</system>")
824
+ else
825
+ history=$(agent_history_add "$history" user "<system>No files were written and no commands ran (any file blocks the response contained targeted write-protected no-read files, or were identical to files already on disk, and were skipped). If the task needs a shell action (delete/move/rename/run/...), you MUST emit it as a \`$ \` or \`\`\`bash command block — a \`## File:\` block never runs anything and is ignored for write-protected files. Then finish with a short summary.</system>")
826
+ fi
827
+ fi
828
+
829
+ history=$(agent_history_manage "$history" "$AGENT_CONTEXT_WINDOW")
830
+ ((iter++))
831
+ done
832
+
833
+ if (( iter >= AGENT_MAX_ITERATIONS )); then
834
+ echo
835
+ log_warn "Stopped after $AGENT_MAX_ITERATIONS iterations (task too complex?)"
836
+ fi
837
+ AGENT_REPL_HISTORY="$history"
838
+ _ems=$(agent_timer_ms)
839
+ echo
840
+ printf ' %s[context %s]%s\n' "$D_GRAY" "$(agent_context_footer "$AGENT_REPL_HISTORY" "$_ems")" "$NC"
841
+ }