@windypro-rourou/dsh-logcat 0.6.1 → 0.6.3

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.en.md ADDED
@@ -0,0 +1,89 @@
1
+ # dsh-logcat
2
+
3
+ An Android real-device debugging workbench for the DSH Web GUI (an Android Studio-style Logcat view).
4
+
5
+ [中文](README.md) | **English**
6
+
7
+ ## Features
8
+
9
+ - **Auto-connect**: probes adb on this machine (`ANDROID_HOME` / `ANDROID_SDK_ROOT` / default `%LOCALAPPDATA%\Android\Sdk` / PATH /
10
+ `~/.dsh/adb`), polls `adb devices -l` every 2s; devices in debug mode get a logcat stream **attached automatically** (`-v threadtime`) — no need to open the panel first.
11
+ - **One-click adb install**: when adb is missing, the panel's status bar shows a "One-click install adb" button — it downloads platform-tools from the USTC mirror (Google official fallback), unpacks them to `~/.dsh/adb` and connects automatically (~10MB).
12
+ - **Live logs**: WebSocket push, 2000-line ring buffer per device; automatic reconnect with exponential backoff.
13
+ - **Logcat panel** (sidebar "Logcat" entry, right-side drawer, **draggable width that is remembered**):
14
+ - Device dropdown (model / serial / state, remembers the last selection)
15
+ - Severity filter (V/D/I/W/E/F single-select, Android Studio colors)
16
+ - Keyword filter (**space-separated keywords = any-match**) and a **test package input** (Enter to set; synced with the agent's `logcat_set_package`; shown live in the status bar)
17
+ - **Screenshot button**: one-click device screen capture and PNG download (`exec-out screencap`)
18
+ - **Crash highlighting**: FATAL EXCEPTION / ANR lines highlighted in red
19
+ - **History button**: loads on-disk persisted logs (survives GUI restarts; merged with the live buffer and deduped by timestamp)
20
+ - **Crash auto-snapshots**: on a FATAL EXCEPTION / ANR in the stream, a screenshot + log context are saved to `~/.dsh/logcat/crashes`; the panel shows a banner and you can inspect the screenshot/log in the "Crash" dialog
21
+ - **Events view**: one-click live `logcat -b events` stream (app start/crash/lifecycle events)
22
+ - **WiFi wireless debugging wizard**: IP/port/pairing-code in, `adb pair` + `adb connect` out — ditch the cable (Android 11+)
23
+ - **Install APK button**: local file picker installs an APK to the current device (streamed upload, 1GB cap)
24
+ - **Quick device keys**: Home / Back / Recents / Wake / Power / Volume ± with one click
25
+ - **Performance sparklines**: CPU / memory / battery sampled every 2s with trend charts in the status bar
26
+ - **Live screen casting + remote control**: the "Screen" tab streams the device display (~1fps over WS binary frames); **click = tap, drag = swipe**, a text input sends keystrokes straight to the phone, quick keys included, one-click download of the current frame — no need to pick up the phone
27
+ - **Reverse-engineering workbench** (Log / Screen / RE tabs): process list → memory hex/string search → match addresses → click to dump 256B
28
+ - Pause/resume (pause buffers new lines, resume replays them), clear, copy, export .txt
29
+ - Windowed rendering + auto-scroll (pauses while you scroll up manually)
30
+ - Unauthorized-device hint ("allow USB debugging on the phone")
31
+ - **Agent tools** (30 in total, all exposed to the agent and announced in its system prompt):
32
+ - Device: `logcat_devices` (list devices), `device_info` (model/version/SDK/resolution/memory/battery), `device_stats` (CPU/memory/battery samples), `app_info` (installed version name / versionCode / APK path)
33
+ - Screen / multimodal: `screen_capture` (screenshot saved locally + embedded as an image block so a multimodal model can see it and fix bugs with `input_*`)
34
+ - Execution: `adb_exec` (shell), `adb_install` (install a local APK), `adb_pull` (pull files), `app_launch` (start an app / specific Activity), `app_stop` (force-stop; destructive — confirm first)
35
+ - Input: `input_tap` / `input_swipe` / `input_text` / `input_keyevent` (real-device UI automation), `ui_dump` (UI hierarchy XML), `activity_current` (current foreground Activity)
36
+ - Logs: `logcat_recent` (filter by package/severity/keyword), `logcat_history` (disk history backfill), `logcat_crash` (crash/ANR blocks with context), `logcat_events` (events buffer), `crash_sessions` (crash snapshot history), `logcat_set_package` (lock the package under test)
37
+ - Reverse engineering / memory: `proc_list` (process list), `proc_maps` (memory maps + module bases), `proc_status` (process state / memory summary), `proc_smaps` (top Pss regions), `mem_dump` (read memory at an address as hex), `mem_search` (search memory for hex/string patterns), `frida_server` (deploy/start/stop frida-server), `frida_script` (generate hook/trace/scan/bypass/dump script templates)
38
+ - **Real-device debugging workflow**: while building an Android app, the agent's announcement dynamically lists connected devices (serial + model) and the current test package. After confirming with the user: `adb_install` to deploy → `adb_exec`/`app_launch` to start → `logcat_set_package` to lock → `logcat_recent` / `logcat_crash` for crashes → `ui_dump` + `input_*` for UI automation → `adb_pull` for artifacts. A closed loop on real hardware.
39
+ - **Reverse-engineering workflow**: `proc_list` to find the process → `proc_maps` for module bases → `mem_dump` at a target address / `mem_search` for signatures → `proc_smaps` for memory detail → `frida_script` + `frida_server` for dynamic instrumentation. Reading another app's memory/maps needs root or a debuggable app (run-as); tools return clear errors otherwise.
40
+ - **Extra routes**: `POST /api/dsh-logcat/exec` (shell), `POST /api/dsh-logcat/package` (set package), `GET /api/dsh-logcat/screenshot`, `POST /api/dsh-logcat/install-adb`, `GET /api/dsh-logcat/history`, `GET /api/dsh-logcat/crashes` + `GET /api/dsh-logcat/crash-file`, `POST /api/dsh-logcat/install-apk`, `POST /api/dsh-logcat/keyevent`, `POST /api/dsh-logcat/wifi-connect` / `wifi-disconnect`, `GET /api/dsh-logcat/processes`, `POST /api/dsh-logcat/mem-search`, `GET /api/dsh-logcat/mem-dump`.
41
+ - **On-disk data**: logs are written per device per day to `~/.dsh/logcat/logs/<serial>/logcat-MM-DD.log` (raw threadtime lines); crash snapshots live in `~/.dsh/logcat/crashes/<serial>/<timestamp>-<FATAL|ANR>/` (`screenshot.png` + `crash.log` + `meta.json`).
42
+
43
+ ## Installation
44
+
45
+ ```bash
46
+ # Recommended (npm install):
47
+ dsh plugin --profile web add @windypro-rourou/dsh-logcat
48
+
49
+ # Update to the latest (no auto-update; older installs show a version hint in the panel/agent prompt):
50
+ dsh plugin --profile web update # latest within the current major
51
+ # or force: dsh plugin --profile web add @windypro-rourou/dsh-logcat@latest
52
+ # restart the GUI (dsh web) afterwards
53
+
54
+ # Optional preview channel (frequent small iterations, may be unstable):
55
+ dsh plugin --profile web add @windypro-rourou/dsh-logcat@preview
56
+ ```
57
+
58
+ ## Release policy (main / latest first)
59
+
60
+ - **`main` branch + npm `latest` tag**: stable releases — the main update channel. Features ship to main/latest as soon as a batch is ready.
61
+ - **`preview` branch + npm `preview` tag**: optional early-access channel (frequent small iterations). If there are no experimental features since the last stable release, the preview tag simply stays at an older preview version — ignore it.
62
+ - The version self-check notifies per channel only (stable users see `latest`, preview users see `preview`; no cross-talk).
63
+
64
+ For development you can also link the source directly into the web profile and append the patch row in `~/.dsh/profiles/web/cordis.patch.yml`:
65
+
66
+ ```bash
67
+ pnpm --dir "%USERPROFILE%\.dsh\profiles\web" add link:<this-directory>
68
+ # then add to ~/.dsh/profiles/web/cordis.patch.yml:
69
+ # - insert:
70
+ # - id: logcat
71
+ # name: '@windypro-rourou/dsh-logcat'
72
+ # The patch file is hot-watched by a running GUI; restart it if it does not take effect.
73
+ ```
74
+
75
+ > Note: every method inserts the same `logcat` row into the profile tree — never use two of them at once, or the next boot fails on a duplicate plugin id.
76
+
77
+ Dependency resolution: `ws` / `react` / `react-dom` / `@deepseek-ai/*` resolve through junctions in this directory's `node_modules` pointing at the host's actually-loaded packages (single instance). Re-point the junctions when the host upgrades its dependencies.
78
+
79
+ ## Limitations
80
+
81
+ - The device must have USB debugging enabled and be authorized on this machine (`unauthorized` state is surfaced).
82
+ - logcat output may contain sensitive information; `/api/dsh-logcat/*` routes are loopback-only.
83
+ - `adb shell` commands consume real device resources — confirm before running destructive ones.
84
+
85
+ ## Files
86
+
87
+ - `lib/index.js` — host half: adb engine, polling, logcat child processes, routes, WebSocket, agent tools.
88
+ - `lib/client.js` — browser half: sidebar entry + Logcat panel (React, no build step).
89
+ - `cordis.patch.yml` — profile bundle patch (applied automatically).
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  DSH Web GUI 的安卓实机调试面板(类似 Android Studio 的 Logcat 视图)。
4
4
 
5
+ [中文](README.md) | [English](README.en.md)
6
+
5
7
  ## 功能
6
8
 
7
9
  - **自动连接**:探测本机 adb(`ANDROID_HOME` / `ANDROID_SDK_ROOT` / 默认 `%LOCALAPPDATA%\Android\Sdk` / PATH /
package/lib/client.js CHANGED
@@ -133,6 +133,8 @@ window.__ModuleLoader__.load({
133
133
  .lc-re-row[data-on] { background: light-dark(rgba(66,133,244,.12), rgba(66,133,244,.2)); }
134
134
  .lc-re-dump { flex: 1; overflow: auto; padding: 8px 10px; font-family: Consolas, "Cascadia Mono", monospace; font-size: 11px; line-height: 16px; white-space: pre-wrap; word-break: break-all; }
135
135
  .lc-re-err { padding: 8px 10px; color: #ef5350; font-size: 12px; white-space: pre-wrap; }
136
+ .lc-re-man { display: flex; gap: 6px; padding: 6px 10px; align-items: center; flex-wrap: wrap; }
137
+ .lc-re-man input { background: light-dark(rgba(0,0,0,.05), rgba(255,255,255,.08)); border: 1px solid light-dark(rgba(0,0,0,.16), rgba(255,255,255,.2)); border-radius: 7px; color: inherit; font: inherit; font-size: 12px; padding: 4px 8px; }
136
138
  .lc-screen { position: absolute; inset: 0; display: flex; flex-direction: column; }
137
139
  .lc-screen-bar { display: flex; gap: 6px; padding: 6px 12px; border-bottom: 1px solid light-dark(rgba(0,0,0,.08), rgba(255,255,255,.1)); flex: none; align-items: center; flex-wrap: wrap; font-size: 11px; }
138
140
  .lc-screen-bar input { background: light-dark(rgba(0,0,0,.05), rgba(255,255,255,.08)); border: 1px solid light-dark(rgba(0,0,0,.16), rgba(255,255,255,.2)); border-radius: 7px; color: inherit; font: inherit; font-size: 12px; padding: 4px 8px; }
@@ -226,6 +228,10 @@ window.__ModuleLoader__.load({
226
228
  const [reDump, setReDump] = useState("");
227
229
  const [reDumpAddr, setReDumpAddr] = useState("");
228
230
  const [reError, setReError] = useState("");
231
+ const [reMaps, setReMaps] = useState([]);
232
+ const [reMapError, setReMapError] = useState("");
233
+ const [reManualAddr, setReManualAddr] = useState("");
234
+ const [reDumpLen, setReDumpLen] = useState(256);
229
235
  const [spark, setSpark] = useState({ cpu: [], mem: [], bat: [] });
230
236
  const [screenImg, setScreenImg] = useState(null);
231
237
  const [screenText, setScreenText] = useState("");
@@ -727,20 +733,57 @@ window.__ModuleLoader__.load({
727
733
  .finally(() => setReSearching(false));
728
734
  };
729
735
 
730
- const reDumpAt = (addr) => {
731
- const hexAddr = addr.startsWith("0x") ? addr : "0x" + addr.toString(16);
736
+ // od hex text -> aligned hex + ASCII columns for readability.
737
+ const formatHexDump = (text) => {
738
+ if (text === "") return "";
739
+ if (/DSH_MEM_FAIL|Permission denied|No such file/i.test(text)) return text;
740
+ const out = [];
741
+ for (const line of text.split(/\r?\n/)) {
742
+ const m = /^([0-9a-f]+):\s+(.*)$/.exec(line.trim());
743
+ if (m === null) { out.push(line); continue; }
744
+ const bytes = (m[2].match(/[0-9a-f]{2}/gi) ?? []).map((b) => Number.parseInt(b, 16));
745
+ const ascii = bytes.map((b) => (b >= 0x20 && b < 0x7f ? String.fromCharCode(b) : ".")).join("");
746
+ out.push(m[1].padStart(7, "0") + " " + m[2].padEnd(48) + " |" + ascii + "|");
747
+ }
748
+ return out.join("\n");
749
+ };
750
+
751
+ const reDumpAt = (addr, len) => {
752
+ const hexAddr = typeof addr === "string" && addr.startsWith("0x") ? addr : "0x" + addr.toString(16);
753
+ const length = Math.min(Math.max(Number(len ?? 256) || 256, 1), 65536);
732
754
  setReDumpAddr(hexAddr);
733
755
  setReDump("读取中…");
734
756
  setReError("");
735
- fetch(API_BASE + "/mem-dump?serial=" + encodeURIComponent(serial) + "&pid=" + rePid + "&address=" + encodeURIComponent(hexAddr) + "&length=256")
757
+ fetch(API_BASE + "/mem-dump?serial=" + encodeURIComponent(serial) + "&pid=" + rePid + "&address=" + encodeURIComponent(hexAddr) + "&length=" + length)
736
758
  .then((res) => res.json())
737
759
  .then((body) => {
738
760
  if (body?.error) { setReError(body.error); setReDump(""); return; }
739
- setReDump(body?.hex ?? "");
761
+ setReDump(formatHexDump(body?.hex ?? ""));
740
762
  })
741
763
  .catch(() => { setReDump(""); setReError("转储失败"); });
742
764
  };
743
765
 
766
+ // Load the selected process's memory-mapped modules (/proc/<pid>/maps).
767
+ const reFetchMaps = (pid) => {
768
+ const target = Number.isInteger(pid) ? pid : rePid;
769
+ if (!Number.isInteger(target) || target <= 0) return;
770
+ setReMaps([]);
771
+ setReMapError("");
772
+ fetch(API_BASE + "/maps?serial=" + encodeURIComponent(serial) + "&pid=" + target)
773
+ .then((res) => res.json())
774
+ .then((body) => {
775
+ if (body?.error) { setReMapError(body.error); setReMaps([]); return; }
776
+ setReMaps(body?.modules ?? []);
777
+ })
778
+ .catch(() => setReMapError("模块列表加载失败"));
779
+ };
780
+
781
+ const manualDump = () => {
782
+ const addr = reManualAddr.trim().replace(/^0x/i, "");
783
+ if (addr === "" || !/^[0-9a-fA-F]+$/.test(addr)) { setReError("地址格式:0x7a0000 或 7a0000"); return; }
784
+ reDumpAt("0x" + addr.toLowerCase(), reDumpLen);
785
+ };
786
+
744
787
  // Live screen stream: on while the 屏幕 tab is open, the panel is open, and a device is selected.
745
788
  useEffect(() => {
746
789
  const prev = prevSerialRef.current;
@@ -1025,16 +1068,32 @@ window.__ModuleLoader__.load({
1025
1068
  rePids.length === 0
1026
1069
  ? h("div", { className: "lc-re-row", title: "点击「进程」或等待自动加载" }, "(尚未加载 — 点击「进程」刷新)")
1027
1070
  : rePids.map((p) =>
1028
- h("div", { key: p.pid, className: "lc-re-row", "data-on": rePid === p.pid ? "" : undefined, title: (p.user ?? "") + " " + p.name, onClick: () => { setRePid(p.pid); setReResults([]); setReDump(""); setReError(""); } },
1071
+ h("div", { key: p.pid, className: "lc-re-row", "data-on": rePid === p.pid ? "" : undefined, title: (p.user ?? "") + " " + p.name, onClick: () => { setRePid(p.pid); setReResults([]); setReDump(""); setReError(""); reFetchMaps(p.pid); } },
1029
1072
  p.pid + " " + p.name)))),
1030
1073
  h("div", { className: "lc-re-col2" },
1074
+ h("div", { className: "lc-re-head" }, "模块 / so 库 (" + reMaps.length + ") — 点击转储基址"),
1075
+ h("div", { className: "lc-re-list" },
1076
+ reMapError !== ""
1077
+ ? h("div", { className: "lc-re-row" }, reMapError)
1078
+ : reMaps.length === 0
1079
+ ? h("div", { className: "lc-re-row" }, "(无模块 — 普通设备无法读取其他应用 maps,需 root)")
1080
+ : reMaps.map((mo) =>
1081
+ h("div", { key: mo.path, className: "lc-re-row", title: mo.path + " [" + mo.perms + "]", onClick: () => reDumpAt(mo.base, 256) },
1082
+ (mo.path.split("/").pop() ?? mo.path) + " @" + mo.base + " " + mo.perms))),
1031
1083
  h("div", { className: "lc-re-head" }, "匹配地址(点击转储 256B)" + (reResults.length > 0 ? " · " + reResults.length : "")),
1032
1084
  h("div", { className: "lc-re-list" },
1033
1085
  reResults.map((o) =>
1034
1086
  h("div", { key: o, className: "lc-re-row", "data-on": reDumpAddr === "0x" + o.toString(16) ? "" : undefined, title: "0x" + o.toString(16), onClick: () => reDumpAt(o) },
1035
1087
  "0x" + o.toString(16)))),
1088
+ h("div", { className: "lc-re-head" }, "手动转储"),
1089
+ h("div", { className: "lc-re-man" },
1090
+ h("input", { placeholder: "地址 0x7a0000…", value: reManualAddr, onChange: (e) => setReManualAddr(e.target.value), onKeyDown: (e) => { if (e.key === "Enter") manualDump(); }, style: { flex: 1, minWidth: 120 } }),
1091
+ h("select", { className: "lc-select", value: String(reDumpLen), onChange: (e) => setReDumpLen(Number(e.target.value)) },
1092
+ [128, 256, 1024, 4096].map((n) => h("option", { key: n, value: String(n) }, n + "B"))),
1093
+ h("button", { type: "button", className: "lc-btn", onClick: manualDump }, "转储"),
1094
+ ),
1036
1095
  reDump !== ""
1037
- ? h("div", { className: "lc-re-head" }, "转储 " + reDumpAddr)
1096
+ ? h("div", { className: "lc-re-head" }, "转储 " + reDumpAddr + " · " + reDumpLen + "B")
1038
1097
  : null,
1039
1098
  reDump !== "" ? h("div", { className: "lc-re-dump" }, reDump) : null,
1040
1099
  reError !== "" ? h("div", { className: "lc-re-err" }, reError) : null,
package/lib/index.js CHANGED
@@ -1275,6 +1275,26 @@ function makeRoutes(engine) {
1275
1275
  }
1276
1276
  },
1277
1277
  },
1278
+ {
1279
+ kind: 'exact',
1280
+ path: API_BASE + '/maps',
1281
+ handler: async (req, res) => {
1282
+ if (!isLoopbackRequest(req)) { writeJson(res, 403, { error: 'forbidden: loopback-only' }); return }
1283
+ if ((req.method ?? 'GET') !== 'GET') { writeJson(res, 405, { error: 'method not allowed' }); return }
1284
+ if (engine.adb === null) { writeJson(res, 500, { error: 'adb not found' }); return }
1285
+ const url = new URL(req.url ?? '/', 'http://localhost')
1286
+ const serial = url.searchParams.get('serial') ?? ''
1287
+ const pid = Number(url.searchParams.get('pid'))
1288
+ if (serial === '' || !engine.devices.has(serial) || !Number.isInteger(pid) || pid <= 0) {
1289
+ writeJson(res, 400, { error: 'serial + valid pid required' }); return
1290
+ }
1291
+ try {
1292
+ writeJson(res, 200, { pid, modules: await listModules(engine, serial, pid) })
1293
+ } catch (error) {
1294
+ writeJson(res, 200, { pid, modules: [], error: error instanceof Error ? error.message : String(error) })
1295
+ }
1296
+ },
1297
+ },
1278
1298
  ]
1279
1299
 
1280
1300
  const upgrade = {
@@ -1966,6 +1986,26 @@ function procListTool(engine) {
1966
1986
  })
1967
1987
  }
1968
1988
 
1989
+ /** Read /proc/<pid>/maps and return unique file-backed module mappings (base/end/perms/path). Throws on permission failure. */
1990
+ export async function listModules(engine, serial, pid) {
1991
+ const result = await runAdbFull(engine.adb, ['-s', serial, 'shell', `cat /proc/${pid}/maps 2>&1`], 20000)
1992
+ if (!result.ok || result.stderr.includes('Permission denied') || /Permission denied|No such file/.test(result.stdout)) {
1993
+ throw new Error(`cannot read /proc/${pid}/maps (need root, or a debuggable app via run-as); output: ${(result.stdout || result.stderr).slice(0, 200)}`)
1994
+ }
1995
+ const seen = new Set()
1996
+ const modules = []
1997
+ for (const line of (result.stdout ?? '').split(/\r?\n/)) {
1998
+ const m = /^([0-9a-f]+)-([0-9a-f]+)\s+([rwxps-]{4})\s+[0-9a-f]+\s+\S+\s+\d+\s+(.+)$/.exec(line)
1999
+ if (m === null) continue
2000
+ const path = m[4]
2001
+ if (!path.startsWith('/')) continue
2002
+ if (seen.has(path)) continue
2003
+ seen.add(path)
2004
+ modules.push({ base: m[1], end: m[2], perms: m[3], path })
2005
+ }
2006
+ return modules
2007
+ }
2008
+
1969
2009
  /** The proc_maps agent tool: memory maps + module bases (reverse engineering core). */
1970
2010
  function procMapsTool(engine) {
1971
2011
  return defineTool({
@@ -2013,22 +2053,12 @@ function procMapsTool(engine) {
2013
2053
  const serial = typeof args.serial === 'string' && args.serial !== '' ? args.serial : devices[0]?.serial ?? ''
2014
2054
  const pid = Number(args.pid)
2015
2055
  if (serial === '' || engine.adb === null || !Number.isInteger(pid) || pid <= 0) return { pid: 0, modules: [], error: 'a valid pid is required' }
2016
- const result = await runAdbFull(engine.adb, ['-s', serial, 'shell', `cat /proc/${pid}/maps 2>&1`], 20000)
2017
- if (!result.ok || result.stderr.includes('Permission denied') || /Permission denied|No such file/.test(result.stdout)) {
2018
- return { pid, modules: [], error: `cannot read /proc/${pid}/maps (need root, or a debuggable app via run-as); output: ${(result.stdout || result.stderr).slice(0, 200)}` }
2019
- }
2020
- const seen = new Set()
2021
- const modules = []
2022
- for (const line of (result.stdout ?? '').split(/\r?\n/)) {
2023
- const m = /^([0-9a-f]+)-([0-9a-f]+)\s+([rwxps-]{4})\s+[0-9a-f]+\s+\S+\s+\d+\s+(.+)$/.exec(line)
2024
- if (m === null) continue
2025
- const path = m[4]
2026
- if (!path.startsWith('/')) continue
2027
- if (seen.has(path)) continue
2028
- seen.add(path)
2029
- modules.push({ base: m[1], end: m[2], perms: m[3], path })
2056
+ try {
2057
+ const modules = await listModules(engine, serial, pid)
2058
+ return { pid, modules }
2059
+ } catch (error) {
2060
+ return { pid, modules: [], error: error instanceof Error ? error.message : String(error) }
2030
2061
  }
2031
- return { pid, modules }
2032
2062
  },
2033
2063
  })
2034
2064
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@windypro-rourou/dsh-logcat",
3
3
  "description": "Android Logcat viewer for the dsh web GUI: auto-connects to any adb device in debug mode, live logcat stream with level/keyword filters, pause/clear/export, plus agent tools (logcat_recent). Hot-pluggable — mounted via ~/.dsh/cordis.patch.yml + a profile node_modules copy, no dsh source changes.",
4
- "version": "0.6.1",
4
+ "version": "0.6.3",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@11.22.0",
7
7
  "engines": {
@@ -38,7 +38,8 @@
38
38
  "files": [
39
39
  "lib/**/*.js",
40
40
  "cordis.patch.yml",
41
- "README.md"
41
+ "README.md",
42
+ "README.en.md"
42
43
  ],
43
44
  "license": "Apache-2.0",
44
45
  "repository": {