karnel-termux 4.17.21 → 4.17.22

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/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"
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.
@@ -1 +1 @@
1
- 6b13b468f425a13040e4dac77eef1b449a553ea8
1
+ 4c9e5da4c38f3c93a5f36809265feb152fd59b27
@@ -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
 
@@ -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
@@ -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
@@ -294,7 +294,7 @@ _install_bun_proot_impl() {
294
294
  cd /tmp &&
295
295
  curl -fsSL "$1" -o bun.zip &&
296
296
  [ "$(sha256sum bun.zip | awk "{print \$1}")" = "$2" ] &&
297
- unzip -l bun.zip | awk "NR>3{print \$NF}" | grep -Eq "(\\.\\./|^\\.\\./|^/)" && { echo "Refusing Bun archive with unsafe paths" >&2; exit 1; } &&
297
+ if unzip -l bun.zip | awk "NR>3{print \$NF}" | grep -Eq "(\\.\\./|^\\.\\./|^/)"; then echo "Refusing Bun archive with unsafe paths" >&2; exit 1; fi &&
298
298
  unzip -o bun.zip >/dev/null 2>&1 &&
299
299
  mkdir -p /usr/local/bin &&
300
300
  mv bun-linux-aarch64/bun /usr/local/bin/bun &&
@@ -408,7 +408,7 @@ _update_bun_proot() {
408
408
  cd /tmp &&
409
409
  curl -fsSL "$1" -o bun.zip &&
410
410
  [ "$(sha256sum bun.zip | awk "{print \$1}")" = "$2" ] &&
411
- unzip -l bun.zip | awk "NR>3{print \$NF}" | grep -Eq "(\\.\\./|^\\.\\./|^/)" && { echo "Refusing Bun archive with unsafe paths" >&2; exit 1; } &&
411
+ if unzip -l bun.zip | awk "NR>3{print \$NF}" | grep -Eq "(\\.\\./|^\\.\\./|^/)"; then echo "Refusing Bun archive with unsafe paths" >&2; exit 1; fi &&
412
412
  unzip -o bun.zip >/dev/null 2>&1 &&
413
413
  mkdir -p /usr/local/bin &&
414
414
  mv bun-linux-aarch64/bun /usr/local/bin/bun &&
@@ -551,11 +551,19 @@ _agent_cmd_is_readonly() {
551
551
  # commands. Anything the write-gate flags, or any command not in this
552
552
  # allowlist (ruby/php/lua/tclsh/expect/sbcl/...), is blocked — so PLAN
553
553
  # mode can never modify the machine or run arbitrary code.
554
+ # Shell syntax makes a string-level allowlist unsafe: quoted command names,
555
+ # separators, substitutions and redirects can hide writes from the first token.
556
+ case "$1" in
557
+ *';'* | *'&'* | *'|'* | *"'"* | *'"'* | *'`'* | *'\'* | *'<'* | *'>'* | *'$'*) return 1 ;;
558
+ esac
554
559
  _agent_cmd_is_write "$1" && return 1
555
560
  local first
556
561
  first=$(printf '%s\n' "$1" | sed 's/^[[:space:]]*//' | cut -d' ' -f1)
557
562
  case "$first" in
558
563
  ls | cat | head | tail | grep | rg | find | stat | wc | file | diff | sort | uniq | cut | tr | tree | du | df | which | type | echo | printf | jq | readlink | realpath | date | pwd | whoami | uname | sed)
564
+ if [[ " $1 " == *" -delete "* || " $1 " == *" -exec "* || " $1 " == *" -execdir "* || " $1 " == *" -ok "* || " $1 " == *" -okdir "* || " $1 " == *" -fprint "* || " $1 " == *" -fls "* || " $1 " == *" -i "* ]]; then
565
+ return 1
566
+ fi
559
567
  return 0
560
568
  ;;
561
569
  git)
@@ -50,10 +50,10 @@ agent_config_load() {
50
50
  # Reject tampered config (defense-in-depth): numeric/boolean fields
51
51
  # must match the expected shape or they are reset to safe defaults.
52
52
  [[ "$AGENT_TEMPERATURE" =~ ^[0-9]+(\.[0-9]+)?$ ]] || AGENT_TEMPERATURE=0.3
53
- [[ "$AGENT_MAX_TOKENS" =~ ^[0-9]+$ ]] || AGENT_MAX_TOKENS=2048
54
- [[ "$AGENT_MAX_ITERATIONS" =~ ^[0-9]+$ ]] || AGENT_MAX_ITERATIONS=12
53
+ [[ "$AGENT_MAX_TOKENS" =~ ^[1-9][0-9]*$ ]] || AGENT_MAX_TOKENS=2048
54
+ [[ "$AGENT_MAX_ITERATIONS" =~ ^[1-9][0-9]*$ ]] || AGENT_MAX_ITERATIONS=12
55
55
  [[ "$AGENT_CONFIRM_COMMANDS" =~ ^[01]$ ]] || AGENT_CONFIRM_COMMANDS=1
56
- [[ "$AGENT_CONTEXT_WINDOW" =~ ^[0-9]+$ ]] || AGENT_CONTEXT_WINDOW=8192
56
+ [[ "$AGENT_CONTEXT_WINDOW" =~ ^[1-9][0-9]*$ ]] || AGENT_CONTEXT_WINDOW=8192
57
57
  }
58
58
 
59
59
  # ------------------------------------------------------------
@@ -89,16 +89,16 @@ agent_config_set() {
89
89
  [[ "$value" =~ ^[0-9]+(\.[0-9]+)?$ ]] || { log_error "temperature must be a number (e.g. 0.3)"; return 1; }
90
90
  AGENT_TEMPERATURE="$value" ;;
91
91
  max_tokens | maxtokens)
92
- [[ "$value" =~ ^[0-9]+$ ]] || { log_error "max_tokens must be a positive integer"; return 1; }
92
+ [[ "$value" =~ ^[1-9][0-9]*$ ]] || { log_error "max_tokens must be a positive integer"; return 1; }
93
93
  AGENT_MAX_TOKENS="$value" ;;
94
94
  iterations | max_iterations)
95
- [[ "$value" =~ ^[0-9]+$ ]] || { log_error "iterations must be a positive integer"; return 1; }
95
+ [[ "$value" =~ ^[1-9][0-9]*$ ]] || { log_error "iterations must be a positive integer"; return 1; }
96
96
  AGENT_MAX_ITERATIONS="$value" ;;
97
97
  confirm_commands | confirm)
98
98
  [[ "$value" =~ ^[01]$ ]] || { log_error "confirm_commands must be 0 or 1"; return 1; }
99
99
  AGENT_CONFIRM_COMMANDS="$value" ;;
100
100
  context_window | context)
101
- [[ "$value" =~ ^[0-9]+$ ]] || { log_error "context_window must be a positive integer"; return 1; }
101
+ [[ "$value" =~ ^[1-9][0-9]*$ ]] || { log_error "context_window must be a positive integer"; return 1; }
102
102
  AGENT_CONTEXT_WINDOW="$value" ;;
103
103
  server_command | server) AGENT_SERVER_CMD="$value" ;;
104
104
  workspace)
@@ -363,4 +363,4 @@ agent_history_ensure_system() {
363
363
  agent_history_drop_system() {
364
364
  local history="$1"
365
365
  echo "$history" | jq -c '[.[] | select(.role != "system")]'
366
- }
366
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karnel-termux",
3
- "version": "4.17.21",
3
+ "version": "4.17.22",
4
4
  "description": "Modular Dev Environment for Termux — install languages, databases, AI agents, editors, and more with one command",
5
5
  "bin": {
6
6
  "karnel": "karnel/bin/karnel"