@lon-ask/dockit 0.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/LICENSE +674 -0
- package/README.md +496 -0
- package/SKILL.md +154 -0
- package/apps/client/dist/assets/index-CqOXxsEZ.js +240 -0
- package/apps/client/dist/assets/index-DwvaANnI.css +1 -0
- package/apps/client/dist/index.html +13 -0
- package/apps/server/src/core/domain/entry.ts +22 -0
- package/apps/server/src/core/domain/errors.ts +27 -0
- package/apps/server/src/core/domain/knowledge-graph.ts +51 -0
- package/apps/server/src/core/domain/types.ts +168 -0
- package/apps/server/src/core/ports/IBuildRepository.ts +7 -0
- package/apps/server/src/core/ports/IDocumentNormalizer.ts +6 -0
- package/apps/server/src/core/ports/IDocumentStore.ts +4 -0
- package/apps/server/src/core/ports/IEntryReadModel.ts +9 -0
- package/apps/server/src/core/ports/IEntryRepository.ts +11 -0
- package/apps/server/src/core/ports/IKnowledgeGraph.ts +10 -0
- package/apps/server/src/core/ports/IPathResolver.ts +3 -0
- package/apps/server/src/core/ports/ISearchEngine.ts +9 -0
- package/apps/server/src/core/ports/ISourceProcessor.ts +7 -0
- package/apps/server/src/core/ports/ISourceRepository.ts +11 -0
- package/apps/server/src/core/usecases/BuildUseCase.ts +98 -0
- package/apps/server/src/core/usecases/ConfigUseCase.ts +64 -0
- package/apps/server/src/core/usecases/SearchUseCase.ts +16 -0
- package/apps/server/src/index.ts +98 -0
- package/apps/server/src/infrastructure/filesystem/FileSystemDocumentStore.ts +27 -0
- package/apps/server/src/infrastructure/graph/GraphSearchDecorator.ts +53 -0
- package/apps/server/src/infrastructure/graph/GraphifyKnowledgeGraph.ts +172 -0
- package/apps/server/src/infrastructure/graph/index.ts +2 -0
- package/apps/server/src/infrastructure/persistence/sqlite/SqliteBuildRepository.ts +34 -0
- package/apps/server/src/infrastructure/persistence/sqlite/SqliteEntryReadModel.ts +17 -0
- package/apps/server/src/infrastructure/persistence/sqlite/SqliteEntryRepository.ts +81 -0
- package/apps/server/src/infrastructure/persistence/sqlite/SqliteSourceRepository.ts +65 -0
- package/apps/server/src/infrastructure/persistence/sqlite/connection.ts +52 -0
- package/apps/server/src/infrastructure/search/SearchEngineFactory.ts +43 -0
- package/apps/server/src/infrastructure/search/json/JsonSearchEngine.ts +164 -0
- package/apps/server/src/infrastructure/search/vector/EmbeddingService.ts +23 -0
- package/apps/server/src/infrastructure/search/vector/VectorSearchEngine.ts +480 -0
- package/apps/server/src/infrastructure/source-processors/AntoraSourceProcessor.ts +14 -0
- package/apps/server/src/infrastructure/source-processors/AsciidocSourceProcessor.ts +12 -0
- package/apps/server/src/infrastructure/source-processors/DocumentNormalizer.ts +16 -0
- package/apps/server/src/infrastructure/source-processors/GithubMarkdownSourceProcessor.ts +12 -0
- package/apps/server/src/infrastructure/source-processors/MavenSourceProcessor.ts +12 -0
- package/apps/server/src/infrastructure/source-processors/PathResolver.ts +6 -0
- package/apps/server/src/infrastructure/source-processors/SourceCodeSourceProcessor.ts +260 -0
- package/apps/server/src/infrastructure/source-processors/ZipSourceProcessor.ts +12 -0
- package/apps/server/src/mcp-http.ts +102 -0
- package/apps/server/src/mcp.ts +432 -0
- package/apps/server/src/routes/build.ts +105 -0
- package/apps/server/src/routes/entries.ts +62 -0
- package/apps/server/src/routes/graph.ts +57 -0
- package/apps/server/src/routes/search.ts +28 -0
- package/apps/server/src/routes/sources.ts +105 -0
- package/apps/server/src/routes/viewer.ts +28 -0
- package/apps/server/src/services/antora.ts +238 -0
- package/apps/server/src/services/asciidoc.ts +221 -0
- package/apps/server/src/services/configLoader.ts +207 -0
- package/apps/server/src/services/githubMarkdown.ts +236 -0
- package/apps/server/src/services/maven.ts +178 -0
- package/apps/server/src/services/normalizer.ts +63 -0
- package/apps/server/src/services/paths.ts +5 -0
- package/apps/server/src/services/textExtractor.ts +49 -0
- package/apps/server/src/services/zip.ts +84 -0
- package/bin/commands/build.ts +85 -0
- package/bin/commands/dev.ts +36 -0
- package/bin/commands/get.ts +36 -0
- package/bin/commands/graph.ts +153 -0
- package/bin/commands/init.ts +170 -0
- package/bin/commands/list.ts +47 -0
- package/bin/commands/mcp.ts +32 -0
- package/bin/commands/search.ts +185 -0
- package/bin/commands/serve.ts +23 -0
- package/bin/commands/status.ts +46 -0
- package/bin/dockit-cli.ts +92 -0
- package/bin/dockit.js +17 -0
- package/bin/utils.ts +85 -0
- package/dockit.yaml +154 -0
- package/package.json +60 -0
- package/scripts/mcp-wrapper.sh +44 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { IDocumentNormalizer } from '../../core/ports/IDocumentNormalizer.js';
|
|
2
|
+
import type { Source } from '../../core/domain/types.js';
|
|
3
|
+
import { normalizeDocs } from '../../services/normalizer.js';
|
|
4
|
+
|
|
5
|
+
export class DocumentNormalizer implements IDocumentNormalizer {
|
|
6
|
+
normalize(sources: Array<{ label: string; dir: string }>, outputDir: string, log: (msg: string) => void): string[] {
|
|
7
|
+
return normalizeDocs(sources, outputDir, log);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
filterSources(sources: Array<{ label: string; dir: string }>, allSourceRecords: Source[]): Array<{ label: string; dir: string }> {
|
|
11
|
+
const sourceCodeLabels = new Set(
|
|
12
|
+
allSourceRecords.filter((s) => s.type === 'source-code').map((s) => s.label),
|
|
13
|
+
);
|
|
14
|
+
return sources.filter((s) => !sourceCodeLabels.has(s.label));
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ISourceProcessor } from '../../core/ports/ISourceProcessor.js';
|
|
2
|
+
import type { Source, GithubMarkdownSourceConfig } from '../../core/domain/types.js';
|
|
3
|
+
import { buildGithubMarkdownSource } from '../../services/githubMarkdown.js';
|
|
4
|
+
|
|
5
|
+
export class GithubMarkdownSourceProcessor implements ISourceProcessor {
|
|
6
|
+
readonly sourceType = 'github-markdown' as const;
|
|
7
|
+
|
|
8
|
+
async process(source: Source, sourceDir: string, _entryDir: string, _entryId: string, log: (msg: string) => void): Promise<string> {
|
|
9
|
+
await buildGithubMarkdownSource(source.config as GithubMarkdownSourceConfig, sourceDir, log);
|
|
10
|
+
return sourceDir;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ISourceProcessor } from '../../core/ports/ISourceProcessor.js';
|
|
2
|
+
import type { Source, MavenSourceConfig } from '../../core/domain/types.js';
|
|
3
|
+
import { downloadAndExtractMavenJar } from '../../services/maven.js';
|
|
4
|
+
|
|
5
|
+
export class MavenSourceProcessor implements ISourceProcessor {
|
|
6
|
+
readonly sourceType = 'maven' as const;
|
|
7
|
+
|
|
8
|
+
async process(source: Source, sourceDir: string, _entryDir: string, _entryId: string, log: (msg: string) => void): Promise<string> {
|
|
9
|
+
await downloadAndExtractMavenJar(source.config as MavenSourceConfig, sourceDir, log);
|
|
10
|
+
return sourceDir;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { spawn, execSync } from 'node:child_process';
|
|
5
|
+
import { Readable } from 'node:stream';
|
|
6
|
+
import { pipeline } from 'node:stream/promises';
|
|
7
|
+
import unzipper from 'unzipper';
|
|
8
|
+
import type { ISourceProcessor } from '../../core/ports/ISourceProcessor.js';
|
|
9
|
+
import type { Source, SourceCodeSourceConfig } from '../../core/domain/types.js';
|
|
10
|
+
|
|
11
|
+
const GRAPHIFY_TIMEOUT = 600_000;
|
|
12
|
+
|
|
13
|
+
function execWithTimeout(cmd: string, args: string[], log: (msg: string) => void): Promise<void> {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
const proc = spawn(cmd, args, {
|
|
16
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
17
|
+
timeout: GRAPHIFY_TIMEOUT,
|
|
18
|
+
});
|
|
19
|
+
let stderr = '';
|
|
20
|
+
proc.stdout?.on('data', (data: Buffer) => {
|
|
21
|
+
for (const line of data.toString().trim().split('\n')) {
|
|
22
|
+
if (line) log(` graphify: ${line}`);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
proc.stderr?.on('data', (data: Buffer) => {
|
|
26
|
+
stderr += data.toString();
|
|
27
|
+
});
|
|
28
|
+
proc.on('close', (code) => {
|
|
29
|
+
if (code === 0) resolve();
|
|
30
|
+
else if (code === 1) {
|
|
31
|
+
log(' graphify: completed with warnings (partial results)');
|
|
32
|
+
resolve();
|
|
33
|
+
}
|
|
34
|
+
else reject(new Error(`graphify exited with code ${code}: ${stderr.slice(-500)}`));
|
|
35
|
+
});
|
|
36
|
+
proc.on('error', (err) => reject(new Error(`Failed to run graphify: ${err.message}`)));
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function ensureGraphify(log: (msg: string) => void): void {
|
|
41
|
+
try {
|
|
42
|
+
execSync('graphify --version', { stdio: 'pipe', timeout: 10_000 });
|
|
43
|
+
log('Graphify is available on PATH');
|
|
44
|
+
} catch {
|
|
45
|
+
log('Graphify not found. Installing via pip...');
|
|
46
|
+
execSync('pip3 install graphify 2>&1', { stdio: 'pipe', timeout: 120_000 });
|
|
47
|
+
log('Graphify installed successfully');
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
execSync('pip3 install openai 2>&1', { stdio: 'pipe', timeout: 120_000 });
|
|
51
|
+
} catch {
|
|
52
|
+
log('Warning: could not install openai (semantic extraction unavailable)');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function cloneRepo(repoUrl: string, targetDir: string, branch: string | undefined, log: (msg: string) => void): Promise<void> {
|
|
57
|
+
log(`Cloning repository ${repoUrl}`);
|
|
58
|
+
const branchArg = branch ? ['--branch', branch] : [];
|
|
59
|
+
if (fs.existsSync(targetDir)) fs.rmSync(targetDir, { recursive: true, force: true });
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
const proc = spawn('git', ['clone', '--depth', '1', ...branchArg, repoUrl, targetDir], {
|
|
62
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
63
|
+
});
|
|
64
|
+
let stderr = '';
|
|
65
|
+
proc.stderr?.on('data', (data: Buffer) => {
|
|
66
|
+
stderr += data.toString();
|
|
67
|
+
for (const line of data.toString().trim().split('\n')) {
|
|
68
|
+
if (line) log(` git: ${line}`);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
proc.on('close', (code) => {
|
|
72
|
+
if (code === 0) { log('Repository cloned successfully'); resolve(); }
|
|
73
|
+
else reject(new Error(`git clone failed with code ${code}: ${stderr.slice(-300)}`));
|
|
74
|
+
});
|
|
75
|
+
proc.on('error', reject);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function extractZip(zipPath: string, targetDir: string, log: (msg: string) => void): Promise<void> {
|
|
80
|
+
log(`Extracting ZIP from ${zipPath}`);
|
|
81
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
82
|
+
const data = fs.readFileSync(zipPath);
|
|
83
|
+
const stream = Readable.from(data);
|
|
84
|
+
await pipeline(stream, unzipper.Extract({ path: targetDir }));
|
|
85
|
+
const entries = fs.readdirSync(targetDir);
|
|
86
|
+
if (entries.length === 1 && fs.statSync(path.join(targetDir, entries[0])).isDirectory()) {
|
|
87
|
+
log('Flattening single root directory...');
|
|
88
|
+
const innerDir = path.join(targetDir, entries[0]);
|
|
89
|
+
for (const entry of fs.readdirSync(innerDir)) {
|
|
90
|
+
fs.renameSync(path.join(innerDir, entry), path.join(targetDir, entry));
|
|
91
|
+
}
|
|
92
|
+
fs.rmdirSync(innerDir);
|
|
93
|
+
}
|
|
94
|
+
log(`Extracted to ${targetDir}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function readGraphMetadata(graphJsonPath: string): { nodeCount: number; edgeCount: number; communities: number } {
|
|
98
|
+
try {
|
|
99
|
+
const data = JSON.parse(fs.readFileSync(graphJsonPath, 'utf-8'));
|
|
100
|
+
const nodes = (data.nodes || []).length;
|
|
101
|
+
const edges = (data.edges || []).length;
|
|
102
|
+
const communities = new Set((data.nodes || []).map((n: { community?: number }) => n.community).filter(Boolean)).size;
|
|
103
|
+
return { nodeCount: nodes, edgeCount: edges, communities };
|
|
104
|
+
} catch {
|
|
105
|
+
return { nodeCount: 0, edgeCount: 0, communities: 0 };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export class SourceCodeSourceProcessor implements ISourceProcessor {
|
|
110
|
+
readonly sourceType = 'source-code' as const;
|
|
111
|
+
|
|
112
|
+
async runGraphify(config: Record<string, unknown>, entryDir: string, log: (msg: string) => void): Promise<void> {
|
|
113
|
+
const cfg = config as unknown as SourceCodeSourceConfig;
|
|
114
|
+
let repoDir: string | undefined;
|
|
115
|
+
let shouldCleanupRepo = false;
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
if (cfg.localPath) {
|
|
119
|
+
if (!fs.existsSync(cfg.localPath)) throw new Error(`Local path not found: ${cfg.localPath}`);
|
|
120
|
+
log(`Using local directory: ${cfg.localPath}`);
|
|
121
|
+
repoDir = cfg.localPath;
|
|
122
|
+
} else if (cfg.repoUrl) {
|
|
123
|
+
repoDir = path.join(os.tmpdir(), `dockit-graphify-${Date.now()}`);
|
|
124
|
+
await cloneRepo(cfg.repoUrl, repoDir, cfg.branch, log);
|
|
125
|
+
shouldCleanupRepo = true;
|
|
126
|
+
} else if (cfg.zipPath) {
|
|
127
|
+
repoDir = path.join(os.tmpdir(), `dockit-graphify-${Date.now()}`);
|
|
128
|
+
fs.mkdirSync(repoDir, { recursive: true });
|
|
129
|
+
await extractZip(cfg.zipPath, repoDir, log);
|
|
130
|
+
shouldCleanupRepo = true;
|
|
131
|
+
} else {
|
|
132
|
+
throw new Error('graphifyEnabled requires repoUrl, localPath, or zipPath on the source');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
ensureGraphify(log);
|
|
136
|
+
const graphifyDir = cfg.graphifySourcePath ? path.join(repoDir, cfg.graphifySourcePath) : repoDir;
|
|
137
|
+
if (!fs.existsSync(graphifyDir)) {
|
|
138
|
+
throw new Error(`graphifySourcePath not found: ${cfg.graphifySourcePath}`);
|
|
139
|
+
}
|
|
140
|
+
log(`Running Graphify on ${graphifyDir}...`);
|
|
141
|
+
await execWithTimeout('graphify', ['update', graphifyDir], log);
|
|
142
|
+
|
|
143
|
+
const graphOutDir = path.join(graphifyDir, 'graphify-out');
|
|
144
|
+
const graphJsonPath = path.join(graphOutDir, 'graph.json');
|
|
145
|
+
const graphHtmlPath = path.join(graphOutDir, 'graph.html');
|
|
146
|
+
|
|
147
|
+
if (fs.existsSync(graphJsonPath)) {
|
|
148
|
+
const destJson = path.join(entryDir, 'graph.json');
|
|
149
|
+
fs.copyFileSync(graphJsonPath, destJson);
|
|
150
|
+
log(`Copied graph.json to ${destJson}`);
|
|
151
|
+
const meta = readGraphMetadata(destJson);
|
|
152
|
+
log(`Graph: ${meta.nodeCount} nodes, ${meta.edgeCount} edges, ${meta.communities} communities`);
|
|
153
|
+
} else {
|
|
154
|
+
log('WARNING: graph.json not produced by Graphify');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (fs.existsSync(graphHtmlPath)) {
|
|
158
|
+
const destHtml = path.join(entryDir, 'graph.html');
|
|
159
|
+
fs.copyFileSync(graphHtmlPath, destHtml);
|
|
160
|
+
log(`Copied graph.html to ${destHtml}`);
|
|
161
|
+
}
|
|
162
|
+
} finally {
|
|
163
|
+
if (shouldCleanupRepo && repoDir && fs.existsSync(repoDir)) {
|
|
164
|
+
fs.rmSync(repoDir, { recursive: true, force: true });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async process(source: Source, sourceDir: string, entryDir: string, _entryId: string, log: (msg: string) => void): Promise<string> {
|
|
170
|
+
const config = source.config as SourceCodeSourceConfig;
|
|
171
|
+
let repoDir: string;
|
|
172
|
+
let shouldCleanup: boolean;
|
|
173
|
+
|
|
174
|
+
if (config.localPath) {
|
|
175
|
+
if (!fs.existsSync(config.localPath)) throw new Error(`Local path not found: ${config.localPath}`);
|
|
176
|
+
if (!fs.statSync(config.localPath).isDirectory()) throw new Error(`localPath must be a directory: ${config.localPath}`);
|
|
177
|
+
log(`Using local directory: ${config.localPath}`);
|
|
178
|
+
repoDir = config.localPath;
|
|
179
|
+
shouldCleanup = false;
|
|
180
|
+
} else if (config.repoUrl) {
|
|
181
|
+
repoDir = path.join(os.tmpdir(), `dockit-source-${Date.now()}`);
|
|
182
|
+
await cloneRepo(config.repoUrl, repoDir, config.branch, log);
|
|
183
|
+
shouldCleanup = true;
|
|
184
|
+
} else if (config.zipPath) {
|
|
185
|
+
repoDir = path.join(os.tmpdir(), `dockit-source-${Date.now()}`);
|
|
186
|
+
fs.mkdirSync(repoDir, { recursive: true });
|
|
187
|
+
await extractZip(config.zipPath, repoDir, log);
|
|
188
|
+
shouldCleanup = true;
|
|
189
|
+
} else {
|
|
190
|
+
throw new Error('Source code source requires repoUrl, localPath, or zipPath');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
const targetDir = config.sourcePath ? path.join(repoDir, config.sourcePath) : repoDir;
|
|
195
|
+
if (!fs.existsSync(targetDir)) {
|
|
196
|
+
throw new Error(`Source path not found: ${config.sourcePath || '(root)'}`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
fs.mkdirSync(sourceDir, { recursive: true });
|
|
200
|
+
copyDirContents(targetDir, sourceDir, log);
|
|
201
|
+
|
|
202
|
+
log('Ensuring Graphify is available...');
|
|
203
|
+
ensureGraphify(log);
|
|
204
|
+
|
|
205
|
+
const graphifyDir = config.graphifySourcePath
|
|
206
|
+
? path.join(repoDir, config.graphifySourcePath)
|
|
207
|
+
: config.sourcePath
|
|
208
|
+
? targetDir
|
|
209
|
+
: repoDir;
|
|
210
|
+
if (!fs.existsSync(graphifyDir)) {
|
|
211
|
+
throw new Error(`Graphify target not found: ${config.graphifySourcePath || config.sourcePath || '(root)'}`);
|
|
212
|
+
}
|
|
213
|
+
log(`Running Graphify on ${graphifyDir}...`);
|
|
214
|
+
await execWithTimeout('graphify', ['update', graphifyDir], log);
|
|
215
|
+
|
|
216
|
+
const graphOutDir = path.join(graphifyDir, 'graphify-out');
|
|
217
|
+
const graphJsonPath = path.join(graphOutDir, 'graph.json');
|
|
218
|
+
const graphHtmlPath = path.join(graphOutDir, 'graph.html');
|
|
219
|
+
|
|
220
|
+
if (fs.existsSync(graphJsonPath)) {
|
|
221
|
+
const destJson = path.join(entryDir, 'graph.json');
|
|
222
|
+
fs.copyFileSync(graphJsonPath, destJson);
|
|
223
|
+
log(`Copied graph.json to ${destJson}`);
|
|
224
|
+
|
|
225
|
+
const meta = readGraphMetadata(destJson);
|
|
226
|
+
log(`Graph: ${meta.nodeCount} nodes, ${meta.edgeCount} edges, ${meta.communities} communities`);
|
|
227
|
+
} else {
|
|
228
|
+
log('WARNING: graph.json not produced by Graphify');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (fs.existsSync(graphHtmlPath)) {
|
|
232
|
+
const destHtml = path.join(entryDir, 'graph.html');
|
|
233
|
+
fs.copyFileSync(graphHtmlPath, destHtml);
|
|
234
|
+
log(`Copied graph.html to ${destHtml}`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return sourceDir;
|
|
238
|
+
} finally {
|
|
239
|
+
if (shouldCleanup && fs.existsSync(repoDir)) {
|
|
240
|
+
log('Cleaning up cloned repository...');
|
|
241
|
+
fs.rmSync(repoDir, { recursive: true, force: true });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function copyDirContents(srcDir: string, destDir: string, log: (msg: string) => void): void {
|
|
248
|
+
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
249
|
+
for (const entry of entries) {
|
|
250
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules' || entry.name === 'target') continue;
|
|
251
|
+
const srcPath = path.join(srcDir, entry.name);
|
|
252
|
+
const destPath = path.join(destDir, entry.name);
|
|
253
|
+
if (entry.isDirectory()) {
|
|
254
|
+
fs.mkdirSync(destPath, { recursive: true });
|
|
255
|
+
copyDirContents(srcPath, destPath, log);
|
|
256
|
+
} else {
|
|
257
|
+
fs.copyFileSync(srcPath, destPath);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ISourceProcessor } from '../../core/ports/ISourceProcessor.js';
|
|
2
|
+
import type { Source, ZipSourceConfig } from '../../core/domain/types.js';
|
|
3
|
+
import { downloadAndExtractZip } from '../../services/zip.js';
|
|
4
|
+
|
|
5
|
+
export class ZipSourceProcessor implements ISourceProcessor {
|
|
6
|
+
readonly sourceType = 'zip' as const;
|
|
7
|
+
|
|
8
|
+
async process(source: Source, sourceDir: string, _entryDir: string, _entryId: string, log: (msg: string) => void): Promise<string> {
|
|
9
|
+
await downloadAndExtractZip(source.config as ZipSourceConfig, sourceDir, log);
|
|
10
|
+
return sourceDir;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Dockit MCP HTTP Bridge
|
|
4
|
+
* Proxies JSON-RPC requests over HTTP to the Dockit MCP stdio server.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* node --import=tsx apps/server/src/mcp-http.ts [port]
|
|
8
|
+
* DOCKIT_MCP_HTTP_PORT=3456 npx tsx apps/server/src/mcp-http.ts
|
|
9
|
+
*
|
|
10
|
+
* Then curl:
|
|
11
|
+
* curl -X POST http://localhost:3456 \
|
|
12
|
+
* -H "Content-Type: application/json" \
|
|
13
|
+
* -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { spawn } from 'node:child_process';
|
|
17
|
+
import http from 'node:http';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
|
|
21
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
const MCP_SCRIPT = path.join(__dirname, 'mcp.ts');
|
|
23
|
+
const PORT = parseInt(process.env.DOCKIT_MCP_HTTP_PORT || process.argv[2] || '3456', 10);
|
|
24
|
+
|
|
25
|
+
// Spawn the stdio MCP server
|
|
26
|
+
const mcp = spawn('npx', ['tsx', MCP_SCRIPT], {
|
|
27
|
+
cwd: path.join(__dirname, '..', '..', '..'),
|
|
28
|
+
env: { ...process.env, PATH: process.env.PATH },
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
let buffer = '';
|
|
32
|
+
const pending = new Map<number | string, (response: object) => void>();
|
|
33
|
+
|
|
34
|
+
mcp.stdout.on('data', (data: Buffer) => {
|
|
35
|
+
buffer += data.toString('utf-8');
|
|
36
|
+
let lines = buffer.split('\n');
|
|
37
|
+
buffer = lines.pop() || '';
|
|
38
|
+
for (const line of lines) {
|
|
39
|
+
const trimmed = line.trim();
|
|
40
|
+
if (!trimmed) continue;
|
|
41
|
+
try {
|
|
42
|
+
const msg = JSON.parse(trimmed);
|
|
43
|
+
if (msg.id !== undefined && pending.has(msg.id)) {
|
|
44
|
+
pending.get(msg.id)!(msg);
|
|
45
|
+
pending.delete(msg.id);
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
// ignore non-JSON lines
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
mcp.stderr.on('data', (data: Buffer) => {
|
|
54
|
+
process.stderr.write(data);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
mcp.on('exit', (code) => {
|
|
58
|
+
console.error(`[mcp-http] MCP server exited with code ${code}`);
|
|
59
|
+
process.exit(code ?? 1);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// HTTP server
|
|
63
|
+
const server = http.createServer((req, res) => {
|
|
64
|
+
if (req.method !== 'POST') {
|
|
65
|
+
res.writeHead(405, { 'Content-Type': 'application/json' });
|
|
66
|
+
res.end(JSON.stringify({ error: 'Method not allowed' }));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let body = '';
|
|
71
|
+
req.on('data', (chunk) => { body += chunk; });
|
|
72
|
+
req.on('end', () => {
|
|
73
|
+
try {
|
|
74
|
+
const request = JSON.parse(body);
|
|
75
|
+
const id = request.id ?? Math.random().toString(36).slice(2);
|
|
76
|
+
request.id = id;
|
|
77
|
+
|
|
78
|
+
const timeout = setTimeout(() => {
|
|
79
|
+
if (pending.has(id)) {
|
|
80
|
+
pending.delete(id);
|
|
81
|
+
res.writeHead(504, { 'Content-Type': 'application/json' });
|
|
82
|
+
res.end(JSON.stringify({ jsonrpc: '2.0', id, error: { code: -32000, message: 'Request timeout' } }));
|
|
83
|
+
}
|
|
84
|
+
}, 30000);
|
|
85
|
+
|
|
86
|
+
pending.set(id, (response) => {
|
|
87
|
+
clearTimeout(timeout);
|
|
88
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
89
|
+
res.end(JSON.stringify(response));
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
mcp.stdin.write(JSON.stringify(request) + '\n');
|
|
93
|
+
} catch (err) {
|
|
94
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
95
|
+
res.end(JSON.stringify({ error: (err as Error).message }));
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
server.listen(PORT, () => {
|
|
101
|
+
console.error(`[mcp-http] Bridge listening on http://localhost:${PORT}`);
|
|
102
|
+
});
|