@vibekiln/cutline-mcp-cli 0.4.4 → 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 +17 -0
- package/dist/commands/serve.d.ts +8 -1
- package/dist/commands/serve.js +175 -4
- package/dist/index.js +10 -1
- package/dist/servers/cutline-server.js +189 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -135,6 +135,19 @@ cutline-mcp serve output # Export and rendering
|
|
|
135
135
|
cutline-mcp serve integrations # External integrations
|
|
136
136
|
```
|
|
137
137
|
|
|
138
|
+
HTTP bridge mode (for registries/hosts that require an HTTPS MCP URL):
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
cutline-mcp serve constraints --http --host 0.0.0.0 --port 8080 --path /mcp
|
|
142
|
+
# Health: GET /health
|
|
143
|
+
# MCP endpoint: POST /mcp
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Bridge notes:
|
|
147
|
+
- Default mode remains stdio (no behavior change for Cursor/Claude Desktop local configs).
|
|
148
|
+
- The bridge forwards JSON-RPC requests to the bundled stdio server process.
|
|
149
|
+
- Batch JSON-RPC payloads are not supported by the bridge.
|
|
150
|
+
|
|
138
151
|
### `upgrade`
|
|
139
152
|
|
|
140
153
|
Open the upgrade page and refresh your session.
|
|
@@ -206,6 +219,10 @@ Config: [`server.json`](./server.json)
|
|
|
206
219
|
|
|
207
220
|
Config: [`smithery.yaml`](./smithery.yaml) with [`Dockerfile`](./Dockerfile)
|
|
208
221
|
|
|
222
|
+
If the publish UI requires an MCP Server URL, deploy the bridge and provide your public HTTPS endpoint, for example:
|
|
223
|
+
|
|
224
|
+
`https://mcp.thecutline.ai/mcp`
|
|
225
|
+
|
|
209
226
|
### Claude Desktop Extension
|
|
210
227
|
|
|
211
228
|
```bash
|
package/dist/commands/serve.d.ts
CHANGED
|
@@ -1 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
interface ServeOptions {
|
|
2
|
+
http?: boolean;
|
|
3
|
+
host?: string;
|
|
4
|
+
port?: string;
|
|
5
|
+
path?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function serveCommand(serverName: string, options?: ServeOptions): void;
|
|
8
|
+
export {};
|
package/dist/commands/serve.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { execFileSync } from 'node:child_process';
|
|
1
|
+
import { execFileSync, spawn } from 'node:child_process';
|
|
2
|
+
import { createServer } from 'node:http';
|
|
2
3
|
import { resolve, dirname } from 'node:path';
|
|
3
4
|
import { fileURLToPath } from 'node:url';
|
|
4
5
|
import { existsSync } from 'node:fs';
|
|
@@ -11,7 +12,174 @@ const SERVER_MAP = {
|
|
|
11
12
|
output: 'output-server.js',
|
|
12
13
|
integrations: 'integrations-server.js',
|
|
13
14
|
};
|
|
14
|
-
|
|
15
|
+
function readJsonBody(req) {
|
|
16
|
+
return new Promise((resolveBody, rejectBody) => {
|
|
17
|
+
const chunks = [];
|
|
18
|
+
let total = 0;
|
|
19
|
+
const MAX_BODY_BYTES = 1024 * 1024; // 1MB safety cap
|
|
20
|
+
req.on('data', (chunk) => {
|
|
21
|
+
total += chunk.length;
|
|
22
|
+
if (total > MAX_BODY_BYTES) {
|
|
23
|
+
rejectBody(new Error('Request body too large'));
|
|
24
|
+
req.destroy();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
chunks.push(chunk);
|
|
28
|
+
});
|
|
29
|
+
req.on('end', () => {
|
|
30
|
+
try {
|
|
31
|
+
const text = Buffer.concat(chunks).toString('utf8');
|
|
32
|
+
resolveBody(text ? JSON.parse(text) : {});
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
rejectBody(new Error('Invalid JSON body'));
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
req.on('error', rejectBody);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
function writeJson(res, statusCode, body) {
|
|
42
|
+
const payload = JSON.stringify(body);
|
|
43
|
+
res.writeHead(statusCode, {
|
|
44
|
+
'Content-Type': 'application/json',
|
|
45
|
+
'Content-Length': Buffer.byteLength(payload),
|
|
46
|
+
});
|
|
47
|
+
res.end(payload);
|
|
48
|
+
}
|
|
49
|
+
function normalizeId(id) {
|
|
50
|
+
if (typeof id === 'string' || typeof id === 'number')
|
|
51
|
+
return String(id);
|
|
52
|
+
return '';
|
|
53
|
+
}
|
|
54
|
+
function serveHttpBridge(serverName, serverPath, opts) {
|
|
55
|
+
const host = opts.host || process.env.CUTLINE_MCP_HTTP_HOST || '0.0.0.0';
|
|
56
|
+
const port = Number(opts.port || process.env.CUTLINE_MCP_HTTP_PORT || '8080');
|
|
57
|
+
const mcpPath = opts.path || process.env.CUTLINE_MCP_HTTP_PATH || '/mcp';
|
|
58
|
+
const requestTimeoutMs = Number(process.env.CUTLINE_MCP_HTTP_TIMEOUT_MS || '30000');
|
|
59
|
+
if (!Number.isFinite(port) || port <= 0) {
|
|
60
|
+
console.error(`Invalid --port value: ${opts.port}`);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
const child = spawn(process.execPath, [serverPath], {
|
|
64
|
+
stdio: ['pipe', 'pipe', 'inherit'],
|
|
65
|
+
env: process.env,
|
|
66
|
+
});
|
|
67
|
+
const pending = new Map();
|
|
68
|
+
let stdoutBuffer = Buffer.alloc(0);
|
|
69
|
+
const failAllPending = (reason) => {
|
|
70
|
+
for (const [key, entry] of pending.entries()) {
|
|
71
|
+
clearTimeout(entry.timer);
|
|
72
|
+
entry.reject(reason);
|
|
73
|
+
pending.delete(key);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
const sendToStdioServer = (message) => {
|
|
77
|
+
if (!child.stdin || child.killed) {
|
|
78
|
+
throw new Error('MCP stdio server is not available');
|
|
79
|
+
}
|
|
80
|
+
const body = JSON.stringify(message);
|
|
81
|
+
const packet = `Content-Length: ${Buffer.byteLength(body, 'utf8')}\r\n\r\n${body}`;
|
|
82
|
+
child.stdin.write(packet, 'utf8');
|
|
83
|
+
};
|
|
84
|
+
child.stdout?.on('data', (chunk) => {
|
|
85
|
+
stdoutBuffer = Buffer.concat([stdoutBuffer, chunk]);
|
|
86
|
+
while (true) {
|
|
87
|
+
const separatorIndex = stdoutBuffer.indexOf('\r\n\r\n');
|
|
88
|
+
if (separatorIndex < 0)
|
|
89
|
+
break;
|
|
90
|
+
const headers = stdoutBuffer.slice(0, separatorIndex).toString('utf8');
|
|
91
|
+
const lengthMatch = headers.match(/content-length:\s*(\d+)/i);
|
|
92
|
+
if (!lengthMatch) {
|
|
93
|
+
stdoutBuffer = stdoutBuffer.slice(separatorIndex + 4);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const contentLength = Number(lengthMatch[1]);
|
|
97
|
+
const packetLength = separatorIndex + 4 + contentLength;
|
|
98
|
+
if (stdoutBuffer.length < packetLength)
|
|
99
|
+
break;
|
|
100
|
+
const jsonBytes = stdoutBuffer.slice(separatorIndex + 4, packetLength);
|
|
101
|
+
stdoutBuffer = stdoutBuffer.slice(packetLength);
|
|
102
|
+
try {
|
|
103
|
+
const message = JSON.parse(jsonBytes.toString('utf8'));
|
|
104
|
+
const id = normalizeId(message?.id);
|
|
105
|
+
if (!id)
|
|
106
|
+
continue;
|
|
107
|
+
const entry = pending.get(id);
|
|
108
|
+
if (!entry)
|
|
109
|
+
continue;
|
|
110
|
+
clearTimeout(entry.timer);
|
|
111
|
+
pending.delete(id);
|
|
112
|
+
entry.resolve(message);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// Ignore malformed child output and keep processing subsequent frames.
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
child.on('exit', (code, signal) => {
|
|
120
|
+
failAllPending(new Error(`MCP stdio server exited unexpectedly (${signal ? `signal ${signal}` : `code ${code ?? 'unknown'}`})`));
|
|
121
|
+
});
|
|
122
|
+
const httpServer = createServer(async (req, res) => {
|
|
123
|
+
const { method } = req;
|
|
124
|
+
const reqPath = (req.url || '/').split('?')[0];
|
|
125
|
+
if (method === 'GET' && reqPath === '/health') {
|
|
126
|
+
writeJson(res, 200, {
|
|
127
|
+
ok: true,
|
|
128
|
+
server: serverName,
|
|
129
|
+
transport: 'http-bridge-stdio',
|
|
130
|
+
mcp_path: mcpPath,
|
|
131
|
+
});
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (method !== 'POST' || reqPath !== mcpPath) {
|
|
135
|
+
writeJson(res, 404, { error: `Not found. Use POST ${mcpPath}` });
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
const body = await readJsonBody(req);
|
|
140
|
+
if (Array.isArray(body)) {
|
|
141
|
+
writeJson(res, 400, { error: 'Batch JSON-RPC is not supported by this bridge' });
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const message = body;
|
|
145
|
+
const id = normalizeId(message?.id);
|
|
146
|
+
// Notifications do not have an id and do not expect a response.
|
|
147
|
+
if (!id) {
|
|
148
|
+
sendToStdioServer(message);
|
|
149
|
+
writeJson(res, 202, { ok: true });
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const response = await new Promise((resolveResponse, rejectResponse) => {
|
|
153
|
+
const timer = setTimeout(() => {
|
|
154
|
+
pending.delete(id);
|
|
155
|
+
rejectResponse(new Error(`Timed out waiting for response id=${id}`));
|
|
156
|
+
}, requestTimeoutMs);
|
|
157
|
+
pending.set(id, {
|
|
158
|
+
resolve: resolveResponse,
|
|
159
|
+
reject: rejectResponse,
|
|
160
|
+
timer,
|
|
161
|
+
});
|
|
162
|
+
try {
|
|
163
|
+
sendToStdioServer(message);
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
clearTimeout(timer);
|
|
167
|
+
pending.delete(id);
|
|
168
|
+
rejectResponse(error);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
writeJson(res, 200, response);
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
writeJson(res, 500, { error: error?.message || 'Bridge request failed' });
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
httpServer.listen(port, host, () => {
|
|
178
|
+
console.error(`Cutline MCP HTTP bridge listening on http://${host}:${port}${mcpPath}`);
|
|
179
|
+
console.error(`Health check: http://${host}:${port}/health`);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
export function serveCommand(serverName, options = {}) {
|
|
15
183
|
const fileName = SERVER_MAP[serverName];
|
|
16
184
|
if (!fileName) {
|
|
17
185
|
const valid = Object.keys(SERVER_MAP).join(', ');
|
|
@@ -24,8 +192,11 @@ export function serveCommand(serverName) {
|
|
|
24
192
|
console.error('The package may not have been built correctly.');
|
|
25
193
|
process.exit(1);
|
|
26
194
|
}
|
|
27
|
-
|
|
28
|
-
|
|
195
|
+
if (options.http) {
|
|
196
|
+
serveHttpBridge(serverName, serverPath, options);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
// Replace this process with the MCP server (default stdio mode).
|
|
29
200
|
try {
|
|
30
201
|
execFileSync(process.execPath, [serverPath], {
|
|
31
202
|
stdio: 'inherit',
|
package/dist/index.js
CHANGED
|
@@ -47,7 +47,16 @@ program
|
|
|
47
47
|
program
|
|
48
48
|
.command('serve <server>')
|
|
49
49
|
.description('Start an MCP server (constraints, premortem, exploration, tools, output, integrations)')
|
|
50
|
-
.
|
|
50
|
+
.option('--http', 'Expose the selected stdio server over an HTTP bridge')
|
|
51
|
+
.option('--host <host>', 'HTTP bind host for bridge mode (default: 0.0.0.0)')
|
|
52
|
+
.option('--port <port>', 'HTTP port for bridge mode (default: 8080)')
|
|
53
|
+
.option('--path <path>', 'HTTP MCP path for bridge mode (default: /mcp)')
|
|
54
|
+
.action((server, opts) => serveCommand(server, {
|
|
55
|
+
http: opts.http,
|
|
56
|
+
host: opts.host,
|
|
57
|
+
port: opts.port,
|
|
58
|
+
path: opts.path,
|
|
59
|
+
}));
|
|
51
60
|
program
|
|
52
61
|
.command('setup')
|
|
53
62
|
.description('One-command onboarding: authenticate, write IDE MCP config, generate rules')
|
|
@@ -7408,6 +7408,146 @@ var ACT_NAMES = {
|
|
|
7408
7408
|
5: "Value Ranking",
|
|
7409
7409
|
6: "Graduation"
|
|
7410
7410
|
};
|
|
7411
|
+
function assessScopeExpansionIntent(params) {
|
|
7412
|
+
const task = (params.task_description || "").trim();
|
|
7413
|
+
const filePaths = params.file_paths || [];
|
|
7414
|
+
const reasons = [];
|
|
7415
|
+
let confidence = 0;
|
|
7416
|
+
const explicitScopePatterns = [
|
|
7417
|
+
/\b(scope increase|expand(?:ing)? scope|broaden scope|new scope)\b/i,
|
|
7418
|
+
/\b(seed|ingest|add)\b.{0,24}\b(graph|constraint|entity|entities)\b/i,
|
|
7419
|
+
/\b(new feature|new module|new domain|new subsystem)\b/i
|
|
7420
|
+
];
|
|
7421
|
+
const hasExplicitScopeIntent = explicitScopePatterns.some((rx) => rx.test(task));
|
|
7422
|
+
if (hasExplicitScopeIntent) {
|
|
7423
|
+
confidence += 0.55;
|
|
7424
|
+
reasons.push("Task description includes explicit scope-expansion language.");
|
|
7425
|
+
}
|
|
7426
|
+
const hintedName = params.hinted_entity_name?.trim();
|
|
7427
|
+
const inferredName = hintedName || inferEntityNameFromTask(task);
|
|
7428
|
+
if (inferredName) {
|
|
7429
|
+
confidence += 0.12;
|
|
7430
|
+
reasons.push("Potential new entity name detected from task context.");
|
|
7431
|
+
}
|
|
7432
|
+
if (filePaths.length > 0) {
|
|
7433
|
+
const dirHints = new Set(filePaths.map((p) => p.split("/").filter(Boolean)).filter((parts) => parts.length > 1).map((parts) => parts[parts.length - 2].toLowerCase()).filter((v) => v.length >= 3));
|
|
7434
|
+
if (dirHints.size >= 2) {
|
|
7435
|
+
confidence += 0.08;
|
|
7436
|
+
reasons.push("Multiple code areas touched, indicating potential boundary expansion.");
|
|
7437
|
+
}
|
|
7438
|
+
}
|
|
7439
|
+
if (inferredName && params.known_entity_names?.length) {
|
|
7440
|
+
const norm = normalizeName(inferredName);
|
|
7441
|
+
const known = params.known_entity_names.some((n) => normalizeName(n) === norm);
|
|
7442
|
+
if (!known) {
|
|
7443
|
+
confidence += 0.25;
|
|
7444
|
+
reasons.push("Entity candidate does not match existing graph entities.");
|
|
7445
|
+
} else {
|
|
7446
|
+
confidence -= 0.1;
|
|
7447
|
+
reasons.push("Entity candidate already exists in graph.");
|
|
7448
|
+
}
|
|
7449
|
+
}
|
|
7450
|
+
confidence = Math.max(0, Math.min(1, confidence));
|
|
7451
|
+
const triggered = confidence >= 0.6 || hasExplicitScopeIntent;
|
|
7452
|
+
const recommended_tool_calls = triggered ? [
|
|
7453
|
+
"constraints_learn(product_id, name, entity_type, description, ...)",
|
|
7454
|
+
"graph_bind_codebase(product_id, file_paths)",
|
|
7455
|
+
"graph_bind_confirm(product_id, entity_id, file_patterns)",
|
|
7456
|
+
"graph_metrics(product_id)"
|
|
7457
|
+
] : [];
|
|
7458
|
+
return {
|
|
7459
|
+
triggered,
|
|
7460
|
+
confidence: Math.round(confidence * 100) / 100,
|
|
7461
|
+
reasons,
|
|
7462
|
+
candidate_name: inferredName,
|
|
7463
|
+
recommended_tool_calls
|
|
7464
|
+
};
|
|
7465
|
+
}
|
|
7466
|
+
function inferEntityNameFromTask(task) {
|
|
7467
|
+
if (!task)
|
|
7468
|
+
return void 0;
|
|
7469
|
+
const match = task.match(/\b(?:add|build|create|introduce|expand(?:ing)?\s+scope(?:\s+for|\s+to)?)\s+(?:a|an|the)?\s*([a-zA-Z0-9][a-zA-Z0-9 _-]{2,60})/i);
|
|
7470
|
+
if (!match?.[1])
|
|
7471
|
+
return void 0;
|
|
7472
|
+
const candidate = match[1].trim().replace(/\s{2,}/g, " ");
|
|
7473
|
+
if (candidate.length < 3)
|
|
7474
|
+
return void 0;
|
|
7475
|
+
return candidate;
|
|
7476
|
+
}
|
|
7477
|
+
function normalizeName(value) {
|
|
7478
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
|
|
7479
|
+
}
|
|
7480
|
+
async function seedScopeEntityFromAuto(params) {
|
|
7481
|
+
const { product_id, name: name2, entity_type, description, parent_id, tags, similarity_threshold } = params;
|
|
7482
|
+
const slug = normalizeName(name2).slice(0, 40);
|
|
7483
|
+
const entityId = `${entity_type}:${slug}`;
|
|
7484
|
+
const now = /* @__PURE__ */ new Date();
|
|
7485
|
+
const entity = {
|
|
7486
|
+
id: entityId,
|
|
7487
|
+
type: entity_type,
|
|
7488
|
+
name: name2,
|
|
7489
|
+
aliases: tags ?? [],
|
|
7490
|
+
description,
|
|
7491
|
+
source_id: "ide",
|
|
7492
|
+
source_type: "ide",
|
|
7493
|
+
ingested_at: now
|
|
7494
|
+
};
|
|
7495
|
+
await addEntity(product_id, entity);
|
|
7496
|
+
if (parent_id) {
|
|
7497
|
+
const edgeType = entity_type === "component" ? "REQUIRES" : "DEPENDS_ON";
|
|
7498
|
+
await addEdges(product_id, [{
|
|
7499
|
+
id: `edge:auto_scope:${parent_id}:${entityId}`,
|
|
7500
|
+
source_id: parent_id,
|
|
7501
|
+
source_type: "feature",
|
|
7502
|
+
target_id: entityId,
|
|
7503
|
+
target_type: entity_type,
|
|
7504
|
+
edge_type: edgeType
|
|
7505
|
+
}]);
|
|
7506
|
+
}
|
|
7507
|
+
let embeddingGenerated = false;
|
|
7508
|
+
try {
|
|
7509
|
+
const embedding = await generateEntityEmbedding(entity);
|
|
7510
|
+
if (embedding.some((v) => v !== 0)) {
|
|
7511
|
+
await updateEntityEmbedding(product_id, entityId, embedding);
|
|
7512
|
+
entity.embedding = embedding;
|
|
7513
|
+
embeddingGenerated = true;
|
|
7514
|
+
}
|
|
7515
|
+
} catch {
|
|
7516
|
+
}
|
|
7517
|
+
const similar = embeddingGenerated ? await findSimilarEntities(product_id, entity, {
|
|
7518
|
+
threshold: similarity_threshold ?? 0.65
|
|
7519
|
+
}) : [];
|
|
7520
|
+
let propagatedCount = 0;
|
|
7521
|
+
for (const match of similar) {
|
|
7522
|
+
const propagated = await propagateConstraints(product_id, match.entity.id, entityId, match.similarity);
|
|
7523
|
+
propagatedCount += propagated.length;
|
|
7524
|
+
}
|
|
7525
|
+
try {
|
|
7526
|
+
const [entities, edges, constraints, bindings] = await Promise.all([
|
|
7527
|
+
getAllEntities(product_id),
|
|
7528
|
+
getAllEdges(product_id),
|
|
7529
|
+
getAllNodes(product_id),
|
|
7530
|
+
getAllBindings(product_id)
|
|
7531
|
+
]);
|
|
7532
|
+
const metrics = computeMetricsFromGraph(entities, edges, constraints, bindings);
|
|
7533
|
+
await updateGraphMetadata(product_id, {
|
|
7534
|
+
last_build: now,
|
|
7535
|
+
entity_count: entities.length,
|
|
7536
|
+
edge_count: edges.length,
|
|
7537
|
+
constraint_count: constraints.length,
|
|
7538
|
+
binding_count: bindings.length,
|
|
7539
|
+
sources: ["ide", "deep_dive_pipeline"],
|
|
7540
|
+
metrics
|
|
7541
|
+
});
|
|
7542
|
+
await recordScoreSnapshot(product_id, metrics, "constraint_learn", name2);
|
|
7543
|
+
} catch {
|
|
7544
|
+
}
|
|
7545
|
+
return {
|
|
7546
|
+
entity_id: entityId,
|
|
7547
|
+
propagated_count: propagatedCount,
|
|
7548
|
+
similar_count: similar.length
|
|
7549
|
+
};
|
|
7550
|
+
}
|
|
7411
7551
|
var server = new Server({
|
|
7412
7552
|
name: "cutline",
|
|
7413
7553
|
version: "0.2.0"
|
|
@@ -7862,7 +8002,14 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
7862
8002
|
},
|
|
7863
8003
|
max_constraints: { type: "number", description: "Max constraints to return (default: 5)" },
|
|
7864
8004
|
use_semantic: { type: "boolean", description: "Use semantic search if embeddings available (default: true)" },
|
|
7865
|
-
phase: { type: "string", enum: ["test_spec", "functional", "security", "performance", "economics", "full", "auto"], description: "RGR phase filter. 'auto' uses complexity heuristic. Default: 'full'" }
|
|
8005
|
+
phase: { type: "string", enum: ["test_spec", "functional", "security", "performance", "economics", "full", "auto"], description: "RGR phase filter. 'auto' uses complexity heuristic. Default: 'full'" },
|
|
8006
|
+
auto_scope_expand: { type: "boolean", description: "If true, auto-seed a new graph entity when scope expansion intent is confidently detected and entity fields are provided." },
|
|
8007
|
+
scope_entity_name: { type: "string", description: "Optional explicit entity name to seed during scope expansion (for example 'Vibe Check Extension')." },
|
|
8008
|
+
scope_entity_type: { type: "string", enum: ["feature", "component", "data_type"], description: "Entity type for auto scope expansion." },
|
|
8009
|
+
scope_entity_description: { type: "string", description: "1-2 sentence description of the new entity to seed." },
|
|
8010
|
+
scope_entity_tags: { type: "array", items: { type: "string" }, description: "Optional tags/aliases for the new entity." },
|
|
8011
|
+
scope_parent_id: { type: "string", description: "Optional parent graph entity ID for linking this scope expansion." },
|
|
8012
|
+
scope_similarity_threshold: { type: "number", description: "Optional similarity threshold for propagated constraints (default: 0.65)." }
|
|
7866
8013
|
},
|
|
7867
8014
|
required: ["product_id"]
|
|
7868
8015
|
}
|
|
@@ -9035,7 +9182,7 @@ Meta: ${JSON.stringify(output.meta)}` }
|
|
|
9035
9182
|
return void 0;
|
|
9036
9183
|
};
|
|
9037
9184
|
var detectFramework = detectFramework2;
|
|
9038
|
-
const { product_id, file_paths, code_snippet, task_description, mode = "auto", max_constraints = 5, use_semantic = true, phase: autoPhase = "full" } = args;
|
|
9185
|
+
const { product_id, file_paths, code_snippet, task_description, mode = "auto", max_constraints = 5, use_semantic = true, phase: autoPhase = "full", auto_scope_expand = false, scope_entity_name, scope_entity_type, scope_entity_description, scope_entity_tags, scope_parent_id, scope_similarity_threshold } = args;
|
|
9039
9186
|
if (!product_id) {
|
|
9040
9187
|
throw new McpError(ErrorCode.InvalidParams, "product_id is required");
|
|
9041
9188
|
}
|
|
@@ -9061,6 +9208,34 @@ Meta: ${JSON.stringify(output.meta)}` }
|
|
|
9061
9208
|
};
|
|
9062
9209
|
}
|
|
9063
9210
|
const analysis = analyzeFileContext(fileContext);
|
|
9211
|
+
let knownEntityNames = [];
|
|
9212
|
+
if (task_description || scope_entity_name) {
|
|
9213
|
+
try {
|
|
9214
|
+
const entities = await getAllEntities(product_id);
|
|
9215
|
+
knownEntityNames = entities.map((e) => e.name);
|
|
9216
|
+
} catch {
|
|
9217
|
+
}
|
|
9218
|
+
}
|
|
9219
|
+
const scopeExpansion = assessScopeExpansionIntent({
|
|
9220
|
+
task_description,
|
|
9221
|
+
file_paths,
|
|
9222
|
+
known_entity_names: knownEntityNames,
|
|
9223
|
+
hinted_entity_name: scope_entity_name
|
|
9224
|
+
});
|
|
9225
|
+
let scopeSeedResult;
|
|
9226
|
+
const hasSeedPayload = !!scope_entity_name && !!scope_entity_type && !!scope_entity_description;
|
|
9227
|
+
const shouldAutoSeed = auto_scope_expand && scopeExpansion.triggered && hasSeedPayload;
|
|
9228
|
+
if (shouldAutoSeed) {
|
|
9229
|
+
scopeSeedResult = await seedScopeEntityFromAuto({
|
|
9230
|
+
product_id,
|
|
9231
|
+
name: scope_entity_name,
|
|
9232
|
+
entity_type: scope_entity_type,
|
|
9233
|
+
description: scope_entity_description,
|
|
9234
|
+
parent_id: scope_parent_id,
|
|
9235
|
+
tags: scope_entity_tags,
|
|
9236
|
+
similarity_threshold: scope_similarity_threshold
|
|
9237
|
+
});
|
|
9238
|
+
}
|
|
9064
9239
|
const actualMode = mode === "auto" ? analysis.suggested_mode : mode;
|
|
9065
9240
|
if (actualMode === "silent") {
|
|
9066
9241
|
return {
|
|
@@ -9074,7 +9249,9 @@ Meta: ${JSON.stringify(output.meta)}` }
|
|
|
9074
9249
|
mode: "silent",
|
|
9075
9250
|
detected_domains: analysis.detected_domains,
|
|
9076
9251
|
signal_strength: Math.round(analysis.confidence * 100) / 100,
|
|
9077
|
-
keywords_detected: analysis.signal.keywords?.slice(0, 5)
|
|
9252
|
+
keywords_detected: analysis.signal.keywords?.slice(0, 5),
|
|
9253
|
+
scope_expansion: scopeExpansion,
|
|
9254
|
+
scope_seeded: scopeSeedResult || void 0
|
|
9078
9255
|
}
|
|
9079
9256
|
})
|
|
9080
9257
|
}]
|
|
@@ -9229,6 +9406,12 @@ Meta: ${JSON.stringify(output.meta)}` }
|
|
|
9229
9406
|
\u2192 You may need to prioritize one over the other.`;
|
|
9230
9407
|
}).join("\n\n");
|
|
9231
9408
|
}
|
|
9409
|
+
if (scopeExpansion.triggered) {
|
|
9410
|
+
const recommendation = shouldAutoSeed ? `\u2705 Scope expansion detected and entity seeded: ${scopeSeedResult?.entity_id ?? "(created)"}` : `\u{1F9ED} Scope expansion detected. Recommended next tool: constraints_learn(...)`;
|
|
9411
|
+
humanReadable += `
|
|
9412
|
+
|
|
9413
|
+
${recommendation}`;
|
|
9414
|
+
}
|
|
9232
9415
|
}
|
|
9233
9416
|
return {
|
|
9234
9417
|
content: [{
|
|
@@ -9249,7 +9432,9 @@ Meta: ${JSON.stringify(output.meta)}` }
|
|
|
9249
9432
|
used_semantic: use_semantic && finalResults.some((r) => r.match_type === "semantic" || r.match_type === "hybrid"),
|
|
9250
9433
|
used_category_prefilter: usePreFilter,
|
|
9251
9434
|
phase: autoPhase,
|
|
9252
|
-
rgr_plan: autoRgrPlan || void 0
|
|
9435
|
+
rgr_plan: autoRgrPlan || void 0,
|
|
9436
|
+
scope_expansion: scopeExpansion,
|
|
9437
|
+
scope_seeded: scopeSeedResult || void 0
|
|
9253
9438
|
}
|
|
9254
9439
|
})
|
|
9255
9440
|
}]
|
package/package.json
CHANGED