@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.
- package/AGENTS.md +12 -12
- package/README.md +40 -31
- package/README.zh-CN.md +37 -31
- package/crates/unicli-atspi/src/errors.rs +2 -2
- package/crates/unicli-uia/src/errors.rs +1 -1
- package/dist/adapters/acl-anthology/papers.d.ts +16 -0
- package/dist/adapters/acl-anthology/papers.d.ts.map +1 -0
- package/dist/adapters/acl-anthology/papers.js +135 -0
- package/dist/adapters/acl-anthology/papers.js.map +1 -0
- package/dist/adapters/arxiv/papers.js +2 -0
- package/dist/adapters/arxiv/papers.js.map +1 -1
- package/dist/adapters/baidu-scholar/search.js +5 -0
- package/dist/adapters/baidu-scholar/search.js.map +1 -1
- package/dist/adapters/crossref/works.d.ts +42 -0
- package/dist/adapters/crossref/works.d.ts.map +1 -0
- package/dist/adapters/crossref/works.js +157 -0
- package/dist/adapters/crossref/works.js.map +1 -0
- package/dist/adapters/cvf/papers.d.ts +17 -0
- package/dist/adapters/cvf/papers.d.ts.map +1 -0
- package/dist/adapters/cvf/papers.js +124 -0
- package/dist/adapters/cvf/papers.js.map +1 -0
- package/dist/adapters/dblp/publications.js +4 -0
- package/dist/adapters/dblp/publications.js.map +1 -1
- package/dist/adapters/google-scholar/cite.js +1 -0
- package/dist/adapters/google-scholar/cite.js.map +1 -1
- package/dist/adapters/google-scholar/profile.js +5 -0
- package/dist/adapters/google-scholar/profile.js.map +1 -1
- package/dist/adapters/google-scholar/search.js +5 -0
- package/dist/adapters/google-scholar/search.js.map +1 -1
- package/dist/adapters/hf/paper.js +1 -0
- package/dist/adapters/hf/paper.js.map +1 -1
- package/dist/adapters/neurips/proceedings.d.ts +17 -0
- package/dist/adapters/neurips/proceedings.d.ts.map +1 -0
- package/dist/adapters/neurips/proceedings.js +112 -0
- package/dist/adapters/neurips/proceedings.js.map +1 -0
- package/dist/adapters/openalex/works.d.ts.map +1 -1
- package/dist/adapters/openalex/works.js +32 -0
- package/dist/adapters/openalex/works.js.map +1 -1
- package/dist/adapters/openreview/papers.js +5 -0
- package/dist/adapters/openreview/papers.js.map +1 -1
- package/dist/adapters/pmlr/proceedings.d.ts +35 -0
- package/dist/adapters/pmlr/proceedings.d.ts.map +1 -0
- package/dist/adapters/pmlr/proceedings.js +139 -0
- package/dist/adapters/pmlr/proceedings.js.map +1 -0
- package/dist/adapters/pubmed/articles.js +5 -0
- package/dist/adapters/pubmed/articles.js.map +1 -1
- package/dist/adapters/semantic-scholar/papers.d.ts +36 -0
- package/dist/adapters/semantic-scholar/papers.d.ts.map +1 -0
- package/dist/adapters/semantic-scholar/papers.js +214 -0
- package/dist/adapters/semantic-scholar/papers.js.map +1 -0
- package/dist/adapters/unpaywall/works.d.ts +33 -0
- package/dist/adapters/unpaywall/works.d.ts.map +1 -0
- package/dist/adapters/unpaywall/works.js +101 -0
- package/dist/adapters/unpaywall/works.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +15 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/compute.d.ts.map +1 -1
- package/dist/commands/compute.js +1 -1
- package/dist/commands/compute.js.map +1 -1
- package/dist/commands/do.d.ts +30 -0
- package/dist/commands/do.d.ts.map +1 -0
- package/dist/commands/do.js +248 -0
- package/dist/commands/do.js.map +1 -0
- package/dist/commands/doctor-compute.js +11 -7
- package/dist/commands/doctor-compute.js.map +1 -1
- package/dist/commands/extract.d.ts +34 -0
- package/dist/commands/extract.d.ts.map +1 -0
- package/dist/commands/extract.js +316 -0
- package/dist/commands/extract.js.map +1 -0
- package/dist/commands/lint.js +3 -3
- package/dist/commands/lint.js.map +1 -1
- package/dist/commands/migrate-schema.d.ts.map +1 -1
- package/dist/commands/migrate-schema.js +26 -22
- package/dist/commands/migrate-schema.js.map +1 -1
- package/dist/commands/scholar.d.ts +33 -0
- package/dist/commands/scholar.d.ts.map +1 -0
- package/dist/commands/scholar.js +494 -0
- package/dist/commands/scholar.js.map +1 -0
- package/dist/commands/search.d.ts.map +1 -1
- package/dist/commands/search.js +2 -5
- package/dist/commands/search.js.map +1 -1
- package/dist/discovery/aliases.d.ts +2 -2
- package/dist/discovery/aliases.d.ts.map +1 -1
- package/dist/discovery/aliases.js +182 -11
- package/dist/discovery/aliases.js.map +1 -1
- package/dist/discovery/intents.d.ts +10 -0
- package/dist/discovery/intents.d.ts.map +1 -0
- package/dist/discovery/intents.js +255 -0
- package/dist/discovery/intents.js.map +1 -0
- package/dist/discovery/loader.d.ts.map +1 -1
- package/dist/discovery/loader.js +47 -2
- package/dist/discovery/loader.js.map +1 -1
- package/dist/discovery/search.d.ts +4 -1
- package/dist/discovery/search.d.ts.map +1 -1
- package/dist/discovery/search.js +28 -140
- package/dist/discovery/search.js.map +1 -1
- package/dist/electron-apps.d.ts +1 -1
- package/dist/electron-apps.d.ts.map +1 -1
- package/dist/electron-apps.js +2 -2
- package/dist/electron-apps.js.map +1 -1
- package/dist/engine/executor.d.ts +1 -1
- package/dist/engine/executor.js +7 -7
- package/dist/engine/executor.js.map +1 -1
- package/dist/engine/repair/remedies.js +3 -3
- package/dist/engine/repair/remedies.js.map +1 -1
- package/dist/engine/steps/desktop-ax.d.ts +4 -0
- package/dist/engine/steps/desktop-ax.d.ts.map +1 -1
- package/dist/engine/steps/desktop-ax.js +8 -0
- package/dist/engine/steps/desktop-ax.js.map +1 -1
- package/dist/engine/steps/desktop-sidecar.d.ts +1 -1
- package/dist/engine/steps/desktop-sidecar.js +1 -1
- package/dist/engine/steps/index.d.ts +1 -1
- package/dist/engine/steps/index.d.ts.map +1 -1
- package/dist/engine/steps/index.js +1 -1
- package/dist/engine/steps/index.js.map +1 -1
- package/dist/engine/steps/visual.d.ts +47 -0
- package/dist/engine/steps/visual.d.ts.map +1 -0
- package/dist/engine/steps/visual.js +66 -0
- package/dist/engine/steps/visual.js.map +1 -0
- package/dist/engine/transport/mcp-browser.d.ts +1 -1
- package/dist/engine/transport/mcp-browser.js +1 -1
- package/dist/fast-path/handlers/discovery.d.ts.map +1 -1
- package/dist/fast-path/handlers/discovery.js +17 -3
- package/dist/fast-path/handlers/discovery.js.map +1 -1
- package/dist/manifest-compact.txt +13 -11
- package/dist/manifest-search.json +1 -1
- package/dist/manifest.json +529 -121
- package/dist/mcp/handler.d.ts.map +1 -1
- package/dist/mcp/handler.js +14 -2
- package/dist/mcp/handler.js.map +1 -1
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +11 -3
- package/dist/mcp/tools.js.map +1 -1
- package/dist/registry.d.ts +1 -0
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +5 -0
- package/dist/registry.js.map +1 -1
- package/dist/transport/adapters/desktop-atspi.js +1 -1
- package/dist/transport/adapters/desktop-atspi.js.map +1 -1
- package/dist/transport/adapters/desktop-ax-background-activation-swift.d.ts +9 -0
- package/dist/transport/adapters/desktop-ax-background-activation-swift.d.ts.map +1 -0
- package/dist/transport/adapters/desktop-ax-background-activation-swift.js +99 -0
- package/dist/transport/adapters/desktop-ax-background-activation-swift.js.map +1 -0
- package/dist/transport/adapters/desktop-ax-background-click-swift.d.ts.map +1 -1
- package/dist/transport/adapters/desktop-ax-background-click-swift.js +10 -133
- package/dist/transport/adapters/desktop-ax-background-click-swift.js.map +1 -1
- package/dist/transport/adapters/desktop-ax-background-click.d.ts +1 -3
- package/dist/transport/adapters/desktop-ax-background-click.d.ts.map +1 -1
- package/dist/transport/adapters/desktop-ax-background-click.js +1 -69
- package/dist/transport/adapters/desktop-ax-background-click.js.map +1 -1
- package/dist/transport/adapters/desktop-ax-background-dispatch-swift.d.ts +9 -0
- package/dist/transport/adapters/desktop-ax-background-dispatch-swift.d.ts.map +1 -0
- package/dist/transport/adapters/desktop-ax-background-dispatch-swift.js +169 -0
- package/dist/transport/adapters/desktop-ax-background-dispatch-swift.js.map +1 -0
- package/dist/transport/adapters/desktop-ax-background-input-swift.d.ts +23 -0
- package/dist/transport/adapters/desktop-ax-background-input-swift.d.ts.map +1 -0
- package/dist/transport/adapters/desktop-ax-background-input-swift.js +157 -0
- package/dist/transport/adapters/desktop-ax-background-input-swift.js.map +1 -0
- package/dist/transport/adapters/desktop-ax-background-input.d.ts +13 -0
- package/dist/transport/adapters/desktop-ax-background-input.d.ts.map +1 -0
- package/dist/transport/adapters/desktop-ax-background-input.js +124 -0
- package/dist/transport/adapters/desktop-ax-background-input.js.map +1 -0
- package/dist/transport/adapters/desktop-ax-background-window-swift.d.ts +9 -0
- package/dist/transport/adapters/desktop-ax-background-window-swift.d.ts.map +1 -0
- package/dist/transport/adapters/desktop-ax-background-window-swift.js +110 -0
- package/dist/transport/adapters/desktop-ax-background-window-swift.js.map +1 -0
- package/dist/transport/adapters/desktop-ax-swift.d.ts +1 -0
- package/dist/transport/adapters/desktop-ax-swift.d.ts.map +1 -1
- package/dist/transport/adapters/desktop-ax-swift.js +1 -0
- package/dist/transport/adapters/desktop-ax-swift.js.map +1 -1
- package/dist/transport/adapters/desktop-ax.d.ts +4 -1
- package/dist/transport/adapters/desktop-ax.d.ts.map +1 -1
- package/dist/transport/adapters/desktop-ax.js +57 -6
- package/dist/transport/adapters/desktop-ax.js.map +1 -1
- package/dist/transport/adapters/desktop-uia.js +1 -1
- package/dist/transport/adapters/desktop-uia.js.map +1 -1
- package/dist/transport/adapters/visual.d.ts +114 -0
- package/dist/transport/adapters/visual.d.ts.map +1 -0
- package/dist/transport/adapters/visual.js +473 -0
- package/dist/transport/adapters/visual.js.map +1 -0
- package/dist/transport/bus.d.ts +1 -1
- package/dist/transport/bus.js +3 -3
- package/dist/transport/bus.js.map +1 -1
- package/dist/transport/capability.d.ts.map +1 -1
- package/dist/transport/capability.js +32 -30
- package/dist/transport/capability.js.map +1 -1
- package/dist/transport/cascade.js +27 -27
- package/dist/transport/cascade.js.map +1 -1
- package/dist/transport/types.d.ts +5 -5
- package/dist/transport/types.d.ts.map +1 -1
- package/dist/transport/types.js +1 -1
- package/dist/types/scholarly.d.ts +49 -0
- package/dist/types/scholarly.d.ts.map +1 -0
- package/dist/types/scholarly.js +16 -0
- package/dist/types/scholarly.js.map +1 -0
- package/docs/operate/compute.md +20 -7
- package/docs/operate/focus-behavior.md +21 -12
- package/docs/operate/troubleshooting.md +17 -12
- package/package.json +5 -11
- package/server.json +2 -2
- package/skills/unicli/SKILL.md +1 -1
- package/skills/unicli-claude-code/SKILL.md +1 -1
- package/skills/unicli-hermes/SKILL.md +1 -1
- package/skills/unicli-repair/references/error-codes.md +7 -7
- package/src/adapters/_archived/README.md +6 -6
- package/src/adapters/_archived/apple-music/rate-album.yaml +15 -15
- package/src/adapters/_archived/archive.json +2 -2
- package/src/adapters/acl-anthology/papers.ts +157 -0
- package/src/adapters/arxiv/download.yaml +1 -1
- package/src/adapters/arxiv/paper.yaml +1 -1
- package/src/adapters/arxiv/papers.ts +2 -0
- package/src/adapters/arxiv/search.yaml +1 -1
- package/src/adapters/arxiv/trending.yaml +1 -1
- package/src/adapters/baidu-scholar/search.ts +5 -0
- package/src/adapters/crossref/works.ts +209 -0
- package/src/adapters/cvf/papers.ts +136 -0
- package/src/adapters/dblp/publications.ts +4 -0
- package/src/adapters/figma/export-selected.yaml +16 -16
- package/src/adapters/google-scholar/cite.ts +1 -0
- package/src/adapters/google-scholar/profile.ts +5 -0
- package/src/adapters/google-scholar/search.ts +5 -0
- package/src/adapters/hf/paper.test.ts +10 -0
- package/src/adapters/hf/paper.ts +1 -0
- package/src/adapters/hf/top.yaml +1 -1
- package/src/adapters/huggingface-papers/daily.yaml +1 -1
- package/src/adapters/huggingface-papers/search.yaml +1 -1
- package/src/adapters/neurips/proceedings.ts +126 -0
- package/src/adapters/openalex/works.test.ts +40 -32
- package/src/adapters/openalex/works.ts +33 -0
- package/src/adapters/openreview/papers.ts +5 -0
- package/src/adapters/pmlr/proceedings.ts +167 -0
- package/src/adapters/pubmed/articles.ts +5 -0
- package/src/adapters/semantic-scholar/papers.ts +268 -0
- package/src/adapters/unpaywall/works.ts +138 -0
- package/src/adapters/zoom/toggle-mute.yaml +1 -1
- package/src/adapters/zotero/search.yaml +1 -1
- package/dist/engine/steps/cua.d.ts +0 -41
- package/dist/engine/steps/cua.d.ts.map +0 -1
- package/dist/engine/steps/cua.js +0 -59
- package/dist/engine/steps/cua.js.map +0 -1
- package/dist/transport/adapters/cua.d.ts +0 -239
- package/dist/transport/adapters/cua.d.ts.map +0 -1
- package/dist/transport/adapters/cua.js +0 -661
- package/dist/transport/adapters/cua.js.map +0 -1
- package/src/adapters/cua/bench-list.yaml +0 -28
- 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(/</g, "<")
|
|
25
|
+
.replace(/>/g, ">")
|
|
26
|
+
.replace(/"/g, '"')
|
|
27
|
+
.replace(/'/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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
).
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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);
|