code-graph-context 2.13.1 → 2.13.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/dist/cli/cli.js +4 -1
- package/dist/core/embeddings/embedding-sidecar.js +42 -2
- package/dist/core/embeddings/local-embeddings.service.js +7 -1
- package/dist/mcp/constants.js +27 -0
- package/dist/mcp/handlers/graph-generator.handler.js +71 -2
- package/dist/mcp/tools/parse-typescript-project.tool.js +16 -2
- package/dist/mcp/tools/swarm-cleanup.tool.js +3 -1
- package/dist/mcp/tools/swarm-message.tool.js +9 -35
- package/dist/mcp/tools/swarm-pheromone.tool.js +27 -2
- package/dist/mcp/tools/swarm-sense.tool.js +18 -1
- package/dist/mcp/workers/parse-coordinator.js +6 -1
- package/package.json +1 -1
package/dist/cli/cli.js
CHANGED
|
@@ -174,7 +174,10 @@ const installSidecarDeps = (sidecarDir) => {
|
|
|
174
174
|
const verifySidecar = (sidecarDir) => {
|
|
175
175
|
return new Promise((resolve) => {
|
|
176
176
|
const python = getSidecarPython(sidecarDir);
|
|
177
|
-
const test = spawnProcess(python, [
|
|
177
|
+
const test = spawnProcess(python, [
|
|
178
|
+
'-c',
|
|
179
|
+
`import transformers.modeling_utils as _mu; hasattr(_mu,"Conv1D") or setattr(_mu,"Conv1D",__import__("transformers.pytorch_utils",fromlist=["Conv1D"]).Conv1D); from sentence_transformers import SentenceTransformer; print("ok")`,
|
|
180
|
+
], {
|
|
178
181
|
cwd: sidecarDir,
|
|
179
182
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
180
183
|
});
|
|
@@ -25,9 +25,40 @@ export class EmbeddingSidecar {
|
|
|
25
25
|
stopping = false;
|
|
26
26
|
_exitHandler = null;
|
|
27
27
|
_idleTimer = null;
|
|
28
|
+
// Concurrency semaphore — model.encode() is GPU-bound and processes
|
|
29
|
+
// requests serially inside uvicorn. If 15 workers send requests at once,
|
|
30
|
+
// the first takes ~3s, the last waits ~45s and times out. By queuing
|
|
31
|
+
// excess requests here (no timeout pressure) we let only N through at a
|
|
32
|
+
// time, keeping each request well within the 60s timeout.
|
|
33
|
+
maxConcurrent = parseInt(process.env.EMBEDDING_MAX_CONCURRENT ?? '', 10) || 2;
|
|
34
|
+
inflight = 0;
|
|
35
|
+
waitQueue = [];
|
|
28
36
|
constructor(config = {}) {
|
|
29
37
|
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
30
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Wait for a concurrency slot. If under the limit, returns immediately.
|
|
41
|
+
* Otherwise parks the caller in a FIFO queue until a slot opens.
|
|
42
|
+
*/
|
|
43
|
+
acquireSlot() {
|
|
44
|
+
if (this.inflight < this.maxConcurrent) {
|
|
45
|
+
this.inflight++;
|
|
46
|
+
return; // fast path — no allocation, no Promise
|
|
47
|
+
}
|
|
48
|
+
return new Promise((resolve) => this.waitQueue.push(() => {
|
|
49
|
+
this.inflight++;
|
|
50
|
+
resolve();
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Release a slot, unblocking the next queued caller if any.
|
|
55
|
+
*/
|
|
56
|
+
releaseSlot() {
|
|
57
|
+
this.inflight--;
|
|
58
|
+
const next = this.waitQueue.shift();
|
|
59
|
+
if (next)
|
|
60
|
+
next(); // wake one waiter — it will increment inflight
|
|
61
|
+
}
|
|
31
62
|
get baseUrl() {
|
|
32
63
|
return `http://${this.config.host}:${this.config.port}`;
|
|
33
64
|
}
|
|
@@ -183,9 +214,19 @@ export class EmbeddingSidecar {
|
|
|
183
214
|
}
|
|
184
215
|
/**
|
|
185
216
|
* Embed an array of texts. Lazily starts the sidecar if not running.
|
|
217
|
+
* Concurrency-limited: at most `maxConcurrent` requests hit the sidecar
|
|
218
|
+
* at once. Excess callers wait in a FIFO queue (no timeout pressure).
|
|
186
219
|
*/
|
|
187
220
|
async embed(texts, gpuBatchSize) {
|
|
188
221
|
await this.start();
|
|
222
|
+
// Wait for a concurrency slot — the timeout only starts AFTER we
|
|
223
|
+
// acquire the slot, so queued requests don't eat into their timeout.
|
|
224
|
+
const queuedAt = Date.now();
|
|
225
|
+
await this.acquireSlot();
|
|
226
|
+
const queueMs = Date.now() - queuedAt;
|
|
227
|
+
if (queueMs > 100) {
|
|
228
|
+
console.error(`[embedding-sidecar] Waited ${queueMs}ms for concurrency slot (inflight=${this.inflight}, queued=${this.waitQueue.length})`);
|
|
229
|
+
}
|
|
189
230
|
const controller = new AbortController();
|
|
190
231
|
const timeout = setTimeout(() => controller.abort(), this.config.requestTimeoutMs);
|
|
191
232
|
const startTime = Date.now();
|
|
@@ -203,8 +244,6 @@ export class EmbeddingSidecar {
|
|
|
203
244
|
const detail = await res.text();
|
|
204
245
|
const isOOM = detail.toLowerCase().includes('out of memory');
|
|
205
246
|
if (res.status === 500 && isOOM) {
|
|
206
|
-
// OOM leaves GPU memory in a corrupted state — kill the sidecar
|
|
207
|
-
// so the next request spawns a fresh process with clean memory
|
|
208
247
|
console.error('[embedding-sidecar] OOM detected, restarting sidecar to reclaim GPU memory');
|
|
209
248
|
await this.stop();
|
|
210
249
|
}
|
|
@@ -230,6 +269,7 @@ export class EmbeddingSidecar {
|
|
|
230
269
|
}
|
|
231
270
|
finally {
|
|
232
271
|
clearTimeout(timeout);
|
|
272
|
+
this.releaseSlot(); // always release, even on error
|
|
233
273
|
}
|
|
234
274
|
}
|
|
235
275
|
/**
|
|
@@ -31,7 +31,13 @@ export class LocalEmbeddingsService {
|
|
|
31
31
|
const httpBatches = Math.ceil(texts.length / httpLimit);
|
|
32
32
|
const gpuBatchesPerRequest = Math.ceil(httpLimit / gpuBatchSize);
|
|
33
33
|
console.error(`[embedding] ${texts.length} texts → ${httpBatches} HTTP requests (http_limit=${httpLimit}, gpu_batch_size=${gpuBatchSize}, ~${gpuBatchesPerRequest} GPU batches/req)`);
|
|
34
|
-
await debugLog('Batch embedding started', {
|
|
34
|
+
await debugLog('Batch embedding started', {
|
|
35
|
+
provider: 'local',
|
|
36
|
+
textCount: texts.length,
|
|
37
|
+
gpuBatchSize,
|
|
38
|
+
httpLimit,
|
|
39
|
+
httpBatches,
|
|
40
|
+
});
|
|
35
41
|
const sidecar = getEmbeddingSidecar();
|
|
36
42
|
const allResults = [];
|
|
37
43
|
for (let i = 0; i < texts.length; i += httpLimit) {
|
package/dist/mcp/constants.js
CHANGED
|
@@ -449,3 +449,30 @@ export const MESSAGES = {
|
|
|
449
449
|
startingServer: 'Starting MCP server...',
|
|
450
450
|
},
|
|
451
451
|
};
|
|
452
|
+
export const CONFIG_FILE_PATTERNS = {
|
|
453
|
+
defaultGlobs: [
|
|
454
|
+
'docker-compose*.{yml,yaml}',
|
|
455
|
+
'Dockerfile*',
|
|
456
|
+
'**/*.json', // All JSON files (package.json, tsconfig.json, .mcp.json, etc.)
|
|
457
|
+
'**/*.{yml,yaml}', // All YAML files (CI configs, k8s manifests, etc.)
|
|
458
|
+
'**/*.toml', // Cargo.toml, pyproject.toml, etc.
|
|
459
|
+
'**/*.cfg', // Python setup.cfg, etc.
|
|
460
|
+
'**/*.ini', // INI config files
|
|
461
|
+
'**/Makefile', // Makefiles
|
|
462
|
+
'.env*',
|
|
463
|
+
'**/*.sh', // Shell scripts
|
|
464
|
+
'**/*.py', // Python files (sidecar, scripts)
|
|
465
|
+
],
|
|
466
|
+
excludeGlobs: [
|
|
467
|
+
'**/node_modules/**',
|
|
468
|
+
'**/dist/**',
|
|
469
|
+
'**/build/**',
|
|
470
|
+
'**/.git/**',
|
|
471
|
+
'**/coverage/**',
|
|
472
|
+
'**/.next/**',
|
|
473
|
+
'**/package-lock.json', // Too large, not useful for search
|
|
474
|
+
'**/yarn.lock',
|
|
475
|
+
'**/pnpm-lock.yaml',
|
|
476
|
+
],
|
|
477
|
+
maxFileSizeBytes: 512 * 1024, // 512 KB — skip large generated files
|
|
478
|
+
};
|
|
@@ -3,7 +3,14 @@
|
|
|
3
3
|
* Handles importing parsed graph data into Neo4j with embeddings
|
|
4
4
|
*/
|
|
5
5
|
import fs from 'fs/promises';
|
|
6
|
-
import
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { glob } from 'glob';
|
|
8
|
+
import { EXCLUDE_PATTERNS_GLOB } from '../../constants.js';
|
|
9
|
+
import { CoreNodeType } from '../../core/config/schema.js';
|
|
10
|
+
import { EMBEDDING_BATCH_CONFIG, getEmbeddingDimensions, } from '../../core/embeddings/embeddings.service.js';
|
|
11
|
+
import { normalizeCode } from '../../core/utils/code-normalizer.js';
|
|
12
|
+
import { hashFile } from '../../core/utils/file-utils.js';
|
|
13
|
+
import { generateDeterministicId } from '../../core/utils/graph-factory.js';
|
|
7
14
|
import { QUERIES } from '../../storage/neo4j/neo4j.service.js';
|
|
8
15
|
import { DEFAULTS } from '../constants.js';
|
|
9
16
|
import { debugLog } from '../utils.js';
|
|
@@ -35,7 +42,14 @@ export class GraphGeneratorHandler {
|
|
|
35
42
|
* Use this for chunked imports where indexes are created once before/after all chunks.
|
|
36
43
|
*/
|
|
37
44
|
async generateGraphFromData(nodes, edges, batchSize = DEFAULTS.batchSize, clearExisting = true, metadata = {}, skipIndexes = false) {
|
|
38
|
-
await debugLog('Starting graph generation', {
|
|
45
|
+
await debugLog('Starting graph generation', {
|
|
46
|
+
nodeCount: nodes.length,
|
|
47
|
+
edgeCount: edges.length,
|
|
48
|
+
batchSize,
|
|
49
|
+
clearExisting,
|
|
50
|
+
skipIndexes,
|
|
51
|
+
projectId: this.projectId,
|
|
52
|
+
});
|
|
39
53
|
try {
|
|
40
54
|
console.error(`Generating graph with ${nodes.length} nodes and ${edges.length} edges`);
|
|
41
55
|
if (clearExisting) {
|
|
@@ -63,6 +77,61 @@ export class GraphGeneratorHandler {
|
|
|
63
77
|
throw error;
|
|
64
78
|
}
|
|
65
79
|
}
|
|
80
|
+
async ingestConfigFiles(projectPath, projectId, globs, excludeGlobs, maxFileSizeBytes, batchSize = DEFAULTS.batchSize) {
|
|
81
|
+
const files = await glob(globs, {
|
|
82
|
+
cwd: projectPath,
|
|
83
|
+
ignore: [...EXCLUDE_PATTERNS_GLOB, ...excludeGlobs],
|
|
84
|
+
absolute: true,
|
|
85
|
+
});
|
|
86
|
+
console.error(`[config-ingest] Found ${files.length} config files to ingest`);
|
|
87
|
+
const nodes = [];
|
|
88
|
+
for (const file of files) {
|
|
89
|
+
const stats = await fs.stat(file);
|
|
90
|
+
if (stats.size > maxFileSizeBytes) {
|
|
91
|
+
console.error(`[config-ingest] Skipping ${file} (${stats.size} bytes > ${maxFileSizeBytes})`);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const content = await fs.readFile(file, 'utf-8');
|
|
95
|
+
const name = path.basename(file);
|
|
96
|
+
const relativePath = path.relative(projectPath, file);
|
|
97
|
+
const lineCount = content.split('\n').length;
|
|
98
|
+
const contentHash = await hashFile(file);
|
|
99
|
+
const nodeId = generateDeterministicId(projectId, CoreNodeType.SOURCE_FILE, relativePath, name);
|
|
100
|
+
let normalizedHash;
|
|
101
|
+
try {
|
|
102
|
+
const result = normalizeCode(content);
|
|
103
|
+
if (result.normalizedHash)
|
|
104
|
+
normalizedHash = result.normalizedHash;
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// Config files may not normalize cleanly — skip
|
|
108
|
+
}
|
|
109
|
+
const properties = {
|
|
110
|
+
id: nodeId,
|
|
111
|
+
projectId,
|
|
112
|
+
name,
|
|
113
|
+
coreType: CoreNodeType.SOURCE_FILE,
|
|
114
|
+
semanticType: 'ConfigFile',
|
|
115
|
+
filePath: relativePath,
|
|
116
|
+
sourceCode: content,
|
|
117
|
+
startLine: 1,
|
|
118
|
+
endLine: lineCount,
|
|
119
|
+
createdAt: new Date().toISOString(),
|
|
120
|
+
size: Number(stats.size),
|
|
121
|
+
mtime: Number(stats.mtimeMs),
|
|
122
|
+
contentHash,
|
|
123
|
+
...(normalizedHash && { normalizedHash }),
|
|
124
|
+
};
|
|
125
|
+
nodes.push({
|
|
126
|
+
id: nodeId,
|
|
127
|
+
labels: ['SourceFile'],
|
|
128
|
+
properties,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
console.error(`[config-ingest] Importing ${nodes.length} config file nodes`);
|
|
132
|
+
await this.importNodes(nodes, batchSize);
|
|
133
|
+
return { nodesCreated: nodes.length };
|
|
134
|
+
}
|
|
66
135
|
/**
|
|
67
136
|
* Create all indexes. Call once before chunked imports start.
|
|
68
137
|
*/
|
|
@@ -16,7 +16,7 @@ import { ParserFactory } from '../../core/parsers/parser-factory.js';
|
|
|
16
16
|
import { detectChangedFiles } from '../../core/utils/file-change-detection.js';
|
|
17
17
|
import { resolveProjectId, getProjectName, UPSERT_PROJECT_QUERY, UPDATE_PROJECT_STATUS_QUERY, } from '../../core/utils/project-id.js';
|
|
18
18
|
import { Neo4jService, QUERIES } from '../../storage/neo4j/neo4j.service.js';
|
|
19
|
-
import { TOOL_NAMES, TOOL_METADATA, DEFAULTS, FILE_PATHS, LOG_CONFIG, PARSING } from '../constants.js';
|
|
19
|
+
import { TOOL_NAMES, TOOL_METADATA, DEFAULTS, FILE_PATHS, LOG_CONFIG, PARSING, CONFIG_FILE_PATTERNS, } from '../constants.js';
|
|
20
20
|
import { deleteSourceFileSubgraphs, loadExistingNodesForEdgeDetection, getCrossFileEdges, } from '../handlers/cross-file-edge.helpers.js';
|
|
21
21
|
import { GraphGeneratorHandler } from '../handlers/graph-generator.handler.js';
|
|
22
22
|
import { StreamingImportHandler } from '../handlers/streaming-import.handler.js';
|
|
@@ -67,6 +67,11 @@ export const createParseTypescriptProjectTool = (server) => {
|
|
|
67
67
|
inputSchema: {
|
|
68
68
|
projectPath: z.string().describe('Path to the TypeScript project root directory'),
|
|
69
69
|
tsconfigPath: z.string().describe('Path to TypeScript project tsconfig.json file'),
|
|
70
|
+
configFileGlobs: z
|
|
71
|
+
.array(z.string())
|
|
72
|
+
.optional()
|
|
73
|
+
.describe('Custom glob patterns for config files (default: all JSON, YAML, .env, Dockerfile, .sh, .py)')
|
|
74
|
+
.default(CONFIG_FILE_PATTERNS.defaultGlobs),
|
|
70
75
|
projectId: z
|
|
71
76
|
.string()
|
|
72
77
|
.optional()
|
|
@@ -107,7 +112,7 @@ export const createParseTypescriptProjectTool = (server) => {
|
|
|
107
112
|
.default(1000)
|
|
108
113
|
.describe('Debounce delay for watch mode in milliseconds (default: 1000)'),
|
|
109
114
|
},
|
|
110
|
-
}, async ({ tsconfigPath, projectPath, projectId, clearExisting, projectType = 'auto', chunkSize = 100, useStreaming = 'auto', async: asyncMode = false, watch = false, watchDebounceMs = 1000, }) => {
|
|
115
|
+
}, async ({ tsconfigPath, projectPath, projectId, configFileGlobs, clearExisting, projectType = 'auto', chunkSize = 100, useStreaming = 'auto', async: asyncMode = false, watch = false, watchDebounceMs = 1000, }) => {
|
|
111
116
|
try {
|
|
112
117
|
// SECURITY: Validate input paths before processing
|
|
113
118
|
await validatePathExists(projectPath, 'directory');
|
|
@@ -145,6 +150,7 @@ export const createParseTypescriptProjectTool = (server) => {
|
|
|
145
150
|
projectId: resolvedProjectId,
|
|
146
151
|
projectType,
|
|
147
152
|
chunkSize: chunkSize > 0 ? chunkSize : 50,
|
|
153
|
+
configFileGlobs,
|
|
148
154
|
},
|
|
149
155
|
resourceLimits: {
|
|
150
156
|
maxOldGenerationSizeMb: 8192, // 8GB heap for large monorepos
|
|
@@ -254,6 +260,10 @@ export const createParseTypescriptProjectTool = (server) => {
|
|
|
254
260
|
projectId: resolvedProjectId,
|
|
255
261
|
});
|
|
256
262
|
await debugLog('Streaming import completed', result);
|
|
263
|
+
// Ingest config files
|
|
264
|
+
const configResult = await graphGeneratorHandler.ingestConfigFiles(projectPath, resolvedProjectId, configFileGlobs, CONFIG_FILE_PATTERNS.excludeGlobs, CONFIG_FILE_PATTERNS.maxFileSizeBytes);
|
|
265
|
+
result.nodesImported += configResult.nodesCreated;
|
|
266
|
+
await debugLog('Config file ingestion completed', { nodesCreated: configResult.nodesCreated });
|
|
257
267
|
// Update Project node status to complete
|
|
258
268
|
await neo4jService.run(UPDATE_PROJECT_STATUS_QUERY, {
|
|
259
269
|
projectId: resolvedProjectId,
|
|
@@ -328,6 +338,10 @@ export const createParseTypescriptProjectTool = (server) => {
|
|
|
328
338
|
}
|
|
329
339
|
console.error('Graph generation completed:', result);
|
|
330
340
|
await debugLog('Neo4j import completed', result);
|
|
341
|
+
// Ingest config files
|
|
342
|
+
const configResult = await graphGeneratorHandler.ingestConfigFiles(projectPath, finalProjectId, configFileGlobs, CONFIG_FILE_PATTERNS.excludeGlobs, CONFIG_FILE_PATTERNS.maxFileSizeBytes);
|
|
343
|
+
result.nodesImported += configResult.nodesCreated;
|
|
344
|
+
await debugLog('Config file ingestion completed', { nodesCreated: configResult.nodesCreated });
|
|
331
345
|
// Update Project node status to complete
|
|
332
346
|
await neo4jService.run(UPDATE_PROJECT_STATUS_QUERY, {
|
|
333
347
|
projectId: finalProjectId,
|
|
@@ -165,7 +165,9 @@ export const createSwarmCleanupTool = (server) => {
|
|
|
165
165
|
const msgResult = await neo4jService.run(COUNT_MESSAGES_BY_SWARM_QUERY, params);
|
|
166
166
|
messageCount = msgResult[0]?.count ?? 0;
|
|
167
167
|
messageCount =
|
|
168
|
-
typeof messageCount === 'object' && 'toNumber' in messageCount
|
|
168
|
+
typeof messageCount === 'object' && 'toNumber' in messageCount
|
|
169
|
+
? messageCount.toNumber()
|
|
170
|
+
: messageCount;
|
|
169
171
|
messageCategories = msgResult[0]?.categories ?? [];
|
|
170
172
|
}
|
|
171
173
|
return createSuccessResponse(JSON.stringify({
|
|
@@ -164,46 +164,24 @@ export const createSwarmMessageTool = (server) => {
|
|
|
164
164
|
.enum(['send', 'read', 'acknowledge'])
|
|
165
165
|
.describe('Action: send (post message), read (get messages), acknowledge (mark as read)'),
|
|
166
166
|
// Send parameters
|
|
167
|
-
toAgentId: z
|
|
168
|
-
.string()
|
|
169
|
-
.optional()
|
|
170
|
-
.describe('Target agent ID. Omit for broadcast to all swarm agents.'),
|
|
167
|
+
toAgentId: z.string().optional().describe('Target agent ID. Omit for broadcast to all swarm agents.'),
|
|
171
168
|
category: z
|
|
172
169
|
.enum(MESSAGE_CATEGORY_KEYS)
|
|
173
170
|
.optional()
|
|
174
171
|
.describe('Message category: blocked (need help), conflict (resource clash), finding (important discovery), ' +
|
|
175
172
|
'request (direct ask), alert (urgent notification), handoff (context transfer)'),
|
|
176
|
-
content: z
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
.describe('Message content (required for send action)'),
|
|
180
|
-
taskId: z
|
|
181
|
-
.string()
|
|
182
|
-
.optional()
|
|
183
|
-
.describe('Related task ID for context'),
|
|
184
|
-
filePaths: z
|
|
185
|
-
.array(z.string())
|
|
186
|
-
.optional()
|
|
187
|
-
.describe('File paths relevant to this message'),
|
|
173
|
+
content: z.string().optional().describe('Message content (required for send action)'),
|
|
174
|
+
taskId: z.string().optional().describe('Related task ID for context'),
|
|
175
|
+
filePaths: z.array(z.string()).optional().describe('File paths relevant to this message'),
|
|
188
176
|
ttlMs: z
|
|
189
177
|
.number()
|
|
190
178
|
.int()
|
|
191
179
|
.optional()
|
|
192
180
|
.describe(`Time-to-live in ms (default: ${MESSAGE_DEFAULT_TTL_MS / 3600000}h). Set 0 for swarm lifetime.`),
|
|
193
181
|
// Read parameters
|
|
194
|
-
unreadOnly: z
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
.default(true)
|
|
198
|
-
.describe('Only return unread messages (default: true)'),
|
|
199
|
-
categories: z
|
|
200
|
-
.array(z.enum(MESSAGE_CATEGORY_KEYS))
|
|
201
|
-
.optional()
|
|
202
|
-
.describe('Filter by message categories'),
|
|
203
|
-
fromAgentId: z
|
|
204
|
-
.string()
|
|
205
|
-
.optional()
|
|
206
|
-
.describe('Filter messages from a specific agent'),
|
|
182
|
+
unreadOnly: z.boolean().optional().default(true).describe('Only return unread messages (default: true)'),
|
|
183
|
+
categories: z.array(z.enum(MESSAGE_CATEGORY_KEYS)).optional().describe('Filter by message categories'),
|
|
184
|
+
fromAgentId: z.string().optional().describe('Filter messages from a specific agent'),
|
|
207
185
|
limit: z
|
|
208
186
|
.number()
|
|
209
187
|
.int()
|
|
@@ -218,11 +196,7 @@ export const createSwarmMessageTool = (server) => {
|
|
|
218
196
|
.optional()
|
|
219
197
|
.describe('Specific message IDs to acknowledge. Omit to acknowledge all unread.'),
|
|
220
198
|
// Maintenance
|
|
221
|
-
cleanup: z
|
|
222
|
-
.boolean()
|
|
223
|
-
.optional()
|
|
224
|
-
.default(false)
|
|
225
|
-
.describe('Also clean up expired messages'),
|
|
199
|
+
cleanup: z.boolean().optional().default(false).describe('Also clean up expired messages'),
|
|
226
200
|
},
|
|
227
201
|
}, async ({ projectId, swarmId, agentId, action, toAgentId, category, content, taskId, filePaths, ttlMs, unreadOnly = true, categories, fromAgentId, limit = 20, messageIds, cleanup = false, }) => {
|
|
228
202
|
const neo4jService = new Neo4jService();
|
|
@@ -341,7 +315,7 @@ export const createSwarmMessageTool = (server) => {
|
|
|
341
315
|
});
|
|
342
316
|
const count = typeof result[0]?.acknowledged === 'object'
|
|
343
317
|
? result[0].acknowledged.toNumber()
|
|
344
|
-
: result[0]?.acknowledged ?? 0;
|
|
318
|
+
: (result[0]?.acknowledged ?? 0);
|
|
345
319
|
return createSuccessResponse(JSON.stringify({
|
|
346
320
|
action: 'acknowledged_all',
|
|
347
321
|
count,
|
|
@@ -66,13 +66,20 @@ const DELETE_PHEROMONE_QUERY = `
|
|
|
66
66
|
DETACH DELETE p
|
|
67
67
|
RETURN count(p) as deleted
|
|
68
68
|
`;
|
|
69
|
+
const BY_FILE_PATH = `MATCH (n:SourceFile)
|
|
70
|
+
WHERE n.projectId = $projectId AND n.filePath ENDS WITH $filePath
|
|
71
|
+
RETURN n.id as nodeId LIMIT 1`;
|
|
69
72
|
export const createSwarmPheromoneTool = (server) => {
|
|
70
73
|
server.registerTool(TOOL_NAMES.swarmPheromone, {
|
|
71
74
|
title: TOOL_METADATA[TOOL_NAMES.swarmPheromone].title,
|
|
72
75
|
description: TOOL_METADATA[TOOL_NAMES.swarmPheromone].description,
|
|
73
76
|
inputSchema: {
|
|
74
77
|
projectId: z.string().describe('Project ID, name, or path (e.g., "backend" or "proj_a1b2c3d4e5f6")'),
|
|
75
|
-
nodeId: z.string().describe('The code node ID to mark with a pheromone'),
|
|
78
|
+
nodeId: z.string().optional().describe('The code node ID to mark with a pheromone'),
|
|
79
|
+
filePath: z
|
|
80
|
+
.string()
|
|
81
|
+
.optional()
|
|
82
|
+
.describe('File path to mark (alternative to nodeId — auto-resolves to SourceFile node)'),
|
|
76
83
|
type: z
|
|
77
84
|
.enum(PHEROMONE_TYPES)
|
|
78
85
|
.describe('Type of pheromone: exploring (browsing), modifying (active work), claiming (ownership), completed (done), warning (danger), blocked (stuck), proposal (awaiting approval), needs_review (review request), session_context (session working set)'),
|
|
@@ -99,8 +106,11 @@ export const createSwarmPheromoneTool = (server) => {
|
|
|
99
106
|
.default(false)
|
|
100
107
|
.describe('If true, removes the pheromone instead of creating/updating it'),
|
|
101
108
|
},
|
|
102
|
-
}, async ({ projectId, nodeId, type, intensity = 1.0, agentId, swarmId, sessionId, data, remove = false }) => {
|
|
109
|
+
}, async ({ projectId, nodeId, filePath, type, intensity = 1.0, agentId, swarmId, sessionId, data, remove = false, }) => {
|
|
103
110
|
const neo4jService = new Neo4jService();
|
|
111
|
+
if (!nodeId && !filePath) {
|
|
112
|
+
return createErrorResponse(new Error('nodeId or filePath required'));
|
|
113
|
+
}
|
|
104
114
|
// Resolve project ID
|
|
105
115
|
const projectResult = await resolveProjectIdOrError(projectId, neo4jService);
|
|
106
116
|
if (!projectResult.success) {
|
|
@@ -109,6 +119,21 @@ export const createSwarmPheromoneTool = (server) => {
|
|
|
109
119
|
}
|
|
110
120
|
const resolvedProjectId = projectResult.projectId;
|
|
111
121
|
try {
|
|
122
|
+
// Resolve filePath to nodeId before any queries that need it
|
|
123
|
+
let resolvedFromFilePath = false;
|
|
124
|
+
let linkedToNode = true;
|
|
125
|
+
if (!nodeId && filePath) {
|
|
126
|
+
const records = await neo4jService.run(BY_FILE_PATH, { projectId: resolvedProjectId, filePath });
|
|
127
|
+
if (records[0]?.nodeId) {
|
|
128
|
+
nodeId = records[0].nodeId;
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
// No graph node — use filePath as identifier so pheromone is still created
|
|
132
|
+
nodeId = filePath;
|
|
133
|
+
linkedToNode = false;
|
|
134
|
+
}
|
|
135
|
+
resolvedFromFilePath = true;
|
|
136
|
+
}
|
|
112
137
|
if (remove) {
|
|
113
138
|
const result = await neo4jService.run(DELETE_PHEROMONE_QUERY, {
|
|
114
139
|
projectId: resolvedProjectId,
|
|
@@ -101,6 +101,10 @@ export const createSwarmSenseTool = (server) => {
|
|
|
101
101
|
.optional()
|
|
102
102
|
.describe('Filter by pheromone types. If empty, returns all types. Options: exploring, modifying, claiming, completed, warning, blocked, proposal, needs_review'),
|
|
103
103
|
nodeIds: z.array(z.string()).optional().describe('Filter by specific node IDs. If empty, searches all nodes.'),
|
|
104
|
+
filePaths: z
|
|
105
|
+
.array(z.string())
|
|
106
|
+
.optional()
|
|
107
|
+
.describe('Filter by file paths (resolves to SourceFile nodeIds). Also matches pheromones placed via filePath.'),
|
|
104
108
|
agentIds: z
|
|
105
109
|
.array(z.string())
|
|
106
110
|
.optional()
|
|
@@ -136,7 +140,7 @@ export const createSwarmSenseTool = (server) => {
|
|
|
136
140
|
.default(false)
|
|
137
141
|
.describe('Run cleanup of fully decayed pheromones (intensity < 0.01)'),
|
|
138
142
|
},
|
|
139
|
-
}, async ({ projectId, types, nodeIds, agentIds, swarmId, excludeAgentId, sessionId, minIntensity = 0.3, limit = 50, includeStats = false, cleanup = false, }) => {
|
|
143
|
+
}, async ({ projectId, types, nodeIds, filePaths, agentIds, swarmId, excludeAgentId, sessionId, minIntensity = 0.3, limit = 50, includeStats = false, cleanup = false, }) => {
|
|
140
144
|
const neo4jService = new Neo4jService();
|
|
141
145
|
// Resolve project ID
|
|
142
146
|
const projectResult = await resolveProjectIdOrError(projectId, neo4jService);
|
|
@@ -146,6 +150,19 @@ export const createSwarmSenseTool = (server) => {
|
|
|
146
150
|
}
|
|
147
151
|
const resolvedProjectId = projectResult.projectId;
|
|
148
152
|
try {
|
|
153
|
+
// Resolve filePaths to nodeIds and merge with any explicit nodeIds
|
|
154
|
+
if (filePaths?.length) {
|
|
155
|
+
const resolvedIds = [...(nodeIds ?? [])];
|
|
156
|
+
const records = await neo4jService.run(`MATCH (n:SourceFile)
|
|
157
|
+
WHERE n.projectId = $projectId AND any(fp IN $filePaths WHERE n.filePath ENDS WITH fp)
|
|
158
|
+
RETURN collect(n.id) as nodeIds`, { projectId: resolvedProjectId, filePaths });
|
|
159
|
+
if (records[0]?.nodeIds) {
|
|
160
|
+
resolvedIds.push(...records[0].nodeIds);
|
|
161
|
+
}
|
|
162
|
+
// Also include raw filePaths — catches pheromones placed on new files via filePath
|
|
163
|
+
resolvedIds.push(...filePaths);
|
|
164
|
+
nodeIds = resolvedIds;
|
|
165
|
+
}
|
|
149
166
|
const result = {
|
|
150
167
|
pheromones: [],
|
|
151
168
|
projectId: resolvedProjectId,
|
|
@@ -18,7 +18,7 @@ import { debugLog } from '../../core/utils/file-utils.js';
|
|
|
18
18
|
import { getProjectName, UPSERT_PROJECT_QUERY, UPDATE_PROJECT_STATUS_QUERY } from '../../core/utils/project-id.js';
|
|
19
19
|
import { WorkspaceDetector } from '../../core/workspace/index.js';
|
|
20
20
|
import { Neo4jService, QUERIES } from '../../storage/neo4j/neo4j.service.js';
|
|
21
|
-
import { PARSING } from '../constants.js';
|
|
21
|
+
import { PARSING, CONFIG_FILE_PATTERNS } from '../constants.js';
|
|
22
22
|
import { GraphGeneratorHandler } from '../handlers/graph-generator.handler.js';
|
|
23
23
|
import { ParallelImportHandler } from '../handlers/parallel-import.handler.js';
|
|
24
24
|
import { StreamingImportHandler } from '../handlers/streaming-import.handler.js';
|
|
@@ -122,6 +122,11 @@ const runParser = async () => {
|
|
|
122
122
|
totalNodesImported = result.nodesImported;
|
|
123
123
|
totalEdgesImported = result.edgesImported;
|
|
124
124
|
}
|
|
125
|
+
// Ingest config files (JSON, YAML, Dockerfiles, etc.)
|
|
126
|
+
const configGlobs = config.configFileGlobs ?? CONFIG_FILE_PATTERNS.defaultGlobs;
|
|
127
|
+
const configResult = await graphGeneratorHandler.ingestConfigFiles(config.projectPath, resolvedProjectId, configGlobs, CONFIG_FILE_PATTERNS.excludeGlobs, CONFIG_FILE_PATTERNS.maxFileSizeBytes);
|
|
128
|
+
totalNodesImported += configResult.nodesCreated;
|
|
129
|
+
await debugLog('Config file ingestion completed', { nodesCreated: configResult.nodesCreated });
|
|
125
130
|
await neo4jService.run(UPDATE_PROJECT_STATUS_QUERY, {
|
|
126
131
|
projectId: resolvedProjectId,
|
|
127
132
|
status: 'complete',
|
package/package.json
CHANGED