@minhpnq1807/contextos 0.5.42 → 0.5.44
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/CHANGELOG.md +26 -0
- package/README.md +41 -10
- package/bin/ctx.js +136 -39
- package/package.json +2 -1
- package/plugins/ctx/.codex-plugin/plugin.json +1 -1
- package/plugins/ctx/bin/on-prompt.js +12 -9
- package/plugins/ctx/lib/analyzer.js +12 -94
- package/plugins/ctx/lib/auto-warm.js +74 -0
- package/plugins/ctx/lib/ctx-mcp-client.js +58 -9
- package/plugins/ctx/lib/embedding-scorer.js +109 -7
- package/plugins/ctx/lib/file-embedding-retriever.js +20 -23
- package/plugins/ctx/lib/global-hooks.js +20 -0
- package/plugins/ctx/lib/graph-retriever.js +82 -15
- package/plugins/ctx/lib/graph-strategy.js +107 -0
- package/plugins/ctx/lib/hook-io.js +29 -1
- package/plugins/ctx/lib/import-graph.js +37 -40
- package/plugins/ctx/lib/mcp-proxy-install.js +18 -90
- package/plugins/ctx/lib/output-config.js +85 -0
- package/plugins/ctx/lib/package-install.js +8 -0
- package/plugins/ctx/lib/prompt-hook.js +95 -20
- package/plugins/ctx/lib/ruler-sync.js +9 -71
- package/plugins/ctx/lib/scheduler.js +33 -21
- package/plugins/ctx/lib/score-context.js +60 -32
- package/plugins/ctx/lib/setup-wizard.js +5 -2
- package/plugins/ctx/lib/shell-runner.js +88 -0
- package/plugins/ctx/lib/skill-discoverer.js +110 -10
- package/plugins/ctx/lib/skillshare-sync.js +51 -2
- package/plugins/ctx/lib/stop-hook.js +2 -3
- package/plugins/ctx/lib/toml-config.js +116 -0
- package/plugins/ctx/mcp/server.js +6 -2
|
@@ -10,6 +10,15 @@ const DEFAULT_EMBEDDING_CANDIDATES = 120;
|
|
|
10
10
|
const DEFAULT_SEMANTIC_CATALOG_LIMIT = 300;
|
|
11
11
|
const SCAN_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
12
12
|
const MAX_DESCRIPTION_CHARS = 500;
|
|
13
|
+
const GENERIC_SKILL_TOKENS = new Set([
|
|
14
|
+
"active", "agent", "agents", "code", "config", "configuration", "create", "development",
|
|
15
|
+
"environment", "file", "files", "graph", "install", "integration", "local", "node", "package",
|
|
16
|
+
"project", "refresh", "rebuild", "setup", "skill", "skills", "sync", "tool", "tools", "using",
|
|
17
|
+
"build", "production", "https", "http", "com", "www"
|
|
18
|
+
]);
|
|
19
|
+
const SPECIALIZED_SKILL_TOKENS = new Set([
|
|
20
|
+
"android", "cicd", "eas", "expo", "ios", "postgres", "postgresql", "react-native"
|
|
21
|
+
]);
|
|
13
22
|
|
|
14
23
|
const scanCache = new Map();
|
|
15
24
|
|
|
@@ -154,13 +163,14 @@ export async function suggestSkills({
|
|
|
154
163
|
prompt = "",
|
|
155
164
|
skills = [],
|
|
156
165
|
dataDir,
|
|
166
|
+
cwd = process.cwd(),
|
|
157
167
|
limit = DEFAULT_LIMIT,
|
|
158
168
|
timeoutMs = Number(process.env.CONTEXTOS_SKILL_EMBEDDING_TIMEOUT_MS || process.env.CONTEXTOS_EMBEDDING_TIMEOUT_MS || 800)
|
|
159
169
|
} = {}) {
|
|
160
170
|
if (!String(prompt || "").trim() || !skills.length) return [];
|
|
161
|
-
const base = scoreSkillsByKeyword({ prompt, skills });
|
|
171
|
+
const base = scoreSkillsByKeyword({ prompt, skills, projectHints: projectSkillHints({ cwd }) });
|
|
162
172
|
if (skills.length > DEFAULT_SEMANTIC_CATALOG_LIMIT) {
|
|
163
|
-
return finalizeSkillScores(base, limit);
|
|
173
|
+
return finalizeSkillScores(base, limit, { minimumKeywordScore: 0.5 });
|
|
164
174
|
}
|
|
165
175
|
|
|
166
176
|
const embeddingCandidates = selectEmbeddingCandidates(base);
|
|
@@ -176,8 +186,9 @@ export async function suggestSkills({
|
|
|
176
186
|
return finalizeSkillScores(embedding.rules, limit);
|
|
177
187
|
}
|
|
178
188
|
|
|
179
|
-
function finalizeSkillScores(skills, limit) {
|
|
180
|
-
|
|
189
|
+
function finalizeSkillScores(skills, limit, { minimumKeywordScore = 0.35 } = {}) {
|
|
190
|
+
const ranked = skills
|
|
191
|
+
.filter((rule) => rule.domainEligible !== false)
|
|
181
192
|
.map((rule) => ({
|
|
182
193
|
name: rule.name,
|
|
183
194
|
description: rule.description,
|
|
@@ -188,11 +199,23 @@ function finalizeSkillScores(skills, limit) {
|
|
|
188
199
|
embeddingScore: rule.embeddingScore,
|
|
189
200
|
reasons: rule.reasons || []
|
|
190
201
|
}))
|
|
191
|
-
.filter((skill) => Number(skill.keywordScore || 0) >=
|
|
192
|
-
.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name))
|
|
202
|
+
.filter((skill) => Number(skill.keywordScore || 0) >= minimumKeywordScore || Number(skill.embeddingScore || 0) >= 0.62)
|
|
203
|
+
.sort((a, b) => b.score - a.score || scopePriority(b.scope) - scopePriority(a.scope) || a.name.localeCompare(b.name));
|
|
204
|
+
const seen = new Set();
|
|
205
|
+
return ranked
|
|
206
|
+
.filter((skill) => {
|
|
207
|
+
const key = normalize(skill.name);
|
|
208
|
+
if (seen.has(key)) return false;
|
|
209
|
+
seen.add(key);
|
|
210
|
+
return true;
|
|
211
|
+
})
|
|
193
212
|
.slice(0, limit);
|
|
194
213
|
}
|
|
195
214
|
|
|
215
|
+
function scopePriority(scope) {
|
|
216
|
+
return scope === "project" ? 1 : 0;
|
|
217
|
+
}
|
|
218
|
+
|
|
196
219
|
function selectEmbeddingCandidates(skills) {
|
|
197
220
|
if (skills.length <= DEFAULT_EMBEDDING_CANDIDATES) return skills;
|
|
198
221
|
return [...skills]
|
|
@@ -217,21 +240,30 @@ export async function warmSkillEmbeddings({
|
|
|
217
240
|
});
|
|
218
241
|
}
|
|
219
242
|
|
|
220
|
-
function scoreSkillsByKeyword({ prompt, skills }) {
|
|
221
|
-
const normalizedPrompt =
|
|
243
|
+
function scoreSkillsByKeyword({ prompt, skills, projectHints = [] }) {
|
|
244
|
+
const normalizedPrompt = normalizePrompt(prompt);
|
|
222
245
|
const promptTokens = new Set(normalizedPrompt.split(/\s+/).filter(Boolean));
|
|
246
|
+
const projectTokens = new Set(projectHints);
|
|
223
247
|
return skills.map((skill, index) => {
|
|
224
248
|
const enriched = skill.searchTokens ? skill : enrichSkill(skill);
|
|
225
249
|
const name = String(enriched.name || "");
|
|
226
250
|
const description = truncateDescription(enriched.description || "");
|
|
227
251
|
const content = `${name} ${description}`;
|
|
228
|
-
const matches =
|
|
252
|
+
const matches = filterSkillMatches(
|
|
253
|
+
enriched.searchTokens.filter((token) => promptTokens.has(token) && token.length > 2 && !GENERIC_SKILL_TOKENS.has(token)),
|
|
254
|
+
{ normalizedPrompt, enriched }
|
|
255
|
+
);
|
|
256
|
+
const projectMatches = enriched.searchTokens.filter((token) => projectTokens.has(token) && SPECIALIZED_SKILL_TOKENS.has(token));
|
|
229
257
|
const normalizedName = enriched.normalizedName;
|
|
230
258
|
const nameTokens = enriched.nameTokens;
|
|
231
259
|
const nameHit = normalizedPrompt.includes(normalizedName);
|
|
232
260
|
const nameTokenHit = nameTokens.length > 1 && nameTokens.every((token) => promptTokens.has(token));
|
|
233
261
|
const scopeBonus = enriched.scope === "project" ? 0.08 : 0;
|
|
234
|
-
const
|
|
262
|
+
const intentBonus = skillIntentBonus(normalizedPrompt, enriched);
|
|
263
|
+
const domainEligible = isSkillDomainEligible(normalizedPrompt, enriched);
|
|
264
|
+
const matchScore = matches.reduce((sum, token) => sum + (SPECIALIZED_SKILL_TOKENS.has(token) ? 0.2 : 0.08), 0);
|
|
265
|
+
const projectBonus = matches.length && intentBonus ? Math.min(0.16, projectMatches.length * 0.04) : 0;
|
|
266
|
+
const score = Math.min(1, (matches.length ? 0.25 + matchScore : 0) + projectBonus + intentBonus + (nameHit ? 0.2 : 0) + (nameTokenHit ? 0.18 : 0) + scopeBonus);
|
|
235
267
|
return {
|
|
236
268
|
id: `skill-${index + 1}`,
|
|
237
269
|
name,
|
|
@@ -241,8 +273,11 @@ function scoreSkillsByKeyword({ prompt, skills }) {
|
|
|
241
273
|
content,
|
|
242
274
|
score,
|
|
243
275
|
keywordScore: score,
|
|
276
|
+
domainEligible,
|
|
244
277
|
reasons: [
|
|
245
278
|
...(matches.length ? [`keyword:${matches.slice(0, 4).join(",")}`] : []),
|
|
279
|
+
...(projectBonus ? [`project:${projectMatches.slice(0, 4).join(",")}`] : []),
|
|
280
|
+
...(intentBonus ? ["intent-match"] : []),
|
|
246
281
|
...(nameHit || nameTokenHit ? ["name-match"] : [])
|
|
247
282
|
],
|
|
248
283
|
originalOrder: index
|
|
@@ -250,6 +285,67 @@ function scoreSkillsByKeyword({ prompt, skills }) {
|
|
|
250
285
|
});
|
|
251
286
|
}
|
|
252
287
|
|
|
288
|
+
function filterSkillMatches(matches, { normalizedPrompt, enriched }) {
|
|
289
|
+
if (!/\beas\b/.test(normalizedPrompt)) return matches;
|
|
290
|
+
const skillText = normalize(`${enriched.name} ${enriched.description}`);
|
|
291
|
+
if (/\b(eas|expo|cicd)\b/.test(skillText)) return matches;
|
|
292
|
+
return matches.filter((token) => token !== "android" && token !== "ios");
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function isSkillDomainEligible(normalizedPrompt, enriched) {
|
|
296
|
+
if (!/\beas\b/.test(normalizedPrompt)) return true;
|
|
297
|
+
const skillText = normalize(`${enriched.name} ${enriched.description}`);
|
|
298
|
+
if (!/\b(android|ios)\b/.test(skillText)) return true;
|
|
299
|
+
return /\b(eas|expo|cicd)\b/.test(skillText);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function skillIntentBonus(normalizedPrompt, enriched) {
|
|
303
|
+
const skillText = normalize(`${enriched.name} ${enriched.description}`);
|
|
304
|
+
if (/\beas\b/.test(normalizedPrompt)
|
|
305
|
+
&& /\b(eas|expo)\b/.test(skillText)
|
|
306
|
+
&& /\b(cicd|workflow|workflows|build|deploy|deployment|pipeline|pipelines)\b/.test(skillText)) {
|
|
307
|
+
return 0.28;
|
|
308
|
+
}
|
|
309
|
+
return 0;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export function projectSkillHints({ cwd = process.cwd() } = {}) {
|
|
313
|
+
const hints = new Set();
|
|
314
|
+
const packagePaths = [path.join(cwd, "package.json")];
|
|
315
|
+
const rootPackage = readJson(path.join(cwd, "package.json"));
|
|
316
|
+
for (const workspace of rootPackage?.workspaces || []) {
|
|
317
|
+
if (typeof workspace !== "string" || workspace.includes("*")) continue;
|
|
318
|
+
packagePaths.push(path.join(cwd, workspace, "package.json"));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
for (const packagePath of packagePaths) {
|
|
322
|
+
const packageDir = path.dirname(packagePath);
|
|
323
|
+
const packageJson = readJson(packagePath);
|
|
324
|
+
addHintText(hints, JSON.stringify({
|
|
325
|
+
name: packageJson?.name,
|
|
326
|
+
description: packageJson?.description,
|
|
327
|
+
dependencies: Object.keys(packageJson?.dependencies || {}),
|
|
328
|
+
devDependencies: Object.keys(packageJson?.devDependencies || {})
|
|
329
|
+
}));
|
|
330
|
+
for (const fileName of ["app.json", "app.config.js", "app.config.ts", "eas.json"]) {
|
|
331
|
+
if (fs.existsSync(path.join(packageDir, fileName))) addHintText(hints, fileName);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return [...hints];
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function readJson(filePath) {
|
|
338
|
+
try {
|
|
339
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
340
|
+
} catch {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function addHintText(hints, value) {
|
|
346
|
+
for (const token of normalize(value).split(/\s+/).filter(Boolean)) hints.add(token);
|
|
347
|
+
}
|
|
348
|
+
|
|
253
349
|
function enrichSkill(skill) {
|
|
254
350
|
const name = String(skill.name || "");
|
|
255
351
|
const description = truncateDescription(skill.description || "");
|
|
@@ -268,3 +364,7 @@ function enrichSkill(skill) {
|
|
|
268
364
|
function normalize(value) {
|
|
269
365
|
return String(value || "").toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
|
|
270
366
|
}
|
|
367
|
+
|
|
368
|
+
function normalizePrompt(value) {
|
|
369
|
+
return normalize(String(value || "").replace(/https?:\/\/\S+/gi, " "));
|
|
370
|
+
}
|
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import readline from "node:readline/promises";
|
|
5
5
|
import { stdin as input, stdout as output } from "node:process";
|
|
6
6
|
import { execFileSync, execSync, spawn } from "node:child_process";
|
|
7
|
+
import { shellInvocation } from "./shell-runner.js";
|
|
7
8
|
|
|
8
9
|
const DEFAULT_AGENTS = ["codex", "claude", "antigravity", "copilot"];
|
|
9
10
|
const INSTALL_SH_URL = "https://raw.githubusercontent.com/runkids/skillshare/main/install.sh";
|
|
@@ -141,7 +142,8 @@ export async function installSkillshare({
|
|
|
141
142
|
if (osName === "windows") {
|
|
142
143
|
await spawnShellStreaming("powershell", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", `irm ${INSTALL_PS_URL} | iex`]);
|
|
143
144
|
} else {
|
|
144
|
-
|
|
145
|
+
const shell = shellInvocation(`curl -fsSL ${INSTALL_SH_URL} | sh`, { platform: process.platform });
|
|
146
|
+
await spawnShellStreaming(shell.command, shell.args);
|
|
145
147
|
}
|
|
146
148
|
}
|
|
147
149
|
|
|
@@ -206,14 +208,52 @@ export function detectExistingSkills({ cwd = process.cwd(), home = os.homedir()
|
|
|
206
208
|
.filter((entry) => entry.count > 0);
|
|
207
209
|
}
|
|
208
210
|
|
|
211
|
+
export function repairSkillSymlinks({
|
|
212
|
+
cwd = process.cwd(),
|
|
213
|
+
home = os.homedir(),
|
|
214
|
+
roots = skillRoots({ cwd, home }),
|
|
215
|
+
dryRun = false
|
|
216
|
+
} = {}) {
|
|
217
|
+
const repaired = [];
|
|
218
|
+
const removedBroken = [];
|
|
219
|
+
for (const root of roots) {
|
|
220
|
+
let entries = [];
|
|
221
|
+
try {
|
|
222
|
+
entries = fs.readdirSync(root, { withFileTypes: true });
|
|
223
|
+
} catch {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
for (const entry of entries) {
|
|
227
|
+
if (!entry.isSymbolicLink()) continue;
|
|
228
|
+
const linkPath = path.join(root, entry.name);
|
|
229
|
+
const real = safeRealpath(linkPath);
|
|
230
|
+
if (!real) {
|
|
231
|
+
if (!dryRun) fs.rmSync(linkPath, { force: true, recursive: true });
|
|
232
|
+
removedBroken.push(linkPath);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (!dryRun) {
|
|
236
|
+
fs.rmSync(linkPath, { force: true, recursive: true });
|
|
237
|
+
const stat = fs.statSync(real);
|
|
238
|
+
if (stat.isDirectory()) copyDirectory(real, linkPath);
|
|
239
|
+
else if (stat.isFile()) fs.copyFileSync(real, linkPath);
|
|
240
|
+
}
|
|
241
|
+
repaired.push(linkPath);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return { repaired: [...new Set(repaired)], removedBroken: [...new Set(removedBroken)] };
|
|
245
|
+
}
|
|
246
|
+
|
|
209
247
|
function skillRoots({ cwd, home }) {
|
|
210
248
|
return uniquePaths([
|
|
211
249
|
path.join(home, ".claude", "skills"),
|
|
212
250
|
path.join(home, ".codex", "skills"),
|
|
251
|
+
path.join(home, ".agents", "skills"),
|
|
213
252
|
path.join(home, ".gemini", "antigravity", "skills"),
|
|
214
253
|
path.join(home, ".gemini", "antigravity-cli", "skills"),
|
|
215
254
|
path.join(cwd, ".claude", "skills"),
|
|
216
255
|
path.join(cwd, ".codex", "skills"),
|
|
256
|
+
path.join(cwd, ".agents", "skills"),
|
|
217
257
|
path.join(cwd, ".gemini", "antigravity", "skills"),
|
|
218
258
|
path.join(cwd, ".gemini", "antigravity-cli", "skills"),
|
|
219
259
|
...discoverSkillRoots({ cwd, home })
|
|
@@ -236,9 +276,11 @@ export function discoverSkillRoots({ cwd = process.cwd(), home = os.homedir() }
|
|
|
236
276
|
path.join(home, ".gemini"),
|
|
237
277
|
path.join(home, ".codex"),
|
|
238
278
|
path.join(home, ".claude"),
|
|
279
|
+
path.join(home, ".agents"),
|
|
239
280
|
path.join(cwd, ".gemini"),
|
|
240
281
|
path.join(cwd, ".codex"),
|
|
241
|
-
path.join(cwd, ".claude")
|
|
282
|
+
path.join(cwd, ".claude"),
|
|
283
|
+
path.join(cwd, ".agents")
|
|
242
284
|
]) {
|
|
243
285
|
findSkillRoots(base, 0, roots);
|
|
244
286
|
}
|
|
@@ -436,6 +478,13 @@ export async function syncSkills({
|
|
|
436
478
|
}
|
|
437
479
|
|
|
438
480
|
if (!options.noCollect) {
|
|
481
|
+
const repaired = repairSkillSymlinks({ cwd, home, dryRun: options.dryRun });
|
|
482
|
+
if (repaired.repaired.length || repaired.removedBroken.length) {
|
|
483
|
+
logger(statusLine("Repairing skill symlinks...", options.dryRun
|
|
484
|
+
? `dry-run (${repaired.repaired.length} would copy, ${repaired.removedBroken.length} broken)`
|
|
485
|
+
: `✓ ${repaired.repaired.length} copied, ${repaired.removedBroken.length} broken removed`));
|
|
486
|
+
}
|
|
487
|
+
|
|
439
488
|
const legacy = collectAntigravityLegacySkills({ cwd, home, dryRun: options.dryRun });
|
|
440
489
|
if (legacy.copied.length || legacy.skipped.length) {
|
|
441
490
|
const value = options.dryRun
|
|
@@ -2,7 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
|
|
3
3
|
import { appendJsonLine, readJsonFile, writeJsonFile } from "./fs-utils.js";
|
|
4
4
|
import { readGitSnapshot, checkCompliance } from "./measure.js";
|
|
5
|
-
import { buildReport
|
|
5
|
+
import { buildReport } from "./reporter.js";
|
|
6
6
|
import { loadRuntimeEvidence } from "./telemetry.js";
|
|
7
7
|
import { filterActionableRules } from "./analyzer.js";
|
|
8
8
|
import { resolveHookCwd } from "./hook-io.js";
|
|
@@ -54,7 +54,6 @@ export function handleStopPayload(payload, { contextPath, reportPath, historyPat
|
|
|
54
54
|
if (historyPath) appendJsonLine(historyPath, report);
|
|
55
55
|
|
|
56
56
|
return {
|
|
57
|
-
continue: true
|
|
58
|
-
systemMessage: formatReport(report)
|
|
57
|
+
continue: true
|
|
59
58
|
};
|
|
60
59
|
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { parse } from "smol-toml";
|
|
2
|
+
|
|
3
|
+
export function readMcpServersFromToml(content) {
|
|
4
|
+
const config = parseToml(content);
|
|
5
|
+
const servers = config.mcp_servers && typeof config.mcp_servers === "object"
|
|
6
|
+
? config.mcp_servers
|
|
7
|
+
: {};
|
|
8
|
+
|
|
9
|
+
return Object.entries(servers)
|
|
10
|
+
.filter(([, server]) => server && typeof server.command === "string")
|
|
11
|
+
.map(([name, server]) => ({
|
|
12
|
+
name,
|
|
13
|
+
command: server.command,
|
|
14
|
+
args: Array.isArray(server.args) ? server.args.map(String) : []
|
|
15
|
+
}));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function updateMcpServerFields(content, name, fields) {
|
|
19
|
+
parseToml(content);
|
|
20
|
+
const lines = String(content || "").split(/\r?\n/);
|
|
21
|
+
const section = findMcpServerSection(lines, name);
|
|
22
|
+
if (!section) return content;
|
|
23
|
+
|
|
24
|
+
let body = lines.slice(section.start + 1, section.end);
|
|
25
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
26
|
+
body = replaceOrInsertField(body, key, formatTomlValue(value));
|
|
27
|
+
}
|
|
28
|
+
lines.splice(section.start + 1, section.end - section.start - 1, ...body);
|
|
29
|
+
return lines.join("\n");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function formatTomlValue(value) {
|
|
33
|
+
if (Array.isArray(value)) return `[${value.map((item) => JSON.stringify(String(item))).join(", ")}]`;
|
|
34
|
+
return JSON.stringify(String(value));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseToml(content) {
|
|
38
|
+
return parse(String(content || ""));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function findMcpServerSection(lines, name) {
|
|
42
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
43
|
+
const sectionName = mcpServerNameFromHeader(lines[index]);
|
|
44
|
+
if (sectionName !== name) continue;
|
|
45
|
+
let end = lines.length;
|
|
46
|
+
for (let cursor = index + 1; cursor < lines.length; cursor += 1) {
|
|
47
|
+
if (/^\s*\[/.test(lines[cursor])) {
|
|
48
|
+
end = cursor;
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return { start: index, end };
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function mcpServerNameFromHeader(line) {
|
|
58
|
+
const match = String(line || "").match(/^\s*\[mcp_servers\.([^\]]+)\]\s*(?:#.*)?$/);
|
|
59
|
+
if (!match || match[1].includes(".tools.")) return null;
|
|
60
|
+
const raw = match[1].trim();
|
|
61
|
+
if (!raw.startsWith('"')) return raw;
|
|
62
|
+
try {
|
|
63
|
+
return JSON.parse(raw);
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function replaceOrInsertField(body, key, value) {
|
|
70
|
+
const next = [...body];
|
|
71
|
+
const start = next.findIndex((line) => new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`).test(line));
|
|
72
|
+
const line = `${key} = ${value}`;
|
|
73
|
+
if (start < 0) {
|
|
74
|
+
next.unshift(line);
|
|
75
|
+
return next;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let end = start + 1;
|
|
79
|
+
if (valueForField(next[start]).trimStart().startsWith("[")) {
|
|
80
|
+
while (end < next.length && !isBalancedTomlArray(next.slice(start, end).join("\n"))) end += 1;
|
|
81
|
+
}
|
|
82
|
+
next.splice(start, Math.max(1, end - start), line);
|
|
83
|
+
return next;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function valueForField(line) {
|
|
87
|
+
return String(line || "").slice(String(line || "").indexOf("=") + 1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function isBalancedTomlArray(value) {
|
|
91
|
+
let depth = 0;
|
|
92
|
+
let quoted = false;
|
|
93
|
+
let escaped = false;
|
|
94
|
+
for (const char of String(value || "")) {
|
|
95
|
+
if (escaped) {
|
|
96
|
+
escaped = false;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (char === "\\" && quoted) {
|
|
100
|
+
escaped = true;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (char === '"') {
|
|
104
|
+
quoted = !quoted;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (quoted) continue;
|
|
108
|
+
if (char === "[") depth += 1;
|
|
109
|
+
if (char === "]") depth -= 1;
|
|
110
|
+
}
|
|
111
|
+
return depth <= 0;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function escapeRegExp(value) {
|
|
115
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
116
|
+
}
|
|
@@ -6,7 +6,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
6
6
|
|
|
7
7
|
import { isModelCacheReady, modelCacheDir } from "../lib/embedding-scorer.js";
|
|
8
8
|
import { scoreContext } from "../lib/score-context.js";
|
|
9
|
-
import { ctxMcpSocketPath } from "../lib/ctx-mcp-client.js";
|
|
9
|
+
import { CTX_MCP_BRIDGE_REVISION, ctxMcpSocketPath } from "../lib/ctx-mcp-client.js";
|
|
10
10
|
import { defaultDataRoot } from "../lib/workspace-data.js";
|
|
11
11
|
import { createContextOSMcpServer } from "./contextos-server.js";
|
|
12
12
|
|
|
@@ -33,6 +33,9 @@ function startBridge() {
|
|
|
33
33
|
fs.rmSync(socketPath, { force: true });
|
|
34
34
|
const bridge = net.createServer((socket) => {
|
|
35
35
|
let raw = "";
|
|
36
|
+
socket.on("error", () => {
|
|
37
|
+
// Clients may time out and close while scoring is still in progress.
|
|
38
|
+
});
|
|
36
39
|
socket.on("data", (chunk) => {
|
|
37
40
|
raw += chunk.toString("utf8");
|
|
38
41
|
if (raw.includes("\n")) handleBridgeRequest(socket, raw);
|
|
@@ -68,9 +71,10 @@ async function handleBridgeRequest(socket, raw) {
|
|
|
68
71
|
skills: payload.skills,
|
|
69
72
|
workflows: payload.workflows
|
|
70
73
|
});
|
|
71
|
-
socket.end(JSON.stringify(result));
|
|
74
|
+
socket.end(JSON.stringify({ ...result, bridgeRevision: CTX_MCP_BRIDGE_REVISION }));
|
|
72
75
|
} catch (error) {
|
|
73
76
|
socket.end(JSON.stringify({
|
|
77
|
+
bridgeRevision: CTX_MCP_BRIDGE_REVISION,
|
|
74
78
|
error: error?.message || String(error),
|
|
75
79
|
scoredRules: [],
|
|
76
80
|
suggestedFiles: [],
|