blamcode 0.4.0__py3-none-any.whl

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 (58) hide show
  1. blamcode/__init__.py +9 -0
  2. blamcode/cli.py +122 -0
  3. blamcode/layer/README.md +43 -0
  4. blamcode/layer/config/agent/build.md +118 -0
  5. blamcode/layer/config/agent/debugger.md +32 -0
  6. blamcode/layer/config/agent/designer.md +31 -0
  7. blamcode/layer/config/agent/vision.md +26 -0
  8. blamcode/layer/config/agent/writer.md +28 -0
  9. blamcode/layer/config/command/ask.md +41 -0
  10. blamcode/layer/config/command/blam.md +24 -0
  11. blamcode/layer/config/command/explore.md +14 -0
  12. blamcode/layer/config/command/fix.md +14 -0
  13. blamcode/layer/config/command/open.md +18 -0
  14. blamcode/layer/config/command/review.md +14 -0
  15. blamcode/layer/config/opencode.json +53 -0
  16. blamcode/layer/config/themes/bangladeshi.json +67 -0
  17. blamcode/layer/config/themes/blamcode.json +217 -0
  18. blamcode/layer/config/tui.json +4 -0
  19. blamcode/layer/install.sh +752 -0
  20. blamcode/layer/scripts/__pycache__/patch-brand.cpython-312.pyc +0 -0
  21. blamcode/layer/scripts/blamcode +518 -0
  22. blamcode/layer/scripts/blamcode-browser +315 -0
  23. blamcode/layer/scripts/blamcode-menu +140 -0
  24. blamcode/layer/scripts/blamcode-uninstall +71 -0
  25. blamcode/layer/scripts/blamcode-vision +542 -0
  26. blamcode/layer/scripts/oc-settings.sh +138 -0
  27. blamcode/layer/scripts/patch-brand.py +267 -0
  28. blamcode/layer/skills/android-app/SKILL.md +53 -0
  29. blamcode/layer/skills/api-integration/SKILL.md +49 -0
  30. blamcode/layer/skills/bash-cli-expert/SKILL.md +48 -0
  31. blamcode/layer/skills/bot-development/SKILL.md +53 -0
  32. blamcode/layer/skills/clean-code-performance/SKILL.md +45 -0
  33. blamcode/layer/skills/database/SKILL.md +56 -0
  34. blamcode/layer/skills/debugging-fixes/SKILL.md +45 -0
  35. blamcode/layer/skills/deploy-hosting/SKILL.md +38 -0
  36. blamcode/layer/skills/docker/SKILL.md +74 -0
  37. blamcode/layer/skills/firebase-supabase/SKILL.md +61 -0
  38. blamcode/layer/skills/git-workflow/SKILL.md +63 -0
  39. blamcode/layer/skills/lets-scroll/SKILL.md +877 -0
  40. blamcode/layer/skills/lets-scroll/references/index-template.html +73 -0
  41. blamcode/layer/skills/lets-scroll/references/knockout.py +89 -0
  42. blamcode/layer/skills/lets-scroll/references/pipeline.md +312 -0
  43. blamcode/layer/skills/lets-scroll/references/prompts.md +194 -0
  44. blamcode/layer/skills/lets-scroll/references/scrub-engine.js +448 -0
  45. blamcode/layer/skills/project-structure/SKILL.md +74 -0
  46. blamcode/layer/skills/python-automation/SKILL.md +52 -0
  47. blamcode/layer/skills/react-next-best-practices/SKILL.md +54 -0
  48. blamcode/layer/skills/security-review/SKILL.md +48 -0
  49. blamcode/layer/skills/seo-basics/SKILL.md +44 -0
  50. blamcode/layer/skills/testing/SKILL.md +58 -0
  51. blamcode/layer/skills/ui-ux-responsive/SKILL.md +53 -0
  52. blamcode/layer/skills/website-builder/SKILL.md +47 -0
  53. blamcode-0.4.0.dist-info/METADATA +62 -0
  54. blamcode-0.4.0.dist-info/RECORD +58 -0
  55. blamcode-0.4.0.dist-info/WHEEL +5 -0
  56. blamcode-0.4.0.dist-info/entry_points.txt +2 -0
  57. blamcode-0.4.0.dist-info/licenses/LICENSE +21 -0
  58. blamcode-0.4.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,518 @@
1
+ #!/data/data/com.termux/files/usr/bin/sh
2
+ # blamcode — wrapper that disables Android bionic TBI heap pointer tagging
3
+ #
4
+ # The real blamcode binary is blamcode.bin; this script LD_PRELOAD's libtagfix.so
5
+ # (a constructor that calls mallopt to turn off heap tagging) before exec'ing it.
6
+ # Without this, Bun/JSC's NaN-boxing clears the 0xB4 top-byte tag on heap
7
+ # pointers, causing bionic to SIGABRT on free(): "Pointer tag ... was truncated".
8
+ #
9
+ # Permission system: none — everything is allowed by config (beginner CLI
10
+ # design, no prompts). The wrapper does not enforce any permissions.
11
+ #
12
+ # Named sessions: blamcode session <name> — opens <base>/blamcode/<name>/ and
13
+ # resumes the previous chat history with --continue. blamcode session = list.
14
+ #
15
+ # Path resolution order (supports both standalone zip and installed package):
16
+ # zip: wrapper, blamcode.bin, libtagfix.so all live in the same dir
17
+ # installed: bin/blamcode, libexec/opencode/blamcode.bin, lib/libtagfix.so
18
+ # glibc Linux (Ubuntu proot etc.): ~/.local/bin/blamcode, ~/.local/libexec/blamcode/
19
+
20
+ set -e
21
+
22
+ dir="$(cd "$(dirname "$0")" && pwd)"
23
+ export ANDROID_ROOT="${ANDROID_ROOT:-/system}"
24
+ export TERMUX_VERSION="${TERMUX_VERSION:-blamcode-termux}"
25
+ # always have a sane HOME — some launch paths (widgets, menu, shared
26
+ # sessions) start us with an empty environment, and every ~/.config
27
+ # lookup in the TUI's child processes (vision key, yolo state) breaks
28
+ # when HOME is unset
29
+ : "${HOME:=/data/data/com.termux/files/home}"
30
+ export HOME
31
+ export TMPDIR="${BLAMCODE_TMPDIR:-${HOME}/tmp}"
32
+ export TEMP="$TMPDIR"
33
+ export TMP="$TMPDIR"
34
+ export OPENCODE_DISABLE_TUI_AUDIO="${OPENCODE_DISABLE_TUI_AUDIO:-1}"
35
+ mkdir -p "$TMPDIR" 2>/dev/null || true
36
+
37
+ # ---- blamcode update: reruns the delta installer ----
38
+ # (if the core is unchanged it is 0 MB — only the BLAMCODE layer refreshes)
39
+ if [ "${1:-}" = "update" ] || [ "${1:-}" = "upgrade" ]; then
40
+ shift
41
+ echo "blamcode: checking for updates (delta — 0 MB if the core is unchanged)"
42
+ UPDATER_URL="https://raw.githubusercontent.com/zyvo9/blamcode/main/install.sh"
43
+ if curl -fsSL --retry 3 --connect-timeout 15 --max-time 120 -o "$TMPDIR/blamcode-update.sh" "$UPDATER_URL" 2>/dev/null; then
44
+ exec sh "$TMPDIR/blamcode-update.sh" "$@"
45
+ fi
46
+ echo "blamcode: could not fetch the update script — check your internet" >&2
47
+ exit 1
48
+ fi
49
+
50
+ # ---- blamcode uninstall: removes BLAMCODE cleanly (needs the repo script) ----
51
+ if [ "${1:-}" = "uninstall" ]; then
52
+ shift
53
+ UNINSTALLER_URL="https://raw.githubusercontent.com/zyvo9/blamcode/main/scripts/blamcode-uninstall"
54
+ UNINSTALLER=""
55
+ # prefer a local copy (installed layout), else the current repo
56
+ for c in "$dir/../share/blamcode/scripts/blamcode-uninstall" "$dir/blamcode-uninstall" "$TMPDIR/blamcode-uninstall"; do
57
+ if [ -f "$c" ]; then UNINSTALLER="$c"; break; fi
58
+ done
59
+ if [ -z "$UNINSTALLER" ]; then
60
+ if curl -fsSL --retry 2 --connect-timeout 15 --max-time 60 -o "$TMPDIR/blamcode-uninstall" "$UNINSTALLER_URL" 2>/dev/null; then
61
+ UNINSTALLER="$TMPDIR/blamcode-uninstall"
62
+ fi
63
+ fi
64
+ if [ -n "$UNINSTALLER" ]; then
65
+ exec sh "$UNINSTALLER" "$@"
66
+ fi
67
+ echo "blamcode: could not fetch the uninstaller — check your internet" >&2
68
+ exit 1
69
+ fi
70
+
71
+ # ---- blamcode preview / blamcode share: local server + public HTTPS tunnel + auto-open ----
72
+ # Browsers block scripts/fetch/CSS on file:// URLs, so serving over HTTP/HTTPS fixes it.
73
+ # Usage:
74
+ # blamcode preview [folder] -> Local server (http://localhost:8080) + auto-open on device
75
+ # blamcode preview --share [folder] -> Local server + Instant live Public HTTPS link (Pinggy/LHR)
76
+ # blamcode share [folder] -> Same as preview --share
77
+ if [ "${1:-}" = "preview" ] || [ "${1:-}" = "open" ] || [ "${1:-}" = "share" ] || [ "${1:-}" = "public" ]; then
78
+ CMD_TYPE="$1"
79
+ shift
80
+ SHARE_MODE=1
81
+ PREVIEW_DIR=""
82
+ while [ "$#" -gt 0 ]; do
83
+ case "$1" in
84
+ --local|-l|--no-share) SHARE_MODE=0; shift ;;
85
+ --share|-s|--public|-p) SHARE_MODE=1; shift ;;
86
+ *) [ -z "$PREVIEW_DIR" ] && PREVIEW_DIR="$1"; shift ;;
87
+ esac
88
+ done
89
+ PREVIEW_DIR="${PREVIEW_DIR:-$PWD}"
90
+ [ -d "$PREVIEW_DIR" ] || { echo "blamcode: no such folder: $PREVIEW_DIR" >&2; exit 1; }
91
+ if ! command -v python3 >/dev/null 2>&1; then
92
+ echo "blamcode: python3 is required for preview — install it (pkg install python)" >&2
93
+ exit 1
94
+ fi
95
+
96
+ exec python3 - "$PREVIEW_DIR" "$SHARE_MODE" "$TMPDIR" "${BLAMCODE_PREVIEW_PORT:-8080}" <<'PYPREV'
97
+ import os, sys, socket, subprocess, time, re
98
+
99
+ preview_dir = os.path.abspath(sys.argv[1])
100
+ share_mode = (sys.argv[2] == "1")
101
+ tmpdir = sys.argv[3]
102
+ default_port = int(sys.argv[4]) if sys.argv[4].isdigit() else 8080
103
+
104
+ def is_port_in_use(port):
105
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
106
+ s.settimeout(0.2)
107
+ return s.connect_ex(('127.0.0.1', port)) == 0
108
+
109
+ def find_free_port(start_port):
110
+ for p in range(start_port, start_port + 20):
111
+ if not is_port_in_use(p):
112
+ return p
113
+ return start_port
114
+
115
+ def shutil_which(cmd):
116
+ if cmd.startswith("/") and os.path.isfile(cmd) and os.access(cmd, os.X_OK):
117
+ return True
118
+ for path in os.environ.get("PATH", "").split(os.pathsep):
119
+ exe = os.path.join(path, cmd)
120
+ if os.path.isfile(exe) and os.access(exe, os.X_OK):
121
+ return True
122
+ return False
123
+
124
+ def open_browser(url):
125
+ open_cmds = [
126
+ ["termux-open-url", url],
127
+ ["am", "start", "-a", "android.intent.action.VIEW", "-d", url],
128
+ ["/system/bin/am", "start", "-a", "android.intent.action.VIEW", "-d", url],
129
+ ["termux-open", url],
130
+ ["xdg-open", url]
131
+ ]
132
+ for cmd in open_cmds:
133
+ if shutil_which(cmd[0]):
134
+ try:
135
+ subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
136
+ return True
137
+ except Exception:
138
+ pass
139
+ return False
140
+
141
+ port = find_free_port(default_port)
142
+ local_url = f"http://localhost:{port}/"
143
+
144
+ # Start local HTTP server in background
145
+ log_file = os.path.join(tmpdir, f"blamcode-preview-{port}.log")
146
+ log_fh = open(log_file, "w")
147
+ server_proc = subprocess.Popen(
148
+ [sys.executable, "-m", "http.server", str(port), "--bind", "127.0.0.1"],
149
+ cwd=preview_dir,
150
+ stdout=log_fh,
151
+ stderr=log_fh,
152
+ start_new_session=True
153
+ )
154
+ time.sleep(0.3)
155
+
156
+ public_url = None
157
+ if share_mode and shutil_which("ssh"):
158
+ tunnel_cmds = [
159
+ ["ssh", "-p", "443", "-R0:localhost:" + str(port), "-o", "StrictHostKeyChecking=no", "-o", "ServerAliveInterval=30", "a.pinggy.io"],
160
+ ["ssh", "-R", "80:localhost:" + str(port), "-o", "StrictHostKeyChecking=no", "nokey@localhost.run"]
161
+ ]
162
+ for tcmd in tunnel_cmds:
163
+ try:
164
+ tproc = subprocess.Popen(
165
+ tcmd,
166
+ stdout=subprocess.PIPE,
167
+ stderr=subprocess.STDOUT,
168
+ text=True,
169
+ bufsize=1,
170
+ start_new_session=True
171
+ )
172
+ start_t = time.time()
173
+ while time.time() - start_t < 4.0:
174
+ line = tproc.stdout.readline()
175
+ if not line and tproc.poll() is not None:
176
+ break
177
+ m = re.search(r'https://[a-zA-Z0-9\-\.]+\.(?:pinggy\.link|lhr\.life|serveo\.net)[^\s]*', line)
178
+ if m:
179
+ public_url = m.group(0).rstrip('.')
180
+ break
181
+ if public_url:
182
+ break
183
+ except Exception:
184
+ pass
185
+
186
+ target_url = public_url if public_url else local_url
187
+ open_browser(target_url)
188
+
189
+ print("\n" + "=" * 50)
190
+ print(" 🚀 BLAMCODE Web Preview Ready!")
191
+ print("=" * 50)
192
+ if public_url:
193
+ print(f" 🌍 Public Live URL : {public_url}")
194
+ print(f" (Share this live HTTPS link with anyone!)")
195
+ print(f" 🏠 Local URL : {local_url}")
196
+ print(f" 📂 Folder : {preview_dir}")
197
+ print(f" 📱 Opened automatically in your device browser.")
198
+ print("=" * 50 + "\n")
199
+
200
+ sys.exit(0)
201
+ PYPREV
202
+ fi
203
+
204
+ # ---- workspace and session resolver ----
205
+ # Structure:
206
+ # /storage/emulated/0/blamcode/
207
+ # ├── <project1>/ (blamcode <project1> or blamcode session <project1>)
208
+ # ├── <project2>/ (blamcode <project2>)
209
+ # └── default/ (blamcode)
210
+ #
211
+ # Every project works strictly inside its own dedicated subfolder
212
+ # so files from different projects never mix or conflict!
213
+
214
+ WS_BASE=""
215
+ if [ -d /storage/emulated/0 ]; then WS_BASE=/storage/emulated/0
216
+ elif [ -d /sdcard ]; then WS_BASE=/sdcard
217
+ fi
218
+ # ---- zyvo -> blamcode migration (one-time) ----
219
+ # old installs kept state + projects under the zyvo name — carry them over
220
+ if [ -d "$HOME/.config/zyvo" ] && [ ! -d "$HOME/.config/blamcode" ]; then
221
+ mv "$HOME/.config/zyvo" "$HOME/.config/blamcode" 2>/dev/null || true
222
+ fi
223
+ rm -f "$HOME/.config/opencode/command/zyvo.md" 2>/dev/null || true
224
+ if [ -n "${WS_BASE:-}" ] && [ -d "$WS_BASE/zyvo" ] && [ ! -d "$WS_BASE/blamcode" ]; then
225
+ mv "$WS_BASE/zyvo" "$WS_BASE/blamcode" 2>/dev/null || true
226
+ fi
227
+
228
+ SDIR_ROOT="${WS_BASE:-$HOME}/blamcode"
229
+ mkdir -p "$SDIR_ROOT" 2>/dev/null || true
230
+
231
+ SESSION_MODE=0
232
+ SESSION_NAME=""
233
+
234
+ while [ "$#" -gt 0 ]; do
235
+ case "$1" in
236
+ --ask)
237
+ # force ASK mode on for this launch (and persist it)
238
+ mkdir -p "$HOME/.config/blamcode" 2>/dev/null || true
239
+ echo on > "$HOME/.config/blamcode/ask.mode" 2>/dev/null || true
240
+ shift; continue ;;
241
+ --yolo|-y|--safe)
242
+ # free/default mode — ASK off (yolo = the usual convention:
243
+ # no questions, full auto)
244
+ mkdir -p "$HOME/.config/blamcode" 2>/dev/null || true
245
+ echo off > "$HOME/.config/blamcode/ask.mode" 2>/dev/null || true
246
+ shift; continue ;;
247
+ session|sessions|ls|list)
248
+ shift
249
+ if [ "$#" -gt 0 ] && [ "$1" != "ls" ] && [ "$1" != "list" ]; then
250
+ SESSION_NAME="$1"; shift; SESSION_MODE=1
251
+ else
252
+ echo ""
253
+ echo "📁 BLAMCODE Projects ($SDIR_ROOT):"
254
+ if [ -d "$SDIR_ROOT" ]; then
255
+ count=0
256
+ for d in "$SDIR_ROOT"/*; do
257
+ if [ -d "$d" ]; then
258
+ b="$(basename "$d")"
259
+ [ "$b" != ".opencode" ] && echo " 🔹 $b" && count=$((count+1))
260
+ fi
261
+ done
262
+ [ $count -eq 0 ] && echo " (no project folders yet)"
263
+ fi
264
+ echo ""
265
+ echo "Open or create: blamcode <project-name>"
266
+ echo "Example: blamcode coffee-shop"
267
+ echo ""
268
+ exit 0
269
+ fi
270
+ break ;;
271
+ -*)
272
+ # Flag for opencode binary, stop parsing
273
+ break ;;
274
+ *)
275
+ if [ -d "$1" ]; then
276
+ # User passed an existing path (e.g. blamcode /path/to/folder)
277
+ cd "$1"
278
+ export OPENCODE_DEFAULT_DIR="$PWD"
279
+ shift
280
+ break
281
+ else
282
+ # User passed a project name directly (e.g. blamcode coffee-shop)
283
+ SESSION_NAME="$1"; shift; SESSION_MODE=1
284
+ break
285
+ fi
286
+ ;;
287
+ esac
288
+ done
289
+
290
+ if [ -z "$SESSION_NAME" ]; then
291
+ case "$PWD" in
292
+ "$SDIR_ROOT"/*)
293
+ # Already inside a project subfolder
294
+ export OPENCODE_DEFAULT_DIR="$PWD"
295
+ ;;
296
+ *)
297
+ if [ "$PWD" = "$HOME" ] || [ "$PWD" = "$SDIR_ROOT" ]; then
298
+ # Default dedicated folder so root is never cluttered
299
+ SDIR="$SDIR_ROOT/default"
300
+ mkdir -p "$SDIR" 2>/dev/null || true
301
+ cd "$SDIR"
302
+ export OPENCODE_DEFAULT_DIR="$SDIR"
303
+ else
304
+ export OPENCODE_DEFAULT_DIR="$PWD"
305
+ fi
306
+ ;;
307
+ esac
308
+ else
309
+ case "$SESSION_NAME" in
310
+ ""|"."|".."|*[!A-Za-z0-9._-]*)
311
+ echo "blamcode: project names may only contain a-z 0-9 . _ - (no spaces/symbols)" >&2
312
+ exit 1 ;;
313
+ esac
314
+ SDIR="$SDIR_ROOT/$SESSION_NAME"
315
+ mkdir -p "$SDIR" 2>/dev/null || true
316
+ if [ -d "$SDIR" ]; then
317
+ cd "$SDIR"
318
+ export OPENCODE_DEFAULT_DIR="$SDIR"
319
+ SESSION_MODE=1
320
+ else
321
+ echo "blamcode: could not create project folder ($SDIR)" >&2
322
+ exit 1
323
+ fi
324
+ fi
325
+
326
+ # ---- ask mode: persistent state + live /ask label ------------------------
327
+ # State: ~/.config/blamcode/ask.mode (on|off, default off). On every launch:
328
+ # 1. rewrite the /ask command description so the picker always shows
329
+ # the actionable label — "/ask off ..." when ON, "/ask on ..." when OFF
330
+ # 2. keep ~/.config/blamcode/ask-instructions.md in sync (ON = strict
331
+ # ask-before-every-step + stay-on-task instructions, OFF = empty).
332
+ # It is wired into the global opencode.json "instructions" list
333
+ # below, so ON applies to every new session.
334
+ # 3. migrate old yolo-mode installs (rename /yolo -> /ask)
335
+ BLAMCODE_CFG="$HOME/.config/blamcode"
336
+ mkdir -p "$BLAMCODE_CFG" 2>/dev/null || true
337
+ if [ ! -f "$BLAMCODE_CFG/ask.mode" ] && [ -f "$BLAMCODE_CFG/yolo.mode" ]; then
338
+ mv "$BLAMCODE_CFG/yolo.mode" "$BLAMCODE_CFG/ask.mode" 2>/dev/null || true
339
+ fi
340
+ rm -f "$HOME/.config/opencode/command/yolo.md" "$BLAMCODE_CFG/yolo-instructions.md" 2>/dev/null || true
341
+ BLAMCODE_ASK="$(cat "$BLAMCODE_CFG/ask.mode" 2>/dev/null || true)"
342
+ case "$BLAMCODE_ASK" in on|off) ;; *) BLAMCODE_ASK="off" ;; esac
343
+ export BLAMCODE_ASK
344
+ ACMD="$HOME/.config/opencode/command/ask.md"
345
+ if [ -f "$ACMD" ]; then
346
+ if [ "$BLAMCODE_ASK" = "on" ]; then
347
+ ADESC="/ask off — ASK ON: AI asks before every step, stays on your task"
348
+ else
349
+ ADESC="/ask on — ASK off: AI works freely (no interruptions)"
350
+ fi
351
+ sed -i "s|^description:.*|description: $ADESC|" "$ACMD" 2>/dev/null || true
352
+ fi
353
+ if [ "$BLAMCODE_ASK" = "on" ]; then
354
+ [ -s "$BLAMCODE_CFG/ask-instructions.md" ] || printf 'ASK MODE ON — MANDATORY persistent BLAMCODE instruction.\n1. STAY ON TASK — work ONLY on the user\x27s current request. Never drift to unrelated files, features, or fixes mid-work. Notice something unrelated? List it as an option for LATER — never touch it now.\n2. ASK BEFORE EVERY major step (file edit, tech/framework choice, design decision, fix strategy, project structure): present 2-4 options with a one-line reason each and WAIT for the user to pick. Keep asking at every step — that is the whole point of this mode.\n3. ONE task at a time — finish it or reach a decision point before anything else.\n4. Task DONE -> stop and report. Never start new work on your own.\n5. Unsure what the user wants? ASK — never guess and build.\n' > "$BLAMCODE_CFG/ask-instructions.md" 2>/dev/null || true
355
+ else
356
+ : > "$BLAMCODE_CFG/ask-instructions.md" 2>/dev/null || true
357
+ fi
358
+
359
+ # ---- ghost-config self-heal ----
360
+ # Old BLAMCODE versions wrote model ids in a format opencode rejects
361
+ # ("deepseek-v4-flash-free" bare → popup "…/ is not valid"). Those keys
362
+ # can survive in global config, the workspace project dir, or project
363
+ # .opencode/ agent files — strip them on every launch so the picker
364
+ # falls back to the stock default. Fast, silent, safe.
365
+ if command -v python3 >/dev/null 2>&1; then
366
+ python3 - "$HOME" "${WS_BASE:-$HOME}" >/dev/null 2>&1 <<'PYGC' || true
367
+ import json, os, sys
368
+ home, base = sys.argv[1], sys.argv[2]
369
+ roots = [
370
+ os.path.join(home, ".config", "opencode"),
371
+ os.path.join(base, "blamcode"),
372
+ os.path.join(base, ".opencode"),
373
+ ]
374
+ def clean_json(p):
375
+ try:
376
+ d = json.load(open(p))
377
+ except Exception:
378
+ return
379
+ if not isinstance(d, dict):
380
+ return
381
+ ch = False
382
+ for k in ("model", "small_model"):
383
+ if k in d:
384
+ del d[k]; ch = True
385
+ a = d.get("agent")
386
+ if isinstance(a, dict) and isinstance(a.get("build"), dict) and "model" in a["build"]:
387
+ del a["build"]["model"]; ch = True
388
+ if ch:
389
+ try:
390
+ json.dump(d, open(p, "w"), indent=2)
391
+ except Exception:
392
+ pass
393
+ def clean_md(p):
394
+ try:
395
+ lines = open(p).readlines()
396
+ except Exception:
397
+ return
398
+ out = [l for l in lines if not l.startswith("model:")]
399
+ if len(out) != len(lines):
400
+ try:
401
+ open(p, "w").writelines(out)
402
+ except Exception:
403
+ pass
404
+ for root in roots:
405
+ if not os.path.isdir(root):
406
+ continue
407
+ # stale /model command files are removed — the built-in /models picker
408
+ # is the only model command that should exist
409
+ cdir = os.path.join(root, "command")
410
+ if os.path.isdir(cdir):
411
+ for fn in ("model.md", "model.json", "model.jsonc"):
412
+ p = os.path.join(cdir, fn)
413
+ if os.path.exists(p):
414
+ try:
415
+ os.remove(p)
416
+ except Exception:
417
+ pass
418
+ for dirpath, dirnames, filenames in os.walk(root):
419
+ dirnames[:] = [d for d in dirnames
420
+ if d not in ("node_modules", ".git", "skills", "themes")]
421
+ if dirpath.count(os.sep) - root.count(os.sep) > 3:
422
+ dirnames[:] = []
423
+ continue
424
+ for fn in filenames:
425
+ p = os.path.join(dirpath, fn)
426
+ if fn in ("opencode.json", "opencode.jsonc"):
427
+ clean_json(p)
428
+ elif fn == "config.json" and ".opencode" in dirpath:
429
+ clean_json(p)
430
+ elif fn.endswith(".md") and (".opencode" in dirpath or
431
+ dirpath.endswith(os.sep + "agent") or dirpath.endswith("/agent")):
432
+ clean_md(p)
433
+
434
+ # wire the persistent ask-mode instructions into the global config
435
+ # (idempotent — opencode appends every "instructions" file to the system
436
+ # prompt, so ASK ON applies to all sessions; the file is empty when OFF)
437
+ try:
438
+ gp = os.path.join(home, ".config", "opencode", "opencode.json")
439
+ try:
440
+ d = json.load(open(gp))
441
+ except Exception:
442
+ d = {}
443
+ if not isinstance(d, dict):
444
+ d = {}
445
+ ains = os.path.join(home, ".config", "blamcode", "ask-instructions.md")
446
+ ins = d.get("instructions")
447
+ if isinstance(ins, str):
448
+ ins = [ins]
449
+ elif not isinstance(ins, list):
450
+ ins = []
451
+ if ains not in ins:
452
+ ins.append(ains)
453
+ d["instructions"] = ins
454
+ json.dump(d, open(gp, "w"), indent=2)
455
+ except Exception:
456
+ pass
457
+ PYGC
458
+ fi
459
+
460
+ # Set the terminal title so the toolbar shows BLAMCODE
461
+ if ( : >/dev/tty ) 2>/dev/null; then printf '\033]0;BLAMCODE\007' >/dev/tty 2>/dev/null; fi
462
+
463
+ # Locate the native libraries we ship alongside the wrapper.
464
+ # In the flat layout they sit next to the wrapper; in the Termux package
465
+ # layout they are under ../lib; on glibc Linux under ~/.local/lib/blamcode.
466
+ NATIVE_LIB_DIR=""
467
+ for candidate in \
468
+ "$dir/../lib" \
469
+ "${PREFIX:-/data/data/com.termux/files/usr}/lib" \
470
+ "$HOME/.local/lib/blamcode" \
471
+ "$dir"
472
+ do
473
+ if [ -f "$candidate/libtagfix.so" ]; then
474
+ NATIVE_LIB_DIR="$candidate"
475
+ break
476
+ fi
477
+ done
478
+
479
+ if [ -n "$NATIVE_LIB_DIR" ]; then
480
+ export LD_PRELOAD="${NATIVE_LIB_DIR}/libtagfix.so${LD_PRELOAD:+:$LD_PRELOAD}"
481
+ export LD_LIBRARY_PATH="${NATIVE_LIB_DIR}${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
482
+ export OPENTUI_LIB_PATH="${NATIVE_LIB_DIR}/libopentui.so"
483
+ if [ -f "${NATIVE_LIB_DIR}/librust_pty_arm64.so" ]; then
484
+ export BUN_PTY_LIB="${NATIVE_LIB_DIR}/librust_pty_arm64.so"
485
+ fi
486
+ export OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER="${OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER:-true}"
487
+ if [ -x "$NATIVE_LIB_DIR/bun" ]; then
488
+ export OPENCODE_BUN_PATH="$NATIVE_LIB_DIR/bun"
489
+ fi
490
+ elif [ -n "$PREFIX" ] && [ -d "$PREFIX" ]; then
491
+ # warn only on real Termux — glibc Linux does not need the native lib
492
+ echo "blamcode: warning: native library directory not found, may crash on Android 11+" >&2
493
+ fi
494
+
495
+ # Locate the real binary. Prefer the package layout first so upgrades do not
496
+ # accidentally execute a stale flat-layout binary left in $PREFIX/bin.
497
+ BIN=""
498
+ for candidate in \
499
+ "$dir/../libexec/opencode/opencode.bin" \
500
+ "$HOME/.local/libexec/blamcode/opencode.bin" \
501
+ "${PREFIX:-/data/data/com.termux/files/usr}/libexec/opencode/opencode.bin" \
502
+ "$dir/opencode.bin" \
503
+ "$HOME/.opencode/bin/opencode"
504
+ do
505
+ if [ -x "$candidate" ]; then
506
+ BIN="$candidate"
507
+ break
508
+ fi
509
+ done
510
+ [ -n "$BIN" ] || { echo "blamcode: error: could not find opencode.bin" >&2; exit 127; }
511
+
512
+ rc=0
513
+ if [ "$SESSION_MODE" = 1 ]; then
514
+ "$BIN" --continue "$@" || rc=$?
515
+ else
516
+ "$BIN" "$@" || rc=$?
517
+ fi
518
+ exit $rc