@nahisaho/shikigami 1.1.0

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 (80) hide show
  1. package/.github/prompts/shikigami-deep-research.prompt.md +32 -0
  2. package/.github/prompts/shikigami-framework-analysis.prompt.md +40 -0
  3. package/.github/prompts/shikigami-full-research.prompt.md +54 -0
  4. package/.github/prompts/shikigami-purpose-discovery.prompt.md +32 -0
  5. package/.github/prompts/shikigami-report-writing.prompt.md +36 -0
  6. package/.github/skills/shikigami-consulting-framework/SKILL.md +403 -0
  7. package/.github/skills/shikigami-consulting-framework/frameworks/README.md +173 -0
  8. package/.github/skills/shikigami-consulting-framework/frameworks/customer/nps.md +164 -0
  9. package/.github/skills/shikigami-consulting-framework/frameworks/customer/rfm.md +160 -0
  10. package/.github/skills/shikigami-consulting-framework/frameworks/decision-making/cost-benefit.md +168 -0
  11. package/.github/skills/shikigami-consulting-framework/frameworks/decision-making/decision-matrix.md +138 -0
  12. package/.github/skills/shikigami-consulting-framework/frameworks/decision-making/pros-cons.md +162 -0
  13. package/.github/skills/shikigami-consulting-framework/frameworks/decision-making/risk-matrix.md +159 -0
  14. package/.github/skills/shikigami-consulting-framework/frameworks/general/5w1h.md +152 -0
  15. package/.github/skills/shikigami-consulting-framework/frameworks/general/jtbd.md +176 -0
  16. package/.github/skills/shikigami-consulting-framework/frameworks/general/kpt.md +149 -0
  17. package/.github/skills/shikigami-consulting-framework/frameworks/general/okr.md +155 -0
  18. package/.github/skills/shikigami-consulting-framework/frameworks/general/smart.md +130 -0
  19. package/.github/skills/shikigami-consulting-framework/frameworks/innovation/aarrr.md +193 -0
  20. package/.github/skills/shikigami-consulting-framework/frameworks/innovation/business-model-canvas.md +182 -0
  21. package/.github/skills/shikigami-consulting-framework/frameworks/innovation/design-thinking.md +275 -0
  22. package/.github/skills/shikigami-consulting-framework/frameworks/innovation/lean-canvas.md +199 -0
  23. package/.github/skills/shikigami-consulting-framework/frameworks/innovation/scamper.md +188 -0
  24. package/.github/skills/shikigami-consulting-framework/frameworks/innovation/tam-sam-som.md +231 -0
  25. package/.github/skills/shikigami-consulting-framework/frameworks/innovation/value-proposition-canvas.md +194 -0
  26. package/.github/skills/shikigami-consulting-framework/frameworks/marketing/4c.md +179 -0
  27. package/.github/skills/shikigami-consulting-framework/frameworks/marketing/4p.md +161 -0
  28. package/.github/skills/shikigami-consulting-framework/frameworks/marketing/aidma-aisas.md +146 -0
  29. package/.github/skills/shikigami-consulting-framework/frameworks/marketing/customer-journey.md +155 -0
  30. package/.github/skills/shikigami-consulting-framework/frameworks/marketing/persona.md +182 -0
  31. package/.github/skills/shikigami-consulting-framework/frameworks/marketing/positioning-map.md +116 -0
  32. package/.github/skills/shikigami-consulting-framework/frameworks/marketing/stp.md +177 -0
  33. package/.github/skills/shikigami-consulting-framework/frameworks/organization/7s.md +154 -0
  34. package/.github/skills/shikigami-consulting-framework/frameworks/organization/raci.md +147 -0
  35. package/.github/skills/shikigami-consulting-framework/frameworks/problem-solving/5whys.md +142 -0
  36. package/.github/skills/shikigami-consulting-framework/frameworks/problem-solving/as-is-to-be.md +186 -0
  37. package/.github/skills/shikigami-consulting-framework/frameworks/problem-solving/fishbone.md +201 -0
  38. package/.github/skills/shikigami-consulting-framework/frameworks/problem-solving/issue-tree.md +178 -0
  39. package/.github/skills/shikigami-consulting-framework/frameworks/problem-solving/logic-tree.md +161 -0
  40. package/.github/skills/shikigami-consulting-framework/frameworks/problem-solving/mece.md +127 -0
  41. package/.github/skills/shikigami-consulting-framework/frameworks/problem-solving/sora-ame-kasa.md +176 -0
  42. package/.github/skills/shikigami-consulting-framework/frameworks/process/ecrs.md +168 -0
  43. package/.github/skills/shikigami-consulting-framework/frameworks/process/ooda.md +144 -0
  44. package/.github/skills/shikigami-consulting-framework/frameworks/process/pdca.md +113 -0
  45. package/.github/skills/shikigami-consulting-framework/frameworks/strategic/3c.md +118 -0
  46. package/.github/skills/shikigami-consulting-framework/frameworks/strategic/5forces.md +135 -0
  47. package/.github/skills/shikigami-consulting-framework/frameworks/strategic/ansoff-matrix.md +168 -0
  48. package/.github/skills/shikigami-consulting-framework/frameworks/strategic/bcg-matrix.md +134 -0
  49. package/.github/skills/shikigami-consulting-framework/frameworks/strategic/blue-ocean.md +184 -0
  50. package/.github/skills/shikigami-consulting-framework/frameworks/strategic/ge-matrix.md +158 -0
  51. package/.github/skills/shikigami-consulting-framework/frameworks/strategic/pest.md +106 -0
  52. package/.github/skills/shikigami-consulting-framework/frameworks/strategic/swot.md +90 -0
  53. package/.github/skills/shikigami-consulting-framework/frameworks/strategic/value-chain.md +192 -0
  54. package/.github/skills/shikigami-consulting-framework/frameworks/strategic/vrio.md +163 -0
  55. package/.github/skills/shikigami-consulting-framework/frameworks/thinking/prep.md +105 -0
  56. package/.github/skills/shikigami-consulting-framework/frameworks/thinking/pyramid.md +171 -0
  57. package/.github/skills/shikigami-consulting-framework/frameworks/thinking/so-what-why-so.md +175 -0
  58. package/.github/skills/shikigami-deep-research/SKILL.md +395 -0
  59. package/.github/skills/shikigami-planner/SKILL.md +267 -0
  60. package/.github/skills/shikigami-writing/SKILL.md +782 -0
  61. package/.vscode/mcp.json +9 -0
  62. package/AGENTS.md +310 -0
  63. package/CHANGELOG.md +109 -0
  64. package/README.md +144 -0
  65. package/mcp-server/README.md +80 -0
  66. package/mcp-server/package-lock.json +2123 -0
  67. package/mcp-server/package.json +38 -0
  68. package/mcp-server/shikigami.config.example.yaml +93 -0
  69. package/mcp-server/src/config/index.ts +8 -0
  70. package/mcp-server/src/config/loader.ts +246 -0
  71. package/mcp-server/src/config/types.ts +184 -0
  72. package/mcp-server/src/index.ts +418 -0
  73. package/mcp-server/src/tools/embedding.ts +279 -0
  74. package/mcp-server/src/tools/file-parser.ts +332 -0
  75. package/mcp-server/src/tools/search.ts +181 -0
  76. package/mcp-server/src/tools/visit.ts +168 -0
  77. package/mcp-server/tsconfig.json +19 -0
  78. package/package.json +82 -0
  79. package/scripts/init.js +181 -0
  80. package/scripts/postinstall.js +129 -0
@@ -0,0 +1,332 @@
1
+ /**
2
+ * File Parser Tool
3
+ *
4
+ * Implements REQ-DR-003: ファイル入力
5
+ * Implements REQ-CS-003: コード/ドキュメント解析
6
+ *
7
+ * Supports:
8
+ * - PDF (via pdf-parse or external API)
9
+ * - Text files (txt, md, json, yaml, csv)
10
+ * - Code files (ts, js, py, etc.)
11
+ *
12
+ * Note: For advanced parsing (complex PDF, Excel), use docling or external API
13
+ */
14
+
15
+ import * as fs from 'fs';
16
+ import * as path from 'path';
17
+
18
+ export interface ParsedFile {
19
+ /** ファイルパス */
20
+ filePath: string;
21
+ /** ファイル名 */
22
+ fileName: string;
23
+ /** 拡張子 */
24
+ extension: string;
25
+ /** MIMEタイプ */
26
+ mimeType: string;
27
+ /** 抽出されたテキスト */
28
+ content: string;
29
+ /** メタデータ */
30
+ metadata: FileMetadata;
31
+ /** エラー(あれば) */
32
+ error?: string;
33
+ }
34
+
35
+ export interface FileMetadata {
36
+ /** ファイルサイズ(bytes) */
37
+ size: number;
38
+ /** 最終更新日時 */
39
+ modifiedAt: string;
40
+ /** 行数(テキストファイルの場合) */
41
+ lineCount?: number;
42
+ /** 文字数 */
43
+ charCount?: number;
44
+ /** 言語(コードファイルの場合) */
45
+ language?: string;
46
+ }
47
+
48
+ /** 対応拡張子とMIMEタイプのマッピング */
49
+ const EXTENSION_MIME_MAP: Record<string, string> = {
50
+ // Text
51
+ '.txt': 'text/plain',
52
+ '.md': 'text/markdown',
53
+ '.markdown': 'text/markdown',
54
+ // Data
55
+ '.json': 'application/json',
56
+ '.yaml': 'application/yaml',
57
+ '.yml': 'application/yaml',
58
+ '.csv': 'text/csv',
59
+ '.tsv': 'text/tab-separated-values',
60
+ '.xml': 'application/xml',
61
+ // Code
62
+ '.ts': 'text/typescript',
63
+ '.tsx': 'text/typescript',
64
+ '.js': 'text/javascript',
65
+ '.jsx': 'text/javascript',
66
+ '.py': 'text/x-python',
67
+ '.rb': 'text/x-ruby',
68
+ '.go': 'text/x-go',
69
+ '.rs': 'text/x-rust',
70
+ '.java': 'text/x-java',
71
+ '.c': 'text/x-c',
72
+ '.cpp': 'text/x-c++',
73
+ '.h': 'text/x-c',
74
+ '.hpp': 'text/x-c++',
75
+ '.cs': 'text/x-csharp',
76
+ '.swift': 'text/x-swift',
77
+ '.kt': 'text/x-kotlin',
78
+ '.scala': 'text/x-scala',
79
+ '.php': 'text/x-php',
80
+ '.sh': 'text/x-shellscript',
81
+ '.bash': 'text/x-shellscript',
82
+ '.zsh': 'text/x-shellscript',
83
+ '.sql': 'text/x-sql',
84
+ // Config
85
+ '.ini': 'text/plain',
86
+ '.conf': 'text/plain',
87
+ '.cfg': 'text/plain',
88
+ '.toml': 'application/toml',
89
+ '.env': 'text/plain',
90
+ // Document
91
+ '.html': 'text/html',
92
+ '.htm': 'text/html',
93
+ '.css': 'text/css',
94
+ '.scss': 'text/x-scss',
95
+ '.less': 'text/x-less',
96
+ // Binary (will fail gracefully)
97
+ '.pdf': 'application/pdf',
98
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
99
+ '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
100
+ '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
101
+ };
102
+
103
+ /** 言語判定 */
104
+ const EXTENSION_LANGUAGE_MAP: Record<string, string> = {
105
+ '.ts': 'typescript',
106
+ '.tsx': 'typescript',
107
+ '.js': 'javascript',
108
+ '.jsx': 'javascript',
109
+ '.py': 'python',
110
+ '.rb': 'ruby',
111
+ '.go': 'go',
112
+ '.rs': 'rust',
113
+ '.java': 'java',
114
+ '.c': 'c',
115
+ '.cpp': 'c++',
116
+ '.h': 'c',
117
+ '.hpp': 'c++',
118
+ '.cs': 'csharp',
119
+ '.swift': 'swift',
120
+ '.kt': 'kotlin',
121
+ '.scala': 'scala',
122
+ '.php': 'php',
123
+ '.sh': 'shell',
124
+ '.bash': 'bash',
125
+ '.zsh': 'zsh',
126
+ '.sql': 'sql',
127
+ '.md': 'markdown',
128
+ '.json': 'json',
129
+ '.yaml': 'yaml',
130
+ '.yml': 'yaml',
131
+ '.html': 'html',
132
+ '.css': 'css',
133
+ };
134
+
135
+ /**
136
+ * テキストファイルかどうか判定
137
+ */
138
+ function isTextFile(extension: string): boolean {
139
+ const textExtensions = new Set([
140
+ '.txt', '.md', '.markdown',
141
+ '.json', '.yaml', '.yml', '.csv', '.tsv', '.xml', '.toml',
142
+ '.ts', '.tsx', '.js', '.jsx',
143
+ '.py', '.rb', '.go', '.rs', '.java',
144
+ '.c', '.cpp', '.h', '.hpp', '.cs',
145
+ '.swift', '.kt', '.scala', '.php',
146
+ '.sh', '.bash', '.zsh', '.sql',
147
+ '.ini', '.conf', '.cfg', '.env',
148
+ '.html', '.htm', '.css', '.scss', '.less',
149
+ ]);
150
+ return textExtensions.has(extension.toLowerCase());
151
+ }
152
+
153
+ /**
154
+ * ファイルを解析してテキストを抽出
155
+ */
156
+ export async function parseFile(filePath: string): Promise<ParsedFile> {
157
+ const absolutePath = path.resolve(filePath);
158
+ const fileName = path.basename(absolutePath);
159
+ const extension = path.extname(absolutePath).toLowerCase();
160
+ const mimeType = EXTENSION_MIME_MAP[extension] ?? 'application/octet-stream';
161
+
162
+ // ファイル存在確認
163
+ if (!fs.existsSync(absolutePath)) {
164
+ return {
165
+ filePath: absolutePath,
166
+ fileName,
167
+ extension,
168
+ mimeType,
169
+ content: '',
170
+ metadata: { size: 0, modifiedAt: '' },
171
+ error: `File not found: ${absolutePath}`,
172
+ };
173
+ }
174
+
175
+ // ファイル情報取得
176
+ const stat = fs.statSync(absolutePath);
177
+ const metadata: FileMetadata = {
178
+ size: stat.size,
179
+ modifiedAt: stat.mtime.toISOString(),
180
+ };
181
+
182
+ // テキストファイルの場合
183
+ if (isTextFile(extension)) {
184
+ try {
185
+ const content = fs.readFileSync(absolutePath, 'utf-8');
186
+ const lines = content.split('\n');
187
+
188
+ metadata.lineCount = lines.length;
189
+ metadata.charCount = content.length;
190
+ metadata.language = EXTENSION_LANGUAGE_MAP[extension];
191
+
192
+ // 大きすぎる場合は切り詰め
193
+ const maxLength = 100000; // 100KB
194
+ const truncatedContent = content.length > maxLength
195
+ ? content.slice(0, maxLength) + '\n\n[Content truncated...]'
196
+ : content;
197
+
198
+ return {
199
+ filePath: absolutePath,
200
+ fileName,
201
+ extension,
202
+ mimeType,
203
+ content: truncatedContent,
204
+ metadata,
205
+ };
206
+ } catch (error) {
207
+ const message = error instanceof Error ? error.message : String(error);
208
+ return {
209
+ filePath: absolutePath,
210
+ fileName,
211
+ extension,
212
+ mimeType,
213
+ content: '',
214
+ metadata,
215
+ error: `Failed to read file: ${message}`,
216
+ };
217
+ }
218
+ }
219
+
220
+ // バイナリファイル(PDF, Office等)の場合
221
+ // 注: 本格的な解析には docling や専用ライブラリが必要
222
+ return {
223
+ filePath: absolutePath,
224
+ fileName,
225
+ extension,
226
+ mimeType,
227
+ content: '',
228
+ metadata,
229
+ error: `Binary file parsing not supported for ${extension}. Use docling or external API for PDF/Office documents.`,
230
+ };
231
+ }
232
+
233
+ /**
234
+ * 複数ファイルを解析
235
+ */
236
+ export async function parseFiles(filePaths: string[]): Promise<ParsedFile[]> {
237
+ const results: ParsedFile[] = [];
238
+
239
+ for (const filePath of filePaths) {
240
+ const result = await parseFile(filePath);
241
+ results.push(result);
242
+ }
243
+
244
+ return results;
245
+ }
246
+
247
+ /**
248
+ * ディレクトリ内のファイルを再帰的に解析
249
+ */
250
+ export async function parseDirectory(
251
+ dirPath: string,
252
+ options?: {
253
+ /** 解析する拡張子(指定しない場合は全て) */
254
+ extensions?: string[];
255
+ /** 除外パターン(glob形式ではなく、パスに含まれる文字列) */
256
+ exclude?: string[];
257
+ /** 最大ファイル数 */
258
+ maxFiles?: number;
259
+ }
260
+ ): Promise<ParsedFile[]> {
261
+ const absolutePath = path.resolve(dirPath);
262
+ const results: ParsedFile[] = [];
263
+
264
+ const extensions = options?.extensions?.map(e => e.startsWith('.') ? e : `.${e}`);
265
+ const exclude = options?.exclude ?? ['node_modules', '.git', 'dist', 'build'];
266
+ const maxFiles = options?.maxFiles ?? 100;
267
+
268
+ function walk(dir: string): void {
269
+ if (results.length >= maxFiles) return;
270
+
271
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
272
+
273
+ for (const entry of entries) {
274
+ if (results.length >= maxFiles) break;
275
+
276
+ const fullPath = path.join(dir, entry.name);
277
+
278
+ // 除外パターンチェック
279
+ if (exclude.some(pattern => fullPath.includes(pattern))) {
280
+ continue;
281
+ }
282
+
283
+ if (entry.isDirectory()) {
284
+ walk(fullPath);
285
+ } else if (entry.isFile()) {
286
+ const ext = path.extname(entry.name).toLowerCase();
287
+
288
+ // 拡張子フィルタ
289
+ if (extensions && !extensions.includes(ext)) {
290
+ continue;
291
+ }
292
+
293
+ // テキストファイルのみ
294
+ if (!isTextFile(ext)) {
295
+ continue;
296
+ }
297
+
298
+ // 同期的に解析(簡易実装)
299
+ const stat = fs.statSync(fullPath);
300
+ try {
301
+ const content = fs.readFileSync(fullPath, 'utf-8');
302
+ const lines = content.split('\n');
303
+
304
+ const maxLength = 100000;
305
+ const truncatedContent = content.length > maxLength
306
+ ? content.slice(0, maxLength) + '\n\n[Content truncated...]'
307
+ : content;
308
+
309
+ results.push({
310
+ filePath: fullPath,
311
+ fileName: entry.name,
312
+ extension: ext,
313
+ mimeType: EXTENSION_MIME_MAP[ext] ?? 'text/plain',
314
+ content: truncatedContent,
315
+ metadata: {
316
+ size: stat.size,
317
+ modifiedAt: stat.mtime.toISOString(),
318
+ lineCount: lines.length,
319
+ charCount: content.length,
320
+ language: EXTENSION_LANGUAGE_MAP[ext],
321
+ },
322
+ });
323
+ } catch {
324
+ // エラー時はスキップ
325
+ }
326
+ }
327
+ }
328
+ }
329
+
330
+ walk(absolutePath);
331
+ return results;
332
+ }
@@ -0,0 +1,181 @@
1
+ /**
2
+ * DuckDuckGo Search Tool
3
+ *
4
+ * Implements REQ-DR-002: Web検索
5
+ * Implements REQ-ERR-001: Web検索失敗時のリトライ(最大3回、指数バックオフ)
6
+ * Implements REQ-NF-007: プロバイダー設定ファイル対応
7
+ * Uses DuckDuckGo HTML search (no API key required)
8
+ */
9
+
10
+ import * as cheerio from 'cheerio';
11
+ import { getConfig } from '../config/loader.js';
12
+
13
+ export interface SearchResult {
14
+ title: string;
15
+ url: string;
16
+ snippet: string;
17
+ }
18
+
19
+ export interface SearchError {
20
+ query: string;
21
+ error: string;
22
+ retries: number;
23
+ lastStatusCode?: number;
24
+ }
25
+
26
+ const USER_AGENT =
27
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
28
+
29
+ // Rate limiting (will be initialized from config)
30
+ let lastRequestTime = 0;
31
+
32
+ /**
33
+ * Get rate limit interval from config
34
+ */
35
+ function getMinRequestInterval(): number {
36
+ const config = getConfig();
37
+ return config.search?.options?.rateLimitMs ?? 1500;
38
+ }
39
+
40
+ /**
41
+ * Get max retries from config
42
+ */
43
+ function getMaxRetries(): number {
44
+ const config = getConfig();
45
+ return config.search?.options?.maxRetries ?? 3;
46
+ }
47
+
48
+ /**
49
+ * Get base backoff from config (default 2000ms)
50
+ */
51
+ function getBaseBackoff(): number {
52
+ return 2000;
53
+ }
54
+
55
+ async function rateLimit(): Promise<void> {
56
+ const now = Date.now();
57
+ const minInterval = getMinRequestInterval();
58
+ const timeSinceLastRequest = now - lastRequestTime;
59
+
60
+ if (timeSinceLastRequest < minInterval) {
61
+ await new Promise((resolve) =>
62
+ setTimeout(resolve, minInterval - timeSinceLastRequest)
63
+ );
64
+ }
65
+
66
+ lastRequestTime = Date.now();
67
+ }
68
+
69
+ /**
70
+ * Sleep for exponential backoff
71
+ */
72
+ async function exponentialBackoff(attempt: number): Promise<void> {
73
+ const baseBackoff = getBaseBackoff();
74
+ const maxRetries = getMaxRetries();
75
+ const delay = baseBackoff * Math.pow(2, attempt);
76
+ console.error(`[SHIKIGAMI] Retry ${attempt + 1}/${maxRetries} - waiting ${delay}ms`);
77
+ await new Promise((resolve) => setTimeout(resolve, delay));
78
+ }
79
+
80
+ /**
81
+ * Search DuckDuckGo with retry logic (REQ-ERR-001)
82
+ */
83
+ export async function searchDuckDuckGo(
84
+ query: string,
85
+ maxResults: number = 10
86
+ ): Promise<SearchResult[]> {
87
+ const config = getConfig();
88
+ const maxRetries = getMaxRetries();
89
+ const locale = config.search?.options?.locale ?? 'ja,en-US;q=0.9,en;q=0.8';
90
+
91
+ let lastError: Error | null = null;
92
+ let lastStatusCode: number | undefined;
93
+
94
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
95
+ try {
96
+ await rateLimit();
97
+
98
+ const encodedQuery = encodeURIComponent(query);
99
+ const url = `https://html.duckduckgo.com/html/?q=${encodedQuery}`;
100
+
101
+ const response = await fetch(url, {
102
+ headers: {
103
+ 'User-Agent': USER_AGENT,
104
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
105
+ 'Accept-Language': locale,
106
+ },
107
+ });
108
+
109
+ lastStatusCode = response.status;
110
+
111
+ // Handle rate limiting (429) with exponential backoff
112
+ if (response.status === 429) {
113
+ console.error(`[SHIKIGAMI] Rate limited (429) for query: "${query}"`);
114
+ if (attempt < maxRetries - 1) {
115
+ await exponentialBackoff(attempt);
116
+ continue;
117
+ }
118
+ throw new Error(`Rate limited after ${maxRetries} retries`);
119
+ }
120
+
121
+ if (!response.ok) {
122
+ throw new Error(`DuckDuckGo search failed: ${response.status}`);
123
+ }
124
+
125
+ const html = await response.text();
126
+ const $ = cheerio.load(html);
127
+ const results: SearchResult[] = [];
128
+
129
+ // Parse search results
130
+ $('.result').each((index, element) => {
131
+ if (index >= maxResults) return false;
132
+
133
+ const $el = $(element);
134
+ const titleEl = $el.find('.result__title a');
135
+ const snippetEl = $el.find('.result__snippet');
136
+
137
+ const title = titleEl.text().trim();
138
+ let resultUrl = titleEl.attr('href') || '';
139
+ const snippet = snippetEl.text().trim();
140
+
141
+ // DuckDuckGo uses redirect URLs, extract the actual URL
142
+ if (resultUrl.includes('uddg=')) {
143
+ const match = resultUrl.match(/uddg=([^&]+)/);
144
+ if (match) {
145
+ resultUrl = decodeURIComponent(match[1]);
146
+ }
147
+ }
148
+
149
+ if (title && resultUrl) {
150
+ results.push({
151
+ title,
152
+ url: resultUrl,
153
+ snippet,
154
+ });
155
+ }
156
+ });
157
+
158
+ // Success - log and return
159
+ if (attempt > 0) {
160
+ console.error(`[SHIKIGAMI] Search succeeded after ${attempt + 1} attempts`);
161
+ }
162
+ return results;
163
+
164
+ } catch (error) {
165
+ lastError = error instanceof Error ? error : new Error(String(error));
166
+ console.error(`[SHIKIGAMI] Search attempt ${attempt + 1} failed: ${lastError.message}`);
167
+
168
+ // Retry on network errors
169
+ if (attempt < maxRetries - 1) {
170
+ await exponentialBackoff(attempt);
171
+ }
172
+ }
173
+ }
174
+
175
+ // All retries exhausted - log and throw
176
+ console.error(`[SHIKIGAMI] Search failed after ${maxRetries} retries for query: "${query}"`);
177
+ throw new Error(
178
+ `Search failed after ${maxRetries} retries: ${lastError?.message || 'Unknown error'}` +
179
+ (lastStatusCode ? ` (last status: ${lastStatusCode})` : '')
180
+ );
181
+ }
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Page Visit Tool
3
+ *
4
+ * Implements REQ-DR-002: Web検索 (page fetching)
5
+ * Implements REQ-NF-007: プロバイダー設定ファイル対応
6
+ * Uses Jina AI Reader for LLM-optimized text extraction
7
+ */
8
+
9
+ import { getConfig } from '../config/loader.js';
10
+
11
+ export interface PageContent {
12
+ url: string;
13
+ title: string;
14
+ content: string;
15
+ fetchedAt: string;
16
+ error?: string;
17
+ }
18
+
19
+ const USER_AGENT =
20
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
21
+
22
+ // Rate limiting (will be initialized from config)
23
+ let lastRequestTime = 0;
24
+
25
+ /**
26
+ * Get rate limit interval from config
27
+ */
28
+ function getMinRequestInterval(): number {
29
+ const config = getConfig();
30
+ return config.pageFetcher?.options?.rateLimitMs ?? 1000;
31
+ }
32
+
33
+ /**
34
+ * Get Jina API key from config
35
+ */
36
+ function getJinaApiKey(): string | undefined {
37
+ const config = getConfig();
38
+ return config.pageFetcher?.options?.apiKey;
39
+ }
40
+
41
+ async function rateLimit(): Promise<void> {
42
+ const now = Date.now();
43
+ const minInterval = getMinRequestInterval();
44
+ const timeSinceLastRequest = now - lastRequestTime;
45
+
46
+ if (timeSinceLastRequest < minInterval) {
47
+ await new Promise((resolve) =>
48
+ setTimeout(resolve, minInterval - timeSinceLastRequest)
49
+ );
50
+ }
51
+
52
+ lastRequestTime = Date.now();
53
+ }
54
+
55
+ /**
56
+ * Visit a page and extract content using Jina AI Reader
57
+ */
58
+ export async function visitPage(url: string, goal?: string): Promise<PageContent> {
59
+ await rateLimit();
60
+
61
+ const fetchedAt = new Date().toISOString();
62
+ const apiKey = getJinaApiKey();
63
+
64
+ try {
65
+ // Use Jina AI Reader for clean text extraction
66
+ const jinaUrl = `https://r.jina.ai/${url}`;
67
+
68
+ const headers: Record<string, string> = {
69
+ 'User-Agent': USER_AGENT,
70
+ 'Accept': 'text/plain',
71
+ };
72
+
73
+ // Add API key if configured (for higher rate limits)
74
+ if (apiKey) {
75
+ headers['Authorization'] = `Bearer ${apiKey}`;
76
+ }
77
+
78
+ const response = await fetch(jinaUrl, { headers });
79
+
80
+ if (!response.ok) {
81
+ // Fallback to direct fetch if Jina fails
82
+ return await directFetch(url, fetchedAt);
83
+ }
84
+
85
+ const content = await response.text();
86
+
87
+ // Extract title from the first line (Jina format)
88
+ const lines = content.split('\n');
89
+ let title = '';
90
+ let bodyContent = content;
91
+
92
+ if (lines[0]?.startsWith('Title:')) {
93
+ title = lines[0].replace('Title:', '').trim();
94
+ bodyContent = lines.slice(1).join('\n').trim();
95
+ }
96
+
97
+ // Truncate if too long (for context management)
98
+ const maxLength = 50000;
99
+ if (bodyContent.length > maxLength) {
100
+ bodyContent = bodyContent.slice(0, maxLength) + '\n\n[Content truncated...]';
101
+ }
102
+
103
+ return {
104
+ url,
105
+ title,
106
+ content: bodyContent,
107
+ fetchedAt,
108
+ };
109
+ } catch (error) {
110
+ const message = error instanceof Error ? error.message : String(error);
111
+ return {
112
+ url,
113
+ title: '',
114
+ content: '',
115
+ fetchedAt,
116
+ error: `Failed to fetch page: ${message}`,
117
+ };
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Direct fetch fallback (basic HTML text extraction)
123
+ */
124
+ async function directFetch(url: string, fetchedAt: string): Promise<PageContent> {
125
+ try {
126
+ const response = await fetch(url, {
127
+ headers: {
128
+ 'User-Agent': USER_AGENT,
129
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
130
+ },
131
+ });
132
+
133
+ if (!response.ok) {
134
+ throw new Error(`HTTP ${response.status}`);
135
+ }
136
+
137
+ const html = await response.text();
138
+
139
+ // Basic HTML to text conversion
140
+ const { load } = await import('cheerio');
141
+ const $ = load(html);
142
+
143
+ // Remove scripts and styles
144
+ $('script, style, nav, header, footer, aside').remove();
145
+
146
+ const title = $('title').text().trim() || $('h1').first().text().trim();
147
+ const content = $('body').text()
148
+ .replace(/\s+/g, ' ')
149
+ .trim()
150
+ .slice(0, 50000);
151
+
152
+ return {
153
+ url,
154
+ title,
155
+ content,
156
+ fetchedAt,
157
+ };
158
+ } catch (error) {
159
+ const message = error instanceof Error ? error.message : String(error);
160
+ return {
161
+ url,
162
+ title: '',
163
+ content: '',
164
+ fetchedAt,
165
+ error: `Direct fetch failed: ${message}`,
166
+ };
167
+ }
168
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": ["ES2022"],
7
+ "outDir": "./dist",
8
+ "rootDir": "./src",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "declaration": true,
14
+ "declarationMap": true,
15
+ "sourceMap": true
16
+ },
17
+ "include": ["src/**/*"],
18
+ "exclude": ["node_modules", "dist"]
19
+ }