@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
|
@@ -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(/&/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 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
|
-
#
|
|
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.
|
|
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
|
|
9
|
-
#
|
|
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
|
|
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
|
|
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
|
-
-
|
|
33
|
+
- visual_wait:
|
|
34
34
|
ms: 500
|
|
35
35
|
|
|
36
|
-
-
|
|
36
|
+
- visual_snapshot: {}
|
|
37
37
|
|
|
38
|
-
-
|
|
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
|
-
-
|
|
47
|
+
- visual_wait:
|
|
48
48
|
ms: 800
|
|
49
49
|
|
|
50
|
-
-
|
|
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
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
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:
|
|
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
|
});
|
package/src/adapters/hf/paper.ts
CHANGED
|
@@ -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())];
|
package/src/adapters/hf/top.yaml
CHANGED
|
@@ -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
|