@11agents/cli 0.1.28 → 0.1.30

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.
@@ -983,7 +983,7 @@ def main() -> None:
983
983
  topics=args.topics,
984
984
  dry_run=args.dry_run,
985
985
  confirm_no_disclosure_needed=args.confirm_no_disclosure_needed,
986
- copy_link_after_publish=args.copy_link_after_publish,
986
+ copy_link_after_publish=args.copy_link_after_publish and not args.dry_run,
987
987
  resume_post_form=args.resume_post_form,
988
988
  )
989
989
 
@@ -1174,8 +1174,16 @@ def _add_publish_xiaohongshu_args(parser: argparse.ArgumentParser) -> None:
1174
1174
  )
1175
1175
  parser.add_argument(
1176
1176
  "--copy-link-after-publish",
1177
+ dest="copy_link_after_publish",
1177
1178
  action="store_true",
1178
- help="After a successful publish, open the share sheet, tap copy link, and try to read the copied URL.",
1179
+ default=True,
1180
+ help="After a successful publish, open the share sheet, tap copy link, and try to read the copied URL. Enabled by default for live publish.",
1181
+ )
1182
+ parser.add_argument(
1183
+ "--no-copy-link-after-publish",
1184
+ dest="copy_link_after_publish",
1185
+ action="store_false",
1186
+ help="Disable automatic Xiaohongshu link recovery after publish.",
1179
1187
  )
1180
1188
  parser.add_argument("--parallel", action="store_true", help="Run target devices concurrently. Hard-capped at 5.")
1181
1189
  parser.add_argument("--max-concurrency", type=int, default=MAX_PARALLEL_DEVICES, help="Max concurrent devices, 1-5.")
@@ -319,7 +319,13 @@ class XiaohongshuAdbPublisher:
319
319
  )
320
320
  status = "published"
321
321
  if copy_link_after_publish:
322
- link_result = self.copy_current_note_link(device, prepare=False, run_dir=run_dir / "link")
322
+ link_result = self.copy_current_note_link(
323
+ device,
324
+ prepare=False,
325
+ run_dir=run_dir / "link",
326
+ expected_title=title,
327
+ expected_caption=composed_caption,
328
+ )
323
329
 
324
330
  ended_epoch = int(time.time())
325
331
  if status == "published":
@@ -757,6 +763,7 @@ class XiaohongshuAdbPublisher:
757
763
  expected_caption: str,
758
764
  ) -> Path:
759
765
  self._wait_for_publish_exit_after_confirm(device, run_dir)
766
+ self._restart_xiaohongshu_for_link_recovery(device, run_dir)
760
767
  return self._open_latest_own_note_for_link(
761
768
  device,
762
769
  run_dir,
@@ -797,7 +804,13 @@ class XiaohongshuAdbPublisher:
797
804
 
798
805
  self._open_me_tab(device, run_dir)
799
806
  self._dismiss_link_recovery_prompts(device, run_dir)
800
- self._tap_latest_profile_note(device, run_dir, expected_title=expected_title)
807
+ self._tap_latest_profile_note(
808
+ device,
809
+ run_dir,
810
+ expected_title=expected_title,
811
+ require_expected_match=bool(expected_title or expected_caption),
812
+ allow_first_card_fallback=bool(expected_title or expected_caption),
813
+ )
801
814
  self._wait_for_detail_screen(device, run_dir)
802
815
  return self._verify_current_note_detail(
803
816
  device,
@@ -807,6 +820,13 @@ class XiaohongshuAdbPublisher:
807
820
  prefix="published-note-detail",
808
821
  )
809
822
 
823
+ def _restart_xiaohongshu_for_link_recovery(self, device: Device, run_dir: Path) -> None:
824
+ self._close_xiaohongshu(device)
825
+ time.sleep(self._wait("after_prompt_dismiss"))
826
+ self.adb.screenshot(device.adb_serial, run_dir / "after-publish-app-closed.png")
827
+ self._launch_xiaohongshu(device)
828
+ self.adb.screenshot(device.adb_serial, run_dir / "after-publish-relaunch.png")
829
+
810
830
  def _dismiss_link_recovery_prompts(self, device: Device, run_dir: Path) -> None:
811
831
  for idx in range(2):
812
832
  try:
@@ -844,7 +864,22 @@ class XiaohongshuAdbPublisher:
844
864
  self._tap_coord_or_default(device, "bottom_me", self._relative_coord(0.9, 0.93), self._wait("after_me_tab"))
845
865
  self.adb.screenshot(device.adb_serial, run_dir / "profile.png")
846
866
 
847
- def _tap_latest_profile_note(self, device: Device, run_dir: Path, *, expected_title: str = "") -> None:
867
+ def _tap_latest_profile_note(
868
+ self,
869
+ device: Device,
870
+ run_dir: Path,
871
+ *,
872
+ expected_title: str = "",
873
+ require_expected_match: bool = False,
874
+ allow_first_card_fallback: bool = False,
875
+ ) -> None:
876
+ if require_expected_match and not expected_title:
877
+ not_identifiable_shot = run_dir / "expected-profile-note-not-identifiable.png"
878
+ self.adb.screenshot(device.adb_serial, not_identifiable_shot)
879
+ raise XiaohongshuAutomationError(
880
+ "expected Xiaohongshu profile note cannot be selected safely without a title",
881
+ not_identifiable_shot,
882
+ )
848
883
  attempts = 8 if expected_title else 3
849
884
  for attempt in range(attempts):
850
885
  root = ui.dump_ui(self.adb, device, run_dir / f"profile-before-latest-note-{attempt + 1}.xml")
@@ -857,7 +892,14 @@ class XiaohongshuAdbPublisher:
857
892
  self._scroll_profile_notes_down_half_screen(device)
858
893
  self.adb.screenshot(device.adb_serial, run_dir / f"profile-after-scroll-{attempt + 1}.png")
859
894
  time.sleep(2 if expected_title else 1)
860
- if expected_title:
895
+ if allow_first_card_fallback:
896
+ root = ui.dump_ui(self.adb, device, run_dir / "profile-before-first-card-fallback.xml")
897
+ node = self._find_latest_profile_note_node(root, expected_title="")
898
+ if node is not None:
899
+ ui.tap_node(self.adb, device, node, sleep_s=self._wait("after_latest_note_tap"))
900
+ self.adb.screenshot(device.adb_serial, run_dir / "first-card-fallback-opened.png")
901
+ return
902
+ if expected_title or require_expected_match:
861
903
  not_found_shot = run_dir / "expected-profile-note-not-found.png"
862
904
  self.adb.screenshot(device.adb_serial, not_found_shot)
863
905
  raise XiaohongshuAutomationError(
@@ -966,7 +1008,8 @@ class XiaohongshuAdbPublisher:
966
1008
  continue
967
1009
  note_text = f"{text}\n{desc}"
968
1010
  if expected:
969
- if not _looks_like_profile_note_card(node) or expected not in _normalize_match_text(note_text):
1011
+ note_match = _normalize_match_text(note_text)
1012
+ if not _looks_like_profile_note_card(node) or not _profile_card_matches_expected_title(note_match, expected):
970
1013
  continue
971
1014
  return node
972
1015
  item = (top, left, node)
@@ -1462,7 +1505,7 @@ def _note_text_matches_expected(visible_text: str, expected_title: str, expected
1462
1505
  title = _normalize_match_text(expected_title)
1463
1506
  caption_fragment = _verification_fragment(expected_caption)
1464
1507
 
1465
- if title and title not in haystack:
1508
+ if title and title not in haystack and not _visible_text_matches_truncated_title(haystack, title):
1466
1509
  return False, f"title not visible: {expected_title!r}"
1467
1510
  if caption_fragment and caption_fragment not in haystack:
1468
1511
  return False, f"caption fragment not visible: {caption_fragment!r}"
@@ -1471,6 +1514,13 @@ def _note_text_matches_expected(visible_text: str, expected_title: str, expected
1471
1514
  return True, ""
1472
1515
 
1473
1516
 
1517
+ def _visible_text_matches_truncated_title(visible_text: str, expected_title: str) -> bool:
1518
+ if not visible_text or not expected_title:
1519
+ return False
1520
+ min_prefix = min(18, len(expected_title))
1521
+ return len(expected_title) >= min_prefix and expected_title[:min_prefix] in visible_text
1522
+
1523
+
1474
1524
  def _looks_like_profile_note_card(node) -> bool:
1475
1525
  text = node.attrib.get("text", "")
1476
1526
  desc = node.attrib.get("content-desc", "")
@@ -1570,6 +1620,19 @@ def _verification_fragment(text: str) -> str:
1570
1620
  return normalized[:18]
1571
1621
 
1572
1622
 
1623
+ def _profile_card_matches_expected_title(card_text: str, expected_title: str) -> bool:
1624
+ if not card_text or not expected_title:
1625
+ return False
1626
+ if expected_title in card_text:
1627
+ return True
1628
+ min_prefix = min(18, len(expected_title))
1629
+ if len(card_text) >= min_prefix and card_text in expected_title:
1630
+ return True
1631
+ if len(card_text) >= min_prefix and expected_title.startswith(card_text):
1632
+ return True
1633
+ return expected_title[:min_prefix] in card_text
1634
+
1635
+
1573
1636
  def _strip_hashtags(text: str) -> str:
1574
1637
  return re.sub(r"#[^\s#]+", "", text or "").strip()
1575
1638
 
@@ -84,7 +84,7 @@ X uses one `body` field, not a title/body split.
84
84
  11agents mobile copy-xiaohongshu-link --device D03 --record-id pub_123 --json --task-id TASK_123
85
85
  ```
86
86
 
87
- Use `--confirm-no-disclosure-needed` only when the approved package and policy explicitly allow it.
87
+ Live Xiaohongshu publish recovers the note link by default after publish. Use `--no-copy-link-after-publish` only when automatic link recovery is explicitly not wanted. Use `--confirm-no-disclosure-needed` only when the approved package and policy explicitly allow it.
88
88
 
89
89
  ## Records And Data Recovery
90
90
 
@@ -23,8 +23,8 @@ If approval, account lifecycle, media/package path, text fields, or device readi
23
23
  Image/video note:
24
24
 
25
25
  ```bash
26
- 11agents mobile publish-xiaohongshu --device D03 --post-type image --image "/path/image.jpg" --title "title" --caption "body" --copy-link-after-publish --json --task-id TASK_123
27
- 11agents mobile publish-xiaohongshu --device D03 --post-type video --video "/path/video.mp4" --title "title" --caption "body" --copy-link-after-publish --json --task-id TASK_123
26
+ 11agents mobile publish-xiaohongshu --device D03 --post-type image --image "/path/image.jpg" --title "title" --caption "body" --json --task-id TASK_123
27
+ 11agents mobile publish-xiaohongshu --device D03 --post-type video --video "/path/video.mp4" --title "title" --caption "body" --json --task-id TASK_123
28
28
  ```
29
29
 
30
30
  Structured package:
@@ -33,7 +33,7 @@ Structured package:
33
33
  11agents mobile publish-xiaohongshu --device D03 --publish-package "/path/publish_package.json" --json --task-id TASK_123
34
34
  ```
35
35
 
36
- Successful live publish is only confirmed after the CLI taps the final publish button, waits without tapping while Xiaohongshu shows upload/progress state, then opens the "我" page, opens the note whose title matches the expected title, and verifies the detail page against the expected title/body. If that evidence does not match, treat the result as `failed`; do not claim publish success from the publish button tap alone.
36
+ Successful live publish is only confirmed after the CLI taps the final publish button, waits without tapping while Xiaohongshu shows upload/progress state, closes and relaunches Xiaohongshu, then opens the "我" page, opens the note whose title matches the expected title or the first visible profile note as a verified fallback, and verifies the detail page against the expected title/body. The CLI recovers the note link by default for live publish; use `--no-copy-link-after-publish` only when link recovery is explicitly not wanted. If that evidence does not match, treat the result as `failed`; do not claim publish success from the publish button tap alone.
37
37
 
38
38
  Link copy after publish or during the recovery skill:
39
39
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@11agents/cli",
3
- "version": "0.1.28",
3
+ "version": "0.1.30",
4
4
  "description": "11agents local runtime and telemetry CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1083,13 +1083,12 @@ function buildCodexPrompt(task) {
1083
1083
  '- image_size alternatives: "1K", "2K". For a specific output size, pass aspect and image_size; pass size only when the MCP tool/provider explicitly supports the requested exact dimensions.',
1084
1084
  '- Use model="gpt" with model_id only when the task or current project notes confirm that GPT image generation is available for the hosted MCP key.',
1085
1085
  '- MCP authentication (agent_planning, database_sync, knowledge_sync, image_generate, etc.):',
1086
- ' 1. Find your project_slug from the task context (task.workspace.slug).',
1087
- ' 2. Open ~/.11agents/credentials.yaml and read the value under tokens.<project_slug>.',
1088
- ' 3. Use that value as the Bearer token in the Authorization header for the 11agents hosted MCP endpoint.',
1089
- ' 4. If the key is missing from credentials.yaml, add it:',
1090
- ' tokens:',
1091
- ' <project_slug>: <your-token>',
1092
- ' Get the token from the 11agents platform → open your project → Tokens menu.',
1086
+ ' - The GTM_SWARM_TOKEN env var is already set to the correct project token for this workspace.',
1087
+ ' - Configure the 11agents hosted MCP server using that env var:',
1088
+ ' {"mcpServers":{"11agents-project-sync":{"url":"https://app.11agents.ai/mcp","transport":"streamable-http","headers":{"Authorization":"Bearer $GTM_SWARM_TOKEN"}}}}',
1089
+ ' - NEVER hardcode the token value always reference $GTM_SWARM_TOKEN so the config stays correct across workspaces.',
1090
+ ' - If GTM_SWARM_TOKEN is missing, get the token from ~/.11agents/credentials.yaml under tokens.<project_slug>.',
1091
+ ' If that file is empty, go to the 11agents platform → your project → Tokens menu.',
1093
1092
  '',
1094
1093
  'Final task comment requirements:',
1095
1094
  '> [!IMPORTANT]',