mcp-coordinator 0.4.0 → 0.6.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 +938 -846
- package/dashboard/Dockerfile +19 -19
- package/dashboard/public/index.html +1201 -1178
- package/dist/cli/server/start.js +33 -0
- package/dist/src/agent-activity.js +6 -6
- package/dist/src/agent-registry.js +6 -6
- package/dist/src/announce-workflow.d.ts +1 -0
- package/dist/src/announce-workflow.js +28 -0
- package/dist/src/consultation.js +20 -20
- package/dist/src/database.js +191 -126
- package/dist/src/dependency-map.js +3 -3
- package/dist/src/file-tracker.d.ts +2 -0
- package/dist/src/file-tracker.js +13 -12
- package/dist/src/git-cochange-builder.d.ts +32 -0
- package/dist/src/git-cochange-builder.js +238 -0
- package/dist/src/http/handle-health.d.ts +1 -1
- package/dist/src/http/handle-health.js +26 -0
- package/dist/src/http/handle-rest.js +98 -2
- package/dist/src/http/utils.d.ts +0 -4
- package/dist/src/http/utils.js +16 -2
- package/dist/src/impact-scorer.d.ts +5 -1
- package/dist/src/impact-scorer.js +98 -8
- package/dist/src/introspection.js +1 -1
- package/dist/src/metrics.d.ts +5 -0
- package/dist/src/metrics.js +33 -0
- package/dist/src/path-normalize.d.ts +17 -0
- package/dist/src/path-normalize.js +38 -0
- package/dist/src/serve-http.js +41 -2
- package/dist/src/server-setup.d.ts +6 -0
- package/dist/src/server-setup.js +23 -3
- package/dist/src/tools/consultation-tools.js +4 -2
- package/dist/src/tree-sitter-extractor.d.ts +36 -0
- package/dist/src/tree-sitter-extractor.js +354 -0
- package/dist/src/working-files-tracker.d.ts +42 -0
- package/dist/src/working-files-tracker.js +111 -0
- package/package.json +100 -83
package/dist/src/metrics.d.ts
CHANGED
|
@@ -42,6 +42,11 @@ export declare class Metrics {
|
|
|
42
42
|
readonly threadsResolving: Gauge<string>;
|
|
43
43
|
readonly mqttListenersActive: Gauge<string>;
|
|
44
44
|
readonly sseClientsActive: Gauge<string>;
|
|
45
|
+
readonly workingFilesActive: Gauge<string>;
|
|
46
|
+
readonly gitCochangePairs: Gauge<string>;
|
|
47
|
+
readonly workingFilesStarts: Counter<"result">;
|
|
48
|
+
readonly treeSitterParseFailures: Counter<string>;
|
|
49
|
+
readonly gitCochangeBuilds: Counter<"outcome">;
|
|
45
50
|
constructor(opts?: MetricsOptions);
|
|
46
51
|
recordAnnounce(result: AnnounceResult): void;
|
|
47
52
|
recordThreadResolved(type: ResolutionType): void;
|
package/dist/src/metrics.js
CHANGED
|
@@ -14,6 +14,12 @@ export class Metrics {
|
|
|
14
14
|
threadsResolving;
|
|
15
15
|
mqttListenersActive;
|
|
16
16
|
sseClientsActive;
|
|
17
|
+
workingFilesActive;
|
|
18
|
+
gitCochangePairs;
|
|
19
|
+
// v0.6 counters
|
|
20
|
+
workingFilesStarts;
|
|
21
|
+
treeSitterParseFailures;
|
|
22
|
+
gitCochangeBuilds;
|
|
17
23
|
constructor(opts = {}) {
|
|
18
24
|
this.registry = new Registry();
|
|
19
25
|
if (opts.collectDefault !== false) {
|
|
@@ -72,6 +78,33 @@ export class Metrics {
|
|
|
72
78
|
help: "Current number of connected SSE clients",
|
|
73
79
|
registers: [this.registry],
|
|
74
80
|
});
|
|
81
|
+
this.workingFilesActive = new Gauge({
|
|
82
|
+
name: "mcp_coordinator_working_files_active",
|
|
83
|
+
help: "Current working_files row count",
|
|
84
|
+
registers: [this.registry],
|
|
85
|
+
});
|
|
86
|
+
this.workingFilesStarts = new Counter({
|
|
87
|
+
name: "mcp_coordinator_working_files_starts_total",
|
|
88
|
+
help: "Total working_files start calls",
|
|
89
|
+
labelNames: ["result"],
|
|
90
|
+
registers: [this.registry],
|
|
91
|
+
});
|
|
92
|
+
this.treeSitterParseFailures = new Counter({
|
|
93
|
+
name: "mcp_coordinator_tree_sitter_parse_failures_total",
|
|
94
|
+
help: "Tree-sitter parse failures",
|
|
95
|
+
registers: [this.registry],
|
|
96
|
+
});
|
|
97
|
+
this.gitCochangeBuilds = new Counter({
|
|
98
|
+
name: "mcp_coordinator_git_cochange_builds_total",
|
|
99
|
+
help: "git_cochange build attempts",
|
|
100
|
+
labelNames: ["outcome"],
|
|
101
|
+
registers: [this.registry],
|
|
102
|
+
});
|
|
103
|
+
this.gitCochangePairs = new Gauge({
|
|
104
|
+
name: "mcp_coordinator_git_cochange_pairs_total",
|
|
105
|
+
help: "Current git_cochange row count",
|
|
106
|
+
registers: [this.registry],
|
|
107
|
+
});
|
|
75
108
|
}
|
|
76
109
|
// ── Counter helpers (named methods make hook points obvious) ──
|
|
77
110
|
recordAnnounce(result) {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize a file path for matching/correctness — NOT security.
|
|
3
|
+
*
|
|
4
|
+
* Returns POSIX (forward slash), repo-relative when repoRoot is provided,
|
|
5
|
+
* lower-cased when the path is Windows-style (drive letter prefix in repoRoot
|
|
6
|
+
* or input, or backslash in input). Collapses ./ and .. segments via
|
|
7
|
+
* path.posix.normalize.
|
|
8
|
+
*
|
|
9
|
+
* The lowercase pass is anchored to path SHAPE rather than `process.platform`
|
|
10
|
+
* so a Linux coordinator processing paths from a Windows agent (or a CI run
|
|
11
|
+
* exercising Windows-shaped fixtures) still produces consistent canonical
|
|
12
|
+
* forms.
|
|
13
|
+
*
|
|
14
|
+
* Throws when an absolute path falls outside repoRoot. Security path
|
|
15
|
+
* traversal checks are separate (see path-guard.ts:safeJoinUnderRoot).
|
|
16
|
+
*/
|
|
17
|
+
export declare function normalizePath(repoRoot: string | null, input: string): string;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
/**
|
|
3
|
+
* Normalize a file path for matching/correctness — NOT security.
|
|
4
|
+
*
|
|
5
|
+
* Returns POSIX (forward slash), repo-relative when repoRoot is provided,
|
|
6
|
+
* lower-cased when the path is Windows-style (drive letter prefix in repoRoot
|
|
7
|
+
* or input, or backslash in input). Collapses ./ and .. segments via
|
|
8
|
+
* path.posix.normalize.
|
|
9
|
+
*
|
|
10
|
+
* The lowercase pass is anchored to path SHAPE rather than `process.platform`
|
|
11
|
+
* so a Linux coordinator processing paths from a Windows agent (or a CI run
|
|
12
|
+
* exercising Windows-shaped fixtures) still produces consistent canonical
|
|
13
|
+
* forms.
|
|
14
|
+
*
|
|
15
|
+
* Throws when an absolute path falls outside repoRoot. Security path
|
|
16
|
+
* traversal checks are separate (see path-guard.ts:safeJoinUnderRoot).
|
|
17
|
+
*/
|
|
18
|
+
export function normalizePath(repoRoot, input) {
|
|
19
|
+
const isWindowsStyle = (repoRoot != null && (/^[a-zA-Z]:/.test(repoRoot) || repoRoot.includes("\\"))) ||
|
|
20
|
+
/^[a-zA-Z]:/.test(input) ||
|
|
21
|
+
input.includes("\\");
|
|
22
|
+
let p = input.replace(/\\/g, "/");
|
|
23
|
+
if (repoRoot) {
|
|
24
|
+
const root = repoRoot.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
25
|
+
if (path.isAbsolute(input) || /^[a-zA-Z]:/.test(input)) {
|
|
26
|
+
const lowerP = isWindowsStyle ? p.toLowerCase() : p;
|
|
27
|
+
const lowerRoot = isWindowsStyle ? root.toLowerCase() : root;
|
|
28
|
+
if (!lowerP.startsWith(lowerRoot + "/") && lowerP !== lowerRoot) {
|
|
29
|
+
throw new Error(`path is outside repoRoot: ${input}`);
|
|
30
|
+
}
|
|
31
|
+
p = p.slice(root.length).replace(/^\/+/, "");
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
p = path.posix.normalize(p).replace(/^\.\//, "");
|
|
35
|
+
if (isWindowsStyle)
|
|
36
|
+
p = p.toLowerCase();
|
|
37
|
+
return p;
|
|
38
|
+
}
|
package/dist/src/serve-http.js
CHANGED
|
@@ -17,6 +17,8 @@ import { createLogger } from "./logger.js";
|
|
|
17
17
|
import { initAuth, authenticateRequest, createToken, refreshToken, revokeAgent, setAuthLogger, verifyToken } from "./auth.js";
|
|
18
18
|
import { safeJoinUnderRoot } from "./path-guard.js";
|
|
19
19
|
import { handleRest as handleRestExt } from "./http/handle-rest.js";
|
|
20
|
+
import { handleLivez, handleReadyz, handleHealth } from "./http/handle-health.js";
|
|
21
|
+
import { serveMetrics } from "./metrics.js";
|
|
20
22
|
import { parseBody as parseBodyShared, json as jsonShared } from "./http/utils.js";
|
|
21
23
|
import { getVersion } from "../cli/version.js";
|
|
22
24
|
const VERSION = getVersion();
|
|
@@ -83,7 +85,15 @@ async function handleRest(req, res) {
|
|
|
83
85
|
}
|
|
84
86
|
async function handleAuth(req, res) {
|
|
85
87
|
const url = req.url || "";
|
|
86
|
-
|
|
88
|
+
let body;
|
|
89
|
+
try {
|
|
90
|
+
body = await parseBody(req);
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
const e = err;
|
|
94
|
+
json(res, { error: e.message || "Invalid request" }, e.statusCode || 400);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
87
97
|
if (url === "/api/auth/register" && req.method === "POST") {
|
|
88
98
|
const { agent_name, registration_secret } = body;
|
|
89
99
|
if (!agent_name || !registration_secret) {
|
|
@@ -184,6 +194,8 @@ function handleSse(req, res) {
|
|
|
184
194
|
Connection: "keep-alive",
|
|
185
195
|
"Access-Control-Allow-Origin": "*",
|
|
186
196
|
});
|
|
197
|
+
services.metrics.incSseClients();
|
|
198
|
+
services.metrics.recordHttpRequest("/api/events", 200);
|
|
187
199
|
// Use Last-Event-ID for resumption, otherwise send last 50
|
|
188
200
|
const lastEventId = parseInt(req.headers["last-event-id"] || "0", 10);
|
|
189
201
|
const events = lastEventId > 0
|
|
@@ -217,6 +229,7 @@ function handleSse(req, res) {
|
|
|
217
229
|
// fires between close and unsubscribe can't write to a dead socket.
|
|
218
230
|
clearInterval(heartbeat);
|
|
219
231
|
unsubscribe();
|
|
232
|
+
services.metrics.decSseClients();
|
|
220
233
|
});
|
|
221
234
|
}
|
|
222
235
|
export async function startServer(opts) {
|
|
@@ -303,8 +316,21 @@ export async function startServer(opts) {
|
|
|
303
316
|
}
|
|
304
317
|
return;
|
|
305
318
|
}
|
|
319
|
+
else if (url === "/livez") {
|
|
320
|
+
handleLivez(req, res);
|
|
321
|
+
services.metrics.recordHttpRequest("/livez", 200);
|
|
322
|
+
}
|
|
323
|
+
else if (url === "/readyz") {
|
|
324
|
+
handleReadyz(req, res, services);
|
|
325
|
+
services.metrics.recordHttpRequest("/readyz", res.statusCode || 0);
|
|
326
|
+
}
|
|
306
327
|
else if (url === "/health") {
|
|
307
|
-
|
|
328
|
+
handleHealth(req, res);
|
|
329
|
+
services.metrics.recordHttpRequest("/health", 200);
|
|
330
|
+
}
|
|
331
|
+
else if (url === "/metrics" && req.method === "GET") {
|
|
332
|
+
await serveMetrics(req, res, services, services.metrics);
|
|
333
|
+
services.metrics.recordHttpRequest("/metrics", 200);
|
|
308
334
|
}
|
|
309
335
|
else if (url === "/api/events" && req.method === "GET") {
|
|
310
336
|
handleSse(req, res);
|
|
@@ -361,15 +387,18 @@ export async function startServer(opts) {
|
|
|
361
387
|
const authResult = await authenticateRequest(req);
|
|
362
388
|
if (!authResult.ok) {
|
|
363
389
|
authLog.warn({ reason: authResult.error, url, ip: req.socket.remoteAddress }, "Auth rejected");
|
|
390
|
+
services.metrics.recordAuthRejected();
|
|
364
391
|
json(res, { error: authResult.error }, authResult.status);
|
|
365
392
|
return;
|
|
366
393
|
}
|
|
367
394
|
}
|
|
368
395
|
if (url.startsWith("/api/") && (req.method === "POST" || req.method === "GET")) {
|
|
369
396
|
await handleRest(req, res);
|
|
397
|
+
services.metrics.recordHttpRequest((url.split("?")[0] || ""), res.statusCode || 0);
|
|
370
398
|
}
|
|
371
399
|
else {
|
|
372
400
|
json(res, { error: "not found" }, 404);
|
|
401
|
+
services.metrics.recordHttpRequest((url.split("?")[0] || ""), 404);
|
|
373
402
|
}
|
|
374
403
|
}
|
|
375
404
|
}
|
|
@@ -418,6 +447,10 @@ export async function startServer(opts) {
|
|
|
418
447
|
services.mqttBridge.onOffline((agentId) => {
|
|
419
448
|
services.registry.setOffline(agentId);
|
|
420
449
|
services.consultation.handleAgentDeparture(agentId);
|
|
450
|
+
// Clear in-flight working_files AFTER consultation cleanup so any future
|
|
451
|
+
// consultation logic that might inspect working_files state for this agent
|
|
452
|
+
// sees the pre-cleanup view.
|
|
453
|
+
services.workingFiles.clearForAgent(agentId);
|
|
421
454
|
services.sseEmitter.emit("agent_offline", { agent_id: agentId });
|
|
422
455
|
});
|
|
423
456
|
// Wait for the HTTP server to be actually listening before resolving the
|
|
@@ -483,6 +516,12 @@ export async function startServer(opts) {
|
|
|
483
516
|
catch (err) {
|
|
484
517
|
log.warn({ err }, "Error stopping timeout sweeper");
|
|
485
518
|
}
|
|
519
|
+
try {
|
|
520
|
+
services.workingFiles.stopSweeper();
|
|
521
|
+
}
|
|
522
|
+
catch (err) {
|
|
523
|
+
log.warn({ err }, "Error stopping working-files sweeper");
|
|
524
|
+
}
|
|
486
525
|
try {
|
|
487
526
|
const { closeDb } = await import("./database.js");
|
|
488
527
|
closeDb?.();
|
|
@@ -11,7 +11,10 @@ import { SseEmitter } from "./sse-emitter.js";
|
|
|
11
11
|
import { MqttBridge } from "./mqtt-bridge.js";
|
|
12
12
|
import { AgentActivityTracker } from "./agent-activity.js";
|
|
13
13
|
import { QuotaCache } from "./quota/quota-cache.js";
|
|
14
|
+
import { WorkingFilesTracker } from "./working-files-tracker.js";
|
|
14
15
|
import { Metrics } from "./metrics.js";
|
|
16
|
+
import { TreeSitterExtractor } from "./tree-sitter-extractor.js";
|
|
17
|
+
import { GitCochangeBuilder } from "./git-cochange-builder.js";
|
|
15
18
|
import type { CoordinatorConfig } from "./types.js";
|
|
16
19
|
import { type Logger } from "./logger.js";
|
|
17
20
|
export interface CoordinatorServices {
|
|
@@ -23,12 +26,15 @@ export interface CoordinatorServices {
|
|
|
23
26
|
depMap: DependencyMapper;
|
|
24
27
|
fileTracker: FileTracker;
|
|
25
28
|
impactScorer: ImpactScorer;
|
|
29
|
+
workingFiles: WorkingFilesTracker;
|
|
26
30
|
introspection: IntrospectionManager;
|
|
27
31
|
contextProvider: SummaryContextProvider;
|
|
28
32
|
sseEmitter: SseEmitter;
|
|
29
33
|
mqttBridge: MqttBridge;
|
|
30
34
|
quotaCache: QuotaCache;
|
|
31
35
|
metrics: Metrics;
|
|
36
|
+
treeSitter: TreeSitterExtractor;
|
|
37
|
+
gitCochange: GitCochangeBuilder | null;
|
|
32
38
|
}
|
|
33
39
|
/** Create shared services (once at startup). */
|
|
34
40
|
export declare function createServices(config: CoordinatorConfig): CoordinatorServices;
|
package/dist/src/server-setup.js
CHANGED
|
@@ -18,7 +18,10 @@ import { SseEmitter } from "./sse-emitter.js";
|
|
|
18
18
|
import { MqttBridge } from "./mqtt-bridge.js";
|
|
19
19
|
import { AgentActivityTracker } from "./agent-activity.js";
|
|
20
20
|
import { QuotaCache } from "./quota/quota-cache.js";
|
|
21
|
+
import { WorkingFilesTracker } from "./working-files-tracker.js";
|
|
21
22
|
import { Metrics } from "./metrics.js";
|
|
23
|
+
import { TreeSitterExtractor } from "./tree-sitter-extractor.js";
|
|
24
|
+
import { GitCochangeBuilder } from "./git-cochange-builder.js";
|
|
22
25
|
import { createLogger } from "./logger.js";
|
|
23
26
|
import { getVersion } from "../cli/version.js";
|
|
24
27
|
const VERSION = getVersion();
|
|
@@ -26,18 +29,35 @@ const VERSION = getVersion();
|
|
|
26
29
|
export function createServices(config) {
|
|
27
30
|
initDatabase(config.dataDir);
|
|
28
31
|
const logger = createLogger();
|
|
32
|
+
const metrics = new Metrics();
|
|
29
33
|
const registry = new AgentRegistry();
|
|
30
34
|
const activityTracker = new AgentActivityTracker(registry);
|
|
31
35
|
const consultation = new Consultation(logger.child({ component: "consultation" }));
|
|
32
36
|
const depMap = new DependencyMapper();
|
|
33
37
|
const fileTracker = new FileTracker();
|
|
34
|
-
const
|
|
38
|
+
const workingFiles = new WorkingFilesTracker(logger.child({ component: "working-files" }), metrics);
|
|
39
|
+
workingFiles.startSweeper(parseInt(process.env.COORDINATOR_WORKING_FILES_SWEEP_INTERVAL_MS || "60000", 10));
|
|
40
|
+
const impactScorer = new ImpactScorer(registry, fileTracker, consultation, workingFiles);
|
|
35
41
|
const introspection = new IntrospectionManager();
|
|
36
42
|
const conflictDetector = new ConflictDetector(consultation, depMap, fileTracker, logger.child({ component: "conflict" }));
|
|
37
43
|
const contextProvider = new SummaryContextProvider(registry, consultation, fileTracker);
|
|
38
44
|
const sseEmitter = new SseEmitter();
|
|
39
45
|
const mqttBridge = new MqttBridge(logger.child({ component: "mqtt" }));
|
|
40
|
-
const
|
|
46
|
+
const treeSitter = new TreeSitterExtractor(metrics);
|
|
47
|
+
treeSitter.load().catch(() => { });
|
|
48
|
+
const repoRoot = process.env.COORDINATOR_REPO_ROOT;
|
|
49
|
+
const gitCochange = repoRoot
|
|
50
|
+
? new GitCochangeBuilder({
|
|
51
|
+
repoRoot,
|
|
52
|
+
logger: logger.child({ component: "gitcc" }),
|
|
53
|
+
metrics,
|
|
54
|
+
sinceDays: parseInt(process.env.COORDINATOR_LAYER4_SINCE_DAYS || "7", 10),
|
|
55
|
+
maxCount: parseInt(process.env.COORDINATOR_LAYER4_MAX_COMMITS || "2000", 10),
|
|
56
|
+
refreshMs: parseInt(process.env.COORDINATOR_LAYER4_REFRESH_INTERVAL_MS || "1800000", 10),
|
|
57
|
+
retryMs: parseInt(process.env.COORDINATOR_LAYER4_RETRY_MS || "300000", 10),
|
|
58
|
+
})
|
|
59
|
+
: null;
|
|
60
|
+
gitCochange?.startScheduler();
|
|
41
61
|
// Quota cache — macOS-only for now, Linux/Windows stubs return 503 via the
|
|
42
62
|
// /api/quota handler so raids keep running without a quota guardrail there.
|
|
43
63
|
// onRefresh fans the new data out to dashboard (SSE) + any live listener (MQTT)
|
|
@@ -88,7 +108,7 @@ export function createServices(config) {
|
|
|
88
108
|
});
|
|
89
109
|
return {
|
|
90
110
|
logger, registry, activityTracker, consultation, conflictDetector,
|
|
91
|
-
depMap, fileTracker, impactScorer, introspection, contextProvider, sseEmitter, mqttBridge, quotaCache, metrics,
|
|
111
|
+
depMap, fileTracker, impactScorer, workingFiles, introspection, contextProvider, sseEmitter, mqttBridge, quotaCache, metrics, treeSitter, gitCochange,
|
|
92
112
|
};
|
|
93
113
|
}
|
|
94
114
|
/** Create a new McpServer bound to the shared services (one per MCP session). */
|
|
@@ -30,7 +30,9 @@ export function registerConsultationTools(server, services, mcpLog) {
|
|
|
30
30
|
exports_affected: z.array(z.string()).optional(),
|
|
31
31
|
keep_open: z.boolean().optional().describe("Keep thread open even if no agents are concerned (for manual coordination like games or debates)"),
|
|
32
32
|
assigned_to: z.string().optional().describe("Directed-dispatch: only this agent_id will be allowed to claim the thread. Use for lead→worker handoffs in maitre/chaine/relais presets. Implies keep_open=true."),
|
|
33
|
-
|
|
33
|
+
target_symbols: z.array(z.string().max(256)).max(200).optional()
|
|
34
|
+
.describe("Qualified symbol names you intend to touch (e.g. 'UserService.getById'). Used by Layer 0.5 to annotate same-file overlaps."),
|
|
35
|
+
}, async ({ agent_id, subject, plan, target_modules, target_files, depends_on_files, exports_affected, keep_open, assigned_to, target_symbols }) => {
|
|
34
36
|
mcpLog.info({ tool: "announce_work", agent_id, subject, target_modules, target_files, assigned_to }, "Tool called");
|
|
35
37
|
const conflicts = conflictDetector.detect({ agent_id, target_modules, target_files });
|
|
36
38
|
const thread = consultation.announceWork({
|
|
@@ -41,7 +43,7 @@ export function registerConsultationTools(server, services, mcpLog) {
|
|
|
41
43
|
.run(JSON.stringify(conflicts), thread.id);
|
|
42
44
|
}
|
|
43
45
|
const { updated, categorized, respondents, planQuality } = runCommonAnnounceFlow(services, thread.id, {
|
|
44
|
-
agent_id, subject, plan, target_modules, target_files, depends_on_files, exports_affected, keep_open,
|
|
46
|
+
agent_id, subject, plan, target_modules, target_files, depends_on_files, exports_affected, keep_open, target_symbols,
|
|
45
47
|
});
|
|
46
48
|
sseEmitter.emit("thread_opened", {
|
|
47
49
|
thread_id: thread.id, initiator: agent_id, subject, target_modules, conflicts,
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Metrics } from "./metrics.js";
|
|
2
|
+
/**
|
|
3
|
+
* Tree-sitter symbol extractor.
|
|
4
|
+
*
|
|
5
|
+
* Loads grammars asynchronously at boot. extract() runs synchronously per call
|
|
6
|
+
* so it slots into the existing synchronous file_activity ingest path.
|
|
7
|
+
*
|
|
8
|
+
* Naming table per language documented in the v0.6 spec:
|
|
9
|
+
* - top-level fn / arrow assigned to const → `name`
|
|
10
|
+
* - class member → `Class.method`
|
|
11
|
+
* - anonymous default export → `<file_basename>:default`
|
|
12
|
+
* - re-exports, anonymous IIFE → not emitted
|
|
13
|
+
*/
|
|
14
|
+
export declare class TreeSitterExtractor {
|
|
15
|
+
private grammars;
|
|
16
|
+
private ready;
|
|
17
|
+
private grammarsLoaded;
|
|
18
|
+
private totalGrammars;
|
|
19
|
+
private metrics?;
|
|
20
|
+
constructor(metrics?: Metrics);
|
|
21
|
+
load(): Promise<void>;
|
|
22
|
+
status(): {
|
|
23
|
+
ok: boolean;
|
|
24
|
+
grammars_loaded: number;
|
|
25
|
+
total_grammars: number;
|
|
26
|
+
optional: true;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Extract qualified symbol names from `content`. Returns null on parse
|
|
30
|
+
* failure, unsupported extension, or grammar not loaded.
|
|
31
|
+
* Caps output at 200 entries (per spec).
|
|
32
|
+
*/
|
|
33
|
+
extract(filePath: string, content: string, _changedRanges: Array<[number, number]> | null): string[] | null;
|
|
34
|
+
private extToKey;
|
|
35
|
+
private walk;
|
|
36
|
+
}
|