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.
Files changed (230) hide show
  1. package/LICENSE +202 -21
  2. package/NOTICE +16 -0
  3. package/README.md +55 -29
  4. package/build/bridge/adb-bridge.d.ts +14 -0
  5. package/build/bridge/adb-bridge.d.ts.map +1 -1
  6. package/build/bridge/adb-bridge.js +34 -5
  7. package/build/bridge/adb-bridge.js.map +1 -1
  8. package/build/bridge/device-manager.d.ts.map +1 -1
  9. package/build/bridge/device-manager.js +18 -9
  10. package/build/bridge/device-manager.js.map +1 -1
  11. package/build/bridge/local-bridge.d.ts +10 -2
  12. package/build/bridge/local-bridge.d.ts.map +1 -1
  13. package/build/bridge/local-bridge.js +51 -16
  14. package/build/bridge/local-bridge.js.map +1 -1
  15. package/build/config/config.d.ts +7 -0
  16. package/build/config/config.d.ts.map +1 -1
  17. package/build/config/config.js +26 -10
  18. package/build/config/config.js.map +1 -1
  19. package/build/graphql-api.d.ts.map +1 -1
  20. package/build/graphql-api.js +73 -3
  21. package/build/graphql-api.js.map +1 -1
  22. package/build/http-transport.d.ts.map +1 -1
  23. package/build/http-transport.js +44 -7
  24. package/build/http-transport.js.map +1 -1
  25. package/build/index.d.ts.map +1 -1
  26. package/build/index.js +41 -16
  27. package/build/index.js.map +1 -1
  28. package/build/middleware/auth.d.ts.map +1 -1
  29. package/build/middleware/auth.js +43 -6
  30. package/build/middleware/auth.js.map +1 -1
  31. package/build/middleware/chipset.d.ts.map +1 -1
  32. package/build/middleware/chipset.js +16 -1
  33. package/build/middleware/chipset.js.map +1 -1
  34. package/build/middleware/cleanup.d.ts +39 -7
  35. package/build/middleware/cleanup.d.ts.map +1 -1
  36. package/build/middleware/cleanup.js +148 -16
  37. package/build/middleware/cleanup.js.map +1 -1
  38. package/build/middleware/fetch-utils.d.ts +50 -7
  39. package/build/middleware/fetch-utils.d.ts.map +1 -1
  40. package/build/middleware/fetch-utils.js +127 -14
  41. package/build/middleware/fetch-utils.js.map +1 -1
  42. package/build/middleware/fs-utils.d.ts +126 -0
  43. package/build/middleware/fs-utils.d.ts.map +1 -0
  44. package/build/middleware/fs-utils.js +229 -0
  45. package/build/middleware/fs-utils.js.map +1 -0
  46. package/build/middleware/logger.d.ts +16 -0
  47. package/build/middleware/logger.d.ts.map +1 -1
  48. package/build/middleware/logger.js +18 -0
  49. package/build/middleware/logger.js.map +1 -1
  50. package/build/middleware/output-processor.d.ts.map +1 -1
  51. package/build/middleware/output-processor.js +43 -8
  52. package/build/middleware/output-processor.js.map +1 -1
  53. package/build/middleware/parse-utils.d.ts +19 -0
  54. package/build/middleware/parse-utils.d.ts.map +1 -0
  55. package/build/middleware/parse-utils.js +30 -0
  56. package/build/middleware/parse-utils.js.map +1 -0
  57. package/build/middleware/png-utils.d.ts.map +1 -1
  58. package/build/middleware/png-utils.js +33 -11
  59. package/build/middleware/png-utils.js.map +1 -1
  60. package/build/middleware/result-handle.d.ts +76 -0
  61. package/build/middleware/result-handle.d.ts.map +1 -0
  62. package/build/middleware/result-handle.js +378 -0
  63. package/build/middleware/result-handle.js.map +1 -0
  64. package/build/middleware/sanitize.d.ts +26 -0
  65. package/build/middleware/sanitize.d.ts.map +1 -1
  66. package/build/middleware/sanitize.js +43 -2
  67. package/build/middleware/sanitize.js.map +1 -1
  68. package/build/middleware/security.d.ts.map +1 -1
  69. package/build/middleware/security.js +30 -5
  70. package/build/middleware/security.js.map +1 -1
  71. package/build/middleware/ui-dump.d.ts.map +1 -1
  72. package/build/middleware/ui-dump.js +30 -10
  73. package/build/middleware/ui-dump.js.map +1 -1
  74. package/build/server.d.ts.map +1 -1
  75. package/build/server.js +34 -5
  76. package/build/server.js.map +1 -1
  77. package/build/tool-context.d.ts.map +1 -1
  78. package/build/tool-context.js +2 -0
  79. package/build/tool-context.js.map +1 -1
  80. package/build/tools/accessibility.d.ts.map +1 -1
  81. package/build/tools/accessibility.js +39 -40
  82. package/build/tools/accessibility.js.map +1 -1
  83. package/build/tools/at-commands.d.ts.map +1 -1
  84. package/build/tools/at-commands.js +31 -6
  85. package/build/tools/at-commands.js.map +1 -1
  86. package/build/tools/baseband.d.ts.map +1 -1
  87. package/build/tools/baseband.js +18 -7
  88. package/build/tools/baseband.js.map +1 -1
  89. package/build/tools/build.d.ts.map +1 -1
  90. package/build/tools/build.js +30 -8
  91. package/build/tools/build.js.map +1 -1
  92. package/build/tools/ci.d.ts.map +1 -1
  93. package/build/tools/ci.js +16 -5
  94. package/build/tools/ci.js.map +1 -1
  95. package/build/tools/control.d.ts.map +1 -1
  96. package/build/tools/control.js +19 -11
  97. package/build/tools/control.js.map +1 -1
  98. package/build/tools/device-farm.d.ts.map +1 -1
  99. package/build/tools/device-farm.js +27 -3
  100. package/build/tools/device-farm.js.map +1 -1
  101. package/build/tools/device-profiles.d.ts.map +1 -1
  102. package/build/tools/device-profiles.js +70 -8
  103. package/build/tools/device-profiles.js.map +1 -1
  104. package/build/tools/device.d.ts.map +1 -1
  105. package/build/tools/device.js +4 -2
  106. package/build/tools/device.js.map +1 -1
  107. package/build/tools/diagnostics.d.ts.map +1 -1
  108. package/build/tools/diagnostics.js +17 -9
  109. package/build/tools/diagnostics.js.map +1 -1
  110. package/build/tools/emulator.d.ts.map +1 -1
  111. package/build/tools/emulator.js +12 -13
  112. package/build/tools/emulator.js.map +1 -1
  113. package/build/tools/files.d.ts +1 -1
  114. package/build/tools/files.d.ts.map +1 -1
  115. package/build/tools/files.js +98 -56
  116. package/build/tools/files.js.map +1 -1
  117. package/build/tools/firmware-analysis.d.ts.map +1 -1
  118. package/build/tools/firmware-analysis.js +35 -12
  119. package/build/tools/firmware-analysis.js.map +1 -1
  120. package/build/tools/forwarding.d.ts.map +1 -1
  121. package/build/tools/forwarding.js +248 -6
  122. package/build/tools/forwarding.js.map +1 -1
  123. package/build/tools/health.d.ts.map +1 -1
  124. package/build/tools/health.js +14 -3
  125. package/build/tools/health.js.map +1 -1
  126. package/build/tools/input-gestures.d.ts.map +1 -1
  127. package/build/tools/input-gestures.js +65 -13
  128. package/build/tools/input-gestures.js.map +1 -1
  129. package/build/tools/logcat-watch.d.ts.map +1 -1
  130. package/build/tools/logcat-watch.js +21 -14
  131. package/build/tools/logcat-watch.js.map +1 -1
  132. package/build/tools/logs.d.ts.map +1 -1
  133. package/build/tools/logs.js +8 -4
  134. package/build/tools/logs.js.map +1 -1
  135. package/build/tools/mirroring.d.ts.map +1 -1
  136. package/build/tools/mirroring.js +21 -19
  137. package/build/tools/mirroring.js.map +1 -1
  138. package/build/tools/multi-device.d.ts.map +1 -1
  139. package/build/tools/multi-device.js +10 -0
  140. package/build/tools/multi-device.js.map +1 -1
  141. package/build/tools/network-capture.d.ts.map +1 -1
  142. package/build/tools/network-capture.js +32 -8
  143. package/build/tools/network-capture.js.map +1 -1
  144. package/build/tools/network-discovery.d.ts.map +1 -1
  145. package/build/tools/network-discovery.js +11 -2
  146. package/build/tools/network-discovery.js.map +1 -1
  147. package/build/tools/ota-monitor.d.ts.map +1 -1
  148. package/build/tools/ota-monitor.js +26 -12
  149. package/build/tools/ota-monitor.js.map +1 -1
  150. package/build/tools/packages.d.ts.map +1 -1
  151. package/build/tools/packages.js +18 -11
  152. package/build/tools/packages.js.map +1 -1
  153. package/build/tools/plugins.d.ts +18 -0
  154. package/build/tools/plugins.d.ts.map +1 -1
  155. package/build/tools/plugins.js +47 -2
  156. package/build/tools/plugins.js.map +1 -1
  157. package/build/tools/prompts.d.ts.map +1 -1
  158. package/build/tools/prompts.js +2 -0
  159. package/build/tools/prompts.js.map +1 -1
  160. package/build/tools/qemu.d.ts +6 -3
  161. package/build/tools/qemu.d.ts.map +1 -1
  162. package/build/tools/qemu.js +28 -16
  163. package/build/tools/qemu.js.map +1 -1
  164. package/build/tools/registry.d.ts.map +1 -1
  165. package/build/tools/registry.js +76 -26
  166. package/build/tools/registry.js.map +1 -1
  167. package/build/tools/regression.d.ts +37 -0
  168. package/build/tools/regression.d.ts.map +1 -1
  169. package/build/tools/regression.js +63 -22
  170. package/build/tools/regression.js.map +1 -1
  171. package/build/tools/resources.d.ts.map +1 -1
  172. package/build/tools/resources.js +62 -15
  173. package/build/tools/resources.js.map +1 -1
  174. package/build/tools/result-handles.d.ts +37 -0
  175. package/build/tools/result-handles.d.ts.map +1 -0
  176. package/build/tools/result-handles.js +286 -0
  177. package/build/tools/result-handles.js.map +1 -0
  178. package/build/tools/ril-intercept.d.ts.map +1 -1
  179. package/build/tools/ril-intercept.js +15 -18
  180. package/build/tools/ril-intercept.js.map +1 -1
  181. package/build/tools/screen-record.d.ts.map +1 -1
  182. package/build/tools/screen-record.js +34 -5
  183. package/build/tools/screen-record.js.map +1 -1
  184. package/build/tools/screenshot-diff.d.ts.map +1 -1
  185. package/build/tools/screenshot-diff.js +29 -21
  186. package/build/tools/screenshot-diff.js.map +1 -1
  187. package/build/tools/selinux-audit.d.ts.map +1 -1
  188. package/build/tools/selinux-audit.js +7 -5
  189. package/build/tools/selinux-audit.js.map +1 -1
  190. package/build/tools/sensors.d.ts.map +1 -1
  191. package/build/tools/sensors.js +26 -4
  192. package/build/tools/sensors.js.map +1 -1
  193. package/build/tools/shell.d.ts.map +1 -1
  194. package/build/tools/shell.js +9 -0
  195. package/build/tools/shell.js.map +1 -1
  196. package/build/tools/snapshot.d.ts.map +1 -1
  197. package/build/tools/snapshot.js +38 -19
  198. package/build/tools/snapshot.js.map +1 -1
  199. package/build/tools/split-apk.d.ts.map +1 -1
  200. package/build/tools/split-apk.js +48 -24
  201. package/build/tools/split-apk.js.map +1 -1
  202. package/build/tools/test-gen.d.ts.map +1 -1
  203. package/build/tools/test-gen.js +29 -10
  204. package/build/tools/test-gen.js.map +1 -1
  205. package/build/tools/testing.d.ts.map +1 -1
  206. package/build/tools/testing.js +27 -9
  207. package/build/tools/testing.js.map +1 -1
  208. package/build/tools/thermal-power.d.ts +8 -1
  209. package/build/tools/thermal-power.d.ts.map +1 -1
  210. package/build/tools/thermal-power.js +35 -10
  211. package/build/tools/thermal-power.js.map +1 -1
  212. package/build/tools/ui.d.ts.map +1 -1
  213. package/build/tools/ui.js +20 -13
  214. package/build/tools/ui.js.map +1 -1
  215. package/build/tools/wireless-firmware.d.ts.map +1 -1
  216. package/build/tools/wireless-firmware.js +21 -7
  217. package/build/tools/wireless-firmware.js.map +1 -1
  218. package/build/tools/wireless.d.ts.map +1 -1
  219. package/build/tools/wireless.js +39 -3
  220. package/build/tools/wireless.js.map +1 -1
  221. package/build/tools/workflow-market.d.ts.map +1 -1
  222. package/build/tools/workflow-market.js +67 -13
  223. package/build/tools/workflow-market.js.map +1 -1
  224. package/build/tools/workflow.d.ts.map +1 -1
  225. package/build/tools/workflow.js +77 -8
  226. package/build/tools/workflow.js.map +1 -1
  227. package/build/ws-transport.d.ts.map +1 -1
  228. package/build/ws-transport.js +21 -3
  229. package/build/ws-transport.js.map +1 -1
  230. package/package.json +20 -8
@@ -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
- * shellEscape() for injection prevention.
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 { shellEscape } from "../middleware/sanitize.js";
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 '${shellEscape(path)}' 2>/dev/null || echo '${shellEscape(path)}'`, {
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 = shellEscape(path);
104
+ const escaped = shellQuote(path);
102
105
  const parentDir = path.replace(/\/[^/]+\/?$/, "") || "/";
103
- const parentEscaped = shellEscape(parentDir);
104
- const result = await shell(`df -h '${escaped}' 2>/dev/null || df -h '${parentEscaped}' 2>/dev/null`, { device: serial, timeout: 5000, ignoreExitCode: true });
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' '${shellEscape(path)}' 2>/dev/null`, {
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(/[[\].*^$\\\/&]/g, "\\$&")
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(/[\\\/&]/g, "\\$&")
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 '${shellEscape(path)}'` : `ls '${shellEscape(path)}'`;
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 '${shellEscape(path)}'`;
262
+ let cmd = `cat ${shellQuote(path)}`;
260
263
  if (maxLines)
261
- cmd = `head -n ${maxLines} '${shellEscape(path)}'`;
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} '${shellEscape(filePath)}'\n${content}\n${delimiter}`;
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' '${shellEscape(filePath)}' 2>/dev/null`, {
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
- }, async ({ searchPath, name, type, maxDepth, maxResults, root, device }) => {
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 '${shellEscape(searchPath)}' -maxdepth ${maxDepth}`;
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 '${shellEscape(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 = shellEscape(filePath);
399
+ const escaped = shellQuote(filePath);
396
400
  // stat + ls -Z for SELinux context in a single command
397
- const result = await shell(`stat '${escaped}' 2>&1 && echo '---SELINUX---' && ls -Zd '${escaped}' 2>/dev/null`, { device: serial, timeout: 10000, ignoreExitCode: true });
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 = shellEscape(filePath);
431
+ const escaped = shellQuote(filePath);
428
432
  // Get size first for timeout estimation
429
- const sizeResult = await shell(`stat -c '%s' '${escaped}' 2>/dev/null`, {
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} '${escaped}' 2>&1`, {
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}'${shellEscape(dirPath)}' 2>&1`, {
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 '${shellEscape(targetPath)}' 2>/dev/null | wc -l`, { device: serial, timeout: 15000, ignoreExitCode: true });
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} '${shellEscape(targetPath)}' 2>&1`, {
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' '${shellEscape(source)}' 2>/dev/null`, {
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 '${shellEscape(source)}' '${shellEscape(destination)}' 2>&1`, {
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 '${shellEscape(destination)}' && echo EXISTS`, {
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' '${shellEscape(source)}' 2>/dev/null && echo '---DF---' && df '${shellEscape(destination)}' 2>/dev/null | tail -1`, { device: serial, timeout: 5000, ignoreExitCode: true });
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}'${shellEscape(source)}' '${shellEscape(destination)}' 2>&1`, {
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' '${shellEscape(destination)}' 2>/dev/null`, {
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} '${shellEscape(filePath)}' 2>&1`, {
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' '${shellEscape(filePath)}' 2>/dev/null`, {
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 '${shellEscape(filePath)}' && echo EXISTS`, {
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} '${shellEscape(filePath)}' 2>&1`;
822
+ cmd = `touch -t ${touchTs} ${shellQuote(filePath)} 2>&1`;
793
823
  }
794
824
  else {
795
- cmd = `touch '${shellEscape(filePath)}' 2>&1`;
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' '${shellEscape(filePath)}' 2>/dev/null`, { device: serial, timeout: 3000, ignoreExitCode: true });
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 = shellEscape(filePath);
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' '${escaped}' 2>/dev/null`, {
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 '${escaped}' 2>/dev/null | tail -1`, {
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 '${escaped}' 2>/dev/null`, {
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} '${shellEscape(filePath)}' 2>&1`, {
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)' '${shellEscape(filePath)}' 2>/dev/null`, {
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
- }, async ({ pattern, path: searchPath, recursive, fixedString, ignoreCase, maxResults, maxDepth, filesWithMatches, root, device }) => {
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 = shellEscape(searchPath);
1021
- const patternEscaped = shellEscape(pattern);
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
- cmd = `find '${escaped}' -maxdepth ${maxDepth} -type f 2>/dev/null | xargs grep ${grepFlags} '${patternEscaped}' 2>/dev/null | head -n ${maxResults + 1}`;
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} '${patternEscaped}' '${escaped}' 2>/dev/null | head -n ${maxResults + 1}`;
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 = shellEscape(filePath);
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
- ? `sed -n '${lineNumber}p' '${escaped}' | grep -cF '${shellEscape(find)}' 2>/dev/null`
1079
- : `grep -cF '${shellEscape(find)}' '${escaped}' 2>/dev/null`;
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 '${escaped}' '${escaped}.bak' 2>&1`, {
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}' '${escaped}' 2>&1`;
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}' '${escaped}' 2>&1`;
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,