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,315 @@
1
+ #!/data/data/com.termux/files/usr/bin/bash
2
+ # blamcode-browser — Web page reader + AI extractor + real-time search for BLAMCODE
3
+ #
4
+ # Usage:
5
+ # blamcode-browser <url> [question] — read a page, AI answers about it
6
+ # blamcode-browser <file.html> [question] — read a local HTML file
7
+ # blamcode-browser search "query" [count] — real-time web search (DuckDuckGo)
8
+ #
9
+ # AI model: nemotron-3-ultra-free (default — fastest ~3s, handles 37KB+ text)
10
+ # Override: BLAMCODE_BROWSER_MODEL=deepseek-v4-flash-free
11
+ # Provider: OpenCode Zen (key from OPENCODE_API_KEY, built-in fallback)
12
+ #
13
+ # Termux notes baked in:
14
+ # - python stdout is broken in some Termux builds → os.write(1, ...)
15
+ # - no /tmp folder → temp dirs come from TMPDIR/TEMP/TMP
16
+ # - payloads are built with json.dump (printf breaks JSON on quotes)
17
+
18
+ set -e
19
+
20
+ if [ $# -lt 1 ]; then
21
+ echo "Usage:"
22
+ echo " blamcode-browser <url> [question] — read a page, AI answers"
23
+ echo " blamcode-browser search \"query\" [count] — real-time web search"
24
+ echo " blamcode-browser <file.html> [question] — read a local HTML file"
25
+ exit 1
26
+ fi
27
+
28
+ if command -v python3 >/dev/null 2>&1; then
29
+ PY=python3
30
+ elif command -v python >/dev/null 2>&1; then
31
+ PY=python
32
+ else
33
+ echo "❌ Python is required for blamcode-browser" >&2
34
+ exit 1
35
+ fi
36
+
37
+ ZEN_KEYS="${OPENCODE_API_KEY:-sk-PKOWRt2391BL0MP3W90yaG8qx4vofQJQgigJreBBYjrArj0lwuU1HkWUqOHgDGHP}"
38
+ MODEL="${BLAMCODE_BROWSER_MODEL:-nemotron-3-ultra-free}"
39
+
40
+ exec "$PY" - "$1" "$2" "$3" "$MODEL" "$ZEN_KEYS" <<'PYBR'
41
+ import sys, os, json, re, time
42
+ import urllib.request, urllib.error, urllib.parse
43
+ import html as html_mod
44
+ from html.parser import HTMLParser
45
+
46
+ argv = sys.argv[1:]
47
+ target = argv[0] if len(argv) > 0 else ""
48
+ question = argv[1] if len(argv) > 1 else ""
49
+ count = argv[2] if len(argv) > 2 else ""
50
+ model = argv[3] if len(argv) > 3 else "nemotron-3-ultra-free"
51
+ raw_key = argv[4] if len(argv) > 4 else ""
52
+
53
+ keys = [k.strip() for k in re.split(r"[,;\s\n]+", raw_key) if k.strip()]
54
+ if not keys:
55
+ keys = [os.environ.get("OPENCODE_API_KEY", "")]
56
+
57
+ def out(s):
58
+ try:
59
+ os.write(1, (str(s) + "\n").encode("utf-8", "replace"))
60
+ except Exception:
61
+ pass
62
+
63
+ def fetch(url, timeout=40):
64
+ req = urllib.request.Request(url, headers={
65
+ "User-Agent": ("Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 "
66
+ "(KHTML, like Gecko) Chrome/120.0 Mobile Safari/537.36"),
67
+ "Accept-Language": "en-US,en;q=0.9",
68
+ })
69
+ with urllib.request.urlopen(req, timeout=timeout) as r:
70
+ return r.read().decode("utf-8", "replace")
71
+
72
+ def strip_tags(s):
73
+ s = re.sub(r"<[^>]+>", "", s or "")
74
+ s = html_mod.unescape(s)
75
+ return re.sub(r"\s+", " ", s).strip()
76
+
77
+ class TextExtractor(HTMLParser):
78
+ def __init__(self):
79
+ super().__init__(convert_charrefs=True)
80
+ self.parts = []
81
+ self.skip = 0
82
+ self.blocks = {"p", "div", "li", "tr", "h1", "h2", "h3", "h4", "h5",
83
+ "section", "article", "br", "pre", "blockquote", "td", "th"}
84
+ self.hidden = {"script", "style", "noscript", "template", "svg",
85
+ "head", "iframe", "form", "nav", "footer"}
86
+ def handle_starttag(self, tag, attrs):
87
+ if tag in self.hidden:
88
+ self.skip += 1
89
+ if tag in self.blocks:
90
+ self.parts.append("\n")
91
+ def handle_endtag(self, tag):
92
+ if tag in self.hidden and self.skip:
93
+ self.skip -= 1
94
+ if tag in self.blocks:
95
+ self.parts.append("\n")
96
+ def handle_data(self, data):
97
+ if not self.skip:
98
+ self.parts.append(data)
99
+
100
+ def html_to_text(raw):
101
+ p = TextExtractor()
102
+ try:
103
+ p.feed(raw)
104
+ except Exception:
105
+ pass
106
+ text = "".join(p.parts)
107
+ text = re.sub(r"[ \t]+", " ", text)
108
+ text = re.sub(r"\n\s*\n+", "\n", text)
109
+ return text.strip()
110
+
111
+ def decode_uddg(href):
112
+ href = (href or "").strip()
113
+ if href.startswith("//"):
114
+ href = "https:" + href
115
+ if "uddg=" in href:
116
+ q = urllib.parse.parse_qs(urllib.parse.urlparse(href).query)
117
+ if q.get("uddg"):
118
+ return q["uddg"][0]
119
+ return href
120
+
121
+ def parse_ddg(page):
122
+ """Parse html.duckduckgo.com results — tolerant of markup changes."""
123
+ results = []
124
+ blocks = re.split(r'<div[^>]*class="[^"]*result[^"]*"[^>]*>', page)[1:]
125
+ for b in blocks:
126
+ m = re.search(r'<a[^>]*href="([^"]+)"[^>]*>(.*?)</a>', b, re.S)
127
+ if not m:
128
+ continue
129
+ url = decode_uddg(m.group(1))
130
+ title = strip_tags(m.group(2))
131
+ if not title or not url.startswith("http"):
132
+ continue
133
+ sm = re.search(r'class="[^"]*snippet[^"]*"[^>]*>(.*?)</(?:a|div|span)>', b, re.S)
134
+ snippet = strip_tags(sm.group(1)) if sm else ""
135
+ results.append({"title": title, "url": url, "snippet": snippet})
136
+ if len(results) >= 10:
137
+ break
138
+ if not results:
139
+ for m in re.finditer(r'<a[^>]*href="([^"]+)"[^>]*>(.*?)</a>', page, re.S):
140
+ url = decode_uddg(m.group(1))
141
+ title = strip_tags(m.group(2))
142
+ if not title or not url.startswith("http"):
143
+ continue
144
+ if any(d in url for d in ("duckduckgo.com", "duck.co", "w3.org")):
145
+ continue
146
+ results.append({"title": title, "url": url, "snippet": ""})
147
+ if len(results) >= 10:
148
+ break
149
+ return results
150
+
151
+ def parse_ddg_lite(page):
152
+ results = []
153
+ for m in re.finditer(r'<a[^>]*href="([^"]+)"[^>]*>(.*?)</a>', page, re.S):
154
+ url = m.group(1)
155
+ title = strip_tags(m.group(2))
156
+ if not title or not url.startswith("http"):
157
+ continue
158
+ if "duckduckgo.com" in url or "duck.co" in url:
159
+ continue
160
+ results.append({"title": title, "url": url, "snippet": ""})
161
+ if len(results) >= 10:
162
+ break
163
+ return results
164
+
165
+ def ddg_search(query, n=8):
166
+ q = urllib.parse.quote_plus(query)
167
+ try:
168
+ page = fetch("https://html.duckduckgo.com/html/?q=" + q)
169
+ results = parse_ddg(page)
170
+ except Exception:
171
+ results = []
172
+ if not results:
173
+ try:
174
+ page = fetch("https://lite.duckduckgo.com/lite/?q=" + q)
175
+ results = parse_ddg_lite(page)
176
+ except Exception:
177
+ results = []
178
+ return results[:max(1, n)]
179
+
180
+ def zen_chat(system, user, key, max_tokens=2500):
181
+ url = "https://opencode.ai/zen/v1/chat/completions"
182
+ payload = {
183
+ "model": model,
184
+ "messages": [
185
+ {"role": "system", "content": system},
186
+ {"role": "user", "content": user},
187
+ ],
188
+ "max_tokens": max_tokens,
189
+ }
190
+ req = urllib.request.Request(
191
+ url,
192
+ data=json.dumps(payload).encode("utf-8"),
193
+ headers={
194
+ "Content-Type": "application/json",
195
+ "Authorization": "Bearer " + key,
196
+ # Zen keys the free quota to the opencode client id — BLAMCODE is a
197
+ # rebranded opencode, so this is our real identity (other UAs
198
+ # land in a permanently-exhausted anonymous bucket).
199
+ "User-Agent": "opencode/1.0.0",
200
+ },
201
+ )
202
+ with urllib.request.urlopen(req, timeout=180) as resp:
203
+ d = json.loads(resp.read().decode("utf-8"))
204
+ return d["choices"][0]["message"]["content"]
205
+
206
+ def zen_chat_retry(system, user):
207
+ """Rate limits (429) → wait 12s and retry up to 4 times across all keys."""
208
+ errors = []
209
+ for attempt in range(4):
210
+ for k in keys:
211
+ if not k:
212
+ continue
213
+ try:
214
+ return zen_chat(system, user, k)
215
+ except urllib.error.HTTPError as e:
216
+ errors.append(f"HTTP {e.code}")
217
+ if e.code == 429:
218
+ time.sleep(12)
219
+ else:
220
+ return f"[browser: provider error {e.code}]"
221
+ except Exception as e:
222
+ errors.append(str(e)[:120])
223
+ if attempt < 3:
224
+ time.sleep(5)
225
+ return "[browser: all attempts failed — " + "; ".join(errors[-3:]) + "]"
226
+
227
+ def summarize_search(query, results):
228
+ lines = [f"Real-time search results for: {query}\n"]
229
+ for i, r in enumerate(results, 1):
230
+ lines.append(f"{i}. {r['title']}")
231
+ lines.append(f" URL: {r['url']}")
232
+ if r["snippet"]:
233
+ lines.append(f" {r['snippet']}")
234
+ text = "\n".join(lines)
235
+ system = ("You are a real-time search assistant. Answer the user's question "
236
+ "using ONLY the search results below. Give a clear, detailed answer "
237
+ "with numbered sources. If results are missing or irrelevant, say so "
238
+ "honestly instead of guessing.")
239
+ return zen_chat_retry(system, text + "\n\nQuestion: " + query)
240
+
241
+ def answer_page(page_title, text, question):
242
+ system = ("You are a precise web-page reader. Answer the user's question "
243
+ "using ONLY the page text below. Include exact names, numbers, "
244
+ "facts and steps; quote the page when relevant. If the page does "
245
+ "not contain the answer, say so clearly.")
246
+ user = f"Page: {page_title}\n\n--- page text ---\n{text}\n--- end ---\n\nQuestion: {question}"
247
+ return zen_chat_retry(system, user)
248
+
249
+ def local_or_url_path(p):
250
+ if os.path.isfile(p):
251
+ return os.path.abspath(p)
252
+ if p.startswith(("http://", "https://", "file://")):
253
+ return p
254
+ return "https://" + p
255
+
256
+ # ---------------- main ----------------
257
+ if target == "search":
258
+ query = question or ""
259
+ if not query:
260
+ out("Usage: blamcode-browser search \"query\" [count]")
261
+ sys.exit(1)
262
+ n = 8
263
+ if count and count.isdigit():
264
+ n = int(count)
265
+ out(f"🔎 Searching: {query}\n")
266
+ results = ddg_search(query, n)
267
+ if not results:
268
+ out("⚠ No results found. Try a different query.")
269
+ sys.exit(0)
270
+ out(f"Found {len(results)} result(s):\n")
271
+ for i, r in enumerate(results, 1):
272
+ out(f"{i}. {r['title']}")
273
+ out(f" {r['url']}")
274
+ if r["snippet"]:
275
+ out(f" {r['snippet']}")
276
+ out("\n--- AI summary ---")
277
+ answer = summarize_search(query, results)
278
+ out(answer)
279
+ sys.exit(0)
280
+
281
+ # page mode
282
+ p = local_or_url_path(target)
283
+ page_title = p
284
+ if os.path.isfile(p):
285
+ try:
286
+ with open(p, "r", encoding="utf-8", errors="replace") as fh:
287
+ raw = fh.read()
288
+ page_title = os.path.basename(p)
289
+ except Exception as e:
290
+ out(f"❌ Could not read file {p}: {e}")
291
+ sys.exit(1)
292
+ else:
293
+ out(f"🌐 Fetching: {p}\n")
294
+ try:
295
+ raw = fetch(p)
296
+ except Exception as e:
297
+ out(f"❌ Could not fetch {p}: {e}")
298
+ sys.exit(1)
299
+ m = re.search(r"<title[^>]*>(.*?)</title>", raw, re.S | re.I)
300
+ if m:
301
+ page_title = strip_tags(m.group(1))
302
+
303
+ text = html_to_text(raw)
304
+ if len(text) > 55000:
305
+ text = text[:55000] + "\n...[truncated]"
306
+ if not text:
307
+ out("⚠ No readable text found on the page.")
308
+ sys.exit(0)
309
+
310
+ question = question or "Summarize this page in detail — main topic, key facts, structure and anything notable."
311
+ out(f"📄 {page_title} ({len(text)} chars)\n")
312
+ out("--- AI answer ---")
313
+ answer = answer_page(page_title, text, question)
314
+ out(answer)
315
+ PYBR
@@ -0,0 +1,140 @@
1
+ #!/data/data/com.termux/files/usr/bin/sh
2
+ # blamcode-menu — optional branded launcher for BLAMCODE on Termux.
3
+ #
4
+ # Safe way to start BLAMCODE: every option below exec's the real blamcode wrapper
5
+ # (which sets LD_PRELOAD=libtagfix.so + all env). If blamcode is not on PATH,
6
+ # paper over it with a direct opencode.bin launch using the same env chain.
7
+ #
8
+ # Note: /models, /theme and /auto are TUI-internal commands — they can't be
9
+ # run from a shell. The menu prints the exact keys to press inside the TUI.
10
+
11
+ set -e
12
+
13
+ PREFIX="${PREFIX:-/data/data/com.termux/files/usr}"
14
+
15
+ find_blamcode() {
16
+ for c in \
17
+ "$PREFIX/bin/blamcode" \
18
+ "$PREFIX/libexec/opencode/blamcode.bin" \
19
+ "$PREFIX/libexec/opencode/opencode.bin" \
20
+ "$HOME/.local/bin/blamcode" \
21
+ "$HOME/.local/libexec/blamcode/opencode.bin" \
22
+ "$HOME/.opencode/bin/opencode"
23
+ do
24
+ [ -x "$c" ] && { echo "$c"; return 0; }
25
+ done
26
+ return 1
27
+ }
28
+
29
+ launch() { # run the real wrapper with args ($1 = bin, rest = args)
30
+ bin="$1"; shift
31
+ if [ "$(basename "$bin")" = "blamcode" ]; then
32
+ exec "$bin" "$@"
33
+ else
34
+ # raw binary: replicate the wrapper env chain
35
+ export ANDROID_ROOT="${ANDROID_ROOT:-/system}"
36
+ export OPENCODE_DISABLE_TUI_AUDIO="${OPENCODE_DISABLE_TUI_AUDIO:-1}"
37
+ export TMPDIR="${BLAMCODE_TMPDIR:-${HOME:-/data/data/com.termux/files/home}/tmp}"
38
+ mkdir -p "$TMPDIR" 2>/dev/null || true
39
+ for libdir in "$PREFIX/lib" "$HOME/.local/lib/blamcode"; do
40
+ if [ -f "$libdir/libtagfix.so" ]; then
41
+ export LD_PRELOAD="$libdir/libtagfix.so${LD_PRELOAD:+:$LD_PRELOAD}"
42
+ export LD_LIBRARY_PATH="$libdir${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
43
+ [ -f "$libdir/libopentui.so" ] && export OPENTUI_LIB_PATH="$libdir/libopentui.so"
44
+ [ -f "$libdir/librust_pty_arm64.so" ] && export BUN_PTY_LIB="$libdir/librust_pty_arm64.so"
45
+ [ -x "$libdir/bun" ] && export OPENCODE_BUN_PATH="$libdir/bun"
46
+ export OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER="${OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER:-true}"
47
+ break
48
+ fi
49
+ done
50
+ exec "$bin" "$@"
51
+ fi
52
+ }
53
+
54
+ BIN="$(find_blamcode || true)"
55
+
56
+ menu() {
57
+ printf '\033[1;96m=== BLAMCODE launcher ===\033[0m\n'
58
+ printf ' 1) Chat (new session)\n'
59
+ printf ' 2) Open project blamcode <name>\n'
60
+ printf ' 3) List projects blamcode ls\n'
61
+ printf ' 4) Keys help\n'
62
+ printf ' 5) Version\n'
63
+ printf ' 6) Update BLAMCODE\n'
64
+ printf ' 7) Headless server (serve)\n'
65
+ printf ' 8) Plugins\n'
66
+ printf ' 9) Trust a folder\n'
67
+ printf ' 0) Exit\n'
68
+ printf 'Choose [0-9]: '
69
+ }
70
+
71
+ while :; do
72
+ menu
73
+ read -r choice
74
+ case "$choice" in
75
+ 1)
76
+ [ -n "$BIN" ] || { echo "blamcode not found — run install.sh first" >&2; exit 1; }
77
+ launch "$BIN"
78
+ ;;
79
+ 2)
80
+ printf 'Project name: '
81
+ read -r pname
82
+ [ -n "$BIN" ] || { echo "blamcode not found" >&2; exit 1; }
83
+ launch "$BIN" "$pname"
84
+ ;;
85
+ 3)
86
+ [ -n "$BIN" ] || { echo "blamcode not found" >&2; exit 1; }
87
+ SDIR_ROOT=""
88
+ if [ -d /storage/emulated/0 ]; then SDIR_ROOT=/storage/emulated/0/blamcode
89
+ elif [ -d /sdcard ]; then SDIR_ROOT=/sdcard/blamcode
90
+ else SDIR_ROOT="$HOME/blamcode"
91
+ fi
92
+ echo ""
93
+ echo "📁 BLAMCODE Projects ($SDIR_ROOT):"
94
+ if [ -d "$SDIR_ROOT" ]; then
95
+ count=0
96
+ for d in "$SDIR_ROOT"/*; do
97
+ if [ -d "$d" ]; then
98
+ b="$(basename "$d")"
99
+ [ "$b" != ".opencode" ] && echo " 🔹 $b" && count=$((count+1))
100
+ fi
101
+ done
102
+ [ $count -eq 0 ] && echo " (no project folders yet)"
103
+ else
104
+ echo " (no blamcode folder yet)"
105
+ fi
106
+ echo ""
107
+ echo "Open or create: blamcode <project-name>"
108
+ echo ""
109
+ read -r _ || true
110
+ ;;
111
+ 4)
112
+ printf '\nInside the BLAMCODE TUI:\n Ctrl+K -> command palette (/models, /theme...)\n Esc -> back\n /explore /fix /review /ask /blamcode /open -> in-chat commands\nPress any key to return to menu...\n'
113
+ read -r _
114
+ ;;
115
+ 5)
116
+ if [ -n "$BIN" ]; then launch "$BIN" --version; else echo "blamcode not installed"; fi
117
+ read -r _ || true
118
+ ;;
119
+ 6)
120
+ echo "Run: sh $PREFIX/../share/blamcode/install.sh (or re-download the installer)"
121
+ read -r _ || true
122
+ ;;
123
+ 7)
124
+ [ -n "$BIN" ] || { echo "blamcode not found" >&2; exit 1; }
125
+ launch "$BIN" serve 2>/dev/null || launch "$BIN" serve --port 4020
126
+ ;;
127
+ 8)
128
+ [ -n "$BIN" ] || { echo "blamcode not found" >&2; exit 1; }
129
+ launch "$BIN" plugin
130
+ ;;
131
+ 9)
132
+ printf 'Folder to trust: '
133
+ read -r folder
134
+ [ -n "$BIN" ] || { echo "blamcode not found" >&2; exit 1; }
135
+ launch "$BIN" trust include "$folder"
136
+ ;;
137
+ 0) exit 0 ;;
138
+ *) ;;
139
+ esac
140
+ done
@@ -0,0 +1,71 @@
1
+ #!/data/data/com.termux/files/usr/bin/bash
2
+ # blamcode-uninstall — cleanly removes BLAMCODE from the device
3
+ #
4
+ # Usage:
5
+ # blamcode uninstall (asks for confirmation)
6
+ # blamcode uninstall -y (skip confirmation)
7
+ #
8
+ # Removes:
9
+ # - the blamcode / oc-settings / blamcode-menu launchers
10
+ # - the BLAMCODE layer (config, skills, commands) at ~/.config/opencode
11
+ # - the branded core binary at libexec + native libs
12
+ # - PATH + OPENCODE_API_KEY lines from ~/.bashrc
13
+ #
14
+ # Keeps (never touched):
15
+ # - your projects / session folders (/storage/emulated/0/blamcode)
16
+ # - the opencode core install at ~/.opencode (separate tool)
17
+ set -e
18
+
19
+ PREFIX="${PREFIX:-/data/data/com.termux/files/usr}"
20
+ HOME_DIR="${HOME:-/data/data/com.termux/files/home}"
21
+ CONFIG_DIR="$HOME/.config/opencode"
22
+ BIN_DIR=""
23
+ LIBEXEC_DIR=""
24
+ LIB_DIR=""
25
+
26
+ # resolve install layout (Termux vs ~/.local)
27
+ if [ -n "$PREFIX" ] && [ -d "$PREFIX" ] && [ -n "$(command -v pkg 2>/dev/null || true)" ]; then
28
+ BIN_DIR="$PREFIX/bin"
29
+ LIBEXEC_DIR="$PREFIX/libexec/opencode"
30
+ LIB_DIR="$PREFIX/lib"
31
+ else
32
+ BIN_DIR="$HOME/.local/bin"
33
+ LIBEXEC_DIR="$HOME/.local/libexec/blamcode"
34
+ LIB_DIR="$HOME/.local/lib/blamcode"
35
+ fi
36
+
37
+ if [ "${1:-}" != "-y" ] && [ "${1:-}" != "--yes" ]; then
38
+ echo "This will remove BLAMCODE (AI coding CLI) from this device."
39
+ echo " launchers: $BIN_DIR/{blamcode,blamcode-menu,oc-settings}"
40
+ echo " config: $CONFIG_DIR (opencode.json, agent, commands, skills)"
41
+ echo " core: $LIBEXEC_DIR + $LIB_DIR"
42
+ echo
43
+ echo "Your projects and session folders are NOT touched."
44
+ echo -n "Continue? [y/N]: "
45
+ read -r CONFIRM || CONFIRM=""
46
+ case "$CONFIRM" in
47
+ y|Y|yes|YES) ;;
48
+ *) echo "Aborted."; exit 1;;
49
+ esac
50
+ fi
51
+
52
+ rm -f "$BIN_DIR/blamcode" "$BIN_DIR/blamcode-menu" "$BIN_DIR/oc-settings"
53
+ rm -f "$LIBEXEC_DIR/opencode.bin" "$LIBEXEC_DIR/blamcode-core-version"
54
+ rm -rf "$LIBEXEC_DIR" "$LIB_DIR"
55
+ rm -rf "$CONFIG_DIR"
56
+ echo " ✓ launchers removed"
57
+ echo " ✓ config layer removed ($CONFIG_DIR)"
58
+ echo " ✓ core removed"
59
+
60
+ # clean ~/.bashrc additions (only the BLAMCODE lines)
61
+ RC="$HOME_DIR/.bashrc"
62
+ [ -f "$RC" ] || RC="$HOME_DIR/.profile"
63
+ if [ -f "$RC" ]; then
64
+ grep -v "BLAMCODE AI\|BLAMCODE PATH\|OPENCODE_API_KEY=" "$RC" > "$RC.blamcode-clean" 2>/dev/null || true
65
+ mv "$RC.blamcode-clean" "$RC"
66
+ echo " ✓ PATH + API key lines removed from $(basename "$RC")"
67
+ fi
68
+
69
+ echo
70
+ echo "BLAMCODE uninstalled. Your projects are still at /storage/emulated/0/blamcode."
71
+ echo "Reinstall any time with: curl -fsSL https://raw.githubusercontent.com/zyvo9/blamcode/main/install.sh | bash"