@zenalexa/unicli 0.221.0 → 0.222.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 (247) hide show
  1. package/AGENTS.md +12 -12
  2. package/README.md +40 -31
  3. package/README.zh-CN.md +37 -31
  4. package/crates/unicli-atspi/src/errors.rs +2 -2
  5. package/crates/unicli-uia/src/errors.rs +1 -1
  6. package/dist/adapters/acl-anthology/papers.d.ts +16 -0
  7. package/dist/adapters/acl-anthology/papers.d.ts.map +1 -0
  8. package/dist/adapters/acl-anthology/papers.js +135 -0
  9. package/dist/adapters/acl-anthology/papers.js.map +1 -0
  10. package/dist/adapters/arxiv/papers.js +2 -0
  11. package/dist/adapters/arxiv/papers.js.map +1 -1
  12. package/dist/adapters/baidu-scholar/search.js +5 -0
  13. package/dist/adapters/baidu-scholar/search.js.map +1 -1
  14. package/dist/adapters/crossref/works.d.ts +42 -0
  15. package/dist/adapters/crossref/works.d.ts.map +1 -0
  16. package/dist/adapters/crossref/works.js +157 -0
  17. package/dist/adapters/crossref/works.js.map +1 -0
  18. package/dist/adapters/cvf/papers.d.ts +17 -0
  19. package/dist/adapters/cvf/papers.d.ts.map +1 -0
  20. package/dist/adapters/cvf/papers.js +124 -0
  21. package/dist/adapters/cvf/papers.js.map +1 -0
  22. package/dist/adapters/dblp/publications.js +4 -0
  23. package/dist/adapters/dblp/publications.js.map +1 -1
  24. package/dist/adapters/google-scholar/cite.js +1 -0
  25. package/dist/adapters/google-scholar/cite.js.map +1 -1
  26. package/dist/adapters/google-scholar/profile.js +5 -0
  27. package/dist/adapters/google-scholar/profile.js.map +1 -1
  28. package/dist/adapters/google-scholar/search.js +5 -0
  29. package/dist/adapters/google-scholar/search.js.map +1 -1
  30. package/dist/adapters/hf/paper.js +1 -0
  31. package/dist/adapters/hf/paper.js.map +1 -1
  32. package/dist/adapters/neurips/proceedings.d.ts +17 -0
  33. package/dist/adapters/neurips/proceedings.d.ts.map +1 -0
  34. package/dist/adapters/neurips/proceedings.js +112 -0
  35. package/dist/adapters/neurips/proceedings.js.map +1 -0
  36. package/dist/adapters/openalex/works.d.ts.map +1 -1
  37. package/dist/adapters/openalex/works.js +32 -0
  38. package/dist/adapters/openalex/works.js.map +1 -1
  39. package/dist/adapters/openreview/papers.js +5 -0
  40. package/dist/adapters/openreview/papers.js.map +1 -1
  41. package/dist/adapters/pmlr/proceedings.d.ts +35 -0
  42. package/dist/adapters/pmlr/proceedings.d.ts.map +1 -0
  43. package/dist/adapters/pmlr/proceedings.js +139 -0
  44. package/dist/adapters/pmlr/proceedings.js.map +1 -0
  45. package/dist/adapters/pubmed/articles.js +5 -0
  46. package/dist/adapters/pubmed/articles.js.map +1 -1
  47. package/dist/adapters/semantic-scholar/papers.d.ts +36 -0
  48. package/dist/adapters/semantic-scholar/papers.d.ts.map +1 -0
  49. package/dist/adapters/semantic-scholar/papers.js +214 -0
  50. package/dist/adapters/semantic-scholar/papers.js.map +1 -0
  51. package/dist/adapters/unpaywall/works.d.ts +33 -0
  52. package/dist/adapters/unpaywall/works.d.ts.map +1 -0
  53. package/dist/adapters/unpaywall/works.js +101 -0
  54. package/dist/adapters/unpaywall/works.js.map +1 -0
  55. package/dist/cli.d.ts.map +1 -1
  56. package/dist/cli.js +15 -1
  57. package/dist/cli.js.map +1 -1
  58. package/dist/commands/compute.d.ts.map +1 -1
  59. package/dist/commands/compute.js +1 -1
  60. package/dist/commands/compute.js.map +1 -1
  61. package/dist/commands/do.d.ts +30 -0
  62. package/dist/commands/do.d.ts.map +1 -0
  63. package/dist/commands/do.js +248 -0
  64. package/dist/commands/do.js.map +1 -0
  65. package/dist/commands/doctor-compute.js +11 -7
  66. package/dist/commands/doctor-compute.js.map +1 -1
  67. package/dist/commands/extract.d.ts +34 -0
  68. package/dist/commands/extract.d.ts.map +1 -0
  69. package/dist/commands/extract.js +316 -0
  70. package/dist/commands/extract.js.map +1 -0
  71. package/dist/commands/lint.js +3 -3
  72. package/dist/commands/lint.js.map +1 -1
  73. package/dist/commands/migrate-schema.d.ts.map +1 -1
  74. package/dist/commands/migrate-schema.js +26 -22
  75. package/dist/commands/migrate-schema.js.map +1 -1
  76. package/dist/commands/scholar.d.ts +33 -0
  77. package/dist/commands/scholar.d.ts.map +1 -0
  78. package/dist/commands/scholar.js +494 -0
  79. package/dist/commands/scholar.js.map +1 -0
  80. package/dist/commands/search.d.ts.map +1 -1
  81. package/dist/commands/search.js +2 -5
  82. package/dist/commands/search.js.map +1 -1
  83. package/dist/discovery/aliases.d.ts +2 -2
  84. package/dist/discovery/aliases.d.ts.map +1 -1
  85. package/dist/discovery/aliases.js +182 -11
  86. package/dist/discovery/aliases.js.map +1 -1
  87. package/dist/discovery/intents.d.ts +10 -0
  88. package/dist/discovery/intents.d.ts.map +1 -0
  89. package/dist/discovery/intents.js +255 -0
  90. package/dist/discovery/intents.js.map +1 -0
  91. package/dist/discovery/loader.d.ts.map +1 -1
  92. package/dist/discovery/loader.js +47 -2
  93. package/dist/discovery/loader.js.map +1 -1
  94. package/dist/discovery/search.d.ts +4 -1
  95. package/dist/discovery/search.d.ts.map +1 -1
  96. package/dist/discovery/search.js +28 -140
  97. package/dist/discovery/search.js.map +1 -1
  98. package/dist/electron-apps.d.ts +1 -1
  99. package/dist/electron-apps.d.ts.map +1 -1
  100. package/dist/electron-apps.js +2 -2
  101. package/dist/electron-apps.js.map +1 -1
  102. package/dist/engine/executor.d.ts +1 -1
  103. package/dist/engine/executor.js +7 -7
  104. package/dist/engine/executor.js.map +1 -1
  105. package/dist/engine/repair/remedies.js +3 -3
  106. package/dist/engine/repair/remedies.js.map +1 -1
  107. package/dist/engine/steps/desktop-ax.d.ts +4 -0
  108. package/dist/engine/steps/desktop-ax.d.ts.map +1 -1
  109. package/dist/engine/steps/desktop-ax.js +8 -0
  110. package/dist/engine/steps/desktop-ax.js.map +1 -1
  111. package/dist/engine/steps/desktop-sidecar.d.ts +1 -1
  112. package/dist/engine/steps/desktop-sidecar.js +1 -1
  113. package/dist/engine/steps/index.d.ts +1 -1
  114. package/dist/engine/steps/index.d.ts.map +1 -1
  115. package/dist/engine/steps/index.js +1 -1
  116. package/dist/engine/steps/index.js.map +1 -1
  117. package/dist/engine/steps/visual.d.ts +47 -0
  118. package/dist/engine/steps/visual.d.ts.map +1 -0
  119. package/dist/engine/steps/visual.js +66 -0
  120. package/dist/engine/steps/visual.js.map +1 -0
  121. package/dist/engine/transport/mcp-browser.d.ts +1 -1
  122. package/dist/engine/transport/mcp-browser.js +1 -1
  123. package/dist/fast-path/handlers/discovery.d.ts.map +1 -1
  124. package/dist/fast-path/handlers/discovery.js +17 -3
  125. package/dist/fast-path/handlers/discovery.js.map +1 -1
  126. package/dist/manifest-compact.txt +13 -11
  127. package/dist/manifest-search.json +1 -1
  128. package/dist/manifest.json +529 -121
  129. package/dist/mcp/handler.d.ts.map +1 -1
  130. package/dist/mcp/handler.js +14 -2
  131. package/dist/mcp/handler.js.map +1 -1
  132. package/dist/mcp/tools.d.ts.map +1 -1
  133. package/dist/mcp/tools.js +11 -3
  134. package/dist/mcp/tools.js.map +1 -1
  135. package/dist/registry.d.ts +1 -0
  136. package/dist/registry.d.ts.map +1 -1
  137. package/dist/registry.js +5 -0
  138. package/dist/registry.js.map +1 -1
  139. package/dist/transport/adapters/desktop-atspi.js +1 -1
  140. package/dist/transport/adapters/desktop-atspi.js.map +1 -1
  141. package/dist/transport/adapters/desktop-ax-background-activation-swift.d.ts +9 -0
  142. package/dist/transport/adapters/desktop-ax-background-activation-swift.d.ts.map +1 -0
  143. package/dist/transport/adapters/desktop-ax-background-activation-swift.js +99 -0
  144. package/dist/transport/adapters/desktop-ax-background-activation-swift.js.map +1 -0
  145. package/dist/transport/adapters/desktop-ax-background-click-swift.d.ts.map +1 -1
  146. package/dist/transport/adapters/desktop-ax-background-click-swift.js +10 -133
  147. package/dist/transport/adapters/desktop-ax-background-click-swift.js.map +1 -1
  148. package/dist/transport/adapters/desktop-ax-background-click.d.ts +1 -3
  149. package/dist/transport/adapters/desktop-ax-background-click.d.ts.map +1 -1
  150. package/dist/transport/adapters/desktop-ax-background-click.js +1 -69
  151. package/dist/transport/adapters/desktop-ax-background-click.js.map +1 -1
  152. package/dist/transport/adapters/desktop-ax-background-dispatch-swift.d.ts +9 -0
  153. package/dist/transport/adapters/desktop-ax-background-dispatch-swift.d.ts.map +1 -0
  154. package/dist/transport/adapters/desktop-ax-background-dispatch-swift.js +169 -0
  155. package/dist/transport/adapters/desktop-ax-background-dispatch-swift.js.map +1 -0
  156. package/dist/transport/adapters/desktop-ax-background-input-swift.d.ts +23 -0
  157. package/dist/transport/adapters/desktop-ax-background-input-swift.d.ts.map +1 -0
  158. package/dist/transport/adapters/desktop-ax-background-input-swift.js +157 -0
  159. package/dist/transport/adapters/desktop-ax-background-input-swift.js.map +1 -0
  160. package/dist/transport/adapters/desktop-ax-background-input.d.ts +13 -0
  161. package/dist/transport/adapters/desktop-ax-background-input.d.ts.map +1 -0
  162. package/dist/transport/adapters/desktop-ax-background-input.js +124 -0
  163. package/dist/transport/adapters/desktop-ax-background-input.js.map +1 -0
  164. package/dist/transport/adapters/desktop-ax-background-window-swift.d.ts +9 -0
  165. package/dist/transport/adapters/desktop-ax-background-window-swift.d.ts.map +1 -0
  166. package/dist/transport/adapters/desktop-ax-background-window-swift.js +110 -0
  167. package/dist/transport/adapters/desktop-ax-background-window-swift.js.map +1 -0
  168. package/dist/transport/adapters/desktop-ax-swift.d.ts +1 -0
  169. package/dist/transport/adapters/desktop-ax-swift.d.ts.map +1 -1
  170. package/dist/transport/adapters/desktop-ax-swift.js +1 -0
  171. package/dist/transport/adapters/desktop-ax-swift.js.map +1 -1
  172. package/dist/transport/adapters/desktop-ax.d.ts +4 -1
  173. package/dist/transport/adapters/desktop-ax.d.ts.map +1 -1
  174. package/dist/transport/adapters/desktop-ax.js +57 -6
  175. package/dist/transport/adapters/desktop-ax.js.map +1 -1
  176. package/dist/transport/adapters/desktop-uia.js +1 -1
  177. package/dist/transport/adapters/desktop-uia.js.map +1 -1
  178. package/dist/transport/adapters/visual.d.ts +114 -0
  179. package/dist/transport/adapters/visual.d.ts.map +1 -0
  180. package/dist/transport/adapters/visual.js +473 -0
  181. package/dist/transport/adapters/visual.js.map +1 -0
  182. package/dist/transport/bus.d.ts +1 -1
  183. package/dist/transport/bus.js +3 -3
  184. package/dist/transport/bus.js.map +1 -1
  185. package/dist/transport/capability.d.ts.map +1 -1
  186. package/dist/transport/capability.js +32 -30
  187. package/dist/transport/capability.js.map +1 -1
  188. package/dist/transport/cascade.js +27 -27
  189. package/dist/transport/cascade.js.map +1 -1
  190. package/dist/transport/types.d.ts +5 -5
  191. package/dist/transport/types.d.ts.map +1 -1
  192. package/dist/transport/types.js +1 -1
  193. package/dist/types/scholarly.d.ts +49 -0
  194. package/dist/types/scholarly.d.ts.map +1 -0
  195. package/dist/types/scholarly.js +16 -0
  196. package/dist/types/scholarly.js.map +1 -0
  197. package/docs/operate/compute.md +20 -7
  198. package/docs/operate/focus-behavior.md +21 -12
  199. package/docs/operate/troubleshooting.md +17 -12
  200. package/package.json +5 -11
  201. package/server.json +2 -2
  202. package/skills/unicli/SKILL.md +1 -1
  203. package/skills/unicli-claude-code/SKILL.md +1 -1
  204. package/skills/unicli-hermes/SKILL.md +1 -1
  205. package/skills/unicli-repair/references/error-codes.md +7 -7
  206. package/src/adapters/_archived/README.md +6 -6
  207. package/src/adapters/_archived/apple-music/rate-album.yaml +15 -15
  208. package/src/adapters/_archived/archive.json +2 -2
  209. package/src/adapters/acl-anthology/papers.ts +157 -0
  210. package/src/adapters/arxiv/download.yaml +1 -1
  211. package/src/adapters/arxiv/paper.yaml +1 -1
  212. package/src/adapters/arxiv/papers.ts +2 -0
  213. package/src/adapters/arxiv/search.yaml +1 -1
  214. package/src/adapters/arxiv/trending.yaml +1 -1
  215. package/src/adapters/baidu-scholar/search.ts +5 -0
  216. package/src/adapters/crossref/works.ts +209 -0
  217. package/src/adapters/cvf/papers.ts +136 -0
  218. package/src/adapters/dblp/publications.ts +4 -0
  219. package/src/adapters/figma/export-selected.yaml +16 -16
  220. package/src/adapters/google-scholar/cite.ts +1 -0
  221. package/src/adapters/google-scholar/profile.ts +5 -0
  222. package/src/adapters/google-scholar/search.ts +5 -0
  223. package/src/adapters/hf/paper.test.ts +10 -0
  224. package/src/adapters/hf/paper.ts +1 -0
  225. package/src/adapters/hf/top.yaml +1 -1
  226. package/src/adapters/huggingface-papers/daily.yaml +1 -1
  227. package/src/adapters/huggingface-papers/search.yaml +1 -1
  228. package/src/adapters/neurips/proceedings.ts +126 -0
  229. package/src/adapters/openalex/works.test.ts +40 -32
  230. package/src/adapters/openalex/works.ts +33 -0
  231. package/src/adapters/openreview/papers.ts +5 -0
  232. package/src/adapters/pmlr/proceedings.ts +167 -0
  233. package/src/adapters/pubmed/articles.ts +5 -0
  234. package/src/adapters/semantic-scholar/papers.ts +268 -0
  235. package/src/adapters/unpaywall/works.ts +138 -0
  236. package/src/adapters/zoom/toggle-mute.yaml +1 -1
  237. package/src/adapters/zotero/search.yaml +1 -1
  238. package/dist/engine/steps/cua.d.ts +0 -41
  239. package/dist/engine/steps/cua.d.ts.map +0 -1
  240. package/dist/engine/steps/cua.js +0 -59
  241. package/dist/engine/steps/cua.js.map +0 -1
  242. package/dist/transport/adapters/cua.d.ts +0 -239
  243. package/dist/transport/adapters/cua.d.ts.map +0 -1
  244. package/dist/transport/adapters/cua.js +0 -661
  245. package/dist/transport/adapters/cua.js.map +0 -1
  246. package/src/adapters/cua/bench-list.yaml +0 -28
  247. package/src/adapters/cua/bench-run.yaml +0 -40
@@ -38,7 +38,7 @@ pipeline:
38
38
  columns: [title, authors, published, id]
39
39
 
40
40
  # schema-v2 metadata — injected by `unicli migrate schema-v2`
41
- capabilities: ["http.fetch"]
41
+ capabilities: ["http.fetch", "scholar.search", "scholar.venue"]
42
42
  minimum_capability: http.fetch
43
43
  trust: public
44
44
  confidentiality: public
@@ -14,6 +14,11 @@ cli({
14
14
  { name: "limit", type: "int", default: 10 },
15
15
  ],
16
16
  columns: ["title", "authors", "source", "url"],
17
+ capabilities: [
18
+ "mcp-browser.navigate",
19
+ "mcp-browser.evaluate",
20
+ "scholar.search",
21
+ ],
17
22
  func: async (page, kwargs) => {
18
23
  const p = page as IPage;
19
24
  const limit = intArg(kwargs.limit, 10, 50);
@@ -0,0 +1,209 @@
1
+ /**
2
+ * @owner src::adapters::crossref::works
3
+ * @does Registers Crossref REST work search and DOI lookup commands for publisher metadata.
4
+ * @needs api.crossref.org REST API, optional CROSSREF_MAILTO, src/registry.ts
5
+ * @feeds src/commands/scholar.ts via scholar.search and scholar.get
6
+ * @breaks Crossref response-shape drift or rate limiting surfaces as explicit adapter errors.
7
+ * @invariants DOI lookup accepts only DOI-shaped references; output maps to ScholarlyWorkRecord.
8
+ * @side-effects HTTPS egress to api.crossref.org only
9
+ * @perf O(limit) JSON mapping
10
+ * @concurrency safe
11
+ * @test tests/unit/adapters/scholar-sources.test.ts
12
+ * @stability experimental
13
+ * @since 2026-05-19
14
+ */
15
+
16
+ import { cli, Strategy } from "../../registry.js";
17
+ import type { ScholarlyWorkRecord } from "../../types/scholarly.js";
18
+
19
+ const API = "https://api.crossref.org";
20
+
21
+ interface CrossrefPerson {
22
+ given?: unknown;
23
+ family?: unknown;
24
+ name?: unknown;
25
+ }
26
+
27
+ interface CrossrefItem {
28
+ DOI?: unknown;
29
+ title?: unknown[];
30
+ subtitle?: unknown[];
31
+ author?: CrossrefPerson[];
32
+ "container-title"?: unknown[];
33
+ issued?: { "date-parts"?: unknown[][] };
34
+ published?: { "date-parts"?: unknown[][] };
35
+ "is-referenced-by-count"?: unknown;
36
+ reference?: unknown[];
37
+ URL?: unknown;
38
+ type?: unknown;
39
+ abstract?: unknown;
40
+ }
41
+
42
+ function str(value: unknown): string {
43
+ return typeof value === "string" ? value.trim() : "";
44
+ }
45
+
46
+ function arrFirst(value: unknown): string {
47
+ return Array.isArray(value) ? str(value[0]) : str(value);
48
+ }
49
+
50
+ function num(value: unknown): number | undefined {
51
+ return typeof value === "number" && Number.isFinite(value)
52
+ ? value
53
+ : undefined;
54
+ }
55
+
56
+ function dateParts(item: CrossrefItem): unknown[] {
57
+ return (
58
+ item.issued?.["date-parts"]?.[0] ??
59
+ item.published?.["date-parts"]?.[0] ??
60
+ []
61
+ );
62
+ }
63
+
64
+ function year(item: CrossrefItem): number | undefined {
65
+ const first = dateParts(item)[0];
66
+ return typeof first === "number" && Number.isFinite(first)
67
+ ? first
68
+ : undefined;
69
+ }
70
+
71
+ function date(item: CrossrefItem): string | undefined {
72
+ const parts = dateParts(item).filter(
73
+ (part): part is number => typeof part === "number",
74
+ );
75
+ if (parts.length === 0) return undefined;
76
+ return [
77
+ String(parts[0]).padStart(4, "0"),
78
+ String(parts[1] ?? 1).padStart(2, "0"),
79
+ String(parts[2] ?? 1).padStart(2, "0"),
80
+ ].join("-");
81
+ }
82
+
83
+ function authors(value: CrossrefPerson[] | undefined): string[] | undefined {
84
+ if (!Array.isArray(value)) return undefined;
85
+ const out = value
86
+ .map(
87
+ (person) =>
88
+ str(person.name) ||
89
+ [person.given, person.family].map(str).filter(Boolean).join(" "),
90
+ )
91
+ .filter(Boolean);
92
+ return out.length > 0 ? out : undefined;
93
+ }
94
+
95
+ function bareDoi(value: unknown): string {
96
+ return str(value)
97
+ .replace(/^doi:/i, "")
98
+ .replace(/^https?:\/\/(?:dx\.)?doi\.org\//i, "");
99
+ }
100
+
101
+ export function requireCrossrefDoi(value: unknown): string {
102
+ const doi = bareDoi(value);
103
+ if (!/^10\.\S+\/\S+/.test(doi)) {
104
+ throw new Error(`crossref DOI "${String(value ?? "")}" is not recognised.`);
105
+ }
106
+ return doi;
107
+ }
108
+
109
+ function maybeMailto(params: URLSearchParams): void {
110
+ const mailto = process.env.CROSSREF_MAILTO?.trim();
111
+ if (mailto) params.set("mailto", mailto);
112
+ }
113
+
114
+ async function fetchCrossref(path: string, label: string): Promise<unknown> {
115
+ const response = await fetch(`${API}${path}`, {
116
+ headers: {
117
+ Accept: "application/json",
118
+ "User-Agent":
119
+ "unicli-crossref/1.0 (https://github.com/olo-dot-io/Uni-CLI)",
120
+ },
121
+ });
122
+ if (response.status === 404) throw new Error(`${label} returned no result.`);
123
+ if (response.status === 429) throw new Error(`${label} returned HTTP 429.`);
124
+ if (!response.ok)
125
+ throw new Error(`${label} returned HTTP ${response.status}.`);
126
+ return response.json();
127
+ }
128
+
129
+ export function mapCrossrefItem(
130
+ item: CrossrefItem,
131
+ source: string,
132
+ ): ScholarlyWorkRecord {
133
+ const doi = requireCrossrefDoi(item.DOI);
134
+ return {
135
+ id: doi,
136
+ title: arrFirst(item.title),
137
+ authors: authors(item.author),
138
+ year: year(item),
139
+ date: date(item),
140
+ venue: arrFirst(item["container-title"]) || undefined,
141
+ type: str(item.type) || undefined,
142
+ abstract: str(item.abstract).replace(/<[^>]+>/g, " ") || undefined,
143
+ doi,
144
+ cited_by_count: num(item["is-referenced-by-count"]),
145
+ references_count: Array.isArray(item.reference)
146
+ ? item.reference.length
147
+ : undefined,
148
+ source_adapter: source,
149
+ source_url: str(item.URL) || `https://doi.org/${doi}`,
150
+ retrieved_at: new Date().toISOString(),
151
+ };
152
+ }
153
+
154
+ cli({
155
+ site: "crossref",
156
+ name: "search",
157
+ description:
158
+ "Search Crossref Works by title, author, DOI, or bibliographic text",
159
+ domain: "api.crossref.org",
160
+ strategy: Strategy.PUBLIC,
161
+ args: [
162
+ { name: "query", type: "str", required: true, positional: true },
163
+ { name: "limit", type: "int", default: 20 },
164
+ ],
165
+ columns: ["id", "title", "authors", "year", "venue", "doi", "source_url"],
166
+ capabilities: ["http.fetch", "scholar.search"],
167
+ func: async (_page, kwargs) => {
168
+ const query = String(kwargs.query ?? "").trim();
169
+ if (!query) throw new Error("crossref search query cannot be empty.");
170
+ const limit = Math.min(Math.max(Number(kwargs.limit ?? 20), 1), 100);
171
+ const params = new URLSearchParams({ query, rows: String(limit) });
172
+ maybeMailto(params);
173
+ const body = (await fetchCrossref(
174
+ `/works?${params.toString()}`,
175
+ "crossref search",
176
+ )) as {
177
+ message?: { items?: CrossrefItem[] };
178
+ };
179
+ const rows = (body.message?.items ?? []).map((item) =>
180
+ mapCrossrefItem(item, "crossref"),
181
+ );
182
+ if (rows.length === 0)
183
+ throw new Error(`No Crossref works matched "${query}".`);
184
+ return rows;
185
+ },
186
+ });
187
+
188
+ cli({
189
+ site: "crossref",
190
+ name: "work",
191
+ description: "Fetch one Crossref Work by DOI",
192
+ domain: "api.crossref.org",
193
+ strategy: Strategy.PUBLIC,
194
+ args: [{ name: "doi", type: "str", required: true, positional: true }],
195
+ columns: ["id", "title", "authors", "year", "venue", "doi", "source_url"],
196
+ capabilities: ["http.fetch", "scholar.get"],
197
+ func: async (_page, kwargs) => {
198
+ const doi = requireCrossrefDoi(kwargs.doi ?? kwargs.id ?? kwargs.ref);
199
+ const params = new URLSearchParams();
200
+ maybeMailto(params);
201
+ const suffix = params.size > 0 ? `?${params.toString()}` : "";
202
+ const body = (await fetchCrossref(
203
+ `/works/${encodeURIComponent(doi)}${suffix}`,
204
+ `crossref work ${doi}`,
205
+ )) as { message?: CrossrefItem };
206
+ if (!body.message) throw new Error(`Crossref returned no work for ${doi}.`);
207
+ return [mapCrossrefItem(body.message, "crossref")];
208
+ },
209
+ });
@@ -0,0 +1,136 @@
1
+ /**
2
+ * @owner src::adapters::cvf::papers
3
+ * @does Registers CVF OpenAccess conference paper search for CVPR/ICCV/ECCV-style proceedings pages.
4
+ * @needs openaccess.thecvf.com static proceedings HTML, src/registry.ts
5
+ * @feeds src/commands/scholar.ts via scholar.search, scholar.pdf, and scholar.venue
6
+ * @breaks CVF markup drift surfaces as empty/parse errors rather than non-CVF fallbacks.
7
+ * @invariants Venue/year map to explicit CVF event pages; PDF URLs are absolutized against openaccess.thecvf.com.
8
+ * @side-effects HTTPS egress to openaccess.thecvf.com only
9
+ * @perf O(N) over one proceedings HTML page
10
+ * @concurrency safe
11
+ * @test tests/unit/adapters/scholar-sources.test.ts
12
+ * @stability experimental
13
+ * @since 2026-05-19
14
+ */
15
+
16
+ import { cli, Strategy } from "../../registry.js";
17
+ import type { ScholarlyWorkRecord } from "../../types/scholarly.js";
18
+
19
+ const ORIGIN = "https://openaccess.thecvf.com";
20
+
21
+ function decode(value: string): string {
22
+ return value
23
+ .replace(/&amp;/g, "&")
24
+ .replace(/&lt;/g, "<")
25
+ .replace(/&gt;/g, ">")
26
+ .replace(/&quot;/g, '"')
27
+ .replace(/&#39;/g, "'")
28
+ .replace(/\s+/g, " ")
29
+ .trim();
30
+ }
31
+
32
+ function absolute(path: string): string {
33
+ return /^https?:\/\//i.test(path)
34
+ ? path
35
+ : `${ORIGIN}${path.startsWith("/") ? "" : "/"}${path}`;
36
+ }
37
+
38
+ function eventId(venue: unknown, year: unknown): string {
39
+ const v = String(venue ?? "CVPR")
40
+ .trim()
41
+ .toUpperCase();
42
+ const y = String(year ?? "").trim();
43
+ if (!/^(CVPR|ICCV|ECCV|WACV)$/.test(v))
44
+ throw new Error(`unsupported CVF venue: ${v}`);
45
+ if (!/^\d{4}$/.test(y)) throw new Error(`cvf year "${y}" is not valid.`);
46
+ return `${v}${y}`;
47
+ }
48
+
49
+ export function parseCvfRows(
50
+ html: string,
51
+ event = "CVPR2024",
52
+ ): ScholarlyWorkRecord[] {
53
+ const out: ScholarlyWorkRecord[] = [];
54
+ const re =
55
+ /<dt class="ptitle">[\s\S]*?<a href="([^"]+)">([\s\S]*?)<\/a><\/dt>([\s\S]*?)(?=<dt class="ptitle">|$)/g;
56
+ let match: RegExpExecArray | null;
57
+ while ((match = re.exec(html)) !== null) {
58
+ const sourceUrl = absolute(match[1]);
59
+ const title = decode(match[2].replace(/<[^>]+>/g, " "));
60
+ const block = match[3];
61
+ const pdf = block.match(/<a href="([^"]+\.pdf)">pdf<\/a>/i)?.[1] ?? "";
62
+ const authorText = block
63
+ .replace(/\[[\s\S]*?\]/g, " ")
64
+ .replace(/<form[\s\S]*?<\/form>/g, " ")
65
+ .replace(/<[^>]+>/g, " ");
66
+ const authors = decode(authorText)
67
+ .split(",")
68
+ .map((author) => author.trim())
69
+ .filter(Boolean);
70
+ out.push({
71
+ id:
72
+ sourceUrl
73
+ .split("/")
74
+ .pop()
75
+ ?.replace(/\.html$/, "") ?? title,
76
+ title,
77
+ authors: authors.length > 0 ? authors : undefined,
78
+ year: Number(event.slice(-4)),
79
+ venue: event.replace(/\d{4}$/, ""),
80
+ pdf_url: pdf ? absolute(pdf) : undefined,
81
+ source_adapter: "cvf",
82
+ source_url: sourceUrl,
83
+ retrieved_at: new Date().toISOString(),
84
+ });
85
+ }
86
+ return out;
87
+ }
88
+
89
+ cli({
90
+ site: "cvf",
91
+ name: "search",
92
+ description: "Search CVF OpenAccess proceedings (CVPR/ICCV/ECCV/WACV)",
93
+ domain: "openaccess.thecvf.com",
94
+ strategy: Strategy.PUBLIC,
95
+ args: [
96
+ { name: "query", type: "str", required: true, positional: true },
97
+ { name: "venue", type: "str", default: "CVPR" },
98
+ { name: "year", type: "str", default: "2024" },
99
+ { name: "limit", type: "int", default: 20 },
100
+ ],
101
+ columns: ["id", "title", "authors", "year", "venue", "pdf_url", "source_url"],
102
+ capabilities: [
103
+ "http.fetch",
104
+ "scholar.search",
105
+ "scholar.venue",
106
+ "scholar.pdf",
107
+ ],
108
+ func: async (_page, kwargs) => {
109
+ const query = String(kwargs.query ?? "")
110
+ .trim()
111
+ .toLowerCase();
112
+ if (!query) throw new Error("cvf search query cannot be empty.");
113
+ const event = eventId(kwargs.venue, kwargs.year);
114
+ const response = await fetch(`${ORIGIN}/${event}?day=all`, {
115
+ headers: {
116
+ Accept: "*/*",
117
+ "User-Agent": "unicli-cvf/1.0 (https://github.com/olo-dot-io/Uni-CLI)",
118
+ },
119
+ });
120
+ if (response.status === 404)
121
+ throw new Error(`CVF ${event} returned no proceedings page.`);
122
+ if (!response.ok)
123
+ throw new Error(`CVF ${event} returned HTTP ${response.status}.`);
124
+ const limit = Math.min(Math.max(Number(kwargs.limit ?? 20), 1), 200);
125
+ const rows = parseCvfRows(await response.text(), event)
126
+ .filter((row) =>
127
+ `${row.title} ${row.authors?.join(" ") ?? ""}`
128
+ .toLowerCase()
129
+ .includes(query),
130
+ )
131
+ .slice(0, limit);
132
+ if (rows.length === 0)
133
+ throw new Error(`No CVF ${event} papers matched "${query}".`);
134
+ return rows;
135
+ },
136
+ });
@@ -332,6 +332,7 @@ cli({
332
332
  "doi",
333
333
  "url",
334
334
  ],
335
+ capabilities: ["http.fetch", "scholar.search"],
335
336
  func: async (_page, kwargs) => {
336
337
  const query = requireDblpQuery(kwargs.query);
337
338
  const limit = requireDblpLimit(kwargs.limit, 20, 100);
@@ -375,6 +376,7 @@ cli({
375
376
  "open_access_url",
376
377
  "dblp_url",
377
378
  ],
379
+ capabilities: ["http.fetch", "scholar.get", "scholar.pdf"],
378
380
  func: async (_page, kwargs) => {
379
381
  const key = requireRecordKey(kwargs.key);
380
382
  const xml = await fetchDblpXml(
@@ -406,6 +408,7 @@ cli({
406
408
  { name: "limit", type: "int", default: 20, description: "Max venues" },
407
409
  ],
408
410
  columns: ["rank", "acronym", "venue", "type", "url"],
411
+ capabilities: ["http.fetch", "scholar.venue"],
409
412
  func: async (_page, kwargs) => {
410
413
  const query = requireDblpQuery(kwargs.query);
411
414
  const limit = requireDblpLimit(kwargs.limit, 20, 100);
@@ -455,6 +458,7 @@ cli({
455
458
  "pid",
456
459
  "url",
457
460
  ],
461
+ capabilities: ["http.fetch", "scholar.author", "scholar.search"],
458
462
  func: async (_page, kwargs) => {
459
463
  const limit = requireDblpLimit(kwargs.limit, 20, 200);
460
464
  let pid = kwargs.pid ? requirePid(kwargs.pid) : "";
@@ -1,21 +1,21 @@
1
- # CUA+AX demo — export the currently-selected Figma frame.
1
+ # Visual+AX demo — export the currently-selected Figma frame.
2
2
  #
3
3
  # Composes three transports:
4
4
  # 1. desktop-ax → launch & focus the desktop Figma app
5
- # 2. cua → VLM confirms the export dialog is visible
5
+ # 2. visual → VLM confirms the export dialog is visible
6
6
  # 3. desktop-ax → click through the File > Export menu path
7
7
  #
8
- # Quarantined because the Figma desktop app and a VLM backend
9
- # (ANTHROPIC_API_KEY | TRYCUA_API_KEY | …) are both required.
8
+ # Quarantined because the Figma desktop app and a visual backend are both
9
+ # required.
10
10
  site: figma
11
11
  name: export-selected
12
- description: Export the currently-selected Figma frame via CUA + macOS AX
12
+ description: Export the currently-selected Figma frame via Visual + macOS AX
13
13
  type: browser
14
14
  strategy: ui
15
15
  domain: figma.com
16
16
  browser: false
17
17
  quarantine: true
18
- quarantineReason: Requires macOS + Figma desktop app + a CUA backend (ANTHROPIC_API_KEY etc.) (quarantined 2026-04-15)
18
+ quarantineReason: Requires macOS + Figma desktop app + a visual backend (quarantined 2026-04-15)
19
19
  args:
20
20
  format:
21
21
  type: str
@@ -30,12 +30,12 @@ pipeline:
30
30
  - ax_focus:
31
31
  app: Figma
32
32
 
33
- - cua_wait:
33
+ - visual_wait:
34
34
  ms: 500
35
35
 
36
- - cua_snapshot: {}
36
+ - visual_snapshot: {}
37
37
 
38
- - cua_ask:
38
+ - visual_ask:
39
39
  question: Is a frame currently selected on the Figma canvas?
40
40
 
41
41
  - ax_menu_select:
@@ -44,10 +44,10 @@ pipeline:
44
44
  - File
45
45
  - Export ${{ args.format }}
46
46
 
47
- - cua_wait:
47
+ - visual_wait:
48
48
  ms: 800
49
49
 
50
- - cua_assert:
50
+ - visual_assert:
51
51
  predicate: The Figma export dialog is visible with a Save button
52
52
 
53
53
  columns: [backend, ok]
@@ -55,15 +55,15 @@ columns: [backend, ok]
55
55
  # schema-v2 metadata — injected by `unicli migrate schema-v2`
56
56
  capabilities:
57
57
  [
58
- "cua.ask",
59
- "cua.assert",
60
- "cua.snapshot",
61
- "cua.wait",
58
+ "visual.ask",
59
+ "visual.assert",
60
+ "visual.snapshot",
61
+ "visual.wait",
62
62
  "desktop-ax.focus",
63
63
  "desktop-ax.launch_app",
64
64
  "desktop-ax.menu_select",
65
65
  ]
66
- minimum_capability: cua.snapshot
66
+ minimum_capability: visual.snapshot
67
67
  trust: user
68
68
  confidentiality: public
69
69
  schema_version: v2
@@ -27,6 +27,7 @@ cli({
27
27
  { name: "index", type: "int", default: 1 },
28
28
  ],
29
29
  columns: ["title", "format", "citation"],
30
+ capabilities: ["mcp-browser.navigate", "mcp-browser.evaluate", "scholar.get"],
30
31
  func: async (page, kwargs) => {
31
32
  const p = page as IPage;
32
33
  const query = str(kwargs.query).trim();
@@ -18,6 +18,11 @@ cli({
18
18
  { name: "limit", type: "int", default: 10 },
19
19
  ],
20
20
  columns: ["rank", "kind", "title", "authors", "year", "cited", "url"],
21
+ capabilities: [
22
+ "mcp-browser.navigate",
23
+ "mcp-browser.evaluate",
24
+ "scholar.author",
25
+ ],
21
26
  func: async (page, kwargs) => {
22
27
  const p = page as IPage;
23
28
  const author = str(kwargs.author).trim();
@@ -14,6 +14,11 @@ cli({
14
14
  { name: "limit", type: "int", default: 10 },
15
15
  ],
16
16
  columns: ["rank", "title", "authors", "source", "year", "cited", "url"],
17
+ capabilities: [
18
+ "mcp-browser.navigate",
19
+ "mcp-browser.evaluate",
20
+ "scholar.search",
21
+ ],
17
22
  func: async (page, kwargs) => {
18
23
  const p = page as IPage;
19
24
  const limit = intArg(kwargs.limit, 10, 20);
@@ -1,4 +1,5 @@
1
1
  import { describe, expect, it } from "vitest";
2
+ import { getAdapter } from "../../registry.js";
2
3
  import { hfEndpoint, mapHfPaperRow, requireHfPaperId } from "./paper.js";
3
4
 
4
5
  describe("hf agent-facing paper command", () => {
@@ -45,4 +46,13 @@ describe("hf agent-facing paper command", () => {
45
46
  it("rejects empty HF paper payloads", () => {
46
47
  expect(() => mapHfPaperRow({})).toThrow("no paper data");
47
48
  });
49
+
50
+ it("advertises scholarly capabilities for meta-command discovery", () => {
51
+ expect(getAdapter("hf")?.commands.paper?.capabilities).toEqual([
52
+ "http.fetch",
53
+ "scholar.get",
54
+ "scholar.pdf",
55
+ "scholar.code",
56
+ ]);
57
+ });
48
58
  });
@@ -131,6 +131,7 @@ cli({
131
131
  "aiSummary",
132
132
  "url",
133
133
  ],
134
+ capabilities: ["http.fetch", "scholar.get", "scholar.pdf", "scholar.code"],
134
135
  func: async (_page, kwargs) => {
135
136
  const id = requireHfPaperId(kwargs.id);
136
137
  return [mapHfPaperRow(await fetchHfPaper(id), hfEndpoint())];
@@ -32,7 +32,7 @@ pipeline:
32
32
  columns: [rank, id, title, upvotes, authors]
33
33
 
34
34
  # schema-v2 metadata — injected by `unicli migrate schema-v2`
35
- capabilities: ["http.fetch"]
35
+ capabilities: ["http.fetch", "scholar.search", "scholar.code"]
36
36
  minimum_capability: http.fetch
37
37
  trust: public
38
38
  confidentiality: public
@@ -21,7 +21,7 @@ pipeline:
21
21
  columns: [title, authors, upvotes, url]
22
22
 
23
23
  # schema-v2 metadata — injected by `unicli migrate schema-v2`
24
- capabilities: ["http.fetch"]
24
+ capabilities: ["http.fetch", "scholar.search", "scholar.code"]
25
25
  minimum_capability: http.fetch
26
26
  trust: public
27
27
  confidentiality: public
@@ -34,7 +34,7 @@ pipeline:
34
34
  columns: [title, authors, upvotes, published, url]
35
35
 
36
36
  # schema-v2 metadata — injected by `unicli migrate schema-v2`
37
- capabilities: ["http.fetch"]
37
+ capabilities: ["http.fetch", "scholar.search", "scholar.code"]
38
38
  minimum_capability: http.fetch
39
39
  trust: public
40
40
  confidentiality: public