neoagent 2.1.12 → 2.1.14

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.
@@ -276,6 +276,49 @@ function extractZip(zipPath, destDir) {
276
276
  throw new Error('Neither unzip nor ditto is available to extract Android SDK archives');
277
277
  }
278
278
 
279
+ function listFilesRecursive(rootDir, predicate, bucket = []) {
280
+ for (const entry of fs.readdirSync(rootDir, { withFileTypes: true })) {
281
+ const fullPath = path.join(rootDir, entry.name);
282
+ if (entry.isDirectory()) {
283
+ listFilesRecursive(fullPath, predicate, bucket);
284
+ continue;
285
+ }
286
+ if (!predicate || predicate(fullPath, entry)) {
287
+ bucket.push(fullPath);
288
+ }
289
+ }
290
+ return bucket;
291
+ }
292
+
293
+ function resolveBundleInstallTargets(bundleDir) {
294
+ const apkFiles = listFilesRecursive(bundleDir, (filePath) => path.extname(filePath).toLowerCase() === '.apk')
295
+ .sort((a, b) => a.localeCompare(b));
296
+ if (apkFiles.length === 0) {
297
+ throw new Error('APK bundle did not contain any installable .apk files.');
298
+ }
299
+
300
+ const universalApk = apkFiles.find((filePath) => path.basename(filePath).toLowerCase() === 'universal.apk');
301
+ if (universalApk) {
302
+ return {
303
+ mode: 'single',
304
+ installPaths: [universalApk],
305
+ layout: 'universal',
306
+ };
307
+ }
308
+
309
+ if (apkFiles.length === 1) {
310
+ return {
311
+ mode: 'single',
312
+ installPaths: apkFiles,
313
+ layout: 'single-apk',
314
+ };
315
+ }
316
+
317
+ throw new Error(
318
+ 'APK bundles must include a universal APK. Export a universal .apks bundle or upload a single .apk instead.'
319
+ );
320
+ }
321
+
279
322
  function parseLatestCmdlineToolsUrl(xml) {
280
323
  const tag = platformTag() === 'mac' ? 'macosx' : 'linux';
281
324
  const packageMatch = xml.match(new RegExp(`<remotePackage\\s+path="cmdline-tools;latest">([\\s\\S]*?)<\\/remotePackage>`));
@@ -1104,7 +1147,7 @@ class AndroidController {
1104
1147
  }
1105
1148
 
1106
1149
  async screenshot(options = {}) {
1107
- const serial = await this.ensureDevice();
1150
+ const serial = options.serial || await this.ensureDevice();
1108
1151
  const filename = `android_${Date.now()}.png`;
1109
1152
  const fullPath = path.join(SCREENSHOTS_DIR, filename);
1110
1153
  await this.#run(`${quoteShell(adbBinary())} -s ${quoteShell(serial)} exec-out screencap -p > ${quoteShell(fullPath)}`, { timeout: 30000 });
@@ -1117,7 +1160,7 @@ class AndroidController {
1117
1160
  }
1118
1161
 
1119
1162
  async dumpUi(options = {}) {
1120
- const serial = await this.ensureDevice();
1163
+ const serial = options.serial || await this.ensureDevice();
1121
1164
  let xml = await this.#adb(serial, 'shell uiautomator dump --compressed /dev/tty', { timeout: 30000 });
1122
1165
  if (!String(xml || '').includes('<hierarchy')) {
1123
1166
  const remote = '/sdcard/neoagent-ui.xml';
@@ -1140,6 +1183,65 @@ class AndroidController {
1140
1183
  };
1141
1184
  }
1142
1185
 
1186
+ async #captureObservation(serial, options = {}) {
1187
+ const resolvedSerial = serial || await this.ensureDevice();
1188
+ const observation = {
1189
+ serial: resolvedSerial,
1190
+ screenshotPath: null,
1191
+ fullPath: null,
1192
+ uiDumpPath: null,
1193
+ nodeCount: null,
1194
+ preview: undefined,
1195
+ observationWarnings: [],
1196
+ };
1197
+
1198
+ if (options.screenshot !== false) {
1199
+ try {
1200
+ const shot = await this.screenshot({ serial: resolvedSerial });
1201
+ observation.screenshotPath = shot?.screenshotPath || null;
1202
+ observation.fullPath = shot?.fullPath || null;
1203
+ } catch (err) {
1204
+ observation.observationWarnings.push(`screenshot: ${err.message}`);
1205
+ }
1206
+ }
1207
+
1208
+ if (options.uiDump !== false) {
1209
+ try {
1210
+ const dump = await this.dumpUi({
1211
+ serial: resolvedSerial,
1212
+ includeNodes: options.includeNodes !== false,
1213
+ });
1214
+ observation.uiDumpPath = dump.uiDumpPath;
1215
+ observation.nodeCount = dump.nodeCount;
1216
+ observation.preview = dump.preview;
1217
+ } catch (err) {
1218
+ observation.observationWarnings.push(`ui_dump: ${err.message}`);
1219
+ }
1220
+ }
1221
+
1222
+ if (observation.observationWarnings.length === 0) {
1223
+ delete observation.observationWarnings;
1224
+ }
1225
+
1226
+ return observation;
1227
+ }
1228
+
1229
+ async observe(options = {}) {
1230
+ const serial = options.serial || await this.ensureDevice();
1231
+ const observation = await this.#captureObservation(serial, options);
1232
+ if (!observation.screenshotPath && !observation.uiDumpPath) {
1233
+ throw new Error(
1234
+ Array.isArray(observation.observationWarnings) && observation.observationWarnings.length > 0
1235
+ ? observation.observationWarnings.join(' | ')
1236
+ : 'Unable to capture Android observation',
1237
+ );
1238
+ }
1239
+ return {
1240
+ success: true,
1241
+ ...observation,
1242
+ };
1243
+ }
1244
+
1143
1245
  async #resolveSelector(args = {}) {
1144
1246
  const dump = await this.dumpUi({ includeNodes: false });
1145
1247
  const selector = {
@@ -1164,27 +1266,27 @@ class AndroidController {
1164
1266
  let y = Number(args.y);
1165
1267
  let node = null;
1166
1268
  let serial = await this.ensureDevice();
1167
- let uiDumpPath = null;
1269
+ let resolvedFromUiDumpPath = null;
1168
1270
 
1169
1271
  if (!Number.isFinite(x) || !Number.isFinite(y)) {
1170
1272
  const resolved = await this.#resolveSelector(args);
1171
1273
  serial = resolved.serial;
1172
1274
  node = resolved.node;
1173
- uiDumpPath = resolved.uiDumpPath;
1275
+ resolvedFromUiDumpPath = resolved.uiDumpPath;
1174
1276
  x = node.bounds.centerX;
1175
1277
  y = node.bounds.centerY;
1176
1278
  }
1177
1279
 
1178
1280
  await this.#adb(serial, `shell input tap ${Math.round(x)} ${Math.round(y)}`, { timeout: 15000 });
1179
- const shot = await this.screenshot();
1281
+ const observation = await this.#captureObservation(serial, args);
1180
1282
  return {
1181
1283
  success: true,
1182
1284
  serial,
1183
1285
  x: Math.round(x),
1184
1286
  y: Math.round(y),
1185
1287
  target: summarizeNode(node),
1186
- uiDumpPath,
1187
- screenshotPath: shot.screenshotPath,
1288
+ resolvedFromUiDumpPath,
1289
+ ...observation,
1188
1290
  };
1189
1291
  }
1190
1292
 
@@ -1193,13 +1295,13 @@ class AndroidController {
1193
1295
  let y = Number(args.y);
1194
1296
  let node = null;
1195
1297
  let serial = await this.ensureDevice();
1196
- let uiDumpPath = null;
1298
+ let resolvedFromUiDumpPath = null;
1197
1299
 
1198
1300
  if (!Number.isFinite(x) || !Number.isFinite(y)) {
1199
1301
  const resolved = await this.#resolveSelector(args);
1200
1302
  serial = resolved.serial;
1201
1303
  node = resolved.node;
1202
- uiDumpPath = resolved.uiDumpPath;
1304
+ resolvedFromUiDumpPath = resolved.uiDumpPath;
1203
1305
  x = node.bounds.centerX;
1204
1306
  y = node.bounds.centerY;
1205
1307
  }
@@ -1210,7 +1312,7 @@ class AndroidController {
1210
1312
  `shell input swipe ${Math.round(x)} ${Math.round(y)} ${Math.round(x)} ${Math.round(y)} ${Math.round(durationMs)}`,
1211
1313
  { timeout: Math.max(15000, durationMs + 5000) },
1212
1314
  );
1213
- const shot = await this.screenshot();
1315
+ const observation = await this.#captureObservation(serial, args);
1214
1316
  return {
1215
1317
  success: true,
1216
1318
  serial,
@@ -1218,8 +1320,8 @@ class AndroidController {
1218
1320
  y: Math.round(y),
1219
1321
  durationMs,
1220
1322
  target: summarizeNode(node),
1221
- uiDumpPath,
1222
- screenshotPath: shot.screenshotPath,
1323
+ resolvedFromUiDumpPath,
1324
+ ...observation,
1223
1325
  };
1224
1326
  }
1225
1327
 
@@ -1237,6 +1339,8 @@ class AndroidController {
1237
1339
  description: args.description,
1238
1340
  className: args.className,
1239
1341
  clickable: true,
1342
+ screenshot: false,
1343
+ uiDump: false,
1240
1344
  }).catch(() => {});
1241
1345
  }
1242
1346
 
@@ -1244,12 +1348,12 @@ class AndroidController {
1244
1348
  if (args.pressEnter) {
1245
1349
  await this.#adb(serial, 'shell input keyevent 66', { timeout: 10000 });
1246
1350
  }
1247
- const shot = await this.screenshot();
1351
+ const observation = await this.#captureObservation(serial, args);
1248
1352
  return {
1249
1353
  success: true,
1250
1354
  serial,
1251
1355
  typed: args.text || '',
1252
- screenshotPath: shot.screenshotPath,
1356
+ ...observation,
1253
1357
  };
1254
1358
  }
1255
1359
 
@@ -1264,11 +1368,11 @@ class AndroidController {
1264
1368
  throw new Error('x1, y1, x2, and y2 are required for android_swipe');
1265
1369
  }
1266
1370
  await this.#adb(serial, `shell input swipe ${Math.round(x1)} ${Math.round(y1)} ${Math.round(x2)} ${Math.round(y2)} ${Math.round(duration)}`, { timeout: 15000 });
1267
- const shot = await this.screenshot();
1371
+ const observation = await this.#captureObservation(serial, args);
1268
1372
  return {
1269
1373
  success: true,
1270
1374
  serial,
1271
- screenshotPath: shot.screenshotPath,
1375
+ ...observation,
1272
1376
  };
1273
1377
  }
1274
1378
 
@@ -1278,13 +1382,13 @@ class AndroidController {
1278
1382
  const keyCode = Number.isFinite(Number(raw)) ? Number(raw) : (DEFAULT_KEYEVENTS[raw] || null);
1279
1383
  if (!keyCode) throw new Error(`Unsupported Android key: ${args.key}`);
1280
1384
  await this.#adb(serial, `shell input keyevent ${keyCode}`, { timeout: 10000 });
1281
- const shot = await this.screenshot();
1385
+ const observation = await this.#captureObservation(serial, args);
1282
1386
  return {
1283
1387
  success: true,
1284
1388
  serial,
1285
1389
  key: args.key,
1286
1390
  keyCode,
1287
- screenshotPath: shot.screenshotPath,
1391
+ ...observation,
1288
1392
  };
1289
1393
  }
1290
1394
 
@@ -1304,13 +1408,17 @@ class AndroidController {
1304
1408
  clickable: args.clickable,
1305
1409
  });
1306
1410
  if (node) {
1307
- const shot = args.screenshot === false ? null : await this.screenshot();
1411
+ const observation = await this.#captureObservation(dump.serial, {
1412
+ screenshot: args.screenshot !== false,
1413
+ uiDump: args.uiDump !== false,
1414
+ includeNodes: args.includeNodes,
1415
+ });
1308
1416
  return {
1309
1417
  success: true,
1310
1418
  serial: dump.serial,
1311
1419
  matched: summarizeNode(node),
1312
- uiDumpPath: dump.uiDumpPath,
1313
- screenshotPath: shot?.screenshotPath || null,
1420
+ matchedFromUiDumpPath: dump.uiDumpPath,
1421
+ ...observation,
1314
1422
  };
1315
1423
  }
1316
1424
  await sleep(intervalMs);
@@ -1341,13 +1449,13 @@ class AndroidController {
1341
1449
  } else {
1342
1450
  throw new Error('packageName is required for android_open_app');
1343
1451
  }
1344
- const shot = await this.screenshot();
1452
+ const observation = await this.#captureObservation(serial, args);
1345
1453
  return {
1346
1454
  success: true,
1347
1455
  serial,
1348
1456
  packageName: args.packageName,
1349
1457
  activity: args.activity || null,
1350
- screenshotPath: shot.screenshotPath,
1458
+ ...observation,
1351
1459
  };
1352
1460
  }
1353
1461
 
@@ -1367,11 +1475,11 @@ class AndroidController {
1367
1475
  }
1368
1476
 
1369
1477
  await this.#adb(serial, parts.join(' '), { timeout: 20000 });
1370
- const shot = await this.screenshot();
1478
+ const observation = await this.#captureObservation(serial, args);
1371
1479
  return {
1372
1480
  success: true,
1373
1481
  serial,
1374
- screenshotPath: shot.screenshotPath,
1482
+ ...observation,
1375
1483
  };
1376
1484
  }
1377
1485
 
@@ -1397,11 +1505,38 @@ class AndroidController {
1397
1505
  const apkPath = path.resolve(String(args.apkPath || ''));
1398
1506
  if (!apkPath || !fs.existsSync(apkPath)) throw new Error(`APK not found: ${apkPath}`);
1399
1507
  const serial = await this.ensureDevice();
1508
+ const extension = path.extname(apkPath).toLowerCase();
1509
+
1510
+ if (extension === '.aab') {
1511
+ throw new Error('.aab app bundles are not directly installable. Export a .apks bundle or .apk first.');
1512
+ }
1513
+
1514
+ if (extension === '.apks') {
1515
+ const extractDir = fs.mkdtempSync(path.join(TMP_DIR, 'apk-bundle-'));
1516
+ try {
1517
+ extractZip(apkPath, extractDir);
1518
+ const bundle = resolveBundleInstallTargets(extractDir);
1519
+ await this.#adb(serial, `install -r ${quoteShell(bundle.installPaths[0])}`, { timeout: 300000 });
1520
+ return {
1521
+ success: true,
1522
+ serial,
1523
+ apkPath,
1524
+ artifactType: 'apks',
1525
+ installedPaths: bundle.installPaths,
1526
+ bundleLayout: bundle.layout,
1527
+ };
1528
+ } finally {
1529
+ fs.rmSync(extractDir, { recursive: true, force: true });
1530
+ }
1531
+ }
1532
+
1400
1533
  await this.#adb(serial, `install -r ${quoteShell(apkPath)}`, { timeout: 300000 });
1401
1534
  return {
1402
1535
  success: true,
1403
1536
  serial,
1404
1537
  apkPath,
1538
+ artifactType: 'apk',
1539
+ installedPaths: [apkPath],
1405
1540
  };
1406
1541
  }
1407
1542
 
@@ -1412,13 +1547,19 @@ class AndroidController {
1412
1547
 
1413
1548
  const timeout = Math.max(1000, Number(args.timeoutMs) || 20000);
1414
1549
  const stdout = await this.#adb(serial, `shell ${quoteShell(command)}`, { timeout });
1415
- const shot = args.screenshot === true ? await this.screenshot() : null;
1550
+ const observation = args.screenshot === true
1551
+ ? await this.#captureObservation(serial)
1552
+ : null;
1416
1553
  return {
1417
1554
  success: true,
1418
1555
  serial,
1419
1556
  command,
1420
1557
  stdout,
1421
- screenshotPath: shot?.screenshotPath || null,
1558
+ screenshotPath: observation?.screenshotPath || null,
1559
+ fullPath: observation?.fullPath || null,
1560
+ uiDumpPath: observation?.uiDumpPath || null,
1561
+ nodeCount: observation?.nodeCount,
1562
+ preview: observation?.preview,
1422
1563
  };
1423
1564
  }
1424
1565
 
@@ -76,6 +76,25 @@ function setupWebSocket(io, services) {
76
76
  }
77
77
  }
78
78
 
79
+ const activeRun = agentEngine.findSteerableRunForUser(userId, 'web');
80
+ if (activeRun) {
81
+ const queued = agentEngine.enqueueSteering(activeRun.runId, task, {
82
+ platform: 'web',
83
+ socketId: socket.id
84
+ });
85
+ if (queued) {
86
+ db.prepare('INSERT INTO conversation_history (user_id, agent_run_id, role, content, metadata) VALUES (?, ?, ?, ?, ?)')
87
+ .run(
88
+ userId,
89
+ activeRun.runId,
90
+ 'user',
91
+ task,
92
+ JSON.stringify({ platform: 'web', steering: true })
93
+ );
94
+ return;
95
+ }
96
+ }
97
+
79
98
  db.prepare('INSERT INTO conversation_history (user_id, role, content, metadata) VALUES (?, ?, ?, ?)')
80
99
  .run(userId, 'user', task, JSON.stringify({ platform: 'web' }));
81
100