@sylphx/flow 1.0.1 → 1.0.3
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/CHANGELOG.md +12 -0
- package/package.json +10 -9
- package/src/commands/codebase-command.ts +168 -0
- package/src/commands/flow-command.ts +1137 -0
- package/src/commands/flow-orchestrator.ts +296 -0
- package/src/commands/hook-command.ts +444 -0
- package/src/commands/init-command.ts +92 -0
- package/src/commands/init-core.ts +322 -0
- package/src/commands/knowledge-command.ts +161 -0
- package/src/commands/run-command.ts +120 -0
- package/src/components/benchmark-monitor.tsx +331 -0
- package/src/components/reindex-progress.tsx +261 -0
- package/src/composables/functional/index.ts +14 -0
- package/src/composables/functional/useEnvironment.ts +171 -0
- package/src/composables/functional/useFileSystem.ts +139 -0
- package/src/composables/index.ts +5 -0
- package/src/composables/useEnv.ts +13 -0
- package/src/composables/useRuntimeConfig.ts +27 -0
- package/src/composables/useTargetConfig.ts +45 -0
- package/src/config/ai-config.ts +376 -0
- package/src/config/constants.ts +35 -0
- package/src/config/index.ts +27 -0
- package/src/config/rules.ts +43 -0
- package/src/config/servers.ts +371 -0
- package/src/config/targets.ts +126 -0
- package/src/core/agent-loader.ts +141 -0
- package/src/core/agent-manager.ts +174 -0
- package/src/core/ai-sdk.ts +603 -0
- package/src/core/app-factory.ts +381 -0
- package/src/core/builtin-agents.ts +9 -0
- package/src/core/command-system.ts +550 -0
- package/src/core/config-system.ts +550 -0
- package/src/core/connection-pool.ts +390 -0
- package/src/core/di-container.ts +155 -0
- package/src/core/error-handling.ts +519 -0
- package/src/core/formatting/bytes.test.ts +115 -0
- package/src/core/formatting/bytes.ts +64 -0
- package/src/core/functional/async.ts +313 -0
- package/src/core/functional/either.ts +109 -0
- package/src/core/functional/error-handler.ts +135 -0
- package/src/core/functional/error-types.ts +311 -0
- package/src/core/functional/index.ts +19 -0
- package/src/core/functional/option.ts +142 -0
- package/src/core/functional/pipe.ts +189 -0
- package/src/core/functional/result.ts +204 -0
- package/src/core/functional/validation.ts +138 -0
- package/src/core/headless-display.ts +96 -0
- package/src/core/index.ts +6 -0
- package/src/core/installers/file-installer.ts +303 -0
- package/src/core/installers/mcp-installer.ts +213 -0
- package/src/core/interfaces/index.ts +22 -0
- package/src/core/interfaces/repository.interface.ts +91 -0
- package/src/core/interfaces/service.interface.ts +133 -0
- package/src/core/interfaces.ts +129 -0
- package/src/core/loop-controller.ts +200 -0
- package/src/core/result.ts +351 -0
- package/src/core/rule-loader.ts +147 -0
- package/src/core/rule-manager.ts +240 -0
- package/src/core/service-config.ts +252 -0
- package/src/core/session-service.ts +121 -0
- package/src/core/state-detector.ts +389 -0
- package/src/core/storage-factory.ts +115 -0
- package/src/core/stream-handler.ts +288 -0
- package/src/core/target-manager.ts +161 -0
- package/src/core/type-utils.ts +427 -0
- package/src/core/unified-storage.ts +456 -0
- package/src/core/upgrade-manager.ts +300 -0
- package/src/core/validation/limit.test.ts +155 -0
- package/src/core/validation/limit.ts +46 -0
- package/src/core/validation/query.test.ts +44 -0
- package/src/core/validation/query.ts +20 -0
- package/src/db/auto-migrate.ts +322 -0
- package/src/db/base-database-client.ts +144 -0
- package/src/db/cache-db.ts +218 -0
- package/src/db/cache-schema.ts +75 -0
- package/src/db/database.ts +70 -0
- package/src/db/index.ts +252 -0
- package/src/db/memory-db.ts +153 -0
- package/src/db/memory-schema.ts +29 -0
- package/src/db/schema.ts +289 -0
- package/src/db/session-repository.ts +733 -0
- package/src/domains/codebase/index.ts +5 -0
- package/src/domains/codebase/tools.ts +139 -0
- package/src/domains/index.ts +8 -0
- package/src/domains/knowledge/index.ts +10 -0
- package/src/domains/knowledge/resources.ts +537 -0
- package/src/domains/knowledge/tools.ts +174 -0
- package/src/domains/utilities/index.ts +6 -0
- package/src/domains/utilities/time/index.ts +5 -0
- package/src/domains/utilities/time/tools.ts +291 -0
- package/src/index.ts +211 -0
- package/src/services/agent-service.ts +273 -0
- package/src/services/claude-config-service.ts +252 -0
- package/src/services/config-service.ts +258 -0
- package/src/services/evaluation-service.ts +271 -0
- package/src/services/functional/evaluation-logic.ts +296 -0
- package/src/services/functional/file-processor.ts +273 -0
- package/src/services/functional/index.ts +12 -0
- package/src/services/index.ts +13 -0
- package/src/services/mcp-service.ts +432 -0
- package/src/services/memory.service.ts +476 -0
- package/src/services/search/base-indexer.ts +156 -0
- package/src/services/search/codebase-indexer-types.ts +38 -0
- package/src/services/search/codebase-indexer.ts +647 -0
- package/src/services/search/embeddings-provider.ts +455 -0
- package/src/services/search/embeddings.ts +316 -0
- package/src/services/search/functional-indexer.ts +323 -0
- package/src/services/search/index.ts +27 -0
- package/src/services/search/indexer.ts +380 -0
- package/src/services/search/knowledge-indexer.ts +422 -0
- package/src/services/search/semantic-search.ts +244 -0
- package/src/services/search/tfidf.ts +559 -0
- package/src/services/search/unified-search-service.ts +888 -0
- package/src/services/smart-config-service.ts +385 -0
- package/src/services/storage/cache-storage.ts +487 -0
- package/src/services/storage/drizzle-storage.ts +581 -0
- package/src/services/storage/index.ts +15 -0
- package/src/services/storage/lancedb-vector-storage.ts +494 -0
- package/src/services/storage/memory-storage.ts +268 -0
- package/src/services/storage/separated-storage.ts +467 -0
- package/src/services/storage/vector-storage.ts +13 -0
- package/src/shared/agents/index.ts +63 -0
- package/src/shared/files/index.ts +99 -0
- package/src/shared/index.ts +32 -0
- package/src/shared/logging/index.ts +24 -0
- package/src/shared/processing/index.ts +153 -0
- package/src/shared/types/index.ts +25 -0
- package/src/targets/claude-code.ts +574 -0
- package/src/targets/functional/claude-code-logic.ts +185 -0
- package/src/targets/functional/index.ts +6 -0
- package/src/targets/opencode.ts +529 -0
- package/src/types/agent.types.ts +32 -0
- package/src/types/api/batch.ts +108 -0
- package/src/types/api/errors.ts +118 -0
- package/src/types/api/index.ts +55 -0
- package/src/types/api/requests.ts +76 -0
- package/src/types/api/responses.ts +180 -0
- package/src/types/api/websockets.ts +85 -0
- package/src/types/api.types.ts +9 -0
- package/src/types/benchmark.ts +49 -0
- package/src/types/cli.types.ts +87 -0
- package/src/types/common.types.ts +35 -0
- package/src/types/database.types.ts +510 -0
- package/src/types/mcp-config.types.ts +448 -0
- package/src/types/mcp.types.ts +69 -0
- package/src/types/memory-types.ts +63 -0
- package/src/types/provider.types.ts +28 -0
- package/src/types/rule.types.ts +24 -0
- package/src/types/session.types.ts +214 -0
- package/src/types/target-config.types.ts +295 -0
- package/src/types/target.types.ts +140 -0
- package/src/types/todo.types.ts +25 -0
- package/src/types.ts +40 -0
- package/src/utils/advanced-tokenizer.ts +191 -0
- package/src/utils/agent-enhancer.ts +114 -0
- package/src/utils/ai-model-fetcher.ts +19 -0
- package/src/utils/async-file-operations.ts +516 -0
- package/src/utils/audio-player.ts +345 -0
- package/src/utils/cli-output.ts +266 -0
- package/src/utils/codebase-helpers.ts +211 -0
- package/src/utils/console-ui.ts +79 -0
- package/src/utils/database-errors.ts +140 -0
- package/src/utils/debug-logger.ts +49 -0
- package/src/utils/error-handler.ts +53 -0
- package/src/utils/file-operations.ts +310 -0
- package/src/utils/file-scanner.ts +259 -0
- package/src/utils/functional/array.ts +355 -0
- package/src/utils/functional/index.ts +15 -0
- package/src/utils/functional/object.ts +279 -0
- package/src/utils/functional/string.ts +281 -0
- package/src/utils/functional.ts +543 -0
- package/src/utils/help.ts +20 -0
- package/src/utils/immutable-cache.ts +106 -0
- package/src/utils/index.ts +78 -0
- package/src/utils/jsonc.ts +158 -0
- package/src/utils/logger.ts +396 -0
- package/src/utils/mcp-config.ts +249 -0
- package/src/utils/memory-tui.ts +414 -0
- package/src/utils/models-dev.ts +91 -0
- package/src/utils/notifications.ts +169 -0
- package/src/utils/object-utils.ts +51 -0
- package/src/utils/parallel-operations.ts +487 -0
- package/src/utils/paths.ts +143 -0
- package/src/utils/process-manager.ts +155 -0
- package/src/utils/prompts.ts +120 -0
- package/src/utils/search-tool-builder.ts +214 -0
- package/src/utils/secret-utils.ts +179 -0
- package/src/utils/security.ts +537 -0
- package/src/utils/session-manager.ts +168 -0
- package/src/utils/session-title.ts +87 -0
- package/src/utils/settings.ts +182 -0
- package/src/utils/simplified-errors.ts +410 -0
- package/src/utils/sync-utils.ts +159 -0
- package/src/utils/target-config.ts +570 -0
- package/src/utils/target-utils.ts +394 -0
- package/src/utils/template-engine.ts +94 -0
- package/src/utils/test-audio.ts +71 -0
- package/src/utils/todo-context.ts +46 -0
- package/src/utils/token-counter.ts +288 -0
- package/dist/index.d.ts +0 -10
- package/dist/index.js +0 -59554
- package/dist/lancedb.linux-x64-gnu-b7f0jgsz.node +0 -0
- package/dist/lancedb.linux-x64-musl-tgcv22rx.node +0 -0
- package/dist/shared/chunk-25dwp0dp.js +0 -89
- package/dist/shared/chunk-3pjb6063.js +0 -208
- package/dist/shared/chunk-4d6ydpw7.js +0 -2854
- package/dist/shared/chunk-4wjcadjk.js +0 -225
- package/dist/shared/chunk-5j4w74t6.js +0 -30
- package/dist/shared/chunk-5j8m3dh3.js +0 -58
- package/dist/shared/chunk-5thh3qem.js +0 -91
- package/dist/shared/chunk-6g9xy73m.js +0 -252
- package/dist/shared/chunk-7eq34c42.js +0 -23
- package/dist/shared/chunk-c2gwgx3r.js +0 -115
- package/dist/shared/chunk-cjd3mk4c.js +0 -1320
- package/dist/shared/chunk-g5cv6703.js +0 -368
- package/dist/shared/chunk-hpkhykhq.js +0 -574
- package/dist/shared/chunk-m2322pdk.js +0 -122
- package/dist/shared/chunk-nd5fdvaq.js +0 -26
- package/dist/shared/chunk-pgd3m6zf.js +0 -108
- package/dist/shared/chunk-qk8n91hw.js +0 -494
- package/dist/shared/chunk-rkkn8szp.js +0 -16855
- package/dist/shared/chunk-t16rfxh0.js +0 -61
- package/dist/shared/chunk-t4fbfa5v.js +0 -19
- package/dist/shared/chunk-t77h86w6.js +0 -276
- package/dist/shared/chunk-v0ez4aef.js +0 -71
- package/dist/shared/chunk-v29j2r3s.js +0 -32051
- package/dist/shared/chunk-vfbc6ew5.js +0 -765
- package/dist/shared/chunk-vmeqwm1c.js +0 -204
- package/dist/shared/chunk-x66eh37x.js +0 -137
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codebase tools
|
|
3
|
+
* All tools for working with project source code and files
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { getSearchService } from '../../services/search/unified-search-service.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Register codebase search tool
|
|
12
|
+
*/
|
|
13
|
+
export function registerCodebaseSearchTool(server: McpServer): void {
|
|
14
|
+
server.registerTool(
|
|
15
|
+
'codebase_search',
|
|
16
|
+
{
|
|
17
|
+
description: `Search project source files, documentation, and code. Use this to find implementations, functions, classes, or any code-related content.
|
|
18
|
+
|
|
19
|
+
**IMPORTANT: Use this tool PROACTIVELY before starting work, not reactively when stuck.**
|
|
20
|
+
|
|
21
|
+
This tool searches across all codebase files and returns the most relevant matches with content snippets.
|
|
22
|
+
|
|
23
|
+
When to use this tool (BEFORE starting work):
|
|
24
|
+
- **Before implementation**: Find existing patterns, similar functions, or reusable components
|
|
25
|
+
- **Before refactoring**: Understand current implementation and dependencies
|
|
26
|
+
- **Before adding features**: Check for existing similar functionality or conflicting code
|
|
27
|
+
- **Before debugging**: Search for error messages, function names, or related code
|
|
28
|
+
- **Before writing tests**: Find existing test patterns and test utilities
|
|
29
|
+
|
|
30
|
+
The search includes:
|
|
31
|
+
- Source code files (.ts, .js, .tsx, .jsx, etc.)
|
|
32
|
+
- Configuration files (.json, .yaml, .toml, etc.)
|
|
33
|
+
- Documentation files (.md, .txt, etc.)
|
|
34
|
+
- Build and deployment files
|
|
35
|
+
|
|
36
|
+
**Best Practice**: Search the codebase BEFORE writing new code to avoid duplication and follow existing patterns.`,
|
|
37
|
+
inputSchema: {
|
|
38
|
+
query: z
|
|
39
|
+
.string()
|
|
40
|
+
.describe('Search query - use natural language, function names, or technical terms'),
|
|
41
|
+
limit: z
|
|
42
|
+
.number()
|
|
43
|
+
.default(10)
|
|
44
|
+
.optional()
|
|
45
|
+
.describe('Maximum number of results to return (default: 10)'),
|
|
46
|
+
include_content: z
|
|
47
|
+
.boolean()
|
|
48
|
+
.default(true)
|
|
49
|
+
.optional()
|
|
50
|
+
.describe('Include file content snippets in results (default: true)'),
|
|
51
|
+
file_extensions: z
|
|
52
|
+
.array(z.string())
|
|
53
|
+
.optional()
|
|
54
|
+
.describe('Filter by file extensions (e.g., [".ts", ".tsx", ".js"])'),
|
|
55
|
+
path_filter: z
|
|
56
|
+
.string()
|
|
57
|
+
.optional()
|
|
58
|
+
.describe('Filter by path pattern (e.g., "src/components", "tests", "docs")'),
|
|
59
|
+
exclude_paths: z
|
|
60
|
+
.array(z.string())
|
|
61
|
+
.optional()
|
|
62
|
+
.describe(
|
|
63
|
+
'Exclude paths containing these patterns (e.g., ["node_modules", ".git", "dist"])'
|
|
64
|
+
),
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
async ({
|
|
68
|
+
query,
|
|
69
|
+
limit = 10,
|
|
70
|
+
include_content = true,
|
|
71
|
+
file_extensions,
|
|
72
|
+
path_filter,
|
|
73
|
+
exclude_paths,
|
|
74
|
+
}) => {
|
|
75
|
+
try {
|
|
76
|
+
// Use UnifiedSearchService - same logic as CLI
|
|
77
|
+
const searchService = getSearchService();
|
|
78
|
+
await searchService.initialize();
|
|
79
|
+
|
|
80
|
+
// Check codebase status
|
|
81
|
+
const status = await searchService.getStatus();
|
|
82
|
+
|
|
83
|
+
// If indexing in progress, show progress
|
|
84
|
+
if (status.codebase.isIndexing) {
|
|
85
|
+
const progressBar =
|
|
86
|
+
'█'.repeat(Math.floor(status.codebase.progress / 5)) +
|
|
87
|
+
'░'.repeat(20 - Math.floor(status.codebase.progress / 5));
|
|
88
|
+
return {
|
|
89
|
+
content: [
|
|
90
|
+
{
|
|
91
|
+
type: 'text',
|
|
92
|
+
text: `⏳ **Codebase Indexing In Progress**\n\nThe codebase is currently being indexed. Please wait...\n\n**Progress:** ${status.codebase.progress}%\n\`${progressBar}\`\n\n**Status:**\n- Files indexed: ${status.codebase.progress > 0 ? Math.floor((status.codebase.fileCount * status.codebase.progress) / 100) : 0}/${status.codebase.fileCount}\n${status.codebase.currentFile ? `- Current file: \`${status.codebase.currentFile}\`` : ''}\n\n**Estimated time:** ${status.codebase.progress > 0 ? 'Less than 1 minute' : 'Starting...'}\n\n💡 **Tip:** Try your search again in a few seconds.`,
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!status.codebase.indexed) {
|
|
99
|
+
return {
|
|
100
|
+
content: [
|
|
101
|
+
{
|
|
102
|
+
type: 'text',
|
|
103
|
+
text: `📭 **Codebase Not Indexed**\n\nThe codebase has not been indexed yet.\n\n**To fix:**\n- Run: \`sylphx codebase reindex\` from the command line\n- This will create a search index for all source files\n\n**Why this is needed:**\nThe first time you use codebase search, you need to build an index of all files. This only needs to be done once (or when files change significantly).`,
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Perform search using unified service
|
|
110
|
+
const result = await searchService.searchCodebase(query, {
|
|
111
|
+
limit,
|
|
112
|
+
include_content,
|
|
113
|
+
file_extensions,
|
|
114
|
+
path_filter,
|
|
115
|
+
exclude_paths,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Return MCP-formatted results
|
|
119
|
+
return searchService.formatResultsForMCP(result.results, query, result.totalIndexed);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
return {
|
|
122
|
+
content: [
|
|
123
|
+
{
|
|
124
|
+
type: 'text',
|
|
125
|
+
text: `✗ Codebase search error: ${(error as Error).message}`,
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Register all codebase tools
|
|
136
|
+
*/
|
|
137
|
+
export function registerCodebaseTools(server: McpServer): void {
|
|
138
|
+
registerCodebaseSearchTool(server);
|
|
139
|
+
}
|
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Knowledge Resources - 知識庫資源
|
|
3
|
+
* 動態掃描文件夾並讀取 frontmatter 來提供知識庫內容
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'node:fs/promises';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
import { logger } from '../../utils/logger.js';
|
|
11
|
+
import { getKnowledgeDir } from '../../utils/paths.js';
|
|
12
|
+
|
|
13
|
+
export interface KnowledgeResource {
|
|
14
|
+
uri: string;
|
|
15
|
+
title: string;
|
|
16
|
+
description: string;
|
|
17
|
+
content: string;
|
|
18
|
+
category: 'stacks' | 'guides' | 'universal' | 'data';
|
|
19
|
+
filePath?: string;
|
|
20
|
+
lastModified?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface KnowledgeFrontmatter {
|
|
24
|
+
name: string;
|
|
25
|
+
description: string;
|
|
26
|
+
category?: string;
|
|
27
|
+
tags?: string[];
|
|
28
|
+
author?: string;
|
|
29
|
+
lastUpdated?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 知識庫配置
|
|
34
|
+
*/
|
|
35
|
+
interface KnowledgeConfig {
|
|
36
|
+
knowledgeDir: string;
|
|
37
|
+
supportedExtensions: string[];
|
|
38
|
+
cacheTimeout: number; // milliseconds
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const DEFAULT_CONFIG: KnowledgeConfig = {
|
|
42
|
+
knowledgeDir: getKnowledgeDir(),
|
|
43
|
+
supportedExtensions: ['.md'],
|
|
44
|
+
cacheTimeout: 5 * 60 * 1000, // 5 minutes
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 知識庫掃描器
|
|
49
|
+
*/
|
|
50
|
+
class KnowledgeScanner {
|
|
51
|
+
private cache = new Map<string, { data: KnowledgeResource[]; timestamp: number }>();
|
|
52
|
+
private config: KnowledgeConfig;
|
|
53
|
+
|
|
54
|
+
constructor(config: Partial<KnowledgeConfig> = {}) {
|
|
55
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 掃描知識庫目錄並獲取所有資源
|
|
60
|
+
*/
|
|
61
|
+
async scanKnowledgeResources(): Promise<KnowledgeResource[]> {
|
|
62
|
+
const cacheKey = 'all_resources';
|
|
63
|
+
const cached = this.cache.get(cacheKey);
|
|
64
|
+
|
|
65
|
+
// 檢查緩存是否有效
|
|
66
|
+
if (cached && Date.now() - cached.timestamp < this.config.cacheTimeout) {
|
|
67
|
+
logger.debug('Returning cached knowledge resources');
|
|
68
|
+
return cached.data;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
logger.info('Scanning knowledge directory', { dir: this.config.knowledgeDir });
|
|
73
|
+
|
|
74
|
+
const resources: KnowledgeResource[] = [];
|
|
75
|
+
const categories = await this.getCategories();
|
|
76
|
+
|
|
77
|
+
// 並行掃描所有類別
|
|
78
|
+
const scanPromises = categories.map(async (category) => {
|
|
79
|
+
const categoryResources = await this.scanCategory(category);
|
|
80
|
+
return categoryResources;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const categoryResults = await Promise.all(scanPromises);
|
|
84
|
+
categoryResults.forEach((categoryResources) => {
|
|
85
|
+
resources.push(...categoryResources);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// 緩存結果
|
|
89
|
+
this.cache.set(cacheKey, {
|
|
90
|
+
data: resources,
|
|
91
|
+
timestamp: Date.now(),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
logger.info('Knowledge scan completed', {
|
|
95
|
+
totalResources: resources.length,
|
|
96
|
+
categories: categories.length,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return resources;
|
|
100
|
+
} catch (error) {
|
|
101
|
+
logger.error('Failed to scan knowledge resources', {
|
|
102
|
+
error: (error as Error).message,
|
|
103
|
+
dir: this.config.knowledgeDir,
|
|
104
|
+
});
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 獲取所有類別
|
|
111
|
+
*/
|
|
112
|
+
private async getCategories(): Promise<string[]> {
|
|
113
|
+
try {
|
|
114
|
+
const entries = await fs.readdir(this.config.knowledgeDir, { withFileTypes: true });
|
|
115
|
+
return entries
|
|
116
|
+
.filter((entry) => entry.isDirectory())
|
|
117
|
+
.map((entry) => entry.name)
|
|
118
|
+
.filter((name) => !name.startsWith('.')); // 忽略隱藏文件夾
|
|
119
|
+
} catch (error) {
|
|
120
|
+
logger.warn('Failed to read knowledge directory', {
|
|
121
|
+
error: (error as Error).message,
|
|
122
|
+
dir: this.config.knowledgeDir,
|
|
123
|
+
});
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 掃描特定類別
|
|
130
|
+
*/
|
|
131
|
+
private async scanCategory(category: string): Promise<KnowledgeResource[]> {
|
|
132
|
+
const categoryPath = path.join(this.config.knowledgeDir, category);
|
|
133
|
+
const resources: KnowledgeResource[] = [];
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const files = await fs.readdir(categoryPath);
|
|
137
|
+
const markdownFiles = files.filter((file) =>
|
|
138
|
+
this.config.supportedExtensions.some((ext) => file.endsWith(ext))
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
// 並行處理所有文件
|
|
142
|
+
const filePromises = markdownFiles.map(async (file) => {
|
|
143
|
+
const filePath = path.join(categoryPath, file);
|
|
144
|
+
return this.processFile(filePath, category);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const fileResults = await Promise.allSettled(filePromises);
|
|
148
|
+
fileResults.forEach((result, index) => {
|
|
149
|
+
if (result.status === 'fulfilled' && result.value) {
|
|
150
|
+
resources.push(result.value);
|
|
151
|
+
} else if (result.status === 'rejected') {
|
|
152
|
+
logger.warn('Failed to process knowledge file', {
|
|
153
|
+
file: markdownFiles[index],
|
|
154
|
+
error: result.reason,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
logger.debug('Category scan completed', {
|
|
160
|
+
category,
|
|
161
|
+
filesFound: markdownFiles.length,
|
|
162
|
+
resourcesProcessed: resources.length,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
return resources;
|
|
166
|
+
} catch (error) {
|
|
167
|
+
logger.warn('Failed to scan category', {
|
|
168
|
+
category,
|
|
169
|
+
error: (error as Error).message,
|
|
170
|
+
});
|
|
171
|
+
return [];
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* 處理單個文件
|
|
177
|
+
*/
|
|
178
|
+
private async processFile(filePath: string, category: string): Promise<KnowledgeResource | null> {
|
|
179
|
+
try {
|
|
180
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
181
|
+
const frontmatter = this.parseFrontmatter(content);
|
|
182
|
+
|
|
183
|
+
if (!frontmatter) {
|
|
184
|
+
logger.warn('No frontmatter found in file', { filePath });
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// 生成 URI
|
|
189
|
+
const fileName = path.basename(filePath, path.extname(filePath));
|
|
190
|
+
const uri = `knowledge://${category}/${fileName}`;
|
|
191
|
+
|
|
192
|
+
// 獲取文件統計信息
|
|
193
|
+
const stats = await fs.stat(filePath);
|
|
194
|
+
|
|
195
|
+
const resource: KnowledgeResource = {
|
|
196
|
+
uri,
|
|
197
|
+
title: frontmatter.name,
|
|
198
|
+
description: frontmatter.description,
|
|
199
|
+
content: this.extractMainContent(content),
|
|
200
|
+
category: this.mapCategory(category),
|
|
201
|
+
filePath,
|
|
202
|
+
lastModified: stats.mtime.toISOString(),
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
logger.debug('Knowledge resource processed', {
|
|
206
|
+
uri,
|
|
207
|
+
title: resource.title,
|
|
208
|
+
category: resource.category,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
return resource;
|
|
212
|
+
} catch (error) {
|
|
213
|
+
logger.error('Failed to process knowledge file', {
|
|
214
|
+
filePath,
|
|
215
|
+
error: (error as Error).message,
|
|
216
|
+
});
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* 解析 frontmatter
|
|
223
|
+
*/
|
|
224
|
+
private parseFrontmatter(content: string): KnowledgeFrontmatter | null {
|
|
225
|
+
const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---\s*\n/;
|
|
226
|
+
const match = content.match(frontmatterRegex);
|
|
227
|
+
|
|
228
|
+
if (!match) {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
// 簡單的 YAML 解析(可以考慮使用 js-yaml 庫)
|
|
234
|
+
const frontmatterText = match[1];
|
|
235
|
+
const frontmatter: Partial<KnowledgeFrontmatter> = {};
|
|
236
|
+
|
|
237
|
+
// 解析 key: value 格式
|
|
238
|
+
frontmatterText.split('\n').forEach((line) => {
|
|
239
|
+
const colonIndex = line.indexOf(':');
|
|
240
|
+
if (colonIndex > 0) {
|
|
241
|
+
const key = line.substring(0, colonIndex).trim();
|
|
242
|
+
let value = line.substring(colonIndex + 1).trim();
|
|
243
|
+
|
|
244
|
+
// 移除引號
|
|
245
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
246
|
+
value = value.slice(1, -1);
|
|
247
|
+
} else if (value.startsWith("'") && value.endsWith("'")) {
|
|
248
|
+
value = value.slice(1, -1);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
(frontmatter as any)[key] = value;
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// 驗證必需字段
|
|
256
|
+
if (!frontmatter.name || !frontmatter.description) {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return frontmatter as KnowledgeFrontmatter;
|
|
261
|
+
} catch (error) {
|
|
262
|
+
logger.warn('Failed to parse frontmatter', {
|
|
263
|
+
error: (error as Error).message,
|
|
264
|
+
});
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* 提取主要內容(移除 frontmatter)
|
|
271
|
+
*/
|
|
272
|
+
private extractMainContent(content: string): string {
|
|
273
|
+
const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/;
|
|
274
|
+
return content.replace(frontmatterRegex, '').trim();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* 映射類別
|
|
279
|
+
*/
|
|
280
|
+
private mapCategory(folderName: string): 'stacks' | 'guides' | 'universal' | 'data' {
|
|
281
|
+
const categoryMap: Record<string, 'stacks' | 'guides' | 'universal' | 'data'> = {
|
|
282
|
+
stacks: 'stacks',
|
|
283
|
+
guides: 'guides',
|
|
284
|
+
universal: 'universal',
|
|
285
|
+
data: 'data',
|
|
286
|
+
database: 'data',
|
|
287
|
+
architecture: 'guides',
|
|
288
|
+
patterns: 'guides',
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
return categoryMap[folderName.toLowerCase()] || 'guides';
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* 根據 URI 獲取完整內容
|
|
296
|
+
*/
|
|
297
|
+
async getKnowledgeContent(uri: string): Promise<string> {
|
|
298
|
+
try {
|
|
299
|
+
const resources = await this.scanKnowledgeResources();
|
|
300
|
+
const resource = resources.find((r) => r.uri === uri);
|
|
301
|
+
|
|
302
|
+
if (!resource) {
|
|
303
|
+
throw new Error(`Knowledge resource not found: ${uri}`);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (!resource.filePath) {
|
|
307
|
+
throw new Error(`No file path for resource: ${uri}`);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// 讀取完整文件內容
|
|
311
|
+
const fullContent = await fs.readFile(resource.filePath, 'utf-8');
|
|
312
|
+
const mainContent = this.extractMainContent(fullContent);
|
|
313
|
+
|
|
314
|
+
return mainContent;
|
|
315
|
+
} catch (error) {
|
|
316
|
+
logger.error('Failed to get knowledge content', {
|
|
317
|
+
uri,
|
|
318
|
+
error: (error as Error).message,
|
|
319
|
+
});
|
|
320
|
+
throw error;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* 清除緩存
|
|
326
|
+
*/
|
|
327
|
+
clearCache(): void {
|
|
328
|
+
this.cache.clear();
|
|
329
|
+
logger.info('Knowledge cache cleared');
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// 全局掃描器實例
|
|
334
|
+
const knowledgeScanner = new KnowledgeScanner();
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* 獲取所有知識庫資源(動態掃描)
|
|
338
|
+
*/
|
|
339
|
+
export async function getAllKnowledgeResources(): Promise<KnowledgeResource[]> {
|
|
340
|
+
return knowledgeScanner.scanKnowledgeResources();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* 根據 URI 獲取知識庫內容
|
|
345
|
+
*/
|
|
346
|
+
export async function getKnowledgeContent(uri: string): Promise<string> {
|
|
347
|
+
return knowledgeScanner.getKnowledgeContent(uri);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* 根據類別獲取知識庫資源
|
|
352
|
+
*/
|
|
353
|
+
export async function getKnowledgeResourcesByCategory(
|
|
354
|
+
category: 'stacks' | 'guides' | 'universal' | 'data'
|
|
355
|
+
): Promise<KnowledgeResource[]> {
|
|
356
|
+
const allResources = await getAllKnowledgeResources();
|
|
357
|
+
return allResources.filter((resource) => resource.category === category);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* 搜索知識庫資源
|
|
362
|
+
*/
|
|
363
|
+
export async function searchKnowledgeResources(
|
|
364
|
+
query: string,
|
|
365
|
+
options?: {
|
|
366
|
+
category?: 'stacks' | 'guides' | 'universal' | 'data';
|
|
367
|
+
limit?: number;
|
|
368
|
+
}
|
|
369
|
+
): Promise<KnowledgeResource[]> {
|
|
370
|
+
const allResources = await getAllKnowledgeResources();
|
|
371
|
+
const queryLower = query.toLowerCase();
|
|
372
|
+
|
|
373
|
+
let filtered = allResources.filter(
|
|
374
|
+
(resource) =>
|
|
375
|
+
resource.title.toLowerCase().includes(queryLower) ||
|
|
376
|
+
resource.description.toLowerCase().includes(queryLower) ||
|
|
377
|
+
resource.content.toLowerCase().includes(queryLower)
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
// 按類別過濾
|
|
381
|
+
if (options?.category) {
|
|
382
|
+
filtered = filtered.filter((resource) => resource.category === options.category);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// 限制結果數量
|
|
386
|
+
if (options?.limit) {
|
|
387
|
+
filtered = filtered.slice(0, options.limit);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return filtered;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* 清除知識庫緩存
|
|
395
|
+
*/
|
|
396
|
+
export function clearKnowledgeCache(): void {
|
|
397
|
+
knowledgeScanner.clearCache();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* 註冊 MCP 工具
|
|
402
|
+
*/
|
|
403
|
+
export function registerKnowledgeTools(server: McpServer): void {
|
|
404
|
+
// 獲取所有知識資源
|
|
405
|
+
server.tool(
|
|
406
|
+
'get-all-knowledge-resources',
|
|
407
|
+
'Get all available knowledge resources',
|
|
408
|
+
{
|
|
409
|
+
category: z
|
|
410
|
+
.enum(['stacks', 'guides', 'universal', 'data'])
|
|
411
|
+
.optional()
|
|
412
|
+
.describe('Filter by category'),
|
|
413
|
+
},
|
|
414
|
+
async ({ category }) => {
|
|
415
|
+
try {
|
|
416
|
+
const resources = category
|
|
417
|
+
? await getKnowledgeResourcesByCategory(category)
|
|
418
|
+
: await getAllKnowledgeResources();
|
|
419
|
+
|
|
420
|
+
return {
|
|
421
|
+
content: [
|
|
422
|
+
{
|
|
423
|
+
type: 'text',
|
|
424
|
+
text: JSON.stringify(
|
|
425
|
+
{
|
|
426
|
+
total: resources.length,
|
|
427
|
+
resources: resources.map((r) => ({
|
|
428
|
+
uri: r.uri,
|
|
429
|
+
title: r.title,
|
|
430
|
+
description: r.description,
|
|
431
|
+
category: r.category,
|
|
432
|
+
lastModified: r.lastModified,
|
|
433
|
+
})),
|
|
434
|
+
},
|
|
435
|
+
null,
|
|
436
|
+
2
|
|
437
|
+
),
|
|
438
|
+
},
|
|
439
|
+
],
|
|
440
|
+
};
|
|
441
|
+
} catch (error) {
|
|
442
|
+
return {
|
|
443
|
+
content: [
|
|
444
|
+
{
|
|
445
|
+
type: 'text',
|
|
446
|
+
text: `Error: ${(error as Error).message}`,
|
|
447
|
+
},
|
|
448
|
+
],
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
// 獲取知識內容
|
|
455
|
+
server.tool(
|
|
456
|
+
'get-knowledge-content',
|
|
457
|
+
'Get full content of a knowledge resource',
|
|
458
|
+
{
|
|
459
|
+
uri: z.string().describe('Knowledge resource URI'),
|
|
460
|
+
},
|
|
461
|
+
async ({ uri }) => {
|
|
462
|
+
try {
|
|
463
|
+
const content = await getKnowledgeContent(uri);
|
|
464
|
+
return {
|
|
465
|
+
content: [
|
|
466
|
+
{
|
|
467
|
+
type: 'text',
|
|
468
|
+
text: content,
|
|
469
|
+
},
|
|
470
|
+
],
|
|
471
|
+
};
|
|
472
|
+
} catch (error) {
|
|
473
|
+
return {
|
|
474
|
+
content: [
|
|
475
|
+
{
|
|
476
|
+
type: 'text',
|
|
477
|
+
text: `Error: ${(error as Error).message}`,
|
|
478
|
+
},
|
|
479
|
+
],
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
// 搜索知識庫
|
|
486
|
+
server.tool(
|
|
487
|
+
'search-knowledge',
|
|
488
|
+
'Search knowledge resources',
|
|
489
|
+
{
|
|
490
|
+
query: z.string().describe('Search query'),
|
|
491
|
+
category: z
|
|
492
|
+
.enum(['stacks', 'guides', 'universal', 'data'])
|
|
493
|
+
.optional()
|
|
494
|
+
.describe('Filter by category'),
|
|
495
|
+
limit: z.number().optional().describe('Maximum results'),
|
|
496
|
+
},
|
|
497
|
+
async ({ query, category, limit }) => {
|
|
498
|
+
try {
|
|
499
|
+
const results = await searchKnowledgeResources(query, {
|
|
500
|
+
category,
|
|
501
|
+
limit,
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
return {
|
|
505
|
+
content: [
|
|
506
|
+
{
|
|
507
|
+
type: 'text',
|
|
508
|
+
text: JSON.stringify(
|
|
509
|
+
{
|
|
510
|
+
query,
|
|
511
|
+
total: results.length,
|
|
512
|
+
results: results.map((r) => ({
|
|
513
|
+
uri: r.uri,
|
|
514
|
+
title: r.title,
|
|
515
|
+
description: r.description,
|
|
516
|
+
category: r.category,
|
|
517
|
+
})),
|
|
518
|
+
},
|
|
519
|
+
null,
|
|
520
|
+
2
|
|
521
|
+
),
|
|
522
|
+
},
|
|
523
|
+
],
|
|
524
|
+
};
|
|
525
|
+
} catch (error) {
|
|
526
|
+
return {
|
|
527
|
+
content: [
|
|
528
|
+
{
|
|
529
|
+
type: 'text',
|
|
530
|
+
text: `Error: ${(error as Error).message}`,
|
|
531
|
+
},
|
|
532
|
+
],
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
);
|
|
537
|
+
}
|