skill-any-code 1.0.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/README.md +48 -0
- package/dist/cli.js +319 -0
- package/dist/index.js +22 -0
- package/jest.config.js +27 -0
- package/package.json +59 -0
- package/src/adapters/command.schemas.ts +21 -0
- package/src/application/analysis.app.service.ts +272 -0
- package/src/application/bootstrap.ts +35 -0
- package/src/application/services/llm.analysis.service.ts +237 -0
- package/src/cli.ts +297 -0
- package/src/common/config.ts +209 -0
- package/src/common/constants.ts +8 -0
- package/src/common/errors.ts +34 -0
- package/src/common/logger.ts +82 -0
- package/src/common/types.ts +385 -0
- package/src/common/ui.ts +228 -0
- package/src/common/utils.ts +81 -0
- package/src/domain/index.ts +1 -0
- package/src/domain/interfaces.ts +188 -0
- package/src/domain/services/analysis.service.ts +735 -0
- package/src/domain/services/incremental.service.ts +50 -0
- package/src/index.ts +6 -0
- package/src/infrastructure/blacklist.service.ts +37 -0
- package/src/infrastructure/cache/file.hash.cache.ts +119 -0
- package/src/infrastructure/git/git.service.ts +120 -0
- package/src/infrastructure/git.service.ts +121 -0
- package/src/infrastructure/index.service.ts +94 -0
- package/src/infrastructure/llm/llm.usage.tracker.ts +65 -0
- package/src/infrastructure/llm/openai.client.ts +162 -0
- package/src/infrastructure/llm/prompt.template.ts +175 -0
- package/src/infrastructure/llm.service.ts +70 -0
- package/src/infrastructure/skill/skill.generator.ts +53 -0
- package/src/infrastructure/skill/templates/resolve.script.ts +97 -0
- package/src/infrastructure/skill/templates/skill.md.template.ts +45 -0
- package/src/infrastructure/splitter/code.splitter.ts +176 -0
- package/src/infrastructure/storage.service.ts +413 -0
- package/src/infrastructure/worker-pool/parse.worker.impl.ts +135 -0
- package/src/infrastructure/worker-pool/parse.worker.ts +9 -0
- package/src/infrastructure/worker-pool/worker-pool.service.ts +173 -0
- package/tsconfig.json +24 -0
- package/tsconfig.test.json +5 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import * as path from 'path'
|
|
2
|
+
import { DirectoryAnalysis, FileAnalysis, LLMConfig, ModificationLog } from '../../common/types'
|
|
3
|
+
import { OpenAIClient } from '../llm/openai.client'
|
|
4
|
+
import { LLMUsageTracker } from '../llm/llm.usage.tracker'
|
|
5
|
+
import { CodeSplitter } from '../splitter/code.splitter'
|
|
6
|
+
import { FileHashCache } from '../cache/file.hash.cache'
|
|
7
|
+
import { LLMAnalysisService } from '../../application/services/llm.analysis.service'
|
|
8
|
+
import * as crypto from 'crypto'
|
|
9
|
+
|
|
10
|
+
export type WorkerUsageDelta = {
|
|
11
|
+
totalPromptTokens: number
|
|
12
|
+
totalCompletionTokens: number
|
|
13
|
+
totalTokens: number
|
|
14
|
+
totalCalls: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function parseFile(
|
|
18
|
+
filePath: string,
|
|
19
|
+
fileContent: string,
|
|
20
|
+
fileHash: string,
|
|
21
|
+
language?: string,
|
|
22
|
+
llmConfig?: LLMConfig
|
|
23
|
+
): Promise<{ analysis: FileAnalysis; usage: WorkerUsageDelta }> {
|
|
24
|
+
if (!llmConfig) {
|
|
25
|
+
throw new Error('LLM config is required for file parsing')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 初始化LLM相关服务
|
|
29
|
+
const tracker = new LLMUsageTracker()
|
|
30
|
+
const llmClient = new OpenAIClient(llmConfig, tracker)
|
|
31
|
+
const fileSplitter = new CodeSplitter(llmClient)
|
|
32
|
+
const cache = new FileHashCache({
|
|
33
|
+
cacheDir: llmConfig.cache_dir,
|
|
34
|
+
maxSizeMb: llmConfig.cache_max_size_mb,
|
|
35
|
+
})
|
|
36
|
+
const llmAnalysisService = new LLMAnalysisService(llmClient, fileSplitter, cache, llmConfig)
|
|
37
|
+
|
|
38
|
+
const result = await llmAnalysisService.analyzeFile(filePath, fileContent, fileHash)
|
|
39
|
+
return { analysis: result, usage: tracker.getStats() }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function aggregateDirectory(
|
|
43
|
+
dirPath: string,
|
|
44
|
+
payload: {
|
|
45
|
+
childrenDirs: Array<{ name: string; summary: string; description?: string }>
|
|
46
|
+
childrenFiles: Array<{ name: string; summary: string; description?: string }>
|
|
47
|
+
},
|
|
48
|
+
llmConfig?: LLMConfig
|
|
49
|
+
): Promise<{ description: string; summary: string; usage: WorkerUsageDelta }> {
|
|
50
|
+
const name = path.basename(dirPath)
|
|
51
|
+
const childrenDirsPayload = (payload?.childrenDirs ?? []).map(d => ({
|
|
52
|
+
name: d.name,
|
|
53
|
+
summary: d.summary,
|
|
54
|
+
description: d.description ?? d.summary,
|
|
55
|
+
}))
|
|
56
|
+
const childrenFilesPayload = (payload?.childrenFiles ?? []).map(f => ({
|
|
57
|
+
name: f.name,
|
|
58
|
+
summary: f.summary,
|
|
59
|
+
description: f.description ?? f.summary,
|
|
60
|
+
}))
|
|
61
|
+
|
|
62
|
+
// LLM 不可用时回退到可解释的聚合文案
|
|
63
|
+
if (!llmConfig || !llmConfig.base_url || !llmConfig.model) {
|
|
64
|
+
const fileCount = childrenFilesPayload.length
|
|
65
|
+
const dirCount = childrenDirsPayload.length
|
|
66
|
+
const fallback = `The "${name}" directory contains ${fileCount} file(s) and ${dirCount} subdirectory(ies) and helps organize related source code and modules.`
|
|
67
|
+
return { description: fallback, summary: fallback, usage: { totalPromptTokens: 0, totalCompletionTokens: 0, totalTokens: 0, totalCalls: 0 } }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 初始化LLM相关服务(worker 内做本任务 token 统计,返回给主线程聚合)
|
|
71
|
+
const tracker = new LLMUsageTracker()
|
|
72
|
+
const llmClient = new OpenAIClient(llmConfig, tracker)
|
|
73
|
+
const fileSplitter = new CodeSplitter(llmClient)
|
|
74
|
+
const cache = new FileHashCache({
|
|
75
|
+
cacheDir: llmConfig.cache_dir,
|
|
76
|
+
maxSizeMb: llmConfig.cache_max_size_mb,
|
|
77
|
+
})
|
|
78
|
+
const llmAnalysisService = new LLMAnalysisService(llmClient, fileSplitter, cache, llmConfig)
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const dirResultFromLLM = await llmAnalysisService.analyzeDirectory(childrenDirsPayload, childrenFilesPayload)
|
|
82
|
+
return {
|
|
83
|
+
description: dirResultFromLLM.description,
|
|
84
|
+
summary: dirResultFromLLM.summary,
|
|
85
|
+
usage: tracker.getStats(),
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
const fileCount = childrenFilesPayload.length
|
|
89
|
+
const dirCount = childrenDirsPayload.length
|
|
90
|
+
const fallback = `The "${name}" directory contains ${fileCount} file(s) and ${dirCount} subdirectory(ies) and helps organize related source code and modules.`
|
|
91
|
+
return { description: fallback, summary: fallback, usage: tracker.getStats() }
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function validateResult(parentResult: DirectoryAnalysis, childResult: FileAnalysis | DirectoryAnalysis): Promise<{
|
|
96
|
+
valid: boolean
|
|
97
|
+
corrections?: Partial<FileAnalysis | DirectoryAnalysis>
|
|
98
|
+
log?: ModificationLog
|
|
99
|
+
}> {
|
|
100
|
+
// 简单的校验逻辑:检查子项是否在父项的结构中存在
|
|
101
|
+
const existsInStructure = parentResult.structure.some(item => item.name === childResult.name)
|
|
102
|
+
const corrections: Partial<FileAnalysis | DirectoryAnalysis> = {}
|
|
103
|
+
let log: ModificationLog | undefined
|
|
104
|
+
|
|
105
|
+
if (!existsInStructure) {
|
|
106
|
+
// 修正:将子项添加到父项的结构中
|
|
107
|
+
;(corrections as Partial<DirectoryAnalysis>).structure = [
|
|
108
|
+
...parentResult.structure,
|
|
109
|
+
{
|
|
110
|
+
name: childResult.name,
|
|
111
|
+
type: childResult.type,
|
|
112
|
+
description: childResult.summary.substring(0, 100),
|
|
113
|
+
},
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
log = {
|
|
117
|
+
id: crypto.randomUUID(),
|
|
118
|
+
timestamp: new Date().toISOString(),
|
|
119
|
+
path: parentResult.path,
|
|
120
|
+
type: 'omission',
|
|
121
|
+
originalContent: JSON.stringify(parentResult.structure),
|
|
122
|
+
correctedContent: JSON.stringify((corrections as Partial<DirectoryAnalysis>).structure),
|
|
123
|
+
reason: `Child item ${childResult.name} missing from directory structure`,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
valid: false,
|
|
128
|
+
corrections,
|
|
129
|
+
log,
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return { valid: true }
|
|
134
|
+
}
|
|
135
|
+
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import * as workerpool from 'workerpool'
|
|
2
|
+
import * as path from 'path'
|
|
3
|
+
import * as fs from 'fs'
|
|
4
|
+
import { IWorkerPoolService } from '../../domain/interfaces'
|
|
5
|
+
import { FileAnalysis, DirectoryAnalysis, ModificationLog, LLMConfig } from '../../common/types'
|
|
6
|
+
import { AppError, ErrorCode } from '../../common/errors'
|
|
7
|
+
import { DEFAULT_CONCURRENCY } from '../../common/constants'
|
|
8
|
+
import os from 'os'
|
|
9
|
+
import { aggregateDirectory, parseFile, validateResult } from './parse.worker.impl'
|
|
10
|
+
|
|
11
|
+
export interface DirectoryAggregationPayload {
|
|
12
|
+
childrenDirs: Array<{ name: string; summary: string; description?: string }>
|
|
13
|
+
childrenFiles: Array<{ name: string; summary: string; description?: string }>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface DirectoryAggregationLLMResult {
|
|
17
|
+
description: string
|
|
18
|
+
summary: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type WorkerUsageDelta = {
|
|
22
|
+
totalPromptTokens: number
|
|
23
|
+
totalCompletionTokens: number
|
|
24
|
+
totalTokens: number
|
|
25
|
+
totalCalls: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class WorkerPoolService implements IWorkerPoolService {
|
|
29
|
+
private pool?: workerpool.Pool
|
|
30
|
+
private pendingTasks: Promise<any>[] = []
|
|
31
|
+
private currentConcurrency: number
|
|
32
|
+
private llmConfig: LLMConfig
|
|
33
|
+
private readonly localMode: boolean
|
|
34
|
+
|
|
35
|
+
constructor(llmConfig: LLMConfig, concurrency: number = DEFAULT_CONCURRENCY) {
|
|
36
|
+
this.currentConcurrency = concurrency
|
|
37
|
+
this.llmConfig = {
|
|
38
|
+
...llmConfig,
|
|
39
|
+
cache_dir: WorkerPoolService.expandTilde(llmConfig.cache_dir),
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const workerScript = path.join(__dirname, 'parse.worker.js')
|
|
43
|
+
this.localMode = !fs.existsSync(workerScript)
|
|
44
|
+
|
|
45
|
+
// Jest/ts-jest 直接执行 src 时,parse.worker.js 不存在。此时回退到“本进程执行”以保证核心逻辑可测。
|
|
46
|
+
// 生产构建(dist)与 CLI 运行时,parse.worker.js 存在,继续使用 worker threads 提升性能。
|
|
47
|
+
if (!this.localMode) {
|
|
48
|
+
this.pool = workerpool.pool(workerScript, {
|
|
49
|
+
maxWorkers: concurrency,
|
|
50
|
+
workerType: 'thread',
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private static expandTilde(p: string): string {
|
|
56
|
+
if (!p) return p
|
|
57
|
+
if (p.startsWith('~') && (p.length === 1 || p[1] === '/' || p[1] === '\\')) {
|
|
58
|
+
return path.join(os.homedir(), p.slice(1))
|
|
59
|
+
}
|
|
60
|
+
return p
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async submitFileAnalysisTask(
|
|
64
|
+
filePath: string,
|
|
65
|
+
fileContent: string,
|
|
66
|
+
fileHash: string,
|
|
67
|
+
language?: string
|
|
68
|
+
): Promise<{ analysis: FileAnalysis; usage: WorkerUsageDelta }> {
|
|
69
|
+
if (this.localMode) {
|
|
70
|
+
const task = parseFile(filePath, fileContent, fileHash, language, this.llmConfig)
|
|
71
|
+
this.pendingTasks.push(task)
|
|
72
|
+
try {
|
|
73
|
+
return await task
|
|
74
|
+
} finally {
|
|
75
|
+
this.pendingTasks = this.pendingTasks.filter(t => t !== task)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
const task = this.pool!.exec('parseFile', [filePath, fileContent, fileHash, language, this.llmConfig])
|
|
80
|
+
this.pendingTasks.push(task)
|
|
81
|
+
|
|
82
|
+
const result = await task
|
|
83
|
+
// 移除已完成的任务
|
|
84
|
+
this.pendingTasks = this.pendingTasks.filter(t => t !== task)
|
|
85
|
+
|
|
86
|
+
return result
|
|
87
|
+
} catch (e) {
|
|
88
|
+
throw new AppError(ErrorCode.WORKER_SCHEDULE_FAILED, 'File analysis task failed', (e as Error).message)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async submitDirectoryAggregationTask(
|
|
93
|
+
dirPath: string,
|
|
94
|
+
payload: DirectoryAggregationPayload
|
|
95
|
+
): Promise<DirectoryAggregationLLMResult & { usage: WorkerUsageDelta }> {
|
|
96
|
+
if (this.localMode) {
|
|
97
|
+
const task = aggregateDirectory(dirPath, payload, this.llmConfig)
|
|
98
|
+
this.pendingTasks.push(task)
|
|
99
|
+
try {
|
|
100
|
+
return await task
|
|
101
|
+
} finally {
|
|
102
|
+
this.pendingTasks = this.pendingTasks.filter(t => t !== task)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
const task = this.pool!.exec('aggregateDirectory', [dirPath, payload, this.llmConfig])
|
|
107
|
+
this.pendingTasks.push(task)
|
|
108
|
+
|
|
109
|
+
const result = await task
|
|
110
|
+
this.pendingTasks = this.pendingTasks.filter(t => t !== task)
|
|
111
|
+
|
|
112
|
+
return result
|
|
113
|
+
} catch (e) {
|
|
114
|
+
throw new AppError(ErrorCode.WORKER_SCHEDULE_FAILED, 'Directory aggregation task failed', (e as Error).message)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async submitValidationTask(parentResult: DirectoryAnalysis, childResult: FileAnalysis | DirectoryAnalysis): Promise<{
|
|
119
|
+
valid: boolean
|
|
120
|
+
corrections?: Partial<FileAnalysis | DirectoryAnalysis>
|
|
121
|
+
log?: ModificationLog
|
|
122
|
+
}> {
|
|
123
|
+
if (this.localMode) {
|
|
124
|
+
const task = validateResult(parentResult, childResult)
|
|
125
|
+
this.pendingTasks.push(task)
|
|
126
|
+
try {
|
|
127
|
+
return await task
|
|
128
|
+
} finally {
|
|
129
|
+
this.pendingTasks = this.pendingTasks.filter(t => t !== task)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
const task = this.pool!.exec('validateResult', [parentResult, childResult])
|
|
134
|
+
this.pendingTasks.push(task)
|
|
135
|
+
|
|
136
|
+
const result = await task
|
|
137
|
+
this.pendingTasks = this.pendingTasks.filter(t => t !== task)
|
|
138
|
+
|
|
139
|
+
return result
|
|
140
|
+
} catch (e) {
|
|
141
|
+
throw new AppError(ErrorCode.WORKER_SCHEDULE_FAILED, 'Validation task failed', (e as Error).message)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
setConcurrency(concurrency: number): void {
|
|
146
|
+
this.currentConcurrency = concurrency
|
|
147
|
+
if (this.localMode) {
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
// 立即终止旧 worker,避免遗留线程占用资源
|
|
151
|
+
void this.pool!.terminate(true).catch(() => {})
|
|
152
|
+
this.pool = workerpool.pool(path.join(__dirname, 'parse.worker.js'), {
|
|
153
|
+
maxWorkers: concurrency,
|
|
154
|
+
workerType: 'thread'
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async waitAll(): Promise<void> {
|
|
159
|
+
await Promise.all(this.pendingTasks)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
cancelAll(): void {
|
|
163
|
+
void this.pool?.terminate(true).catch(() => {})
|
|
164
|
+
this.pendingTasks = []
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async terminate(force: boolean = true): Promise<void> {
|
|
168
|
+
if (this.pool) {
|
|
169
|
+
await this.pool.terminate(force)
|
|
170
|
+
}
|
|
171
|
+
this.pendingTasks = []
|
|
172
|
+
}
|
|
173
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2022"],
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"moduleResolution": "node",
|
|
14
|
+
"experimentalDecorators": true,
|
|
15
|
+
"emitDecoratorMetadata": true,
|
|
16
|
+
"baseUrl": ".",
|
|
17
|
+
"paths": {
|
|
18
|
+
"@/*": ["src/*"]
|
|
19
|
+
},
|
|
20
|
+
"types": ["node", "jest"]
|
|
21
|
+
},
|
|
22
|
+
"include": ["src/**/*"],
|
|
23
|
+
"exclude": ["node_modules", "dist", "coverage", "tests"]
|
|
24
|
+
}
|