@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
@@ -0,0 +1,126 @@
1
+ /**
2
+ * @owner src::adapters::neurips::proceedings
3
+ * @does Registers NeurIPS proceedings search over the official yearly paper list.
4
+ * @needs proceedings.neurips.cc static HTML, src/registry.ts
5
+ * @feeds src/commands/scholar.ts via scholar.search, scholar.pdf, and scholar.venue
6
+ * @breaks NeurIPS markup drift surfaces as empty parse output; no unrelated source fallback is used.
7
+ * @invariants Year is explicit; paper URLs are absolutized against proceedings.neurips.cc.
8
+ * @side-effects HTTPS egress to proceedings.neurips.cc 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://proceedings.neurips.cc";
20
+
21
+ function decode(value: string): string {
22
+ return value
23
+ .replace(/&/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 requireYear(value: unknown): string {
39
+ const year = String(value ?? "").trim();
40
+ if (!/^\d{4}$/.test(year))
41
+ throw new Error(`neurips year "${year}" is not valid.`);
42
+ return year;
43
+ }
44
+
45
+ export function parseNeuripsRows(
46
+ html: string,
47
+ year = "2024",
48
+ ): ScholarlyWorkRecord[] {
49
+ const out: ScholarlyWorkRecord[] = [];
50
+ const re =
51
+ /<div class="paper-content">[\s\S]*?<a title="paper title" href="([^"]+)">([\s\S]*?)<\/a>[\s\S]*?<span class="paper-authors">([\s\S]*?)<\/span>/g;
52
+ let match: RegExpExecArray | null;
53
+ while ((match = re.exec(html)) !== null) {
54
+ const sourceUrl = absolute(match[1]);
55
+ out.push({
56
+ id:
57
+ sourceUrl
58
+ .split("/")
59
+ .pop()
60
+ ?.replace(/\.html$/, "") ?? decode(match[2]),
61
+ title: decode(match[2].replace(/<[^>]+>/g, " ")),
62
+ authors: decode(match[3])
63
+ .split(",")
64
+ .map((author) => author.trim())
65
+ .filter(Boolean),
66
+ year: Number(year),
67
+ venue: "NeurIPS",
68
+ pdf_url: sourceUrl
69
+ .replace("-Abstract-", "-Paper-")
70
+ .replace(/\.html$/, ".pdf"),
71
+ source_adapter: "neurips",
72
+ source_url: sourceUrl,
73
+ retrieved_at: new Date().toISOString(),
74
+ });
75
+ }
76
+ return out;
77
+ }
78
+
79
+ cli({
80
+ site: "neurips",
81
+ name: "search",
82
+ description: "Search NeurIPS proceedings by year",
83
+ domain: "proceedings.neurips.cc",
84
+ strategy: Strategy.PUBLIC,
85
+ args: [
86
+ { name: "query", type: "str", required: true, positional: true },
87
+ { name: "year", type: "str", default: "2024" },
88
+ { name: "limit", type: "int", default: 20 },
89
+ ],
90
+ columns: ["id", "title", "authors", "year", "venue", "pdf_url", "source_url"],
91
+ capabilities: [
92
+ "http.fetch",
93
+ "scholar.search",
94
+ "scholar.venue",
95
+ "scholar.pdf",
96
+ ],
97
+ func: async (_page, kwargs) => {
98
+ const query = String(kwargs.query ?? "")
99
+ .trim()
100
+ .toLowerCase();
101
+ if (!query) throw new Error("neurips search query cannot be empty.");
102
+ const year = requireYear(kwargs.year);
103
+ const response = await fetch(`${ORIGIN}/paper_files/paper/${year}`, {
104
+ headers: {
105
+ Accept: "text/html",
106
+ "User-Agent":
107
+ "unicli-neurips/1.0 (https://github.com/olo-dot-io/Uni-CLI)",
108
+ },
109
+ });
110
+ if (response.status === 404)
111
+ throw new Error(`NeurIPS ${year} returned no proceedings page.`);
112
+ if (!response.ok)
113
+ throw new Error(`NeurIPS ${year} returned HTTP ${response.status}.`);
114
+ const limit = Math.min(Math.max(Number(kwargs.limit ?? 20), 1), 200);
115
+ const rows = parseNeuripsRows(await response.text(), year)
116
+ .filter((row) =>
117
+ `${row.title} ${row.authors?.join(" ") ?? ""}`
118
+ .toLowerCase()
119
+ .includes(query),
120
+ )
121
+ .slice(0, limit);
122
+ if (rows.length === 0)
123
+ throw new Error(`No NeurIPS ${year} papers matched "${query}".`);
124
+ return rows;
125
+ },
126
+ });
@@ -31,38 +31,46 @@ describe("openalex agent-facing commands", () => {
31
31
  });
32
32
 
33
33
  it("maps search rows", () => {
34
- expect(
35
- mapOpenAlexSearchRows(
36
- [
37
- {
38
- id: "https://openalex.org/W1234",
39
- doi: "https://doi.org/10.1/example",
40
- title: "A paper",
41
- publication_year: 2026,
42
- cited_by_count: 5,
43
- authorships: [{ author: { display_name: "Ada" } }],
44
- primary_location: { source: { display_name: "Journal" } },
45
- open_access: { is_oa: true },
46
- type: "article",
47
- },
48
- ],
49
- 20,
50
- ),
51
- ).toEqual([
52
- {
53
- rank: 1,
54
- id: "W1234",
55
- title: "A paper",
56
- year: 2026,
57
- citations: 5,
58
- firstAuthor: "Ada",
59
- venue: "Journal",
60
- openAccess: true,
61
- type: "article",
62
- doi: "10.1/example",
63
- url: "https://openalex.org/W1234",
64
- },
65
- ]);
34
+ const rows = mapOpenAlexSearchRows(
35
+ [
36
+ {
37
+ id: "https://openalex.org/W1234",
38
+ doi: "https://doi.org/10.1/example",
39
+ title: "A paper",
40
+ publication_year: 2026,
41
+ cited_by_count: 5,
42
+ authorships: [{ author: { display_name: "Ada" } }],
43
+ primary_location: { source: { display_name: "Journal" } },
44
+ open_access: { is_oa: true },
45
+ type: "article",
46
+ },
47
+ ],
48
+ 20,
49
+ );
50
+
51
+ expect(rows).toHaveLength(1);
52
+ expect(rows[0]).toMatchObject({
53
+ rank: 1,
54
+ id: "W1234",
55
+ title: "A paper",
56
+ year: 2026,
57
+ citations: 5,
58
+ firstAuthor: "Ada",
59
+ authors: ["Ada"],
60
+ venue: "Journal",
61
+ openAccess: true,
62
+ is_open_access: true,
63
+ type: "article",
64
+ doi: "10.1/example",
65
+ pdf_url: "",
66
+ openalex_id: "W1234",
67
+ source_adapter: "openalex",
68
+ source_url: "https://openalex.org/W1234",
69
+ url: "https://openalex.org/W1234",
70
+ });
71
+ expect(rows[0]?.retrieved_at).toEqual(
72
+ expect.stringMatching(/^\d{4}-\d{2}-\d{2}T/),
73
+ );
66
74
  });
67
75
 
68
76
  it("maps work detail rows", () => {
@@ -164,6 +164,14 @@ function authors(work: OpenAlexWork): string {
164
164
  : "";
165
165
  }
166
166
 
167
+ function authorList(work: OpenAlexWork): string[] {
168
+ return Array.isArray(work.authorships)
169
+ ? work.authorships
170
+ .map((item) => stringField(item.author?.display_name).trim())
171
+ .filter(Boolean)
172
+ : [];
173
+ }
174
+
167
175
  function venue(work: OpenAlexWork): string {
168
176
  return stringField(work.primary_location?.source?.display_name).trim();
169
177
  }
@@ -181,10 +189,17 @@ export function mapOpenAlexSearchRows(
181
189
  year: numberField(work.publication_year),
182
190
  citations: numberField(work.cited_by_count),
183
191
  firstAuthor: firstAuthor(work),
192
+ authors: authorList(work),
184
193
  venue: venue(work),
185
194
  openAccess: Boolean(work.open_access?.is_oa),
195
+ is_open_access: Boolean(work.open_access?.is_oa),
186
196
  type: stringField(work.type).trim(),
187
197
  doi: bareDoi(work.doi),
198
+ pdf_url: stringField(work.open_access?.oa_url).trim(),
199
+ openalex_id: id,
200
+ source_adapter: "openalex",
201
+ source_url: id ? `https://openalex.org/${id}` : "",
202
+ retrieved_at: new Date().toISOString(),
188
203
  url: id ? `https://openalex.org/${id}` : "",
189
204
  };
190
205
  });
@@ -203,15 +218,26 @@ export function mapOpenAlexWorkRow(
203
218
  date: stringField(work.publication_date).trim(),
204
219
  language: stringField(work.language).trim(),
205
220
  authors: authors(work),
221
+ author_list: authorList(work),
206
222
  venue: venue(work),
207
223
  citations: numberField(work.cited_by_count),
224
+ cited_by_count: numberField(work.cited_by_count),
208
225
  openAccess: Boolean(work.open_access?.is_oa),
226
+ is_open_access: Boolean(work.open_access?.is_oa),
209
227
  openAccessUrl: stringField(work.open_access?.oa_url).trim(),
228
+ pdf_url: stringField(work.open_access?.oa_url).trim(),
210
229
  referencedCount: Array.isArray(work.referenced_works)
211
230
  ? work.referenced_works.length
212
231
  : null,
232
+ references_count: Array.isArray(work.referenced_works)
233
+ ? work.referenced_works.length
234
+ : null,
213
235
  doi: bareDoi(work.doi),
214
236
  abstract: reconstructOpenAlexAbstract(work.abstract_inverted_index),
237
+ openalex_id: id,
238
+ source_adapter: "openalex",
239
+ source_url: `https://openalex.org/${id}`,
240
+ retrieved_at: new Date().toISOString(),
215
241
  url: `https://openalex.org/${id}`,
216
242
  };
217
243
  }
@@ -259,6 +285,7 @@ cli({
259
285
  "doi",
260
286
  "url",
261
287
  ],
288
+ capabilities: ["http.fetch", "scholar.search"],
262
289
  func: async (_page, kwargs) => {
263
290
  const query = requireOpenAlexString(kwargs.query, "query");
264
291
  const limit = requireOpenAlexLimit(kwargs.limit);
@@ -308,6 +335,12 @@ cli({
308
335
  "abstract",
309
336
  "url",
310
337
  ],
338
+ capabilities: [
339
+ "http.fetch",
340
+ "scholar.get",
341
+ "scholar.pdf",
342
+ "scholar.references",
343
+ ],
311
344
  func: async (_page, kwargs) => {
312
345
  const ref = requireOpenAlexWorkRef(kwargs.id);
313
346
  const work = (await fetchOpenAlex(
@@ -303,6 +303,7 @@ cli({
303
303
  { name: "limit", type: "int", default: 25, description: "Max results" },
304
304
  ],
305
305
  columns: ["rank", "id", "title", "authors", "venue", "pdate", "url"],
306
+ capabilities: ["http.fetch", "scholar.search", "scholar.review"],
306
307
  func: async (_page, kwargs) => {
307
308
  const query = String(kwargs.query ?? "").trim();
308
309
  if (!query) throw new Error("openreview search query cannot be empty.");
@@ -363,6 +364,7 @@ cli({
363
364
  "pdf",
364
365
  "url",
365
366
  ],
367
+ capabilities: ["http.fetch", "scholar.get", "scholar.pdf", "scholar.review"],
366
368
  func: async (_page, kwargs) => {
367
369
  const id = requireForumId(kwargs.id);
368
370
  const notes = notesFromEnvelope(
@@ -409,6 +411,7 @@ cli({
409
411
  { name: "limit", type: "int", default: 50, description: "Max submissions" },
410
412
  ],
411
413
  columns: ["rank", "id", "title", "authors", "venue", "pdate", "url"],
414
+ capabilities: ["http.fetch", "scholar.author", "scholar.search"],
412
415
  func: async (_page, kwargs) => {
413
416
  const profile = requireProfileId(kwargs.profile);
414
417
  const limit = requireOpenReviewLimit(kwargs.limit, 50, 1000);
@@ -473,6 +476,7 @@ cli({
473
476
  "pdf",
474
477
  "url",
475
478
  ],
479
+ capabilities: ["http.fetch", "scholar.venue", "scholar.search"],
476
480
  func: async (_page, kwargs) => {
477
481
  const venue = String(kwargs.venue ?? "").trim();
478
482
  if (!venue) throw new Error("openreview venue cannot be empty.");
@@ -531,6 +535,7 @@ cli({
531
535
  },
532
536
  ],
533
537
  columns: ["type", "author", "rating", "confidence", "text"],
538
+ capabilities: ["http.fetch", "scholar.review"],
534
539
  func: async (_page, kwargs) => {
535
540
  const forum = requireForumId(kwargs.forum, "forum");
536
541
  const maxLength = coerceOpenReviewInt(
@@ -0,0 +1,167 @@
1
+ /**
2
+ * @owner src::adapters::pmlr::proceedings
3
+ * @does Registers Proceedings of Machine Learning Research volume search using official citeproc.yaml metadata.
4
+ * @needs proceedings.mlr.press citeproc.yaml files, js-yaml, src/registry.ts
5
+ * @feeds src/commands/scholar.ts via scholar.search, scholar.get, scholar.pdf, and scholar.venue
6
+ * @breaks Missing volume metadata or citeproc drift surfaces as explicit adapter errors.
7
+ * @invariants Volume is explicit; rows are filtered locally from official YAML metadata, not scraped from rendered cards.
8
+ * @side-effects HTTPS egress to proceedings.mlr.press only
9
+ * @perf O(N) over one proceedings volume
10
+ * @concurrency safe
11
+ * @test tests/unit/adapters/scholar-sources.test.ts
12
+ * @stability experimental
13
+ * @since 2026-05-19
14
+ */
15
+
16
+ import yaml from "js-yaml";
17
+ import { cli, Strategy } from "../../registry.js";
18
+ import type { ScholarlyWorkRecord } from "../../types/scholarly.js";
19
+
20
+ interface PmlrEntry {
21
+ title?: unknown;
22
+ abstract?: unknown;
23
+ URL?: unknown;
24
+ PDF?: unknown;
25
+ "container-title"?: unknown;
26
+ author?: Array<{ given?: unknown; family?: unknown }>;
27
+ id?: unknown;
28
+ issued?: { "date-parts"?: unknown[] };
29
+ volume?: unknown;
30
+ }
31
+
32
+ function str(value: unknown): string {
33
+ return typeof value === "string" ? value.trim() : "";
34
+ }
35
+
36
+ function authors(value: PmlrEntry["author"]): string[] | undefined {
37
+ if (!Array.isArray(value)) return undefined;
38
+ const out = value
39
+ .map((person) =>
40
+ [person.given, person.family].map(str).filter(Boolean).join(" "),
41
+ )
42
+ .filter(Boolean);
43
+ return out.length > 0 ? out : undefined;
44
+ }
45
+
46
+ function issuedYear(entry: PmlrEntry): number | undefined {
47
+ const first = entry.issued?.["date-parts"]?.[0];
48
+ return typeof first === "number" && Number.isFinite(first)
49
+ ? first
50
+ : undefined;
51
+ }
52
+
53
+ export function parsePmlrCiteproc(text: string): PmlrEntry[] {
54
+ const parsed = yaml.load(text);
55
+ return Array.isArray(parsed) ? (parsed as PmlrEntry[]) : [];
56
+ }
57
+
58
+ export function mapPmlrEntry(
59
+ entry: PmlrEntry,
60
+ source: string,
61
+ ): ScholarlyWorkRecord {
62
+ const id = str(entry.id);
63
+ if (!id) throw new Error("PMLR entry did not include id.");
64
+ return {
65
+ id,
66
+ title: str(entry.title),
67
+ abstract: str(entry.abstract) || undefined,
68
+ authors: authors(entry.author),
69
+ year: issuedYear(entry),
70
+ venue: str(entry["container-title"]) || undefined,
71
+ type: entry.volume ? `pmlr:${String(entry.volume)}` : "pmlr",
72
+ pdf_url: str(entry.PDF) || undefined,
73
+ source_adapter: source,
74
+ source_url: str(entry.URL) || undefined,
75
+ retrieved_at: new Date().toISOString(),
76
+ };
77
+ }
78
+
79
+ function requireVolume(value: unknown): string {
80
+ const raw = String(value ?? "")
81
+ .trim()
82
+ .replace(/^v/i, "");
83
+ if (!/^\d+$/.test(raw))
84
+ throw new Error(`pmlr volume "${String(value)}" is not valid.`);
85
+ return raw;
86
+ }
87
+
88
+ async function fetchVolume(volume: string): Promise<PmlrEntry[]> {
89
+ const response = await fetch(
90
+ `https://proceedings.mlr.press/v${volume}/assets/bib/citeproc.yaml`,
91
+ {
92
+ headers: {
93
+ Accept: "application/x-yaml,text/yaml,text/plain",
94
+ "User-Agent": "unicli-pmlr/1.0 (https://github.com/olo-dot-io/Uni-CLI)",
95
+ },
96
+ },
97
+ );
98
+ if (response.status === 404)
99
+ throw new Error(`PMLR volume v${volume} returned no metadata.`);
100
+ if (!response.ok)
101
+ throw new Error(`PMLR volume v${volume} returned HTTP ${response.status}.`);
102
+ return parsePmlrCiteproc(await response.text());
103
+ }
104
+
105
+ cli({
106
+ site: "pmlr",
107
+ name: "search",
108
+ description: "Search a PMLR proceedings volume (e.g. v235 for ICML 2024)",
109
+ domain: "proceedings.mlr.press",
110
+ strategy: Strategy.PUBLIC,
111
+ args: [
112
+ { name: "query", type: "str", required: true, positional: true },
113
+ { name: "volume", type: "str", default: "235" },
114
+ { name: "limit", type: "int", default: 20 },
115
+ ],
116
+ columns: ["id", "title", "authors", "year", "venue", "pdf_url", "source_url"],
117
+ capabilities: [
118
+ "http.fetch",
119
+ "scholar.search",
120
+ "scholar.venue",
121
+ "scholar.pdf",
122
+ ],
123
+ func: async (_page, kwargs) => {
124
+ const query = String(kwargs.query ?? "")
125
+ .trim()
126
+ .toLowerCase();
127
+ if (!query) throw new Error("pmlr search query cannot be empty.");
128
+ const volume = requireVolume(kwargs.volume);
129
+ const limit = Math.min(Math.max(Number(kwargs.limit ?? 20), 1), 200);
130
+ const rows = (await fetchVolume(volume))
131
+ .map((entry) => mapPmlrEntry(entry, "pmlr"))
132
+ .filter((row) =>
133
+ `${row.title} ${row.abstract ?? ""} ${row.authors?.join(" ") ?? ""}`
134
+ .toLowerCase()
135
+ .includes(query),
136
+ )
137
+ .slice(0, limit);
138
+ if (rows.length === 0)
139
+ throw new Error(`No PMLR v${volume} papers matched "${query}".`);
140
+ return rows;
141
+ },
142
+ });
143
+
144
+ cli({
145
+ site: "pmlr",
146
+ name: "paper",
147
+ description: "Fetch a PMLR paper by id inside a proceedings volume",
148
+ domain: "proceedings.mlr.press",
149
+ strategy: Strategy.PUBLIC,
150
+ args: [
151
+ { name: "id", type: "str", required: true, positional: true },
152
+ { name: "volume", type: "str", default: "235" },
153
+ ],
154
+ columns: ["id", "title", "authors", "year", "venue", "pdf_url", "source_url"],
155
+ capabilities: ["http.fetch", "scholar.get", "scholar.pdf"],
156
+ func: async (_page, kwargs) => {
157
+ const id = String(kwargs.id ?? kwargs.ref ?? "").trim();
158
+ if (!id) throw new Error("pmlr paper id is required.");
159
+ const volume = requireVolume(kwargs.volume);
160
+ const row = (await fetchVolume(volume))
161
+ .map((entry) => mapPmlrEntry(entry, "pmlr"))
162
+ .find((entry) => entry.id === id);
163
+ if (!row)
164
+ throw new Error(`No PMLR v${volume} paper found with id "${id}".`);
165
+ return [row];
166
+ },
167
+ });
@@ -304,6 +304,7 @@ cli({
304
304
  { name: "limit", type: "int", default: 20, description: "Max results" },
305
305
  ],
306
306
  columns: SUMMARY_COLUMNS,
307
+ capabilities: ["http.fetch", "scholar.search"],
307
308
  func: async (_page, kwargs) => {
308
309
  const query = requirePubMedText(kwargs.query, "query");
309
310
  const limit = requirePubMedLimit(kwargs.limit);
@@ -345,6 +346,7 @@ cli({
345
346
  },
346
347
  ],
347
348
  columns: ["field", "value"],
349
+ capabilities: ["http.fetch", "scholar.get"],
348
350
  func: async (_page, kwargs) => {
349
351
  const pmid = requirePmid(kwargs.pmid);
350
352
  const xml = String(
@@ -371,6 +373,7 @@ cli({
371
373
  { name: "limit", type: "int", default: 20, description: "Max results" },
372
374
  ],
373
375
  columns: SUMMARY_COLUMNS,
376
+ capabilities: ["http.fetch", "scholar.author", "scholar.search"],
374
377
  func: async (_page, kwargs) => {
375
378
  const name = requirePubMedText(kwargs.name, "author");
376
379
  const limit = requirePubMedLimit(kwargs.limit);
@@ -414,6 +417,7 @@ cli({
414
417
  { name: "limit", type: "int", default: 20, description: "Max results" },
415
418
  ],
416
419
  columns: SUMMARY_COLUMNS,
420
+ capabilities: ["http.fetch", "scholar.citations", "scholar.references"],
417
421
  func: async (_page, kwargs) => {
418
422
  const pmid = requirePmid(kwargs.pmid);
419
423
  const direction = requireChoice(
@@ -459,6 +463,7 @@ cli({
459
463
  { name: "limit", type: "int", default: 20, description: "Max results" },
460
464
  ],
461
465
  columns: RELATED_COLUMNS,
466
+ capabilities: ["http.fetch", "scholar.search"],
462
467
  func: async (_page, kwargs) => {
463
468
  const pmid = requirePmid(kwargs.pmid);
464
469
  const limit = requirePubMedLimit(kwargs.limit);