@houwert/conductor 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,487 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HELP = void 0;
4
+ exports.memory = memory;
5
+ exports.HELP = ` memory [<appId>] Show device + app memory usage and object counts`;
6
+ const runner_js_1 = require("../runner.js");
7
+ const session_js_1 = require("../session.js");
8
+ const output_js_1 = require("../output.js");
9
+ const bootstrap_js_1 = require("../drivers/bootstrap.js");
10
+ const ios_js_1 = require("../drivers/ios.js");
11
+ const android_js_1 = require("../drivers/android.js");
12
+ const web_js_1 = require("../drivers/web.js");
13
+ async function resolveDeviceId(sessionName) {
14
+ if (sessionName !== 'default')
15
+ return sessionName;
16
+ const session = await (0, session_js_1.getSession)(sessionName);
17
+ return session.deviceId ?? (await (0, runner_js_1.detectFirstDevice)());
18
+ }
19
+ async function resolveAppId(explicit, sessionName) {
20
+ if (explicit)
21
+ return explicit;
22
+ const session = await (0, session_js_1.getSession)(sessionName);
23
+ if (session.appId)
24
+ return session.appId;
25
+ // Fall back to whatever's in the foreground.
26
+ try {
27
+ const driver = await (0, runner_js_1.getDriver)(sessionName);
28
+ if (driver instanceof android_js_1.AndroidDriver)
29
+ return await driver.getForegroundApp();
30
+ if (driver instanceof web_js_1.WebDriver)
31
+ return await driver.runningApp();
32
+ if (driver instanceof ios_js_1.IOSDriver)
33
+ return await driver.runningApp([]);
34
+ }
35
+ catch {
36
+ /* ignore */
37
+ }
38
+ return undefined;
39
+ }
40
+ // ── Android ───────────────────────────────────────────────────────────────────
41
+ function parseAndroidMeminfo(out) {
42
+ const get = (key) => {
43
+ const m = out.match(new RegExp(`^${key}:\\s+(\\d+)\\s*kB`, 'm'));
44
+ return m ? Number(m[1]) * 1024 : undefined;
45
+ };
46
+ return {
47
+ totalBytes: get('MemTotal'),
48
+ availableBytes: get('MemAvailable'),
49
+ freeBytes: get('MemFree'),
50
+ cachedBytes: get('Cached'),
51
+ swapTotalBytes: get('SwapTotal'),
52
+ swapFreeBytes: get('SwapFree'),
53
+ };
54
+ }
55
+ function parseAndroidDumpsysMeminfo(out) {
56
+ const app = {};
57
+ const objects = {};
58
+ const pidMatch = out.match(/\*\* MEMINFO in pid (\d+)/);
59
+ const pid = pidMatch ? Number(pidMatch[1]) : undefined;
60
+ // App Summary block — most useful, single-line entries with KB values.
61
+ // e.g. " Java Heap: 12345"
62
+ const summary = out.match(/App Summary[\s\S]*?(?:\n\s*\n|TOTAL:)/);
63
+ if (summary) {
64
+ const block = summary[0];
65
+ const grab = (label) => {
66
+ const m = block.match(new RegExp(`${label}:\\s+(\\d+)`));
67
+ return m ? Number(m[1]) * 1024 : undefined;
68
+ };
69
+ app.javaHeapBytes = grab('Java Heap');
70
+ app.nativeHeapBytes = grab('Native Heap');
71
+ app.codeBytes = grab('Code');
72
+ app.stackBytes = grab('Stack');
73
+ app.graphicsBytes = grab('Graphics');
74
+ app.privateOtherBytes = grab('Private Other');
75
+ app.systemBytes = grab('System');
76
+ const totalPss = block.match(/TOTAL PSS:\s+(\d+)/);
77
+ if (totalPss)
78
+ app.totalPssBytes = Number(totalPss[1]) * 1024;
79
+ const totalRss = block.match(/TOTAL RSS:\s+(\d+)/);
80
+ if (totalRss)
81
+ app.totalRssBytes = Number(totalRss[1]) * 1024;
82
+ const totalSwap = block.match(/TOTAL SWAP[^:]*:\s+(\d+)/);
83
+ if (totalSwap)
84
+ app['totalSwapBytes'] = Number(totalSwap[1]) * 1024;
85
+ }
86
+ // Fallback: TOTAL line in the main table — "TOTAL 12345 ..."
87
+ if (app.totalPssBytes === undefined) {
88
+ const total = out.match(/^\s*TOTAL\s+(\d+)/m);
89
+ if (total)
90
+ app.totalPssBytes = Number(total[1]) * 1024;
91
+ }
92
+ // Objects block:
93
+ // Objects
94
+ // Views: 3 ViewRootImpl: 1
95
+ // AppContexts: 2 Activities: 1
96
+ // Assets: 7 AssetManagers: 0
97
+ // Local Binders: 5 Proxy Binders: 17
98
+ // Parcel memory: 1 Parcel count: 4
99
+ // Death Recipients: 0 OpenSSL Sockets: 0
100
+ // WebViews: 0
101
+ const objSection = out.match(/Objects[\s\S]*?(?:\n\s*\n|SQL\s|DATABASES|$)/);
102
+ if (objSection) {
103
+ const text = objSection[0];
104
+ // Capture every "Label: number" pair (labels may have spaces).
105
+ const re = /([A-Za-z][A-Za-z _]*?):\s+(\d+)/g;
106
+ let m;
107
+ while ((m = re.exec(text)) !== null) {
108
+ const key = m[1].trim();
109
+ if (key === 'Objects')
110
+ continue;
111
+ objects[key] = Number(m[2]);
112
+ }
113
+ }
114
+ return { app, objects, pid };
115
+ }
116
+ async function collectAndroid(deviceId, appId) {
117
+ const report = { platform: 'android', deviceId, appId, notes: [] };
118
+ const meminfo = await (0, runner_js_1.spawnCommand)('adb', ['-s', deviceId, 'shell', 'cat', '/proc/meminfo']);
119
+ if (meminfo.success) {
120
+ const sys = parseAndroidMeminfo(meminfo.stdout);
121
+ report.system = {
122
+ ...sys,
123
+ usedBytes: sys.totalBytes !== undefined && sys.availableBytes !== undefined
124
+ ? sys.totalBytes - sys.availableBytes
125
+ : undefined,
126
+ };
127
+ }
128
+ else {
129
+ report.notes.push(`/proc/meminfo unavailable: ${meminfo.stderr.trim()}`);
130
+ }
131
+ if (appId) {
132
+ const dump = await (0, runner_js_1.spawnCommand)('adb', ['-s', deviceId, 'shell', 'dumpsys', 'meminfo', appId]);
133
+ if (dump.success && !dump.stdout.includes('No process found')) {
134
+ const { app, objects, pid } = parseAndroidDumpsysMeminfo(dump.stdout);
135
+ report.app = app;
136
+ report.objects = objects;
137
+ report.pid = pid;
138
+ }
139
+ else {
140
+ report.notes.push(`dumpsys meminfo ${appId} returned no data — app may not be running`);
141
+ }
142
+ }
143
+ if (report.notes.length === 0)
144
+ delete report.notes;
145
+ return report;
146
+ }
147
+ // ── iOS / tvOS ────────────────────────────────────────────────────────────────
148
+ function parseVmStat(out) {
149
+ // vm_stat output uses "page size of 16384 bytes" and counts in pages.
150
+ const pageMatch = out.match(/page size of (\d+)/);
151
+ const pageSize = pageMatch ? Number(pageMatch[1]) : 4096;
152
+ const grab = (label) => {
153
+ const m = out.match(new RegExp(`${label}:\\s+(\\d+)`));
154
+ return m ? Number(m[1]) * pageSize : undefined;
155
+ };
156
+ const free = grab('Pages free');
157
+ const inactive = grab('Pages inactive');
158
+ const speculative = grab('Pages speculative');
159
+ const wired = grab('Pages wired down');
160
+ const active = grab('Pages active');
161
+ const compressed = grab('Pages occupied by compressor');
162
+ const total = [free, inactive, speculative, wired, active, compressed]
163
+ .filter((v) => v !== undefined)
164
+ .reduce((a, b) => a + b, 0);
165
+ const available = free !== undefined && inactive !== undefined && speculative !== undefined
166
+ ? free + inactive + speculative
167
+ : undefined;
168
+ return {
169
+ totalBytes: total > 0 ? total : undefined,
170
+ freeBytes: free,
171
+ availableBytes: available,
172
+ };
173
+ }
174
+ async function findIOSPid(deviceId, appId) {
175
+ // Inside-simulator PIDs == host PIDs for app processes.
176
+ const list = await (0, runner_js_1.spawnCommand)('xcrun', ['simctl', 'spawn', deviceId, 'launchctl', 'list']);
177
+ if (!list.success)
178
+ return undefined;
179
+ for (const line of list.stdout.split('\n')) {
180
+ if (!line.includes(appId))
181
+ continue;
182
+ const parts = line.trim().split(/\s+/);
183
+ const pid = Number(parts[0]);
184
+ if (Number.isFinite(pid) && pid > 0)
185
+ return pid;
186
+ }
187
+ return undefined;
188
+ }
189
+ function parseVmmapSummary(out) {
190
+ const regions = {};
191
+ const app = {};
192
+ // Lines look like: "MALLOC 1.2G 234.5M ..."
193
+ // We want the "RESIDENT" column (2nd numeric). The columns are whitespace-delimited.
194
+ // Easier: parse the per-region rows under "REGION TYPE" header.
195
+ const headerIdx = out.indexOf('REGION TYPE');
196
+ if (headerIdx === -1)
197
+ return { app, regions };
198
+ const body = out.slice(headerIdx);
199
+ const lines = body.split('\n').slice(1);
200
+ for (const raw of lines) {
201
+ const line = raw.trimEnd();
202
+ if (!line.trim())
203
+ continue;
204
+ if (line.startsWith('='))
205
+ break;
206
+ if (line.startsWith('TOTAL')) {
207
+ // "TOTAL 5.2G 1.4G 950M ..."
208
+ const m = line.match(/TOTAL\s+\S+\s+(\S+)/);
209
+ if (m)
210
+ app.totalRssBytes = humanToBytes(m[1]);
211
+ break;
212
+ }
213
+ // Region rows: name (may have spaces), virtual, resident, dirty, swap, ...
214
+ const m = line.match(/^(.+?)\s{2,}(\S+)\s+(\S+)/);
215
+ if (!m)
216
+ continue;
217
+ const name = m[1].trim();
218
+ const resident = humanToBytes(m[3]);
219
+ if (resident !== undefined)
220
+ regions[name] = resident;
221
+ }
222
+ // Roll up a few well-known regions into the canonical fields.
223
+ app.nativeHeapBytes = regions['MALLOC'] ?? regions['MALLOC_NANO'];
224
+ app.stackBytes = regions['Stack'];
225
+ return { app, regions };
226
+ }
227
+ function humanToBytes(s) {
228
+ // Matches "1.2G", "234.5M", "950K", "1024", "1.2GB" etc.
229
+ const m = s.match(/^([\d.]+)\s*([KMGT]?)B?$/i);
230
+ if (!m)
231
+ return undefined;
232
+ const n = parseFloat(m[1]);
233
+ if (Number.isNaN(n))
234
+ return undefined;
235
+ const mult = {
236
+ '': 1,
237
+ K: 1024,
238
+ M: 1024 ** 2,
239
+ G: 1024 ** 3,
240
+ T: 1024 ** 4,
241
+ };
242
+ return Math.round(n * (mult[m[2].toUpperCase()] ?? 1));
243
+ }
244
+ async function collectIOS(deviceId, platform, appId) {
245
+ const report = { platform, deviceId, appId, notes: [] };
246
+ // System-wide memory: vm_stat from inside the simulator (== host RAM).
247
+ const vm = await (0, runner_js_1.spawnCommand)('xcrun', ['simctl', 'spawn', deviceId, 'vm_stat']);
248
+ if (vm.success) {
249
+ const sys = parseVmStat(vm.stdout);
250
+ report.system = {
251
+ ...sys,
252
+ usedBytes: sys.totalBytes !== undefined && sys.availableBytes !== undefined
253
+ ? sys.totalBytes - sys.availableBytes
254
+ : undefined,
255
+ };
256
+ report.notes.push('System memory reflects host Mac RAM — simulators share the host memory pool.');
257
+ }
258
+ else {
259
+ report.notes.push(`vm_stat unavailable: ${vm.stderr.trim()}`);
260
+ }
261
+ if (!appId) {
262
+ if (report.notes.length === 0)
263
+ delete report.notes;
264
+ return report;
265
+ }
266
+ const pid = await findIOSPid(deviceId, appId);
267
+ if (!pid) {
268
+ report.notes.push(`No running process found for ${appId}.`);
269
+ if (report.notes.length === 0)
270
+ delete report.notes;
271
+ return report;
272
+ }
273
+ report.pid = pid;
274
+ // ps for RSS/VSZ — fast, always available.
275
+ const ps = await (0, runner_js_1.spawnCommand)('ps', ['-o', 'rss=,vsz=', '-p', String(pid)]);
276
+ if (ps.success) {
277
+ const m = ps.stdout.trim().match(/(\d+)\s+(\d+)/);
278
+ if (m) {
279
+ report.app = {
280
+ ...(report.app ?? {}),
281
+ // ps reports rss/vsz in KB on macOS.
282
+ totalRssBytes: Number(m[1]) * 1024,
283
+ vszBytes: Number(m[2]) * 1024,
284
+ };
285
+ }
286
+ }
287
+ // vmmap -summary for region breakdown — slower but provides categories.
288
+ const vmmap = await (0, runner_js_1.spawnCommand)('vmmap', ['-summary', String(pid)]);
289
+ if (vmmap.success) {
290
+ const { app, regions } = parseVmmapSummary(vmmap.stdout);
291
+ report.app = {
292
+ ...(report.app ?? {}),
293
+ ...Object.fromEntries(Object.entries(app).filter(([, v]) => v !== undefined)),
294
+ regions,
295
+ };
296
+ }
297
+ else {
298
+ report.notes.push('vmmap unavailable — run `sudo DevToolsSecurity --enable` if it errors.');
299
+ }
300
+ if (report.notes.length === 0)
301
+ delete report.notes;
302
+ return report;
303
+ }
304
+ // ── Web (Playwright via CDP) ──────────────────────────────────────────────────
305
+ async function collectWeb(deviceId, sessionName) {
306
+ const report = { platform: 'web', deviceId, notes: [] };
307
+ let driver;
308
+ try {
309
+ const d = await (0, runner_js_1.getDriver)(sessionName);
310
+ if (!(d instanceof web_js_1.WebDriver)) {
311
+ report.notes.push('Expected web driver, got something else.');
312
+ return report;
313
+ }
314
+ driver = d;
315
+ }
316
+ catch (err) {
317
+ report.notes.push(`Could not attach to web driver: ${err instanceof Error ? err.message : String(err)}`);
318
+ return report;
319
+ }
320
+ const data = await driver.memory().catch((err) => {
321
+ report.notes.push(`Performance.getMetrics failed: ${err.message}`);
322
+ return null;
323
+ });
324
+ if (!data) {
325
+ if (report.notes.length === 0)
326
+ delete report.notes;
327
+ return report;
328
+ }
329
+ report.appId = data.url;
330
+ const m = data.metrics;
331
+ const pm = data.pageMemory;
332
+ report.app = {
333
+ // Heap totals — prefer page-context performance.memory (Chromium) over
334
+ // CDP's JSHeapUsedSize (which can lag). Both are JS heap only.
335
+ nativeHeapBytes: pm?.usedJSHeapSize ?? m['JSHeapUsedSize'],
336
+ codeBytes: m['JSHeapTotalSize'] ? m['JSHeapTotalSize'] - (m['JSHeapUsedSize'] ?? 0) : undefined,
337
+ // Roll the raw CDP metrics into `regions` so they're inspectable verbatim.
338
+ regions: { ...m },
339
+ };
340
+ if (pm) {
341
+ report.app.regions = {
342
+ ...report.app.regions,
343
+ 'JS Heap Used': pm.usedJSHeapSize,
344
+ 'JS Heap Total': pm.totalJSHeapSize,
345
+ 'JS Heap Limit': pm.jsHeapSizeLimit,
346
+ };
347
+ }
348
+ // Object counts — direct CDP equivalents of Android's "Views/Activities/Binders".
349
+ const objectKeys = [
350
+ 'Nodes',
351
+ 'Documents',
352
+ 'Frames',
353
+ 'JSEventListeners',
354
+ 'LayoutCount',
355
+ 'RecalcStyleCount',
356
+ ];
357
+ const objects = {};
358
+ for (const k of objectKeys) {
359
+ if (m[k] !== undefined)
360
+ objects[k] = m[k];
361
+ }
362
+ if (Object.keys(objects).length > 0)
363
+ report.objects = objects;
364
+ report.notes.push('Web memory is per-page (Performance.getMetrics + performance.memory). Run-wide RSS for the browser process is not exposed via CDP.');
365
+ if (report.notes.length === 0)
366
+ delete report.notes;
367
+ return report;
368
+ }
369
+ // ── Formatting ────────────────────────────────────────────────────────────────
370
+ function fmtBytes(n) {
371
+ if (n === undefined)
372
+ return '—';
373
+ if (n < 1024)
374
+ return `${n} B`;
375
+ const units = ['KB', 'MB', 'GB', 'TB'];
376
+ let v = n / 1024;
377
+ let i = 0;
378
+ while (v >= 1024 && i < units.length - 1) {
379
+ v /= 1024;
380
+ i++;
381
+ }
382
+ return `${v.toFixed(v >= 100 ? 0 : v >= 10 ? 1 : 2)} ${units[i]}`;
383
+ }
384
+ function formatReport(r) {
385
+ const lines = [];
386
+ lines.push(`Device: ${r.deviceId} (${r.platform})`);
387
+ if (r.appId)
388
+ lines.push(`App: ${r.appId}${r.pid ? ` (pid ${r.pid})` : ''}`);
389
+ if (r.system) {
390
+ lines.push('');
391
+ lines.push('System memory:');
392
+ const s = r.system;
393
+ if (s.totalBytes !== undefined)
394
+ lines.push(` Total: ${fmtBytes(s.totalBytes)}`);
395
+ if (s.usedBytes !== undefined)
396
+ lines.push(` Used: ${fmtBytes(s.usedBytes)}`);
397
+ if (s.availableBytes !== undefined)
398
+ lines.push(` Available: ${fmtBytes(s.availableBytes)}`);
399
+ if (s.freeBytes !== undefined)
400
+ lines.push(` Free: ${fmtBytes(s.freeBytes)}`);
401
+ if (s.cachedBytes !== undefined)
402
+ lines.push(` Cached: ${fmtBytes(s.cachedBytes)}`);
403
+ if (s.swapTotalBytes !== undefined)
404
+ lines.push(` Swap: ${fmtBytes((s.swapTotalBytes ?? 0) - (s.swapFreeBytes ?? 0))} / ${fmtBytes(s.swapTotalBytes)}`);
405
+ }
406
+ if (r.app) {
407
+ lines.push('');
408
+ lines.push('App memory:');
409
+ const a = r.app;
410
+ if (a.totalPssBytes !== undefined)
411
+ lines.push(` PSS total: ${fmtBytes(a.totalPssBytes)}`);
412
+ if (a.totalRssBytes !== undefined)
413
+ lines.push(` RSS total: ${fmtBytes(a.totalRssBytes)}`);
414
+ if (a.totalUssBytes !== undefined)
415
+ lines.push(` USS total: ${fmtBytes(a.totalUssBytes)}`);
416
+ if (a.vszBytes !== undefined)
417
+ lines.push(` VSZ: ${fmtBytes(a.vszBytes)}`);
418
+ if (a.javaHeapBytes !== undefined)
419
+ lines.push(` Java heap: ${fmtBytes(a.javaHeapBytes)}`);
420
+ if (a.nativeHeapBytes !== undefined)
421
+ lines.push(` Native: ${fmtBytes(a.nativeHeapBytes)}`);
422
+ if (a.codeBytes !== undefined)
423
+ lines.push(` Code: ${fmtBytes(a.codeBytes)}`);
424
+ if (a.stackBytes !== undefined)
425
+ lines.push(` Stack: ${fmtBytes(a.stackBytes)}`);
426
+ if (a.graphicsBytes !== undefined)
427
+ lines.push(` Graphics: ${fmtBytes(a.graphicsBytes)}`);
428
+ if (a.privateOtherBytes !== undefined)
429
+ lines.push(` Private: ${fmtBytes(a.privateOtherBytes)}`);
430
+ if (a.systemBytes !== undefined)
431
+ lines.push(` System: ${fmtBytes(a.systemBytes)}`);
432
+ if (a.regions && Object.keys(a.regions).length > 0) {
433
+ lines.push('');
434
+ lines.push('Top memory regions (resident):');
435
+ const entries = Object.entries(a.regions)
436
+ .filter(([, v]) => v > 0)
437
+ .sort((a, b) => b[1] - a[1])
438
+ .slice(0, 12);
439
+ for (const [name, bytes] of entries) {
440
+ lines.push(` ${name.padEnd(28)} ${fmtBytes(bytes)}`);
441
+ }
442
+ }
443
+ }
444
+ if (r.objects && Object.keys(r.objects).length > 0) {
445
+ lines.push('');
446
+ lines.push('Object counts:');
447
+ const entries = Object.entries(r.objects).sort((a, b) => b[1] - a[1]);
448
+ for (const [name, count] of entries) {
449
+ lines.push(` ${name.padEnd(20)} ${count}`);
450
+ }
451
+ }
452
+ if (r.notes && r.notes.length > 0) {
453
+ lines.push('');
454
+ for (const n of r.notes)
455
+ lines.push(`note: ${n}`);
456
+ }
457
+ return lines.join('\n');
458
+ }
459
+ // ── Entry point ───────────────────────────────────────────────────────────────
460
+ async function memory(appIdArg, opts = {}, sessionName = 'default', _memOpts = {}) {
461
+ const deviceId = await resolveDeviceId(sessionName);
462
+ if (!deviceId) {
463
+ (0, output_js_1.printError)('No device found. Connect a device or start a simulator first.', opts);
464
+ return 1;
465
+ }
466
+ const platform = await (0, bootstrap_js_1.detectPlatform)(deviceId);
467
+ let report;
468
+ if (platform === 'web') {
469
+ report = await collectWeb(deviceId, sessionName);
470
+ }
471
+ else {
472
+ const appId = await resolveAppId(appIdArg, sessionName);
473
+ if (platform === 'android') {
474
+ report = await collectAndroid(deviceId, appId);
475
+ }
476
+ else {
477
+ report = await collectIOS(deviceId, platform, appId);
478
+ }
479
+ }
480
+ if (opts.json) {
481
+ (0, output_js_1.printData)({ status: 'ok', ...report }, opts);
482
+ }
483
+ else {
484
+ console.log(formatReport(report));
485
+ }
486
+ return 0;
487
+ }
@@ -839,6 +839,35 @@ async function handleRequest(req, res, dlog) {
839
839
  jsonResponse(res, { isScreenStatic: snap1 === snap2 });
840
840
  return;
841
841
  }
842
+ case '/memory': {
843
+ const p = await getPage(dlog);
844
+ const session = await p.context().newCDPSession(p);
845
+ try {
846
+ await session.send('Performance.enable').catch(() => { });
847
+ const perf = (await session.send('Performance.getMetrics'));
848
+ const metricsMap = {};
849
+ for (const m of perf.metrics)
850
+ metricsMap[m.name] = m.value;
851
+ // Chrome-only: performance.memory in the page context.
852
+ const pageMemory = await p
853
+ .evaluate(() => {
854
+ const pm = performance.memory;
855
+ return pm
856
+ ? {
857
+ usedJSHeapSize: pm.usedJSHeapSize,
858
+ totalJSHeapSize: pm.totalJSHeapSize,
859
+ jsHeapSizeLimit: pm.jsHeapSizeLimit,
860
+ }
861
+ : null;
862
+ })
863
+ .catch(() => null);
864
+ jsonResponse(res, { metrics: metricsMap, pageMemory, url: p.url() });
865
+ }
866
+ finally {
867
+ await session.detach().catch(() => { });
868
+ }
869
+ return;
870
+ }
842
871
  case '/consoleLogs': {
843
872
  const since = parsedUrl.query['since'] ?? '';
844
873
  const entries = since
@@ -136,6 +136,9 @@ class WebDriver {
136
136
  const result = await this.get('runningApp');
137
137
  return result.runningAppBundleId;
138
138
  }
139
+ async memory() {
140
+ return this.get('memory');
141
+ }
139
142
  async eraseAllText(count = 50) {
140
143
  await this.post('eraseText', { count });
141
144
  }
package/dist/index.js CHANGED
@@ -46,6 +46,7 @@ const start_device_js_1 = require("./commands/start-device.js");
46
46
  const stop_device_js_1 = require("./commands/stop-device.js");
47
47
  const delete_device_js_1 = require("./commands/delete-device.js");
48
48
  const logs_js_1 = require("./commands/logs.js");
49
+ const memory_js_1 = require("./commands/memory.js");
49
50
  const device_picker_js_1 = require("./device-picker.js");
50
51
  const update_check_js_1 = require("./update-check.js");
51
52
  const pkg_root_js_1 = require("./pkg-root.js");
@@ -96,6 +97,7 @@ const COMMAND_HELP = {
96
97
  'device-pool': device_pool_js_1.HELP,
97
98
  'run-parallel': run_parallel_js_1.HELP,
98
99
  logs: logs_js_1.HELP,
100
+ memory: memory_js_1.HELP,
99
101
  };
100
102
  const OPTIONS_HELP = `Options:
101
103
  --device <id> Target device ID (also keys the session and daemon)
@@ -482,6 +484,11 @@ async function main() {
482
484
  duration: argv['duration'] !== undefined ? Number(argv['duration']) : undefined,
483
485
  });
484
486
  break;
487
+ case 'memory': {
488
+ const appId = rest[0];
489
+ exitCode = await (0, memory_js_1.memory)(appId, opts, sessionName);
490
+ break;
491
+ }
485
492
  case 'run-flow': {
486
493
  const file = rest[0] ?? '';
487
494
  const rawEnv = argv['env'];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@houwert/conductor",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "CLI tool for mobile app interactions — optimized for AI agents",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: conductor
3
- version: 0.11.0
3
+ version: 0.12.0
4
4
  description: "Token-efficient CLI for mobile UI testing (iOS simulator + Android emulator), designed for AI agents"
5
5
  metadata.openclaw:
6
6
  category: service
@@ -3,6 +3,6 @@ skills:
3
3
  path: conductor/SKILL.md
4
4
  description: "Token-efficient CLI for mobile UI testing, designed for AI agents"
5
5
  category: service
6
- version: 0.11.0
6
+ version: 0.12.0
7
7
  requires:
8
8
  bins: [conductor]