@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.
- package/.github/prompts/shikigami-deep-research.prompt.md +32 -0
- package/.github/prompts/shikigami-framework-analysis.prompt.md +40 -0
- package/.github/prompts/shikigami-full-research.prompt.md +54 -0
- package/.github/prompts/shikigami-purpose-discovery.prompt.md +32 -0
- package/.github/prompts/shikigami-report-writing.prompt.md +36 -0
- package/.github/skills/shikigami-consulting-framework/SKILL.md +403 -0
- package/.github/skills/shikigami-consulting-framework/frameworks/README.md +173 -0
- package/.github/skills/shikigami-consulting-framework/frameworks/customer/nps.md +164 -0
- package/.github/skills/shikigami-consulting-framework/frameworks/customer/rfm.md +160 -0
- package/.github/skills/shikigami-consulting-framework/frameworks/decision-making/cost-benefit.md +168 -0
- package/.github/skills/shikigami-consulting-framework/frameworks/decision-making/decision-matrix.md +138 -0
- package/.github/skills/shikigami-consulting-framework/frameworks/decision-making/pros-cons.md +162 -0
- package/.github/skills/shikigami-consulting-framework/frameworks/decision-making/risk-matrix.md +159 -0
- package/.github/skills/shikigami-consulting-framework/frameworks/general/5w1h.md +152 -0
- package/.github/skills/shikigami-consulting-framework/frameworks/general/jtbd.md +176 -0
- package/.github/skills/shikigami-consulting-framework/frameworks/general/kpt.md +149 -0
- package/.github/skills/shikigami-consulting-framework/frameworks/general/okr.md +155 -0
- package/.github/skills/shikigami-consulting-framework/frameworks/general/smart.md +130 -0
- package/.github/skills/shikigami-consulting-framework/frameworks/innovation/aarrr.md +193 -0
- package/.github/skills/shikigami-consulting-framework/frameworks/innovation/business-model-canvas.md +182 -0
- package/.github/skills/shikigami-consulting-framework/frameworks/innovation/design-thinking.md +275 -0
- package/.github/skills/shikigami-consulting-framework/frameworks/innovation/lean-canvas.md +199 -0
- package/.github/skills/shikigami-consulting-framework/frameworks/innovation/scamper.md +188 -0
- package/.github/skills/shikigami-consulting-framework/frameworks/innovation/tam-sam-som.md +231 -0
- package/.github/skills/shikigami-consulting-framework/frameworks/innovation/value-proposition-canvas.md +194 -0
- package/.github/skills/shikigami-consulting-framework/frameworks/marketing/4c.md +179 -0
- package/.github/skills/shikigami-consulting-framework/frameworks/marketing/4p.md +161 -0
- package/.github/skills/shikigami-consulting-framework/frameworks/marketing/aidma-aisas.md +146 -0
- package/.github/skills/shikigami-consulting-framework/frameworks/marketing/customer-journey.md +155 -0
- package/.github/skills/shikigami-consulting-framework/frameworks/marketing/persona.md +182 -0
- package/.github/skills/shikigami-consulting-framework/frameworks/marketing/positioning-map.md +116 -0
- package/.github/skills/shikigami-consulting-framework/frameworks/marketing/stp.md +177 -0
- package/.github/skills/shikigami-consulting-framework/frameworks/organization/7s.md +154 -0
- package/.github/skills/shikigami-consulting-framework/frameworks/organization/raci.md +147 -0
- package/.github/skills/shikigami-consulting-framework/frameworks/problem-solving/5whys.md +142 -0
- package/.github/skills/shikigami-consulting-framework/frameworks/problem-solving/as-is-to-be.md +186 -0
- package/.github/skills/shikigami-consulting-framework/frameworks/problem-solving/fishbone.md +201 -0
- package/.github/skills/shikigami-consulting-framework/frameworks/problem-solving/issue-tree.md +178 -0
- package/.github/skills/shikigami-consulting-framework/frameworks/problem-solving/logic-tree.md +161 -0
- package/.github/skills/shikigami-consulting-framework/frameworks/problem-solving/mece.md +127 -0
- package/.github/skills/shikigami-consulting-framework/frameworks/problem-solving/sora-ame-kasa.md +176 -0
- package/.github/skills/shikigami-consulting-framework/frameworks/process/ecrs.md +168 -0
- package/.github/skills/shikigami-consulting-framework/frameworks/process/ooda.md +144 -0
- package/.github/skills/shikigami-consulting-framework/frameworks/process/pdca.md +113 -0
- package/.github/skills/shikigami-consulting-framework/frameworks/strategic/3c.md +118 -0
- package/.github/skills/shikigami-consulting-framework/frameworks/strategic/5forces.md +135 -0
- package/.github/skills/shikigami-consulting-framework/frameworks/strategic/ansoff-matrix.md +168 -0
- package/.github/skills/shikigami-consulting-framework/frameworks/strategic/bcg-matrix.md +134 -0
- package/.github/skills/shikigami-consulting-framework/frameworks/strategic/blue-ocean.md +184 -0
- package/.github/skills/shikigami-consulting-framework/frameworks/strategic/ge-matrix.md +158 -0
- package/.github/skills/shikigami-consulting-framework/frameworks/strategic/pest.md +106 -0
- package/.github/skills/shikigami-consulting-framework/frameworks/strategic/swot.md +90 -0
- package/.github/skills/shikigami-consulting-framework/frameworks/strategic/value-chain.md +192 -0
- package/.github/skills/shikigami-consulting-framework/frameworks/strategic/vrio.md +163 -0
- package/.github/skills/shikigami-consulting-framework/frameworks/thinking/prep.md +105 -0
- package/.github/skills/shikigami-consulting-framework/frameworks/thinking/pyramid.md +171 -0
- package/.github/skills/shikigami-consulting-framework/frameworks/thinking/so-what-why-so.md +175 -0
- package/.github/skills/shikigami-deep-research/SKILL.md +395 -0
- package/.github/skills/shikigami-planner/SKILL.md +267 -0
- package/.github/skills/shikigami-writing/SKILL.md +782 -0
- package/.vscode/mcp.json +9 -0
- package/AGENTS.md +310 -0
- package/CHANGELOG.md +109 -0
- package/README.md +144 -0
- package/mcp-server/README.md +80 -0
- package/mcp-server/package-lock.json +2123 -0
- package/mcp-server/package.json +38 -0
- package/mcp-server/shikigami.config.example.yaml +93 -0
- package/mcp-server/src/config/index.ts +8 -0
- package/mcp-server/src/config/loader.ts +246 -0
- package/mcp-server/src/config/types.ts +184 -0
- package/mcp-server/src/index.ts +418 -0
- package/mcp-server/src/tools/embedding.ts +279 -0
- package/mcp-server/src/tools/file-parser.ts +332 -0
- package/mcp-server/src/tools/search.ts +181 -0
- package/mcp-server/src/tools/visit.ts +168 -0
- package/mcp-server/tsconfig.json +19 -0
- package/package.json +82 -0
- package/scripts/init.js +181 -0
- 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
|
+
}
|