@minhpnq1807/contextos 0.5.50 → 0.5.52
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 +15 -0
- package/README.md +114 -9
- package/bin/ctx.js +64 -8
- package/eval/skill-routing/cases.yaml +366 -0
- package/eval/skill-routing/fixtures/docker-node/Dockerfile +4 -0
- package/eval/skill-routing/fixtures/docker-node/docker-compose.yml +5 -0
- package/eval/skill-routing/fixtures/docker-node/package.json +6 -0
- package/eval/skill-routing/fixtures/expo-eas/.github/workflows/eas.yml +1 -0
- package/eval/skill-routing/fixtures/expo-eas/app.json +5 -0
- package/eval/skill-routing/fixtures/expo-eas/eas.json +6 -0
- package/eval/skill-routing/fixtures/expo-eas/package.json +11 -0
- package/eval/skill-routing/fixtures/expo-with-vercel-json/app.json +6 -0
- package/eval/skill-routing/fixtures/expo-with-vercel-json/eas.json +5 -0
- package/eval/skill-routing/fixtures/expo-with-vercel-json/package.json +8 -0
- package/eval/skill-routing/fixtures/expo-with-vercel-json/vercel.json +3 -0
- package/eval/skill-routing/fixtures/express-mongo-jwt/package.json +8 -0
- package/eval/skill-routing/fixtures/firebase-hosting/firebase.json +11 -0
- package/eval/skill-routing/fixtures/firebase-hosting/package.json +6 -0
- package/eval/skill-routing/fixtures/flutter-firebase/pubspec.yaml +5 -0
- package/eval/skill-routing/fixtures/frontend-only-next/package.json +8 -0
- package/eval/skill-routing/fixtures/integration-test/jest.config.js +3 -0
- package/eval/skill-routing/fixtures/integration-test/package.json +10 -0
- package/eval/skill-routing/fixtures/jest-project/jest.config.js +3 -0
- package/eval/skill-routing/fixtures/jest-project/package.json +7 -0
- package/eval/skill-routing/fixtures/nest-prisma/package.json +10 -0
- package/eval/skill-routing/fixtures/nest-prisma/prisma/schema.prisma +4 -0
- package/eval/skill-routing/fixtures/next-vercel/.github/workflows/deploy.yml +1 -0
- package/eval/skill-routing/fixtures/next-vercel/package.json +8 -0
- package/eval/skill-routing/fixtures/next-vercel/vercel.json +3 -0
- package/eval/skill-routing/fixtures/oauth-google/.env.example +3 -0
- package/eval/skill-routing/fixtures/oauth-google/package.json +9 -0
- package/eval/skill-routing/fixtures/password-reset/package.json +8 -0
- package/eval/skill-routing/fixtures/playwright-project/package.json +6 -0
- package/eval/skill-routing/fixtures/playwright-project/playwright.config.ts +5 -0
- package/eval/skill-routing/fixtures/railway-render/package.json +6 -0
- package/eval/skill-routing/fixtures/railway-render/railway.json +6 -0
- package/eval/skill-routing/fixtures/railway-render/render.yaml +5 -0
- package/eval/skill-routing/fixtures/rbac-api/package.json +8 -0
- package/eval/skill-routing/fixtures/redis-cache/package.json +7 -0
- package/eval/skill-routing/fixtures/static-docs/README.md +3 -0
- package/eval/skill-routing/run-eval.js +278 -0
- package/package.json +3 -1
- package/plugins/ctx/.codex-plugin/plugin.json +1 -1
- package/plugins/ctx/lib/analyzer.js +17 -2
- package/plugins/ctx/lib/auto-warm.js +1 -0
- package/plugins/ctx/lib/ctx-mcp-client.js +21 -0
- package/plugins/ctx/lib/embedding-scorer.js +34 -0
- package/plugins/ctx/lib/hook-io.js +11 -1
- package/plugins/ctx/lib/package-install.js +1 -1
- package/plugins/ctx/lib/project-profiler.js +5 -1
- package/plugins/ctx/lib/prompt-hook.js +17 -2
- package/plugins/ctx/lib/score-context.js +13 -2
- package/plugins/ctx/lib/setup-wizard.js +8 -3
- package/plugins/ctx/lib/skill-discoverer.js +480 -27
- package/plugins/ctx/lib/skillshare-sync.js +112 -0
- package/plugins/ctx/lib/workflow-discoverer.js +3 -1
- package/plugins/ctx/mcp/contextos-server.js +29 -1
- package/plugins/ctx/mcp/server.js +50 -4
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
import { suggestSkills } from "../../plugins/ctx/lib/skill-discoverer.js";
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const evalRoot = __dirname;
|
|
10
|
+
|
|
11
|
+
export async function runSkillRoutingEval({
|
|
12
|
+
rootDir = path.resolve(evalRoot, "..", ".."),
|
|
13
|
+
casesPath = path.join(evalRoot, "cases.yaml"),
|
|
14
|
+
topK = 3,
|
|
15
|
+
threshold = 0.5
|
|
16
|
+
} = {}) {
|
|
17
|
+
const config = parseEvalYaml(fs.readFileSync(casesPath, "utf8"));
|
|
18
|
+
const skills = config.skills.map((skill) => ({
|
|
19
|
+
name: skill.id,
|
|
20
|
+
description: skill.description,
|
|
21
|
+
path: path.join(evalRoot, "skills", skill.id, "SKILL.md"),
|
|
22
|
+
metadata: skillToMetadata(skill)
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
const rows = [];
|
|
26
|
+
for (const testCase of config.cases) {
|
|
27
|
+
const cwd = testCase.fixture === "contextos"
|
|
28
|
+
? rootDir
|
|
29
|
+
: path.join(evalRoot, "fixtures", testCase.fixture);
|
|
30
|
+
const scores = semanticScores({ prompt: testCase.prompt, skills });
|
|
31
|
+
const suggestions = await suggestSkills({
|
|
32
|
+
cwd,
|
|
33
|
+
prompt: testCase.prompt,
|
|
34
|
+
skills,
|
|
35
|
+
limit: Math.max(topK, 10),
|
|
36
|
+
dataDir: path.join(evalRoot, ".tmp"),
|
|
37
|
+
indexedSearcher: async () => ({
|
|
38
|
+
status: "enabled",
|
|
39
|
+
items: skills.map((skill) => ({
|
|
40
|
+
id: normalizeSkillId(skill.name),
|
|
41
|
+
text: skill.name,
|
|
42
|
+
embeddingScore: scores.get(skill.name) || 0.45
|
|
43
|
+
}))
|
|
44
|
+
})
|
|
45
|
+
});
|
|
46
|
+
const selected = suggestions
|
|
47
|
+
.filter((skill) => Number(skill.confidence || skill.score || 0) >= threshold)
|
|
48
|
+
.slice(0, topK);
|
|
49
|
+
rows.push({
|
|
50
|
+
prompt: testCase.prompt,
|
|
51
|
+
fixture: testCase.fixture,
|
|
52
|
+
expected: testCase.expected,
|
|
53
|
+
allowed: testCase.allowed || [],
|
|
54
|
+
forbidden: testCase.forbidden,
|
|
55
|
+
selected,
|
|
56
|
+
selectedIds: selected.map((skill) => skill.name)
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return summarizeRows(rows, { topK });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function formatSkillRoutingBenchmark(result) {
|
|
64
|
+
const percent = (value) => `${(value * 100).toFixed(1)}%`;
|
|
65
|
+
const failures = result.rows.filter((row) => row.failed);
|
|
66
|
+
const lines = [
|
|
67
|
+
"Skill Routing Benchmark",
|
|
68
|
+
`Cases: ${result.caseCount}`,
|
|
69
|
+
`Top-1 Accuracy: ${percent(result.top1Accuracy)}`,
|
|
70
|
+
`Top-3 Recall: ${percent(result.top3Recall)}`,
|
|
71
|
+
`False Positive Rate: ${percent(result.falsePositiveRate)}`,
|
|
72
|
+
`Confidence Calibration: ${percent(result.confidenceCalibration)}`,
|
|
73
|
+
`Negative Gate Accuracy: ${percent(result.negativeGateAccuracy)}`,
|
|
74
|
+
"",
|
|
75
|
+
failures.length ? "Failures:" : "Failures: none"
|
|
76
|
+
];
|
|
77
|
+
for (const row of failures) {
|
|
78
|
+
lines.push(`- ${row.fixture}: "${row.prompt}"`);
|
|
79
|
+
lines.push(` expected: ${row.expected.join(", ") || "(none)"}`);
|
|
80
|
+
if (row.allowed.length) {
|
|
81
|
+
lines.push(` allowed: ${row.allowed.join(", ")}`);
|
|
82
|
+
}
|
|
83
|
+
lines.push(` selected: ${row.selectedIds.join(", ") || "(none)"}`);
|
|
84
|
+
if (row.forbidden.length) {
|
|
85
|
+
lines.push(` rejected: ${row.forbidden.join(", ")}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return lines.join("\n");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function summarizeRows(rows, { topK }) {
|
|
92
|
+
let top1Hits = 0;
|
|
93
|
+
let recallHits = 0;
|
|
94
|
+
let recallTotal = 0;
|
|
95
|
+
let falsePositiveHits = 0;
|
|
96
|
+
let falsePositiveTotal = 0;
|
|
97
|
+
let negativeGateHits = 0;
|
|
98
|
+
let negativeGateTotal = 0;
|
|
99
|
+
let calibrationHits = 0;
|
|
100
|
+
let calibrationTotal = 0;
|
|
101
|
+
|
|
102
|
+
for (const row of rows) {
|
|
103
|
+
const expected = new Set(row.expected);
|
|
104
|
+
const accepted = new Set([...row.expected, ...row.allowed]);
|
|
105
|
+
const selected = row.selectedIds.slice(0, topK);
|
|
106
|
+
row.failed = false;
|
|
107
|
+
if (!row.expected.length) {
|
|
108
|
+
if (!selected.length) {
|
|
109
|
+
top1Hits += 1;
|
|
110
|
+
}
|
|
111
|
+
} else if (expected.has(selected[0])) {
|
|
112
|
+
top1Hits += 1;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
recallTotal += row.expected.length;
|
|
116
|
+
for (const skill of row.expected) {
|
|
117
|
+
if (selected.includes(skill)) {
|
|
118
|
+
recallHits += 1;
|
|
119
|
+
} else {
|
|
120
|
+
row.failed = true;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
falsePositiveTotal += selected.length;
|
|
125
|
+
for (const skill of selected) {
|
|
126
|
+
if (!accepted.has(skill)) {
|
|
127
|
+
falsePositiveHits += 1;
|
|
128
|
+
row.failed = true;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
negativeGateTotal += row.forbidden.length;
|
|
133
|
+
for (const skill of row.forbidden) {
|
|
134
|
+
if (!selected.includes(skill)) {
|
|
135
|
+
negativeGateHits += 1;
|
|
136
|
+
} else {
|
|
137
|
+
row.failed = true;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
for (const skill of row.selected) {
|
|
142
|
+
calibrationTotal += 1;
|
|
143
|
+
const acceptedSelected = accepted.has(skill.name);
|
|
144
|
+
const confident = Number(skill.confidence || 0) >= 0.65;
|
|
145
|
+
if (acceptedSelected === confident) calibrationHits += 1;
|
|
146
|
+
}
|
|
147
|
+
if (!row.selected.length && !row.expected.length) {
|
|
148
|
+
calibrationTotal += 1;
|
|
149
|
+
calibrationHits += 1;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
caseCount: rows.length,
|
|
155
|
+
top1Accuracy: rows.length ? top1Hits / rows.length : 0,
|
|
156
|
+
top3Recall: recallTotal ? recallHits / recallTotal : 1,
|
|
157
|
+
falsePositiveRate: falsePositiveTotal ? falsePositiveHits / falsePositiveTotal : 0,
|
|
158
|
+
confidenceCalibration: calibrationTotal ? calibrationHits / calibrationTotal : 1,
|
|
159
|
+
negativeGateAccuracy: negativeGateTotal ? negativeGateHits / negativeGateTotal : 1,
|
|
160
|
+
rows
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function skillToMetadata(skill) {
|
|
165
|
+
return {
|
|
166
|
+
id: skill.id,
|
|
167
|
+
name: skill.id,
|
|
168
|
+
positivePrompts: skill.positive_triggers?.prompts || [],
|
|
169
|
+
files: skill.positive_triggers?.files || [],
|
|
170
|
+
dependencies: skill.positive_triggers?.dependencies || [],
|
|
171
|
+
negativePrompts: skill.negative_triggers?.prompts || [],
|
|
172
|
+
negativeFiles: skill.negative_triggers?.files || [],
|
|
173
|
+
negativeDependencies: skill.negative_triggers?.dependencies || [],
|
|
174
|
+
relatedSkills: skill.related_skills || []
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function semanticScores({ prompt, skills }) {
|
|
179
|
+
const promptTokens = new Set(tokenize(prompt));
|
|
180
|
+
const scores = new Map();
|
|
181
|
+
for (const skill of skills) {
|
|
182
|
+
const tokens = new Set(tokenize(`${skill.name} ${skill.description}`));
|
|
183
|
+
const overlap = [...promptTokens].filter((token) => tokens.has(token)).length;
|
|
184
|
+
scores.set(skill.name, Math.max(0.45, Math.min(0.92, 0.5 + overlap * 0.1)));
|
|
185
|
+
}
|
|
186
|
+
return scores;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function tokenize(value) {
|
|
190
|
+
return String(value || "").toLowerCase().split(/[^a-z0-9@.-]+/).filter((token) => token.length > 2);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function normalizeSkillId(name) {
|
|
194
|
+
return String(name || "").toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function parseEvalYaml(content) {
|
|
198
|
+
const lines = String(content || "").split(/\r?\n/);
|
|
199
|
+
const result = { skills: [], cases: [] };
|
|
200
|
+
let section = null;
|
|
201
|
+
let current = null;
|
|
202
|
+
let triggerGroup = null;
|
|
203
|
+
|
|
204
|
+
for (const rawLine of lines) {
|
|
205
|
+
if (!rawLine.trim() || rawLine.trimStart().startsWith("#")) continue;
|
|
206
|
+
const line = rawLine.trim();
|
|
207
|
+
if (line === "skills:") {
|
|
208
|
+
section = "skills";
|
|
209
|
+
current = null;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (line === "cases:") {
|
|
213
|
+
section = "cases";
|
|
214
|
+
current = null;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (line.startsWith("- id:")) {
|
|
218
|
+
current = {
|
|
219
|
+
id: scalar(line.slice(5)),
|
|
220
|
+
description: "",
|
|
221
|
+
positive_triggers: {},
|
|
222
|
+
negative_triggers: {},
|
|
223
|
+
related_skills: []
|
|
224
|
+
};
|
|
225
|
+
result.skills.push(current);
|
|
226
|
+
triggerGroup = null;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (line.startsWith("- prompt:")) {
|
|
230
|
+
current = {
|
|
231
|
+
prompt: scalar(line.slice(9)),
|
|
232
|
+
fixture: "",
|
|
233
|
+
expected: [],
|
|
234
|
+
allowed: [],
|
|
235
|
+
forbidden: []
|
|
236
|
+
};
|
|
237
|
+
result.cases.push(current);
|
|
238
|
+
triggerGroup = null;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if (!current) continue;
|
|
242
|
+
if (line === "positive_triggers:") {
|
|
243
|
+
triggerGroup = current.positive_triggers;
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (line === "negative_triggers:") {
|
|
247
|
+
triggerGroup = current.negative_triggers;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
|
|
251
|
+
if (!match) continue;
|
|
252
|
+
const [, key, rawValue] = match;
|
|
253
|
+
const normalizedKey = key === "rejected" ? "forbidden" : key;
|
|
254
|
+
const target = triggerGroup && ["prompts", "files", "dependencies"].includes(key)
|
|
255
|
+
? triggerGroup
|
|
256
|
+
: current;
|
|
257
|
+
target[normalizedKey] = parseValue(rawValue);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return result;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function parseValue(value) {
|
|
264
|
+
const trimmed = String(value || "").trim();
|
|
265
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
266
|
+
return trimmed.slice(1, -1).split(",").map(scalar).filter(Boolean);
|
|
267
|
+
}
|
|
268
|
+
return scalar(trimmed);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function scalar(value) {
|
|
272
|
+
return String(value || "").trim().replace(/^["']|["']$/g, "");
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
276
|
+
const result = await runSkillRoutingEval();
|
|
277
|
+
console.log(formatSkillRoutingBenchmark(result));
|
|
278
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@minhpnq1807/contextos",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.52",
|
|
4
4
|
"description": "Task-aware AGENTS.md context injection and compliance reporting for Codex, Claude Code, and Antigravity.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"README.md",
|
|
15
15
|
"DEMO.md",
|
|
16
16
|
"LAUNCH.md",
|
|
17
|
+
"eval/",
|
|
17
18
|
"docs/",
|
|
18
19
|
"LICENSE",
|
|
19
20
|
"CHANGELOG.md"
|
|
@@ -22,6 +23,7 @@
|
|
|
22
23
|
"test": "vitest run test",
|
|
23
24
|
"build": "node bin/ctx.js --version",
|
|
24
25
|
"validate:plugin": "node test/validate-plugin.js",
|
|
26
|
+
"benchmark:skills": "node bin/ctx.js benchmark --skills",
|
|
25
27
|
"test:mcp": "node test/mcp-protocol-smoke.js"
|
|
26
28
|
},
|
|
27
29
|
"engines": {
|
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { findGraphRelevantFiles, mergeRelevantFiles } from "./graph-retriever.js";
|
|
4
4
|
import { expandImportGraph } from "./import-graph.js";
|
|
5
5
|
import { findEmbeddingRelevantFiles } from "./file-embedding-retriever.js";
|
|
6
|
+
import { workspacePackagePaths } from "./project-profiler.js";
|
|
6
7
|
|
|
7
8
|
const STOP_WORDS = new Set([
|
|
8
9
|
"a", "an", "and", "are", "as", "at", "be", "by", "cho", "co", "cua", "do", "fix", "for",
|
|
@@ -433,9 +434,9 @@ function addAll(target, values) {
|
|
|
433
434
|
export function findExplicitPromptFiles({ cwd = process.cwd(), task = "", limit = 6 } = {}) {
|
|
434
435
|
const candidates = new Set();
|
|
435
436
|
const normalizedTask = String(task || "").replace(/\/\s+/g, "/");
|
|
436
|
-
const matches = normalizedTask.match(/[A-Za-z0-9_.()[\]
|
|
437
|
+
const matches = normalizedTask.match(/[A-Za-z0-9_.()[\]@~:,-]+(?:\/[A-Za-z0-9_.()[\]@~:,-]+)+/g) || [];
|
|
437
438
|
for (const match of matches) {
|
|
438
|
-
const cleaned = match
|
|
439
|
+
const cleaned = cleanPromptFilePath(match);
|
|
439
440
|
for (const filePath of resolvePromptPathCandidates({ cwd, promptPath: cleaned })) {
|
|
440
441
|
candidates.add(filePath);
|
|
441
442
|
if (candidates.size >= limit) break;
|
|
@@ -450,6 +451,13 @@ export function findExplicitPromptFiles({ cwd = process.cwd(), task = "", limit
|
|
|
450
451
|
}));
|
|
451
452
|
}
|
|
452
453
|
|
|
454
|
+
function cleanPromptFilePath(value) {
|
|
455
|
+
return String(value || "")
|
|
456
|
+
.replace(/\.(tsx?|jsx?|mjs|cjs|json|md|sql|py)\(\d+(?:,\d+)?\)[),.;:]*$/i, ".$1")
|
|
457
|
+
.replace(/\.(tsx?|jsx?|mjs|cjs|json|md|sql|py):\d+(?::\d+)?[),.;:]*$/i, ".$1")
|
|
458
|
+
.replace(/[),.;:]+$/g, "");
|
|
459
|
+
}
|
|
460
|
+
|
|
453
461
|
function resolvePromptPathCandidates({ cwd, promptPath }) {
|
|
454
462
|
if (!promptPath || promptPath.includes("://")) return [];
|
|
455
463
|
const relative = promptPath.replace(/^\.?\//, "");
|
|
@@ -470,6 +478,13 @@ function resolvePromptPathCandidates({ cwd, promptPath }) {
|
|
|
470
478
|
if (isSourceFile(candidate)) resolved.push(path.relative(cwd, candidate));
|
|
471
479
|
}
|
|
472
480
|
}
|
|
481
|
+
if (!resolved.length && !relative.startsWith("..")) {
|
|
482
|
+
for (const packagePath of workspacePackagePaths(cwd).slice(1)) {
|
|
483
|
+
const packageDir = path.dirname(packagePath);
|
|
484
|
+
const candidate = path.join(packageDir, relative);
|
|
485
|
+
if (isSourceFile(candidate)) resolved.push(path.relative(cwd, candidate));
|
|
486
|
+
}
|
|
487
|
+
}
|
|
473
488
|
return resolved;
|
|
474
489
|
}
|
|
475
490
|
|
|
@@ -57,6 +57,7 @@ export function maybeAutoWarmWorkspace({
|
|
|
57
57
|
function shouldAutoWarm(reason) {
|
|
58
58
|
if (reason === "no-context-candidates") return true;
|
|
59
59
|
if (reason === "enabled-sections-empty-after-formatting") return true;
|
|
60
|
+
if (String(reason || "").startsWith("enabled-sections-missing-candidates:")) return true;
|
|
60
61
|
return false;
|
|
61
62
|
}
|
|
62
63
|
|
|
@@ -29,6 +29,25 @@ export async function callCtxScoreContext(payload, {
|
|
|
29
29
|
connectTimeoutMs = Number(process.env.CONTEXTOS_MCP_CONNECT_TIMEOUT_MS || DEFAULT_CONNECT_TIMEOUT_MS),
|
|
30
30
|
createConnection = net.createConnection
|
|
31
31
|
} = {}) {
|
|
32
|
+
return callBridge(payload, { dataDir, timeoutMs, connectTimeoutMs, createConnection });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function callCtxHealth({
|
|
36
|
+
dataDir = defaultDataDir(),
|
|
37
|
+
timeoutMs = Number(process.env.CONTEXTOS_MCP_HEALTH_TIMEOUT_MS || 250),
|
|
38
|
+
connectTimeoutMs = Number(process.env.CONTEXTOS_MCP_CONNECT_TIMEOUT_MS || DEFAULT_CONNECT_TIMEOUT_MS),
|
|
39
|
+
createConnection = net.createConnection
|
|
40
|
+
} = {}) {
|
|
41
|
+
const response = await callBridge({ type: "health" }, { dataDir, timeoutMs, connectTimeoutMs, createConnection });
|
|
42
|
+
return response.health || {};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function callBridge(payload, {
|
|
46
|
+
dataDir,
|
|
47
|
+
timeoutMs,
|
|
48
|
+
connectTimeoutMs,
|
|
49
|
+
createConnection
|
|
50
|
+
}) {
|
|
32
51
|
const socketPath = ctxMcpSocketPath(dataDir);
|
|
33
52
|
if (!fs.existsSync(socketPath)) {
|
|
34
53
|
throw new Error(`ctx-mcp bridge socket not found: ${socketPath}`);
|
|
@@ -40,6 +59,7 @@ export async function callCtxScoreContext(payload, {
|
|
|
40
59
|
let raw = "";
|
|
41
60
|
let responseTimer;
|
|
42
61
|
const connectTimer = setTimeout(() => {
|
|
62
|
+
invalidateSocketIfUnchanged(socketPath, socketIdentity);
|
|
43
63
|
client.destroy();
|
|
44
64
|
reject(new Error(`ctx-mcp bridge connect timed out after ${connectTimeoutMs}ms`));
|
|
45
65
|
}, connectTimeoutMs);
|
|
@@ -47,6 +67,7 @@ export async function callCtxScoreContext(payload, {
|
|
|
47
67
|
client.on("connect", () => {
|
|
48
68
|
clearTimeout(connectTimer);
|
|
49
69
|
responseTimer = setTimeout(() => {
|
|
70
|
+
invalidateSocketIfUnchanged(socketPath, socketIdentity);
|
|
50
71
|
client.destroy();
|
|
51
72
|
reject(new Error(`ctx-mcp bridge timed out after ${timeoutMs}ms`));
|
|
52
73
|
}, timeoutMs);
|
|
@@ -152,6 +152,40 @@ export async function warmIndexedEmbeddings({
|
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
export async function preloadEmbeddingPipeline({
|
|
156
|
+
dataDir = defaultDataRoot(),
|
|
157
|
+
allowRemote = false,
|
|
158
|
+
warmText = "contextos warmup"
|
|
159
|
+
} = {}) {
|
|
160
|
+
if (!allowRemote && !isModelCacheReady(dataDir)) {
|
|
161
|
+
return { status: "missing-model", loaded: false, cachePath: path.join(dataDir, "embeddings.db") };
|
|
162
|
+
}
|
|
163
|
+
const started = Date.now();
|
|
164
|
+
try {
|
|
165
|
+
const embedder = await getExtractor({ allowRemote, dataDir });
|
|
166
|
+
await embedder(String(warmText || "contextos warmup"), {
|
|
167
|
+
pooling: "mean",
|
|
168
|
+
normalize: true
|
|
169
|
+
});
|
|
170
|
+
return {
|
|
171
|
+
status: "loaded",
|
|
172
|
+
loaded: true,
|
|
173
|
+
model: DEFAULT_MODEL,
|
|
174
|
+
cachePath: path.join(dataDir, "embeddings.db"),
|
|
175
|
+
elapsedMs: Date.now() - started
|
|
176
|
+
};
|
|
177
|
+
} catch (error) {
|
|
178
|
+
return {
|
|
179
|
+
status: "load-failed",
|
|
180
|
+
loaded: false,
|
|
181
|
+
model: DEFAULT_MODEL,
|
|
182
|
+
cachePath: path.join(dataDir, "embeddings.db"),
|
|
183
|
+
elapsedMs: Date.now() - started,
|
|
184
|
+
error: error?.message || String(error)
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
155
189
|
async function enhanceRuleScores(rules, task, { dataDir, sources, allowRemote }) {
|
|
156
190
|
const cache = await openEmbeddingCache(dataDir);
|
|
157
191
|
const embedder = await getExtractor({ allowRemote, dataDir });
|
|
@@ -3,6 +3,8 @@ import path from "node:path";
|
|
|
3
3
|
import { writeJsonFile } from "./fs-utils.js";
|
|
4
4
|
import { defaultDataRoot, workspaceDataDir } from "./workspace-data.js";
|
|
5
5
|
|
|
6
|
+
let pendingStdoutWrites = 0;
|
|
7
|
+
|
|
6
8
|
export async function readStdinJson() {
|
|
7
9
|
const chunks = [];
|
|
8
10
|
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
@@ -13,13 +15,21 @@ export async function readStdinJson() {
|
|
|
13
15
|
|
|
14
16
|
export function writeJson(value) {
|
|
15
17
|
try {
|
|
16
|
-
|
|
18
|
+
pendingStdoutWrites += 1;
|
|
19
|
+
process.stdout.write(`${JSON.stringify(value)}\n`, () => {
|
|
20
|
+
pendingStdoutWrites = Math.max(0, pendingStdoutWrites - 1);
|
|
21
|
+
});
|
|
17
22
|
} catch (error) {
|
|
23
|
+
pendingStdoutWrites = Math.max(0, pendingStdoutWrites - 1);
|
|
18
24
|
if (error?.code !== "EPIPE") throw error;
|
|
19
25
|
}
|
|
20
26
|
}
|
|
21
27
|
|
|
22
28
|
export function exitAfterStdout(code = 0) {
|
|
29
|
+
if (pendingStdoutWrites > 0) {
|
|
30
|
+
setImmediate(() => exitAfterStdout(code));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
23
33
|
if (process.stdout.writableNeedDrain) {
|
|
24
34
|
process.stdout.once("drain", () => process.exit(code));
|
|
25
35
|
return;
|
|
@@ -28,7 +28,7 @@ export function copyPath(src, dest) {
|
|
|
28
28
|
|
|
29
29
|
export function copyPackageRoot({ rootDir, targetRoot }) {
|
|
30
30
|
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
31
|
-
for (const entry of [".agents", "bin", "plugins", "package.json", "package-lock.json", "README.md", "LICENSE", "node_modules"]) {
|
|
31
|
+
for (const entry of [".agents", "bin", "plugins", "eval", "docs", "package.json", "package-lock.json", "README.md", "CHANGELOG.md", "DEMO.md", "LAUNCH.md", "LICENSE", "node_modules"]) {
|
|
32
32
|
const src = path.join(rootDir, entry);
|
|
33
33
|
if (fs.existsSync(src)) copyPath(src, path.join(targetRoot, entry));
|
|
34
34
|
}
|
|
@@ -6,6 +6,7 @@ const PROFILE_CACHE_FILE = "project-profile.json";
|
|
|
6
6
|
const MAX_DEPENDENCIES = 80;
|
|
7
7
|
const MAX_SCRIPTS = 30;
|
|
8
8
|
const MAX_RECENT_FILES = 20;
|
|
9
|
+
const MAX_FUSED_PROFILE_CHARS = 500;
|
|
9
10
|
|
|
10
11
|
export function projectProfile({ cwd = process.cwd(), dataDir } = {}) {
|
|
11
12
|
const fingerprint = projectFingerprint(cwd);
|
|
@@ -30,7 +31,10 @@ export function projectProfile({ cwd = process.cwd(), dataDir } = {}) {
|
|
|
30
31
|
|
|
31
32
|
export function fusedProjectQuery({ prompt = "", cwd = process.cwd(), dataDir } = {}) {
|
|
32
33
|
const profile = projectProfile({ cwd, dataDir });
|
|
33
|
-
|
|
34
|
+
const profileSignal = profile.embeddableString
|
|
35
|
+
? profile.embeddableString.slice(0, MAX_FUSED_PROFILE_CHARS)
|
|
36
|
+
: "";
|
|
37
|
+
return [String(prompt || "").trim(), profileSignal].filter(Boolean).join("\n");
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
function readCachedProfile(cachePath, fingerprint) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { scheduleContext } from "./scheduler.js";
|
|
2
2
|
import { appendJsonLine, writeJsonFile } from "./fs-utils.js";
|
|
3
3
|
import { maybeAutoWarmWorkspace } from "./auto-warm.js";
|
|
4
|
-
import { callCtxScoreContext } from "./ctx-mcp-client.js";
|
|
4
|
+
import { callCtxHealth, callCtxScoreContext } from "./ctx-mcp-client.js";
|
|
5
5
|
import { resolveHookCwd } from "./hook-io.js";
|
|
6
6
|
import { loadOutputConfig, outputConfigLimits } from "./output-config.js";
|
|
7
7
|
import { scoreContext as scoreContextDirect } from "./score-context.js";
|
|
@@ -17,11 +17,13 @@ export async function handlePromptPayload(
|
|
|
17
17
|
started = Date.now(),
|
|
18
18
|
injectContext = process.env.CONTEXTOS_INJECT !== "0",
|
|
19
19
|
scoreContextClient = callCtxScoreContext,
|
|
20
|
+
healthContextClient = callCtxHealth,
|
|
20
21
|
scoreContextDirectClient = scoreContextDirect,
|
|
21
22
|
autoWarmWorkspace = maybeAutoWarmWorkspace,
|
|
22
23
|
mcpDataDir,
|
|
23
24
|
outputConfig,
|
|
24
|
-
directFallbackTimeoutMs = Number(process.env.CONTEXTOS_DIRECT_FALLBACK_TIMEOUT_MS ||
|
|
25
|
+
directFallbackTimeoutMs = Number(process.env.CONTEXTOS_DIRECT_FALLBACK_TIMEOUT_MS || 2500),
|
|
26
|
+
requireHotMcp = scoreContextClient === callCtxScoreContext
|
|
25
27
|
} = {}
|
|
26
28
|
) {
|
|
27
29
|
const prompt = payload.prompt || payload.message || payload.user_prompt || "";
|
|
@@ -34,6 +36,15 @@ export async function handlePromptPayload(
|
|
|
34
36
|
|
|
35
37
|
let scored;
|
|
36
38
|
try {
|
|
39
|
+
if (requireHotMcp) {
|
|
40
|
+
const health = await healthContextClient({
|
|
41
|
+
dataDir: mcpDataDir || dataDir,
|
|
42
|
+
timeoutMs: Number(process.env.CONTEXTOS_MCP_HEALTH_TIMEOUT_MS || 250)
|
|
43
|
+
});
|
|
44
|
+
if (!health.embedding_pipeline_loaded) {
|
|
45
|
+
throw new Error(`ctx-mcp scorer not hot: ${health.preload_status || "unknown"}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
37
48
|
scored = await scoreContextClient({
|
|
38
49
|
cwd,
|
|
39
50
|
prompt,
|
|
@@ -55,6 +66,7 @@ export async function handlePromptPayload(
|
|
|
55
66
|
maxSkills: promptLimits.skills,
|
|
56
67
|
maxWorkflows: promptLimits.workflows,
|
|
57
68
|
dataDir: mcpDataDir || dataDir,
|
|
69
|
+
allowEmbeddings: false,
|
|
58
70
|
embeddingTimeoutMs: Number(process.env.CONTEXTOS_HOOK_EMBEDDING_TIMEOUT_MS || 500),
|
|
59
71
|
fileEmbeddingTimeoutMs: Number(process.env.CONTEXTOS_HOOK_FILE_EMBEDDING_TIMEOUT_MS || 1000),
|
|
60
72
|
skillEmbeddingTimeoutMs: Number(process.env.CONTEXTOS_HOOK_SKILL_EMBEDDING_TIMEOUT_MS || 2000)
|
|
@@ -197,6 +209,9 @@ function emptyContextReason({ scheduled, outputConfig, injectContext }) {
|
|
|
197
209
|
if (scheduled.suggestedSkills?.length) available.push("skills");
|
|
198
210
|
if (scheduled.suggestedWorkflows?.length) available.push("workflows");
|
|
199
211
|
if (!available.length) return "no-context-candidates";
|
|
212
|
+
const enabledMissing = ["rules", "files", "skills", "workflows"]
|
|
213
|
+
.filter((section) => sections[section] !== false && !available.includes(section));
|
|
214
|
+
if (enabledMissing.length) return `enabled-sections-missing-candidates:${enabledMissing.join(",")}`;
|
|
200
215
|
const enabled = available.filter((section) => sections[section] !== false);
|
|
201
216
|
return enabled.length ? "enabled-sections-empty-after-formatting" : `available-sections-disabled:${available.join(",")}`;
|
|
202
217
|
}
|
|
@@ -19,7 +19,8 @@ export async function scoreContext({
|
|
|
19
19
|
embeddingTimeoutMs = 5000,
|
|
20
20
|
fileEmbeddingTimeoutMs = Number(process.env.CONTEXTOS_FILE_EMBEDDING_TIMEOUT_MS || 1000),
|
|
21
21
|
skillEmbeddingTimeoutMs = Number(process.env.CONTEXTOS_SKILL_EMBEDDING_TIMEOUT_MS || embeddingTimeoutMs),
|
|
22
|
-
skillSearchOptions = {}
|
|
22
|
+
skillSearchOptions = {},
|
|
23
|
+
allowEmbeddings = true
|
|
23
24
|
} = {}) {
|
|
24
25
|
const started = Date.now();
|
|
25
26
|
const ruleInputsPromise = Promise.resolve().then(() => {
|
|
@@ -35,6 +36,14 @@ export async function scoreContext({
|
|
|
35
36
|
});
|
|
36
37
|
|
|
37
38
|
const rulesPromise = ruleInputsPromise.then(({ merged, baseScoredRules }) => {
|
|
39
|
+
if (!allowEmbeddings) {
|
|
40
|
+
return {
|
|
41
|
+
rules: baseScoredRules,
|
|
42
|
+
status: "disabled",
|
|
43
|
+
model: null,
|
|
44
|
+
cachePath: dataDir
|
|
45
|
+
};
|
|
46
|
+
}
|
|
38
47
|
return enhanceRuleScoresWithEmbeddings(baseScoredRules, prompt, {
|
|
39
48
|
dataDir,
|
|
40
49
|
sources: merged.sources,
|
|
@@ -52,6 +61,7 @@ export async function scoreContext({
|
|
|
52
61
|
limit: maxFiles,
|
|
53
62
|
fileEmbeddingTimeoutMs,
|
|
54
63
|
fileEmbeddingOptions: {
|
|
64
|
+
enabled: allowEmbeddings,
|
|
55
65
|
allowRemote: false
|
|
56
66
|
}
|
|
57
67
|
});
|
|
@@ -68,6 +78,7 @@ export async function scoreContext({
|
|
|
68
78
|
dataDir,
|
|
69
79
|
limit: maxSkills,
|
|
70
80
|
timeoutMs: skillEmbeddingTimeoutMs,
|
|
81
|
+
embeddingsEnabled: allowEmbeddings,
|
|
71
82
|
...skillSearchOptions
|
|
72
83
|
})
|
|
73
84
|
};
|
|
@@ -77,7 +88,7 @@ export async function scoreContext({
|
|
|
77
88
|
const catalog = Array.isArray(workflows) ? workflows : scanWorkflows({ cwd });
|
|
78
89
|
return {
|
|
79
90
|
catalog,
|
|
80
|
-
suggestions: await suggestWorkflows({ prompt, workflows: catalog, dataDir, limit: maxWorkflows })
|
|
91
|
+
suggestions: await suggestWorkflows({ prompt, workflows: catalog, dataDir, limit: maxWorkflows, embeddingsEnabled: allowEmbeddings })
|
|
81
92
|
};
|
|
82
93
|
});
|
|
83
94
|
|
|
@@ -1,17 +1,22 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Interactive setup starts empty so users choose intentionally. Non-interactive
|
|
2
|
+
// --yes needs a deterministic target, so it defaults to Codex.
|
|
2
3
|
const DEFAULT_AGENTS = [];
|
|
4
|
+
const DEFAULT_YES_AGENTS = ["codex"];
|
|
3
5
|
|
|
4
6
|
export function parseSetupArgs(args = []) {
|
|
5
7
|
const agentsFlag = args.indexOf("--agents");
|
|
6
8
|
const agentsProvided = agentsFlag >= 0;
|
|
9
|
+
const yes = args.includes("--yes") || args.includes("-y");
|
|
7
10
|
const agents = agentsFlag >= 0
|
|
8
11
|
? parseAgentList(args[agentsFlag + 1])
|
|
9
|
-
:
|
|
12
|
+
: yes
|
|
13
|
+
? DEFAULT_YES_AGENTS
|
|
14
|
+
: DEFAULT_AGENTS;
|
|
10
15
|
|
|
11
16
|
return {
|
|
12
17
|
agents,
|
|
13
18
|
agentsProvided,
|
|
14
|
-
yes
|
|
19
|
+
yes,
|
|
15
20
|
quiet: args.includes("--quiet"),
|
|
16
21
|
syncRules: !args.includes("--no-rules"),
|
|
17
22
|
syncSkills: !args.includes("--no-skills")
|