@zenalexa/unicli 0.220.0 → 0.220.1

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 (182) hide show
  1. package/AGENTS.md +10 -19
  2. package/README.md +17 -11
  3. package/README.zh-CN.md +17 -11
  4. package/dist/adapters/anilist/web.d.ts +11 -0
  5. package/dist/adapters/anilist/web.d.ts.map +1 -0
  6. package/dist/adapters/anilist/web.js +284 -0
  7. package/dist/adapters/anilist/web.js.map +1 -0
  8. package/dist/adapters/bangumi/web.d.ts +14 -0
  9. package/dist/adapters/bangumi/web.d.ts.map +1 -0
  10. package/dist/adapters/bangumi/web.js +257 -0
  11. package/dist/adapters/bangumi/web.js.map +1 -0
  12. package/dist/adapters/dlsite/web.d.ts +31 -0
  13. package/dist/adapters/dlsite/web.d.ts.map +1 -0
  14. package/dist/adapters/dlsite/web.js +455 -0
  15. package/dist/adapters/dlsite/web.js.map +1 -0
  16. package/dist/adapters/ehentai/web.d.ts +66 -0
  17. package/dist/adapters/ehentai/web.d.ts.map +1 -0
  18. package/dist/adapters/ehentai/web.js +608 -0
  19. package/dist/adapters/ehentai/web.js.map +1 -0
  20. package/dist/adapters/jikan/web.d.ts +9 -0
  21. package/dist/adapters/jikan/web.d.ts.map +1 -0
  22. package/dist/adapters/jikan/web.js +154 -0
  23. package/dist/adapters/jikan/web.js.map +1 -0
  24. package/dist/adapters/kitsu/web.d.ts +9 -0
  25. package/dist/adapters/kitsu/web.d.ts.map +1 -0
  26. package/dist/adapters/kitsu/web.js +97 -0
  27. package/dist/adapters/kitsu/web.js.map +1 -0
  28. package/dist/adapters/mangadex/web.d.ts +10 -0
  29. package/dist/adapters/mangadex/web.d.ts.map +1 -0
  30. package/dist/adapters/mangadex/web.js +188 -0
  31. package/dist/adapters/mangadex/web.js.map +1 -0
  32. package/dist/adapters/moegirl/web.d.ts +23 -0
  33. package/dist/adapters/moegirl/web.d.ts.map +1 -0
  34. package/dist/adapters/moegirl/web.js +269 -0
  35. package/dist/adapters/moegirl/web.js.map +1 -0
  36. package/dist/adapters/safebooru/web.d.ts +10 -0
  37. package/dist/adapters/safebooru/web.d.ts.map +1 -0
  38. package/dist/adapters/safebooru/web.js +120 -0
  39. package/dist/adapters/safebooru/web.js.map +1 -0
  40. package/dist/adapters/vndb/web.d.ts +10 -0
  41. package/dist/adapters/vndb/web.d.ts.map +1 -0
  42. package/dist/adapters/vndb/web.js +321 -0
  43. package/dist/adapters/vndb/web.js.map +1 -0
  44. package/dist/agents/codex-pack.d.ts +62 -0
  45. package/dist/agents/codex-pack.d.ts.map +1 -0
  46. package/dist/agents/codex-pack.js +163 -0
  47. package/dist/agents/codex-pack.js.map +1 -0
  48. package/dist/commands/agents.d.ts.map +1 -1
  49. package/dist/commands/agents.js +6 -43
  50. package/dist/commands/agents.js.map +1 -1
  51. package/dist/commands/browser/adapter.d.ts.map +1 -1
  52. package/dist/commands/browser/adapter.js +17 -3
  53. package/dist/commands/browser/adapter.js.map +1 -1
  54. package/dist/commands/describe.d.ts.map +1 -1
  55. package/dist/commands/describe.js +6 -7
  56. package/dist/commands/describe.js.map +1 -1
  57. package/dist/commands/dispatch.d.ts +1 -1
  58. package/dist/commands/dispatch.d.ts.map +1 -1
  59. package/dist/commands/dispatch.js +4 -2
  60. package/dist/commands/dispatch.js.map +1 -1
  61. package/dist/commands/mcp.d.ts +1 -1
  62. package/dist/commands/mcp.d.ts.map +1 -1
  63. package/dist/commands/mcp.js +10 -5
  64. package/dist/commands/mcp.js.map +1 -1
  65. package/dist/core/command-contract-lint.d.ts +10 -0
  66. package/dist/core/command-contract-lint.d.ts.map +1 -0
  67. package/dist/core/command-contract-lint.js +41 -0
  68. package/dist/core/command-contract-lint.js.map +1 -0
  69. package/dist/core/command-contract.d.ts +100 -0
  70. package/dist/core/command-contract.d.ts.map +1 -0
  71. package/dist/core/command-contract.js +174 -0
  72. package/dist/core/command-contract.js.map +1 -0
  73. package/dist/core/index.d.ts +2 -0
  74. package/dist/core/index.d.ts.map +1 -1
  75. package/dist/core/index.js +2 -0
  76. package/dist/core/index.js.map +1 -1
  77. package/dist/discovery/aliases.d.ts +2 -2
  78. package/dist/discovery/aliases.d.ts.map +1 -1
  79. package/dist/discovery/aliases.js +464 -6
  80. package/dist/discovery/aliases.js.map +1 -1
  81. package/dist/discovery/search.d.ts.map +1 -1
  82. package/dist/discovery/search.js +147 -2
  83. package/dist/discovery/search.js.map +1 -1
  84. package/dist/engine/args.d.ts.map +1 -1
  85. package/dist/engine/args.js +18 -1
  86. package/dist/engine/args.js.map +1 -1
  87. package/dist/engine/artifact-validation.d.ts +29 -0
  88. package/dist/engine/artifact-validation.d.ts.map +1 -0
  89. package/dist/engine/artifact-validation.js +211 -0
  90. package/dist/engine/artifact-validation.js.map +1 -0
  91. package/dist/engine/browser/diagnostics.d.ts +38 -0
  92. package/dist/engine/browser/diagnostics.d.ts.map +1 -0
  93. package/dist/engine/browser/diagnostics.js +40 -0
  94. package/dist/engine/browser/diagnostics.js.map +1 -0
  95. package/dist/engine/invoke.d.ts +1 -0
  96. package/dist/engine/invoke.d.ts.map +1 -1
  97. package/dist/engine/invoke.js +1 -0
  98. package/dist/engine/invoke.js.map +1 -1
  99. package/dist/engine/kernel/errors.d.ts +11 -0
  100. package/dist/engine/kernel/errors.d.ts.map +1 -0
  101. package/dist/engine/kernel/errors.js +15 -0
  102. package/dist/engine/kernel/errors.js.map +1 -0
  103. package/dist/engine/kernel/execute.d.ts +7 -18
  104. package/dist/engine/kernel/execute.d.ts.map +1 -1
  105. package/dist/engine/kernel/execute.js +25 -410
  106. package/dist/engine/kernel/execute.js.map +1 -1
  107. package/dist/engine/kernel/stages.d.ts +44 -0
  108. package/dist/engine/kernel/stages.d.ts.map +1 -0
  109. package/dist/engine/kernel/stages.js +428 -0
  110. package/dist/engine/kernel/stages.js.map +1 -0
  111. package/dist/engine/kernel/types.d.ts +21 -1
  112. package/dist/engine/kernel/types.d.ts.map +1 -1
  113. package/dist/engine/steps/download.d.ts +1 -0
  114. package/dist/engine/steps/download.d.ts.map +1 -1
  115. package/dist/engine/steps/download.js +10 -6
  116. package/dist/engine/steps/download.js.map +1 -1
  117. package/dist/fast-path/render.js +1 -1
  118. package/dist/fast-path/render.js.map +1 -1
  119. package/dist/manifest-compact.txt +3 -3
  120. package/dist/manifest-search.json +1 -1
  121. package/dist/manifest.json +3074 -3
  122. package/dist/mcp/handler.d.ts.map +1 -1
  123. package/dist/mcp/handler.js +11 -1
  124. package/dist/mcp/handler.js.map +1 -1
  125. package/dist/mcp/server.d.ts +1 -1
  126. package/dist/mcp/server.js +1 -1
  127. package/dist/mcp/tools.d.ts.map +1 -1
  128. package/dist/mcp/tools.js +18 -10
  129. package/dist/mcp/tools.js.map +1 -1
  130. package/dist/output/error-map.d.ts.map +1 -1
  131. package/dist/output/error-map.js +1 -1
  132. package/dist/output/error-map.js.map +1 -1
  133. package/dist/registry.d.ts.map +1 -1
  134. package/dist/registry.js +2 -1
  135. package/dist/registry.js.map +1 -1
  136. package/package.json +2 -2
  137. package/server.json +3 -3
  138. package/skills/unicli/SKILL.md +1 -1
  139. package/skills/unicli-claude-code/SKILL.md +1 -1
  140. package/skills/unicli-hermes/SKILL.md +1 -1
  141. package/src/adapters/anilist/web.test.ts +93 -0
  142. package/src/adapters/anilist/web.ts +341 -0
  143. package/src/adapters/arxiv/download.yaml +53 -0
  144. package/src/adapters/bangumi/web.test.ts +109 -0
  145. package/src/adapters/bangumi/web.ts +295 -0
  146. package/src/adapters/danbooru/artists.yaml +44 -0
  147. package/src/adapters/danbooru/comments.yaml +45 -0
  148. package/src/adapters/danbooru/detail.yaml +78 -0
  149. package/src/adapters/danbooru/download.yaml +51 -0
  150. package/src/adapters/danbooru/pools.yaml +56 -0
  151. package/src/adapters/danbooru/search.yaml +69 -0
  152. package/src/adapters/danbooru/tags.yaml +42 -0
  153. package/src/adapters/danbooru/wiki.yaml +44 -0
  154. package/src/adapters/dlsite/web.test.ts +132 -0
  155. package/src/adapters/dlsite/web.ts +557 -0
  156. package/src/adapters/ehentai/web.test.ts +157 -0
  157. package/src/adapters/ehentai/web.ts +750 -0
  158. package/src/adapters/jikan/web.test.ts +50 -0
  159. package/src/adapters/jikan/web.ts +177 -0
  160. package/src/adapters/kitsu/web.test.ts +29 -0
  161. package/src/adapters/kitsu/web.ts +109 -0
  162. package/src/adapters/konachan/detail.yaml +62 -0
  163. package/src/adapters/konachan/download.yaml +55 -0
  164. package/src/adapters/konachan/search.yaml +65 -0
  165. package/src/adapters/konachan/tags.yaml +40 -0
  166. package/src/adapters/mangadex/web.test.ts +46 -0
  167. package/src/adapters/mangadex/web.ts +210 -0
  168. package/src/adapters/moegirl/web.test.ts +87 -0
  169. package/src/adapters/moegirl/web.ts +343 -0
  170. package/src/adapters/pdf/read.yaml +49 -0
  171. package/src/adapters/pixiv/download.yaml +15 -2
  172. package/src/adapters/safebooru/detail.yaml +63 -0
  173. package/src/adapters/safebooru/download.yaml +58 -0
  174. package/src/adapters/safebooru/search.yaml +69 -0
  175. package/src/adapters/safebooru/web.test.ts +60 -0
  176. package/src/adapters/safebooru/web.ts +130 -0
  177. package/src/adapters/vndb/web.test.ts +86 -0
  178. package/src/adapters/vndb/web.ts +393 -0
  179. package/src/adapters/yandere/detail.yaml +61 -0
  180. package/src/adapters/yandere/download.yaml +56 -0
  181. package/src/adapters/yandere/search.yaml +67 -0
  182. package/src/adapters/yandere/tags.yaml +41 -0
@@ -0,0 +1,341 @@
1
+ /**
2
+ * @owner src/adapters/anilist/web.ts
3
+ * @does Register AniList public GraphQL search commands for anime, manga, characters, staff, and studios.
4
+ * @needs AniList public GraphQL schema and rate-limited unauthenticated search.
5
+ * @feeds ACG entity discovery across titles, characters, creators, and studios.
6
+ * @breaks AniList GraphQL field or rate-limit changes can block search workflows.
7
+ */
8
+
9
+ import { USER_AGENT } from "../../constants.js";
10
+ import { cli, Strategy } from "../../registry.js";
11
+
12
+ const API_URL = "https://graphql.anilist.co";
13
+
14
+ function text(value: unknown, label: string): string {
15
+ const result = String(value ?? "").trim();
16
+ if (!result) throw new Error(`anilist ${label} cannot be empty.`);
17
+ return result;
18
+ }
19
+
20
+ function limit(value: unknown, fallback = 10): number {
21
+ if (value === undefined || value === null || value === "") return fallback;
22
+ const n = Number(value);
23
+ if (!Number.isInteger(n) || n < 1 || n > 50) {
24
+ throw new Error("anilist limit must be an integer in [1, 50].");
25
+ }
26
+ return n;
27
+ }
28
+
29
+ function optionalYear(value: unknown): number | undefined {
30
+ if (value === undefined || value === null || value === "") return undefined;
31
+ const n = Number(value);
32
+ if (!Number.isInteger(n) || n < 1900 || n > 2100) {
33
+ throw new Error("anilist year must be an integer in [1900, 2100].");
34
+ }
35
+ return n;
36
+ }
37
+
38
+ const MEDIA_SORTS: Record<string, string[]> = {
39
+ popular: ["POPULARITY_DESC"],
40
+ trending: ["TRENDING_DESC"],
41
+ recent: ["START_DATE_DESC"],
42
+ score: ["SCORE_DESC"],
43
+ relevance: ["SEARCH_MATCH"],
44
+ };
45
+
46
+ function mediaSort(value: unknown): string[] {
47
+ const key = String(value ?? "relevance").trim();
48
+ const sort = MEDIA_SORTS[key];
49
+ if (!sort) {
50
+ throw new Error(
51
+ `anilist sort must be one of: ${Object.keys(MEDIA_SORTS).join(", ")}.`,
52
+ );
53
+ }
54
+ return sort;
55
+ }
56
+
57
+ function str(value: unknown): string {
58
+ return value === undefined || value === null ? "" : String(value);
59
+ }
60
+
61
+ function title(value: unknown): string {
62
+ const obj =
63
+ value && typeof value === "object"
64
+ ? (value as Record<string, unknown>)
65
+ : {};
66
+ return str(obj.english) || str(obj.romaji) || str(obj.native);
67
+ }
68
+
69
+ async function postGraphql<T>(
70
+ query: string,
71
+ variables: Record<string, unknown>,
72
+ ): Promise<T> {
73
+ const response = await fetch(API_URL, {
74
+ method: "POST",
75
+ headers: {
76
+ Accept: "application/json",
77
+ "Content-Type": "application/json",
78
+ "User-Agent": USER_AGENT,
79
+ },
80
+ body: JSON.stringify({ query, variables }),
81
+ });
82
+ if (!response.ok)
83
+ throw new Error(`anilist request failed with HTTP ${response.status}.`);
84
+ const data = (await response.json()) as {
85
+ data?: T;
86
+ errors?: Array<{ message?: string }>;
87
+ };
88
+ if (data.errors?.length) {
89
+ throw new Error(
90
+ `anilist API error: ${data.errors.map((e) => e.message).join("; ")}`,
91
+ );
92
+ }
93
+ if (!data.data) throw new Error("anilist response did not include data.");
94
+ return data.data;
95
+ }
96
+
97
+ export function mapAniListMedia(rows: unknown[]): Record<string, unknown>[] {
98
+ return rows.map((row, index) => {
99
+ const item = row as Record<string, unknown>;
100
+ return {
101
+ rank: index + 1,
102
+ id: item.id,
103
+ title: title(item.title),
104
+ native: str((item.title as Record<string, unknown> | undefined)?.native),
105
+ type: str(item.type),
106
+ format: str(item.format),
107
+ status: str(item.status),
108
+ score: item.averageScore ?? null,
109
+ popularity: item.popularity ?? null,
110
+ trending: item.trending ?? null,
111
+ start_date: formatDate(item.startDate),
112
+ episodes: item.episodes ?? null,
113
+ chapters: item.chapters ?? null,
114
+ url: str(item.siteUrl),
115
+ };
116
+ });
117
+ }
118
+
119
+ export function mapAniListNamed(
120
+ rows: unknown[],
121
+ kind: string,
122
+ ): Record<string, unknown>[] {
123
+ return rows.map((row, index) => {
124
+ const item = row as Record<string, unknown>;
125
+ const name = item.name as Record<string, unknown> | undefined;
126
+ return {
127
+ rank: index + 1,
128
+ id: item.id,
129
+ kind,
130
+ name: str(name?.full ?? item.name),
131
+ native: str(name?.native),
132
+ favourites: item.favourites ?? null,
133
+ url: str(item.siteUrl),
134
+ };
135
+ });
136
+ }
137
+
138
+ export function rerankAniListNamed(rows: unknown[], query: string): unknown[] {
139
+ const needle = query.trim().toLowerCase();
140
+ if (!needle) return rows;
141
+
142
+ return [...rows].sort((left, right) => {
143
+ const leftScore = namedMatchScore(left, needle);
144
+ const rightScore = namedMatchScore(right, needle);
145
+ if (leftScore !== rightScore) return rightScore - leftScore;
146
+ return 0;
147
+ });
148
+ }
149
+
150
+ function namedMatchScore(row: unknown, needle: string): number {
151
+ const item = row as Record<string, unknown>;
152
+ const name =
153
+ item.name && typeof item.name === "object"
154
+ ? (item.name as Record<string, unknown>)
155
+ : {};
156
+ const candidates = [item.name, name.full, name.native].map((value) =>
157
+ str(value).toLowerCase(),
158
+ );
159
+ if (candidates.some((value) => value === needle)) return 4;
160
+ if (candidates.some((value) => value.includes(needle))) return 3;
161
+ if (candidates.some((value) => needle.includes(value) && value.length > 1)) {
162
+ return 2;
163
+ }
164
+ return 0;
165
+ }
166
+
167
+ function formatDate(value: unknown): string {
168
+ const obj =
169
+ value && typeof value === "object"
170
+ ? (value as Record<string, unknown>)
171
+ : {};
172
+ const year = Number(obj.year);
173
+ if (!Number.isInteger(year) || year <= 0) return "";
174
+ const month = Number(obj.month);
175
+ const day = Number(obj.day);
176
+ if (!Number.isInteger(month) || month <= 0) return String(year);
177
+ if (!Number.isInteger(day) || day <= 0) {
178
+ return [String(year).padStart(4, "0"), String(month).padStart(2, "0")].join(
179
+ "-",
180
+ );
181
+ }
182
+ return [
183
+ String(year).padStart(4, "0"),
184
+ String(month).padStart(2, "0"),
185
+ String(day).padStart(2, "0"),
186
+ ].join("-");
187
+ }
188
+
189
+ async function searchMedia(
190
+ kind: "ANIME" | "MANGA",
191
+ kwargs: Record<string, unknown>,
192
+ ) {
193
+ const query = text(kwargs.query, "query");
194
+ const requested = limit(kwargs.limit);
195
+ const perPage = Math.min(50, Math.max(10, requested * 5));
196
+ const year = optionalYear(kwargs.year);
197
+ const startDateGreater = year ? year * 10000 + 101 : undefined;
198
+ const startDateLesser = year ? year * 10000 + 1231 : undefined;
199
+ const sort = mediaSort(kwargs.sort);
200
+ const data = await postGraphql<{
201
+ Page?: { media?: unknown[] };
202
+ }>(
203
+ `query ($search: String, $perPage: Int, $type: MediaType, $startDateGreater: FuzzyDateInt, $startDateLesser: FuzzyDateInt, $sort: [MediaSort]) {
204
+ Page(page: 1, perPage: $perPage) {
205
+ media(search: $search, type: $type, startDate_greater: $startDateGreater, startDate_lesser: $startDateLesser, sort: $sort) {
206
+ id title { romaji english native } type format status averageScore popularity trending startDate { year month day } episodes chapters siteUrl
207
+ }
208
+ }
209
+ }`,
210
+ {
211
+ search: query,
212
+ perPage,
213
+ type: kind,
214
+ startDateGreater,
215
+ startDateLesser,
216
+ sort,
217
+ },
218
+ );
219
+ const rows = mapAniListMedia((data.Page?.media ?? []).slice(0, requested));
220
+ if (rows.length === 0)
221
+ throw new Error(`No AniList ${kind.toLowerCase()} found for "${query}".`);
222
+ return rows;
223
+ }
224
+
225
+ async function searchNamed(
226
+ kind: "characters" | "staff" | "studios",
227
+ kwargs: Record<string, unknown>,
228
+ ) {
229
+ const query = text(kwargs.query, "query");
230
+ const requested = limit(kwargs.limit);
231
+ const perPage = Math.min(50, Math.max(10, requested * 5));
232
+ const field =
233
+ kind === "characters"
234
+ ? "characters"
235
+ : kind === "staff"
236
+ ? "staff"
237
+ : "studios";
238
+ const fields =
239
+ kind === "studios"
240
+ ? "id name favourites siteUrl"
241
+ : "id name { full native } favourites siteUrl";
242
+ const data = await postGraphql<{
243
+ Page?: Record<string, unknown[]>;
244
+ }>(
245
+ `query ($search: String, $perPage: Int) {
246
+ Page(page: 1, perPage: $perPage) {
247
+ ${field}(search: $search) { ${fields} }
248
+ }
249
+ }`,
250
+ { search: query, perPage },
251
+ );
252
+ const rows = mapAniListNamed(
253
+ rerankAniListNamed(data.Page?.[field] ?? [], query).slice(0, requested),
254
+ kind,
255
+ );
256
+ if (rows.length === 0)
257
+ throw new Error(`No AniList ${kind} found for "${query}".`);
258
+ return rows;
259
+ }
260
+
261
+ const MEDIA_ARGS = [
262
+ { name: "query", type: "str" as const, required: true, positional: true },
263
+ { name: "limit", type: "int" as const, default: 10 },
264
+ { name: "year", type: "int" as const },
265
+ {
266
+ name: "sort",
267
+ type: "str" as const,
268
+ default: "relevance",
269
+ choices: ["relevance", "popular", "trending", "recent", "score"],
270
+ },
271
+ ];
272
+
273
+ const NAMED_ARGS = [
274
+ { name: "query", type: "str" as const, required: true, positional: true },
275
+ { name: "limit", type: "int" as const, default: 10 },
276
+ ];
277
+
278
+ const MEDIA_COLUMNS = [
279
+ "rank",
280
+ "id",
281
+ "title",
282
+ "native",
283
+ "type",
284
+ "format",
285
+ "status",
286
+ "score",
287
+ "popularity",
288
+ "trending",
289
+ "start_date",
290
+ "url",
291
+ ];
292
+
293
+ const NAMED_COLUMNS = [
294
+ "rank",
295
+ "id",
296
+ "kind",
297
+ "name",
298
+ "native",
299
+ "favourites",
300
+ "url",
301
+ ];
302
+
303
+ cli({
304
+ site: "anilist",
305
+ name: "anime",
306
+ description:
307
+ "Search AniList anime by Japanese title, native title, romaji, alias, or keyword",
308
+ domain: "anilist.co",
309
+ strategy: Strategy.PUBLIC,
310
+ browser: false,
311
+ args: MEDIA_ARGS,
312
+ columns: MEDIA_COLUMNS,
313
+ func: async (_page, kwargs) => searchMedia("ANIME", kwargs),
314
+ });
315
+
316
+ cli({
317
+ site: "anilist",
318
+ name: "manga",
319
+ description:
320
+ "Search AniList manga by Japanese title, native title, romaji, alias, or keyword",
321
+ domain: "anilist.co",
322
+ strategy: Strategy.PUBLIC,
323
+ browser: false,
324
+ args: MEDIA_ARGS,
325
+ columns: MEDIA_COLUMNS,
326
+ func: async (_page, kwargs) => searchMedia("MANGA", kwargs),
327
+ });
328
+
329
+ for (const name of ["characters", "staff", "studios"] as const) {
330
+ cli({
331
+ site: "anilist",
332
+ name,
333
+ description: `Search AniList ${name} by Japanese name, native name, romaji, or alias`,
334
+ domain: "anilist.co",
335
+ strategy: Strategy.PUBLIC,
336
+ browser: false,
337
+ args: NAMED_ARGS,
338
+ columns: NAMED_COLUMNS,
339
+ func: async (_page, kwargs) => searchNamed(name, kwargs),
340
+ });
341
+ }
@@ -0,0 +1,53 @@
1
+ site: arxiv
2
+ name: download
3
+ description: Download an arXiv paper PDF by ID
4
+ domain: export.arxiv.org
5
+ type: web-api
6
+ strategy: public
7
+
8
+ args:
9
+ id:
10
+ type: str
11
+ required: true
12
+ positional: true
13
+ description: "arXiv paper ID (e.g. 1706.03762)"
14
+ x-unicli-kind: id
15
+ output:
16
+ type: str
17
+ default: "./arxiv-downloads"
18
+ description: Output directory
19
+ x-unicli-kind: path
20
+
21
+ pipeline:
22
+ - fetch_text:
23
+ url: "https://export.arxiv.org/api/query?id_list=${{ args.id | urlencode }}"
24
+
25
+ - parse_rss:
26
+ fields:
27
+ id: id
28
+ title: title
29
+
30
+ - map:
31
+ id: "${{ item.id.replace(/^https?:\\/\\/arxiv\\.org\\/abs\\//, '').replace(/v\\d+$/, '') }}"
32
+ title: "${{ item.title.replace(/\\s+/g, ' ').trim() }}"
33
+ pdf: "${{ 'https://arxiv.org/pdf/' + item.id.replace(/^https?:\\/\\/arxiv\\.org\\/abs\\//, '').replace(/v\\d+$/, '') }}"
34
+
35
+ - assert:
36
+ condition: "data.length > 0"
37
+ message: "No arXiv paper found for the requested ID."
38
+
39
+ - download:
40
+ url: "${{ item.pdf }}"
41
+ dir: "${{ args.output }}"
42
+ filename: "${{ item.id }}.pdf"
43
+ type: document
44
+
45
+ columns: [id, title, pdf, _download]
46
+
47
+ # schema-v2 metadata — injected by `unicli migrate schema-v2`
48
+ capabilities: ["http.fetch", "http.download"]
49
+ minimum_capability: http.download
50
+ trust: public
51
+ confidentiality: public
52
+ quarantine: false
53
+ schema_version: v2
@@ -0,0 +1,109 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { resolveCommand } from "../../registry.js";
3
+ import {
4
+ bangumiSubjectSearchBody,
5
+ mapBangumiCharacters,
6
+ mapBangumiSubject,
7
+ mapBangumiSubjects,
8
+ } from "./web.js";
9
+
10
+ describe("bangumi public commands", () => {
11
+ it("registers subject commands", () => {
12
+ expect(
13
+ Object.keys(resolveCommand("bangumi", "anime")!.adapter.commands),
14
+ ).toEqual(
15
+ expect.arrayContaining([
16
+ "anime",
17
+ "book",
18
+ "game",
19
+ "subject",
20
+ "characters",
21
+ ]),
22
+ );
23
+ });
24
+
25
+ it("exposes year and sort filters on subject commands", () => {
26
+ const args = resolveCommand("bangumi", "game")!.command.adapterArgs;
27
+
28
+ expect(args).toEqual(
29
+ expect.arrayContaining([
30
+ expect.objectContaining({ name: "year", type: "int" }),
31
+ expect.objectContaining({
32
+ name: "sort",
33
+ choices: ["match", "rank", "score", "heat"],
34
+ }),
35
+ ]),
36
+ );
37
+ });
38
+
39
+ it("builds v0 subject search bodies with type, year, and sort filters", () => {
40
+ expect(
41
+ bangumiSubjectSearchBody("game", {
42
+ query: "学園アイドルマスター",
43
+ year: 2024,
44
+ sort: "rank",
45
+ }),
46
+ ).toEqual({
47
+ keyword: "学園アイドルマスター",
48
+ sort: "rank",
49
+ filter: {
50
+ type: [4],
51
+ air_date: [">=2024-01-01", "<2025-01-01"],
52
+ },
53
+ });
54
+ });
55
+
56
+ it("maps search rows", () => {
57
+ expect(
58
+ mapBangumiSubjects([
59
+ {
60
+ id: 344272,
61
+ type: 4,
62
+ name: "PARQUET",
63
+ name_cn: "",
64
+ url: "http://bgm.tv/subject/344272",
65
+ },
66
+ ]),
67
+ ).toMatchObject([{ rank: 1, id: 344272, name: "PARQUET" }]);
68
+ });
69
+
70
+ it("maps subject detail", () => {
71
+ expect(
72
+ mapBangumiSubject({
73
+ id: 344272,
74
+ name: "PARQUET",
75
+ platform: "游戏",
76
+ rating: { score: 6.7, total: 200 },
77
+ }),
78
+ ).toMatchObject({
79
+ id: 344272,
80
+ name: "PARQUET",
81
+ platform: "游戏",
82
+ score: 6.7,
83
+ });
84
+ });
85
+
86
+ it("maps character rows", () => {
87
+ expect(
88
+ mapBangumiCharacters([
89
+ {
90
+ id: 148287,
91
+ name: "花火",
92
+ gender: "female",
93
+ stat: { comments: 21, collects: 59 },
94
+ summary: "假面愚者",
95
+ },
96
+ ]),
97
+ ).toMatchObject([
98
+ {
99
+ rank: 1,
100
+ id: 148287,
101
+ name: "花火",
102
+ gender: "female",
103
+ comments: 21,
104
+ collects: 59,
105
+ url: "https://bgm.tv/character/148287",
106
+ },
107
+ ]);
108
+ });
109
+ });