@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,50 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { resolveCommand } from "../../registry.js";
3
+ import { mapJikanRows } from "./web.js";
4
+
5
+ describe("jikan public commands", () => {
6
+ it("registers MAL search surfaces", () => {
7
+ expect(
8
+ Object.keys(resolveCommand("jikan", "anime")!.adapter.commands),
9
+ ).toEqual(
10
+ expect.arrayContaining(["anime", "manga", "characters", "people"]),
11
+ );
12
+ });
13
+
14
+ it("exposes year and sort controls for 2024-2026 media lookup", () => {
15
+ expect(resolveCommand("jikan", "anime")!.command.adapterArgs).toEqual(
16
+ expect.arrayContaining([
17
+ expect.objectContaining({ name: "year", type: "int" }),
18
+ expect.objectContaining({
19
+ name: "sort",
20
+ choices: expect.arrayContaining(["score", "popularity", "recent"]),
21
+ }),
22
+ ]),
23
+ );
24
+ });
25
+
26
+ it("maps rows", () => {
27
+ expect(
28
+ mapJikanRows(
29
+ [
30
+ {
31
+ mal_id: 141632,
32
+ name: "Hanabi",
33
+ name_kanji: "花火",
34
+ favorites: 10,
35
+ url: "https://myanimelist.net/character/141632/Hanabi",
36
+ },
37
+ ],
38
+ "characters",
39
+ ),
40
+ ).toMatchObject([
41
+ {
42
+ rank: 1,
43
+ id: 141632,
44
+ kind: "characters",
45
+ title: "Hanabi",
46
+ title_japanese: "花火",
47
+ },
48
+ ]);
49
+ });
50
+ });
@@ -0,0 +1,177 @@
1
+ /**
2
+ * @owner src/adapters/jikan/web.ts
3
+ * @does Register Jikan public MyAnimeList search commands for anime, manga, characters, and people.
4
+ * @needs Jikan v4 public REST API and MAL entity URL formats.
5
+ * @feeds ACG title, character, and creator discovery.
6
+ * @breaks Jikan throttling or response field changes can block MyAnimeList-backed lookup.
7
+ */
8
+
9
+ import { USER_AGENT } from "../../constants.js";
10
+ import { cli, Strategy } from "../../registry.js";
11
+
12
+ const API = "https://api.jikan.moe/v4";
13
+
14
+ function str(value: unknown): string {
15
+ return value === undefined || value === null ? "" : String(value);
16
+ }
17
+
18
+ function required(value: unknown, label: string): string {
19
+ const text = str(value).trim();
20
+ if (!text) throw new Error(`jikan ${label} cannot be empty.`);
21
+ return text;
22
+ }
23
+
24
+ function requireLimit(value: unknown): number {
25
+ if (value === undefined || value === null || value === "") return 10;
26
+ const n = Number(value);
27
+ if (!Number.isInteger(n) || n < 1 || n > 25) {
28
+ throw new Error("jikan limit must be an integer in [1, 25].");
29
+ }
30
+ return n;
31
+ }
32
+
33
+ function optionalYear(value: unknown): number | undefined {
34
+ if (value === undefined || value === null || value === "") return undefined;
35
+ const n = Number(value);
36
+ if (!Number.isInteger(n) || n < 1900 || n > 2100) {
37
+ throw new Error("jikan year must be an integer in [1900, 2100].");
38
+ }
39
+ return n;
40
+ }
41
+
42
+ const SORTS: Record<string, { orderBy: string; direction: string }> = {
43
+ score: { orderBy: "score", direction: "desc" },
44
+ popularity: { orderBy: "popularity", direction: "asc" },
45
+ recent: { orderBy: "start_date", direction: "desc" },
46
+ relevance: { orderBy: "", direction: "" },
47
+ };
48
+
49
+ function sortSpec(value: unknown): { orderBy: string; direction: string } {
50
+ const key = String(value ?? "relevance").trim();
51
+ const spec = SORTS[key];
52
+ if (!spec) {
53
+ throw new Error(
54
+ `jikan sort must be one of: ${Object.keys(SORTS).join(", ")}.`,
55
+ );
56
+ }
57
+ return spec;
58
+ }
59
+
60
+ async function fetchJikan(
61
+ path: string,
62
+ query: string,
63
+ limit: number,
64
+ kwargs: Record<string, unknown>,
65
+ ): Promise<unknown[]> {
66
+ const url = new URL(`${API}/${path}`);
67
+ url.searchParams.set("q", query);
68
+ url.searchParams.set("limit", String(limit));
69
+ if (path === "anime" || path === "manga") {
70
+ const year = optionalYear(kwargs.year);
71
+ if (year) {
72
+ url.searchParams.set("start_date", `${year}-01-01`);
73
+ url.searchParams.set("end_date", `${year}-12-31`);
74
+ }
75
+ const sort = sortSpec(kwargs.sort);
76
+ if (sort.orderBy) {
77
+ url.searchParams.set("order_by", sort.orderBy);
78
+ url.searchParams.set("sort", sort.direction);
79
+ }
80
+ }
81
+ const response = await fetch(url, {
82
+ headers: { Accept: "application/json", "User-Agent": USER_AGENT },
83
+ });
84
+ if (!response.ok)
85
+ throw new Error(`jikan request failed with HTTP ${response.status}.`);
86
+ const data = (await response.json()) as { data?: unknown[] };
87
+ return data.data ?? [];
88
+ }
89
+
90
+ export function mapJikanRows(
91
+ rows: unknown[],
92
+ kind: string,
93
+ ): Record<string, unknown>[] {
94
+ return rows.map((row, index) => {
95
+ const item = row as Record<string, unknown>;
96
+ const images = item.images as { jpg?: { image_url?: string } } | undefined;
97
+ return {
98
+ rank: index + 1,
99
+ id: item.mal_id ?? null,
100
+ kind,
101
+ title: str(item.title ?? item.name),
102
+ title_japanese: str(item.title_japanese ?? item.name_kanji),
103
+ type: str(item.type),
104
+ score: item.score ?? null,
105
+ members: item.members ?? item.favorites ?? null,
106
+ url: str(item.url),
107
+ image: str(images?.jpg?.image_url),
108
+ };
109
+ });
110
+ }
111
+
112
+ async function search(
113
+ kind: "anime" | "manga" | "characters" | "people",
114
+ kwargs: Record<string, unknown>,
115
+ ) {
116
+ const query = required(kwargs.query, "query");
117
+ const cap = requireLimit(kwargs.limit);
118
+ const rows = mapJikanRows(await fetchJikan(kind, query, cap, kwargs), kind);
119
+ if (rows.length === 0)
120
+ throw new Error(`No Jikan ${kind} found for "${query}".`);
121
+ return rows;
122
+ }
123
+
124
+ const ENTITY_ARGS = [
125
+ { name: "query", type: "str" as const, required: true, positional: true },
126
+ { name: "limit", type: "int" as const, default: 10 },
127
+ ];
128
+ const MEDIA_ARGS = [
129
+ { name: "query", type: "str" as const, required: true, positional: true },
130
+ { name: "limit", type: "int" as const, default: 10 },
131
+ { name: "year", type: "int" as const },
132
+ {
133
+ name: "sort",
134
+ type: "str" as const,
135
+ default: "relevance",
136
+ choices: ["relevance", "score", "popularity", "recent"],
137
+ },
138
+ ];
139
+ const COLUMNS = [
140
+ "rank",
141
+ "id",
142
+ "kind",
143
+ "title",
144
+ "title_japanese",
145
+ "type",
146
+ "score",
147
+ "members",
148
+ "url",
149
+ ];
150
+
151
+ for (const name of ["anime", "manga"] as const) {
152
+ cli({
153
+ site: "jikan",
154
+ name,
155
+ description: `Search MyAnimeList ${name} through Jikan by Japanese name, romaji, or alias`,
156
+ domain: "api.jikan.moe",
157
+ strategy: Strategy.PUBLIC,
158
+ browser: false,
159
+ args: MEDIA_ARGS,
160
+ columns: COLUMNS,
161
+ func: async (_page, kwargs) => search(name, kwargs),
162
+ });
163
+ }
164
+
165
+ for (const name of ["characters", "people"] as const) {
166
+ cli({
167
+ site: "jikan",
168
+ name,
169
+ description: `Search MyAnimeList ${name} through Jikan by Japanese name, romaji, or alias`,
170
+ domain: "api.jikan.moe",
171
+ strategy: Strategy.PUBLIC,
172
+ browser: false,
173
+ args: ENTITY_ARGS,
174
+ columns: COLUMNS,
175
+ func: async (_page, kwargs) => search(name, kwargs),
176
+ });
177
+ }
@@ -0,0 +1,29 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { resolveCommand } from "../../registry.js";
3
+ import { mapKitsuMedia } from "./web.js";
4
+
5
+ describe("kitsu public commands", () => {
6
+ it("registers anime and manga commands", () => {
7
+ expect(
8
+ Object.keys(resolveCommand("kitsu", "anime")!.adapter.commands),
9
+ ).toEqual(expect.arrayContaining(["anime", "manga"]));
10
+ });
11
+
12
+ it("maps media rows", () => {
13
+ expect(
14
+ mapKitsuMedia([
15
+ {
16
+ id: "12794",
17
+ type: "anime",
18
+ attributes: {
19
+ canonicalTitle: "Fireworks",
20
+ slug: "uchiage-hanabi",
21
+ subtype: "movie",
22
+ },
23
+ },
24
+ ]),
25
+ ).toMatchObject([
26
+ { rank: 1, id: "12794", title: "Fireworks", subtype: "movie" },
27
+ ]);
28
+ });
29
+ });
@@ -0,0 +1,109 @@
1
+ /**
2
+ * @owner src/adapters/kitsu/web.ts
3
+ * @does Register Kitsu public anime and manga search commands.
4
+ * @needs Kitsu JSON:API text filters and public media pages.
5
+ * @feeds ACG title discovery with community rating and subtype metadata.
6
+ * @breaks Kitsu JSON:API field changes or service availability can block lookup.
7
+ */
8
+
9
+ import { USER_AGENT } from "../../constants.js";
10
+ import { cli, Strategy } from "../../registry.js";
11
+
12
+ const API = "https://kitsu.io/api/edge";
13
+
14
+ function str(value: unknown): string {
15
+ return value === undefined || value === null ? "" : String(value);
16
+ }
17
+
18
+ function required(value: unknown): string {
19
+ const text = str(value).trim();
20
+ if (!text) throw new Error("kitsu query cannot be empty.");
21
+ return text;
22
+ }
23
+
24
+ function requireLimit(value: unknown): number {
25
+ if (value === undefined || value === null || value === "") return 10;
26
+ const n = Number(value);
27
+ if (!Number.isInteger(n) || n < 1 || n > 20) {
28
+ throw new Error("kitsu limit must be an integer in [1, 20].");
29
+ }
30
+ return n;
31
+ }
32
+
33
+ export function mapKitsuMedia(rows: unknown[]): Record<string, unknown>[] {
34
+ return rows.map((row, index) => {
35
+ const item = row as Record<string, unknown>;
36
+ const attrs =
37
+ (item.attributes as Record<string, unknown> | undefined) ?? {};
38
+ const titles = (attrs.titles as Record<string, unknown> | undefined) ?? {};
39
+ return {
40
+ rank: index + 1,
41
+ id: item.id,
42
+ type: item.type,
43
+ title: str(
44
+ attrs.canonicalTitle || titles.en || titles.en_jp || titles.ja_jp,
45
+ ),
46
+ subtype: str(attrs.subtype),
47
+ status: str(attrs.status),
48
+ start_date: str(attrs.startDate),
49
+ average_rating: str(attrs.averageRating),
50
+ popularity_rank: attrs.popularityRank ?? null,
51
+ synopsis: str(attrs.synopsis).slice(0, 500),
52
+ url: `https://kitsu.io/${item.type}/${attrs.slug || item.id}`,
53
+ };
54
+ });
55
+ }
56
+
57
+ async function search(
58
+ kind: "anime" | "manga",
59
+ kwargs: Record<string, unknown>,
60
+ ) {
61
+ const query = required(kwargs.query);
62
+ const cap = requireLimit(kwargs.limit);
63
+ const url = new URL(`${API}/${kind}`);
64
+ url.searchParams.set("filter[text]", query);
65
+ url.searchParams.set("page[limit]", String(cap));
66
+ const response = await fetch(url, {
67
+ headers: {
68
+ Accept: "application/vnd.api+json",
69
+ "User-Agent": USER_AGENT,
70
+ },
71
+ });
72
+ if (!response.ok)
73
+ throw new Error(`kitsu request failed with HTTP ${response.status}.`);
74
+ const data = (await response.json()) as { data?: unknown[] };
75
+ const rows = mapKitsuMedia(data.data ?? []);
76
+ if (rows.length === 0)
77
+ throw new Error(`No Kitsu ${kind} found for "${query}".`);
78
+ return rows;
79
+ }
80
+
81
+ const ARGS = [
82
+ { name: "query", type: "str" as const, required: true, positional: true },
83
+ { name: "limit", type: "int" as const, default: 10 },
84
+ ];
85
+ const COLUMNS = [
86
+ "rank",
87
+ "id",
88
+ "type",
89
+ "title",
90
+ "subtype",
91
+ "status",
92
+ "start_date",
93
+ "average_rating",
94
+ "url",
95
+ ];
96
+
97
+ for (const name of ["anime", "manga"] as const) {
98
+ cli({
99
+ site: "kitsu",
100
+ name,
101
+ description: `Search Kitsu ${name} by Japanese title, romaji, alias, or keyword`,
102
+ domain: "kitsu.io",
103
+ strategy: Strategy.PUBLIC,
104
+ browser: false,
105
+ args: ARGS,
106
+ columns: COLUMNS,
107
+ func: async (_page, kwargs) => search(name, kwargs),
108
+ });
109
+ }
@@ -0,0 +1,62 @@
1
+ site: konachan
2
+ name: detail
3
+ description: Get Konachan post detail by post id
4
+ domain: konachan.com
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://konachan.com/post.json"
17
+ params:
18
+ tags: "${{ 'id:' + args.id }}"
19
+ limit: 1
20
+
21
+ - assert:
22
+ condition: "data.length > 0"
23
+ message: "No Konachan post found for this id."
24
+
25
+ - map:
26
+ id: "${{ item.id }}"
27
+ rating: "${{ item.rating }}"
28
+ score: "${{ item.score }}"
29
+ author: "${{ item.author }}"
30
+ created_at: "${{ item.created_at }}"
31
+ width: "${{ item.width }}"
32
+ height: "${{ item.height }}"
33
+ file_ext: "${{ item.file_ext }}"
34
+ file_size: "${{ item.file_size }}"
35
+ tags: "${{ item.tags }}"
36
+ source: "${{ item.source }}"
37
+ preview_url: "${{ item.preview_url }}"
38
+ sample_url: "${{ item.sample_url }}"
39
+ file_url: "${{ item.file_url }}"
40
+ url: "${{ 'https://konachan.com/post/show/' + item.id }}"
41
+
42
+ columns:
43
+ [
44
+ id,
45
+ rating,
46
+ score,
47
+ author,
48
+ width,
49
+ height,
50
+ tags,
51
+ source,
52
+ sample_url,
53
+ file_url,
54
+ url,
55
+ ]
56
+
57
+ capabilities: ["http.fetch"]
58
+ minimum_capability: http.fetch
59
+ trust: public
60
+ confidentiality: public
61
+ quarantine: false
62
+ schema_version: v2
@@ -0,0 +1,55 @@
1
+ site: konachan
2
+ name: download
3
+ description: Download Konachan post image by post id
4
+ domain: konachan.com
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/konachan"
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://konachan.com/post.json"
28
+ params:
29
+ tags: "${{ 'id:' + args.id }}"
30
+ limit: 1
31
+
32
+ - assert:
33
+ condition: "data.length > 0"
34
+ message: "No Konachan post found for this id."
35
+
36
+ - download:
37
+ url: "${{ args.quality === 'original' ? item.file_url : item.sample_url }}"
38
+ dir: "${{ args.output }}"
39
+ filename: "${{ item.id }}.${{ (args.quality === 'original' ? item.file_url : item.sample_url) | ext | default('jpg') }}"
40
+ type: image
41
+ concurrency: 1
42
+
43
+ - map:
44
+ id: "${{ item.id }}"
45
+ rating: "${{ item.rating }}"
46
+ tags: "${{ item.tags }}"
47
+ url: "${{ 'https://konachan.com/post/show/' + item.id }}"
48
+ _download: "${{ item._download }}"
49
+
50
+ capabilities: ["http.fetch", "http.download"]
51
+ minimum_capability: http.fetch
52
+ trust: public
53
+ confidentiality: public
54
+ quarantine: false
55
+ schema_version: v2
@@ -0,0 +1,65 @@
1
+ site: konachan
2
+ name: search
3
+ description: Search Konachan illustration posts by Japanese, romaji, or Moebooru tag query
4
+ domain: konachan.com
5
+ type: web-api
6
+ strategy: public
7
+
8
+ args:
9
+ tags:
10
+ type: str
11
+ required: true
12
+ positional: true
13
+ description: Tag query, for example "rating:safe" or "sparkle_(honkai:_star_rail)"
14
+ limit:
15
+ type: int
16
+ default: 20
17
+ page:
18
+ type: int
19
+ default: 1
20
+
21
+ pipeline:
22
+ - fetch:
23
+ url: "https://konachan.com/post.json"
24
+ params:
25
+ tags: "${{ args.tags }}"
26
+ limit: "${{ args.limit }}"
27
+ page: "${{ args.page }}"
28
+
29
+ - map:
30
+ rank: "${{ index + 1 }}"
31
+ id: "${{ item.id }}"
32
+ rating: "${{ item.rating }}"
33
+ score: "${{ item.score }}"
34
+ author: "${{ item.author }}"
35
+ width: "${{ item.width }}"
36
+ height: "${{ item.height }}"
37
+ tags: "${{ item.tags }}"
38
+ source: "${{ item.source }}"
39
+ preview_url: "${{ item.preview_url }}"
40
+ sample_url: "${{ item.sample_url }}"
41
+ file_url: "${{ item.file_url }}"
42
+ url: "${{ 'https://konachan.com/post/show/' + item.id }}"
43
+
44
+ columns:
45
+ [
46
+ rank,
47
+ id,
48
+ rating,
49
+ score,
50
+ author,
51
+ width,
52
+ height,
53
+ tags,
54
+ source,
55
+ sample_url,
56
+ file_url,
57
+ url,
58
+ ]
59
+
60
+ capabilities: ["http.fetch"]
61
+ minimum_capability: http.fetch
62
+ trust: public
63
+ confidentiality: public
64
+ quarantine: false
65
+ schema_version: v2
@@ -0,0 +1,40 @@
1
+ site: konachan
2
+ name: tags
3
+ description: Search Konachan tags by Japanese name, romaji, or tag prefix
4
+ domain: konachan.com
5
+ type: web-api
6
+ strategy: public
7
+
8
+ args:
9
+ query:
10
+ type: str
11
+ required: true
12
+ positional: true
13
+ limit:
14
+ type: int
15
+ default: 20
16
+
17
+ pipeline:
18
+ - fetch:
19
+ url: "https://konachan.com/tag.json"
20
+ params:
21
+ name: "${{ args.query }}"
22
+ limit: "${{ args.limit }}"
23
+
24
+ - map:
25
+ rank: "${{ index + 1 }}"
26
+ id: "${{ item.id }}"
27
+ name: "${{ item.name }}"
28
+ count: "${{ item.count }}"
29
+ type: "${{ item.type }}"
30
+ ambiguous: "${{ item.ambiguous }}"
31
+ url: "${{ 'https://konachan.com/post?tags=' + encodeURIComponent(item.name) }}"
32
+
33
+ columns: [rank, id, name, count, type, ambiguous, url]
34
+
35
+ capabilities: ["http.fetch"]
36
+ minimum_capability: http.fetch
37
+ trust: public
38
+ confidentiality: public
39
+ quarantine: false
40
+ schema_version: v2
@@ -0,0 +1,46 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { resolveCommand } from "../../registry.js";
3
+ import { mapMangaDexAuthors, mapMangaDexManga } from "./web.js";
4
+
5
+ describe("mangadex public commands", () => {
6
+ it("registers manga and author search", () => {
7
+ expect(
8
+ Object.keys(resolveCommand("mangadex", "manga")!.adapter.commands),
9
+ ).toEqual(expect.arrayContaining(["manga", "authors"]));
10
+ });
11
+
12
+ it("exposes year, sort, and content rating controls for recent manga lookup", () => {
13
+ expect(resolveCommand("mangadex", "manga")!.command.adapterArgs).toEqual(
14
+ expect.arrayContaining([
15
+ expect.objectContaining({ name: "year", type: "int" }),
16
+ expect.objectContaining({
17
+ name: "sort",
18
+ choices: expect.arrayContaining(["latest", "followed", "relevance"]),
19
+ }),
20
+ expect.objectContaining({
21
+ name: "content-rating",
22
+ choices: expect.arrayContaining(["safe", "suggestive", "erotica"]),
23
+ }),
24
+ ]),
25
+ );
26
+ });
27
+
28
+ it("maps manga rows", () => {
29
+ expect(
30
+ mapMangaDexManga([
31
+ {
32
+ id: "m1",
33
+ attributes: { title: { en: "Hanabi" }, status: "ongoing" },
34
+ },
35
+ ]),
36
+ ).toMatchObject([
37
+ { rank: 1, id: "m1", title: "Hanabi", status: "ongoing" },
38
+ ]);
39
+ });
40
+
41
+ it("maps author rows", () => {
42
+ expect(
43
+ mapMangaDexAuthors([{ id: "a1", attributes: { name: "Yuzusoft" } }]),
44
+ ).toMatchObject([{ rank: 1, id: "a1", name: "Yuzusoft" }]);
45
+ });
46
+ });