@esankhan3/code-search-mcp 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +333 -0
  3. package/dist/core/ast-graph-builder.js +800 -0
  4. package/dist/core/chunker.js +368 -0
  5. package/dist/core/claude-runner.js +173 -0
  6. package/dist/core/config.js +59 -0
  7. package/dist/core/cross-repo-detector.js +887 -0
  8. package/dist/core/embedder.js +334 -0
  9. package/dist/core/env-config.js +86 -0
  10. package/dist/core/file-walker.js +186 -0
  11. package/dist/core/git-diff.js +160 -0
  12. package/dist/core/graph-metrics.js +233 -0
  13. package/dist/core/indexer.js +552 -0
  14. package/dist/core/project-graph-builder.js +202 -0
  15. package/dist/core/query-classifier.js +144 -0
  16. package/dist/core/query-router.js +210 -0
  17. package/dist/core/rag-evaluator.js +213 -0
  18. package/dist/core/repo-profiler.js +383 -0
  19. package/dist/core/reranker.js +218 -0
  20. package/dist/core/retriever.js +242 -0
  21. package/dist/core/semantic-edge-detector.js +147 -0
  22. package/dist/core/service-mesh-inferrer.js +241 -0
  23. package/dist/core/structural-hasher.js +316 -0
  24. package/dist/core/tree-sitter-parser.js +539 -0
  25. package/dist/core/types.js +0 -0
  26. package/dist/core/vector-store.js +259 -0
  27. package/dist/core/workspace-detector.js +481 -0
  28. package/dist/index.js +177 -0
  29. package/dist/middleware/auth.js +109 -0
  30. package/dist/resources/resources.js +79 -0
  31. package/dist/server.js +242 -0
  32. package/dist/sources/github-org.js +109 -0
  33. package/dist/sources/local-path.js +44 -0
  34. package/dist/tools/graph.js +193 -0
  35. package/dist/tools/index-tools.js +43 -0
  36. package/dist/tools/profile.js +88 -0
  37. package/dist/tools/search.js +84 -0
  38. package/dist/transports/http-transport.js +162 -0
  39. package/dist/transports/remote-proxy.js +152 -0
  40. package/package.json +33 -0
@@ -0,0 +1,109 @@
1
+ import { createHmac, timingSafeEqual } from "node:crypto";
2
+ const rateBuckets = /* @__PURE__ */ new Map();
3
+ function checkRateLimit(identity, maxPerMinute) {
4
+ const now = Date.now();
5
+ const bucket = rateBuckets.get(identity);
6
+ if (!bucket || now > bucket.resetAt) {
7
+ rateBuckets.set(identity, { count: 1, resetAt: now + 6e4 });
8
+ return true;
9
+ }
10
+ if (bucket.count >= maxPerMinute) return false;
11
+ bucket.count++;
12
+ return true;
13
+ }
14
+ setInterval(() => {
15
+ const now = Date.now();
16
+ for (const [key, bucket] of rateBuckets) {
17
+ if (now > bucket.resetAt) rateBuckets.delete(key);
18
+ }
19
+ }, 3e5).unref();
20
+ function verifyJwt(token, secret, issuer) {
21
+ try {
22
+ const parts = token.split(".");
23
+ if (parts.length !== 3) return null;
24
+ const header = JSON.parse(Buffer.from(parts[0], "base64url").toString());
25
+ if (header.alg !== "HS256") return null;
26
+ const data = `${parts[0]}.${parts[1]}`;
27
+ const expected = createHmac("sha256", secret).update(data).digest("base64url");
28
+ const sigBuf = Buffer.from(parts[2], "base64url");
29
+ const expectedBuf = Buffer.from(expected, "base64url");
30
+ if (sigBuf.length !== expectedBuf.length || !timingSafeEqual(sigBuf, expectedBuf)) {
31
+ return null;
32
+ }
33
+ const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString());
34
+ if (payload.exp && payload.exp < Math.floor(Date.now() / 1e3)) return null;
35
+ if (issuer && payload.iss !== issuer) return null;
36
+ return { sub: payload.sub ?? "unknown", scope: payload.scope };
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+ function safeCompare(a, b) {
42
+ const bufA = Buffer.from(a);
43
+ const bufB = Buffer.from(b);
44
+ if (bufA.length !== bufB.length) return false;
45
+ return timingSafeEqual(bufA, bufB);
46
+ }
47
+ function createAuthMiddleware(config) {
48
+ return function authenticate(req, res) {
49
+ if (config.authMode === "none") {
50
+ return { mode: "anonymous", subject: "anonymous", scopes: ["*"] };
51
+ }
52
+ const authHeader = req.headers.authorization;
53
+ if (!authHeader?.startsWith("Bearer ")) {
54
+ res.writeHead(401, { "Content-Type": "application/json" });
55
+ res.end(JSON.stringify({ error: "Missing Authorization header" }));
56
+ return null;
57
+ }
58
+ const token = authHeader.slice(7);
59
+ if (config.authMode === "api-key") {
60
+ const matched = config.authApiKeys.some((key) => safeCompare(token, key));
61
+ if (!matched) {
62
+ res.writeHead(401, { "Content-Type": "application/json" });
63
+ res.end(JSON.stringify({ error: "Invalid API key" }));
64
+ return null;
65
+ }
66
+ const identity = {
67
+ mode: "api-key",
68
+ subject: `key:${token.slice(0, 8)}...`,
69
+ scopes: ["*"]
70
+ };
71
+ if (!checkRateLimit(identity.subject, config.rateLimitPerMinute)) {
72
+ res.writeHead(429, { "Content-Type": "application/json" });
73
+ res.end(JSON.stringify({ error: "Rate limit exceeded" }));
74
+ return null;
75
+ }
76
+ return identity;
77
+ }
78
+ if (config.authMode === "jwt") {
79
+ if (!config.authJwtSecret) {
80
+ res.writeHead(500, { "Content-Type": "application/json" });
81
+ res.end(JSON.stringify({ error: "JWT secret not configured" }));
82
+ return null;
83
+ }
84
+ const claims = verifyJwt(token, config.authJwtSecret, config.authJwtIssuer);
85
+ if (!claims) {
86
+ res.writeHead(401, { "Content-Type": "application/json" });
87
+ res.end(JSON.stringify({ error: "Invalid or expired JWT" }));
88
+ return null;
89
+ }
90
+ const identity = {
91
+ mode: "jwt",
92
+ subject: claims.sub,
93
+ scopes: claims.scope ? claims.scope.split(" ") : ["*"]
94
+ };
95
+ if (!checkRateLimit(identity.subject, config.rateLimitPerMinute)) {
96
+ res.writeHead(429, { "Content-Type": "application/json" });
97
+ res.end(JSON.stringify({ error: "Rate limit exceeded" }));
98
+ return null;
99
+ }
100
+ return identity;
101
+ }
102
+ res.writeHead(500, { "Content-Type": "application/json" });
103
+ res.end(JSON.stringify({ error: `Unknown auth mode: ${config.authMode}` }));
104
+ return null;
105
+ };
106
+ }
107
+ export {
108
+ createAuthMiddleware
109
+ };
@@ -0,0 +1,79 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { getKnowledgeBasePath } from "../core/config.js";
4
+ import { loadAllProfiles } from "../core/repo-profiler.js";
5
+ import { loadProfile } from "../core/repo-profiler.js";
6
+ function registerResources(ctx) {
7
+ return [
8
+ {
9
+ uri: `code-search://repos`,
10
+ name: "All Repos",
11
+ description: `List of all repos in project "${ctx.projectName}"`,
12
+ mimeType: "application/json"
13
+ },
14
+ {
15
+ uri: `code-search://system-graph`,
16
+ name: "System Graph",
17
+ description: "Unified knowledge graph with cross-repo edges",
18
+ mimeType: "application/json"
19
+ }
20
+ ];
21
+ }
22
+ async function handleResource(uri, ctx) {
23
+ try {
24
+ const kbPath = getKnowledgeBasePath(ctx.projectName);
25
+ if (uri === "code-search://repos") {
26
+ const profiles = loadAllProfiles(ctx.projectName);
27
+ return {
28
+ contents: [{
29
+ uri,
30
+ mimeType: "application/json",
31
+ text: JSON.stringify(profiles, null, 2)
32
+ }]
33
+ };
34
+ }
35
+ if (uri === "code-search://system-graph") {
36
+ const graphPath = join(kbPath, "system_graph_v2.json");
37
+ if (!existsSync(graphPath)) {
38
+ return { contents: [{ uri, mimeType: "application/json", text: '{"nodes":[],"edges":[]}' }] };
39
+ }
40
+ return {
41
+ contents: [{
42
+ uri,
43
+ mimeType: "application/json",
44
+ text: readFileSync(graphPath, "utf-8")
45
+ }]
46
+ };
47
+ }
48
+ const profileMatch = uri.match(/^code-search:\/\/repo\/([^/]+)\/profile$/);
49
+ if (profileMatch) {
50
+ const profile = loadProfile(ctx.projectName, profileMatch[1]);
51
+ return {
52
+ contents: [{
53
+ uri,
54
+ mimeType: "application/json",
55
+ text: profile ? JSON.stringify(profile, null, 2) : "{}"
56
+ }]
57
+ };
58
+ }
59
+ const graphMatch = uri.match(/^code-search:\/\/repo\/([^/]+)\/graph$/);
60
+ if (graphMatch) {
61
+ const graphPath = join(kbPath, graphMatch[1], "graph.json");
62
+ return {
63
+ contents: [{
64
+ uri,
65
+ mimeType: "application/json",
66
+ text: existsSync(graphPath) ? readFileSync(graphPath, "utf-8") : '{"nodes":[],"links":[]}'
67
+ }]
68
+ };
69
+ }
70
+ return { contents: [{ uri, mimeType: "text/plain", text: `Unknown resource: ${uri}` }] };
71
+ } catch (err) {
72
+ const msg = err instanceof Error ? err.message : String(err);
73
+ return { contents: [{ uri, mimeType: "text/plain", text: `Error: ${msg}` }] };
74
+ }
75
+ }
76
+ export {
77
+ handleResource,
78
+ registerResources
79
+ };
package/dist/server.js ADDED
@@ -0,0 +1,242 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import {
4
+ CallToolRequestSchema,
5
+ ListToolsRequestSchema,
6
+ ListResourcesRequestSchema,
7
+ ReadResourceRequestSchema
8
+ } from "@modelcontextprotocol/sdk/types.js";
9
+ import { existsSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ import { registerSearchTools, handleSearchTool } from "./tools/search.js";
12
+ import { registerGraphTools, handleGraphTool } from "./tools/graph.js";
13
+ import { registerProfileTools, handleProfileTool } from "./tools/profile.js";
14
+ import { registerIndexTools, handleIndexTool } from "./tools/index-tools.js";
15
+ import { registerResources, handleResource } from "./resources/resources.js";
16
+ import { getKnowledgeBasePath } from "./core/config.js";
17
+ import { indexFromPath } from "./core/indexer.js";
18
+ import { loadServerConfig } from "./core/env-config.js";
19
+ import { startHttpTransport } from "./transports/http-transport.js";
20
+ function createMcpServerInstance(ctx) {
21
+ const server = new Server(
22
+ { name: "code-search-mcp", version: "0.1.0" },
23
+ { capabilities: { tools: {}, resources: {} } }
24
+ );
25
+ const allTools = [
26
+ ...registerSearchTools(),
27
+ ...registerGraphTools(),
28
+ ...registerProfileTools(),
29
+ ...registerIndexTools()
30
+ ];
31
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
32
+ tools: allTools
33
+ }));
34
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
35
+ const name = request.params.name;
36
+ const args = request.params.arguments ?? {};
37
+ const searchResult = await handleSearchTool(name, args, ctx);
38
+ if (searchResult) return searchResult;
39
+ const graphResult = await handleGraphTool(name, args, ctx);
40
+ if (graphResult) return graphResult;
41
+ const profileResult = await handleProfileTool(name, args, ctx);
42
+ if (profileResult) return profileResult;
43
+ const indexResult = await handleIndexTool(name, args, ctx);
44
+ if (indexResult) return indexResult;
45
+ return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
46
+ });
47
+ const allResources = registerResources(ctx);
48
+ server.setRequestHandler(ListResourcesRequestSchema, async () => ({
49
+ resources: allResources
50
+ }));
51
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
52
+ return handleResource(request.params.uri, ctx);
53
+ });
54
+ return server;
55
+ }
56
+ async function startServer(projectName, directoryPath) {
57
+ const config = loadServerConfig();
58
+ const ctx = {
59
+ projectName,
60
+ directoryPath,
61
+ indexReady: false,
62
+ startedAt: Date.now(),
63
+ indexing: {
64
+ status: "idle",
65
+ phase: null,
66
+ message: null,
67
+ percent: 0,
68
+ startedAt: null,
69
+ error: null,
70
+ lastSuccess: null,
71
+ lastDurationMs: 0,
72
+ history: []
73
+ }
74
+ };
75
+ const llmInfo = config.llmMode === "none" ? "disabled (profiling + service mesh skipped)" : config.llmMode === "api" ? `api \u2192 ${config.llmProvider}/${config.llmModel}${config.llmApiKey ? "" : " (WARNING: no API key!)"}` : `cli \u2192 ${config.claudeBin}`;
76
+ console.error(`[code-search-mcp] LLM: ${llmInfo}`);
77
+ await autoIndex(ctx);
78
+ if (config.transport === "stdio") {
79
+ const server = createMcpServerInstance(ctx);
80
+ const transport = new StdioServerTransport();
81
+ await server.connect(transport);
82
+ console.error(`[code-search-mcp] Server running for "${projectName}" (stdio)`);
83
+ } else {
84
+ if (config.authMode === "none" && config.host !== "127.0.0.1" && config.host !== "localhost") {
85
+ console.error(`[code-search-mcp] WARNING: Auth is disabled but server binds to ${config.host}. Any machine on the network can access your code search API.`);
86
+ console.error(`[code-search-mcp] Set CODE_SEARCH_AUTH_MODE=api-key or CODE_SEARCH_HOST=127.0.0.1 for security.`);
87
+ }
88
+ await startHttpTransport({
89
+ config,
90
+ createMcpServer: async () => ({
91
+ server: createMcpServerInstance(ctx)
92
+ }),
93
+ onReady: (url) => {
94
+ console.error(`[code-search-mcp] Server running for "${projectName}" at ${url}/mcp`);
95
+ console.error(`[code-search-mcp] Health: GET ${url}/health`);
96
+ console.error(`[code-search-mcp] Status: GET ${url}/status`);
97
+ console.error(`[code-search-mcp] Index: POST ${url}/index`);
98
+ console.error(`[code-search-mcp] Auth: ${config.authMode}`);
99
+ },
100
+ getHealth: () => ({
101
+ project: ctx.projectName,
102
+ indexReady: ctx.indexReady,
103
+ indexing: ctx.indexing.status,
104
+ uptime: Math.floor((Date.now() - ctx.startedAt) / 1e3),
105
+ transport: config.transport,
106
+ authMode: config.authMode
107
+ }),
108
+ getStatus: () => ({
109
+ project: ctx.projectName,
110
+ directoryPath: ctx.directoryPath,
111
+ indexReady: ctx.indexReady,
112
+ uptime: Math.floor((Date.now() - ctx.startedAt) / 1e3),
113
+ indexing: {
114
+ ...ctx.indexing,
115
+ elapsedMs: ctx.indexing.startedAt ? Date.now() - ctx.indexing.startedAt : null
116
+ }
117
+ }),
118
+ onIndex: async (body) => {
119
+ const { resolve } = await import("node:path");
120
+ const { existsSync: existsSync2 } = await import("node:fs");
121
+ const dirPath = resolve(body.path);
122
+ if (!existsSync2(dirPath)) {
123
+ throw new Error(`Path does not exist: ${dirPath}`);
124
+ }
125
+ if (ctx.indexing.status === "indexing") {
126
+ throw new Error(`Indexing already in progress (phase: ${ctx.indexing.phase}). Wait for it to complete or check GET /status.`);
127
+ }
128
+ const project = body.project || dirPath.split("/").filter(Boolean).pop() || "project";
129
+ const stats = await trackedIndex(ctx, project, dirPath, {
130
+ force: body.force,
131
+ label: "admin-index"
132
+ });
133
+ ctx.projectName = project;
134
+ ctx.directoryPath = dirPath;
135
+ return {
136
+ status: "ok",
137
+ project,
138
+ path: dirPath,
139
+ chunks: stats.totalChunks,
140
+ repos: stats.repos.length,
141
+ crossRepoEdges: stats.crossRepoEdges,
142
+ durationMs: stats.indexDurationMs
143
+ };
144
+ }
145
+ });
146
+ }
147
+ const reindexIntervalMs = parseReindexInterval();
148
+ if (reindexIntervalMs > 0 && ctx.directoryPath) {
149
+ console.error(`[code-search-mcp] Auto-reindex every ${Math.round(reindexIntervalMs / 6e4)}m`);
150
+ setInterval(async () => {
151
+ if (!ctx.directoryPath || ctx.indexing.status === "indexing") return;
152
+ try {
153
+ await trackedIndex(ctx, ctx.projectName, ctx.directoryPath, { label: "auto-reindex" });
154
+ } catch (err) {
155
+ console.error(`[auto-reindex] Failed:`, err);
156
+ }
157
+ }, reindexIntervalMs).unref();
158
+ }
159
+ }
160
+ function parseReindexInterval() {
161
+ const raw = process.env.CODE_SEARCH_REINDEX_INTERVAL?.trim();
162
+ if (!raw || raw === "0" || raw === "none") return 0;
163
+ const match = raw.match(/^(\d+)(m|h)$/);
164
+ if (!match) {
165
+ console.error(`[code-search-mcp] Invalid REINDEX_INTERVAL "${raw}" \u2014 use "30m", "1h", etc. Disabling.`);
166
+ return 0;
167
+ }
168
+ const value = parseInt(match[1], 10);
169
+ const unit = match[2];
170
+ return unit === "h" ? value * 60 * 6e4 : value * 6e4;
171
+ }
172
+ const MAX_HISTORY = 50;
173
+ function pushHistory(ctx, type, message) {
174
+ ctx.indexing.history.push({ timestamp: (/* @__PURE__ */ new Date()).toISOString(), type, message });
175
+ if (ctx.indexing.history.length > MAX_HISTORY) {
176
+ ctx.indexing.history = ctx.indexing.history.slice(-MAX_HISTORY);
177
+ }
178
+ }
179
+ async function trackedIndex(ctx, project, dirPath, opts) {
180
+ const label = opts?.label ?? "index";
181
+ ctx.indexing.status = "indexing";
182
+ ctx.indexing.phase = "starting";
183
+ ctx.indexing.message = `Starting ${label}...`;
184
+ ctx.indexing.percent = 0;
185
+ ctx.indexing.startedAt = Date.now();
186
+ ctx.indexing.error = null;
187
+ pushHistory(ctx, "start", `${label}: started for "${project}" at ${dirPath}`);
188
+ try {
189
+ const stats = await indexFromPath(project, dirPath, {
190
+ force: opts?.force,
191
+ onProgress: (m) => {
192
+ ctx.indexing.message = m;
193
+ console.error(`[${label}] ${m}`);
194
+ },
195
+ onDetailedProgress: (p) => {
196
+ ctx.indexing.phase = p.phase;
197
+ ctx.indexing.percent = p.percent;
198
+ ctx.indexing.message = p.message;
199
+ }
200
+ });
201
+ ctx.indexReady = true;
202
+ ctx.indexing.status = "idle";
203
+ ctx.indexing.phase = null;
204
+ ctx.indexing.percent = 100;
205
+ ctx.indexing.lastSuccess = (/* @__PURE__ */ new Date()).toISOString();
206
+ ctx.indexing.lastDurationMs = stats.indexDurationMs;
207
+ ctx.indexing.message = `Completed: ${stats.totalChunks} chunks, ${stats.repos.length} repos in ${Math.round(stats.indexDurationMs / 1e3)}s`;
208
+ pushHistory(ctx, "complete", ctx.indexing.message);
209
+ return stats;
210
+ } catch (err) {
211
+ const msg = err instanceof Error ? err.message : String(err);
212
+ ctx.indexing.status = "error";
213
+ ctx.indexing.error = msg;
214
+ ctx.indexing.message = `Failed: ${msg}`;
215
+ pushHistory(ctx, "error", msg);
216
+ throw err;
217
+ }
218
+ }
219
+ async function autoIndex(ctx) {
220
+ try {
221
+ const kbPath = getKnowledgeBasePath(ctx.projectName);
222
+ const hasLanceDB = existsSync(join(kbPath, "lancedb"));
223
+ const hasGraph = existsSync(join(kbPath, "system_graph_v2.json"));
224
+ if (hasLanceDB && hasGraph) {
225
+ ctx.indexReady = true;
226
+ console.error(`[code-search-mcp] Index loaded for "${ctx.projectName}"`);
227
+ return;
228
+ }
229
+ if (!ctx.directoryPath) {
230
+ console.error(`[code-search-mcp] No index found and no directory path \u2014 tools will return empty results`);
231
+ return;
232
+ }
233
+ console.error(`[code-search-mcp] No index found \u2014 building from ${ctx.directoryPath}...`);
234
+ await trackedIndex(ctx, ctx.projectName, ctx.directoryPath, { label: "auto-index" });
235
+ console.error(`[code-search-mcp] Index ready.`);
236
+ } catch (err) {
237
+ console.error(`[code-search-mcp] Auto-index failed:`, err);
238
+ }
239
+ }
240
+ export {
241
+ startServer
242
+ };
@@ -0,0 +1,109 @@
1
+ import { execSync } from "node:child_process";
2
+ import { existsSync, mkdirSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { homedir } from "node:os";
5
+ async function cloneOrUpdateOrg(org, opts) {
6
+ const log = opts?.onProgress ?? ((m) => console.error(`[github] ${m}`));
7
+ const token = opts?.token ?? process.env.GITHUB_TOKEN;
8
+ const workspacePath = opts?.workspacePath ?? join(homedir(), ".code-search", org);
9
+ const maxRepos = opts?.maxRepos ?? 500;
10
+ mkdirSync(workspacePath, { recursive: true });
11
+ log(`Fetching repos from github:${org}...`);
12
+ let orgRepos;
13
+ try {
14
+ orgRepos = listReposViaGhCli(org, maxRepos);
15
+ log(`Found ${orgRepos.length} repos via gh CLI`);
16
+ } catch {
17
+ if (!token) {
18
+ throw new Error("gh CLI not available and no GITHUB_TOKEN set. Install gh CLI or set GITHUB_TOKEN.");
19
+ }
20
+ orgRepos = await listReposViaApi(org, token, maxRepos);
21
+ log(`Found ${orgRepos.length} repos via GitHub API`);
22
+ }
23
+ if (opts?.pattern) {
24
+ const pattern = opts.pattern.replace(/\*/g, ".*");
25
+ const re = new RegExp(`^${pattern}$`, "i");
26
+ orgRepos = orgRepos.filter((r) => re.test(r.name));
27
+ log(`Filtered to ${orgRepos.length} repos matching "${opts.pattern}"`);
28
+ }
29
+ const results = [];
30
+ for (let i = 0; i < orgRepos.length; i++) {
31
+ const repo = orgRepos[i];
32
+ const repoPath = join(workspacePath, repo.name);
33
+ if (existsSync(join(repoPath, ".git"))) {
34
+ try {
35
+ execSync("git pull --ff-only --quiet", {
36
+ cwd: repoPath,
37
+ stdio: "pipe",
38
+ timeout: 3e4
39
+ });
40
+ log(` [${i + 1}/${orgRepos.length}] Updated ${repo.name}`);
41
+ } catch {
42
+ log(` [${i + 1}/${orgRepos.length}] ${repo.name} \u2014 pull failed (skipping)`);
43
+ }
44
+ } else {
45
+ try {
46
+ const cloneCmd = token ? `git -c "http.extraHeader=Authorization: Bearer ${token}" clone --depth 1 --quiet "${repo.cloneUrl}" "${repoPath}"` : `git clone --depth 1 --quiet "${repo.cloneUrl}" "${repoPath}"`;
47
+ execSync(cloneCmd, {
48
+ stdio: "pipe",
49
+ timeout: 12e4
50
+ });
51
+ log(` [${i + 1}/${orgRepos.length}] Cloned ${repo.name}`);
52
+ } catch (err) {
53
+ const msg = err instanceof Error ? err.message : String(err);
54
+ log(` [${i + 1}/${orgRepos.length}] ${repo.name} \u2014 clone failed: ${msg.slice(0, 80)}`);
55
+ continue;
56
+ }
57
+ }
58
+ results.push({
59
+ name: repo.name,
60
+ path: repoPath,
61
+ language: repo.language || "unknown"
62
+ });
63
+ }
64
+ log(`Ready: ${results.length} repos in ${workspacePath}`);
65
+ return results;
66
+ }
67
+ function listReposViaGhCli(org, limit) {
68
+ const output = execSync(
69
+ `gh repo list "${org}" --json name,url,primaryLanguage --limit ${limit} --no-archived`,
70
+ { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 3e4 }
71
+ );
72
+ const repos = JSON.parse(output);
73
+ return repos.map((r) => ({
74
+ name: r.name,
75
+ cloneUrl: r.url.endsWith(".git") ? r.url : `${r.url}.git`,
76
+ language: (r.primaryLanguage?.name ?? "").toLowerCase()
77
+ }));
78
+ }
79
+ async function listReposViaApi(org, token, limit) {
80
+ const allRepos = [];
81
+ let page = 1;
82
+ while (allRepos.length < limit) {
83
+ const url = `https://api.github.com/orgs/${org}/repos?per_page=100&page=${page}&type=sources`;
84
+ const res = await fetch(url, {
85
+ headers: {
86
+ Authorization: `token ${token}`,
87
+ Accept: "application/vnd.github.v3+json"
88
+ }
89
+ });
90
+ if (!res.ok) {
91
+ throw new Error(`GitHub API error: ${res.status} ${res.statusText}`);
92
+ }
93
+ const repos = await res.json();
94
+ if (repos.length === 0) break;
95
+ for (const r of repos) {
96
+ if (r.archived) continue;
97
+ allRepos.push({
98
+ name: r.name,
99
+ cloneUrl: r.clone_url,
100
+ language: (r.language ?? "").toLowerCase()
101
+ });
102
+ }
103
+ page++;
104
+ }
105
+ return allRepos.slice(0, limit);
106
+ }
107
+ export {
108
+ cloneOrUpdateOrg
109
+ };
@@ -0,0 +1,44 @@
1
+ import { existsSync, readdirSync, statSync } from "node:fs";
2
+ import { join, basename } from "node:path";
3
+ const SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", "dist", ".next", "build", "__pycache__", ".venv", "vendor", "target"]);
4
+ function discoverLocalRepos(directoryPath) {
5
+ if (!existsSync(directoryPath)) return [];
6
+ const repos = [];
7
+ if (existsSync(join(directoryPath, ".git"))) {
8
+ repos.push({
9
+ name: basename(directoryPath),
10
+ path: directoryPath,
11
+ language: detectLanguage(directoryPath)
12
+ });
13
+ return repos;
14
+ }
15
+ try {
16
+ for (const entry of readdirSync(directoryPath)) {
17
+ if (entry.startsWith(".") || SKIP_DIRS.has(entry)) continue;
18
+ const fullPath = join(directoryPath, entry);
19
+ try {
20
+ if (!statSync(fullPath).isDirectory()) continue;
21
+ } catch {
22
+ continue;
23
+ }
24
+ if (!existsSync(join(fullPath, ".git"))) continue;
25
+ repos.push({ name: entry, path: fullPath, language: detectLanguage(fullPath) });
26
+ }
27
+ } catch {
28
+ }
29
+ return repos;
30
+ }
31
+ function detectLanguage(repoPath) {
32
+ if (existsSync(join(repoPath, "go.mod"))) return "go";
33
+ if (existsSync(join(repoPath, "Cargo.toml"))) return "rust";
34
+ if (existsSync(join(repoPath, "pom.xml")) || existsSync(join(repoPath, "build.gradle"))) return "java";
35
+ if (existsSync(join(repoPath, "composer.json"))) return "php";
36
+ if (existsSync(join(repoPath, "pyproject.toml")) || existsSync(join(repoPath, "setup.py"))) return "python";
37
+ if (existsSync(join(repoPath, "package.json"))) {
38
+ return existsSync(join(repoPath, "tsconfig.json")) ? "typescript" : "javascript";
39
+ }
40
+ return "unknown";
41
+ }
42
+ export {
43
+ discoverLocalRepos
44
+ };