karnel-termux 4.17.7 → 4.17.9

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.7-0078D4?style=for-the-badge" alt="Version">
11
+ <img src="https://img.shields.io/badge/version-4.17.9-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">
@@ -61,7 +61,7 @@ Core agent contributions by **devcorex**.
61
61
  ### Via checksummed GitHub release (recommended)
62
62
 
63
63
  ```bash
64
- version=4.17.7
64
+ version=4.17.9
65
65
  tmpdir=$(mktemp -d) && trap 'rm -rf "$tmpdir"' EXIT
66
66
  base="https://github.com/israelmarques1024-dotcom/karnel-termux/releases/download/v$version"
67
67
  curl -fsSL "$base/karnel-termux-install.sh" -o "$tmpdir/karnel-termux-install.sh"
package/docs/CHANGELOG.md CHANGED
@@ -1,5 +1,58 @@
1
+ ---
2
+ title: Documentation Changelog
3
+ permalink: /CHANGELOG/
4
+ ---
5
+
1
6
  # Documentation Changelog
2
7
 
8
+ ## 4.17.9
9
+
10
+ - Hardened installer and runtime security across the toolkit:
11
+ - `droid` binary install now refuses when no published SHA-256 digest is
12
+ available (fail-closed) instead of silently skipping verification.
13
+ - `keelcode` and `supabase` now extract archives through the hardened
14
+ `safe_extract_tar` (rejects symlink/absolute-path traversal) instead of
15
+ raw `tar`.
16
+ - Restored OpenCode installation: upstream publishes no checksum, so
17
+ `github_download_and_extract` warns instead of refusing (other tools that
18
+ publish checksums remain fail-closed).
19
+ - `cursor-cli` now validates the downloaded bundle is a real gzip archive
20
+ before extraction.
21
+ - Agent PLAN (read-only) mode write-guard hardened: now blocks `sed -i` /
22
+ `perl -i`, full-screen editors, and file descriptors like `2>file`.
23
+ - Agent compaction no longer drops the freshly built summary during the
24
+ trim safety-net.
25
+ - `voice` uninstall no longer removes `termux-api` unless Karnel installed it.
26
+ - `karnel show` no longer depends on `realpath` (portable fallback).
27
+ - `karnel update` reports the accurate installed version instead of a
28
+ misleading `latest`.
29
+ - Reinstall no longer deletes the ownership marker before confirming the
30
+ install, preventing a tool from being orphaned on install failure.
31
+
32
+ ## 4.17.8
33
+
34
+ - Documentation site reconciled with the GitHub Pages deployment: the `Publish
35
+ Docs` workflow (`.github/workflows/docs.yml`) builds Jekyll from `docs/` and
36
+ deploys to GitHub Pages, so the live documentation URL is
37
+ `https://israelmarques1024-dotcom.github.io/karnel-termux/`.
38
+ - `docs/CHANGELOG.md` is now a rendered page (added front matter + permalink)
39
+ and all internal links to it use the clean `/CHANGELOG/` path.
40
+ - Tool counts verified against the actual `karnel/tools/*` and
41
+ `karnel/tools/ai/*` directories: 45 AI tools, 3 editors, 8 languages,
42
+ 5 databases, 22 dev tools, 11 npm packages, 10 shell plugins, 4 UI
43
+ components, 4 deploy CLIs, 6 games, 2 network tools, 12 utility tools,
44
+ 30 security tools, 1 automation tool.
45
+
46
+ ## 4.17.7
47
+
48
+ - Documentation site overhauled: valid `_config.yml`, cayman-compatible custom
49
+ stylesheet that imports the full theme, a workflow that builds Jekyll before
50
+ deploying to GitHub Pages, and tool counts reconciled with the actual
51
+ `karnel/tools/*` and `karnel/tools/ai/*` directories (45 AI tools, 3 editors,
52
+ 8 languages, 5 databases, 22 dev tools, 11 npm packages, 10 shell plugins,
53
+ 4 UI components, 4 deploy CLIs, 6 games, 2 network tools, 12 utility tools,
54
+ 30 security tools, 1 automation tool).
55
+
3
56
  ## 4.16.1 - 2026-08-22
4
57
 
5
58
  - `karnel agent ask/run`: when the model endpoint is unreachable and Cactus is
package/docs/_config.yml CHANGED
@@ -2,11 +2,14 @@ title: Karnel Termux
2
2
  description: >-
3
3
  Modular Dev Environment for Termux — install languages, databases, AI agents,
4
4
  editors, and more with one command.
5
+ author: Israel Marques
6
+ lang: en-US
5
7
  theme: jekyll-theme-cayman
6
8
  markdown: kramdown
7
9
  logo: /assets/images/karnel-logo.png
8
- custom_css: /assets/css/style.css
10
+ custom_css: /karnel-termux/assets/css/style.css
9
11
  baseurl: /karnel-termux
12
+ show_downloads: false
10
13
  exclude:
11
14
  - vendor
12
15
  - Gemfile
@@ -1,3 +1,8 @@
1
+ ---
2
+ ---
3
+
4
+ @import "jekyll-theme-cayman";
5
+
1
6
  /* Karnel Termux — custom styling layered on top of jekyll-theme-cayman */
2
7
 
3
8
  :root {
@@ -9,6 +14,7 @@
9
14
  --karnel-text: #c9d1d9;
10
15
  --karnel-muted: #8b949e;
11
16
  --karnel-code-bg: #161b22;
17
+ color-scheme: dark light;
12
18
  }
13
19
 
14
20
  html {
@@ -21,14 +27,34 @@ body {
21
27
  color: #24292f;
22
28
  }
23
29
 
30
+ /* Logo (added in page content; cayman's default.html does not render site.logo) */
31
+ .karnel-logo {
32
+ display: block;
33
+ margin: 0 auto 1.25rem;
34
+ max-width: 220px;
35
+ height: auto;
36
+ border-radius: 14px;
37
+ box-shadow: 0 6px 22px rgba(0, 0, 0, 0.18);
38
+ }
39
+
24
40
  @media (prefers-color-scheme: dark) {
41
+ html {
42
+ background: var(--karnel-bg);
43
+ }
25
44
  body {
26
45
  background: var(--karnel-bg);
27
46
  color: var(--karnel-text);
28
47
  }
48
+ .main-content {
49
+ color: var(--karnel-text);
50
+ }
29
51
  .page-header {
30
52
  background-image: linear-gradient(120deg, #0078d4, #2f81f7);
31
53
  }
54
+ .page-header .project-name,
55
+ .page-header .project-tagline {
56
+ color: #ffffff;
57
+ }
32
58
  .main-content h1,
33
59
  .main-content h2,
34
60
  .main-content h3,
@@ -53,15 +79,6 @@ body {
53
79
  font-size: 1.1rem;
54
80
  }
55
81
 
56
- /* Logo */
57
- .karnel-logo {
58
- display: block;
59
- margin: 0 auto 1.25rem;
60
- max-width: 220px;
61
- height: auto;
62
- filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.35));
63
- }
64
-
65
82
  /* Landing navigation */
66
83
  .karnel-nav {
67
84
  display: flex;
@@ -149,6 +166,15 @@ body {
149
166
  font-size: 0.88em;
150
167
  }
151
168
 
169
+ /* Inline code should not be wrapped in its own block styling */
170
+ .main-content p code,
171
+ .main-content li code,
172
+ .main-content td code {
173
+ background: rgba(120, 120, 120, 0.12);
174
+ padding: 0.12em 0.4em;
175
+ border-radius: 4px;
176
+ }
177
+
152
178
  /* Blockquotes */
153
179
  .main-content blockquote {
154
180
  border-left: 4px solid var(--karnel-accent-2, #0078d4);
@@ -158,6 +184,16 @@ body {
158
184
  border-radius: 0 6px 6px 0;
159
185
  }
160
186
 
187
+ /* Admonition callouts */
188
+ .karnel-note {
189
+ border: 1px solid var(--karnel-accent-2, #0078d4);
190
+ border-left-width: 4px;
191
+ border-radius: 8px;
192
+ padding: 0.75rem 1rem;
193
+ margin: 1.25rem 0;
194
+ background: rgba(0, 120, 212, 0.06);
195
+ }
196
+
161
197
  /* Section spacing */
162
198
  .main-content h2 {
163
199
  margin-top: 2.5rem;
@@ -203,3 +239,10 @@ body {
203
239
  color: #e6edf3;
204
240
  }
205
241
  }
242
+
243
+ /* Footer credit spacing */
244
+ .site-footer {
245
+ margin-top: 2.5rem;
246
+ border-top: 1px solid #e1e4e8;
247
+ padding-top: 1rem;
248
+ }
@@ -239,5 +239,5 @@ Regression coverage lives in `tests/smoke.sh` and verifies nested module syntax,
239
239
  the 76-entry registry, pipeline-safe parsing, mode counts, hidden GitHub workflow
240
240
  detection, scoped NestJS detection, TypeScript, and Python manifests.
241
241
 
242
- See [the 2026-07-16 changelog](../CHANGELOG.md) for the complete audit and hardening
242
+ See [the 2026-07-16 changelog](../CHANGELOG/) for the complete audit and hardening
243
243
  record.
package/docs/index.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Karnel Termux Documentation
2
2
 
3
+ <img class="karnel-logo" src="{{ '/assets/images/karnel-logo.png' | relative_url }}" alt="Karnel Termux">
4
+
3
5
  > Modular Dev Environment for Termux — install languages, databases, AI agents, editors, and more with one command
4
6
 
5
7
  Navigate the docs:
@@ -9,19 +11,20 @@ Navigate the docs:
9
11
  <li><a href="./doctor/">Doctor System</a></li>
10
12
  <li><a href="./troubleshooting/">Troubleshooting</a></li>
11
13
  <li><a href="./ARCHITECTURE/">Architecture</a></li>
12
- <li><a href="./CHANGELOG.md">Changelog</a></li>
14
+ <li><a href="./CHANGELOG/">Changelog</a></li>
13
15
  <li><a href="https://israelmarques1024-dotcom.github.io/karnel-termux/">Live Site</a></li>
14
16
  </ul>
15
17
 
16
18
  ## What is Karnel?
17
19
 
18
20
  Karnel Termux transforms your Android device into a complete development workstation.
19
- With a single CLI (`karnel`), you can install and manage 45 AI tools, 8 languages,
20
- 5 databases, 22 dev tools, 11 npm packages, 10 shell plugins, 4 UI components,
21
- 4 deploy CLIs, 6 games, 2 network tools, 12 utility tools, 30 security tools,
22
- responsible OSINT, a second brain, voice commands, and reviewed plugins — all
23
- from modules: `ai`, `auto`, `db`, `deploy`, `dev`, `editor`, `games`, `lang`,
24
- `network`, `npm`, `osint`, `security`, `shell`, `ui`, `utils`, `voice`, `plugin`.
21
+ With a single CLI (`karnel`), you can install and manage 45 AI tools, 3 editors,
22
+ 8 languages, 5 databases, 22 dev tools, 11 npm packages, 10 shell plugins,
23
+ 4 UI components, 4 deploy CLIs, 6 games, 2 network tools, 12 utility tools,
24
+ 30 security tools, 1 automation tool, responsible OSINT, a second brain,
25
+ voice commands, and reviewed plugins — all from modules: `ai`, `auto`, `db`,
26
+ `deploy`, `dev`, `editor`, `games`, `lang`, `network`, `npm`, `osint`,
27
+ `security`, `shell`, `ui`, `utils`, `voice`, `plugin`.
25
28
 
26
29
  ## Sections
27
30
 
@@ -35,7 +38,7 @@ from modules: `ai`, `auto`, `db`, `deploy`, `dev`, `editor`, `games`, `lang`,
35
38
  <a href="https://github.com/israelmarques1024-dotcom/karnel-termux/blob/main/karnel/tools/utils/fconv/README.md">Utility Scripts — fconv, notes, treex, and more</a>
36
39
  <a href="./cli/#supabase--remote-project-helpers">Supabase CLI — remote-project helpers</a>
37
40
  <a href="./ARCHITECTURE/">Architecture — project structure and module system</a>
38
- <a href="./CHANGELOG.md">Changelog — version history and fixes</a>
41
+ <a href="./CHANGELOG/">Changelog — version history and fixes</a>
39
42
  </div>
40
43
 
41
44
  ## Credits
@@ -43,12 +43,21 @@ npm publish does not block the release. The workflow needs `contents: write`
43
43
  permission (already declared). If `gh release create` fails, confirm the tag
44
44
  exists and the token used by GitHub Actions has write access.
45
45
 
46
- ## Vercel deployment
47
-
48
- The live site deploys automatically through the Vercel integration on push to
49
- `main`; no GitHub Actions workflow is required. If a deploy fails, check the
50
- Vercel dashboard for build logs and confirm the framework preset, install
51
- command, and environment variables.
46
+ ## Documentation site (GitHub Pages)
47
+
48
+ The documentation site is a Jekyll (cayman theme) site built from the `docs/`
49
+ directory and published to GitHub Pages by the `Publish Docs` workflow
50
+ (`.github/workflows/docs.yml`) on every push to `main` that changes `docs/**`.
51
+ The workflow runs `actions/jekyll-build-pages` (from `docs/`, output to
52
+ `docs/_site`), uploads the result as a Pages artifact, and deploys it to the
53
+ `github-pages` environment. The live site is
54
+ `https://israelmarques1024-dotcom.github.io/karnel-termux/`.
55
+
56
+ If a build fails, open the Actions run for the failing job and read the Jekyll
57
+ output: the usual causes are invalid `_config.yml` YAML, an SCSS compile error
58
+ in `assets/css/style.scss`, or a malformed `{{ }}` Liquid tag. After a
59
+ successful deploy, the published site is visible under the repository's
60
+ **Settings → Pages**.
52
61
 
53
62
  ## Reporting a bug
54
63
 
@@ -1 +1 @@
1
- d1343c03d2f32967e36b7f78c8ef8cbb6656410e
1
+ f3ce43f67dd4d213f0af574d1673e8dbd6c910b9
@@ -86,8 +86,15 @@ show_main() {
86
86
  return
87
87
  fi
88
88
 
89
- local readme_path
90
- readme_path="$(realpath "$KARNEL_PATH/tools/$module/$tool/README.md" 2>/dev/null)"
89
+ local readme_path raw
90
+ raw="$KARNEL_PATH/tools/$module/$tool/README.md"
91
+ if command -v realpath &>/dev/null; then
92
+ readme_path="$(realpath "$raw" 2>/dev/null)"
93
+ elif command -v readlink &>/dev/null; then
94
+ readme_path="$(readlink -f "$raw" 2>/dev/null)"
95
+ else
96
+ readme_path="$raw"
97
+ fi
91
98
 
92
99
  if [[ -z "$readme_path" ]]; then
93
100
  log_error "No documentation found for $module/$tool"
@@ -347,12 +347,22 @@ _update_try_git() {
347
347
  return 1
348
348
  }
349
349
 
350
+ _npm_installed_version() {
351
+ local v pr
352
+ v=$(npm list -g karnel-termux 2>/dev/null | grep -oP 'karnel-termux@\K[0-9.]+')
353
+ if [[ -z "$v" ]]; then
354
+ pr="$(npm root -g 2>/dev/null)/karnel-termux/package.json"
355
+ [[ -f "$pr" ]] && v=$(grep -oP '"version"\s*:\s*"\K[0-9.]+' "$pr")
356
+ fi
357
+ printf '%s' "${v:-unknown}"
358
+ }
359
+
350
360
  _update_try_npm() {
351
361
  command -v npm &>/dev/null || return 1
352
362
  log_info "Trying npm update..."
353
363
  if npm update -g karnel-termux --ignore-scripts 2>/dev/null; then
354
364
  local new_ver
355
- new_ver=$(npm list -g karnel-termux 2>/dev/null | grep karnel-termux | grep -oP '@\K[0-9.]+' || echo "latest")
365
+ new_ver=$(_npm_installed_version)
356
366
  log_success "Updated to v$new_ver via npm"
357
367
  return 0
358
368
  fi
@@ -365,7 +375,7 @@ _update_try_npm_install() {
365
375
  log_info "Trying npm install..."
366
376
  if npm install -g karnel-termux@latest --ignore-scripts 2>/dev/null; then
367
377
  local new_ver
368
- new_ver=$(npm list -g karnel-termux 2>/dev/null | grep karnel-termux | grep -oP '@\K[0-9.]+' || echo "latest")
378
+ new_ver=$(_npm_installed_version)
369
379
  log_success "Updated to v$new_ver via npm install"
370
380
  return 0
371
381
  fi
@@ -88,13 +88,16 @@ _antigravity_download_binary_impl() {
88
88
  fi
89
89
 
90
90
  if [[ "$manifest_sha256" =~ ^[0-9a-f]{64}$ ]]; then
91
- verify_sha256 "$tarball" "$manifest_sha256" || return 1
91
+ verify_sha256 "$tarball" "$manifest_sha256" || { rm -f "$tarball"; return 1; }
92
92
  else
93
- log_warn "Antigravity CLI manifest does not publish a SHA-256; skipping integrity verification"
93
+ log_error "Antigravity CLI manifest does not publish a SHA-256; refusing install"
94
+ rm -f "$tarball"
95
+ return 1
94
96
  fi
95
97
 
96
- if ! tar -xzf "$tarball" -C "$AGY_DATA_DIR" &>>"$LOG_FILE"; then
98
+ if ! safe_extract_tar "$tarball" "$AGY_DATA_DIR"; then
97
99
  log_error "Failed to extract Antigravity CLI binary"
100
+ rm -f "$tarball"
98
101
  return 1
99
102
  fi
100
103
 
@@ -129,11 +129,18 @@ _download_cursor_binary_impl() {
129
129
 
130
130
  # Cursor does not publish a verifiable checksum for this asset, so integrity
131
131
  # relies on HTTPS transport only; upstream does not publish a verifiable checksum.
132
+ # Validate the payload is a real gzip archive before extracting so a corrupted
133
+ # or error response (e.g. an HTML error page) is rejected.
132
134
  if ! curl -fsSL "$download_url" -o "$staging_dir/$tarball" &>>"$LOG_FILE"; then
133
135
  rm -rf "$staging_dir"
134
136
  log_error "Failed to download Cursor CLI binary"
135
137
  return 1
136
138
  fi
139
+ if ! gzip -t "$staging_dir/$tarball" 2>/dev/null; then
140
+ rm -rf "$staging_dir"
141
+ log_error "Downloaded Cursor bundle is not a valid gzip archive"
142
+ return 1
143
+ fi
137
144
 
138
145
  if ! tar -zxf "$staging_dir/$tarball" --strip-components=1 -C "$staging_dir" &>>"$LOG_FILE"; then
139
146
  rm -rf "$staging_dir"
@@ -125,13 +125,22 @@ _download_droid_binary_impl() {
125
125
 
126
126
  local expected actual
127
127
  expected="$(curl -fsSL "$sha_url" 2>/dev/null | awk '{print $1}')"
128
- if [ -n "$expected" ] && command -v sha256sum &>/dev/null; then
128
+ if [[ ! "$expected" =~ ^[0-9a-f]{64}$ ]]; then
129
+ log_error "No published SHA-256 for Droid binary; refusing install"
130
+ rm -f "$DROID_DATA_DIR/droid"
131
+ return 1
132
+ fi
133
+ if command -v sha256sum &>/dev/null; then
129
134
  actual="$(sha256sum "$DROID_DATA_DIR/droid" | awk '{print $1}')"
130
135
  if [ "$actual" != "$expected" ]; then
131
136
  log_error "Checksum verification failed"
132
137
  rm -f "$DROID_DATA_DIR/droid"
133
138
  return 1
134
139
  fi
140
+ else
141
+ log_error "sha256sum unavailable; refusing install"
142
+ rm -f "$DROID_DATA_DIR/droid"
143
+ return 1
135
144
  fi
136
145
 
137
146
  chmod +x "$DROID_DATA_DIR/droid"
@@ -2,6 +2,7 @@
2
2
 
3
3
  import "@/utils/log"
4
4
  import "@/utils/version"
5
+ import "@/utils/install"
5
6
 
6
7
  if ! declare -f safe_extract_tar >/dev/null 2>&1; then
7
8
  safe_extract_tar() {
@@ -4,7 +4,7 @@ import "@/utils/log"
4
4
  import "@/utils/install"
5
5
  import "@/utils/version"
6
6
 
7
- LOG_FILE="$KARNEL_CACHE/install_deploy.log"
7
+ LOG_FILE="${KARNEL_CACHE:-${XDG_CACHE_HOME:-$HOME/.cache}/karnel}/install_deploy.log"
8
8
  RAILWAY_DATA_DIR="${KARNEL_DATA:-${XDG_DATA_HOME:-$HOME/.local/share}/karnel-data}/deploy/railway"
9
9
  _RAILWAY_MARKER="$PREFIX/share/karnel-installers/railway"
10
10
  _RAILWAY_DATA_MARKER="$RAILWAY_DATA_DIR/.karnel-managed"
@@ -113,14 +113,17 @@ _install_railway_manual_impl() {
113
113
 
114
114
  local expected
115
115
  expected=$(github_release_asset_sha256 railwayapp/cli "$latest_version" "railway-${version}-aarch64-linux.tar.gz") || expected=""
116
- if [[ "$expected" =~ ^[0-9a-f]{64}$ ]]; then
117
- verify_sha256 "$RAILWAY_DATA_DIR/railway.tar.gz" "$expected" || {
118
- rm -f "$RAILWAY_DATA_DIR/railway.tar.gz"
119
- return 1
120
- }
116
+ if [[ ! "$expected" =~ ^[0-9a-f]{64}$ ]]; then
117
+ log_error "No published SHA-256 for Railway CLI; refusing install"
118
+ rm -f "$RAILWAY_DATA_DIR/railway.tar.gz"
119
+ return 1
121
120
  fi
121
+ verify_sha256 "$RAILWAY_DATA_DIR/railway.tar.gz" "$expected" || {
122
+ rm -f "$RAILWAY_DATA_DIR/railway.tar.gz"
123
+ return 1
124
+ }
122
125
 
123
- tar -zxf "$RAILWAY_DATA_DIR/railway.tar.gz" -C "$RAILWAY_DATA_DIR" 2>>"$LOG_FILE" || {
126
+ safe_extract_tar "$RAILWAY_DATA_DIR/railway.tar.gz" "$RAILWAY_DATA_DIR" || {
124
127
  rm -f "$RAILWAY_DATA_DIR/railway.tar.gz"
125
128
  return 1
126
129
  }
@@ -2,6 +2,14 @@
2
2
 
3
3
  import "@/utils/log"
4
4
  import "@/utils/version"
5
+ import "@/utils/install"
6
+
7
+ if ! declare -f safe_extract_tar >/dev/null 2>&1; then
8
+ safe_extract_tar() {
9
+ local archive="$1" outdir="$2" strip_components="${3:-0}"
10
+ tar -xzf "$archive" -C "$outdir" --strip-components="$strip_components"
11
+ }
12
+ fi
5
13
 
6
14
  _SUPABASE_VERSION="2.20.8"
7
15
  _SUPABASE_RELEASE_URL="https://github.com/supabase/cli/releases/download/v${_SUPABASE_VERSION}"
@@ -43,7 +51,9 @@ install_supabase() (
43
51
  local url="${_SUPABASE_RELEASE_URL}/${filename}"
44
52
  local checksum_url="${_SUPABASE_RELEASE_URL}/supabase_${_SUPABASE_VERSION}_checksums.txt"
45
53
  local tmp_dir staged_bin marker_staging="" old_bin="" old_marker=""
46
- tmp_dir=$(mktemp -d "${TMPDIR:-/tmp}/supabase.XXXXXX") || return 1
54
+ local _kdir="${KARNEL_CACHE:-${XDG_CACHE_HOME:-$HOME/.cache}/karnel}"
55
+ mkdir -p "$_kdir" 2>/dev/null || _kdir="${TMPDIR:-/tmp}"
56
+ tmp_dir=$(mktemp -d "$_kdir/supabase.XXXXXX") || return 1
47
57
  staged_bin=""
48
58
  trap 'rm -rf "$tmp_dir"; [[ -z "$staged_bin" ]] || rm -f "$staged_bin"; [[ -z "$marker_staging" ]] || rm -f "$marker_staging"' EXIT
49
59
 
@@ -79,8 +89,11 @@ install_supabase() (
79
89
  fi
80
90
  log_success "Checksum verified"
81
91
 
82
- # Extract
83
- tar -xzf "$tmp_dir/$filename" -C "$tmp_dir" supabase 2>/dev/null
92
+ # Extract (hardened: rejects symlink/absolute-path traversal)
93
+ safe_extract_tar "$tmp_dir/$filename" "$tmp_dir" || {
94
+ log_error "Extraction failed"
95
+ return 1
96
+ }
84
97
  if [[ ! -f "$tmp_dir/supabase" ]]; then
85
98
  log_error "Extraction failed"
86
99
  return 1
@@ -143,16 +143,24 @@ _download_bun_binary_native_impl() {
143
143
  }
144
144
  if declare -F github_release_asset_sha256 >/dev/null; then
145
145
  expected=$(github_release_asset_sha256 "$BUN_REPO" "bun-v$version" "$zip_name") || expected=""
146
- if [[ "$expected" =~ ^[0-9a-f]{64}$ ]]; then
147
- verify_sha256 "$staging_dir/$zip_name" "$expected" || { rm -rf "$staging_dir"; return 1; }
148
- else
149
- log_warn "No published SHA-256 for Bun native; skipping integrity verification"
146
+ if [[ ! "$expected" =~ ^[0-9a-f]{64}$ ]]; then
147
+ rm -rf "$staging_dir"
148
+ log_error "No published SHA-256 for Bun native; refusing install"
149
+ return 1
150
+ fi
151
+ verify_sha256 "$staging_dir/$zip_name" "$expected" || { rm -rf "$staging_dir"; return 1; }
152
+ if ! safe_extract_zip "$staging_dir/$zip_name" "$staging_dir"; then
153
+ rm -rf "$staging_dir"
154
+ log_error "Failed to extract Bun binary"
155
+ return 1
156
+ fi
157
+ else
158
+ command -v log_warn >/dev/null 2>&1 && log_warn "Integrity helpers unavailable; skipping Bun integrity verification"
159
+ if ! unzip -o "$staging_dir/$zip_name" -d "$staging_dir" &>>"$LOG_FILE"; then
160
+ rm -rf "$staging_dir"
161
+ log_error "Failed to extract Bun binary"
162
+ return 1
150
163
  fi
151
- fi
152
- if ! unzip -o "$staging_dir/$zip_name" -d "$staging_dir" &>>"$LOG_FILE"; then
153
- rm -rf "$staging_dir"
154
- log_error "Failed to extract Bun binary"
155
- return 1
156
164
  fi
157
165
  rm -f "$staging_dir/$zip_name"
158
166
  extracted="$staging_dir/bun-linux-aarch64/bun"
@@ -280,13 +288,14 @@ _install_bun_proot_impl() {
280
288
  local expected
281
289
  expected=$(github_release_asset_sha256 "$BUN_REPO" "bun-v$version" "bun-linux-aarch64.zip") || expected=""
282
290
  if [[ ! "$expected" =~ ^[0-9a-f]{64}$ ]]; then
283
- log_warn "No published SHA-256 for Bun; skipping integrity verification"
291
+ log_error "No published SHA-256 for Bun; refusing install"
292
+ return 1
284
293
  fi
285
294
  _bun_proot_ubuntu /bin/bash -c '
286
295
  export HOME=/root TMPDIR=/tmp
287
296
  cd /tmp &&
288
297
  curl -fsSL "$1" -o bun.zip &&
289
- { [ -z "$2" ] || [ "$(sha256sum bun.zip | awk "{print \$1}")" = "$2" ]; } &&
298
+ [ "$(sha256sum bun.zip | awk "{print \$1}")" = "$2" ] &&
290
299
  unzip -o bun.zip >/dev/null 2>&1 &&
291
300
  mkdir -p /usr/local/bin &&
292
301
  mv bun-linux-aarch64/bun /usr/local/bin/bun &&
@@ -392,13 +401,14 @@ _update_bun_proot() {
392
401
  local expected
393
402
  expected=$(github_release_asset_sha256 "$BUN_REPO" "bun-v$version" "bun-linux-aarch64.zip") || expected=""
394
403
  if [[ ! "$expected" =~ ^[0-9a-f]{64}$ ]]; then
395
- log_warn "No published SHA-256 for Bun; skipping integrity verification"
404
+ log_error "No published SHA-256 for Bun; refusing update"
405
+ return 1
396
406
  fi
397
407
  _bun_proot_ubuntu /bin/bash -c '
398
408
  export HOME=/root TMPDIR=/tmp
399
409
  cd /tmp &&
400
410
  curl -fsSL "$1" -o bun.zip &&
401
- { [ -z "$2" ] || [ "$(sha256sum bun.zip | awk "{print \$1}")" = "$2" ]; } &&
411
+ [ "$(sha256sum bun.zip | awk "{print \$1}")" = "$2" ] &&
402
412
  unzip -o bun.zip >/dev/null 2>&1 &&
403
413
  mkdir -p /usr/local/bin &&
404
414
  mv bun-linux-aarch64/bun /usr/local/bin/bun &&
@@ -2,6 +2,7 @@
2
2
 
3
3
  import "@/utils/log"
4
4
  import "@/utils/version"
5
+ import "@/utils/install"
5
6
 
6
7
  LOG_FILE="$KARNEL_CACHE/install_zork.log"
7
8
  ZORK_DATA_DIR="${KARNEL_DATA:-${XDG_DATA_HOME:-$HOME/.local/share}/karnel-data}/zork"
@@ -62,7 +63,10 @@ _download_zork_data_impl() {
62
63
  return 1
63
64
  fi
64
65
 
65
- if ! unzip -o "$zip_file" -d "$staging_dir" 2>>"$LOG_FILE"; then
66
+ if declare -F safe_extract_zip >/dev/null; then
67
+ safe_extract_zip "$zip_file" "$staging_dir" 2>>"$LOG_FILE" || {
68
+ rm -rf "$staging_dir"; log_error "Failed to extract Zork ${num}"; return 1; }
69
+ elif ! unzip -o "$zip_file" -d "$staging_dir" 2>>"$LOG_FILE"; then
66
70
  rm -rf "$staging_dir"
67
71
  log_error "Failed to extract Zork ${num}"
68
72
  return 1
@@ -3,13 +3,20 @@
3
3
  import "@/utils/log"
4
4
 
5
5
  _uninstall_voice_tool() {
6
+ local karnel_data="${KARNEL_DATA:-${XDG_DATA_HOME:-$HOME/.local/share}/karnel-data}"
7
+ local marker="$karnel_data/voice/.termux-api-managed"
6
8
  if command -v termux-api &>/dev/null; then
7
- log_info "Removing Termux:API..."
8
- if ! pkg uninstall -y termux-api &>>"$LOG_FILE"; then
9
- log_error "Failed to remove Termux:API (see $LOG_FILE)"
10
- return 1
9
+ if [[ -f "$marker" ]]; then
10
+ log_info "Removing Termux:API (installed by Karnel)..."
11
+ if ! pkg uninstall -y termux-api &>>"$LOG_FILE"; then
12
+ log_error "Failed to remove Termux:API (see $LOG_FILE)"
13
+ return 1
14
+ fi
15
+ rm -f "$marker"
16
+ log_success "Termux:API removed"
17
+ else
18
+ log_info "Termux:API is present but was not installed by Karnel; leaving it in place"
11
19
  fi
12
- log_success "Termux:API removed"
13
20
  else
14
21
  log_info "Termux:API is not installed"
15
22
  fi
@@ -481,14 +481,22 @@ _agent_exec_to_file() {
481
481
  # ------------------------------------------------------------
482
482
  _agent_cmd_is_write() {
483
483
  local cmd="$1"
484
- # file redirections (2>&1 / >= are not redirections to a file)
485
- if printf '%s\n' "$cmd" | grep -qE '(^|[;&|[:space:]])(>|>>)[^=&]'; then
484
+ # file redirections: >file >>file 2>file 1>>file &>file (excludes fd-to-fd like >&2 and comparisons >=)
485
+ if printf '%s\n' "$cmd" | grep -qE '(^|[;&|[:space:]])(>|>>|&>|[0-9]>|[0-9]>>)([^&=]|$)'; then
486
486
  return 0
487
487
  fi
488
488
  # command substitution / backticks execute arbitrary code → write
489
489
  if printf '%s\n' "$cmd" | grep -qE '\$\(|`'; then
490
490
  return 0
491
491
  fi
492
+ # in-place editors: sed -i / perl -i modify files in place
493
+ if printf '%s\n' "$cmd" | grep -qiE '(^|[;&|[:space:]])(sed|perl)[[:space:]][^|;&]*-[A-Za-z]*i[A-Za-z]*([[:space:]]|$)'; then
494
+ return 0
495
+ fi
496
+ # full-screen / line editors that can modify files
497
+ if printf '%s\n' "$cmd" | grep -qiE '(^|[;&|[:space:]])(vim|nvim|emacs|ed|ex|vi|nano)[[:space:]]'; then
498
+ return 0
499
+ fi
492
500
  # blocklist of commands that write to disk / change state / spawn a shell
493
501
  printf '%s\n' "$cmd" | grep -qiE '(^|[;&|[:space:]])(rm|rmdir|unlink|mv|cp|mkdir|touch|ln|chmod|chown|chgrp|install|truncate|tee|dd|mkfs|mount|umount|tar|gzip|gunzip|bzip2|xz|zstd|curl|wget|python3|python|node|npm|bun|yarn|pnpm|cargo|go |rustc|git (add|commit|push|pull|merge|rebase|reset|checkout|switch|restore|clean|stash|tag|clone|init|rm|mv)|pkg (install|uninstall|upgrade|reinstall|update)|apt( |-)|apt-get|dpkg|yum|dnf|pacman|brew|pip|pip3|uv|conda|mysql|psql|sqlite3|redis-cli|mongosh|kill|pkill|killall|service|systemctl|halt|reboot|shutdown|sudo|doas|su|pkexec|chroot|bash|sh|dash|zsh|ksh|eval|source|exec|screen|tmux|ssh|scp|sftp|rsync|telnet|nc|ncat|ftp|lftp|rsh|sshpass|socat|crontab|at |iptables|nft|ufw|docker|podman|kubectl|helm)[[:space:]]' && return 0
494
502
  return 1
@@ -889,7 +897,9 @@ agent_exec_loading() {
889
897
  local frames=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
890
898
  local delay=0.08
891
899
  local tmpfile
892
- tmpfile="$(mktemp)"
900
+ local _ktmpdir="${KARNEL_CACHE:-${XDG_CACHE_HOME:-$HOME/.cache}/karnel}"
901
+ mkdir -p "$_ktmpdir" 2>/dev/null || _ktmpdir="${TMPDIR:-/tmp}"
902
+ tmpfile="$(mktemp "$_ktmpdir/karnel.XXXXXX" 2>/dev/null)" || tmpfile="$(mktemp 2>/dev/null)"
893
903
 
894
904
  local stty_saved=""
895
905
  if [[ -t 0 ]]; then
@@ -1357,12 +1367,17 @@ agent_context_footer() {
1357
1367
  agent_history_trim_tokens() {
1358
1368
  local history="$1" budget="${2:-0}"
1359
1369
  (( budget <= 0 )) && { echo "$history"; return; }
1360
- local toks n
1370
+ local toks n drop_idx is_summary
1361
1371
  while :; do
1362
1372
  toks=$(agent_context_tokens "$history")
1363
1373
  (( toks <= budget )) && break
1364
1374
  n=$(printf '%s' "$history" | jq 'length')
1365
1375
  (( n <= 4 )) && break
1376
+ drop_idx=$(printf '%s' "$history" | jq -c 'if .[0].role == "system" then 1 else 0 end')
1377
+ is_summary=$(printf '%s' "$history" | jq -r --argjson i "$drop_idx" '.[$i].content // "" | contains("<compacted_summary>")')
1378
+ if [[ "$is_summary" == "true" ]]; then
1379
+ break
1380
+ fi
1366
1381
  history=$(printf '%s' "$history" | jq -c 'if .[0].role == "system" then .[0:1] + .[2:] else .[1:] end')
1367
1382
  done
1368
1383
  echo "$history"
@@ -244,7 +244,20 @@ agent_chat_completion() {
244
244
  # characters. Real UTF-8 text passes through untouched.
245
245
  # ------------------------------------------------------------
246
246
  _agent_unescape_unicode() {
247
- perl -CS -pe 's/\\u([0-9a-fA-F]{4})/chr(hex($1))/ge' 2>/dev/null
247
+ if command -v perl >/dev/null 2>&1; then
248
+ perl -CS -pe 's/\\u([0-9a-fA-F]{4})/chr(hex($1))/ge'
249
+ elif command -v python3 >/dev/null 2>&1; then
250
+ python3 -c 'import sys,re;sys.stdout.write(re.sub(r"\\u([0-9a-fA-F]{4})",lambda m:chr(int(m.group(1),16)),sys.stdin.read()))'
251
+ else
252
+ awk '{
253
+ while (match($0, /\\u[0-9a-fA-F]{4}/)) {
254
+ h = substr($0, RSTART + 2, 4)
255
+ c = sprintf("%c", strtonum("0x" h))
256
+ $0 = substr($0, 1, RSTART - 1) c substr($0, RSTART + 6)
257
+ }
258
+ print
259
+ }'
260
+ fi
248
261
  }
249
262
 
250
263
  # ------------------------------------------------------------
@@ -211,9 +211,11 @@ github_release_asset_sha256() {
211
211
  return 0
212
212
  fi
213
213
  # Fallback: a checksums.txt / SHA256SUMS asset listing every released file.
214
+ # Match the filename field exactly (it is the LAST field) so a substring or
215
+ # regex-metacharacter in $asset cannot select the wrong line's digest.
214
216
  digest=$(curl -fsSL --connect-timeout 10 --max-time 30 \
215
217
  "https://github.com/$repo/releases/download/$version/checksums.txt" 2>/dev/null \
216
- | awk -v a="$asset" '$0 ~ a {print $1; exit}' | tr -d '\r')
218
+ | awk -v a="$asset" '{ f=$NF; if (f == a) { sub(/^\*/, "", $1); print $1; exit } }' | tr -d '\r')
217
219
  if [[ "$digest" =~ ^[0-9a-f]{64}$ ]]; then
218
220
  echo "$digest"
219
221
  return 0
@@ -248,20 +250,26 @@ extract_tarball() {
248
250
 
249
251
  replace_managed_directory() {
250
252
  local staging="$1" destination="$2" marker="$3"
251
- local backup="${destination}.previous.$$"
253
+ local backup_dir backup_target=""
252
254
  if [[ -e "$destination" && ! -f "$destination/$marker" ]]; then
253
255
  log_error "Refusing to replace unowned installation: $destination"
254
256
  return 1
255
257
  fi
256
258
  printf '%s\n' 'karnel-managed-v1' >"$staging/$marker" || return 1
257
- if [[ -e "$destination" ]] && ! mv "$destination" "$backup"; then
258
- return 1
259
+ backup_dir="$(mktemp -d "$(dirname "$destination")/.karnel-managed.XXXXXX")" || return 1
260
+ if [[ -e "$destination" ]]; then
261
+ backup_target="$backup_dir/$(basename "$destination")"
262
+ if ! mv "$destination" "$backup_target"; then
263
+ rm -rf "$backup_dir"
264
+ return 1
265
+ fi
259
266
  fi
260
267
  if ! mv "$staging" "$destination"; then
261
- [[ ! -e "$backup" ]] || mv "$backup" "$destination"
268
+ [[ -n "$backup_target" ]] && mv "$backup_target" "$destination" 2>/dev/null
269
+ rm -rf "$backup_dir"
262
270
  return 1
263
271
  fi
264
- rm -rf "$backup"
272
+ rm -rf "$backup_dir"
265
273
  }
266
274
 
267
275
  record_managed_file() {
@@ -429,7 +429,9 @@ loading() {
429
429
  local frames=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
430
430
  local delay=0.08
431
431
  local tmpfile
432
- tmpfile="$(mktemp "${TMPDIR:-/tmp}/karnel.XXXXXX")"
432
+ local _ktmpdir="${KARNEL_CACHE:-${XDG_CACHE_HOME:-$HOME/.cache}/karnel}"
433
+ mkdir -p "$_ktmpdir" 2>/dev/null || _ktmpdir="${TMPDIR:-/tmp}"
434
+ tmpfile="$(mktemp "$_ktmpdir/karnel.XXXXXX" 2>/dev/null)" || tmpfile="$(mktemp 2>/dev/null)"
433
435
  local cmd_pid=""
434
436
 
435
437
  trap 'kill "$cmd_pid" 2>/dev/null; rm -f "$tmpfile"; return 1' INT TERM
@@ -60,9 +60,6 @@ _run_tool_lifecycle_action() {
60
60
  "$uninstall_handler"
61
61
  rc=$?
62
62
  (( rc == 0 )) || return "$rc"
63
- if (( protected )); then
64
- rm -f "$marker" || return 1
65
- fi
66
63
  "$install_handler"
67
64
  rc=$?
68
65
  if (( rc == 0 && protected )); then
@@ -51,7 +51,9 @@ _spin_capture() {
51
51
  shift
52
52
  local frames=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
53
53
  local tmp
54
- tmp=$(mktemp)
54
+ local _ktmpdir="${KARNEL_CACHE:-${XDG_CACHE_HOME:-$HOME/.cache}/karnel}"
55
+ mkdir -p "$_ktmpdir" 2>/dev/null || _ktmpdir="${TMPDIR:-/tmp}"
56
+ tmp=$(mktemp "$_ktmpdir/karnel.XXXXXX" 2>/dev/null) || tmp=$(mktemp 2>/dev/null)
55
57
  local spinner_fd
56
58
  if [ -c /dev/tty ] 2>/dev/null; then
57
59
  spinner_fd=/dev/tty
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karnel-termux",
3
- "version": "4.17.7",
3
+ "version": "4.17.9",
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"