@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
@@ -18,7 +18,7 @@ args:
18
18
  description: Output directory
19
19
  x-unicli-kind: path
20
20
 
21
- columns: [illust_id, page, url]
21
+ columns: [illust_id, page, url, _download]
22
22
 
23
23
  pipeline:
24
24
  - navigate: https://www.pixiv.net
@@ -40,8 +40,21 @@ pipeline:
40
40
  }));
41
41
  })()
42
42
 
43
+ - assert:
44
+ condition: "data.length > 0"
45
+ message: "No downloadable Pixiv pages found for this illustration."
46
+
47
+ - download:
48
+ url: "${{ item.url }}"
49
+ dir: "${{ args.output }}"
50
+ filename: "${{ item.illust_id }}_p${{ item.page }}.${{ item.url | ext | default('jpg') }}"
51
+ headers:
52
+ Referer: "https://www.pixiv.net/"
53
+ type: image
54
+ concurrency: 2
55
+
43
56
  # schema-v2 metadata — injected by `unicli migrate schema-v2`
44
- capabilities: ["cdp-browser.evaluate", "cdp-browser.navigate"]
57
+ capabilities: ["cdp-browser.evaluate", "cdp-browser.navigate", "http.download"]
45
58
  minimum_capability: cdp-browser.evaluate
46
59
  trust: public
47
60
  confidentiality: public
@@ -0,0 +1,63 @@
1
+ site: safebooru
2
+ name: detail
3
+ description: Get Safebooru post detail by post id
4
+ domain: safebooru.org
5
+ type: web-api
6
+ strategy: public
7
+
8
+ args:
9
+ id:
10
+ type: str
11
+ required: true
12
+ positional: true
13
+
14
+ pipeline:
15
+ - fetch:
16
+ url: "https://safebooru.org/index.php"
17
+ params:
18
+ page: dapi
19
+ s: post
20
+ q: index
21
+ json: 1
22
+ id: "${{ args.id }}"
23
+
24
+ - assert:
25
+ condition: "data.length > 0"
26
+ message: "No Safebooru post found for this id."
27
+
28
+ - map:
29
+ id: "${{ item.id }}"
30
+ rating: "${{ item.rating }}"
31
+ score: "${{ item.score }}"
32
+ owner: "${{ item.owner }}"
33
+ width: "${{ item.width }}"
34
+ height: "${{ item.height }}"
35
+ file_size: "${{ item.file_size }}"
36
+ tags: "${{ item.tags }}"
37
+ source: "${{ item.source }}"
38
+ preview_url: "${{ item.preview_url }}"
39
+ sample_url: "${{ item.sample_url }}"
40
+ file_url: "${{ item.file_url }}"
41
+ url: "${{ 'https://safebooru.org/index.php?page=post&s=view&id=' + item.id }}"
42
+
43
+ columns:
44
+ [
45
+ id,
46
+ rating,
47
+ score,
48
+ owner,
49
+ width,
50
+ height,
51
+ tags,
52
+ source,
53
+ sample_url,
54
+ file_url,
55
+ url,
56
+ ]
57
+
58
+ capabilities: ["http.fetch"]
59
+ minimum_capability: http.fetch
60
+ trust: public
61
+ confidentiality: public
62
+ quarantine: false
63
+ schema_version: v2
@@ -0,0 +1,58 @@
1
+ site: safebooru
2
+ name: download
3
+ description: Download Safebooru post image by post id
4
+ domain: safebooru.org
5
+ type: web-api
6
+ strategy: public
7
+
8
+ args:
9
+ id:
10
+ type: str
11
+ required: true
12
+ positional: true
13
+ output:
14
+ type: str
15
+ default: "./downloads/safebooru"
16
+ description: Output directory
17
+ x-unicli-kind: path
18
+ quality:
19
+ type: str
20
+ default: sample
21
+ description: sample or original
22
+
23
+ columns: [id, rating, tags, url, _download]
24
+
25
+ pipeline:
26
+ - fetch:
27
+ url: "https://safebooru.org/index.php"
28
+ params:
29
+ page: dapi
30
+ s: post
31
+ q: index
32
+ json: 1
33
+ id: "${{ args.id }}"
34
+
35
+ - assert:
36
+ condition: "data.length > 0"
37
+ message: "No Safebooru post found for this id."
38
+
39
+ - download:
40
+ url: "${{ args.quality === 'original' ? item.file_url : item.sample_url }}"
41
+ dir: "${{ args.output }}"
42
+ filename: "${{ item.id }}.${{ (args.quality === 'original' ? item.file_url : item.sample_url) | ext | default('jpg') }}"
43
+ type: image
44
+ concurrency: 1
45
+
46
+ - map:
47
+ id: "${{ item.id }}"
48
+ rating: "${{ item.rating }}"
49
+ tags: "${{ item.tags }}"
50
+ url: "${{ 'https://safebooru.org/index.php?page=post&s=view&id=' + item.id }}"
51
+ _download: "${{ item._download }}"
52
+
53
+ capabilities: ["http.fetch", "http.download"]
54
+ minimum_capability: http.fetch
55
+ trust: public
56
+ confidentiality: public
57
+ quarantine: false
58
+ schema_version: v2
@@ -0,0 +1,69 @@
1
+ site: safebooru
2
+ name: search
3
+ description: Search Safebooru illustration posts by Moebooru tag query such as blue_archive rating:safe
4
+ domain: safebooru.org
5
+ type: web-api
6
+ strategy: public
7
+
8
+ args:
9
+ tags:
10
+ type: str
11
+ required: true
12
+ positional: true
13
+ description: Moebooru tag query, for example "blue_archive rating:safe"
14
+ limit:
15
+ type: int
16
+ default: 20
17
+ page:
18
+ type: int
19
+ default: 0
20
+
21
+ pipeline:
22
+ - fetch:
23
+ url: "https://safebooru.org/index.php"
24
+ params:
25
+ page: dapi
26
+ s: post
27
+ q: index
28
+ json: 1
29
+ tags: "${{ args.tags }}"
30
+ limit: "${{ args.limit }}"
31
+ pid: "${{ args.page }}"
32
+
33
+ - map:
34
+ rank: "${{ index + 1 }}"
35
+ id: "${{ item.id }}"
36
+ rating: "${{ item.rating }}"
37
+ score: "${{ item.score }}"
38
+ owner: "${{ item.owner }}"
39
+ width: "${{ item.width }}"
40
+ height: "${{ item.height }}"
41
+ tags: "${{ item.tags }}"
42
+ source: "${{ item.source }}"
43
+ preview_url: "${{ item.preview_url }}"
44
+ sample_url: "${{ item.sample_url }}"
45
+ file_url: "${{ item.file_url }}"
46
+ url: "${{ 'https://safebooru.org/index.php?page=post&s=view&id=' + item.id }}"
47
+
48
+ columns:
49
+ [
50
+ rank,
51
+ id,
52
+ rating,
53
+ score,
54
+ owner,
55
+ width,
56
+ height,
57
+ tags,
58
+ source,
59
+ sample_url,
60
+ file_url,
61
+ url,
62
+ ]
63
+
64
+ capabilities: ["http.fetch"]
65
+ minimum_capability: http.fetch
66
+ trust: public
67
+ confidentiality: public
68
+ quarantine: false
69
+ schema_version: v2
@@ -0,0 +1,60 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { resolveCommand } from "../../registry.js";
3
+ import { mergeSafebooruTags, parseSafebooruTags } from "./web.js";
4
+
5
+ describe("safebooru tag command", () => {
6
+ it("registers tags command", () => {
7
+ expect(
8
+ Object.keys(resolveCommand("safebooru", "tags")!.adapter.commands),
9
+ ).toEqual(expect.arrayContaining(["tags"]));
10
+ });
11
+
12
+ it("parses DAPI tag XML", () => {
13
+ expect(
14
+ parseSafebooruTags(
15
+ '<tags><tag type="3" count="108328" name="blue_archive" ambiguous="false" id="1167631"/></tags>',
16
+ ),
17
+ ).toMatchObject([
18
+ {
19
+ rank: 1,
20
+ id: "1167631",
21
+ name: "blue_archive",
22
+ count: "108328",
23
+ type: "3",
24
+ ambiguous: "false",
25
+ },
26
+ ]);
27
+ });
28
+
29
+ it("keeps exact tag rows ahead of prefix matches", () => {
30
+ expect(
31
+ mergeSafebooruTags([
32
+ [
33
+ {
34
+ rank: 1,
35
+ id: "1167631",
36
+ name: "blue_archive",
37
+ count: "108328",
38
+ type: "3",
39
+ ambiguous: "false",
40
+ url: "https://safebooru.org/index.php?page=post&s=list&tags=blue_archive",
41
+ },
42
+ ],
43
+ [
44
+ {
45
+ rank: 1,
46
+ id: "33300201",
47
+ name: "blue_archive_2025_hack",
48
+ count: "22",
49
+ type: "0",
50
+ ambiguous: "false",
51
+ url: "https://safebooru.org/index.php?page=post&s=list&tags=blue_archive_2025_hack",
52
+ },
53
+ ],
54
+ ]),
55
+ ).toMatchObject([
56
+ { rank: 1, name: "blue_archive" },
57
+ { rank: 2, name: "blue_archive_2025_hack" },
58
+ ]);
59
+ });
60
+ });
@@ -0,0 +1,130 @@
1
+ /**
2
+ * @owner src/adapters/safebooru/web.ts
3
+ * @does Register Safebooru tag lookup for exact and prefix-style ACG tag discovery.
4
+ * @needs Safebooru DAPI tag XML endpoint.
5
+ * @feeds Booru tag discovery before image search/detail/download workflows.
6
+ * @breaks Safebooru DAPI XML attribute changes can block tag lookup.
7
+ */
8
+
9
+ import { USER_AGENT } from "../../constants.js";
10
+ import { cli, Strategy } from "../../registry.js";
11
+
12
+ function str(value: unknown): string {
13
+ return value === undefined || value === null ? "" : String(value);
14
+ }
15
+
16
+ function required(value: unknown, label: string): string {
17
+ const text = str(value).trim();
18
+ if (!text) throw new Error(`safebooru ${label} cannot be empty.`);
19
+ return text;
20
+ }
21
+
22
+ function requireLimit(value: unknown): number {
23
+ if (value === undefined || value === null || value === "") return 20;
24
+ const n = Number(value);
25
+ if (!Number.isInteger(n) || n < 1 || n > 100) {
26
+ throw new Error("safebooru limit must be an integer in [1, 100].");
27
+ }
28
+ return n;
29
+ }
30
+
31
+ function decodeXml(value: string): string {
32
+ return value
33
+ .replace(/&quot;/g, '"')
34
+ .replace(/&apos;/g, "'")
35
+ .replace(/&lt;/g, "<")
36
+ .replace(/&gt;/g, ">")
37
+ .replace(/&amp;/g, "&");
38
+ }
39
+
40
+ function attr(source: string, name: string): string {
41
+ const match = source.match(new RegExp(`\\b${name}="([^"]*)"`));
42
+ return match ? decodeXml(match[1]) : "";
43
+ }
44
+
45
+ export function parseSafebooruTags(xml: string): Record<string, unknown>[] {
46
+ const rows: Record<string, unknown>[] = [];
47
+ const tagRegex = /<tag\b[^>]*\/>/g;
48
+ let match: RegExpExecArray | null;
49
+ while ((match = tagRegex.exec(xml)) !== null) {
50
+ const raw = match[0];
51
+ const name = attr(raw, "name");
52
+ if (!name) continue;
53
+ rows.push({
54
+ rank: rows.length + 1,
55
+ id: attr(raw, "id"),
56
+ name,
57
+ count: attr(raw, "count"),
58
+ type: attr(raw, "type"),
59
+ ambiguous: attr(raw, "ambiguous"),
60
+ url: `https://safebooru.org/index.php?page=post&s=list&tags=${encodeURIComponent(name)}`,
61
+ });
62
+ }
63
+ return rows;
64
+ }
65
+
66
+ export function mergeSafebooruTags(
67
+ groups: Record<string, unknown>[][],
68
+ ): Record<string, unknown>[] {
69
+ const rows: Record<string, unknown>[] = [];
70
+ const seen = new Set<string>();
71
+ for (const group of groups) {
72
+ for (const row of group) {
73
+ const name = str(row.name);
74
+ if (!name || seen.has(name)) continue;
75
+ seen.add(name);
76
+ rows.push({ ...row, rank: rows.length + 1 });
77
+ }
78
+ }
79
+ return rows;
80
+ }
81
+
82
+ async function fetchTagRows(url: URL): Promise<Record<string, unknown>[]> {
83
+ const response = await fetch(url, {
84
+ headers: {
85
+ Accept: "application/xml,text/xml",
86
+ "User-Agent": USER_AGENT,
87
+ },
88
+ });
89
+ if (!response.ok)
90
+ throw new Error(`safebooru request failed with HTTP ${response.status}.`);
91
+ return parseSafebooruTags(await response.text());
92
+ }
93
+
94
+ function tagUrl(kind: "name" | "name_pattern", query: string, limit: number) {
95
+ const url = new URL("https://safebooru.org/index.php");
96
+ url.searchParams.set("page", "dapi");
97
+ url.searchParams.set("s", "tag");
98
+ url.searchParams.set("q", "index");
99
+ url.searchParams.set(kind, query);
100
+ url.searchParams.set("limit", String(limit));
101
+ return url;
102
+ }
103
+
104
+ async function searchTags(kwargs: Record<string, unknown>) {
105
+ const query = required(kwargs.query, "query");
106
+ const limit = requireLimit(kwargs.limit);
107
+ const rows = mergeSafebooruTags([
108
+ await fetchTagRows(tagUrl("name", query, 1)),
109
+ await fetchTagRows(tagUrl("name_pattern", query, limit)),
110
+ ]).slice(0, limit);
111
+ if (rows.length === 0)
112
+ throw new Error(`No Safebooru tags found for "${query}".`);
113
+ return rows;
114
+ }
115
+
116
+ cli({
117
+ site: "safebooru",
118
+ name: "tags",
119
+ description:
120
+ "Search Safebooru tags by exact tag name or ASCII tag prefix such as blue_archive",
121
+ domain: "safebooru.org",
122
+ strategy: Strategy.PUBLIC,
123
+ browser: false,
124
+ args: [
125
+ { name: "query", type: "str", required: true, positional: true },
126
+ { name: "limit", type: "int", default: 20 },
127
+ ],
128
+ columns: ["rank", "id", "name", "count", "type", "ambiguous", "url"],
129
+ func: async (_page, kwargs) => searchTags(kwargs),
130
+ });
@@ -0,0 +1,86 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { resolveCommand } from "../../registry.js";
3
+ import { mapVndbReleases, mapVndbVisualNovels } from "./web.js";
4
+
5
+ describe("vndb public commands", () => {
6
+ it("registers core visual novel discovery commands", () => {
7
+ expect(
8
+ Object.keys(resolveCommand("vndb", "search")!.adapter.commands),
9
+ ).toEqual(
10
+ expect.arrayContaining([
11
+ "search",
12
+ "vn",
13
+ "releases",
14
+ "tags",
15
+ "staff",
16
+ "producers",
17
+ "characters",
18
+ ]),
19
+ );
20
+ });
21
+
22
+ it("maps visual novel search rows with tags and developers", () => {
23
+ expect(
24
+ mapVndbVisualNovels([
25
+ {
26
+ id: "v57439",
27
+ title: "Hanabi no Sakase Kata",
28
+ alttitle: "花火の咲かせ方",
29
+ released: "2017-12-29",
30
+ languages: ["ja"],
31
+ platforms: ["win"],
32
+ rating: 70,
33
+ votecount: 10,
34
+ image: { url: "https://t.vndb.org/cv/70/112070.jpg" },
35
+ developers: [{ name: "Ultimate Manju" }],
36
+ tags: [
37
+ { name: "Comedy", rating: 1 },
38
+ { name: "ADV", rating: 2 },
39
+ ],
40
+ },
41
+ ]),
42
+ ).toEqual([
43
+ {
44
+ rank: 1,
45
+ id: "v57439",
46
+ title: "Hanabi no Sakase Kata",
47
+ alttitle: "花火の咲かせ方",
48
+ released: "2017-12-29",
49
+ languages: "ja",
50
+ platforms: "win",
51
+ rating: 70,
52
+ votecount: 10,
53
+ developers: "Ultimate Manju",
54
+ tags: "ADV, Comedy",
55
+ image: "https://t.vndb.org/cv/70/112070.jpg",
56
+ url: "https://vndb.org/v57439",
57
+ },
58
+ ]);
59
+ });
60
+
61
+ it("maps release rows", () => {
62
+ expect(
63
+ mapVndbReleases([
64
+ {
65
+ id: "r91050",
66
+ title: "U-ena -Toohanabi no Shoujo-",
67
+ released: "TBA",
68
+ platforms: ["win"],
69
+ producers: [{ name: "Hemiola Studio" }],
70
+ vns: [{ title: "U-ena -Toohanabi no Shoujo-" }],
71
+ },
72
+ ]),
73
+ ).toEqual([
74
+ {
75
+ rank: 1,
76
+ id: "r91050",
77
+ title: "U-ena -Toohanabi no Shoujo-",
78
+ released: "TBA",
79
+ platforms: "win",
80
+ producers: "Hemiola Studio",
81
+ vns: "U-ena -Toohanabi no Shoujo-",
82
+ url: "https://vndb.org/r91050",
83
+ },
84
+ ]);
85
+ });
86
+ });