@11agents/cli 0.1.24 → 0.1.26

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 +21 -5
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+ from device_control.models import PublishTask
4
+
5
+ from .base import PlatformAdapter
6
+
7
+
8
+ class XAdapter(PlatformAdapter):
9
+ platform = "x"
10
+ package_names = ("com.twitter.android",)
11
+ supported_post_types = ("text", "link", "image")
12
+
13
+ def plan(self, task: PublishTask) -> list[str]:
14
+ steps = [
15
+ "wake device",
16
+ "open X app",
17
+ "tap bottom-right compose plus; scroll timeline and retry if the plus is hidden",
18
+ f"choose content flow for post_type={task.post_type}",
19
+ ]
20
+ if task.body:
21
+ steps.append("fill post text")
22
+ if task.caption:
23
+ steps.append(f"fill caption: {task.caption}")
24
+ if task.link_url:
25
+ steps.append(f"append link: {task.link_url}")
26
+ if task.media_path:
27
+ steps.append(f"push/select image: {task.media_path}")
28
+ steps.extend(["capture pre-publish screenshot", "publish or stop at dry-run", "capture result screenshot"])
29
+ return steps
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ from device_control.models import PublishTask
4
+
5
+ from .base import PlatformAdapter
6
+
7
+
8
+ class XiaohongshuAdapter(PlatformAdapter):
9
+ platform = "xiaohongshu"
10
+ package_names = ("com.xingin.xhs",)
11
+ supported_post_types = ("image", "video")
12
+
13
+ def plan(self, task: PublishTask) -> list[str]:
14
+ steps = [
15
+ "wake device",
16
+ "open Xiaohongshu app",
17
+ f"choose content flow for post_type={task.post_type}",
18
+ ]
19
+ if task.media_path:
20
+ steps.append(f"push/select media: {task.media_path}")
21
+ if task.title:
22
+ steps.append(f"fill title: {task.title}")
23
+ if task.caption:
24
+ steps.append(f"fill caption: {task.caption}")
25
+ steps.extend(["capture pre-publish screenshot", "publish or stop at dry-run", "capture result screenshot"])
26
+ return steps
@@ -0,0 +1,161 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import shutil
5
+ import subprocess
6
+ import time
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class CommandResult:
13
+ code: int
14
+ stdout: str
15
+ stderr: str
16
+
17
+ @property
18
+ def ok(self) -> bool:
19
+ return self.code == 0
20
+
21
+
22
+ class AdbClient:
23
+ def __init__(self, adb_bin: str | None = None, timeout_s: int = 30) -> None:
24
+ self.adb_bin = adb_bin or resolve_adb_bin()
25
+ self.timeout_s = timeout_s
26
+
27
+ def run(self, *args: str, timeout_s: int | None = None) -> CommandResult:
28
+ proc = subprocess.run(
29
+ [self.adb_bin, *args],
30
+ capture_output=True,
31
+ text=True,
32
+ check=False,
33
+ timeout=timeout_s or self.timeout_s,
34
+ )
35
+ return CommandResult(proc.returncode, proc.stdout.strip(), proc.stderr.strip())
36
+
37
+ def list_devices(self) -> list[str]:
38
+ result = self.run("devices")
39
+ devices: list[str] = []
40
+ for line in result.stdout.splitlines()[1:]:
41
+ parts = line.split()
42
+ if len(parts) >= 2 and parts[1] == "device":
43
+ devices.append(parts[0])
44
+ return devices
45
+
46
+ def connect(self, serial: str) -> CommandResult:
47
+ return self.run("connect", serial, timeout_s=10)
48
+
49
+ def shell(self, serial: str, *args: str) -> CommandResult:
50
+ return self.run("-s", serial, "shell", *args)
51
+
52
+ def wake(self, serial: str) -> CommandResult:
53
+ return self.shell(serial, "input", "keyevent", "KEYCODE_WAKEUP")
54
+
55
+ def keyevent(self, serial: str, keycode: str) -> CommandResult:
56
+ return self.shell(serial, "input", "keyevent", keycode)
57
+
58
+ def tap(self, serial: str, x: int, y: int) -> CommandResult:
59
+ return self.shell(serial, "input", "tap", str(x), str(y))
60
+
61
+ def swipe(self, serial: str, x1: int, y1: int, x2: int, y2: int, duration_ms: int = 400) -> CommandResult:
62
+ return self.shell(serial, "input", "swipe", str(x1), str(y1), str(x2), str(y2), str(duration_ms))
63
+
64
+ def input_text(self, serial: str, text: str) -> CommandResult:
65
+ escaped = text.replace("%", "%25").replace("#", r"\#").replace(" ", "%s")
66
+ return self.shell(serial, "input", "text", escaped)
67
+
68
+ def launch_package(self, serial: str, package: str) -> CommandResult:
69
+ return self.shell(
70
+ serial,
71
+ "monkey",
72
+ "-p",
73
+ package,
74
+ "-c",
75
+ "android.intent.category.LAUNCHER",
76
+ "1",
77
+ )
78
+
79
+ def push(self, serial: str, local_path: str | Path, remote_path: str) -> CommandResult:
80
+ return self.run("-s", serial, "push", str(local_path), remote_path, timeout_s=120)
81
+
82
+ def media_scan(self, serial: str, remote_path: str) -> CommandResult:
83
+ return self.shell(
84
+ serial,
85
+ "am",
86
+ "broadcast",
87
+ "-a",
88
+ "android.intent.action.MEDIA_SCANNER_SCAN_FILE",
89
+ "-d",
90
+ f"file://{remote_path}",
91
+ )
92
+
93
+ def screenshot(self, serial: str, out_path: str | Path) -> CommandResult:
94
+ out = Path(out_path)
95
+ out.parent.mkdir(parents=True, exist_ok=True)
96
+ remote = "/sdcard/_groupctl_screen.png"
97
+ shot = self.shell(serial, "screencap", "-p", remote)
98
+ if not shot.ok:
99
+ return shot
100
+ pull = self.run("-s", serial, "pull", remote, str(out), timeout_s=60)
101
+ self.shell(serial, "rm", remote)
102
+ return pull
103
+
104
+ def dump_ui(self, serial: str, out_path: str | Path) -> CommandResult:
105
+ out = Path(out_path)
106
+ out.parent.mkdir(parents=True, exist_ok=True)
107
+ remote = "/data/local/tmp/_groupctl_ui.xml"
108
+ last = CommandResult(1, "", "ui dump was not attempted")
109
+ for _attempt in range(3):
110
+ self.shell(serial, "rm", "-f", remote)
111
+ dump = self.shell(serial, "uiautomator", "dump", remote)
112
+ if not dump.ok:
113
+ last = dump
114
+ time.sleep(0.8)
115
+ continue
116
+ pull = self.run("-s", serial, "pull", remote, str(out), timeout_s=60)
117
+ self.shell(serial, "rm", "-f", remote)
118
+ if pull.ok:
119
+ return pull
120
+ last = pull
121
+ time.sleep(0.8)
122
+ return last
123
+
124
+ def getprop(self, serial: str, prop: str) -> str:
125
+ return self.shell(serial, "getprop", prop).stdout
126
+
127
+ def battery_level(self, serial: str) -> str:
128
+ result = self.shell(serial, "dumpsys", "battery")
129
+ for line in result.stdout.splitlines():
130
+ text = line.strip()
131
+ if text.startswith("level:"):
132
+ return text.split(":", 1)[1].strip()
133
+ return "unknown"
134
+
135
+ def current_focus(self, serial: str) -> str:
136
+ result = self.shell(serial, "dumpsys", "window")
137
+ for line in result.stdout.splitlines():
138
+ if "mCurrentFocus" in line or "mFocusedApp" in line:
139
+ return line.strip()
140
+ return "unknown"
141
+
142
+
143
+ def resolve_adb_bin() -> str:
144
+ env_path = os.environ.get("ADB_BIN")
145
+ if env_path:
146
+ return env_path
147
+
148
+ path_adb = shutil.which("adb")
149
+ if path_adb:
150
+ return path_adb
151
+
152
+ candidates = [
153
+ Path.home() / "Library/Android/sdk/platform-tools/adb",
154
+ Path("/usr/local/bin/adb"),
155
+ Path("/opt/homebrew/bin/adb"),
156
+ ]
157
+ for candidate in candidates:
158
+ if candidate.exists():
159
+ return str(candidate)
160
+
161
+ return "adb"
@@ -0,0 +1,131 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import json
5
+ import urllib.error
6
+ import urllib.request
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+
11
+ class AppiumError(RuntimeError):
12
+ pass
13
+
14
+
15
+ class AppiumClient:
16
+ def __init__(self, server_url: str = "http://127.0.0.1:4723") -> None:
17
+ self.server_url = server_url.rstrip("/")
18
+ self.session_id = ""
19
+
20
+ def start_session(self, capabilities: dict[str, Any]) -> str:
21
+ response = self._request("POST", "/session", {"capabilities": {"alwaysMatch": capabilities}})
22
+ value = response.get("value", {})
23
+ session_id = value.get("sessionId") or response.get("sessionId")
24
+ if not session_id:
25
+ raise AppiumError(f"Appium did not return sessionId: {response}")
26
+ self.session_id = str(session_id)
27
+ return self.session_id
28
+
29
+ def delete_session(self) -> None:
30
+ if self.session_id:
31
+ self._request("DELETE", f"/session/{self.session_id}")
32
+ self.session_id = ""
33
+
34
+ def find_element(self, using: str, value: str) -> str:
35
+ response = self._request("POST", f"/session/{self.session_id}/element", {"using": using, "value": value})
36
+ return _element_id(response["value"])
37
+
38
+ def click(self, element_id: str) -> None:
39
+ self._request("POST", f"/session/{self.session_id}/element/{element_id}/click", {})
40
+
41
+ def clear(self, element_id: str) -> None:
42
+ self._request("POST", f"/session/{self.session_id}/element/{element_id}/clear", {})
43
+
44
+ def send_keys(self, element_id: str, text: str) -> None:
45
+ payload = {"text": text, "value": list(text)}
46
+ self._request("POST", f"/session/{self.session_id}/element/{element_id}/value", payload)
47
+
48
+ def execute_script(self, script: str, args: list[Any] | None = None) -> Any:
49
+ response = self._request("POST", f"/session/{self.session_id}/execute/sync", {"script": script, "args": args or []})
50
+ return response.get("value")
51
+
52
+ def set_clipboard(self, text: str, content_type: str = "plaintext", label: str = "text") -> None:
53
+ raw = base64.b64encode(text.encode("utf-8")).decode("ascii")
54
+ payload = {"content": raw, "contentType": content_type, "label": label}
55
+ try:
56
+ self._request("POST", f"/session/{self.session_id}/appium/device/set_clipboard", payload)
57
+ except AppiumError:
58
+ self.execute_script("mobile: setClipboard", [payload])
59
+
60
+ def tap(self, x: int, y: int) -> None:
61
+ payload = {
62
+ "actions": [
63
+ {
64
+ "type": "pointer",
65
+ "id": "finger1",
66
+ "parameters": {"pointerType": "touch"},
67
+ "actions": [
68
+ {"type": "pointerMove", "duration": 0, "x": x, "y": y},
69
+ {"type": "pointerDown", "button": 0},
70
+ {"type": "pause", "duration": 100},
71
+ {"type": "pointerUp", "button": 0},
72
+ ],
73
+ }
74
+ ]
75
+ }
76
+ self._request("POST", f"/session/{self.session_id}/actions", payload)
77
+
78
+ def window_rect(self) -> dict[str, int]:
79
+ value = self._request("GET", f"/session/{self.session_id}/window/rect")["value"]
80
+ return {key: int(value[key]) for key in ("x", "y", "width", "height")}
81
+
82
+ def source(self, out_path: str | Path | None = None) -> str:
83
+ source = str(self._request("GET", f"/session/{self.session_id}/source")["value"])
84
+ if out_path:
85
+ out = Path(out_path)
86
+ out.parent.mkdir(parents=True, exist_ok=True)
87
+ out.write_text(source, encoding="utf-8")
88
+ return source
89
+
90
+ def screenshot(self, out_path: str | Path) -> None:
91
+ raw = str(self._request("GET", f"/session/{self.session_id}/screenshot")["value"])
92
+ out = Path(out_path)
93
+ out.parent.mkdir(parents=True, exist_ok=True)
94
+ out.write_bytes(base64.b64decode(raw))
95
+
96
+ def get_clipboard(self, content_type: str = "plaintext") -> str:
97
+ response = self._request(
98
+ "POST",
99
+ f"/session/{self.session_id}/appium/device/get_clipboard",
100
+ {"contentType": content_type},
101
+ )
102
+ raw = str(response.get("value", "") or "")
103
+ if not raw:
104
+ return ""
105
+ try:
106
+ return base64.b64decode(raw).decode("utf-8", errors="replace").strip()
107
+ except Exception:
108
+ return raw.strip()
109
+
110
+ def _request(self, method: str, path: str, payload: dict[str, Any] | None = None) -> dict[str, Any]:
111
+ data = json.dumps(payload).encode() if payload is not None else None
112
+ req = urllib.request.Request(
113
+ self.server_url + path,
114
+ data=data,
115
+ headers={"Content-Type": "application/json"},
116
+ method=method,
117
+ )
118
+ try:
119
+ with urllib.request.urlopen(req, timeout=90) as resp:
120
+ raw = resp.read().decode()
121
+ except urllib.error.HTTPError as exc:
122
+ raw = exc.read().decode(errors="replace")
123
+ raise AppiumError(f"HTTP {exc.code}: {raw}") from exc
124
+ return json.loads(raw) if raw else {}
125
+
126
+
127
+ def _element_id(value: dict[str, Any]) -> str:
128
+ element_id = value.get("element-6066-11e4-a52e-4f735466cecf") or value.get("ELEMENT")
129
+ if not element_id:
130
+ raise AppiumError(f"Element response did not include an element id: {value}")
131
+ return str(element_id)