neoagent 2.1.10 → 2.1.12

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.
@@ -213,6 +213,24 @@ function getAvailableTools(app, options = {}) {
213
213
  }
214
214
  }
215
215
  },
216
+ {
217
+ name: 'android_long_press',
218
+ description: 'Long-press an Android UI element or screen coordinate. Useful for context menus, drag handles, rearranging icons, and long-click actions.',
219
+ parameters: {
220
+ type: 'object',
221
+ properties: {
222
+ x: { type: 'number', description: 'Absolute X coordinate' },
223
+ y: { type: 'number', description: 'Absolute Y coordinate' },
224
+ text: { type: 'string', description: 'Visible text to match in the UI dump' },
225
+ resourceId: { type: 'string', description: 'Android resource-id to match' },
226
+ description: { type: 'string', description: 'content-desc / accessibility label to match' },
227
+ className: { type: 'string', description: 'Optional class name filter' },
228
+ packageName: { type: 'string', description: 'Optional package filter' },
229
+ clickable: { type: 'boolean', description: 'Prefer clickable elements' },
230
+ durationMs: { type: 'number', description: 'Press duration in milliseconds (default 650)' }
231
+ }
232
+ }
233
+ },
216
234
  {
217
235
  name: 'android_type',
218
236
  description: 'Type text into the focused Android field, optionally tapping a matched element first.',
@@ -313,6 +331,19 @@ function getAvailableTools(app, options = {}) {
313
331
  required: ['apkPath']
314
332
  }
315
333
  },
334
+ {
335
+ name: 'android_shell',
336
+ description: 'Run an adb shell command on the active Android device or emulator. Use this when a needed phone action is not covered by a higher-level Android tool.',
337
+ parameters: {
338
+ type: 'object',
339
+ properties: {
340
+ command: { type: 'string', description: 'Shell command to run on-device, without the leading "adb shell"' },
341
+ timeoutMs: { type: 'number', description: 'Timeout in milliseconds (default 20000)' },
342
+ screenshot: { type: 'boolean', description: 'Capture a screenshot after the command if it changes the UI (default false)' }
343
+ },
344
+ required: ['command']
345
+ }
346
+ },
316
347
  {
317
348
  name: 'web_search',
318
349
  description: 'Search the public web without opening the browser. Uses Brave Search API for fast result retrieval.',
@@ -879,6 +910,12 @@ async function executeTool(toolName, args, context, engine) {
879
910
  return await controller.tap(args || {});
880
911
  }
881
912
 
913
+ case 'android_long_press': {
914
+ const controller = ac();
915
+ if (!controller) return { error: 'Android controller not available' };
916
+ return await controller.longPress(args || {});
917
+ }
918
+
882
919
  case 'android_type': {
883
920
  const controller = ac();
884
921
  if (!controller) return { error: 'Android controller not available' };
@@ -927,6 +964,12 @@ async function executeTool(toolName, args, context, engine) {
927
964
  return await controller.installApk(args || {});
928
965
  }
929
966
 
967
+ case 'android_shell': {
968
+ const controller = ac();
969
+ if (!controller) return { error: 'Android controller not available' };
970
+ return await controller.shell(args || {});
971
+ }
972
+
930
973
  case 'web_search': {
931
974
  const apiKey = process.env.BRAVE_SEARCH_API_KEY;
932
975
  if (!apiKey) return { error: 'BRAVE_SEARCH_API_KEY is not configured' };
@@ -64,6 +64,25 @@ function commandExists(command) {
64
64
  return probe.status === 0;
65
65
  }
66
66
 
67
+ function parseResolvedLaunchComponent(output, packageName) {
68
+ const lines = String(output || '')
69
+ .split('\n')
70
+ .map((line) => line.trim())
71
+ .filter(Boolean);
72
+ const normalizedPackage = String(packageName || '').trim();
73
+ const componentPattern = /^[A-Za-z0-9._$]+\/[A-Za-z0-9._$]+$/;
74
+ const relativePattern = /^[A-Za-z0-9._$]+\/\.[A-Za-z0-9._$]+$/;
75
+
76
+ const exact = lines.find((line) =>
77
+ normalizedPackage
78
+ ? line.startsWith(`${normalizedPackage}/`)
79
+ : componentPattern.test(line) || relativePattern.test(line)
80
+ );
81
+ if (exact) return exact;
82
+
83
+ return lines.find((line) => componentPattern.test(line) || relativePattern.test(line)) || null;
84
+ }
85
+
67
86
  function appendState(patch) {
68
87
  const current = readState();
69
88
  const next = {
@@ -470,6 +489,39 @@ function systemImagePackageToRelativeDir(packageName) {
470
489
  return `${parts.join('/')}/`;
471
490
  }
472
491
 
492
+ function systemImagePackageToAbi(packageName) {
493
+ const parts = String(packageName || '').split(';').filter(Boolean);
494
+ if (parts.length !== 4 || parts[0] !== 'system-images') {
495
+ return null;
496
+ }
497
+ return parts[3] || null;
498
+ }
499
+
500
+ function abiToCpuArch(abi) {
501
+ const value = String(abi || '').trim().toLowerCase();
502
+ if (value === 'arm64-v8a') return 'arm64';
503
+ if (value === 'armeabi-v7a' || value === 'armeabi') return 'arm';
504
+ if (value === 'x86_64') return 'x86_64';
505
+ if (value === 'x86') return 'x86';
506
+ return null;
507
+ }
508
+
509
+ function systemImagePackageToCpuArch(packageName) {
510
+ return abiToCpuArch(systemImagePackageToAbi(packageName));
511
+ }
512
+
513
+ function describeAutoFixChanges(current, next, fields = []) {
514
+ return fields
515
+ .map((field) => {
516
+ const before = current?.[field] ?? null;
517
+ const after = next?.[field] ?? null;
518
+ if (before === after) return null;
519
+ return `${field}: ${before ?? 'null'} -> ${after ?? 'null'}`;
520
+ })
521
+ .filter(Boolean)
522
+ .join(', ');
523
+ }
524
+
473
525
  function sanitizeUiXml(raw) {
474
526
  const text = String(raw || '');
475
527
  const start = text.indexOf('<?xml');
@@ -581,7 +633,28 @@ class AndroidController {
581
633
  if (!preferredInstalled) {
582
634
  throw new Error(formatSystemImageError(installedImages));
583
635
  }
584
- if (preferredInstalled.packageName !== state.systemImage) {
636
+ const stateApiLevel = Number(state.apiLevel || 0) || 0;
637
+ const stateNeedsRefresh =
638
+ preferredInstalled.packageName !== state.systemImage ||
639
+ preferredInstalled.apiLevel !== stateApiLevel ||
640
+ preferredInstalled.arch !== state.systemImageArch;
641
+ if (stateNeedsRefresh) {
642
+ const changeSummary = describeAutoFixChanges(
643
+ {
644
+ systemImage: state.systemImage || null,
645
+ apiLevel: stateApiLevel || null,
646
+ systemImageArch: state.systemImageArch || null,
647
+ },
648
+ {
649
+ systemImage: preferredInstalled.packageName,
650
+ apiLevel: preferredInstalled.apiLevel,
651
+ systemImageArch: preferredInstalled.arch,
652
+ },
653
+ ['systemImage', 'apiLevel', 'systemImageArch']
654
+ );
655
+ if (changeSummary) {
656
+ console.log(`[Android] Auto-fixed preferred system image (${changeSummary})`);
657
+ }
585
658
  appendState({
586
659
  bootstrapped: true,
587
660
  systemImage: preferredInstalled.packageName,
@@ -670,19 +743,39 @@ class AndroidController {
670
743
  if (!pkg) throw new Error('Android system image not installed');
671
744
  const avdExists = list.includes(`Name: ${this.avdName}`);
672
745
  let avdNeedsRecreate = avdExists && (!state.avdSystemImage || state.avdSystemImage !== pkg);
746
+ const avdRecreateReasons = [];
747
+ if (avdNeedsRecreate && state.avdSystemImage !== pkg) {
748
+ avdRecreateReasons.push(`systemImage: ${state.avdSystemImage || 'null'} -> ${pkg}`);
749
+ }
673
750
  const configPath = path.join(AVD_HOME, `${this.avdName}.avd`, 'config.ini');
674
751
  if (avdExists && fs.existsSync(configPath)) {
675
752
  try {
676
753
  const config = fs.readFileSync(configPath, 'utf8');
677
754
  const currentImageDir = readIniValue(config, 'image.sysdir.1');
678
755
  const expectedImageDir = systemImagePackageToRelativeDir(pkg);
756
+ const currentAbi = readIniValue(config, 'abi.type');
757
+ const expectedAbi = systemImagePackageToAbi(pkg);
758
+ const currentCpuArch = readIniValue(config, 'hw.cpu.arch');
759
+ const expectedCpuArch = systemImagePackageToCpuArch(pkg);
679
760
  if (expectedImageDir && currentImageDir && currentImageDir !== expectedImageDir) {
680
761
  avdNeedsRecreate = true;
762
+ avdRecreateReasons.push(`image.sysdir.1: ${currentImageDir} -> ${expectedImageDir}`);
763
+ }
764
+ if (expectedAbi && currentAbi && currentAbi !== expectedAbi) {
765
+ avdNeedsRecreate = true;
766
+ avdRecreateReasons.push(`abi.type: ${currentAbi} -> ${expectedAbi}`);
767
+ }
768
+ if (expectedCpuArch && currentCpuArch && currentCpuArch !== expectedCpuArch) {
769
+ avdNeedsRecreate = true;
770
+ avdRecreateReasons.push(`hw.cpu.arch: ${currentCpuArch} -> ${expectedCpuArch}`);
681
771
  }
682
772
  } catch {}
683
773
  }
684
774
 
685
775
  if (avdNeedsRecreate) {
776
+ if (avdRecreateReasons.length > 0) {
777
+ console.log(`[Android] Recreating AVD to repair config mismatch (${avdRecreateReasons.join(', ')})`);
778
+ }
686
779
  await this.stopEmulator().catch(() => {});
687
780
  await this.#run(`${quoteShell(avdManagerBinary())} delete avd -n ${quoteShell(this.avdName)}`, {
688
781
  timeout: 120000,
@@ -1095,6 +1188,41 @@ class AndroidController {
1095
1188
  };
1096
1189
  }
1097
1190
 
1191
+ async longPress(args = {}) {
1192
+ let x = Number(args.x);
1193
+ let y = Number(args.y);
1194
+ let node = null;
1195
+ let serial = await this.ensureDevice();
1196
+ let uiDumpPath = null;
1197
+
1198
+ if (!Number.isFinite(x) || !Number.isFinite(y)) {
1199
+ const resolved = await this.#resolveSelector(args);
1200
+ serial = resolved.serial;
1201
+ node = resolved.node;
1202
+ uiDumpPath = resolved.uiDumpPath;
1203
+ x = node.bounds.centerX;
1204
+ y = node.bounds.centerY;
1205
+ }
1206
+
1207
+ const durationMs = Math.max(250, Number(args.durationMs) || 650);
1208
+ await this.#adb(
1209
+ serial,
1210
+ `shell input swipe ${Math.round(x)} ${Math.round(y)} ${Math.round(x)} ${Math.round(y)} ${Math.round(durationMs)}`,
1211
+ { timeout: Math.max(15000, durationMs + 5000) },
1212
+ );
1213
+ const shot = await this.screenshot();
1214
+ return {
1215
+ success: true,
1216
+ serial,
1217
+ x: Math.round(x),
1218
+ y: Math.round(y),
1219
+ durationMs,
1220
+ target: summarizeNode(node),
1221
+ uiDumpPath,
1222
+ screenshotPath: shot.screenshotPath,
1223
+ };
1224
+ }
1225
+
1098
1226
  async type(args = {}) {
1099
1227
  const serial = await this.ensureDevice();
1100
1228
  if (args.clear === true) {
@@ -1196,7 +1324,20 @@ class AndroidController {
1196
1324
  if (args.activity) {
1197
1325
  await this.#adb(serial, `shell am start -n ${quoteShell(`${args.packageName}/${args.activity}`)}`, { timeout: 20000 });
1198
1326
  } else if (args.packageName) {
1199
- await this.#adb(serial, `shell monkey -p ${quoteShell(args.packageName)} -c android.intent.category.LAUNCHER 1`, { timeout: 30000 });
1327
+ const resolved = await this.#runAllowFailure(
1328
+ `${quoteShell(adbBinary())} -s ${quoteShell(serial)} shell cmd package resolve-activity --brief -c android.intent.category.LAUNCHER ${quoteShell(args.packageName)}`,
1329
+ { timeout: 15000 },
1330
+ );
1331
+ const component = parseResolvedLaunchComponent(
1332
+ `${resolved.stdout || ''}\n${resolved.stderr || ''}`,
1333
+ args.packageName,
1334
+ );
1335
+
1336
+ if (component) {
1337
+ await this.#adb(serial, `shell am start -n ${quoteShell(component)}`, { timeout: 20000 });
1338
+ } else {
1339
+ await this.#adb(serial, `shell monkey -p ${quoteShell(args.packageName)} -c android.intent.category.LAUNCHER 1`, { timeout: 30000 });
1340
+ }
1200
1341
  } else {
1201
1342
  throw new Error('packageName is required for android_open_app');
1202
1343
  }
@@ -1264,6 +1405,23 @@ class AndroidController {
1264
1405
  };
1265
1406
  }
1266
1407
 
1408
+ async shell(args = {}) {
1409
+ const serial = await this.ensureDevice();
1410
+ const command = String(args.command || '').trim();
1411
+ if (!command) throw new Error('command is required for android_shell');
1412
+
1413
+ const timeout = Math.max(1000, Number(args.timeoutMs) || 20000);
1414
+ const stdout = await this.#adb(serial, `shell ${quoteShell(command)}`, { timeout });
1415
+ const shot = args.screenshot === true ? await this.screenshot() : null;
1416
+ return {
1417
+ success: true,
1418
+ serial,
1419
+ command,
1420
+ stdout,
1421
+ screenshotPath: shot?.screenshotPath || null,
1422
+ };
1423
+ }
1424
+
1267
1425
  async getStatus() {
1268
1426
  const devices = isExecutable(adbBinary())
1269
1427
  ? await this.listDevices({ ensureBootstrapped: false }).catch(() => [])
@@ -1332,6 +1490,7 @@ module.exports = {
1332
1490
  configuredSystemImagePackage,
1333
1491
  configuredSystemImagePlatform,
1334
1492
  formatSystemImageError,
1493
+ parseResolvedLaunchComponent,
1335
1494
  parseLatestCmdlineToolsUrl,
1336
1495
  parseSystemImages,
1337
1496
  sanitizeUiXml,