deepadb 1.1.1 → 1.1.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/LICENSE +202 -21
- package/NOTICE +16 -0
- package/README.md +55 -29
- package/build/bridge/adb-bridge.d.ts +14 -0
- package/build/bridge/adb-bridge.d.ts.map +1 -1
- package/build/bridge/adb-bridge.js +34 -5
- package/build/bridge/adb-bridge.js.map +1 -1
- package/build/bridge/device-manager.d.ts.map +1 -1
- package/build/bridge/device-manager.js +18 -9
- package/build/bridge/device-manager.js.map +1 -1
- package/build/bridge/local-bridge.d.ts +10 -2
- package/build/bridge/local-bridge.d.ts.map +1 -1
- package/build/bridge/local-bridge.js +51 -16
- package/build/bridge/local-bridge.js.map +1 -1
- package/build/config/config.d.ts +7 -0
- package/build/config/config.d.ts.map +1 -1
- package/build/config/config.js +26 -10
- package/build/config/config.js.map +1 -1
- package/build/graphql-api.d.ts.map +1 -1
- package/build/graphql-api.js +73 -3
- package/build/graphql-api.js.map +1 -1
- package/build/http-transport.d.ts.map +1 -1
- package/build/http-transport.js +44 -7
- package/build/http-transport.js.map +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +41 -16
- package/build/index.js.map +1 -1
- package/build/middleware/auth.d.ts.map +1 -1
- package/build/middleware/auth.js +43 -6
- package/build/middleware/auth.js.map +1 -1
- package/build/middleware/chipset.d.ts.map +1 -1
- package/build/middleware/chipset.js +16 -1
- package/build/middleware/chipset.js.map +1 -1
- package/build/middleware/cleanup.d.ts +39 -7
- package/build/middleware/cleanup.d.ts.map +1 -1
- package/build/middleware/cleanup.js +148 -16
- package/build/middleware/cleanup.js.map +1 -1
- package/build/middleware/fetch-utils.d.ts +50 -7
- package/build/middleware/fetch-utils.d.ts.map +1 -1
- package/build/middleware/fetch-utils.js +127 -14
- package/build/middleware/fetch-utils.js.map +1 -1
- package/build/middleware/fs-utils.d.ts +126 -0
- package/build/middleware/fs-utils.d.ts.map +1 -0
- package/build/middleware/fs-utils.js +229 -0
- package/build/middleware/fs-utils.js.map +1 -0
- package/build/middleware/logger.d.ts +16 -0
- package/build/middleware/logger.d.ts.map +1 -1
- package/build/middleware/logger.js +18 -0
- package/build/middleware/logger.js.map +1 -1
- package/build/middleware/output-processor.d.ts.map +1 -1
- package/build/middleware/output-processor.js +43 -8
- package/build/middleware/output-processor.js.map +1 -1
- package/build/middleware/parse-utils.d.ts +19 -0
- package/build/middleware/parse-utils.d.ts.map +1 -0
- package/build/middleware/parse-utils.js +30 -0
- package/build/middleware/parse-utils.js.map +1 -0
- package/build/middleware/png-utils.d.ts.map +1 -1
- package/build/middleware/png-utils.js +33 -11
- package/build/middleware/png-utils.js.map +1 -1
- package/build/middleware/result-handle.d.ts +76 -0
- package/build/middleware/result-handle.d.ts.map +1 -0
- package/build/middleware/result-handle.js +378 -0
- package/build/middleware/result-handle.js.map +1 -0
- package/build/middleware/sanitize.d.ts +26 -0
- package/build/middleware/sanitize.d.ts.map +1 -1
- package/build/middleware/sanitize.js +43 -2
- package/build/middleware/sanitize.js.map +1 -1
- package/build/middleware/security.d.ts.map +1 -1
- package/build/middleware/security.js +30 -5
- package/build/middleware/security.js.map +1 -1
- package/build/middleware/ui-dump.d.ts.map +1 -1
- package/build/middleware/ui-dump.js +30 -10
- package/build/middleware/ui-dump.js.map +1 -1
- package/build/server.d.ts.map +1 -1
- package/build/server.js +34 -5
- package/build/server.js.map +1 -1
- package/build/tool-context.d.ts.map +1 -1
- package/build/tool-context.js +2 -0
- package/build/tool-context.js.map +1 -1
- package/build/tools/accessibility.d.ts.map +1 -1
- package/build/tools/accessibility.js +39 -40
- package/build/tools/accessibility.js.map +1 -1
- package/build/tools/at-commands.d.ts.map +1 -1
- package/build/tools/at-commands.js +31 -6
- package/build/tools/at-commands.js.map +1 -1
- package/build/tools/baseband.d.ts.map +1 -1
- package/build/tools/baseband.js +18 -7
- package/build/tools/baseband.js.map +1 -1
- package/build/tools/build.d.ts.map +1 -1
- package/build/tools/build.js +30 -8
- package/build/tools/build.js.map +1 -1
- package/build/tools/ci.d.ts.map +1 -1
- package/build/tools/ci.js +16 -5
- package/build/tools/ci.js.map +1 -1
- package/build/tools/control.d.ts.map +1 -1
- package/build/tools/control.js +19 -11
- package/build/tools/control.js.map +1 -1
- package/build/tools/device-farm.d.ts.map +1 -1
- package/build/tools/device-farm.js +27 -3
- package/build/tools/device-farm.js.map +1 -1
- package/build/tools/device-profiles.d.ts.map +1 -1
- package/build/tools/device-profiles.js +70 -8
- package/build/tools/device-profiles.js.map +1 -1
- package/build/tools/device.d.ts.map +1 -1
- package/build/tools/device.js +4 -2
- package/build/tools/device.js.map +1 -1
- package/build/tools/diagnostics.d.ts.map +1 -1
- package/build/tools/diagnostics.js +17 -9
- package/build/tools/diagnostics.js.map +1 -1
- package/build/tools/emulator.d.ts.map +1 -1
- package/build/tools/emulator.js +12 -13
- package/build/tools/emulator.js.map +1 -1
- package/build/tools/files.d.ts +1 -1
- package/build/tools/files.d.ts.map +1 -1
- package/build/tools/files.js +98 -56
- package/build/tools/files.js.map +1 -1
- package/build/tools/firmware-analysis.d.ts.map +1 -1
- package/build/tools/firmware-analysis.js +35 -12
- package/build/tools/firmware-analysis.js.map +1 -1
- package/build/tools/forwarding.d.ts.map +1 -1
- package/build/tools/forwarding.js +248 -6
- package/build/tools/forwarding.js.map +1 -1
- package/build/tools/health.d.ts.map +1 -1
- package/build/tools/health.js +14 -3
- package/build/tools/health.js.map +1 -1
- package/build/tools/input-gestures.d.ts.map +1 -1
- package/build/tools/input-gestures.js +65 -13
- package/build/tools/input-gestures.js.map +1 -1
- package/build/tools/logcat-watch.d.ts.map +1 -1
- package/build/tools/logcat-watch.js +21 -14
- package/build/tools/logcat-watch.js.map +1 -1
- package/build/tools/logs.d.ts.map +1 -1
- package/build/tools/logs.js +8 -4
- package/build/tools/logs.js.map +1 -1
- package/build/tools/mirroring.d.ts.map +1 -1
- package/build/tools/mirroring.js +21 -19
- package/build/tools/mirroring.js.map +1 -1
- package/build/tools/multi-device.d.ts.map +1 -1
- package/build/tools/multi-device.js +10 -0
- package/build/tools/multi-device.js.map +1 -1
- package/build/tools/network-capture.d.ts.map +1 -1
- package/build/tools/network-capture.js +32 -8
- package/build/tools/network-capture.js.map +1 -1
- package/build/tools/network-discovery.d.ts.map +1 -1
- package/build/tools/network-discovery.js +11 -2
- package/build/tools/network-discovery.js.map +1 -1
- package/build/tools/ota-monitor.d.ts.map +1 -1
- package/build/tools/ota-monitor.js +26 -12
- package/build/tools/ota-monitor.js.map +1 -1
- package/build/tools/packages.d.ts.map +1 -1
- package/build/tools/packages.js +18 -11
- package/build/tools/packages.js.map +1 -1
- package/build/tools/plugins.d.ts +18 -0
- package/build/tools/plugins.d.ts.map +1 -1
- package/build/tools/plugins.js +47 -2
- package/build/tools/plugins.js.map +1 -1
- package/build/tools/prompts.d.ts.map +1 -1
- package/build/tools/prompts.js +2 -0
- package/build/tools/prompts.js.map +1 -1
- package/build/tools/qemu.d.ts +6 -3
- package/build/tools/qemu.d.ts.map +1 -1
- package/build/tools/qemu.js +28 -16
- package/build/tools/qemu.js.map +1 -1
- package/build/tools/registry.d.ts.map +1 -1
- package/build/tools/registry.js +76 -26
- package/build/tools/registry.js.map +1 -1
- package/build/tools/regression.d.ts +37 -0
- package/build/tools/regression.d.ts.map +1 -1
- package/build/tools/regression.js +63 -22
- package/build/tools/regression.js.map +1 -1
- package/build/tools/resources.d.ts.map +1 -1
- package/build/tools/resources.js +62 -15
- package/build/tools/resources.js.map +1 -1
- package/build/tools/result-handles.d.ts +37 -0
- package/build/tools/result-handles.d.ts.map +1 -0
- package/build/tools/result-handles.js +286 -0
- package/build/tools/result-handles.js.map +1 -0
- package/build/tools/ril-intercept.d.ts.map +1 -1
- package/build/tools/ril-intercept.js +15 -18
- package/build/tools/ril-intercept.js.map +1 -1
- package/build/tools/screen-record.d.ts.map +1 -1
- package/build/tools/screen-record.js +34 -5
- package/build/tools/screen-record.js.map +1 -1
- package/build/tools/screenshot-diff.d.ts.map +1 -1
- package/build/tools/screenshot-diff.js +29 -21
- package/build/tools/screenshot-diff.js.map +1 -1
- package/build/tools/selinux-audit.d.ts.map +1 -1
- package/build/tools/selinux-audit.js +7 -5
- package/build/tools/selinux-audit.js.map +1 -1
- package/build/tools/sensors.d.ts.map +1 -1
- package/build/tools/sensors.js +26 -4
- package/build/tools/sensors.js.map +1 -1
- package/build/tools/shell.d.ts.map +1 -1
- package/build/tools/shell.js +9 -0
- package/build/tools/shell.js.map +1 -1
- package/build/tools/snapshot.d.ts.map +1 -1
- package/build/tools/snapshot.js +38 -19
- package/build/tools/snapshot.js.map +1 -1
- package/build/tools/split-apk.d.ts.map +1 -1
- package/build/tools/split-apk.js +48 -24
- package/build/tools/split-apk.js.map +1 -1
- package/build/tools/test-gen.d.ts.map +1 -1
- package/build/tools/test-gen.js +29 -10
- package/build/tools/test-gen.js.map +1 -1
- package/build/tools/testing.d.ts.map +1 -1
- package/build/tools/testing.js +27 -9
- package/build/tools/testing.js.map +1 -1
- package/build/tools/thermal-power.d.ts +8 -1
- package/build/tools/thermal-power.d.ts.map +1 -1
- package/build/tools/thermal-power.js +35 -10
- package/build/tools/thermal-power.js.map +1 -1
- package/build/tools/ui.d.ts.map +1 -1
- package/build/tools/ui.js +20 -13
- package/build/tools/ui.js.map +1 -1
- package/build/tools/wireless-firmware.d.ts.map +1 -1
- package/build/tools/wireless-firmware.js +21 -7
- package/build/tools/wireless-firmware.js.map +1 -1
- package/build/tools/wireless.d.ts.map +1 -1
- package/build/tools/wireless.js +39 -3
- package/build/tools/wireless.js.map +1 -1
- package/build/tools/workflow-market.d.ts.map +1 -1
- package/build/tools/workflow-market.js +67 -13
- package/build/tools/workflow-market.js.map +1 -1
- package/build/tools/workflow.d.ts.map +1 -1
- package/build/tools/workflow.js +77 -8
- package/build/tools/workflow.js.map +1 -1
- package/build/ws-transport.d.ts.map +1 -1
- package/build/ws-transport.js +21 -3
- package/build/ws-transport.js.map +1 -1
- package/package.json +20 -8
package/build/tools/files.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
// Copyright 2026 Jason <fullread@github>
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
/**
|
|
2
4
|
* File Tools — Filesystem operations on the Android device.
|
|
3
5
|
*
|
|
4
6
|
* 18 tools covering file transfer, inspection, creation, deletion, search,
|
|
5
7
|
* content editing, and filesystem metadata. All path-accepting tools use
|
|
6
|
-
*
|
|
8
|
+
* shellQuote() for injection prevention.
|
|
7
9
|
*
|
|
8
10
|
* Safety model:
|
|
9
11
|
* - Hard-blocked paths: /, /dev, /proc, /sys — kernel virtual filesystems
|
|
@@ -30,7 +32,8 @@
|
|
|
30
32
|
import { z } from "zod";
|
|
31
33
|
import { join } from "path";
|
|
32
34
|
import { OutputProcessor } from "../middleware/output-processor.js";
|
|
33
|
-
import {
|
|
35
|
+
import { resultHandleSchemaFields, withResultHandle } from "./result-handles.js";
|
|
36
|
+
import { shellQuote } from "../middleware/sanitize.js";
|
|
34
37
|
// ── Constants ────────────────────────────────────────────────────────────────
|
|
35
38
|
/** Kernel virtual filesystems — writes can panic the kernel. */
|
|
36
39
|
const HARD_BLOCKED_PATHS = ["/dev", "/proc", "/sys"];
|
|
@@ -88,7 +91,7 @@ function isHardBlocked(resolvedPath) {
|
|
|
88
91
|
/** Resolve symlinks via realpath on the device. Returns the resolved path or original on failure. */
|
|
89
92
|
async function resolveRealPath(ctx, serial, path, root) {
|
|
90
93
|
const shell = root ? ctx.bridge.rootShell.bind(ctx.bridge) : ctx.bridge.shell.bind(ctx.bridge);
|
|
91
|
-
const result = await shell(`realpath
|
|
94
|
+
const result = await shell(`realpath ${shellQuote(path)} 2>/dev/null || echo ${shellQuote(path)}`, {
|
|
92
95
|
device: serial, timeout: 3000, ignoreExitCode: true,
|
|
93
96
|
});
|
|
94
97
|
return result.stdout.trim() || path;
|
|
@@ -98,10 +101,10 @@ async function resolveRealPath(ctx, serial, path, root) {
|
|
|
98
101
|
async function getStorageInfo(ctx, serial, path, root) {
|
|
99
102
|
const shell = root ? ctx.bridge.rootShell.bind(ctx.bridge) : ctx.bridge.shell.bind(ctx.bridge);
|
|
100
103
|
// Try the path itself, then fall back to its parent directory
|
|
101
|
-
const escaped =
|
|
104
|
+
const escaped = shellQuote(path);
|
|
102
105
|
const parentDir = path.replace(/\/[^/]+\/?$/, "") || "/";
|
|
103
|
-
const parentEscaped =
|
|
104
|
-
const result = await shell(`df -h
|
|
106
|
+
const parentEscaped = shellQuote(parentDir);
|
|
107
|
+
const result = await shell(`df -h ${escaped} 2>/dev/null || df -h ${parentEscaped} 2>/dev/null`, { device: serial, timeout: 5000, ignoreExitCode: true });
|
|
105
108
|
const lines = result.stdout.trim().split("\n");
|
|
106
109
|
const dataLine = lines[lines.length - 1] ?? "";
|
|
107
110
|
if (!dataLine)
|
|
@@ -116,7 +119,7 @@ async function getStorageInfo(ctx, serial, path, root) {
|
|
|
116
119
|
/** Detect filesystem type for a given path. Resolves hex magic numbers to names. */
|
|
117
120
|
async function detectFsType(ctx, serial, path, root) {
|
|
118
121
|
const shell = root ? ctx.bridge.rootShell.bind(ctx.bridge) : ctx.bridge.shell.bind(ctx.bridge);
|
|
119
|
-
const result = await shell(`stat -f -c '%T'
|
|
122
|
+
const result = await shell(`stat -f -c '%T' ${shellQuote(path)} 2>/dev/null`, {
|
|
120
123
|
device: serial, timeout: 3000, ignoreExitCode: true,
|
|
121
124
|
});
|
|
122
125
|
let fsType = result.stdout.trim().toLowerCase();
|
|
@@ -164,7 +167,7 @@ function formatBytes(n) {
|
|
|
164
167
|
* so the final shell command remains well-formed. */
|
|
165
168
|
function sedEscapePattern(str) {
|
|
166
169
|
return str
|
|
167
|
-
.replace(/[[\]
|
|
170
|
+
.replace(/[[\].*^$\\/&]/g, "\\$&")
|
|
168
171
|
.replace(/'/g, "'\\''");
|
|
169
172
|
}
|
|
170
173
|
/** Escape a string for use in a sed replacement string.
|
|
@@ -172,7 +175,7 @@ function sedEscapePattern(str) {
|
|
|
172
175
|
* close/reopen the surrounding shell single-quote around any `'`. */
|
|
173
176
|
function sedEscapeReplacement(str) {
|
|
174
177
|
return str
|
|
175
|
-
.replace(/[
|
|
178
|
+
.replace(/[\\/&]/g, "\\$&")
|
|
176
179
|
.replace(/'/g, "'\\''");
|
|
177
180
|
}
|
|
178
181
|
// ── Tool Registration ────────────────────────────────────────────────────────
|
|
@@ -240,7 +243,7 @@ export function registerFileTools(ctx) {
|
|
|
240
243
|
const start = Date.now();
|
|
241
244
|
try {
|
|
242
245
|
const resolved = await ctx.deviceManager.resolveDevice(device);
|
|
243
|
-
const cmd = details ? `ls -la
|
|
246
|
+
const cmd = details ? `ls -la ${shellQuote(path)}` : `ls ${shellQuote(path)}`;
|
|
244
247
|
const result = await ctx.bridge.shell(cmd, { device: resolved.serial });
|
|
245
248
|
return { content: [{ type: "text", text: `${OutputProcessor.process(result.stdout)}\nExecution time: ${Date.now() - start}ms` }] };
|
|
246
249
|
}
|
|
@@ -256,9 +259,9 @@ export function registerFileTools(ctx) {
|
|
|
256
259
|
const start = Date.now();
|
|
257
260
|
try {
|
|
258
261
|
const resolved = await ctx.deviceManager.resolveDevice(device);
|
|
259
|
-
let cmd = `cat
|
|
262
|
+
let cmd = `cat ${shellQuote(path)}`;
|
|
260
263
|
if (maxLines)
|
|
261
|
-
cmd = `head -n ${maxLines}
|
|
264
|
+
cmd = `head -n ${maxLines} ${shellQuote(path)}`;
|
|
262
265
|
const result = await ctx.bridge.shell(cmd, { device: resolved.serial });
|
|
263
266
|
return { content: [{ type: "text", text: `${OutputProcessor.process(result.stdout)}\nExecution time: ${Date.now() - start}ms` }] };
|
|
264
267
|
}
|
|
@@ -303,7 +306,7 @@ export function registerFileTools(ctx) {
|
|
|
303
306
|
delimiter = `DEEPADB_EOF_${Math.random().toString(36).substring(2, 8)}`;
|
|
304
307
|
}
|
|
305
308
|
const op = append ? ">>" : ">";
|
|
306
|
-
const cmd = `cat << '${delimiter}' ${op}
|
|
309
|
+
const cmd = `cat << '${delimiter}' ${op} ${shellQuote(filePath)}\n${content}\n${delimiter}`;
|
|
307
310
|
const result = await shell(cmd, {
|
|
308
311
|
device: serial, timeout: 30000, ignoreExitCode: true,
|
|
309
312
|
});
|
|
@@ -314,7 +317,7 @@ export function registerFileTools(ctx) {
|
|
|
314
317
|
return { content: [{ type: "text", text: `Write failed: ${result.stderr.trim()}${!root ? "\nTip: try root=true for protected paths" : ""}` }], isError: true };
|
|
315
318
|
}
|
|
316
319
|
// Post-write verification
|
|
317
|
-
const verifyResult = await shell(`stat -c '%s'
|
|
320
|
+
const verifyResult = await shell(`stat -c '%s' ${shellQuote(filePath)} 2>/dev/null`, {
|
|
318
321
|
device: serial, timeout: 3000, ignoreExitCode: true,
|
|
319
322
|
});
|
|
320
323
|
const writtenSize = verifyResult.stdout.trim();
|
|
@@ -348,18 +351,19 @@ export function registerFileTools(ctx) {
|
|
|
348
351
|
maxResults: z.number().min(1).max(10000).optional().default(500).describe("Maximum results to return (1-10000, default 500)"),
|
|
349
352
|
root: z.boolean().optional().default(false).describe("Use root shell for system paths"),
|
|
350
353
|
device: z.string().optional().describe("Device serial"),
|
|
351
|
-
|
|
354
|
+
...resultHandleSchemaFields,
|
|
355
|
+
}, async ({ searchPath, name, type, maxDepth, maxResults, root, device, result_handle, result_handle_ttl }) => {
|
|
352
356
|
const start = Date.now();
|
|
353
357
|
try {
|
|
354
358
|
const resolved = await ctx.deviceManager.resolveDevice(device);
|
|
355
359
|
const serial = resolved.serial;
|
|
356
360
|
const shell = execShell(ctx, root);
|
|
357
|
-
let cmd = `find
|
|
361
|
+
let cmd = `find ${shellQuote(searchPath)} -maxdepth ${maxDepth}`;
|
|
358
362
|
if (type === "file")
|
|
359
363
|
cmd += " -type f";
|
|
360
364
|
else if (type === "directory")
|
|
361
365
|
cmd += " -type d";
|
|
362
|
-
cmd += ` -name
|
|
366
|
+
cmd += ` -name ${shellQuote(name)}`;
|
|
363
367
|
// Request one extra to detect truncation
|
|
364
368
|
cmd += ` 2>/dev/null | head -n ${maxResults + 1}`;
|
|
365
369
|
const result = await shell(cmd, {
|
|
@@ -376,7 +380,7 @@ export function registerFileTools(ctx) {
|
|
|
376
380
|
if (truncated)
|
|
377
381
|
output += ` (truncated at ${maxResults} — more results exist, increase maxResults or narrow the search)`;
|
|
378
382
|
output += `\nExecution time: ${Date.now() - start}ms`;
|
|
379
|
-
return { content: [{ type: "text", text: output }] };
|
|
383
|
+
return withResultHandle({ content: [{ type: "text", text: output }] }, "find", { result_handle, result_handle_ttl });
|
|
380
384
|
}
|
|
381
385
|
catch (error) {
|
|
382
386
|
return { content: [{ type: "text", text: OutputProcessor.formatError(error) }], isError: true };
|
|
@@ -392,9 +396,9 @@ export function registerFileTools(ctx) {
|
|
|
392
396
|
const resolved = await ctx.deviceManager.resolveDevice(device);
|
|
393
397
|
const serial = resolved.serial;
|
|
394
398
|
const shell = execShell(ctx, root);
|
|
395
|
-
const escaped =
|
|
399
|
+
const escaped = shellQuote(filePath);
|
|
396
400
|
// stat + ls -Z for SELinux context in a single command
|
|
397
|
-
const result = await shell(`stat
|
|
401
|
+
const result = await shell(`stat ${escaped} 2>&1 && echo '---SELINUX---' && ls -Zd ${escaped} 2>/dev/null`, { device: serial, timeout: 10000, ignoreExitCode: true });
|
|
398
402
|
const output = result.stdout.trim();
|
|
399
403
|
if (output.includes("No such file or directory")) {
|
|
400
404
|
return { content: [{ type: "text", text: `File not found: ${filePath}` }], isError: true };
|
|
@@ -424,16 +428,16 @@ export function registerFileTools(ctx) {
|
|
|
424
428
|
const resolved = await ctx.deviceManager.resolveDevice(device);
|
|
425
429
|
const serial = resolved.serial;
|
|
426
430
|
const shell = execShell(ctx, root);
|
|
427
|
-
const escaped =
|
|
431
|
+
const escaped = shellQuote(filePath);
|
|
428
432
|
// Get size first for timeout estimation
|
|
429
|
-
const sizeResult = await shell(`stat -c '%s'
|
|
433
|
+
const sizeResult = await shell(`stat -c '%s' ${escaped} 2>/dev/null`, {
|
|
430
434
|
device: serial, timeout: 3000, ignoreExitCode: true,
|
|
431
435
|
});
|
|
432
436
|
const fileSize = parseInt(sizeResult.stdout.trim(), 10) || 0;
|
|
433
437
|
// ~100MB/s for sha256 on typical SoC; minimum 10s, maximum 5min
|
|
434
438
|
const hashTimeout = Math.max(10000, Math.min(300000, Math.ceil(fileSize / (100 * 1024 * 1024)) * 1000 + 10000));
|
|
435
439
|
const hashCmd = algorithm === "md5" ? "md5sum" : algorithm === "sha1" ? "sha1sum" : "sha256sum";
|
|
436
|
-
const result = await shell(`${hashCmd}
|
|
440
|
+
const result = await shell(`${hashCmd} ${escaped} 2>&1`, {
|
|
437
441
|
device: serial, timeout: hashTimeout, ignoreExitCode: true,
|
|
438
442
|
});
|
|
439
443
|
const output = result.stdout.trim();
|
|
@@ -483,7 +487,7 @@ export function registerFileTools(ctx) {
|
|
|
483
487
|
if (fs.readOnly)
|
|
484
488
|
return { content: [{ type: "text", text: fs.warning }], isError: true };
|
|
485
489
|
const pFlag = parents ? "-p " : "";
|
|
486
|
-
const result = await shell(`mkdir ${pFlag}
|
|
490
|
+
const result = await shell(`mkdir ${pFlag}${shellQuote(dirPath)} 2>&1`, {
|
|
487
491
|
device: serial, timeout: 10000, ignoreExitCode: true,
|
|
488
492
|
});
|
|
489
493
|
if (result.exitCode !== 0 && result.stdout.trim()) {
|
|
@@ -537,12 +541,12 @@ export function registerFileTools(ctx) {
|
|
|
537
541
|
// Pre-flight count for recursive delete
|
|
538
542
|
let itemCount = "1";
|
|
539
543
|
if (recursive) {
|
|
540
|
-
const countResult = await shell(`find
|
|
544
|
+
const countResult = await shell(`find ${shellQuote(targetPath)} 2>/dev/null | wc -l`, { device: serial, timeout: 15000, ignoreExitCode: true });
|
|
541
545
|
itemCount = countResult.stdout.trim() || "unknown";
|
|
542
546
|
}
|
|
543
547
|
// Execute deletion
|
|
544
548
|
const rmFlag = recursive ? "-rf" : "-f";
|
|
545
|
-
const result = await shell(`rm ${rmFlag}
|
|
549
|
+
const result = await shell(`rm ${rmFlag} ${shellQuote(targetPath)} 2>&1`, {
|
|
546
550
|
device: serial, timeout: recursive ? 120000 : 10000, ignoreExitCode: true,
|
|
547
551
|
});
|
|
548
552
|
if (result.exitCode !== 0 && result.stdout.trim()) {
|
|
@@ -600,19 +604,19 @@ export function registerFileTools(ctx) {
|
|
|
600
604
|
};
|
|
601
605
|
}
|
|
602
606
|
// Get source size for reporting
|
|
603
|
-
const sizeResult = await shell(`stat -c '%s'
|
|
607
|
+
const sizeResult = await shell(`stat -c '%s' ${shellQuote(source)} 2>/dev/null`, {
|
|
604
608
|
device: serial, timeout: 3000, ignoreExitCode: true,
|
|
605
609
|
});
|
|
606
610
|
const sourceSize = sizeResult.stdout.trim();
|
|
607
611
|
// Execute move
|
|
608
|
-
const result = await shell(`mv
|
|
612
|
+
const result = await shell(`mv ${shellQuote(source)} ${shellQuote(destination)} 2>&1`, {
|
|
609
613
|
device: serial, timeout: 120000, ignoreExitCode: true,
|
|
610
614
|
});
|
|
611
615
|
if (result.exitCode !== 0 && result.stdout.trim()) {
|
|
612
616
|
return { content: [{ type: "text", text: `Move failed: ${result.stdout.trim()}${!root ? "\nTip: try root=true for protected paths" : ""}` }], isError: true };
|
|
613
617
|
}
|
|
614
618
|
// Post-verify: destination exists
|
|
615
|
-
const verifyResult = await shell(`test -e
|
|
619
|
+
const verifyResult = await shell(`test -e ${shellQuote(destination)} && echo EXISTS`, {
|
|
616
620
|
device: serial, timeout: 3000, ignoreExitCode: true,
|
|
617
621
|
});
|
|
618
622
|
const verified = verifyResult.stdout.includes("EXISTS");
|
|
@@ -657,19 +661,45 @@ export function registerFileTools(ctx) {
|
|
|
657
661
|
if (fs.readOnly)
|
|
658
662
|
return { content: [{ type: "text", text: fs.warning }], isError: true };
|
|
659
663
|
// Pre-flight: source size and destination available space
|
|
660
|
-
const preResult = await shell(`stat -c '%s'
|
|
664
|
+
const preResult = await shell(`stat -c '%s' ${shellQuote(source)} 2>/dev/null && echo '---DF---' && df -k ${shellQuote(destination)} 2>/dev/null | tail -1`, { device: serial, timeout: 5000, ignoreExitCode: true });
|
|
661
665
|
const preParts = preResult.stdout.split("---DF---");
|
|
662
666
|
const sourceBytes = parseInt(preParts[0]?.trim() ?? "", 10) || 0;
|
|
667
|
+
// B1: parse df output (-k = 1K blocks). Standard fields:
|
|
668
|
+
// Filesystem 1K-blocks Used Available Use% Mounted on
|
|
669
|
+
// Tail -1 has already stripped the header. Available is field [3].
|
|
670
|
+
// If the destination filesystem has less free space than sourceBytes,
|
|
671
|
+
// surface an actionable error BEFORE attempting the copy — cp would
|
|
672
|
+
// otherwise fail mid-write with "No space left on device" and could
|
|
673
|
+
// leave a partial destination file behind.
|
|
674
|
+
const dfLine = preParts[1]?.trim() ?? "";
|
|
675
|
+
if (dfLine && sourceBytes > 0) {
|
|
676
|
+
const dfFields = dfLine.split(/\s+/);
|
|
677
|
+
const availKb = parseInt(dfFields[3] ?? "", 10);
|
|
678
|
+
if (Number.isFinite(availKb) && availKb >= 0) {
|
|
679
|
+
const availBytes = availKb * 1024;
|
|
680
|
+
// 5% slack — filesystems often refuse the last few percent (reserved blocks).
|
|
681
|
+
const needed = Math.ceil(sourceBytes * 1.05);
|
|
682
|
+
if (needed > availBytes) {
|
|
683
|
+
return {
|
|
684
|
+
content: [{
|
|
685
|
+
type: "text",
|
|
686
|
+
text: `Insufficient space on destination filesystem. Need \`${formatBytes(needed)}\` (source \`${formatBytes(sourceBytes)}\` + 5% slack), available \`${formatBytes(availBytes)}\`.`,
|
|
687
|
+
}],
|
|
688
|
+
isError: true,
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
663
693
|
// Execute copy
|
|
664
694
|
const rFlag = recursive ? "-r " : "";
|
|
665
|
-
const result = await shell(`cp ${rFlag}
|
|
695
|
+
const result = await shell(`cp ${rFlag}${shellQuote(source)} ${shellQuote(destination)} 2>&1`, {
|
|
666
696
|
device: serial, timeout: 300000, ignoreExitCode: true,
|
|
667
697
|
});
|
|
668
698
|
if (result.exitCode !== 0 && result.stdout.trim()) {
|
|
669
699
|
return { content: [{ type: "text", text: `Copy failed: ${result.stdout.trim()}${!root ? "\nTip: try root=true for protected paths" : ""}` }], isError: true };
|
|
670
700
|
}
|
|
671
701
|
// Post-verify: compare sizes
|
|
672
|
-
const destSizeResult = await shell(`stat -c '%s'
|
|
702
|
+
const destSizeResult = await shell(`stat -c '%s' ${shellQuote(destination)} 2>/dev/null`, {
|
|
673
703
|
device: serial, timeout: 3000, ignoreExitCode: true,
|
|
674
704
|
});
|
|
675
705
|
const destBytes = parseInt(destSizeResult.stdout.trim(), 10) || 0;
|
|
@@ -726,14 +756,14 @@ export function registerFileTools(ctx) {
|
|
|
726
756
|
if (fs.readOnly)
|
|
727
757
|
return { content: [{ type: "text", text: fs.warning }], isError: true };
|
|
728
758
|
const rFlag = recursive ? "-R " : "";
|
|
729
|
-
const result = await shell(`chmod ${rFlag}${mode}
|
|
759
|
+
const result = await shell(`chmod ${rFlag}${mode} ${shellQuote(filePath)} 2>&1`, {
|
|
730
760
|
device: serial, timeout: recursive ? 60000 : 10000, ignoreExitCode: true,
|
|
731
761
|
});
|
|
732
762
|
if (result.exitCode !== 0 && result.stdout.trim()) {
|
|
733
763
|
return { content: [{ type: "text", text: `chmod failed: ${result.stdout.trim()}${!root ? "\nTip: try root=true (chmod typically requires root)" : ""}` }], isError: true };
|
|
734
764
|
}
|
|
735
765
|
// Verify resulting permissions
|
|
736
|
-
const verifyResult = await shell(`stat -c '%a %A'
|
|
766
|
+
const verifyResult = await shell(`stat -c '%a %A' ${shellQuote(filePath)} 2>/dev/null`, {
|
|
737
767
|
device: serial, timeout: 3000, ignoreExitCode: true,
|
|
738
768
|
});
|
|
739
769
|
const resulting = verifyResult.stdout.trim();
|
|
@@ -776,7 +806,7 @@ export function registerFileTools(ctx) {
|
|
|
776
806
|
if (fs.readOnly)
|
|
777
807
|
return { content: [{ type: "text", text: fs.warning }], isError: true };
|
|
778
808
|
// Check if file exists before touch (to report create vs update)
|
|
779
|
-
const existResult = await shell(`test -e
|
|
809
|
+
const existResult = await shell(`test -e ${shellQuote(filePath)} && echo EXISTS`, {
|
|
780
810
|
device: serial, timeout: 3000, ignoreExitCode: true,
|
|
781
811
|
});
|
|
782
812
|
const existed = existResult.stdout.includes("EXISTS");
|
|
@@ -789,10 +819,10 @@ export function registerFileTools(ctx) {
|
|
|
789
819
|
return { content: [{ type: "text", text: `Invalid timestamp format: "${timestamp}". Expected: YYYY-MM-DD HH:MM:SS` }], isError: true };
|
|
790
820
|
}
|
|
791
821
|
const touchTs = `${tsMatch[1]}${tsMatch[2]}${tsMatch[3]}${tsMatch[4]}${tsMatch[5]}.${tsMatch[6]}`;
|
|
792
|
-
cmd = `touch -t ${touchTs}
|
|
822
|
+
cmd = `touch -t ${touchTs} ${shellQuote(filePath)} 2>&1`;
|
|
793
823
|
}
|
|
794
824
|
else {
|
|
795
|
-
cmd = `touch
|
|
825
|
+
cmd = `touch ${shellQuote(filePath)} 2>&1`;
|
|
796
826
|
}
|
|
797
827
|
const result = await shell(cmd, {
|
|
798
828
|
device: serial, timeout: 10000, ignoreExitCode: true,
|
|
@@ -801,7 +831,7 @@ export function registerFileTools(ctx) {
|
|
|
801
831
|
return { content: [{ type: "text", text: `Touch failed: ${result.stdout.trim()}${!root ? "\nTip: try root=true for protected paths" : ""}` }], isError: true };
|
|
802
832
|
}
|
|
803
833
|
// Verify resulting timestamps
|
|
804
|
-
const statResult = await shell(`stat -c 'Access: %x\nModify: %y\nChange: %z'
|
|
834
|
+
const statResult = await shell(`stat -c 'Access: %x\nModify: %y\nChange: %z' ${shellQuote(filePath)} 2>/dev/null`, { device: serial, timeout: 3000, ignoreExitCode: true });
|
|
805
835
|
const timestamps = statResult.stdout.trim();
|
|
806
836
|
const mode = !existed ? "created" : timestamp ? "timestamp set" : "updated";
|
|
807
837
|
return {
|
|
@@ -834,21 +864,21 @@ export function registerFileTools(ctx) {
|
|
|
834
864
|
const resolved = await ctx.deviceManager.resolveDevice(device);
|
|
835
865
|
const serial = resolved.serial;
|
|
836
866
|
const shell = execShell(ctx, root);
|
|
837
|
-
const escaped =
|
|
867
|
+
const escaped = shellQuote(filePath);
|
|
838
868
|
// Parallel reads: stat -f, df, full mount table, SELinux context.
|
|
839
869
|
// The mount table is read whole (small) and filtered in TypeScript
|
|
840
870
|
// so we don't interpolate df output into a device-side grep pattern.
|
|
841
871
|
const [statfR, dfR, mountAllR, seR] = await Promise.allSettled([
|
|
842
|
-
shell(`stat -f -c 'Type: %T\nBlock size: %S\nTotal blocks: %b\nFree blocks: %f\nAvailable blocks: %a\nTotal inodes: %c\nFree inodes: %d'
|
|
872
|
+
shell(`stat -f -c 'Type: %T\nBlock size: %S\nTotal blocks: %b\nFree blocks: %f\nAvailable blocks: %a\nTotal inodes: %c\nFree inodes: %d' ${escaped} 2>/dev/null`, {
|
|
843
873
|
device: serial, timeout: 5000, ignoreExitCode: true,
|
|
844
874
|
}),
|
|
845
|
-
shell(`df -h
|
|
875
|
+
shell(`df -h ${escaped} 2>/dev/null | tail -1`, {
|
|
846
876
|
device: serial, timeout: 5000, ignoreExitCode: true,
|
|
847
877
|
}),
|
|
848
878
|
shell(`mount 2>/dev/null`, {
|
|
849
879
|
device: serial, timeout: 5000, ignoreExitCode: true,
|
|
850
880
|
}),
|
|
851
|
-
shell(`ls -Zd
|
|
881
|
+
shell(`ls -Zd ${escaped} 2>/dev/null`, {
|
|
852
882
|
device: serial, timeout: 3000, ignoreExitCode: true,
|
|
853
883
|
}),
|
|
854
884
|
]);
|
|
@@ -969,14 +999,14 @@ export function registerFileTools(ctx) {
|
|
|
969
999
|
if (fs.readOnly)
|
|
970
1000
|
return { content: [{ type: "text", text: fs.warning }], isError: true };
|
|
971
1001
|
const rFlag = recursive ? "-R " : "";
|
|
972
|
-
const result = await shell(`chown ${rFlag}${owner}
|
|
1002
|
+
const result = await shell(`chown ${rFlag}${owner} ${shellQuote(filePath)} 2>&1`, {
|
|
973
1003
|
device: serial, timeout: recursive ? 60000 : 10000, ignoreExitCode: true,
|
|
974
1004
|
});
|
|
975
1005
|
if (result.exitCode !== 0 && result.stdout.trim()) {
|
|
976
1006
|
return { content: [{ type: "text", text: `chown failed: ${result.stdout.trim()}` }], isError: true };
|
|
977
1007
|
}
|
|
978
1008
|
// Verify resulting ownership
|
|
979
|
-
const verifyResult = await shell(`stat -c '%U:%G (uid=%u gid=%g)'
|
|
1009
|
+
const verifyResult = await shell(`stat -c '%U:%G (uid=%u gid=%g)' ${shellQuote(filePath)} 2>/dev/null`, {
|
|
980
1010
|
device: serial, timeout: 3000, ignoreExitCode: true,
|
|
981
1011
|
});
|
|
982
1012
|
const resulting = verifyResult.stdout.trim();
|
|
@@ -1011,23 +1041,28 @@ export function registerFileTools(ctx) {
|
|
|
1011
1041
|
filesWithMatches: z.boolean().optional().default(false).describe("Show only filenames that contain matches, not the matching lines"),
|
|
1012
1042
|
root: z.boolean().optional().default(false).describe("Use root shell for protected paths"),
|
|
1013
1043
|
device: z.string().optional().describe("Device serial"),
|
|
1014
|
-
|
|
1044
|
+
...resultHandleSchemaFields,
|
|
1045
|
+
}, async ({ pattern, path: searchPath, recursive, fixedString, ignoreCase, maxResults, maxDepth, filesWithMatches, root, device, result_handle, result_handle_ttl }) => {
|
|
1015
1046
|
const start = Date.now();
|
|
1016
1047
|
try {
|
|
1017
1048
|
const resolved = await ctx.deviceManager.resolveDevice(device);
|
|
1018
1049
|
const serial = resolved.serial;
|
|
1019
1050
|
const shell = execShell(ctx, root);
|
|
1020
|
-
const escaped =
|
|
1021
|
-
const patternEscaped =
|
|
1051
|
+
const escaped = shellQuote(searchPath);
|
|
1052
|
+
const patternEscaped = shellQuote(pattern);
|
|
1022
1053
|
let cmd;
|
|
1023
1054
|
if (recursive) {
|
|
1024
1055
|
// Use find + grep for recursive with depth control
|
|
1025
1056
|
const grepFlags = [fixedString ? "-F" : "", ignoreCase ? "-i" : "", "-n", filesWithMatches ? "-l" : ""].filter(f => f).join(" ");
|
|
1026
|
-
|
|
1057
|
+
// B2: use -exec grep {} + instead of xargs so filenames containing
|
|
1058
|
+
// whitespace, quotes, or newlines do not split incorrectly and miss
|
|
1059
|
+
// matches. The terminating + (not \;) batches files into one grep
|
|
1060
|
+
// invocation, matching the xargs throughput characteristic.
|
|
1061
|
+
cmd = `find ${escaped} -maxdepth ${maxDepth} -type f -exec grep ${grepFlags} ${patternEscaped} {} + 2>/dev/null | head -n ${maxResults + 1}`;
|
|
1027
1062
|
}
|
|
1028
1063
|
else {
|
|
1029
1064
|
const grepFlags = [fixedString ? "-F" : "", ignoreCase ? "-i" : "", "-n", filesWithMatches ? "-l" : ""].filter(f => f).join(" ");
|
|
1030
|
-
cmd = `grep ${grepFlags}
|
|
1065
|
+
cmd = `grep ${grepFlags} ${patternEscaped} ${escaped} 2>/dev/null | head -n ${maxResults + 1}`;
|
|
1031
1066
|
}
|
|
1032
1067
|
const result = await shell(cmd, {
|
|
1033
1068
|
device: serial, timeout: 60000, ignoreExitCode: true,
|
|
@@ -1043,7 +1078,7 @@ export function registerFileTools(ctx) {
|
|
|
1043
1078
|
if (truncated)
|
|
1044
1079
|
output += ` (truncated at ${maxResults} — more results exist)`;
|
|
1045
1080
|
output += `\nExecution time: ${Date.now() - start}ms`;
|
|
1046
|
-
return { content: [{ type: "text", text: output }] };
|
|
1081
|
+
return withResultHandle({ content: [{ type: "text", text: output }] }, "grep", { result_handle, result_handle_ttl });
|
|
1047
1082
|
}
|
|
1048
1083
|
catch (error) {
|
|
1049
1084
|
return { content: [{ type: "text", text: OutputProcessor.formatError(error) }], isError: true };
|
|
@@ -1064,7 +1099,7 @@ export function registerFileTools(ctx) {
|
|
|
1064
1099
|
const resolved = await ctx.deviceManager.resolveDevice(device);
|
|
1065
1100
|
const serial = resolved.serial;
|
|
1066
1101
|
const shell = execShell(ctx, root);
|
|
1067
|
-
const escaped =
|
|
1102
|
+
const escaped = shellQuote(filePath);
|
|
1068
1103
|
const realPath = await resolveRealPath(ctx, serial, filePath, root);
|
|
1069
1104
|
const blocked = isHardBlocked(realPath);
|
|
1070
1105
|
if (blocked)
|
|
@@ -1075,8 +1110,15 @@ export function registerFileTools(ctx) {
|
|
|
1075
1110
|
// Count matches before replacement
|
|
1076
1111
|
const sedFind = sedEscapePattern(find);
|
|
1077
1112
|
const countCmd = lineNumber
|
|
1078
|
-
|
|
1079
|
-
|
|
1113
|
+
// AD9 note: `lineNumber` is interpolated UNWRAPPED into the sed
|
|
1114
|
+
// expression here and at the sed -i site below. Safe by construction:
|
|
1115
|
+
// `lineNumber` is Zod-constrained to a positive integer (`z.number().min(1)`).
|
|
1116
|
+
// Integer-typed inputs are not vulnerable to the shell-metacharacter
|
|
1117
|
+
// / quote-escape attacks that string-typed inputs need protection
|
|
1118
|
+
// from. `shellQuote` would technically work but adds noise without
|
|
1119
|
+
// adding safety — the type system is the validation.
|
|
1120
|
+
? `sed -n '${lineNumber}p' ${escaped} | grep -cF ${shellQuote(find)} 2>/dev/null`
|
|
1121
|
+
: `grep -cF ${shellQuote(find)} ${escaped} 2>/dev/null`;
|
|
1080
1122
|
const countResult = await shell(countCmd, {
|
|
1081
1123
|
device: serial, timeout: 10000, ignoreExitCode: true,
|
|
1082
1124
|
});
|
|
@@ -1086,7 +1128,7 @@ export function registerFileTools(ctx) {
|
|
|
1086
1128
|
}
|
|
1087
1129
|
// Create backup if requested
|
|
1088
1130
|
if (backup) {
|
|
1089
|
-
await shell(`cp
|
|
1131
|
+
await shell(`cp ${escaped} '${escaped}.bak' 2>&1`, {
|
|
1090
1132
|
device: serial, timeout: 10000, ignoreExitCode: true,
|
|
1091
1133
|
});
|
|
1092
1134
|
}
|
|
@@ -1095,10 +1137,10 @@ export function registerFileTools(ctx) {
|
|
|
1095
1137
|
const gFlag = globalReplace ? "g" : "";
|
|
1096
1138
|
let sedCmd;
|
|
1097
1139
|
if (lineNumber) {
|
|
1098
|
-
sedCmd = `sed -i '${lineNumber}s/${sedFind}/${sedReplace}/${gFlag}'
|
|
1140
|
+
sedCmd = `sed -i '${lineNumber}s/${sedFind}/${sedReplace}/${gFlag}' ${escaped} 2>&1`;
|
|
1099
1141
|
}
|
|
1100
1142
|
else {
|
|
1101
|
-
sedCmd = `sed -i 's/${sedFind}/${sedReplace}/${gFlag}'
|
|
1143
|
+
sedCmd = `sed -i 's/${sedFind}/${sedReplace}/${gFlag}' ${escaped} 2>&1`;
|
|
1102
1144
|
}
|
|
1103
1145
|
const result = await shell(sedCmd, {
|
|
1104
1146
|
device: serial, timeout: 30000, ignoreExitCode: true,
|