dsh-adb 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 xiabing7
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,68 @@
1
+ # dsh-adb
2
+
3
+ > ADB 设备·台架运维工具集 for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)
4
+
5
+ 让 DSH agent 直接操作 Android 设备 / 车机台架:设备发现、结构化 logcat、apk 安装、文件 pull/push、性能快照。面向实车与台架联调场景,业务内通用(不限 Unity、不限具体车机协议)。
6
+
7
+ ## 安装
8
+
9
+ ```sh
10
+ dsh plugin --profile web add dsh-adb
11
+ ```
12
+
13
+ > v0.1 发布前可从 GitHub 直装:`dsh plugin --profile web add github:SamXiaBing/dsh-adb`
14
+
15
+ ## 生态收录
16
+
17
+ - ✅ [awesome-deepseek-harness#87](https://github.com/0xsline/awesome-deepseek-harness/pull/87) — **已合并**(2026-08-14)
18
+ - ✅ [awesome-dsh-plugin#85](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/pull/85) — **已合并**(2026-08-14)
19
+ - ⏳ [awesome-DSH-plugin#29](https://github.com/Alex-Yanggg/awesome-DSH-plugin/pull/29) — 待合并
20
+
21
+ Topics:`dsh-plugin` `dsh` `adb` `android` `automotive` `bench`
22
+
23
+ ## 工具
24
+
25
+ | 工具 | 说明 |
26
+ | --- | --- |
27
+ | `adb_devices` | 列出设备(serial/state/product/model),先发现再操作 |
28
+ | `adb_connect` / `adb_disconnect` | 无线台架连接(host:port,默认 5555) |
29
+ | `adb_logcat` | 过滤读取(tag/级别/关键字/时间窗/tail);`run_in_background` 后台连续采集,job_output 读增量、job_kill 停止 |
30
+ | `adb_install` | 安装 apk(-r/-d/-g 选项),校验本地文件存在 |
31
+ | `adb_file` | pull / push / ls / rm,设备隔离 |
32
+ | `adb_perf_snapshot` | `dumpsys meminfo / gfxinfo / battery` 结构化快照(PSS/帧率百分位/卡顿率/电量) |
33
+
34
+ 错误码:`ADB_NOT_FOUND`、`ADB_UNAVAILABLE`、`DEVICE_NOT_FOUND`、`NO_DEVICES`、`CONNECT_FAILED`、`INSTALL_FAILED`、`ADB_EXIT_<code>` 等,均为结构化 `AdbError`。
35
+
36
+ ## 配置
37
+
38
+ `cordis.patch.yml` 的 `config` 块(或 profile patch):
39
+
40
+ ```yaml
41
+ - id: dsh-adb
42
+ name: dsh-adb
43
+ config:
44
+ adbPath: C:\Users\me\AppData\Local\Android\Sdk\platform-tools\adb.exe
45
+ defaultSerial: emulator-5554
46
+ timeoutMs: 30000
47
+ ```
48
+
49
+ | 键 | 说明 | 默认 |
50
+ | --- | --- | --- |
51
+ | `adbPath` | adb 可执行文件绝对路径 | 自动探测 PATH / ANDROID_HOME / ANDROID_SDK_ROOT/platform-tools |
52
+ | `defaultSerial` | 默认设备 serial | 无 |
53
+ | `timeoutMs` | 命令超时 | 30000 |
54
+
55
+ ## 开发
56
+
57
+ ```sh
58
+ npm install # 本机 NODE_ENV=production 时加 --include=dev
59
+ npm run build # tsc → lib/
60
+ npm test # 解析器/错误分类单测(node --test)
61
+ npm pack --dry-run # 校验发布包内容(lib/ + cordis.patch.yml)
62
+ ```
63
+
64
+ 注意:本机若设了 `NODE_ENV=production`,npm 会跳过 devDependencies,安装时用 `npm install --include=dev`。
65
+
66
+ ## License
67
+
68
+ MIT
@@ -0,0 +1,3 @@
1
+ - insert:
2
+ - id: dsh-adb
3
+ name: dsh-adb
package/lib/adb.d.ts ADDED
@@ -0,0 +1,48 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import type { ToolOutputDefinition } from '@deepseek-ai/dsh-tools';
3
+ /**
4
+ * dsh-adb execution core: locate the adb executable, run one adb command
5
+ * through the host `subprocess` service, and normalize failures into
6
+ * structured {@link AdbError} codes. No shell layer: every argument is an
7
+ * unquoted argv element.
8
+ */
9
+ export interface AdbConfig {
10
+ /** Absolute path to the adb executable. */
11
+ adbPath?: string;
12
+ /** Default target device serial used when a tool omits `serial`. */
13
+ defaultSerial?: string;
14
+ /** Per-command timeout in milliseconds. */
15
+ timeoutMs?: number;
16
+ }
17
+ export declare class AdbError extends Error {
18
+ readonly code: string;
19
+ constructor(code: string, message: string, options?: {
20
+ cause?: unknown;
21
+ });
22
+ }
23
+ /** Shared tool `output` declaration: permissive schema + pretty-printed JSON rendering. */
24
+ export declare function jsonOutput(): ToolOutputDefinition;
25
+ /**
26
+ * Resolve the adb executable: configured `adbPath` (must be absolute), then
27
+ * the provider's PATH, then ANDROID_HOME / ANDROID_SDK_ROOT platform-tools.
28
+ */
29
+ export declare function resolveAdb(ctx: Context, cfg: AdbConfig, signal?: AbortSignal): Promise<string>;
30
+ export interface AdbRunOptions {
31
+ /** Target device serial; defaults to the plugin `defaultSerial`. */
32
+ serial?: string;
33
+ /** Caller-owned cancellation (the tool `exec.signal`). */
34
+ signal?: AbortSignal;
35
+ timeoutMs?: number;
36
+ maxBytes?: number;
37
+ }
38
+ export interface AdbRunResult {
39
+ stdout: string;
40
+ stdoutTruncated: boolean;
41
+ stderr: string;
42
+ stderrTruncated: boolean;
43
+ exitCode: number;
44
+ }
45
+ /** Run one adb command to completion with collected stdout/stderr. */
46
+ export declare function runAdb(ctx: Context, cfg: AdbConfig, argv: readonly string[], options?: AdbRunOptions): Promise<AdbRunResult>;
47
+ /** Map a non-zero adb exit to a structured {@link AdbError}. */
48
+ export declare function classifyFailure(result: AdbRunResult): AdbError;
package/lib/adb.js ADDED
@@ -0,0 +1,144 @@
1
+ import { isAbsolute, join } from 'node:path';
2
+ export class AdbError extends Error {
3
+ code;
4
+ constructor(code, message, options) {
5
+ super(message, options);
6
+ this.name = 'AdbError';
7
+ this.code = code;
8
+ }
9
+ }
10
+ const DEFAULT_TIMEOUT_MS = 30_000;
11
+ const DEFAULT_STDOUT_MAX_BYTES = 8 * 1024 * 1024;
12
+ const DEFAULT_STDERR_MAX_BYTES = 1 * 1024 * 1024;
13
+ /** Shared tool `output` declaration: permissive schema + pretty-printed JSON rendering. */
14
+ export function jsonOutput() {
15
+ return {
16
+ schema: {},
17
+ render: (_args, value) => [
18
+ { type: 'text', text: JSON.stringify(value, null, 2) },
19
+ ],
20
+ };
21
+ }
22
+ function requireSubprocess(ctx) {
23
+ const subprocess = ctx.get('subprocess');
24
+ if (subprocess === undefined) {
25
+ throw new AdbError('ADB_UNAVAILABLE', 'subprocess service is not available: load @deepseek-ai/dsh-subprocess (e.g. dsh-subprocess-local) in the composition');
26
+ }
27
+ return subprocess;
28
+ }
29
+ function exeName(base) {
30
+ return process.platform === 'win32' ? `${base}.exe` : base;
31
+ }
32
+ function platformToolsCandidates() {
33
+ const names = [];
34
+ for (const root of [process.env.ANDROID_HOME, process.env.ANDROID_SDK_ROOT]) {
35
+ if (root !== undefined && root !== '') {
36
+ names.push(join(root, 'platform-tools', exeName('adb')));
37
+ }
38
+ }
39
+ return names;
40
+ }
41
+ /**
42
+ * Resolve the adb executable: configured `adbPath` (must be absolute), then
43
+ * the provider's PATH, then ANDROID_HOME / ANDROID_SDK_ROOT platform-tools.
44
+ */
45
+ export async function resolveAdb(ctx, cfg, signal) {
46
+ const subprocess = requireSubprocess(ctx);
47
+ if (cfg.adbPath !== undefined && !isAbsolute(cfg.adbPath)) {
48
+ throw new AdbError('ADB_CONFIG_INVALID', `adbPath must be an absolute path, got: ${cfg.adbPath}`);
49
+ }
50
+ const candidates = [...(cfg.adbPath !== undefined ? [cfg.adbPath] : []), 'adb', ...platformToolsCandidates()];
51
+ let lastCause;
52
+ for (const candidate of candidates) {
53
+ try {
54
+ const resolved = await subprocess.resolveExecutable(candidate, undefined, signal);
55
+ if (resolved !== '')
56
+ return resolved;
57
+ }
58
+ catch (error) {
59
+ lastCause = error;
60
+ }
61
+ }
62
+ throw new AdbError('ADB_NOT_FOUND', 'adb executable not found. Set `adbPath` in the dsh-adb plugin config to the absolute adb path, or install Android platform-tools and expose them via PATH / ANDROID_HOME / ANDROID_SDK_ROOT.', { cause: lastCause });
63
+ }
64
+ /** Run one adb command to completion with collected stdout/stderr. */
65
+ export async function runAdb(ctx, cfg, argv, options = {}) {
66
+ const adb = await resolveAdb(ctx, cfg, options.signal);
67
+ const serial = options.serial ?? cfg.defaultSerial;
68
+ const args = serial === undefined ? argv : ['-s', serial, ...argv];
69
+ const timeoutMs = options.timeoutMs ?? cfg.timeoutMs ?? DEFAULT_TIMEOUT_MS;
70
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
71
+ const signal = options.signal === undefined ? timeoutSignal : AbortSignal.any([options.signal, timeoutSignal]);
72
+ let handle;
73
+ try {
74
+ handle = requireSubprocess(ctx).spawn({
75
+ argv: [adb, ...args],
76
+ cwd: process.cwd(),
77
+ stdio: {
78
+ stdin: 'ignore',
79
+ stdout: { maxBytes: options.maxBytes ?? DEFAULT_STDOUT_MAX_BYTES },
80
+ stderr: { maxBytes: DEFAULT_STDERR_MAX_BYTES },
81
+ },
82
+ graceMs: 3000,
83
+ signal,
84
+ });
85
+ }
86
+ catch (error) {
87
+ throw new AdbError('ADB_LAUNCH_FAILED', `failed to start adb process: ${describe(error)}`, { cause: error });
88
+ }
89
+ let outcome;
90
+ try {
91
+ outcome = await handle.done;
92
+ }
93
+ catch (error) {
94
+ throw new AdbError('ADB_LAUNCH_FAILED', `adb process could not start: ${describe(error)}`, { cause: error });
95
+ }
96
+ const stdout = handle.collected.stdout?.readFrom(0);
97
+ const stderr = handle.collected.stderr?.readFrom(0);
98
+ const stdoutText = stdout?.text ?? '';
99
+ const stderrText = stderr?.text ?? '';
100
+ if (options.signal?.aborted) {
101
+ const error = new Error('tool call aborted');
102
+ error.name = 'AbortError';
103
+ throw error;
104
+ }
105
+ if (outcome.signal !== null || outcome.exitCode === null) {
106
+ throw new AdbError('ADB_KILLED', `adb was killed by signal ${outcome.signal ?? '(unknown)'}`);
107
+ }
108
+ return {
109
+ stdout: stdoutText,
110
+ stdoutTruncated: stdout?.lossy ?? false,
111
+ stderr: stderrText,
112
+ stderrTruncated: stderr?.lossy ?? false,
113
+ exitCode: outcome.exitCode,
114
+ };
115
+ }
116
+ /** Map a non-zero adb exit to a structured {@link AdbError}. */
117
+ export function classifyFailure(result) {
118
+ const stderr = result.stderr;
119
+ const stdout = result.stdout;
120
+ if (/error: device .* not found/i.test(stderr)) {
121
+ return new AdbError('DEVICE_NOT_FOUND', excerpt(stderr));
122
+ }
123
+ if (/no devices\/emulators found/i.test(stderr)) {
124
+ return new AdbError('NO_DEVICES', 'no connected devices/emulators; run adb_devices first, or connect one via adb_connect');
125
+ }
126
+ if (/failed to connect|cannot connect|connection refused/i.test(stderr)) {
127
+ return new AdbError('CONNECT_FAILED', excerpt(stderr));
128
+ }
129
+ if (/error: closed/i.test(stderr)) {
130
+ return new AdbError('ADB_DEVICE_CLOSED', 'adb device connection closed (device unplugged or crashed)');
131
+ }
132
+ const failure = /Failure \[([^\]]+)\]/.exec(`${stdout}\n${stderr}`);
133
+ if (failure !== null) {
134
+ return new AdbError('INSTALL_FAILED', `install failed: ${failure[1]}`);
135
+ }
136
+ return new AdbError(`ADB_EXIT_${result.exitCode}`, `adb exited with code ${result.exitCode}: ${excerpt(stderr || stdout)}`);
137
+ }
138
+ function excerpt(text, max = 500) {
139
+ const trimmed = text.trim();
140
+ return trimmed.length <= max ? trimmed : `${trimmed.slice(0, max)}…`;
141
+ }
142
+ function describe(error) {
143
+ return error instanceof Error ? error.message : String(error);
144
+ }
package/lib/index.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ import Schema from 'schemastery';
2
+ import type { Context } from '@deepseek-ai/cordis';
3
+ export declare const name = "dsh-adb";
4
+ /** Plugin config (all optional — `Config` supplies the defaults). */
5
+ export interface Config {
6
+ /** Absolute path to the adb executable. */
7
+ adbPath?: string;
8
+ /** Default target device serial used when a tool omits `serial`. */
9
+ defaultSerial?: string;
10
+ /** Per-command timeout in milliseconds. */
11
+ timeoutMs?: number;
12
+ }
13
+ export declare const Config: Schema<Config>;
14
+ export declare function apply(ctx: Context, config: Config): void;
package/lib/index.js ADDED
@@ -0,0 +1,21 @@
1
+ import Schema from 'schemastery';
2
+ import { registerDeviceTools } from './tools/devices.js';
3
+ import { registerFileTool } from './tools/file.js';
4
+ import { registerInstallTool } from './tools/install.js';
5
+ import { registerLogcatTool } from './tools/logcat.js';
6
+ import { registerPerfTool } from './tools/perf.js';
7
+ export const name = 'dsh-adb';
8
+ export const Config = Schema.object({
9
+ adbPath: Schema.string().description('adb 可执行文件绝对路径;缺省自动探测 PATH / ANDROID_HOME / ANDROID_SDK_ROOT / platform-tools'),
10
+ defaultSerial: Schema.string().description('默认目标设备 serial'),
11
+ timeoutMs: Schema.number().default(30000).description('adb 命令超时(毫秒)'),
12
+ });
13
+ export function apply(ctx, config) {
14
+ const cfg = config;
15
+ registerDeviceTools(ctx, cfg);
16
+ registerInstallTool(ctx, cfg);
17
+ registerFileTool(ctx, cfg);
18
+ registerLogcatTool(ctx, cfg);
19
+ registerPerfTool(ctx, cfg);
20
+ ctx.logger.info('[dsh-adb] loaded: adb_devices / adb_connect / adb_disconnect / adb_logcat / adb_install / adb_file / adb_perf_snapshot');
21
+ }
@@ -0,0 +1,10 @@
1
+ /** Parser for `adb devices -l` output. */
2
+ export interface AdbDevice {
3
+ serial: string;
4
+ state: string;
5
+ product?: string;
6
+ model?: string;
7
+ device?: string;
8
+ transportId?: string;
9
+ }
10
+ export declare function parseDevices(text: string): AdbDevice[];
@@ -0,0 +1,32 @@
1
+ /** Parser for `adb devices -l` output. */
2
+ export function parseDevices(text) {
3
+ const devices = [];
4
+ for (const rawLine of text.split(/\r?\n/)) {
5
+ const line = rawLine.trim();
6
+ if (line === '' || line === 'List of devices attached')
7
+ continue;
8
+ const parts = line.split(/\s+/);
9
+ const serial = parts[0];
10
+ const state = parts[1];
11
+ if (serial === undefined || state === undefined)
12
+ continue;
13
+ const device = { serial, state };
14
+ for (const part of parts.slice(2)) {
15
+ const eq = part.indexOf(':');
16
+ if (eq === -1)
17
+ continue;
18
+ const key = part.slice(0, eq);
19
+ const value = part.slice(eq + 1);
20
+ if (key === 'product')
21
+ device.product = value;
22
+ else if (key === 'model')
23
+ device.model = value;
24
+ else if (key === 'device')
25
+ device.device = value;
26
+ else if (key === 'transport_id')
27
+ device.transportId = value;
28
+ }
29
+ devices.push(device);
30
+ }
31
+ return devices;
32
+ }
@@ -0,0 +1,14 @@
1
+ /** Parser for `adb logcat -v threadtime` output. */
2
+ export type LogLevel = 'V' | 'D' | 'I' | 'W' | 'E' | 'F';
3
+ export interface LogcatEntry {
4
+ time: string;
5
+ pid: string;
6
+ tid: string;
7
+ level: LogLevel;
8
+ tag: string;
9
+ message: string;
10
+ }
11
+ /** Parse threadtime lines; unrecognized lines are skipped (headers, sections). */
12
+ export declare function parseLogcat(text: string): LogcatEntry[];
13
+ export declare function matchesLevel(entry: LogcatEntry, minimum: LogLevel): boolean;
14
+ export declare function matchesKeyword(entry: LogcatEntry, keyword: string): boolean;
@@ -0,0 +1,27 @@
1
+ /** Parser for `adb logcat -v threadtime` output. */
2
+ const THREADTIME_LINE = /^(\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})\s+(\d+)\s+(\d+)\s+([VDIWEF])\s+([^:]+):\s?(.*)$/;
3
+ /** Parse threadtime lines; unrecognized lines are skipped (headers, sections). */
4
+ export function parseLogcat(text) {
5
+ const entries = [];
6
+ for (const rawLine of text.split(/\r?\n/)) {
7
+ const match = THREADTIME_LINE.exec(rawLine);
8
+ if (match === null)
9
+ continue;
10
+ entries.push({
11
+ time: match[1],
12
+ pid: match[2],
13
+ tid: match[3],
14
+ level: match[4],
15
+ tag: match[5],
16
+ message: match[6] ?? '',
17
+ });
18
+ }
19
+ return entries;
20
+ }
21
+ const LEVEL_ORDER = { V: 0, D: 1, I: 2, W: 3, E: 4, F: 5 };
22
+ export function matchesLevel(entry, minimum) {
23
+ return LEVEL_ORDER[entry.level] >= LEVEL_ORDER[minimum];
24
+ }
25
+ export function matchesKeyword(entry, keyword) {
26
+ return entry.message.includes(keyword) || entry.tag.includes(keyword);
27
+ }
@@ -0,0 +1,26 @@
1
+ /** Parsers for `dumpsys` performance snapshots (meminfo / gfxinfo / battery). */
2
+ export interface MeminfoSummary {
3
+ totalPssKb?: number;
4
+ totalRssKb?: number;
5
+ javaHeapKb?: number;
6
+ nativeHeapKb?: number;
7
+ graphicsKb?: number;
8
+ }
9
+ export interface GfxinfoSummary {
10
+ totalFrames?: number;
11
+ jankyFrames?: number;
12
+ jankyPercent?: number;
13
+ percentile50Ms?: number;
14
+ percentile90Ms?: number;
15
+ percentile95Ms?: number;
16
+ percentile99Ms?: number;
17
+ missedVsync?: number;
18
+ }
19
+ export interface BatterySummary {
20
+ levelPercent?: number;
21
+ status?: string;
22
+ temperatureC?: number;
23
+ }
24
+ export declare function parseMeminfo(text: string): MeminfoSummary;
25
+ export declare function parseGfxinfo(text: string): GfxinfoSummary;
26
+ export declare function parseBattery(text: string): BatterySummary;
@@ -0,0 +1,67 @@
1
+ /** Parsers for `dumpsys` performance snapshots (meminfo / gfxinfo / battery). */
2
+ export function parseMeminfo(text) {
3
+ const summary = {};
4
+ const total = /TOTAL PSS:\s+(\d+)\s+TOTAL RSS:\s+(\d+)/.exec(text);
5
+ if (total !== null) {
6
+ summary.totalPssKb = Number(total[1]);
7
+ summary.totalRssKb = Number(total[2]);
8
+ }
9
+ const byName = {
10
+ 'Java Heap': 'javaHeapKb',
11
+ 'Native Heap': 'nativeHeapKb',
12
+ Graphics: 'graphicsKb',
13
+ };
14
+ for (const [name, key] of Object.entries(byName)) {
15
+ const match = new RegExp(`^\\s+${name}:\\s+(\\d+)`, 'm').exec(text);
16
+ if (match !== null)
17
+ summary[key] = Number(match[1]);
18
+ }
19
+ return summary;
20
+ }
21
+ export function parseGfxinfo(text) {
22
+ const summary = {};
23
+ const total = /Total frames rendered:\s+(\d+)/.exec(text);
24
+ if (total !== null)
25
+ summary.totalFrames = Number(total[1]);
26
+ const janky = /Janky frames:\s+(\d+)\s*\(([\d.]+)%\)/.exec(text);
27
+ if (janky !== null) {
28
+ summary.jankyFrames = Number(janky[1]);
29
+ summary.jankyPercent = Number(janky[2]);
30
+ }
31
+ const percentiles = [
32
+ [/50th percentile:\s+(\d+)ms/, 'percentile50Ms'],
33
+ [/90th percentile:\s+(\d+)ms/, 'percentile90Ms'],
34
+ [/95th percentile:\s+(\d+)ms/, 'percentile95Ms'],
35
+ [/99th percentile:\s+(\d+)ms/, 'percentile99Ms'],
36
+ ];
37
+ for (const [pattern, key] of percentiles) {
38
+ const match = pattern.exec(text);
39
+ if (match !== null)
40
+ summary[key] = Number(match[1]);
41
+ }
42
+ const missed = /Number Missed Vsync:\s+(\d+)/.exec(text);
43
+ if (missed !== null)
44
+ summary.missedVsync = Number(missed[1]);
45
+ return summary;
46
+ }
47
+ const BATTERY_STATUS = {
48
+ '1': 'unknown',
49
+ '2': 'charging',
50
+ '3': 'discharging',
51
+ '4': 'not-charging',
52
+ '5': 'full',
53
+ };
54
+ export function parseBattery(text) {
55
+ const summary = {};
56
+ const level = /level:\s+(\d+)/.exec(text);
57
+ if (level !== null)
58
+ summary.levelPercent = Number(level[1]);
59
+ const status = /status:\s+(\d+)/.exec(text);
60
+ if (status !== null)
61
+ summary.status = BATTERY_STATUS[status[1]] ?? status[1];
62
+ // temperature is reported in tenths of a degree Celsius
63
+ const temperature = /temperature:\s+(\d+)/.exec(text);
64
+ if (temperature !== null)
65
+ summary.temperatureC = Number(temperature[1]) / 10;
66
+ return summary;
67
+ }
@@ -0,0 +1,4 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import { type AdbConfig } from '../adb.js';
3
+ /** adb_devices / adb_connect / adb_disconnect. */
4
+ export declare function registerDeviceTools(ctx: Context, cfg: AdbConfig): void;
@@ -0,0 +1,61 @@
1
+ import { AdbError, classifyFailure, jsonOutput, runAdb } from '../adb.js';
2
+ import { parseDevices } from '../parsers/devices.js';
3
+ /** adb_devices / adb_connect / adb_disconnect. */
4
+ export function registerDeviceTools(ctx, cfg) {
5
+ ctx.tools.register({
6
+ name: 'adb_devices',
7
+ description: 'List connected Android devices (serial, state, product, model) visible to the adb server. Run this first to discover device serials before other adb_* tools.',
8
+ parameters: { type: 'object', properties: {}, additionalProperties: false },
9
+ output: jsonOutput(),
10
+ async execute(_args, exec) {
11
+ const result = await runAdb(ctx, cfg, ['devices', '-l'], { signal: exec.signal });
12
+ if (result.exitCode !== 0)
13
+ throw classifyFailure(result);
14
+ return { server: 'ok', devices: parseDevices(result.stdout) };
15
+ },
16
+ });
17
+ ctx.tools.register({
18
+ name: 'adb_connect',
19
+ description: 'Connect to a bench/device over TCP/IP (adb connect host:port). Wireless bench devices must be reachable from this machine.',
20
+ parameters: {
21
+ type: 'object',
22
+ additionalProperties: false,
23
+ required: ['host'],
24
+ properties: {
25
+ host: { type: 'string', description: 'Device IP address or hostname, e.g. 192.168.1.100.' },
26
+ port: { type: 'integer', description: 'TCP port; defaults to 5555.' },
27
+ },
28
+ },
29
+ output: jsonOutput(),
30
+ async execute(args, exec) {
31
+ const target = `${args.host}:${args.port ?? 5555}`;
32
+ const result = await runAdb(ctx, cfg, ['connect', target], { signal: exec.signal });
33
+ if (result.exitCode !== 0)
34
+ throw classifyFailure(result);
35
+ const message = result.stdout.trim();
36
+ if (/failed to connect|cannot connect/i.test(message)) {
37
+ throw new AdbError('CONNECT_FAILED', message);
38
+ }
39
+ return { target, connected: /connected|already connected/i.test(message), message };
40
+ },
41
+ });
42
+ ctx.tools.register({
43
+ name: 'adb_disconnect',
44
+ description: 'Disconnect a TCP/IP device (adb disconnect [host:port]). Omit host to disconnect every wireless device.',
45
+ parameters: {
46
+ type: 'object',
47
+ additionalProperties: false,
48
+ properties: {
49
+ host: { type: 'string', description: 'Host or host:port to disconnect; omit to disconnect all.' },
50
+ },
51
+ },
52
+ output: jsonOutput(),
53
+ async execute(args, exec) {
54
+ const argv = ['disconnect', ...(args.host !== undefined ? [args.host] : [])];
55
+ const result = await runAdb(ctx, cfg, argv, { signal: exec.signal });
56
+ if (result.exitCode !== 0)
57
+ throw classifyFailure(result);
58
+ return { message: result.stdout.trim() };
59
+ },
60
+ });
61
+ }
@@ -0,0 +1,4 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import { type AdbConfig } from '../adb.js';
3
+ /** adb_file: pull / push / ls / rm on the target device. */
4
+ export declare function registerFileTool(ctx: Context, cfg: AdbConfig): void;
@@ -0,0 +1,58 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { AdbError, classifyFailure, jsonOutput, runAdb } from '../adb.js';
3
+ /** adb_file: pull / push / ls / rm on the target device. */
4
+ export function registerFileTool(ctx, cfg) {
5
+ ctx.tools.register({
6
+ name: 'adb_file',
7
+ description: 'Transfer or inspect files on an Android device: pull (device -> local), push (local -> device), ls (list a device directory), rm (delete a device file/dir).',
8
+ parameters: {
9
+ type: 'object',
10
+ additionalProperties: false,
11
+ required: ['operation', 'devicePath'],
12
+ properties: {
13
+ operation: {
14
+ type: 'string',
15
+ enum: ['pull', 'push', 'ls', 'rm'],
16
+ description: 'What to do: pull copies devicePath to localPath; push copies localPath to devicePath; ls lists devicePath; rm deletes devicePath.',
17
+ },
18
+ devicePath: { type: 'string', description: 'Path on the device (for pull/ls/rm) or the destination (for push).' },
19
+ localPath: { type: 'string', description: 'Local path: destination for pull, source for push.' },
20
+ serial: { type: 'string', description: 'Target device serial; defaults to the plugin defaultSerial.' },
21
+ recursive: { type: 'boolean', description: 'Recursive for ls (long format) or rm (delete directory trees).' },
22
+ },
23
+ },
24
+ output: jsonOutput(),
25
+ async execute(args, exec) {
26
+ if (args.operation === 'push') {
27
+ if (args.localPath === undefined) {
28
+ throw new AdbError('ARGS_INVALID', 'push requires localPath (the source file)');
29
+ }
30
+ if (!existsSync(args.localPath)) {
31
+ throw new AdbError('LOCAL_FILE_NOT_FOUND', `local file not found: ${args.localPath}`);
32
+ }
33
+ }
34
+ const argv = buildArgv(args);
35
+ const result = await runAdb(ctx, cfg, argv, { signal: exec.signal, serial: args.serial });
36
+ if (result.exitCode !== 0)
37
+ throw classifyFailure(result);
38
+ const text = result.stdout.trim();
39
+ if (args.operation === 'ls') {
40
+ const lines = text.split(/\r?\n/).filter((line) => line.trim() !== '');
41
+ return { operation: args.operation, devicePath: args.devicePath, entries: lines };
42
+ }
43
+ return { operation: args.operation, devicePath: args.devicePath, ...(args.localPath !== undefined ? { localPath: args.localPath } : {}), message: text };
44
+ },
45
+ });
46
+ }
47
+ function buildArgv(args) {
48
+ if (args.operation === 'pull') {
49
+ return ['pull', args.devicePath, args.localPath ?? '.'];
50
+ }
51
+ if (args.operation === 'push') {
52
+ return ['push', args.localPath, args.devicePath];
53
+ }
54
+ if (args.operation === 'ls') {
55
+ return ['shell', 'ls', ...(args.recursive === true ? ['-lR'] : ['-l']), args.devicePath];
56
+ }
57
+ return ['shell', 'rm', ...(args.recursive === true ? ['-rf'] : ['-f']), args.devicePath];
58
+ }
@@ -0,0 +1,4 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import { type AdbConfig } from '../adb.js';
3
+ /** adb_install: push and install an apk on the target device. */
4
+ export declare function registerInstallTool(ctx: Context, cfg: AdbConfig): void;
@@ -0,0 +1,40 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { AdbError, classifyFailure, jsonOutput, runAdb } from '../adb.js';
3
+ /** adb_install: push and install an apk on the target device. */
4
+ export function registerInstallTool(ctx, cfg) {
5
+ ctx.tools.register({
6
+ name: 'adb_install',
7
+ description: 'Install an APK on the target Android device (adb install). Requires a local path to the apk file.',
8
+ parameters: {
9
+ type: 'object',
10
+ additionalProperties: false,
11
+ required: ['apk'],
12
+ properties: {
13
+ apk: { type: 'string', description: 'Local absolute path to the .apk file.' },
14
+ serial: { type: 'string', description: 'Target device serial; defaults to the plugin defaultSerial.' },
15
+ reinstall: { type: 'boolean', description: 'Reinstall an existing app, keeping its data (-r).' },
16
+ downgrade: { type: 'boolean', description: 'Allow version downgrade (-d).' },
17
+ grantPermissions: { type: 'boolean', description: 'Grant all runtime permissions (for targetSdk >= 23, -g).' },
18
+ },
19
+ },
20
+ output: jsonOutput(),
21
+ async execute(args, exec) {
22
+ if (!existsSync(args.apk)) {
23
+ throw new AdbError('LOCAL_FILE_NOT_FOUND', `apk not found: ${args.apk}`);
24
+ }
25
+ const flags = [
26
+ ...(args.reinstall === true ? ['-r'] : []),
27
+ ...(args.downgrade === true ? ['-d'] : []),
28
+ ...(args.grantPermissions === true ? ['-g'] : []),
29
+ ];
30
+ const result = await runAdb(ctx, cfg, ['install', ...flags, args.apk], {
31
+ signal: exec.signal,
32
+ serial: args.serial,
33
+ });
34
+ if (result.exitCode !== 0)
35
+ throw classifyFailure(result);
36
+ const output = result.stdout.trim();
37
+ return { installed: output.includes('Success'), package: args.apk, message: output };
38
+ },
39
+ });
40
+ }
@@ -0,0 +1,4 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import { type AdbConfig } from '../adb.js';
3
+ /** adb_logcat: foreground buffer dump with filters, or a continuous background stream job. */
4
+ export declare function registerLogcatTool(ctx: Context, cfg: AdbConfig): void;
@@ -0,0 +1,131 @@
1
+ import { AdbError, classifyFailure, jsonOutput, resolveAdb, runAdb } from '../adb.js';
2
+ import { matchesKeyword, matchesLevel, parseLogcat } from '../parsers/logcat.js';
3
+ const MAX_ENTRIES = 500;
4
+ const FOREGROUND_MAX_BYTES = 16 * 1024 * 1024;
5
+ const BACKGROUND_MAX_BYTES = 64 * 1024 * 1024;
6
+ /** adb_logcat: foreground buffer dump with filters, or a continuous background stream job. */
7
+ export function registerLogcatTool(ctx, cfg) {
8
+ ctx.tools.register({
9
+ name: 'adb_logcat',
10
+ description: 'Read or stream the Android logcat. Foreground: dump the current buffer (optionally last `tail` entries) filtered by tag/level/keyword/since/until. Background: stream new entries continuously as a background job — the call returns a job id, read deltas with job_output and stop with job_kill.',
11
+ parameters: {
12
+ type: 'object',
13
+ additionalProperties: false,
14
+ properties: {
15
+ serial: { type: 'string', description: 'Target device serial; defaults to the plugin defaultSerial.' },
16
+ tag: { type: 'string', description: 'Only entries whose tag equals this value.' },
17
+ level: { type: 'string', enum: ['V', 'D', 'I', 'W', 'E', 'F'], description: 'Minimum log level (V < D < I < W < E < F).' },
18
+ keyword: { type: 'string', description: 'Only entries whose tag or message contains this keyword.' },
19
+ tail: { type: 'integer', description: 'Foreground: return only the last N entries.' },
20
+ since: { type: 'string', description: 'Foreground: only entries at/after this time, e.g. "08-14 10:00:00.000".' },
21
+ until: { type: 'string', description: 'Foreground: only entries at/before this time, e.g. "08-14 10:30:00.000".' },
22
+ run_in_background: { type: 'boolean', description: 'Stream as a background job and return a job id immediately.' },
23
+ },
24
+ },
25
+ output: jsonOutput(),
26
+ async execute(args, exec) {
27
+ if (args.run_in_background === true) {
28
+ return startBackgroundLogcat(ctx, cfg, args, exec);
29
+ }
30
+ const result = await runAdb(ctx, cfg, ['logcat', '-v', 'threadtime', '-d'], {
31
+ signal: exec.signal,
32
+ maxBytes: FOREGROUND_MAX_BYTES,
33
+ serial: args.serial,
34
+ });
35
+ if (result.exitCode !== 0)
36
+ throw classifyFailure(result);
37
+ const { since, until, tag, level, keyword, tail } = args;
38
+ let entries = parseLogcat(result.stdout);
39
+ // threadtime timestamps ("MM-DD HH:MM:SS.mmm") sort lexicographically.
40
+ if (since !== undefined)
41
+ entries = entries.filter((entry) => entry.time >= since);
42
+ if (until !== undefined)
43
+ entries = entries.filter((entry) => entry.time <= until);
44
+ if (tag !== undefined)
45
+ entries = entries.filter((entry) => entry.tag === tag);
46
+ if (level !== undefined)
47
+ entries = entries.filter((entry) => matchesLevel(entry, level));
48
+ if (keyword !== undefined)
49
+ entries = entries.filter((entry) => matchesKeyword(entry, keyword));
50
+ if (tail !== undefined && tail >= 0)
51
+ entries = entries.slice(-tail);
52
+ const truncated = entries.length > MAX_ENTRIES;
53
+ const returned = truncated ? entries.slice(-MAX_ENTRIES) : entries;
54
+ return {
55
+ total: entries.length,
56
+ truncated,
57
+ entries: returned.map((entry) => ({
58
+ time: entry.time,
59
+ pid: entry.pid,
60
+ tid: entry.tid,
61
+ level: entry.level,
62
+ tag: entry.tag,
63
+ message: entry.message,
64
+ })),
65
+ };
66
+ },
67
+ });
68
+ }
69
+ function startBackgroundLogcat(ctx, cfg, args, exec) {
70
+ const jobs = ctx.get('jobs');
71
+ if (jobs === undefined) {
72
+ throw new AdbError('JOBS_UNAVAILABLE', 'background jobs unavailable: load @deepseek-ai/dsh-jobs and @deepseek-ai/dsh-tool-jobs');
73
+ }
74
+ const subprocess = ctx.get('subprocess');
75
+ if (subprocess === undefined) {
76
+ throw new AdbError('ADB_UNAVAILABLE', 'subprocess service is not available: load @deepseek-ai/dsh-subprocess (e.g. dsh-subprocess-local) in the composition');
77
+ }
78
+ if (exec.signal.aborted) {
79
+ const error = new Error('tool call aborted');
80
+ error.name = 'AbortError';
81
+ throw error;
82
+ }
83
+ const serial = args.serial ?? cfg.defaultSerial;
84
+ const argv = [
85
+ ...(serial !== undefined ? ['-s', serial] : []),
86
+ 'logcat',
87
+ '-v',
88
+ 'threadtime',
89
+ ...(args.tag !== undefined ? ['-s', `${args.tag}:V`] : []),
90
+ ];
91
+ // Preflight: resolve adb before committing the job, so a broken setup fails
92
+ // the call instead of leaving a dead background job.
93
+ const adbPromise = resolveAdb(ctx, cfg, exec.signal);
94
+ const id = jobs.start({
95
+ kind: 'adb-logcat',
96
+ label: `logcat${args.tag !== undefined ? ` tag=${args.tag}` : ''}`,
97
+ ...(exec.agent !== undefined ? { owner: exec.agent } : {}),
98
+ run: () => {
99
+ // The stream must outlive the tool call, so the spawn carries no exec.signal.
100
+ let handle;
101
+ let cursor = 0;
102
+ return {
103
+ cancel: () => {
104
+ handle?.terminate();
105
+ },
106
+ done: adbPromise.then(async (adb) => {
107
+ handle = subprocess.spawn({
108
+ argv: [adb, ...argv],
109
+ cwd: process.cwd(),
110
+ stdio: {
111
+ stdin: 'ignore',
112
+ stdout: { maxBytes: BACKGROUND_MAX_BYTES },
113
+ stderr: { maxBytes: 1024 * 1024 },
114
+ },
115
+ graceMs: 3000,
116
+ });
117
+ const outcome = await handle.done;
118
+ return { exitCode: outcome.exitCode, signal: outcome.signal };
119
+ }),
120
+ readOutput: () => {
121
+ const read = handle?.collected.stdout?.readFrom(cursor);
122
+ if (read === undefined)
123
+ return { added: 0, text: '' };
124
+ cursor += Buffer.byteLength(read.text, 'utf8');
125
+ return { added: read.text.length, text: read.text };
126
+ },
127
+ };
128
+ },
129
+ });
130
+ return { kind: 'background', jobId: id };
131
+ }
@@ -0,0 +1,4 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import { type AdbConfig } from '../adb.js';
3
+ /** adb_perf_snapshot: dumpsys meminfo / gfxinfo / battery for one app. */
4
+ export declare function registerPerfTool(ctx: Context, cfg: AdbConfig): void;
@@ -0,0 +1,44 @@
1
+ import { classifyFailure, jsonOutput, runAdb } from '../adb.js';
2
+ import { parseBattery, parseGfxinfo, parseMeminfo } from '../parsers/perf.js';
3
+ /** adb_perf_snapshot: dumpsys meminfo / gfxinfo / battery for one app. */
4
+ export function registerPerfTool(ctx, cfg) {
5
+ ctx.tools.register({
6
+ name: 'adb_perf_snapshot',
7
+ description: 'Capture a performance snapshot of one installed app on an Android device: memory (dumpsys meminfo), rendering (dumpsys gfxinfo: janky frames, percentiles), and battery. Use it for bench/regression checks before or after a change.',
8
+ parameters: {
9
+ type: 'object',
10
+ additionalProperties: false,
11
+ required: ['package'],
12
+ properties: {
13
+ package: { type: 'string', description: 'App package id, e.g. com.example.hmi.' },
14
+ serial: { type: 'string', description: 'Target device serial; defaults to the plugin defaultSerial.' },
15
+ metrics: {
16
+ type: 'array',
17
+ items: { type: 'string', enum: ['meminfo', 'gfxinfo', 'battery'] },
18
+ description: 'Which metrics to collect; defaults to all three.',
19
+ },
20
+ },
21
+ },
22
+ output: jsonOutput(),
23
+ async execute(args, exec) {
24
+ const metrics = args.metrics ?? ['meminfo', 'gfxinfo', 'battery'];
25
+ const result = { package: args.package, metrics: [...metrics] };
26
+ for (const metric of metrics) {
27
+ const output = await runAdb(ctx, cfg, ['shell', 'dumpsys', metric, args.package], {
28
+ signal: exec.signal,
29
+ serial: args.serial,
30
+ maxBytes: 4 * 1024 * 1024,
31
+ });
32
+ if (output.exitCode !== 0)
33
+ throw classifyFailure(output);
34
+ if (metric === 'meminfo')
35
+ result.meminfo = parseMeminfo(output.stdout);
36
+ else if (metric === 'gfxinfo')
37
+ result.gfxinfo = parseGfxinfo(output.stdout);
38
+ else
39
+ result.battery = parseBattery(output.stdout);
40
+ }
41
+ return result;
42
+ },
43
+ });
44
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "dsh-adb",
3
+ "version": "0.1.0",
4
+ "description": "ADB device & bench operations for DeepSeek Harness: device discovery, structured logcat, apk install, file pull/push, performance snapshots",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "files": [
8
+ "lib",
9
+ "cordis.patch.yml"
10
+ ],
11
+ "keywords": [
12
+ "dsh",
13
+ "dsh-plugin",
14
+ "adb",
15
+ "android",
16
+ "automotive",
17
+ "bench"
18
+ ],
19
+ "license": "MIT",
20
+ "author": "xiabing7",
21
+ "dsh": {
22
+ "bundle": {
23
+ "patch": "./cordis.patch.yml"
24
+ }
25
+ },
26
+ "scripts": {
27
+ "build": "tsc -p tsconfig.json",
28
+ "typecheck": "tsc --noEmit -p tsconfig.json",
29
+ "test": "npm run build && node --test \"test/*.test.mjs\""
30
+ },
31
+ "dependencies": {
32
+ "schemastery": "^3.13.1"
33
+ },
34
+ "devDependencies": {
35
+ "@deepseek-ai/cordis": "^4.0.1",
36
+ "@deepseek-ai/dsh-jobs": "0.0.1-rc.3",
37
+ "@deepseek-ai/dsh-llm": "0.0.1-rc.1",
38
+ "@deepseek-ai/dsh-subprocess": "0.0.1-rc.1",
39
+ "@deepseek-ai/dsh-tools": "0.0.1-rc.1",
40
+ "@types/node": "^24.0.0",
41
+ "typescript": "^5.7.2"
42
+ }
43
+ }