@ahkohd/yagami 0.1.2

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 (81) hide show
  1. package/.beads/.beads-credential-key +1 -0
  2. package/.beads/README.md +81 -0
  3. package/.beads/config.yaml +54 -0
  4. package/.beads/hooks/post-checkout +24 -0
  5. package/.beads/hooks/post-merge +24 -0
  6. package/.beads/hooks/pre-commit +24 -0
  7. package/.beads/hooks/pre-push +24 -0
  8. package/.beads/hooks/prepare-commit-msg +24 -0
  9. package/.beads/metadata.json +7 -0
  10. package/.github/workflows/ci.yml +43 -0
  11. package/.github/workflows/release.yml +115 -0
  12. package/AGENTS.md +150 -0
  13. package/README.md +210 -0
  14. package/biome.json +36 -0
  15. package/config/mcporter.json +8 -0
  16. package/dist/cli/theme.js +202 -0
  17. package/dist/cli/theme.js.map +1 -0
  18. package/dist/cli.js +1883 -0
  19. package/dist/cli.js.map +1 -0
  20. package/dist/config.js +223 -0
  21. package/dist/config.js.map +1 -0
  22. package/dist/daemon.js +745 -0
  23. package/dist/daemon.js.map +1 -0
  24. package/dist/engine/constants.js +131 -0
  25. package/dist/engine/constants.js.map +1 -0
  26. package/dist/engine/deep-research.js +167 -0
  27. package/dist/engine/deep-research.js.map +1 -0
  28. package/dist/engine/defuddle-utils.js +57 -0
  29. package/dist/engine/defuddle-utils.js.map +1 -0
  30. package/dist/engine/github-fetch.js +232 -0
  31. package/dist/engine/github-fetch.js.map +1 -0
  32. package/dist/engine/helpers.js +372 -0
  33. package/dist/engine/helpers.js.map +1 -0
  34. package/dist/engine/limiter.js +75 -0
  35. package/dist/engine/limiter.js.map +1 -0
  36. package/dist/engine/policy.js +313 -0
  37. package/dist/engine/policy.js.map +1 -0
  38. package/dist/engine/runtime-utils.js +65 -0
  39. package/dist/engine/runtime-utils.js.map +1 -0
  40. package/dist/engine/search-discovery.js +275 -0
  41. package/dist/engine/search-discovery.js.map +1 -0
  42. package/dist/engine/url-utils.js +72 -0
  43. package/dist/engine/url-utils.js.map +1 -0
  44. package/dist/engine.js +2030 -0
  45. package/dist/engine.js.map +1 -0
  46. package/dist/mcp.js +282 -0
  47. package/dist/mcp.js.map +1 -0
  48. package/dist/types/cli.js +2 -0
  49. package/dist/types/cli.js.map +1 -0
  50. package/dist/types/config.js +2 -0
  51. package/dist/types/config.js.map +1 -0
  52. package/dist/types/daemon.js +2 -0
  53. package/dist/types/daemon.js.map +1 -0
  54. package/dist/types/engine.js +2 -0
  55. package/dist/types/engine.js.map +1 -0
  56. package/package.json +66 -0
  57. package/packages/pi-yagami-search/README.md +39 -0
  58. package/packages/pi-yagami-search/extensions/yagami-search.ts +273 -0
  59. package/packages/pi-yagami-search/package.json +41 -0
  60. package/src/cli/theme.ts +260 -0
  61. package/src/cli.ts +2226 -0
  62. package/src/config.ts +250 -0
  63. package/src/daemon.ts +990 -0
  64. package/src/engine/constants.ts +147 -0
  65. package/src/engine/deep-research.ts +207 -0
  66. package/src/engine/defuddle-utils.ts +75 -0
  67. package/src/engine/github-fetch.ts +265 -0
  68. package/src/engine/helpers.ts +394 -0
  69. package/src/engine/limiter.ts +97 -0
  70. package/src/engine/policy.ts +392 -0
  71. package/src/engine/runtime-utils.ts +79 -0
  72. package/src/engine/search-discovery.ts +351 -0
  73. package/src/engine/url-utils.ts +86 -0
  74. package/src/engine.ts +2516 -0
  75. package/src/mcp.ts +337 -0
  76. package/src/shims-cli.d.ts +3 -0
  77. package/src/types/cli.ts +7 -0
  78. package/src/types/config.ts +53 -0
  79. package/src/types/daemon.ts +22 -0
  80. package/src/types/engine.ts +194 -0
  81. package/tsconfig.json +18 -0
@@ -0,0 +1,273 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import { StringEnum } from "@mariozechner/pi-ai";
3
+ import { Text } from "@mariozechner/pi-tui";
4
+ import { Type } from "@sinclair/typebox";
5
+
6
+ const YAGAMI_BASE = process.env.YAGAMI_URL || "http://127.0.0.1:43111";
7
+
8
+ type YagamiResponse = {
9
+ ok?: boolean;
10
+ error?: string;
11
+ result?: unknown;
12
+ };
13
+
14
+ type RenderTheme = {
15
+ bold: (text: string) => string;
16
+ fg: (token: string, text: string) => string;
17
+ };
18
+
19
+ type RenderArgs = Record<string, unknown>;
20
+
21
+ function asString(value: unknown): string {
22
+ return typeof value === "string" ? value : "";
23
+ }
24
+
25
+ function errorMessage(error: unknown): string {
26
+ if (error instanceof Error) return error.message;
27
+ return String(error);
28
+ }
29
+
30
+ function isRecord(value: unknown): value is Record<string, unknown> {
31
+ return typeof value === "object" && value !== null;
32
+ }
33
+
34
+ function renderCallLine(toolName: string, value: unknown, theme: RenderTheme): Text {
35
+ return new Text(`${theme.bold(toolName)} ${theme.fg("muted", asString(value))}`, 0, 0);
36
+ }
37
+
38
+ async function yagamiPost(endpoint: string, body: Record<string, unknown>): Promise<unknown> {
39
+ const res = await fetch(`${YAGAMI_BASE}${endpoint}`, {
40
+ method: "POST",
41
+ headers: { "Content-Type": "application/json" },
42
+ body: JSON.stringify(body),
43
+ });
44
+
45
+ if (!res.ok) {
46
+ const text = await res.text();
47
+ throw new Error(`Yagami error ${res.status}: ${text}`);
48
+ }
49
+
50
+ const data = (await res.json()) as YagamiResponse;
51
+ if (!data.ok) throw new Error(data.error || "Unknown yagami error");
52
+ return data.result;
53
+ }
54
+
55
+ function errResult(message: string) {
56
+ return { content: [{ type: "text" as const, text: message }], details: {}, isError: true };
57
+ }
58
+
59
+ function okResult(text: string) {
60
+ return { content: [{ type: "text" as const, text }], details: {} };
61
+ }
62
+
63
+ function formatResult(result: unknown): string {
64
+ if (typeof result === "string") return result;
65
+ if (isRecord(result) && typeof result.answer === "string") return result.answer;
66
+ if (isRecord(result) && typeof result.text === "string") return result.text;
67
+ return JSON.stringify(result, null, 2);
68
+ }
69
+
70
+ export default function (pi: ExtensionAPI) {
71
+ pi.registerTool({
72
+ name: "web_search",
73
+ label: "Yagami Search",
74
+ description:
75
+ "Search the web for any topic and get clean, ready-to-use content.\n\n" +
76
+ "Best for: Finding current information, news, facts, or answering questions about any topic.\n" +
77
+ "Returns: Clean text content from top search results, ready for LLM use.",
78
+ renderCall(args: RenderArgs, theme: RenderTheme) {
79
+ return renderCallLine("web_search", args.query, theme);
80
+ },
81
+ parameters: Type.Object({
82
+ query: Type.String({ description: "Web search query" }),
83
+ }),
84
+ async execute(_id, params) {
85
+ try {
86
+ const result = await yagamiPost("/search", { query: params.query });
87
+ return okResult(formatResult(result));
88
+ } catch (error: unknown) {
89
+ return errResult(`Search error: ${errorMessage(error)}`);
90
+ }
91
+ },
92
+ });
93
+
94
+ pi.registerTool({
95
+ name: "get_code_context",
96
+ label: "Yagami Code Search",
97
+ description:
98
+ "Find code examples, documentation, and programming solutions. Searches GitHub, Stack Overflow, and official docs.\n\n" +
99
+ "Best for: Any programming question - API usage, library examples, code snippets, debugging help.\n" +
100
+ "Returns: Relevant code and documentation, formatted for easy reading.",
101
+ renderCall(args: RenderArgs, theme: RenderTheme) {
102
+ return renderCallLine("get_code_context", args.query, theme);
103
+ },
104
+ parameters: Type.Object({
105
+ query: Type.String({ description: "Search query for code context." }),
106
+ }),
107
+ async execute(_id, params) {
108
+ try {
109
+ const result = await yagamiPost("/code-context", { query: params.query });
110
+ return okResult(formatResult(result));
111
+ } catch (error: unknown) {
112
+ return errResult(`Code search error: ${errorMessage(error)}`);
113
+ }
114
+ },
115
+ });
116
+
117
+ pi.registerTool({
118
+ name: "fetch_content",
119
+ label: "Yagami Fetch",
120
+ description:
121
+ "Get the full content of a specific webpage. Use when you have an exact URL.\n\n" +
122
+ "Best for: Extracting content from a known URL.\n" +
123
+ "Returns: Full text content and metadata from the page.",
124
+ renderCall(args: RenderArgs, theme: RenderTheme) {
125
+ return renderCallLine("fetch_content", args.url, theme);
126
+ },
127
+ parameters: Type.Object({
128
+ url: Type.String({ description: "URL to crawl and extract content from" }),
129
+ maxCharacters: Type.Optional(Type.Number({ description: "Maximum characters to extract (default: 3000)" })),
130
+ }),
131
+ async execute(_id, params) {
132
+ try {
133
+ const result = await yagamiPost("/fetch", {
134
+ url: params.url,
135
+ maxCharacters: params.maxCharacters || 3000,
136
+ });
137
+ return okResult(formatResult(result));
138
+ } catch (error: unknown) {
139
+ return errResult(`Fetch error: ${errorMessage(error)}`);
140
+ }
141
+ },
142
+ });
143
+
144
+ pi.registerTool({
145
+ name: "company_research",
146
+ label: "Yagami Company Research",
147
+ description:
148
+ "Research any company to get business information, news, and insights.\n\n" +
149
+ "Best for: Learning about a company's products, services, recent news, or industry position.\n" +
150
+ "Returns: Company information from trusted business sources.",
151
+ renderCall(args: RenderArgs, theme: RenderTheme) {
152
+ return renderCallLine("company_research", args.companyName, theme);
153
+ },
154
+ parameters: Type.Object({
155
+ companyName: Type.String({ description: "Name of the company to research" }),
156
+ }),
157
+ async execute(_id, params) {
158
+ try {
159
+ const result = await yagamiPost("/company-research", { companyName: params.companyName });
160
+ return okResult(formatResult(result));
161
+ } catch (error: unknown) {
162
+ return errResult(`Company research error: ${errorMessage(error)}`);
163
+ }
164
+ },
165
+ });
166
+
167
+ pi.registerTool({
168
+ name: "web_search_advanced",
169
+ label: "Yagami Advanced Search",
170
+ description:
171
+ "Advanced web search with full control over filters, domains, dates, and content options.\n\n" +
172
+ "Best for: When you need specific filters like date ranges, domain restrictions, or category filters.\n" +
173
+ "Not recommended for: Simple searches - use web_search instead.\n" +
174
+ "Returns: Search results with optional highlights, summaries, and subpage content.",
175
+ renderCall(args: RenderArgs, theme: RenderTheme) {
176
+ return renderCallLine("web_search_advanced", args.query, theme);
177
+ },
178
+ parameters: Type.Object({
179
+ query: Type.String({ description: "Search query" }),
180
+ includeDomains: Type.Optional(
181
+ Type.Array(Type.String(), { description: "Only include results from these domains" }),
182
+ ),
183
+ excludeDomains: Type.Optional(Type.Array(Type.String(), { description: "Exclude results from these domains" })),
184
+ category: Type.Optional(Type.String({ description: "Filter results to a specific category" })),
185
+ }),
186
+ async execute(_id, params) {
187
+ try {
188
+ const result = await yagamiPost("/search/advanced", params);
189
+ return okResult(formatResult(result));
190
+ } catch (error: unknown) {
191
+ return errResult(`Advanced search error: ${errorMessage(error)}`);
192
+ }
193
+ },
194
+ });
195
+
196
+ pi.registerTool({
197
+ name: "find_similar",
198
+ label: "Yagami Similar",
199
+ description:
200
+ "Find web pages similar to a given URL. Useful for finding alternatives, related resources, or similar documentation.",
201
+ renderCall(args: RenderArgs, theme: RenderTheme) {
202
+ return renderCallLine("find_similar", args.url, theme);
203
+ },
204
+ parameters: Type.Object({
205
+ url: Type.String({ description: "URL to find similar pages for" }),
206
+ }),
207
+ async execute(_id, params) {
208
+ try {
209
+ const result = await yagamiPost("/find-similar", { url: params.url });
210
+ return okResult(formatResult(result));
211
+ } catch (error: unknown) {
212
+ return errResult(`Find similar error: ${errorMessage(error)}`);
213
+ }
214
+ },
215
+ });
216
+
217
+ pi.registerTool({
218
+ name: "deep_research_start",
219
+ label: "Yagami Deep Research",
220
+ description:
221
+ "Start an AI research agent that searches, reads, and writes a detailed report.\n\n" +
222
+ "Best for: Complex research questions needing deep analysis and synthesis.\n" +
223
+ "Returns: Research ID - use deep_research_check to get results.\n" +
224
+ "Important: Call deep_research_check with the returned research ID to get the report.",
225
+ renderCall(args: RenderArgs, theme: RenderTheme) {
226
+ const preview = asString(args.instructions).slice(0, 80);
227
+ return renderCallLine("deep_research_start", preview, theme);
228
+ },
229
+ parameters: Type.Object({
230
+ instructions: Type.String({ description: "Complex research question or detailed instructions." }),
231
+ effort: Type.Optional(
232
+ StringEnum(["fast", "balanced", "thorough"] as const, {
233
+ description: "'fast', 'balanced', or 'thorough'. Default: fast",
234
+ }),
235
+ ),
236
+ }),
237
+ async execute(_id, params) {
238
+ try {
239
+ const result = await yagamiPost("/deep-research/start", {
240
+ instructions: params.instructions,
241
+ effort: params.effort || "fast",
242
+ });
243
+ return okResult(JSON.stringify(result, null, 2));
244
+ } catch (error: unknown) {
245
+ return errResult(`Research start error: ${errorMessage(error)}`);
246
+ }
247
+ },
248
+ });
249
+
250
+ pi.registerTool({
251
+ name: "deep_research_check",
252
+ label: "Yagami Research Check",
253
+ description:
254
+ "Check status and get results from a deep research task.\n\n" +
255
+ "Best for: Getting the research report after calling deep_research_start.\n" +
256
+ "Returns: Research report when complete, or status update if still running.\n" +
257
+ "Important: Keep calling with the same research ID until status is 'completed'.",
258
+ renderCall(args: RenderArgs, theme: RenderTheme) {
259
+ return renderCallLine("deep_research_check", args.researchId, theme);
260
+ },
261
+ parameters: Type.Object({
262
+ researchId: Type.String({ description: "The research ID returned from deep_research_start" }),
263
+ }),
264
+ async execute(_id, params) {
265
+ try {
266
+ const result = await yagamiPost("/deep-research/check", { researchId: params.researchId });
267
+ return okResult(JSON.stringify(result, null, 2));
268
+ } catch (error: unknown) {
269
+ return errResult(`Research check error: ${errorMessage(error)}`);
270
+ }
271
+ },
272
+ });
273
+ }
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@ahkohd/pi-yagami-search",
3
+ "version": "0.1.2",
4
+ "private": false,
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "type": "module",
9
+ "description": "Pi package providing Yagami web search tools",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/ahkohd/yagami.git",
13
+ "directory": "packages/pi-yagami-search"
14
+ },
15
+ "homepage": "https://github.com/ahkohd/yagami/tree/main/packages/pi-yagami-search",
16
+ "bugs": {
17
+ "url": "https://github.com/ahkohd/yagami/issues"
18
+ },
19
+ "keywords": [
20
+ "pi-package",
21
+ "yagami",
22
+ "pi-extension",
23
+ "search"
24
+ ],
25
+ "pi": {
26
+ "extensions": [
27
+ "./extensions"
28
+ ]
29
+ },
30
+ "peerDependencies": {
31
+ "@mariozechner/pi-ai": "*",
32
+ "@mariozechner/pi-coding-agent": "*",
33
+ "@mariozechner/pi-tui": "*",
34
+ "@sinclair/typebox": "*"
35
+ },
36
+ "files": [
37
+ "extensions",
38
+ "README.md",
39
+ "package.json"
40
+ ]
41
+ }
@@ -0,0 +1,260 @@
1
+ import type { RuntimeConfig } from "../types/config.js";
2
+
3
+ const THEMES = {
4
+ ansi: {
5
+ domain: "3;94",
6
+ title: "97",
7
+ duration: "3;93",
8
+ error: "91",
9
+ dim: "2",
10
+ cyan: "96",
11
+ bold: "1",
12
+ },
13
+ none: {
14
+ domain: "",
15
+ title: "",
16
+ duration: "",
17
+ error: "",
18
+ dim: "",
19
+ cyan: "",
20
+ bold: "",
21
+ },
22
+ } as const;
23
+
24
+ const THEME_TOKENS = ["domain", "title", "duration", "error", "dim", "cyan", "bold"] as const;
25
+
26
+ const FIXED_ICONS = {
27
+ success: "ok",
28
+ error: "x",
29
+ cache: "[cache]",
30
+ pass: "ok",
31
+ fail: "x",
32
+ bullet: "•",
33
+ web: "•",
34
+ webError: "•",
35
+ connector: "│",
36
+ } as const;
37
+
38
+ type ThemeTokenName = (typeof THEME_TOKENS)[number];
39
+ type ThemeCodeMap = Record<ThemeTokenName, string>;
40
+
41
+ function normalizeThemeCode(rawValue: unknown, token: ThemeTokenName, baseTheme: ThemeCodeMap): string | null {
42
+ if (rawValue === undefined || rawValue === null) return null;
43
+
44
+ const raw = String(rawValue).trim();
45
+ if (!raw) return null;
46
+
47
+ const hexMatch = raw.match(/^#?([0-9a-fA-F]{6})$/);
48
+ if (hexMatch) {
49
+ const hex = hexMatch[1];
50
+ if (!hex) return null;
51
+
52
+ const r = Number.parseInt(hex.slice(0, 2), 16);
53
+ const g = Number.parseInt(hex.slice(2, 4), 16);
54
+ const b = Number.parseInt(hex.slice(4, 6), 16);
55
+
56
+ if (![r, g, b].every((value) => Number.isFinite(value))) return null;
57
+
58
+ const stylePrefix = String(baseTheme[token] || "")
59
+ .split(";")
60
+ .filter((part) => part === "1" || part === "2" || part === "3")
61
+ .join(";");
62
+
63
+ const colorPart = `38;2;${r};${g};${b}`;
64
+ return stylePrefix ? `${stylePrefix};${colorPart}` : colorPart;
65
+ }
66
+
67
+ if (/^[0-9;]+$/.test(raw)) {
68
+ return raw;
69
+ }
70
+
71
+ return null;
72
+ }
73
+
74
+ function applyThemeOverrides(themeName: RuntimeConfig["theme"], overridesRaw: Record<string, string>): ThemeCodeMap {
75
+ const baseTheme = (THEMES[themeName] || THEMES.ansi) as ThemeCodeMap;
76
+ const overrides: Partial<ThemeCodeMap> = {};
77
+
78
+ for (const token of THEME_TOKENS) {
79
+ const code = normalizeThemeCode(overridesRaw[token], token, baseTheme);
80
+ if (code !== null) overrides[token] = code;
81
+ }
82
+
83
+ return {
84
+ ...baseTheme,
85
+ ...overrides,
86
+ };
87
+ }
88
+
89
+ function supportsColor(theme: RuntimeConfig["theme"]): boolean {
90
+ if (theme === "none") return false;
91
+ if (process.env.NO_COLOR !== undefined) return false;
92
+
93
+ const noColor = String(process.env.YAGAMI_NO_COLOR || "")
94
+ .trim()
95
+ .toLowerCase();
96
+
97
+ if (noColor === "1" || noColor === "true" || noColor === "yes") return false;
98
+
99
+ const forceColor = String(process.env.FORCE_COLOR || "").trim();
100
+ if (forceColor && forceColor !== "0") return true;
101
+
102
+ return Boolean(process.stdout.isTTY);
103
+ }
104
+
105
+ function colorize(text: string, code: string, enabled: boolean): string {
106
+ if (!enabled || !code) return text;
107
+ return `\x1b[${code}m${text}\x1b[0m`;
108
+ }
109
+
110
+ function styleToken(text: string, token: ThemeTokenName, theme: ThemeCodeMap, colorEnabled: boolean): string {
111
+ return colorize(text, theme[token], colorEnabled);
112
+ }
113
+
114
+ function icon(name: keyof typeof FIXED_ICONS): string {
115
+ return FIXED_ICONS[name] || "";
116
+ }
117
+
118
+ export type ThemeIconName = keyof typeof FIXED_ICONS;
119
+
120
+ export interface CliThemeRuntime {
121
+ colorEnabled: boolean;
122
+ icon: (name: ThemeIconName) => string;
123
+ styleDomain: (text: string) => string;
124
+ styleTitle: (text: string) => string;
125
+ styleDuration: (text: string) => string;
126
+ styleError: (text: string) => string;
127
+ styleDim: (text: string) => string;
128
+ styleDimItalic: (text: string) => string;
129
+ styleCyan: (text: string) => string;
130
+ styleBold: (text: string) => string;
131
+ }
132
+
133
+ export function createCliThemeRuntime(config: RuntimeConfig): CliThemeRuntime {
134
+ const activeThemeName = THEMES[config.theme] ? config.theme : "ansi";
135
+ const activeTheme = applyThemeOverrides(activeThemeName, config.themeTokens || {});
136
+ const colorEnabled = supportsColor(config.theme);
137
+
138
+ const style = (token: ThemeTokenName, text: string): string => styleToken(text, token, activeTheme, colorEnabled);
139
+
140
+ const styleDimItalic = (text: string): string => {
141
+ if (!colorEnabled) return text;
142
+
143
+ const dimCode = String(activeTheme.dim || "2");
144
+ const parts = dimCode.split(";").filter(Boolean);
145
+ if (!parts.includes("3")) parts.push("3");
146
+ return colorize(text, parts.join(";"), true);
147
+ };
148
+
149
+ return {
150
+ colorEnabled,
151
+ icon: (name: ThemeIconName) => icon(name),
152
+ styleDomain: (text: string) => style("domain", text),
153
+ styleTitle: (text: string) => style("title", text),
154
+ styleDuration: (text: string) => style("duration", text),
155
+ styleError: (text: string) => style("error", text),
156
+ styleDim: (text: string) => style("dim", text),
157
+ styleDimItalic,
158
+ styleCyan: (text: string) => style("cyan", text),
159
+ styleBold: (text: string) => style("bold", text),
160
+ };
161
+ }
162
+
163
+ function parseCliArgs(args: string[]): { positional: string[] } {
164
+ const positional: string[] = [];
165
+ for (const token of args) {
166
+ if (!token.startsWith("--")) positional.push(token);
167
+ }
168
+ return { positional };
169
+ }
170
+
171
+ export async function cmdTheme(
172
+ config: RuntimeConfig,
173
+ args: string[],
174
+ options: { asJson?: boolean; printUsage: () => void } = { printUsage: () => {} },
175
+ ): Promise<void> {
176
+ const asJson = options.asJson ?? false;
177
+ const { positional } = parseCliArgs(args);
178
+ const action = String(positional[0] || "preview")
179
+ .trim()
180
+ .toLowerCase();
181
+
182
+ if (action && action !== "preview") {
183
+ console.error("theme command supports: preview\n");
184
+ options.printUsage();
185
+ process.exitCode = 1;
186
+ return;
187
+ }
188
+
189
+ const activeThemeName = THEMES[config.theme] ? config.theme : "ansi";
190
+ const activeTheme = applyThemeOverrides(activeThemeName, config.themeTokens || {});
191
+ const colorEnabled = supportsColor(config.theme);
192
+
193
+ const samples: Record<ThemeTokenName, string> = {
194
+ domain: "example.com",
195
+ title: "Example page title",
196
+ duration: "1.4s",
197
+ error: "request failed",
198
+ dim: "secondary text",
199
+ cyan: "https://example.com",
200
+ bold: "strong label",
201
+ };
202
+
203
+ if (asJson) {
204
+ console.log(
205
+ JSON.stringify(
206
+ {
207
+ theme: activeThemeName,
208
+ themeName: activeThemeName,
209
+ requestedTheme: config.theme,
210
+ configFile: config.configFile,
211
+ overrides: config.themeTokens || {},
212
+ resolvedTokens: activeTheme,
213
+ samples,
214
+ },
215
+ null,
216
+ 2,
217
+ ),
218
+ );
219
+ return;
220
+ }
221
+
222
+ const styleBold = (text: string): string => styleToken(text, "bold", activeTheme, colorEnabled);
223
+ const styleDim = (text: string): string => styleToken(text, "dim", activeTheme, colorEnabled);
224
+ const styleDuration = (text: string): string => styleToken(text, "duration", activeTheme, colorEnabled);
225
+ const styleDimItalic = (text: string): string => {
226
+ if (!colorEnabled) return text;
227
+
228
+ const dimCode = String(activeTheme.dim || "2");
229
+ const parts = dimCode.split(";").filter(Boolean);
230
+ if (!parts.includes("3")) parts.push("3");
231
+ return colorize(text, parts.join(";"), true);
232
+ };
233
+
234
+ console.log(styleBold("Theme preview"));
235
+ console.log(styleDim(`theme=${config.theme} · config=${config.configFile}`));
236
+
237
+ for (const token of THEME_TOKENS) {
238
+ const sample = samples[token] || token;
239
+ const code = activeTheme[token] || "-";
240
+ const label = `${token}:`.padEnd(10, " ");
241
+ console.log(
242
+ ` ${styleDim(label)} ${styleToken(sample, token, activeTheme, colorEnabled)} ${styleDim(`(${code})`)}`,
243
+ );
244
+ }
245
+
246
+ const sampleSuccessDomain = styleToken("duckduckgo.com", "domain", activeTheme, colorEnabled);
247
+ const sampleSuccessTitle = styleToken("Google AI news March 2026 at DuckDuckGo", "title", activeTheme, colorEnabled);
248
+ const sampleErrorDomain = styleToken("deccanherald.com", "domain", activeTheme, colorEnabled);
249
+ const sampleError = styleToken("— Timeout 7000ms exceeded.", "error", activeTheme, colorEnabled);
250
+
251
+ console.log(`\n${styleDim("Stream markers:")}`);
252
+ console.log(`${icon("bullet")} ${sampleSuccessDomain} ${sampleSuccessTitle}`);
253
+ console.log(`${styleDim(icon("connector"))}`);
254
+ console.log(`${icon("bullet")} ${sampleErrorDomain} ${sampleError}`);
255
+
256
+ console.log(`\n${styleDim("Spinner line:")}`);
257
+ console.log(
258
+ `${styleDuration("⠋")} ${styleDim("Reading")} ${styleDimItalic("duckduckgo.com")} ${styleDim("·")} ${styleDim("1.2s")}`,
259
+ );
260
+ }