karnel-termux 4.17.21 → 4.17.23

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 (48) hide show
  1. package/README.md +5 -4
  2. package/docs/CHANGELOG.md +8 -0
  3. package/install.sh +20 -5
  4. package/karnel/RELEASE_COMMIT +1 -1
  5. package/karnel/cli/commands/agent.sh +9 -5
  6. package/karnel/cli/commands/brain.sh +18 -14
  7. package/karnel/cli/commands/doctor/fixes.sh +4 -4
  8. package/karnel/cli/commands/doctor.sh +4 -2
  9. package/karnel/cli/commands/ia.sh +5 -1
  10. package/karnel/cli/commands/pg.sh +5 -2
  11. package/karnel/cli/commands/restore.sh +3 -0
  12. package/karnel/cli/commands/update.sh +18 -0
  13. package/karnel/cli/commands/upgrade.sh +9 -2
  14. package/karnel/cli/commands/voice.sh +4 -2
  15. package/karnel/modules/voice.sh +8 -5
  16. package/karnel/tools/ai/codebuff/install.sh +69 -9
  17. package/karnel/tools/ai/codex/install.sh +3 -0
  18. package/karnel/tools/ai/gemini-cli/install.sh +3 -0
  19. package/karnel/tools/ai/kimi-code/install.sh +3 -0
  20. package/karnel/tools/ai/minimax-cli/install.sh +3 -0
  21. package/karnel/tools/ai/openclaude/install.sh +3 -0
  22. package/karnel/tools/ai/openclaw/install.sh +3 -0
  23. package/karnel/tools/ai/pi/install.sh +3 -0
  24. package/karnel/tools/ai/qoder/install.sh +140 -29
  25. package/karnel/tools/ai/qwen-code/install.sh +3 -0
  26. package/karnel/tools/deploy/netlify/install.sh +10 -3
  27. package/karnel/tools/deploy/vercel/install.sh +10 -3
  28. package/karnel/tools/lang/bun/install.sh +2 -2
  29. package/karnel/tools/npm/all.sh +1 -23
  30. package/karnel/tools/npm/live-server/install.sh +2 -0
  31. package/karnel/tools/npm/localtunnel/install.sh +3 -1
  32. package/karnel/tools/npm/markserv/install.sh +2 -0
  33. package/karnel/tools/npm/ncu/install.sh +2 -0
  34. package/karnel/tools/npm/nestjs/install.sh +2 -0
  35. package/karnel/tools/npm/ngrok/install.sh +2 -0
  36. package/karnel/tools/npm/prettier/install.sh +2 -0
  37. package/karnel/tools/npm/psqlformat/install.sh +2 -0
  38. package/karnel/tools/npm/typescript/install.sh +2 -0
  39. package/karnel/tools/npm/vercel/install.sh +2 -0
  40. package/karnel/tools/utils/filecheck/filecheck.py +1 -0
  41. package/karnel/tools/utils/qrcode/qrcode.py +1 -0
  42. package/karnel/tools/utils/websites/websites.py +1 -0
  43. package/karnel/tools/voice/uninstall.sh +1 -0
  44. package/karnel/utils/agent_actions.sh +8 -0
  45. package/karnel/utils/agent_llm.sh +7 -7
  46. package/karnel/utils/downloaded-python.sh +1 -1
  47. package/karnel/utils/npm-shebang.sh +30 -0
  48. package/package.json +1 -1
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  <p align="center">
10
10
  <a href="https://github.com/israelmarques1024-dotcom/karnel-termux">
11
- <img src="https://img.shields.io/badge/version-4.17.21-0078D4?style=for-the-badge" alt="Version">
11
+ <img src="https://img.shields.io/badge/version-4.17.22-0078D4?style=for-the-badge" alt="Version">
12
12
  </a>
13
13
  <a href="https://www.npmjs.com/package/karnel-termux">
14
14
  <img src="https://img.shields.io/npm/v/karnel-termux?style=for-the-badge&logo=npm&color=cb3837" alt="npm">
@@ -78,7 +78,7 @@ Why it's great:
78
78
  ### Via checksummed GitHub release (recommended)
79
79
 
80
80
  ```bash
81
- version=4.17.21
81
+ version=4.17.22
82
82
  tmpdir=$(mktemp -d) && trap 'rm -rf "$tmpdir"' EXIT
83
83
  base="https://github.com/israelmarques1024-dotcom/karnel-termux/releases/download/v$version"
84
84
  curl -fsSL "$base/karnel-termux-install.sh" -o "$tmpdir/karnel-termux-install.sh"
@@ -636,8 +636,9 @@ karnel update karnel # Update the framework
636
636
 
637
637
  `karnel update` requires a target such as `karnel`, `ai`, or `security`.
638
638
  `karnel update karnel` first runs the official curl installer, then falls back to
639
- the local Git checkout and package-manager installs if needed. The curl installer
640
- is pinned to the latest GitHub release tag, and its SHA-256 checksum is verified
639
+ the local Git checkout if needed. Package-manager fallback is used only when the
640
+ active framework was installed by that package manager. The curl installer is
641
+ pinned to the latest GitHub release tag, and its SHA-256 checksum is verified
641
642
  before it runs. The framework checks for a new version in the background at most
642
643
  once every 24 hours; the check can use npm or GitHub and writes state under
643
644
  `$KARNEL_CACHE`.
package/docs/CHANGELOG.md CHANGED
@@ -6,6 +6,14 @@ layout: base
6
6
 
7
7
  # Documentation Changelog
8
8
 
9
+ ## 4.17.22
10
+
11
+ - **Data safety and correctness (8):** restore rejects ambiguous multiple archives; Brain preserves same-day title collisions, uses file paths for AI context, initializes/pushes `main`, detects missing `origin`, and supports editors with arguments; PostgreSQL backup filenames are path-safe; AI-session persistence escapes JSON correctly.
12
+ - **Agent safety (3):** positional prompts retain all words, zero-valued execution/context limits are rejected, and PLAN mode rejects shell syntax plus destructive `find`/in-place operations that bypassed its read-only contract.
13
+ - **Installer reliability (1):** Bun proot installation/update no longer reject safe archives before extraction.
14
+ - **Regression coverage:** added critical CLI contracts and strengthened Brain reciprocal-relation coverage; the isolated full suite passes.
15
+ - Version references bumped to `4.17.22`.
16
+
9
17
  ## 4.17.21
10
18
 
11
19
  - **Official-site routing:** `karnel open`, npm package metadata, and the GitHub/npm README now point to the Vercel production site, `https://karneltermux.vercel.app`, rather than the secondary GitHub Pages mirror.
package/install.sh CHANGED
@@ -147,7 +147,10 @@ bootstrap_dependencies() {
147
147
 
148
148
  if [[ $needed_tput -eq 1 ]]; then
149
149
  echo -e " ${P_BORDER}→${P_NC} Installing ncurses-utils..."
150
- pkg install -y ncurses-utils &>/dev/null || true
150
+ if ! pkg install -y ncurses-utils &>/dev/null; then
151
+ log_fail "Failed to install ncurses-utils"
152
+ return 1
153
+ fi
151
154
  echo -e " ${P_OK}✔${P_NC} ncurses installed"
152
155
  echo
153
156
  fi
@@ -155,7 +158,10 @@ bootstrap_dependencies() {
155
158
  if [[ $needed_git -eq 1 ]]; then
156
159
  log_info "Installing git..."
157
160
  progress_bar 0 10
158
- pkg install -y git &>/dev/null || true
161
+ if ! pkg install -y git &>/dev/null; then
162
+ log_fail "Failed to install git"
163
+ return 1
164
+ fi
159
165
  progress_bar 10 10
160
166
  echo
161
167
  log_ok "git installed"
@@ -164,7 +170,10 @@ bootstrap_dependencies() {
164
170
  if [[ $needed_glow -eq 1 ]]; then
165
171
  log_info "Installing glow..."
166
172
  progress_bar 0 10
167
- pkg install -y glow &>/dev/null || true
173
+ if ! pkg install -y glow &>/dev/null; then
174
+ log_fail "Failed to install glow"
175
+ return 1
176
+ fi
168
177
  progress_bar 10 10
169
178
  echo
170
179
  log_ok "glow installed"
@@ -173,7 +182,10 @@ bootstrap_dependencies() {
173
182
  if [[ $needed_gh -eq 1 ]]; then
174
183
  log_info "Installing gh (GitHub CLI)..."
175
184
  progress_bar 0 10
176
- pkg install -y gh &>/dev/null || true
185
+ if ! pkg install -y gh &>/dev/null; then
186
+ log_fail "Failed to install gh"
187
+ return 1
188
+ fi
177
189
  progress_bar 10 10
178
190
  echo
179
191
  log_ok "gh installed"
@@ -182,7 +194,10 @@ bootstrap_dependencies() {
182
194
  if [[ $needed_rg -eq 1 ]]; then
183
195
  log_info "Installing ripgrep..."
184
196
  progress_bar 0 10
185
- pkg install -y ripgrep &>/dev/null || true
197
+ if ! pkg install -y ripgrep &>/dev/null; then
198
+ log_fail "Failed to install ripgrep"
199
+ return 1
200
+ fi
186
201
  progress_bar 10 10
187
202
  echo
188
203
  log_ok "ripgrep installed"
@@ -1 +1 @@
1
- 6b13b468f425a13040e4dac77eef1b449a553ea8
1
+ 9e1cc56cd0e413f5135180c07ae5c9cea021e4ba
@@ -255,10 +255,11 @@ agent_parse_args() {
255
255
  AGENT_PROMPT=""
256
256
  AGENT_SHOW_HELP=0
257
257
  AGENT_YES=0
258
- local prev=""
258
+ local prev="" prompt_set=0
259
+ local -a positional=()
259
260
  for arg in "$@"; do
260
261
  case "$prev" in
261
- -p | --prompt) AGENT_PROMPT="$arg"; prev=""; continue ;;
262
+ -p | --prompt) AGENT_PROMPT="$arg"; prompt_set=1; prev=""; continue ;;
262
263
  -m | --model) AGENT_MODEL="$arg"; prev=""; continue ;;
263
264
  -u | --endpoint) AGENT_ENDPOINT="$arg"; prev=""; continue ;;
264
265
  -t | --temperature) AGENT_TEMPERATURE="$arg"; prev=""; continue ;;
@@ -267,7 +268,7 @@ agent_parse_args() {
267
268
  --max-tokens) AGENT_MAX_TOKENS="$arg"; prev=""; continue ;;
268
269
  esac
269
270
  case "$arg" in
270
- -p=* | --prompt=*) AGENT_PROMPT="${arg#*=}" ;;
271
+ -p=* | --prompt=*) AGENT_PROMPT="${arg#*=}"; prompt_set=1 ;;
271
272
  -m=* | --model=*) AGENT_MODEL="${arg#*=}" ;;
272
273
  -u=* | --endpoint=*) AGENT_ENDPOINT="${arg#*=}" ;;
273
274
  -t=* | --temperature=*) AGENT_TEMPERATURE="${arg#*=}" ;;
@@ -280,9 +281,12 @@ agent_parse_args() {
280
281
  -y | --yes) AGENT_YES=1 ;;
281
282
  -h | --help) AGENT_SHOW_HELP=1 ;;
282
283
  -*) log_warn "Unknown option: $arg" ;;
283
- *) [[ -z "$AGENT_PROMPT" ]] && AGENT_PROMPT="$arg" ;;
284
+ *) positional+=("$arg") ;;
284
285
  esac
285
286
  done
287
+ if (( ! prompt_set && ${#positional[@]} )); then
288
+ AGENT_PROMPT="${positional[*]}"
289
+ fi
286
290
  }
287
291
  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."
288
292
 
@@ -846,4 +850,4 @@ agent_run_loop() {
846
850
  _ems=$(agent_timer_ms)
847
851
  echo
848
852
  printf ' %s[context %s]%s\n' "$D_GRAY" "$(agent_context_footer "$AGENT_REPL_HISTORY" "$_ems")" "$NC"
849
- }
853
+ }
@@ -349,7 +349,7 @@ brain_init() {
349
349
  log_info "Creating private repo: ${D_CYAN}$gh_user/$repo_name${D_NC}..."
350
350
  if gh repo create "$repo_name" --private &>/dev/null; then
351
351
  (
352
- git -C "$BRAIN_DIR" init &>/dev/null
352
+ git -C "$BRAIN_DIR" init -b main &>/dev/null || exit 1
353
353
  echo "# Karnel Brain" >"$BRAIN_DIR/README.md"
354
354
  git -C "$BRAIN_DIR" add -A
355
355
  git -C "$BRAIN_DIR" commit -m "init brain" &>/dev/null
@@ -474,6 +474,10 @@ brain_save() {
474
474
  # ── Write file ──
475
475
  mkdir -p "$BRAIN_DIR/$category"
476
476
  local filepath="$BRAIN_DIR/$category/${date_prefix}_${slug}.md"
477
+ if [[ -e "$filepath" ]]; then
478
+ log_error "A memory with this title already exists today: ${date_prefix}_${slug}.md"
479
+ return 1
480
+ fi
477
481
 
478
482
  {
479
483
  _brain_frontmatter "$title" "$tags_formatted" "$category" "$related_str"
@@ -576,6 +580,10 @@ brain_add() {
576
580
 
577
581
  mkdir -p "$BRAIN_DIR/$category"
578
582
  local filepath="$BRAIN_DIR/$category/${date_prefix}_${slug}.md"
583
+ if [[ -e "$filepath" ]]; then
584
+ log_error "A memory with this title already exists today: ${date_prefix}_${slug}.md"
585
+ return 1
586
+ fi
579
587
 
580
588
  {
581
589
  _brain_frontmatter "$title" "$tags_formatted" "$category" ""
@@ -928,7 +936,7 @@ brain_sync() {
928
936
  local branch
929
937
  branch=$(git -C "$BRAIN_DIR" rev-parse --abbrev-ref HEAD 2>/dev/null)
930
938
 
931
- if ! git -C "$BRAIN_DIR" remote -v &>/dev/null; then
939
+ if ! git -C "$BRAIN_DIR" remote get-url origin &>/dev/null; then
932
940
  log_info "No remote configured — local commit only"
933
941
  list_item "Set up GitHub sync with: ${D_CYAN}karnel brain init${D_NC}"
934
942
  separator
@@ -1231,22 +1239,18 @@ brain_edit() {
1231
1239
  return 0
1232
1240
  fi
1233
1241
 
1234
- local editor="${EDITOR:-}"
1235
- local editor_bin
1236
- editor_bin="${EDITOR:-nano}"
1237
- editor_bin="${editor_bin%% *}"
1238
- if command -v "$editor_bin" &>/dev/null; then
1239
- editor="${EDITOR:-nano}"
1240
- elif [[ -z "$editor" ]] || ! command -v "$editor" &>/dev/null; then
1242
+ local -a editor_cmd=()
1243
+ read -r -a editor_cmd <<< "${EDITOR:-nano}"
1244
+ if ! command -v "${editor_cmd[0]}" &>/dev/null; then
1241
1245
  for e in vim nano vi; do
1242
1246
  if command -v "$e" &>/dev/null; then
1243
- editor="$e"
1247
+ editor_cmd=("$e")
1244
1248
  break
1245
1249
  fi
1246
1250
  done
1247
1251
  fi
1248
1252
 
1249
- if [[ -z "$editor" ]]; then
1253
+ if [[ ${#editor_cmd[@]} -eq 0 ]] || ! command -v "${editor_cmd[0]}" &>/dev/null; then
1250
1254
  log_error "No editor found"
1251
1255
  list_item "Install one: ${D_CYAN}karnel install editor${D_NC}"
1252
1256
  return 1
@@ -1255,7 +1259,7 @@ brain_edit() {
1255
1259
  echo
1256
1260
  log_info "Opening: ${D_CYAN}$(_brain_title "$file")${D_NC}"
1257
1261
 
1258
- "$editor" "$file"
1262
+ "${editor_cmd[@]}" "$file"
1259
1263
 
1260
1264
  log_success "Memory updated"
1261
1265
  echo
@@ -1543,7 +1547,7 @@ EOF
1543
1547
  if [[ -n "$f" ]]; then
1544
1548
  matching_files+=("$f")
1545
1549
  fi
1546
- done < <(_brain_rg -i "$clean_query" | head -n 5 || true)
1550
+ done < <(_brain_rg -l -i "$clean_query" | head -n 5 || true)
1547
1551
 
1548
1552
  if [[ ${#matching_files[@]} -eq 0 ]]; then
1549
1553
  local -a words=($clean_query)
@@ -1562,7 +1566,7 @@ EOF
1562
1566
  if [[ -n "$f" ]]; then
1563
1567
  matching_files+=("$f")
1564
1568
  fi
1565
- done < <(_brain_rg -i -e "$word_regex" | head -n 5 || true)
1569
+ done < <(_brain_rg -l -i -e "$word_regex" | head -n 5 || true)
1566
1570
  fi
1567
1571
  fi
1568
1572
 
@@ -342,10 +342,11 @@ _fix_script_symlinks() {
342
342
  local tool_name
343
343
  tool_name=$(basename "$tool_dir")
344
344
  local bin_names
345
- bin_names=$(grep -E '^local BIN_NAME=' "$installer" 2>/dev/null | head -1 | sed "s/.*BIN_NAME=\"\?//;s/\"\?$//")
345
+ bin_names=$(grep -E '^[[:space:]]*local BIN_NAME=' "$installer" 2>/dev/null | head -1 | sed "s/.*BIN_NAME=\"\?//;s/\"\?$//")
346
346
  [[ -z "$bin_names" ]] && bin_names="$tool_name"
347
347
  for bin_name in $bin_names; do
348
348
  local expected="$tool_dir/$bin_name.py"
349
+ [[ -f "$expected" ]] || continue
349
350
  local link="$PREFIX/bin/$bin_name"
350
351
  if [[ -L "$link" ]]; then
351
352
  local target
@@ -403,15 +404,14 @@ _fix_script_missing() {
403
404
  local module_name
404
405
  module_name=$(basename "$module_dir")
405
406
  local bin_names
406
- bin_names=$(grep -E '^local BIN_NAME=' "$installer" 2>/dev/null | head -1 | sed "s/.*BIN_NAME=\"\?//;s/\"\?$//")
407
+ bin_names=$(grep -E '^[[:space:]]*local BIN_NAME=' "$installer" 2>/dev/null | head -1 | sed "s/.*BIN_NAME=\"\?//;s/\"\?$//")
407
408
  [[ -z "$bin_names" ]] && bin_names="$tool_name"
408
409
  for bin_name in $bin_names; do
409
410
  ((total++))
410
411
  if ! command -v "$bin_name" &>/dev/null; then
411
412
  karnel install "$module_name" "--$tool_name" &>/dev/null && ((fixed++))
412
413
  fi
413
- done
414
- done < <(find "$module_dir" -mindepth 1 -maxdepth 1 -type d -print0 2>/dev/null || true)
414
+ done done < <(find "$module_dir" -mindepth 1 -maxdepth 1 -type d -print0 2>/dev/null || true)
415
415
  done < <(find "$KARNEL_PATH/tools/" -mindepth 1 -maxdepth 1 -type d -print0 2>/dev/null || true)
416
416
  [[ $fixed -gt 0 ]] && log_success "Reinstalled $fixed/$total missing tool(s)"
417
417
  return 0
@@ -671,7 +671,7 @@ doctor_termux() {
671
671
  [[ -f "$installer" ]] || continue
672
672
 
673
673
  local bin_names
674
- bin_names=$(grep -E '^local BIN_NAME=' "$installer" 2>/dev/null | head -1 | sed "s/.*BIN_NAME=\"\?//;s/\"\?$//")
674
+ bin_names=$(grep -E '^[[:space:]]*local BIN_NAME=' "$installer" 2>/dev/null | head -1 | sed "s/.*BIN_NAME=\"\?//;s/\"\?$//")
675
675
  [[ -z "$bin_names" ]] && bin_names="$tool_name"
676
676
 
677
677
  for bin_name in $bin_names; do
@@ -684,12 +684,14 @@ doctor_termux() {
684
684
  if [[ "$target" == "$tool_script" ]]; then
685
685
  log_success "$module_name/$tool_name → $bin_name (OK)"
686
686
  ((script_ok++))
687
+ elif [[ ! -f "$tool_script" ]]; then
688
+ log_info "$bin_name: managed outside Karnel python payloads (→ $target)"
687
689
  else
688
690
  log_warn "$bin_name: symlink target changed (→ $target, expected $tool_script)"
689
691
  ((script_warn++)); found_symlink_issues=true
690
692
  fi
691
693
 
692
- if [[ -f "$bin_path" ]] && head -1 "$bin_path" 2>/dev/null | grep -q "^#!/usr/bin/env"; then
694
+ if [[ -f "$tool_script" ]] && [[ -f "$bin_path" ]] && head -1 "$bin_path" 2>/dev/null | grep -q "^#!/usr/bin/env"; then
693
695
  log_warn "$bin_name: uses #!/usr/bin/env — will fail on Termux"
694
696
  ((script_warn++)); found_shebang_issues=true
695
697
  fi
@@ -420,7 +420,11 @@ PY
420
420
  ts_raw=$(echo "$session" | cut -d'|' -f2)
421
421
  name=$(echo "$session" | cut -d'|' -f3-)
422
422
  local escaped_name
423
- escaped_name=$(printf '%s' "$name" | sed 's/"/\\"/g' | sed "s/'/\\\\'/g")
423
+ escaped_name=${name//\\/\\\\}
424
+ escaped_name=${escaped_name//\"/\\\"}
425
+ escaped_name=${escaped_name//$'\n'/\\n}
426
+ escaped_name=${escaped_name//$'\r'/\\r}
427
+ escaped_name=${escaped_name//$'\t'/\\t}
424
428
  if [[ $first -eq 1 ]]; then
425
429
  first=0
426
430
  else
@@ -514,7 +514,10 @@ pg_backup() {
514
514
  log_info "Creating compressed backup for '$db_name'..."
515
515
  local backup_dir="$KARNEL_DATA/pg_backups"
516
516
  mkdir -p "$backup_dir"
517
- local file_name="${db_name}_$(date +%Y%m%d_%H%M%S).backup.gz"
517
+ local safe_db_name
518
+ safe_db_name=$(printf '%s' "$db_name" | tr -c '[:alnum:]_.-' '_')
519
+ [[ -n "$safe_db_name" ]] || { log_error "Invalid database name"; return 1; }
520
+ local file_name="${safe_db_name}_$(date +%Y%m%d_%H%M%S).backup.gz"
518
521
  local file_path="$backup_dir/$file_name"
519
522
 
520
523
  if loading "Running backup dump" _run_backup_cmd "$db_name" "$file_path"; then
@@ -522,7 +525,7 @@ pg_backup() {
522
525
  log_success "Backup created successfully:"
523
526
  list_item "$file_path"
524
527
  list_item "Checksum generated: $(cat "${file_path}.sha256")"
525
- _pg_cleanup_old_backups "$db_name"
528
+ _pg_cleanup_old_backups "$safe_db_name"
526
529
  else
527
530
  log_error "Failed to create backup"
528
531
  rm -f "$file_path"
@@ -51,6 +51,9 @@ restore_main() {
51
51
  log_error "File not found: $arg"
52
52
  return 1
53
53
  fi
54
+ else
55
+ log_error "Restore accepts exactly one backup file"
56
+ return 1
54
57
  fi
55
58
  ;;
56
59
  esac
@@ -357,8 +357,18 @@ _npm_installed_version() {
357
357
  printf '%s' "${v:-unknown}"
358
358
  }
359
359
 
360
+ _update_global_package_is_active() {
361
+ local manager="${1:-npm}" root
362
+ root=$("$manager" root -g 2>/dev/null) || return 1
363
+ [[ "$KARNEL_PATH" == "$root/karnel-termux/karnel" ]]
364
+ }
365
+
360
366
  _update_try_npm() {
361
367
  command -v npm &>/dev/null || return 1
368
+ if ! _update_global_package_is_active; then
369
+ log_info "Skipping npm update: the active Karnel framework is not npm-managed"
370
+ return 1
371
+ fi
362
372
  log_info "Trying npm update..."
363
373
  if npm update -g karnel-termux --ignore-scripts 2>/dev/null; then
364
374
  local new_ver
@@ -372,6 +382,10 @@ _update_try_npm() {
372
382
 
373
383
  _update_try_npm_install() {
374
384
  command -v npm &>/dev/null || return 1
385
+ if ! _update_global_package_is_active; then
386
+ log_info "Skipping npm install: the active Karnel framework is not npm-managed"
387
+ return 1
388
+ fi
375
389
  log_info "Trying npm install..."
376
390
  if npm install -g karnel-termux@latest --ignore-scripts 2>/dev/null; then
377
391
  local new_ver
@@ -384,6 +398,10 @@ _update_try_npm_install() {
384
398
 
385
399
  _update_try_pnpm() {
386
400
  command -v pnpm &>/dev/null || return 1
401
+ if ! _update_global_package_is_active pnpm; then
402
+ log_info "Skipping pnpm update: the active Karnel framework is not pnpm-managed"
403
+ return 1
404
+ fi
387
405
  log_info "Trying pnpm..."
388
406
  if pnpm add -g karnel-termux@latest 2>/dev/null; then
389
407
  log_success "Updated via pnpm"
@@ -15,11 +15,14 @@ upgrade_main() {
15
15
  return 1
16
16
  fi
17
17
 
18
+ if ! _upgrade_fix_symlink; then
19
+ log_error "Karnel upgrade could not activate its command"
20
+ return 1
21
+ fi
22
+
18
23
  source "$KARNEL_PATH/utils/env.sh" 2>/dev/null
19
24
  log_success "Karnel upgraded to v$KARNEL_VERSION"
20
25
 
21
- _upgrade_fix_symlink
22
-
23
26
  echo
24
27
  log_info "Running cleanup..."
25
28
  import "@/cli/commands/cleanup"
@@ -30,6 +33,10 @@ upgrade_main() {
30
33
  }
31
34
 
32
35
  _upgrade_fix_symlink() {
36
+ if [[ -e "$PREFIX/bin/karnel" && ! -L "$PREFIX/bin/karnel" ]]; then
37
+ log_error "Refusing to replace existing non-symlink: $PREFIX/bin/karnel"
38
+ return 1
39
+ fi
33
40
  if [[ ! -L "$PREFIX/bin/karnel" ]] || [[ "$(readlink "$PREFIX/bin/karnel")" != "$KARNEL_PATH/bin/karnel" ]]; then
34
41
  ln -sf "$KARNEL_PATH/bin/karnel" "$PREFIX/bin/karnel" 2>/dev/null
35
42
  log_success "Symlink updated"
@@ -107,7 +107,9 @@ voice_main() {
107
107
  fi
108
108
  fi
109
109
 
110
- if ! command -v "${EDITOR:-nano}" &>/dev/null && [[ "$skip_edit" == "false" ]]; then
110
+ local -a editor_cmd=()
111
+ read -r -a editor_cmd <<< "${EDITOR:-nano}"
112
+ if ! command -v "${editor_cmd[0]}" &>/dev/null && [[ "$skip_edit" == "false" ]]; then
111
113
  log_warn "${EDITOR:-nano} not installed — falling back to --raw mode"
112
114
  skip_edit=true
113
115
  fi
@@ -154,7 +156,7 @@ voice_main() {
154
156
  tmpfile="$(mktemp "${TMPDIR:-${KARNEL_CACHE:-$HOME/.cache/karnel}}/karnel-XXXXXX")"
155
157
  echo "$raw" >"$tmpfile"
156
158
  $is_text || log_info "Review the prompt in ${EDITOR:-nano}, fix mistakes, then save and quit"
157
- ${EDITOR:-nano} "$tmpfile" </dev/tty >/dev/tty || true
159
+ "${editor_cmd[@]}" "$tmpfile" </dev/tty >/dev/tty || true
158
160
  prompt="$(< "$tmpfile")"
159
161
  rm -f "$tmpfile"
160
162
  elif [[ "$skip_edit" == "false" ]] && ! { [[ -t 0 ]] && [[ -t 1 ]]; }; then
@@ -4,6 +4,7 @@ import "@/utils/log"
4
4
  import "@/utils/colors"
5
5
 
6
6
  LOG_FILE="$KARNEL_CACHE/install_voice.log"
7
+ VOICE_MARKER="${KARNEL_DATA:-${XDG_DATA_HOME:-$HOME/.local/share}/karnel-data}/voice/.termux-api-managed"
7
8
 
8
9
  install_voice() {
9
10
  separator
@@ -41,8 +42,10 @@ install_voice() {
41
42
  }
42
43
 
43
44
  _install_voice_deps() {
44
- pkg install termux-api -y &>>"$LOG_FILE"
45
- return $?
45
+ if ! pkg install termux-api -y &>>"$LOG_FILE"; then
46
+ return 1
47
+ fi
48
+ mkdir -p "$(dirname "$VOICE_MARKER")" && : >"$VOICE_MARKER"
46
49
  }
47
50
 
48
51
  _install_voice_shell() {
@@ -122,7 +125,7 @@ reinstall_voice() {
122
125
  }
123
126
 
124
127
  _reinstall_voice_wrapper() {
125
- import "@/tools/voice/all"
126
- reinstall_all_voice_components
127
- return $?
128
+ _uninstall_voice_wrapper || return $?
129
+ _install_voice_deps || return $?
130
+ _install_voice_shell
128
131
  }
@@ -9,6 +9,8 @@ LOG_FILE="$KARNEL_CACHE/install_ai.log"
9
9
  CODEBUFF_DATA_DIR="$HOME/.local/share/karnel-data/codebuff"
10
10
  CODEBUFF_MARKER=".karnel-managed"
11
11
  CODEBUFF_WRAPPER_MARKER="$CODEBUFF_DATA_DIR/.karnel-wrapper"
12
+ CODEBUFF_PREVIOUS_DATA=""
13
+ CODEBUFF_STAGED_WRAPPER=""
12
14
 
13
15
  _codebuff_detect_ubuntu_root() {
14
16
  local root
@@ -115,14 +117,44 @@ _download_codebuff_binary_impl() {
115
117
  fi
116
118
 
117
119
  chmod +x "$staging_dir/codebuff"
118
- replace_managed_directory "$staging_dir" "$CODEBUFF_DATA_DIR" "$CODEBUFF_MARKER"
120
+ if ! _activate_codebuff_payload "$staging_dir"; then
121
+ rm -rf "$staging_dir"
122
+ return 1
123
+ fi
119
124
  }
120
125
 
121
- _compile_codebuff_helper() {
122
- loading "Compiling helper" _compile_codebuff_helper_impl
126
+ _activate_codebuff_payload() {
127
+ local staging_dir="$1" backup=""
128
+ printf '%s\n' 'karnel-managed-v1' >"$staging_dir/$CODEBUFF_MARKER" || return 1
129
+ if [[ -e "$CODEBUFF_DATA_DIR" ]]; then
130
+ if [[ ! -f "$CODEBUFF_DATA_DIR/$CODEBUFF_MARKER" ]]; then
131
+ log_error "Refusing to replace unowned installation: $CODEBUFF_DATA_DIR"
132
+ return 1
133
+ fi
134
+ backup=$(mktemp -d "$(dirname "$CODEBUFF_DATA_DIR")/.codebuff-previous.XXXXXX") || return 1
135
+ rmdir "$backup" || return 1
136
+ mv "$CODEBUFF_DATA_DIR" "$backup" || return 1
137
+ fi
138
+ if ! mv "$staging_dir" "$CODEBUFF_DATA_DIR"; then
139
+ [[ -z "$backup" ]] || mv "$backup" "$CODEBUFF_DATA_DIR"
140
+ return 1
141
+ fi
142
+ if [[ -n "$backup" && -f "$backup/.karnel-wrapper" ]] &&
143
+ ! cp "$backup/.karnel-wrapper" "$CODEBUFF_WRAPPER_MARKER"; then
144
+ rm -rf "$CODEBUFF_DATA_DIR"
145
+ mv "$backup" "$CODEBUFF_DATA_DIR"
146
+ return 1
147
+ fi
148
+ CODEBUFF_PREVIOUS_DATA="$backup"
149
+ }
150
+
151
+ _restore_codebuff_payload() {
152
+ rm -rf "$CODEBUFF_DATA_DIR"
153
+ [[ -z "$CODEBUFF_PREVIOUS_DATA" ]] || mv "$CODEBUFF_PREVIOUS_DATA" "$CODEBUFF_DATA_DIR"
154
+ CODEBUFF_PREVIOUS_DATA=""
123
155
  }
124
156
 
125
- _compile_codebuff_helper_impl() {
157
+ _stage_codebuff_helper() {
126
158
  local HELPER_SRC="$KARNEL_PATH/tools/ai/codebuff/helper/codebuff_helper.c"
127
159
  if [ ! -f "$HELPER_SRC" ]; then
128
160
  log_error "Helper source not found at $HELPER_SRC"
@@ -130,22 +162,50 @@ _compile_codebuff_helper_impl() {
130
162
  fi
131
163
 
132
164
  local staged_wrapper
165
+ if [[ -e "$PREFIX/bin/codebuff" || -L "$PREFIX/bin/codebuff" ]] &&
166
+ ! installer_file_owned "$PREFIX/bin/codebuff" "$CODEBUFF_WRAPPER_MARKER"; then
167
+ log_error "Refusing to replace an existing Codebuff wrapper not owned by Karnel"
168
+ return 1
169
+ fi
133
170
  staged_wrapper=$(mktemp "$PREFIX/bin/.codebuff.XXXXXX") || return 1
134
171
  if ! cc -O2 -o "$staged_wrapper" "$HELPER_SRC" &>>"$LOG_FILE"; then
135
172
  rm -f "$staged_wrapper"
136
173
  log_error "Failed to compile codebuff helper"
137
174
  return 1
138
175
  fi
176
+ chmod +x "$staged_wrapper" || { rm -f "$staged_wrapper"; return 1; }
177
+ CODEBUFF_STAGED_WRAPPER="$staged_wrapper"
178
+ }
139
179
 
140
- chmod +x "$staged_wrapper"
141
- mv "$staged_wrapper" "$PREFIX/bin/codebuff" || return 1
142
- record_managed_file "$PREFIX/bin/codebuff" "$CODEBUFF_WRAPPER_MARKER"
180
+ _activate_codebuff_wrapper() {
181
+ activate_installer_file "$CODEBUFF_STAGED_WRAPPER" "$PREFIX/bin/codebuff" "$CODEBUFF_WRAPPER_MARKER"
182
+ }
183
+
184
+ _compile_codebuff_helper() {
185
+ loading "Compiling helper" _stage_codebuff_helper || return 1
186
+ if ! _activate_codebuff_wrapper; then
187
+ rm -f "$CODEBUFF_STAGED_WRAPPER"
188
+ return 1
189
+ fi
190
+ CODEBUFF_STAGED_WRAPPER=""
143
191
  }
144
192
 
145
193
  _install_codebuff_native() {
194
+ CODEBUFF_PREVIOUS_DATA="" CODEBUFF_STAGED_WRAPPER=""
146
195
  _codebuff_install_deps_native || return 1
147
- _download_codebuff_binary || return 1
148
- _compile_codebuff_helper || return 1
196
+ loading "Compiling helper" _stage_codebuff_helper || return 1
197
+ if ! _download_codebuff_binary; then
198
+ rm -f "$CODEBUFF_STAGED_WRAPPER"
199
+ return 1
200
+ fi
201
+ if ! _activate_codebuff_wrapper; then
202
+ rm -f "$CODEBUFF_STAGED_WRAPPER"
203
+ _restore_codebuff_payload
204
+ return 1
205
+ fi
206
+ CODEBUFF_STAGED_WRAPPER=""
207
+ [[ -z "$CODEBUFF_PREVIOUS_DATA" ]] || rm -rf "$CODEBUFF_PREVIOUS_DATA"
208
+ CODEBUFF_PREVIOUS_DATA=""
149
209
  log_success "Codebuff installed natively"
150
210
  return 0
151
211
  }
@@ -2,6 +2,7 @@
2
2
 
3
3
  import "@/utils/log"
4
4
  import "@/utils/version"
5
+ import "@/utils/npm-shebang"
5
6
 
6
7
  LOG_FILE="$KARNEL_CACHE/install_ai.log"
7
8
 
@@ -39,6 +40,7 @@ _install_codex_npm_impl() {
39
40
  log_error "Failed to install Codex CLI"
40
41
  return 1
41
42
  fi
43
+ _fix_npm_shebang "codex"
42
44
 
43
45
  return 0
44
46
  }
@@ -90,6 +92,7 @@ _update_codex_impl() {
90
92
  log_error "Failed to update Codex CLI"
91
93
  return 1
92
94
  fi
95
+ _fix_npm_shebang "codex"
93
96
  return 0
94
97
  }
95
98
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  import "@/utils/log"
4
4
  import "@/utils/version"
5
+ import "@/utils/npm-shebang"
5
6
 
6
7
  LOG_FILE="$KARNEL_CACHE/install_ai.log"
7
8
 
@@ -42,6 +43,7 @@ _install_gemini_cli_npm_impl() {
42
43
  log_error "Failed to install Gemini CLI"
43
44
  return 1
44
45
  fi
46
+ _fix_npm_shebang "gemini"
45
47
 
46
48
  return 0
47
49
  }
@@ -97,6 +99,7 @@ _update_gemini_cli_impl() {
97
99
  log_error "Failed to update Gemini CLI"
98
100
  return 1
99
101
  fi
102
+ _fix_npm_shebang "gemini"
100
103
  return 0
101
104
  }
102
105
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  import "@/utils/log"
4
4
  import "@/utils/version"
5
+ import "@/utils/npm-shebang"
5
6
 
6
7
  LOG_FILE="$KARNEL_CACHE/install_ai.log"
7
8
 
@@ -39,6 +40,7 @@ _install_kimi_code_npm_impl() {
39
40
  log_error "Failed to install Kimi Code"
40
41
  return 1
41
42
  fi
43
+ _fix_npm_shebang "kimi"
42
44
 
43
45
  return 0
44
46
  }
@@ -92,6 +94,7 @@ _update_kimi_code_impl() {
92
94
  log_error "Failed to update Kimi Code"
93
95
  return 1
94
96
  fi
97
+ _fix_npm_shebang "kimi"
95
98
  return 0
96
99
  }
97
100