@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
package/src/config.ts ADDED
@@ -0,0 +1,250 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+
5
+ import type { LlmApi, RuntimeConfig, SearchEnginePreset, ThemeName, ToolExecutionMode } from "./types/config.js";
6
+
7
+ function toInt(value: unknown, fallback: number): number {
8
+ if (value === undefined || value === null || value === "") return fallback;
9
+ const parsed = Number.parseInt(String(value), 10);
10
+ return Number.isFinite(parsed) ? parsed : fallback;
11
+ }
12
+
13
+ function toBool(value: unknown, fallback: boolean): boolean {
14
+ if (value === undefined || value === null || value === "") return fallback;
15
+ const normalized = String(value).trim().toLowerCase();
16
+ if (["1", "true", "yes", "y", "on"].includes(normalized)) return true;
17
+ if (["0", "false", "no", "n", "off"].includes(normalized)) return false;
18
+ return fallback;
19
+ }
20
+
21
+ function toStringOr(value: unknown, fallback: string): string {
22
+ if (value === undefined || value === null) return fallback;
23
+ const normalized = String(value).trim();
24
+ return normalized || fallback;
25
+ }
26
+
27
+ function parseToolExecutionMode(value: unknown, fallback: ToolExecutionMode = "parallel"): ToolExecutionMode {
28
+ if (!value) return fallback;
29
+ const normalized = String(value).trim().toLowerCase();
30
+ if (normalized === "parallel" || normalized === "sequential") return normalized;
31
+ return fallback;
32
+ }
33
+
34
+ function parseTheme(value: unknown, fallback: ThemeName = "ansi"): ThemeName {
35
+ if (!value) return fallback;
36
+ const normalized = String(value).trim().toLowerCase();
37
+ if (normalized === "ansi" || normalized === "none") return normalized;
38
+ return fallback;
39
+ }
40
+
41
+ function parseSearchEngine(value: unknown, fallback: SearchEnginePreset = "duckduckgo"): SearchEnginePreset {
42
+ if (!value) return fallback;
43
+ const normalized = String(value).trim().toLowerCase();
44
+
45
+ if (normalized === "duckduckgo") return "duckduckgo";
46
+ if (normalized === "bing") return "bing";
47
+ if (normalized === "google") return "google";
48
+ if (normalized === "brave") return "brave";
49
+ if (normalized === "custom") return "custom";
50
+
51
+ return fallback;
52
+ }
53
+
54
+ function isObject(value: unknown): value is Record<string, unknown> {
55
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
56
+ }
57
+
58
+ function loadFileConfig(runtimeDir: string): { configFile: string; values: Record<string, unknown> } {
59
+ const configFile = process.env.YAGAMI_CONFIG_FILE || path.join(runtimeDir, "config.json");
60
+
61
+ try {
62
+ const raw = fs.readFileSync(configFile, "utf8");
63
+ if (!raw.trim()) {
64
+ return { configFile, values: {} };
65
+ }
66
+
67
+ const parsed: unknown = JSON.parse(raw);
68
+ if (!isObject(parsed)) {
69
+ return { configFile, values: {} };
70
+ }
71
+
72
+ const ui = isObject(parsed.ui) ? parsed.ui : {};
73
+ return {
74
+ configFile,
75
+ values: {
76
+ ...parsed,
77
+ ...ui,
78
+ },
79
+ };
80
+ } catch {
81
+ return { configFile, values: {} };
82
+ }
83
+ }
84
+
85
+ function parseThemeTokens(value: unknown): Record<string, string> {
86
+ if (!isObject(value)) return {};
87
+
88
+ const tokens: Record<string, string> = {};
89
+
90
+ for (const [token, raw] of Object.entries(value)) {
91
+ if (raw === undefined || raw === null) continue;
92
+
93
+ const key = String(token || "").trim();
94
+ if (!key) continue;
95
+
96
+ const normalizedValue = String(raw).trim();
97
+ if (!normalizedValue) continue;
98
+
99
+ tokens[key] = normalizedValue;
100
+ }
101
+
102
+ return tokens;
103
+ }
104
+
105
+ function parseThemeTokensEnv(rawValue: unknown): Record<string, string> {
106
+ if (!rawValue) return {};
107
+
108
+ try {
109
+ const parsed: unknown = JSON.parse(String(rawValue));
110
+ return parseThemeTokens(parsed);
111
+ } catch {
112
+ return {};
113
+ }
114
+ }
115
+
116
+ function normalizeLlmApi(value: unknown, fallback: LlmApi = "openai-completions"): LlmApi {
117
+ const raw = String(value ?? "")
118
+ .trim()
119
+ .toLowerCase();
120
+ if (!raw) return fallback;
121
+
122
+ if (raw === "anthropic-messages") return "anthropic-messages";
123
+ if (raw === "openai-completions") return "openai-completions";
124
+
125
+ return fallback;
126
+ }
127
+
128
+ function defaultLlmBaseUrl(api: LlmApi): string {
129
+ if (api === "anthropic-messages") return "https://api.anthropic.com";
130
+ return "http://127.0.0.1:1234/v1";
131
+ }
132
+
133
+ function parseCdpEndpoint(rawCdpUrl: string): { host: string; port: number } {
134
+ try {
135
+ const url = new URL(rawCdpUrl);
136
+ const host = url.hostname || "127.0.0.1";
137
+ const port = url.port ? Number.parseInt(url.port, 10) : 9222;
138
+
139
+ return {
140
+ host,
141
+ port: Number.isFinite(port) ? port : 9222,
142
+ };
143
+ } catch {
144
+ return {
145
+ host: "127.0.0.1",
146
+ port: 9222,
147
+ };
148
+ }
149
+ }
150
+
151
+ function defaultRuntimeDir(): string {
152
+ const xdgConfigHome = String(process.env.XDG_CONFIG_HOME || "").trim();
153
+ if (xdgConfigHome) {
154
+ return path.join(xdgConfigHome, "yagami");
155
+ }
156
+
157
+ return path.join(os.homedir(), ".config", "yagami");
158
+ }
159
+
160
+ export function getConfig(): RuntimeConfig {
161
+ const runtimeDir = process.env.YAGAMI_RUNTIME_DIR || defaultRuntimeDir();
162
+
163
+ const fileConfig = loadFileConfig(runtimeDir);
164
+ const fileValues = fileConfig.values || {};
165
+
166
+ const fileHost = toStringOr(fileValues.host, "127.0.0.1");
167
+ const filePort = toInt(fileValues.port, 43111);
168
+
169
+ const host = toStringOr(process.env.YAGAMI_HOST, fileHost || "127.0.0.1");
170
+ const port = toInt(process.env.YAGAMI_PORT, filePort);
171
+
172
+ const fileTheme = parseTheme(fileValues.theme, "ansi");
173
+ const fileThemeTokens = parseThemeTokens(fileValues.themeTokens ?? fileValues.colors ?? fileValues.themeColors);
174
+ const envThemeTokens = parseThemeTokensEnv(process.env.YAGAMI_THEME_TOKENS);
175
+
176
+ const fileLlmApi = normalizeLlmApi(fileValues.llmApi, "openai-completions");
177
+ const fileLlmBaseUrl = toStringOr(fileValues.llmBaseUrl, "");
178
+ const fileLlmApiKey = toStringOr(fileValues.llmApiKey, "");
179
+ const fileLlmModel = toStringOr(fileValues.llmModel, "");
180
+ const fileSearchEngine = parseSearchEngine(fileValues.searchEngine, "duckduckgo");
181
+ const fileSearchEngineUrlTemplate = toStringOr(fileValues.searchEngineUrlTemplate, "");
182
+
183
+ const fileBrowseLinkTimeoutMs = toInt(fileValues.browseLinkTimeoutMs, 7000);
184
+ const fileCacheTtlMs = toInt(fileValues.cacheTtlMs, 10 * 60 * 1000);
185
+ const fileMaxMarkdownChars = toInt(fileValues.maxMarkdownChars, 120000);
186
+ const fileOperationConcurrency = toInt(fileValues.operationConcurrency, 4);
187
+ const fileBrowseConcurrency = toInt(fileValues.browseConcurrency, 8);
188
+
189
+ const llmApi = normalizeLlmApi(process.env.YAGAMI_LLM_API, fileLlmApi);
190
+ const llmBaseUrl = toStringOr(process.env.YAGAMI_LLM_BASE_URL, fileLlmBaseUrl || defaultLlmBaseUrl(llmApi));
191
+ const llmApiKey = toStringOr(process.env.YAGAMI_LLM_API_KEY, fileLlmApiKey || "");
192
+ const llmModel = toStringOr(process.env.YAGAMI_LLM_MODEL, fileLlmModel || "");
193
+ const searchEngine = parseSearchEngine(process.env.YAGAMI_SEARCH_ENGINE, fileSearchEngine);
194
+ const searchEngineUrlTemplate = toStringOr(
195
+ process.env.YAGAMI_SEARCH_ENGINE_URL_TEMPLATE,
196
+ fileSearchEngineUrlTemplate || "",
197
+ );
198
+
199
+ const lightpandaCdpUrl = process.env.YAGAMI_CDP_URL || "ws://127.0.0.1:9222";
200
+ const cdpEndpoint = parseCdpEndpoint(lightpandaCdpUrl);
201
+
202
+ const browseLinkTimeoutMs = toInt(process.env.YAGAMI_BROWSE_LINK_TIMEOUT_MS, fileBrowseLinkTimeoutMs);
203
+
204
+ return {
205
+ runtimeDir,
206
+ configFile: fileConfig.configFile,
207
+ host,
208
+ port,
209
+ daemonUrl: `http://${host}:${port}`,
210
+
211
+ pidFile: path.join(runtimeDir, "yagami.pid"),
212
+ logFile: path.join(runtimeDir, "yagami.log"),
213
+
214
+ llmApi,
215
+ llmBaseUrl,
216
+ llmApiKey,
217
+ llmModel,
218
+
219
+ searchEngine,
220
+ searchEngineUrlTemplate,
221
+
222
+ lightpandaCdpUrl,
223
+ lightpandaHost: process.env.YAGAMI_LIGHTPANDA_HOST || cdpEndpoint.host,
224
+ lightpandaPort: toInt(process.env.YAGAMI_LIGHTPANDA_PORT, cdpEndpoint.port),
225
+ lightpandaAutoStart: toBool(process.env.YAGAMI_LIGHTPANDA_AUTO_START, true),
226
+ lightpandaAutoStop: toBool(process.env.YAGAMI_LIGHTPANDA_AUTO_STOP, true),
227
+
228
+ browseLinkTimeoutMs,
229
+ queryTimeoutMs: toInt(process.env.YAGAMI_QUERY_TIMEOUT_MS, 180000),
230
+
231
+ cacheTtlMs: Math.max(1000, toInt(process.env.YAGAMI_CACHE_TTL_MS, fileCacheTtlMs)),
232
+ maxHtmlChars: toInt(process.env.YAGAMI_MAX_HTML_CHARS, 250000),
233
+ maxMarkdownChars: toInt(process.env.YAGAMI_MAX_MARKDOWN_CHARS, fileMaxMarkdownChars),
234
+ maxDocuments: toInt(process.env.YAGAMI_MAX_DOCUMENTS, 200),
235
+
236
+ operationConcurrency: Math.max(1, toInt(process.env.YAGAMI_OPERATION_CONCURRENCY, fileOperationConcurrency)),
237
+ browseConcurrency: Math.max(1, toInt(process.env.YAGAMI_BROWSE_CONCURRENCY, fileBrowseConcurrency)),
238
+
239
+ researchMaxPages: toInt(process.env.YAGAMI_RESEARCH_MAX_PAGES, 12),
240
+ researchMaxHops: toInt(process.env.YAGAMI_RESEARCH_MAX_HOPS, 2),
241
+ researchSameDomainOnly: toBool(process.env.YAGAMI_RESEARCH_SAME_DOMAIN_ONLY, false),
242
+
243
+ toolExecutionMode: parseToolExecutionMode(process.env.YAGAMI_TOOL_EXECUTION, "parallel"),
244
+ theme: parseTheme(process.env.YAGAMI_THEME, fileTheme),
245
+ themeTokens: {
246
+ ...fileThemeTokens,
247
+ ...envThemeTokens,
248
+ },
249
+ };
250
+ }