@nextclaw/x-harness 0.1.0 → 0.2.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.
Files changed (113) hide show
  1. package/LICENSE +231 -6
  2. package/README.md +17 -3
  3. package/SOURCE.md +9 -0
  4. package/cli/main.js +232 -188
  5. package/dist/api.d.ts +56 -2
  6. package/dist/api.js +178 -75
  7. package/dist/boundary.js +1 -0
  8. package/dist/browser-common.d.ts +11 -0
  9. package/dist/browser-common.js +34 -18
  10. package/dist/browser-dom-capabilities.d.ts +9 -0
  11. package/dist/browser-dom-capabilities.js +131 -0
  12. package/dist/browser-dom-dependencies.d.ts +16 -0
  13. package/dist/browser-dom-dependencies.js +1 -0
  14. package/dist/browser-dom-editing.d.ts +166 -0
  15. package/dist/browser-dom-editing.js +418 -0
  16. package/dist/browser-dom-navigation.d.ts +18 -0
  17. package/dist/browser-dom-navigation.js +181 -0
  18. package/dist/browser-dom-network.d.ts +1 -0
  19. package/dist/browser-dom-network.js +128 -0
  20. package/dist/browser-dom-submission.d.ts +7 -0
  21. package/dist/browser-dom-submission.js +101 -0
  22. package/dist/browser-dom.d.ts +14 -2
  23. package/dist/browser-dom.js +120 -824
  24. package/dist/browser-native.d.ts +14 -1
  25. package/dist/browser-native.js +180 -143
  26. package/dist/browser.js +6 -2
  27. package/dist/creator-native.d.ts +3 -0
  28. package/dist/creator-native.js +113 -0
  29. package/dist/creator.js +1 -1
  30. package/dist/diagnostics.d.ts +11 -0
  31. package/dist/diagnostics.js +10 -0
  32. package/dist/growth-batch.d.ts +8 -0
  33. package/dist/growth-batch.js +190 -0
  34. package/dist/growth-constants.d.ts +5 -0
  35. package/dist/growth-constants.js +13 -0
  36. package/dist/growth-dependencies.d.ts +33 -0
  37. package/dist/growth-dependencies.js +1 -0
  38. package/dist/growth-execution.d.ts +17 -0
  39. package/dist/growth-execution.js +346 -0
  40. package/dist/growth-parallel.d.ts +8 -0
  41. package/dist/growth-parallel.js +332 -0
  42. package/dist/growth-plan.d.ts +9 -0
  43. package/dist/growth-plan.js +229 -0
  44. package/dist/growth-policy.d.ts +18 -0
  45. package/dist/growth-policy.js +208 -0
  46. package/dist/growth-publishing.d.ts +10 -0
  47. package/dist/growth-publishing.js +266 -0
  48. package/dist/growth-reading.d.ts +8 -0
  49. package/dist/growth-reading.js +157 -0
  50. package/dist/growth-recovery.d.ts +9 -0
  51. package/dist/growth-recovery.js +187 -0
  52. package/dist/growth-state.d.ts +16 -0
  53. package/dist/growth-state.js +220 -0
  54. package/dist/growth-tasks.d.ts +20 -0
  55. package/dist/growth-tasks.js +164 -0
  56. package/dist/growth.d.ts +26 -1
  57. package/dist/growth.js +211 -1908
  58. package/dist/platform-capabilities.d.ts +8 -0
  59. package/dist/platform-capabilities.js +29 -0
  60. package/dist/platform-collections.d.ts +14 -0
  61. package/dist/platform-collections.js +250 -0
  62. package/dist/platform-dependencies.d.ts +43 -0
  63. package/dist/platform-dependencies.js +1 -0
  64. package/dist/platform-diagnostics.d.ts +14 -0
  65. package/dist/platform-diagnostics.js +63 -0
  66. package/dist/platform-observation.d.ts +16 -0
  67. package/dist/platform-observation.js +96 -0
  68. package/dist/platform-rules.d.ts +17 -0
  69. package/dist/platform-rules.js +41 -0
  70. package/dist/platform-targets.d.ts +14 -0
  71. package/dist/platform-targets.js +154 -0
  72. package/dist/platform-writing.d.ts +16 -0
  73. package/dist/platform-writing.js +202 -0
  74. package/dist/platform.d.ts +23 -2
  75. package/dist/platform.js +125 -678
  76. package/dist/readers-actions.d.ts +2 -0
  77. package/dist/readers-actions.js +66 -0
  78. package/dist/readers-ax-host.d.ts +12 -0
  79. package/dist/readers-ax-host.js +134 -0
  80. package/dist/readers-ax-parser.d.ts +1 -0
  81. package/dist/readers-ax-parser.js +69 -0
  82. package/dist/readers-ax-program.d.ts +1 -0
  83. package/dist/readers-ax-program.js +5 -0
  84. package/dist/readers-ax-tree.d.ts +19 -0
  85. package/dist/readers-ax-tree.js +111 -0
  86. package/dist/readers-ax-views.d.ts +87 -0
  87. package/dist/readers-ax-views.js +247 -0
  88. package/dist/readers-cli.d.ts +6 -0
  89. package/dist/readers-cli.js +79 -0
  90. package/dist/readers-composer.d.ts +2 -0
  91. package/dist/readers-composer.js +131 -0
  92. package/dist/readers-navigation.d.ts +2 -0
  93. package/dist/readers-navigation.js +66 -0
  94. package/dist/readers-posts.d.ts +2 -0
  95. package/dist/readers-posts.js +141 -0
  96. package/dist/readers-profile.d.ts +3 -0
  97. package/dist/readers-profile.js +157 -0
  98. package/dist/readers.d.ts +9 -20
  99. package/dist/readers.js +9 -1045
  100. package/dist/results.js +1 -1
  101. package/dist/round-state.js +20 -3
  102. package/dist/store.d.ts +22 -4
  103. package/dist/store.js +173 -151
  104. package/dist/types.d.ts +10 -0
  105. package/dist/types.js +1 -0
  106. package/generated/observation.mjs +3 -2
  107. package/package.json +23 -9
  108. package/skill/SKILL.md +17 -36
  109. package/skill/references/api.md +7 -34
  110. package/skill/references/reads.md +23 -0
  111. package/skill/references/recovery.md +25 -0
  112. package/skill/references/setup.md +23 -0
  113. package/skill/references/writes.md +51 -0
@@ -0,0 +1,141 @@
1
+ // Self-contained, read-only DOM function for cua_repl's documented evaluate.
2
+ export function readPosts({ offset = 0, limit = 5, targetId = "", } = {}) {
3
+ if (!Number.isInteger(offset) ||
4
+ offset < 0 ||
5
+ !Number.isInteger(limit) ||
6
+ limit < 1 ||
7
+ limit > 5)
8
+ throw new Error("offset>=0;limit为1~5");
9
+ if (targetId && !/^\d+$/.test(targetId))
10
+ throw new Error("targetId必须是数字公开ID");
11
+ const visible = (el) => el.getClientRects().length > 0;
12
+ const primaryTime = (a) => Array.from(a.querySelectorAll("time")).find((t) => visible(t) &&
13
+ /\/status\/\d+(?:$|[/?#])/.test(t.closest("a")?.getAttribute("href") ?? "")) ?? null;
14
+ const all = Array.from(document.querySelectorAll("article")).filter(visible);
15
+ const candidates = targetId
16
+ ? all.filter((a) => {
17
+ const primary = primaryTime(a)?.closest("a")?.getAttribute("href");
18
+ return (primary && new RegExp(`/status/${targetId}(?:$|[/?#])`).test(primary));
19
+ })
20
+ : all;
21
+ const posts = candidates.slice(offset, offset + limit).map((a) => {
22
+ const time = primaryTime(a);
23
+ const primary = time?.closest("a")?.getAttribute("href") ?? null;
24
+ const links = [
25
+ ...new Set(Array.from(a.querySelectorAll("a[href]"))
26
+ .filter(visible)
27
+ .map((l) => l.getAttribute("href"))),
28
+ ];
29
+ const buttons = Array.from(a.querySelectorAll("button"))
30
+ .filter(visible)
31
+ .map((b) => ({
32
+ label: b.getAttribute("aria-label") || b.innerText,
33
+ disabled: b.hasAttribute("disabled"),
34
+ }));
35
+ const text = a.innerText;
36
+ const datetime = time?.getAttribute("datetime") ?? null;
37
+ const id = primary?.match(/\/status\/(\d+)/)?.[1] ?? null;
38
+ const author = primary?.match(/\/([^/]+)\/status\//)?.[1] ?? null;
39
+ const images = Array.from(a.querySelectorAll('[data-testid="tweetPhoto"] img'))
40
+ .filter(visible)
41
+ .map((img) => ({
42
+ alt: img.getAttribute("alt"),
43
+ src: img.getAttribute("src"),
44
+ }));
45
+ const media = Array.from(a.querySelectorAll('[data-testid="videoPlayer"],video'))
46
+ .filter(visible)
47
+ .map((el) => ({
48
+ kind: "video",
49
+ label: el.getAttribute("aria-label"),
50
+ poster: el.getAttribute("poster"),
51
+ requiresViewing: true,
52
+ }));
53
+ return {
54
+ id,
55
+ author,
56
+ datetime,
57
+ primary,
58
+ text,
59
+ links,
60
+ buttons,
61
+ // View/quote counts may be visible links rather than buttons; collect in this same read.
62
+ metrics: [
63
+ ...buttons,
64
+ ...Array.from(a.querySelectorAll('a[href$="/analytics"],a[href$="/quotes"]'))
65
+ .filter(visible)
66
+ .map((link) => ({
67
+ label: link.getAttribute("aria-label") ||
68
+ `${link.getAttribute("href").endsWith("/analytics") ? "观看" : "引用"} ${link.innerText}`,
69
+ })),
70
+ ],
71
+ images,
72
+ media,
73
+ needsExpansion: /显示更多|Show more/i.test(text),
74
+ missing: [
75
+ !id && "id",
76
+ !author && "author",
77
+ !datetime && "datetime",
78
+ !text && "text",
79
+ ].filter(Boolean),
80
+ };
81
+ });
82
+ const versionLinks = Array.from(document.querySelectorAll("a[href]"))
83
+ .filter(visible)
84
+ .filter((a) => /最新版本|查看最新帖子|Latest version|View latest/i.test(a.innerText))
85
+ .slice(0, 5)
86
+ .map((a) => ({ label: a.innerText, href: a.getAttribute("href") }));
87
+ const emptyEvidence = all.length === 0
88
+ ? [
89
+ ...new Set(Array.from(document.querySelectorAll("main span"))
90
+ .filter(visible)
91
+ .map((e) => e.innerText?.trim())
92
+ .filter((text) => text &&
93
+ text.length <= 400 &&
94
+ !text.includes("\n") &&
95
+ /^(?:没有.*结果|未找到.*结果|No results for)/i.test(text))),
96
+ ].slice(0, 2)
97
+ : [];
98
+ return {
99
+ renderedArticles: all.length,
100
+ matched: candidates.length,
101
+ offset,
102
+ moreInDOM: candidates.length > offset + posts.length,
103
+ pageComplete: false,
104
+ needsStateCheck: all.length === 0 && emptyEvidence.length === 0,
105
+ posts,
106
+ versionLinks,
107
+ emptyEvidence,
108
+ alerts: Array.from(document.querySelectorAll('[role="alert"]'))
109
+ .filter(visible)
110
+ .map((e) => e.innerText),
111
+ };
112
+ }
113
+ export function changes(result, cache, page, fresh = false) {
114
+ const unchangedIds = [];
115
+ const posts = result.posts.filter((post) => {
116
+ if (!post.id)
117
+ return true;
118
+ const key = `${page}|${post.id}`;
119
+ const value = JSON.stringify(post);
120
+ const changed = cache[key] !== value;
121
+ cache[key] = value;
122
+ if (!fresh && !changed)
123
+ unchangedIds.push(post.id);
124
+ return fresh || changed;
125
+ });
126
+ const outputChars = JSON.stringify({ ...result, posts, unchangedIds }).length;
127
+ const optimizationHint = outputChars > 8000
128
+ ? posts.length > 1
129
+ ? "缩小读取窗口或按已观察ID定向读取;保留完整正文"
130
+ : "单篇证据本身长,保留全文,不截断"
131
+ : null;
132
+ return {
133
+ ...result,
134
+ posts,
135
+ unchanged: result.posts.length - posts.length,
136
+ unchangedIds,
137
+ fresh,
138
+ outputChars,
139
+ optimizationHint,
140
+ };
141
+ }
@@ -0,0 +1,3 @@
1
+ export declare function readProfile({ handle }?: any): any;
2
+ export declare function readPeople({ offset, limit }?: any): any;
3
+ export declare function screenCandidates(profiles: any, { minRatio }?: any): any;
@@ -0,0 +1,157 @@
1
+ // Profile selectors are the visible controls observed in the live X workflow.
2
+ // Missing/ambiguous counters remain unknown; rounded counts carry their precision.
3
+ export function readProfile({ handle } = {}) {
4
+ if (!/^[A-Za-z0-9_]{1,15}$/.test(handle ?? ""))
5
+ throw new Error("需要已观察的X handle");
6
+ const visible = (el) => el.getClientRects().length > 0;
7
+ const main = document.querySelector("main");
8
+ if (!main)
9
+ return {
10
+ handle,
11
+ blue: null,
12
+ relationship: "unknown",
13
+ alerts: ["missing-main"],
14
+ observedAt: new Date().toISOString(),
15
+ };
16
+ const buttons = Array.from(main.querySelectorAll("button")).filter(visible);
17
+ const labels = buttons.map((b) => b.getAttribute("aria-label") || b.innerText);
18
+ const relationship = labels.includes(`正在关注 @${handle}`) ||
19
+ labels.includes(`取消关注 @${handle}`)
20
+ ? "following"
21
+ : labels.includes(`回关 @${handle}`)
22
+ ? "follows-you"
23
+ : labels.includes(`关注 @${handle}`)
24
+ ? "not-following"
25
+ : "unknown";
26
+ const badges = buttons
27
+ .filter((b) => b.getAttribute("aria-label") === "提供已验证账号的详细信息。")
28
+ .map((b) => b.querySelector("svg"))
29
+ .filter(Boolean);
30
+ const blue = badges.length === 1
31
+ ? [
32
+ getComputedStyle(badges[0]).color,
33
+ getComputedStyle(badges[0]).fill,
34
+ ].includes("rgb(29, 155, 240)")
35
+ : null;
36
+ const links = Array.from(main.querySelectorAll("a[href]")).filter(visible);
37
+ const counter = (suffixes) => {
38
+ const matched = links.filter((a) => suffixes.some((s) => a.getAttribute("href")?.toLowerCase() ===
39
+ `/${handle}/${s}`.toLowerCase()));
40
+ const texts = [...new Set(matched.map((a) => a.innerText.trim()))];
41
+ if (texts.length !== 1)
42
+ return { value: null, text: texts, precision: "unknown" };
43
+ const match = texts[0].match(/^([\d,.]+)\s*(万|亿|[KMB])?/i);
44
+ const factors = {
45
+ 万: 1e4,
46
+ 亿: 1e8,
47
+ K: 1e3,
48
+ M: 1e6,
49
+ B: 1e9,
50
+ };
51
+ const value = match
52
+ ? Number(match[1].replaceAll(",", "")) *
53
+ (factors[match[2]?.toUpperCase()] ?? 1)
54
+ : null;
55
+ const hrefs = [...new Set(matched.map((a) => a.getAttribute("href")))];
56
+ return {
57
+ value: Number.isFinite(value) ? value : null,
58
+ text: texts[0],
59
+ precision: match?.[2] ? "rounded" : "exact",
60
+ url: hrefs.length === 1 ? "https://x.com" + hrefs[0] : null,
61
+ };
62
+ };
63
+ return {
64
+ handle,
65
+ blue,
66
+ relationship,
67
+ relationshipControls: labels
68
+ .filter((label) => label.includes("@" + handle) || /取消关注|订阅/.test(label))
69
+ .slice(0, 5),
70
+ biography: main.querySelector('[data-testid="UserDescription"]')
71
+ ?.innerText ?? null,
72
+ following: counter(["following"]),
73
+ followers: counter(["followers", "verified_followers"]),
74
+ alerts: Array.from(document.querySelectorAll('[role="alert"]'))
75
+ .filter(visible)
76
+ .map((a) => a.innerText),
77
+ observedAt: new Date().toISOString(),
78
+ };
79
+ }
80
+ // Bounded visible person cells for a currently loaded follower/following list.
81
+ export function readPeople({ offset = 0, limit = 5 } = {}) {
82
+ if (!Number.isInteger(offset) ||
83
+ offset < 0 ||
84
+ !Number.isInteger(limit) ||
85
+ limit < 1 ||
86
+ limit > 5)
87
+ throw new Error("人物窗口须为非负offset及1–5 limit");
88
+ const visible = (el) => el.getClientRects().length > 0;
89
+ const cells = Array.from(document.querySelectorAll('main [data-testid="UserCell"]')).filter(visible);
90
+ const people = cells.slice(offset, offset + limit).map((cell) => {
91
+ const links = Array.from(cell.querySelectorAll("a[href]"))
92
+ .filter(visible)
93
+ .map((a) => ({ text: a.innerText, href: a.getAttribute("href") }));
94
+ const buttons = Array.from(cell.querySelectorAll("button"))
95
+ .filter(visible)
96
+ .map((b) => ({
97
+ label: b.getAttribute("aria-label") || b.innerText,
98
+ disabled: b.disabled,
99
+ }));
100
+ return { text: cell.innerText, links, buttons };
101
+ });
102
+ return {
103
+ page: location.href,
104
+ people,
105
+ offset,
106
+ renderedPeople: cells.length,
107
+ moreInDOM: offset + limit < cells.length,
108
+ pageComplete: false,
109
+ needsStateCheck: !people.length,
110
+ missing: people.length ? [] : ["people-cells-not-found"],
111
+ alerts: Array.from(document.querySelectorAll('[role="alert"]'))
112
+ .filter(visible)
113
+ .map((a) => a.innerText),
114
+ observedAt: new Date().toISOString(),
115
+ };
116
+ }
117
+ // This ranks observed clues, never a predicted follow-back probability.
118
+ export function screenCandidates(profiles, { minRatio = 0.8 } = {}) {
119
+ if (!Array.isArray(profiles) || !Number.isFinite(minRatio) || minRatio <= 0)
120
+ throw new Error("profiles数组和正minRatio必填");
121
+ const seen = new Set();
122
+ return profiles
123
+ .map((p) => {
124
+ const key = String(p.handle ?? "").toLowerCase();
125
+ const duplicate = seen.has(key);
126
+ seen.add(key);
127
+ const following = p.following?.value, followers = p.followers?.value;
128
+ const ratio = Number.isFinite(following) &&
129
+ Number.isFinite(followers) &&
130
+ followers > 0
131
+ ? following / followers
132
+ : null;
133
+ const clues = [];
134
+ if (typeof p.mutualEvidence === "string" && p.mutualEvidence.trim())
135
+ clues.push({ kind: "explicit-mutual", evidence: p.mutualEvidence });
136
+ if (ratio !== null && ratio >= minRatio)
137
+ clues.push({
138
+ kind: "following-ratio",
139
+ ratio,
140
+ following: p.following,
141
+ followers: p.followers,
142
+ evidence: "关注/粉丝比例线索,非回关概率",
143
+ });
144
+ const rejected = [
145
+ duplicate && "duplicate",
146
+ p.blue !== true && "blue-unconfirmed",
147
+ p.relationship !== "not-following" && "relationship-ineligible",
148
+ p.alerts?.length && "page-alert",
149
+ !clues.length && "no-followback-clue",
150
+ !/^[A-Za-z0-9_]{1,15}$/.test(p.handle ?? "") && "invalid-handle",
151
+ ].filter(Boolean);
152
+ return { ...p, ratio, clues, eligible: rejected.length === 0, rejected };
153
+ })
154
+ .sort((a, b) => Number(b.eligible) - Number(a.eligible) ||
155
+ Number(b.clues.some((c) => c.kind === "explicit-mutual")) -
156
+ Number(a.clues.some((c) => c.kind === "explicit-mutual")));
157
+ }
package/dist/readers.d.ts CHANGED
@@ -1,20 +1,9 @@
1
- export declare function readPosts({ offset, limit, targetId, }?: any): any;
2
- export declare function changes(result: any, cache: any, page: any, fresh?: any): any;
3
- export declare function readAX(state: any, { mode, offset, limit, targetId, targetHandle, }?: any): any;
4
- export declare function createAXTabHarness(tab: any): any;
5
- export declare function createAXHarness(app: any): any;
6
- export declare function setupCode(): any;
7
- export declare function readProfile({ handle }?: any): any;
8
- export declare function readPeople({ offset, limit }?: any): any;
9
- export declare function readComposer(): any;
10
- export declare function readMenu(): any;
11
- export declare function readNavigation({ targetLabel }?: any): any;
12
- export declare function readMediaEditor(): any;
13
- export declare function attachImage(tab: any, { path }?: any): Promise<any>;
14
- export declare function openReply(tab: any, { postId }?: any): Promise<any>;
15
- export declare function screenCandidates(profiles: any, { minRatio }?: any): any;
16
- export declare function profileCode({ tab, handle }?: any): any;
17
- export declare function profilesCode(entries: any): any;
18
- export declare function browserCode({ handle, offset, limit, targetId, fresh, }?: any): any;
19
- export declare function axCode({ handle, mode, offset, limit, targetId, targetHandle, fresh, }?: any): any;
20
- export declare function runReaderCLI(): void;
1
+ /** Compatibility exports only. Implementation owners are task-specific modules. */
2
+ export { readPosts, changes } from "./readers-posts.js";
3
+ export { readAX } from "./readers-ax-parser.js";
4
+ export { createAXTabHarness, createAXHarness } from "./readers-ax-host.js";
5
+ export { readProfile, readPeople, screenCandidates, } from "./readers-profile.js";
6
+ export { readComposer, readMediaEditor } from "./readers-composer.js";
7
+ export { readMenu, readNavigation } from "./readers-navigation.js";
8
+ export { attachImage, openReply } from "./readers-actions.js";
9
+ export { setupCode, profileCode, profilesCode, browserCode, axCode, runReaderCLI, } from "./readers-cli.js";