@11agents/cli 0.1.23 → 0.1.25

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 (71) hide show
  1. package/README.md +52 -0
  2. package/bin/11agents.js +12 -0
  3. package/mobile-runtime/README.md +19 -0
  4. package/mobile-runtime/configs/platforms/xiaohongshu_d01.json +73 -0
  5. package/mobile-runtime/configs/platforms/xiaohongshu_d02.json +70 -0
  6. package/mobile-runtime/configs/platforms/xiaohongshu_d03.json +73 -0
  7. package/mobile-runtime/configs/publish_policy.json +40 -0
  8. package/mobile-runtime/data-templates/README.md +4 -0
  9. package/mobile-runtime/data-templates/accounts.example.csv +6 -0
  10. package/mobile-runtime/data-templates/devices.example.csv +2 -0
  11. package/mobile-runtime/data-templates/publish_records.example.jsonl +2 -0
  12. package/mobile-runtime/data-templates/tasks.example.jsonl +5 -0
  13. package/mobile-runtime/data-templates/video_metrics.example.jsonl +1 -0
  14. package/mobile-runtime/python/pyproject.toml +34 -0
  15. package/mobile-runtime/python/src/device_control/__init__.py +5 -0
  16. package/mobile-runtime/python/src/device_control/adapters/__init__.py +31 -0
  17. package/mobile-runtime/python/src/device_control/adapters/base.py +43 -0
  18. package/mobile-runtime/python/src/device_control/adapters/facebook.py +30 -0
  19. package/mobile-runtime/python/src/device_control/adapters/instagram.py +25 -0
  20. package/mobile-runtime/python/src/device_control/adapters/reddit.py +29 -0
  21. package/mobile-runtime/python/src/device_control/adapters/tiktok.py +25 -0
  22. package/mobile-runtime/python/src/device_control/adapters/x.py +29 -0
  23. package/mobile-runtime/python/src/device_control/adapters/xiaohongshu.py +26 -0
  24. package/mobile-runtime/python/src/device_control/adb.py +161 -0
  25. package/mobile-runtime/python/src/device_control/appium_client.py +131 -0
  26. package/mobile-runtime/python/src/device_control/appium_manager.py +403 -0
  27. package/mobile-runtime/python/src/device_control/cli.py +1608 -0
  28. package/mobile-runtime/python/src/device_control/entrypoints.py +60 -0
  29. package/mobile-runtime/python/src/device_control/locks.py +162 -0
  30. package/mobile-runtime/python/src/device_control/metrics/__init__.py +33 -0
  31. package/mobile-runtime/python/src/device_control/metrics/collectors.py +320 -0
  32. package/mobile-runtime/python/src/device_control/metrics/tiktok_account_adb.py +367 -0
  33. package/mobile-runtime/python/src/device_control/metrics/tiktok_video_adb.py +714 -0
  34. package/mobile-runtime/python/src/device_control/models.py +439 -0
  35. package/mobile-runtime/python/src/device_control/publish_policy.py +173 -0
  36. package/mobile-runtime/python/src/device_control/publishers/__init__.py +24 -0
  37. package/mobile-runtime/python/src/device_control/publishers/facebook_adb.py +494 -0
  38. package/mobile-runtime/python/src/device_control/publishers/instagram_adb.py +663 -0
  39. package/mobile-runtime/python/src/device_control/publishers/reddit_adb.py +595 -0
  40. package/mobile-runtime/python/src/device_control/publishers/tiktok_adb.py +477 -0
  41. package/mobile-runtime/python/src/device_control/publishers/tiktok_appium.py +259 -0
  42. package/mobile-runtime/python/src/device_control/publishers/ui_helpers.py +372 -0
  43. package/mobile-runtime/python/src/device_control/publishers/x_adb.py +636 -0
  44. package/mobile-runtime/python/src/device_control/publishers/xiaohongshu_adb.py +1143 -0
  45. package/mobile-runtime/python/src/device_control/store.py +137 -0
  46. package/mobile-runtime/scripts/appium_smoke.py +71 -0
  47. package/mobile-runtime/skills/android-collect-tiktok-metrics/SKILL.md +60 -0
  48. package/mobile-runtime/skills/android-collect-tiktok-metrics/agents/openai.yaml +4 -0
  49. package/mobile-runtime/skills/android-group-control-cli/SKILL.md +76 -0
  50. package/mobile-runtime/skills/android-group-control-cli/agents/openai.yaml +4 -0
  51. package/mobile-runtime/skills/android-group-control-cli/references/command-reference.md +122 -0
  52. package/mobile-runtime/skills/android-publish-facebook/SKILL.md +41 -0
  53. package/mobile-runtime/skills/android-publish-facebook/agents/openai.yaml +4 -0
  54. package/mobile-runtime/skills/android-publish-instagram/SKILL.md +45 -0
  55. package/mobile-runtime/skills/android-publish-instagram/agents/openai.yaml +4 -0
  56. package/mobile-runtime/skills/android-publish-reddit/SKILL.md +41 -0
  57. package/mobile-runtime/skills/android-publish-reddit/agents/openai.yaml +4 -0
  58. package/mobile-runtime/skills/android-publish-tiktok/SKILL.md +43 -0
  59. package/mobile-runtime/skills/android-publish-tiktok/agents/openai.yaml +4 -0
  60. package/mobile-runtime/skills/android-publish-x/SKILL.md +40 -0
  61. package/mobile-runtime/skills/android-publish-x/agents/openai.yaml +4 -0
  62. package/mobile-runtime/skills/android-publish-xiaohongshu/SKILL.md +50 -0
  63. package/mobile-runtime/skills/android-publish-xiaohongshu/agents/openai.yaml +4 -0
  64. package/mobile-runtime/skills/mobile-publish-data-collection/SKILL.md +49 -0
  65. package/mobile-runtime/skills/mobile-publish-device-health/SKILL.md +47 -0
  66. package/mobile-runtime/skills/mobile-publish-execution/SKILL.md +57 -0
  67. package/mobile-runtime/skills/mobile-publish-records/SKILL.md +29 -0
  68. package/package.json +4 -1
  69. package/scripts/mobile-postinstall.js +26 -0
  70. package/src/commands/mobile.js +695 -0
  71. package/src/commands/runtime.js +63 -28
@@ -0,0 +1,695 @@
1
+ import { spawn } from 'node:child_process'
2
+ import { constants } from 'node:fs'
3
+ import { access, copyFile, mkdir, readdir, readFile, writeFile } from 'node:fs/promises'
4
+ import os from 'node:os'
5
+ import path from 'node:path'
6
+ import { fileURLToPath } from 'node:url'
7
+ import { flag } from '../args.js'
8
+
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
10
+ const CLI_ROOT = path.resolve(__dirname, '..', '..')
11
+ const MOBILE_RUNTIME_ROOT = path.join(CLI_ROOT, 'mobile-runtime')
12
+ const MOBILE_PYTHON_ROOT = path.join(MOBILE_RUNTIME_ROOT, 'python')
13
+ const MOBILE_CONFIG_ROOT = path.join(MOBILE_RUNTIME_ROOT, 'configs')
14
+ const MOBILE_DATA_TEMPLATE_ROOT = path.join(MOBILE_RUNTIME_ROOT, 'data-templates')
15
+ const MOBILE_SKILLS_ROOT = path.join(MOBILE_RUNTIME_ROOT, 'skills')
16
+ const MOBILE_SCRIPTS_ROOT = path.join(MOBILE_RUNTIME_ROOT, 'scripts')
17
+
18
+ const PYTHON_COMMANDS = new Set([
19
+ 'doctor',
20
+ 'list-adb',
21
+ 'ensure-appium',
22
+ 'stop-appium',
23
+ 'list-devices',
24
+ 'init-one-device',
25
+ 'connect',
26
+ 'health',
27
+ 'screenshot',
28
+ 'dump-ui',
29
+ 'tap',
30
+ 'keyevent',
31
+ 'input-text',
32
+ 'launch',
33
+ 'plan-task',
34
+ 'record-publish',
35
+ 'list-publish-records',
36
+ 'update-publish-record',
37
+ 'add-metrics',
38
+ 'collect-metrics',
39
+ 'list-metrics',
40
+ 'collect-tiktok-account-metrics-adb',
41
+ 'list-tiktok-account-metrics',
42
+ 'collect-tiktok-video-metrics-adb',
43
+ 'list-tiktok-video-metrics',
44
+ 'publish-tiktok-adb',
45
+ 'publish-tiktok',
46
+ 'publish-tiktok-appium',
47
+ 'publish-reddit-adb',
48
+ 'publish-reddit',
49
+ 'publish-facebook',
50
+ 'publish-instagram',
51
+ 'publish-x',
52
+ 'publish-xiaohongshu',
53
+ 'copy-xiaohongshu-link',
54
+ ])
55
+
56
+ const SCRIPT_COMMANDS = new Map([
57
+ ['appium-smoke', 'appium_smoke.py'],
58
+ ])
59
+
60
+ const COMMAND_ALIASES = new Map([
61
+ ['collect-tiktok-account-metrics', 'collect-tiktok-account-metrics-adb'],
62
+ ['collect-tiktok-video-metrics', 'collect-tiktok-video-metrics-adb'],
63
+ ])
64
+
65
+ const WRAPPER_FLAG_NAMES = new Set([
66
+ 'mobile-home',
67
+ 'task-id',
68
+ ])
69
+
70
+ const EMPTY_DATA_FILES = [
71
+ 'publish_records.jsonl',
72
+ 'video_metrics.jsonl',
73
+ 'tiktok_account_metrics.jsonl',
74
+ 'tiktok_video_metrics.jsonl',
75
+ ]
76
+
77
+ function usage() {
78
+ console.log(`11agents mobile
79
+
80
+ Usage:
81
+ 11agents mobile help
82
+ 11agents mobile setup [--json] [--skip-python] [--skip-npm]
83
+ 11agents mobile doctor [--json]
84
+ 11agents mobile migrate --from <android-group-control-mvp>
85
+ 11agents mobile skills list
86
+ 11agents mobile skills show <skill-name>
87
+ 11agents mobile data collect --platform <x|reddit|instagram|facebook|tiktok|xiaohongshu> [--scope account|video|record] [--record-id <id>] [--device Dxx]
88
+ 11agents mobile <device-control-command> [args...]
89
+ 11agents mobile appium-smoke --udid <adb-serial> [--server http://127.0.0.1:4723]
90
+
91
+ Examples:
92
+ 11agents mobile list-devices --health --json
93
+ 11agents mobile publish-tiktok --device D03 --video /path/video.mp4 --caption "..." --json
94
+ 11agents mobile collect-tiktok-video-metrics --device D03 --video-order 1 --json
95
+
96
+ Runtime:
97
+ Python/device-control code is bundled with @11agents/cli.
98
+ Mutable runtime state lives in ~/.11agents/mobile by default.
99
+ Every command writes logs under ~/.11agents/mobile/runs/<task_id>/log.`)
100
+ }
101
+
102
+ function mobileHome(flags = {}, env = process.env) {
103
+ return path.resolve(
104
+ flag(flags, 'mobile-home')
105
+ || env.ELEVENAGENTS_MOBILE_HOME
106
+ || path.join(os.homedir(), '.11agents', 'mobile')
107
+ )
108
+ }
109
+
110
+ function taskIdFrom(flags = {}, env = process.env, now = new Date()) {
111
+ const explicit = flag(flags, 'task-id') || env.ELEVENAGENTS_TASK_ID || ''
112
+ if (explicit) return sanitizeTaskId(explicit)
113
+ const stamp = now.toISOString().replace(/[-:]/g, '').replace(/\..+$/, '').replace('T', '-')
114
+ const suffix = Math.random().toString(16).slice(2, 8)
115
+ return `manual-${stamp}-${suffix}`
116
+ }
117
+
118
+ function sanitizeTaskId(value) {
119
+ return String(value || 'manual')
120
+ .trim()
121
+ .replace(/[^A-Za-z0-9_.-]+/g, '-')
122
+ .replace(/^-+|-+$/g, '')
123
+ .slice(0, 160) || 'manual'
124
+ }
125
+
126
+ function parseRawArgs(argv) {
127
+ const positional = []
128
+ const flags = {}
129
+ for (let i = 0; i < argv.length; i += 1) {
130
+ const arg = argv[i]
131
+ if (!arg.startsWith('--')) {
132
+ positional.push(arg)
133
+ continue
134
+ }
135
+ const raw = arg.slice(2)
136
+ const eq = raw.indexOf('=')
137
+ if (eq >= 0) {
138
+ addFlag(flags, raw.slice(0, eq), raw.slice(eq + 1))
139
+ continue
140
+ }
141
+ const key = raw
142
+ const next = argv[i + 1]
143
+ if (!next || next.startsWith('--')) {
144
+ addFlag(flags, key, true)
145
+ continue
146
+ }
147
+ addFlag(flags, key, next)
148
+ i += 1
149
+ }
150
+ return { positional, flags }
151
+ }
152
+
153
+ function addFlag(flags, key, value) {
154
+ if (flags[key] === undefined) flags[key] = value
155
+ else if (Array.isArray(flags[key])) flags[key].push(value)
156
+ else flags[key] = [flags[key], value]
157
+ }
158
+
159
+ function stripWrapperFlags(args) {
160
+ const out = []
161
+ for (let i = 0; i < args.length; i += 1) {
162
+ const arg = args[i]
163
+ if (!arg.startsWith('--')) {
164
+ out.push(arg)
165
+ continue
166
+ }
167
+ const raw = arg.slice(2)
168
+ const eq = raw.indexOf('=')
169
+ const key = eq >= 0 ? raw.slice(0, eq) : raw
170
+ if (!WRAPPER_FLAG_NAMES.has(key)) {
171
+ out.push(arg)
172
+ continue
173
+ }
174
+ if (eq < 0) {
175
+ const next = args[i + 1]
176
+ if (next && !next.startsWith('--')) i += 1
177
+ }
178
+ }
179
+ return out
180
+ }
181
+
182
+ function venvPython(home, platform = process.platform) {
183
+ return platform === 'win32'
184
+ ? path.join(home, '.venv', 'Scripts', 'python.exe')
185
+ : path.join(home, '.venv', 'bin', 'python')
186
+ }
187
+
188
+ async function exists(filePath, deps = {}) {
189
+ const accessFn = deps.access || access
190
+ try {
191
+ await accessFn(filePath, constants.F_OK)
192
+ return true
193
+ } catch {
194
+ return false
195
+ }
196
+ }
197
+
198
+ async function isExecutable(filePath, deps = {}) {
199
+ const accessFn = deps.access || access
200
+ try {
201
+ await accessFn(filePath, constants.X_OK)
202
+ return true
203
+ } catch {
204
+ return false
205
+ }
206
+ }
207
+
208
+ async function copyDir(source, target, { overwrite = true } = {}) {
209
+ await mkdir(target, { recursive: true })
210
+ const entries = await readdir(source, { withFileTypes: true })
211
+ for (const entry of entries) {
212
+ const from = path.join(source, entry.name)
213
+ const to = path.join(target, entry.name)
214
+ if (entry.isDirectory()) {
215
+ await copyDir(from, to, { overwrite })
216
+ } else if (entry.isFile()) {
217
+ await mkdir(path.dirname(to), { recursive: true })
218
+ if (!overwrite && await exists(to)) continue
219
+ await copyFile(from, to)
220
+ }
221
+ }
222
+ }
223
+
224
+ async function writeFileIfMissing(filePath, content) {
225
+ try {
226
+ await access(filePath, constants.F_OK)
227
+ } catch {
228
+ await mkdir(path.dirname(filePath), { recursive: true })
229
+ await writeFile(filePath, content)
230
+ }
231
+ }
232
+
233
+ async function scaffoldMobileHome(home) {
234
+ await mkdir(home, { recursive: true })
235
+ await mkdir(path.join(home, 'data'), { recursive: true })
236
+ await mkdir(path.join(home, 'artifacts', 'screenshots'), { recursive: true })
237
+ await mkdir(path.join(home, 'artifacts', 'ui'), { recursive: true })
238
+ await mkdir(path.join(home, 'runs'), { recursive: true })
239
+ await copyDir(MOBILE_CONFIG_ROOT, path.join(home, 'configs'), { overwrite: false })
240
+ await copyDir(MOBILE_SKILLS_ROOT, path.join(home, 'skills'), { overwrite: false })
241
+
242
+ await writeFileIfMissing(
243
+ path.join(home, 'data', 'accounts.csv'),
244
+ await readFile(path.join(MOBILE_DATA_TEMPLATE_ROOT, 'accounts.example.csv'), 'utf8')
245
+ )
246
+ await writeFileIfMissing(
247
+ path.join(home, 'data', 'devices.csv'),
248
+ await readFile(path.join(MOBILE_DATA_TEMPLATE_ROOT, 'devices.example.csv'), 'utf8')
249
+ )
250
+ for (const filename of EMPTY_DATA_FILES) {
251
+ await writeFileIfMissing(path.join(home, 'data', filename), '')
252
+ }
253
+ await writeFileIfMissing(path.join(home, 'data', 'tasks.jsonl'), '')
254
+ }
255
+
256
+ function runProcess(command, args, { cwd = process.cwd(), env = process.env, input = '', onStdoutChunk, onStderrChunk } = {}) {
257
+ return new Promise(resolve => {
258
+ const child = spawn(command, args, { cwd, env, stdio: ['pipe', 'pipe', 'pipe'] })
259
+ let stdout = ''
260
+ let stderr = ''
261
+ child.stdout.on('data', chunk => {
262
+ stdout += chunk
263
+ onStdoutChunk?.(chunk)
264
+ })
265
+ child.stderr.on('data', chunk => {
266
+ stderr += chunk
267
+ onStderrChunk?.(chunk)
268
+ })
269
+ child.on('error', error => {
270
+ resolve({ code: 127, stdout, stderr: stderr || error.message })
271
+ })
272
+ child.on('close', code => {
273
+ resolve({ code: code ?? 1, stdout, stderr })
274
+ })
275
+ child.stdin.end(input)
276
+ })
277
+ }
278
+
279
+ async function runAndLog({ command, args, cwd, env, runDir, log, deps = {} }) {
280
+ const run = deps.runProcess || runProcess
281
+ await mkdir(runDir, { recursive: true })
282
+ const stdoutPath = path.join(runDir, 'stdout.log')
283
+ const stderrPath = path.join(runDir, 'stderr.log')
284
+ const commandPath = path.join(runDir, 'command.json')
285
+ await writeFile(commandPath, JSON.stringify({
286
+ command,
287
+ args,
288
+ cwd,
289
+ started_at: new Date().toISOString(),
290
+ }, null, 2))
291
+ await appendLog(log, `command: ${[command, ...args].map(shellQuote).join(' ')}\n`)
292
+ await appendLog(log, `cwd: ${cwd}\n`)
293
+ const result = await run(command, args, {
294
+ cwd,
295
+ env,
296
+ onStdoutChunk: chunk => appendFileBestEffort(stdoutPath, chunk),
297
+ onStderrChunk: chunk => appendFileBestEffort(stderrPath, chunk),
298
+ })
299
+ await writeFile(stdoutPath, result.stdout || '')
300
+ await writeFile(stderrPath, result.stderr || '')
301
+ await appendLog(log, `exit_code: ${result.code}\n`)
302
+ if (result.stdout) await appendLog(log, `stdout:\n${result.stdout}\n`)
303
+ if (result.stderr) await appendLog(log, `stderr:\n${result.stderr}\n`)
304
+ return result
305
+ }
306
+
307
+ async function appendFileBestEffort(filePath, chunk) {
308
+ try {
309
+ await mkdir(path.dirname(filePath), { recursive: true })
310
+ await writeFile(filePath, chunk, { flag: 'a' })
311
+ } catch {}
312
+ }
313
+
314
+ async function appendLog(logPath, text) {
315
+ await mkdir(path.dirname(logPath), { recursive: true })
316
+ await writeFile(logPath, text, { flag: 'a' })
317
+ }
318
+
319
+ function shellQuote(value) {
320
+ const text = String(value)
321
+ if (/^[A-Za-z0-9_./:=@-]+$/.test(text)) return text
322
+ return JSON.stringify(text)
323
+ }
324
+
325
+ function pythonEnv(home, env = process.env) {
326
+ const appiumBin = path.join(home, 'node_modules', '.bin', process.platform === 'win32' ? 'appium.cmd' : 'appium')
327
+ return {
328
+ ...env,
329
+ PYTHONPATH: [
330
+ path.join(MOBILE_PYTHON_ROOT, 'src'),
331
+ env.PYTHONPATH || '',
332
+ ].filter(Boolean).join(path.delimiter),
333
+ APPIUM_BIN: env.APPIUM_BIN || appiumBin,
334
+ APPIUM_HOME: env.APPIUM_HOME || path.join(home, 'appium'),
335
+ ELEVENAGENTS_MOBILE_HOME: home,
336
+ }
337
+ }
338
+
339
+ async function mobileDoctor(flags = {}, deps = {}) {
340
+ const home = mobileHome(flags, deps.env || process.env)
341
+ const python = venvPython(home, deps.platform || process.platform)
342
+ const checks = {
343
+ mobile_home: home,
344
+ bundled_python_runtime: await exists(path.join(MOBILE_PYTHON_ROOT, 'src', 'device_control', 'cli.py'), deps),
345
+ runtime_configs: await exists(MOBILE_CONFIG_ROOT, deps),
346
+ bundled_skills: await exists(path.join(MOBILE_SKILLS_ROOT, 'android-group-control-cli', 'SKILL.md'), deps),
347
+ home_exists: await exists(home, deps),
348
+ venv_python: await isExecutable(python, deps),
349
+ data_devices_csv: await exists(path.join(home, 'data', 'devices.csv'), deps),
350
+ publish_policy: await exists(path.join(home, 'configs', 'publish_policy.json'), deps),
351
+ appium_bin: await isExecutable(path.join(home, 'node_modules', '.bin', process.platform === 'win32' ? 'appium.cmd' : 'appium'), deps),
352
+ appium_home: await exists(path.join(home, 'appium'), deps),
353
+ }
354
+ const ok = checks.bundled_python_runtime && checks.runtime_configs && checks.bundled_skills && checks.home_exists && checks.venv_python
355
+ const result = {
356
+ ok,
357
+ status: ok ? 'ready' : 'needs_setup',
358
+ setup_command: '11agents mobile setup',
359
+ checks,
360
+ }
361
+ if (flags.json) {
362
+ ;(deps.log || console.log)(JSON.stringify(result, null, 2))
363
+ } else {
364
+ ;(deps.log || console.log)([
365
+ `status=${result.status}`,
366
+ `mobile_home=${home}`,
367
+ `setup_command=${result.setup_command}`,
368
+ ].join('\n'))
369
+ }
370
+ return result
371
+ }
372
+
373
+ async function mobileSetup(flags = {}, deps = {}) {
374
+ const env = deps.env || process.env
375
+ const home = mobileHome(flags, env)
376
+ await scaffoldMobileHome(home)
377
+ const python = venvPython(home, deps.platform || process.platform)
378
+ const run = deps.runProcess || runProcess
379
+ const steps = []
380
+
381
+ if (!flags['skip-python']) {
382
+ if (!await exists(python, deps)) {
383
+ const venvResult = await run(env.PYTHON || env.PYTHON3 || 'python3', ['-m', 'venv', path.join(home, '.venv')], { cwd: home, env })
384
+ steps.push({ step: 'python venv', code: venvResult.code, stderr: venvResult.stderr })
385
+ if (venvResult.code !== 0) throw new Error(`python venv failed: ${venvResult.stderr || venvResult.stdout}`)
386
+ }
387
+ const pipResult = await run(python, ['-m', 'pip', 'install', '-U', 'pip'], { cwd: home, env: pythonEnv(home, env) })
388
+ steps.push({ step: 'pip upgrade', code: pipResult.code, stderr: pipResult.stderr })
389
+ if (pipResult.code !== 0) throw new Error(`pip upgrade failed: ${pipResult.stderr || pipResult.stdout}`)
390
+ const installResult = await run(python, ['-m', 'pip', 'install', '-e', `${MOBILE_PYTHON_ROOT}[appium]`], { cwd: home, env: pythonEnv(home, env) })
391
+ steps.push({ step: 'mobile python install', code: installResult.code, stderr: installResult.stderr })
392
+ if (installResult.code !== 0) throw new Error(`mobile runtime install failed: ${installResult.stderr || installResult.stdout}`)
393
+ }
394
+
395
+ if (!flags['skip-npm']) {
396
+ await writeFileIfMissing(path.join(home, 'package.json'), JSON.stringify({
397
+ private: true,
398
+ name: 'elevenagents-mobile-runtime-local',
399
+ version: '0.0.0',
400
+ dependencies: {
401
+ appium: '^3.4.2',
402
+ 'appium-uiautomator2-driver': '^7.5.2',
403
+ },
404
+ }, null, 2))
405
+ const npmResult = await run(env.NPM || 'npm', ['install', '--prefix', home], { cwd: home, env })
406
+ steps.push({ step: 'appium npm install', code: npmResult.code, stderr: npmResult.stderr })
407
+ if (npmResult.code !== 0) throw new Error(`appium npm install failed: ${npmResult.stderr || npmResult.stdout}`)
408
+ const appiumCli = path.join(home, 'node_modules', '.bin', process.platform === 'win32' ? 'appium.cmd' : 'appium')
409
+ const appiumHome = path.join(home, 'appium')
410
+ await mkdir(appiumHome, { recursive: true })
411
+ const appiumEnv = { ...env, APPIUM_HOME: env.APPIUM_HOME || appiumHome }
412
+ const driverResult = await run(appiumCli, ['driver', 'install', 'uiautomator2'], { cwd: home, env: appiumEnv })
413
+ steps.push({ step: 'appium uiautomator2 driver', code: driverResult.code, stderr: driverResult.stderr })
414
+ if (driverResult.code !== 0 && !/already installed|already exists/i.test(`${driverResult.stdout}\n${driverResult.stderr}`)) {
415
+ throw new Error(`appium uiautomator2 driver install failed: ${driverResult.stderr || driverResult.stdout}`)
416
+ }
417
+ }
418
+
419
+ const result = {
420
+ ok: true,
421
+ status: 'ready',
422
+ mobile_home: home,
423
+ python,
424
+ steps,
425
+ }
426
+ ;(deps.log || console.log)(flags.json ? JSON.stringify(result, null, 2) : `mobile runtime ready: ${home}`)
427
+ return result
428
+ }
429
+
430
+ async function mobileMigrate(flags = {}, deps = {}) {
431
+ const from = path.resolve(flag(flags, 'from') || '')
432
+ if (!from) throw new Error('mobile migrate requires --from <android-group-control-mvp>')
433
+ if (!await exists(path.join(from, 'src', 'device_control', 'cli.py'), deps)) {
434
+ throw new Error(`not an android group-control checkout: ${from}`)
435
+ }
436
+ const home = mobileHome(flags, deps.env || process.env)
437
+ await scaffoldMobileHome(home)
438
+ await copyDir(path.join(from, 'configs'), path.join(home, 'configs'), { overwrite: true })
439
+ await mkdir(path.join(home, 'data'), { recursive: true })
440
+ for (const filename of [
441
+ 'accounts.csv',
442
+ 'devices.csv',
443
+ 'publish_records.jsonl',
444
+ 'video_metrics.jsonl',
445
+ 'tiktok_account_metrics.jsonl',
446
+ 'tiktok_video_metrics.jsonl',
447
+ 'tasks.jsonl',
448
+ ]) {
449
+ const source = path.join(from, 'data', filename)
450
+ if (await exists(source, deps)) await copyFile(source, path.join(home, 'data', filename))
451
+ }
452
+ const result = { ok: true, migrated_from: from, mobile_home: home }
453
+ ;(deps.log || console.log)(flags.json ? JSON.stringify(result, null, 2) : `migrated mobile runtime data to ${home}`)
454
+ return result
455
+ }
456
+
457
+ async function mobileSkills(argv = [], deps = {}) {
458
+ const { positional } = parseRawArgs(argv)
459
+ const [subcommand, name] = positional
460
+ if (!subcommand || subcommand === 'list') {
461
+ const entries = await readdir(MOBILE_SKILLS_ROOT, { withFileTypes: true })
462
+ const names = entries.filter(entry => entry.isDirectory()).map(entry => entry.name).sort()
463
+ ;(deps.log || console.log)(names.join('\n'))
464
+ return names
465
+ }
466
+ if (subcommand === 'show') {
467
+ if (!name) throw new Error('mobile skills show requires a skill name')
468
+ const filePath = path.join(MOBILE_SKILLS_ROOT, name, 'SKILL.md')
469
+ if (!await exists(filePath, deps)) throw new Error(`mobile skill not found: ${name}`)
470
+ const text = await readFile(filePath, 'utf8')
471
+ ;(deps.log || console.log)(text)
472
+ return text
473
+ }
474
+ throw new Error(`unsupported mobile skills command: ${subcommand}`)
475
+ }
476
+
477
+ async function ensureRunnable(home, deps = {}) {
478
+ await scaffoldMobileHome(home)
479
+ const python = venvPython(home, deps.platform || process.platform)
480
+ if (!await exists(python, deps)) {
481
+ throw new Error(`mobile runtime is not set up. Run: 11agents mobile setup`)
482
+ }
483
+ return python
484
+ }
485
+
486
+ function normalizePythonCommand(command) {
487
+ return COMMAND_ALIASES.get(command) || command
488
+ }
489
+
490
+ function buildRunContext(rawArgs, deps = {}) {
491
+ const { flags } = parseRawArgs(rawArgs)
492
+ const env = deps.env || process.env
493
+ const home = mobileHome(flags, env)
494
+ const taskId = taskIdFrom(flags, env)
495
+ const runDir = path.join(home, 'runs', taskId)
496
+ return {
497
+ flags,
498
+ home,
499
+ taskId,
500
+ runDir,
501
+ logPath: path.join(runDir, 'log'),
502
+ }
503
+ }
504
+
505
+ async function dispatchPython(rawArgs, deps = {}) {
506
+ const command = normalizePythonCommand(rawArgs[0] || '')
507
+ if (!PYTHON_COMMANDS.has(command)) throw new Error(`unsupported mobile command: ${rawArgs[0] || ''}`)
508
+ const context = buildRunContext(rawArgs, deps)
509
+ const python = await ensureRunnable(context.home, deps)
510
+ const childArgs = ['-m', 'device_control.cli', command, ...stripWrapperFlags(rawArgs.slice(1))]
511
+ const env = pythonEnv(context.home, deps.env || process.env)
512
+ env.ELEVENAGENTS_MOBILE_RUN_DIR = context.runDir
513
+ env.ELEVENAGENTS_TASK_ID = context.taskId
514
+ const result = await runAndLog({
515
+ command: python,
516
+ args: childArgs,
517
+ cwd: context.home,
518
+ env,
519
+ runDir: context.runDir,
520
+ log: context.logPath,
521
+ deps,
522
+ })
523
+ const parsed = parseJsonResult(result.stdout)
524
+ await writeFile(path.join(context.runDir, 'result.json'), JSON.stringify({
525
+ status: result.code === 0 ? 'ok' : 'failed',
526
+ exit_code: result.code,
527
+ parsed,
528
+ }, null, 2))
529
+ if (result.stdout) process.stdout.write(result.stdout)
530
+ if (result.stderr) process.stderr.write(result.stderr)
531
+ if (result.code !== 0) process.exitCode = result.code
532
+ return { ...result, task_id: context.taskId, run_dir: context.runDir, parsed }
533
+ }
534
+
535
+ async function dispatchScript(rawArgs, deps = {}) {
536
+ const command = rawArgs[0] || ''
537
+ const scriptName = SCRIPT_COMMANDS.get(command)
538
+ if (!scriptName) throw new Error(`unsupported mobile command: ${command}`)
539
+ const context = buildRunContext(rawArgs, deps)
540
+ const python = await ensureRunnable(context.home, deps)
541
+ const scriptPath = path.join(MOBILE_SCRIPTS_ROOT, scriptName)
542
+ const env = pythonEnv(context.home, deps.env || process.env)
543
+ env.ELEVENAGENTS_MOBILE_RUN_DIR = context.runDir
544
+ env.ELEVENAGENTS_TASK_ID = context.taskId
545
+ const result = await runAndLog({
546
+ command: python,
547
+ args: [scriptPath, ...stripWrapperFlags(rawArgs.slice(1))],
548
+ cwd: context.home,
549
+ env,
550
+ runDir: context.runDir,
551
+ log: context.logPath,
552
+ deps,
553
+ })
554
+ await writeFile(path.join(context.runDir, 'result.json'), JSON.stringify({
555
+ status: result.code === 0 ? 'ok' : 'failed',
556
+ exit_code: result.code,
557
+ }, null, 2))
558
+ if (result.stdout) process.stdout.write(result.stdout)
559
+ if (result.stderr) process.stderr.write(result.stderr)
560
+ if (result.code !== 0) process.exitCode = result.code
561
+ return { ...result, task_id: context.taskId, run_dir: context.runDir }
562
+ }
563
+
564
+ function parseJsonResult(text) {
565
+ const rows = []
566
+ for (const line of String(text || '').split('\n')) {
567
+ const trimmed = line.trim()
568
+ if (!trimmed) continue
569
+ try {
570
+ rows.push(JSON.parse(trimmed))
571
+ } catch {}
572
+ }
573
+ return rows.length ? rows : null
574
+ }
575
+
576
+ function removeFlags(args, names) {
577
+ const out = []
578
+ for (let i = 0; i < args.length; i += 1) {
579
+ const arg = args[i]
580
+ if (!arg.startsWith('--')) {
581
+ out.push(arg)
582
+ continue
583
+ }
584
+ const raw = arg.slice(2)
585
+ const eq = raw.indexOf('=')
586
+ const key = eq >= 0 ? raw.slice(0, eq) : raw
587
+ if (!names.has(key)) {
588
+ out.push(arg)
589
+ continue
590
+ }
591
+ if (eq < 0) {
592
+ const next = args[i + 1]
593
+ if (next && !next.startsWith('--')) i += 1
594
+ }
595
+ }
596
+ return out
597
+ }
598
+
599
+ function dataCollectCommand(args) {
600
+ const { flags } = parseRawArgs(args)
601
+ const platform = String(flag(flags, 'platform') || '').toLowerCase()
602
+ const scope = String(flag(flags, 'scope') || flag(flags, 'kind') || '').toLowerCase()
603
+ const pass = removeFlags(args, new Set(['platform', 'scope', 'kind']))
604
+ if (!platform) throw new Error('mobile data collect requires --platform')
605
+ if (platform === 'tiktok' || platform === 'tk') {
606
+ if (scope === 'account') return ['collect-tiktok-account-metrics-adb', ...pass]
607
+ if (scope === 'video' || flags['video-order'] || flags.device) return ['collect-tiktok-video-metrics-adb', ...pass]
608
+ return ['collect-metrics', '--platform', 'tiktok', ...pass]
609
+ }
610
+ if (platform === 'reddit' || platform === 'instagram') {
611
+ return ['collect-metrics', '--platform', platform, ...pass]
612
+ }
613
+ if (platform === 'xiaohongshu' && flags['copy-link']) {
614
+ return ['copy-xiaohongshu-link', ...removeFlags(pass, new Set(['copy-link']))]
615
+ }
616
+ return null
617
+ }
618
+
619
+ async function mobileDataCollect(args, deps = {}) {
620
+ const command = dataCollectCommand(args)
621
+ if (command) return dispatchPython(command, deps)
622
+ const { flags } = parseRawArgs(args)
623
+ const context = buildRunContext(args, deps)
624
+ await scaffoldMobileHome(context.home)
625
+ await mkdir(context.runDir, { recursive: true })
626
+ const platform = String(flag(flags, 'platform') || '').toLowerCase()
627
+ const recordId = String(flag(flags, 'record-id') || '')
628
+ const result = {
629
+ platform,
630
+ record_id: recordId,
631
+ status: 'missing_access',
632
+ reason: `No automatic mobile data collector is bundled for platform=${platform}. Use platform API/export or manual values, then append with 11agents mobile add-metrics.`,
633
+ run_dir: context.runDir,
634
+ }
635
+ await writeFile(path.join(context.runDir, 'result.json'), JSON.stringify(result, null, 2))
636
+ await appendLog(context.logPath, `${JSON.stringify(result)}\n`)
637
+ ;(deps.log || console.log)(JSON.stringify(result, null, 2))
638
+ return { code: 0, stdout: JSON.stringify(result), stderr: '', parsed: [result] }
639
+ }
640
+
641
+ export async function runMobile(argv = [], deps = {}) {
642
+ const { flags } = parseRawArgs(argv)
643
+ const { positional } = parseRawArgs(argv)
644
+ const [command, subcommand] = positional
645
+ if (!argv.length || command === 'help') {
646
+ usage()
647
+ return null
648
+ }
649
+ if (command === 'setup') return mobileSetup(flags, deps)
650
+ if (command === 'doctor') return mobileDoctor(flags, deps)
651
+ if (command === 'migrate') return mobileMigrate(flags, deps)
652
+ if (command === 'skills') return mobileSkills(restAndFlagsAfter(argv, 1), deps)
653
+ if (command === 'data' && subcommand === 'collect') return mobileDataCollect(restAndFlagsAfter(argv, 2), deps)
654
+ if (command === 'groupctl') {
655
+ if (!subcommand) throw new Error('mobile groupctl requires a command')
656
+ return dispatchPython([subcommand, ...restAndFlagsAfter(argv, 2)], deps)
657
+ }
658
+ if (SCRIPT_COMMANDS.has(command)) return dispatchScript([command, ...restAndFlagsAfter(argv, 1)], deps)
659
+ return dispatchPython([command, ...restAndFlagsAfter(argv, 1)], deps)
660
+ }
661
+
662
+ function restAndFlagsAfter(argv, positionalCount) {
663
+ const out = []
664
+ let seen = 0
665
+ for (let i = 0; i < argv.length; i += 1) {
666
+ const arg = argv[i]
667
+ if (arg.startsWith('--')) {
668
+ out.push(arg)
669
+ const raw = arg.slice(2)
670
+ if (!raw.includes('=')) {
671
+ const next = argv[i + 1]
672
+ if (next && !next.startsWith('--')) {
673
+ out.push(next)
674
+ i += 1
675
+ }
676
+ }
677
+ continue
678
+ }
679
+ if (seen < positionalCount) {
680
+ seen += 1
681
+ continue
682
+ }
683
+ out.push(arg)
684
+ }
685
+ return out
686
+ }
687
+
688
+ export const mobileInternals = {
689
+ dataCollectCommand,
690
+ mobileHome,
691
+ parseRawArgs,
692
+ stripWrapperFlags,
693
+ taskIdFrom,
694
+ venvPython,
695
+ }