@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
|
@@ -244,6 +244,106 @@ export function repairSkillSymlinks({
|
|
|
244
244
|
return { repaired: [...new Set(repaired)], removedBroken: [...new Set(removedBroken)] };
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
+
export function dedupeAgentVisibleSkills({
|
|
248
|
+
cwd = process.cwd(),
|
|
249
|
+
home = os.homedir(),
|
|
250
|
+
agents = DEFAULT_AGENTS,
|
|
251
|
+
dryRun = false
|
|
252
|
+
} = {}) {
|
|
253
|
+
const roots = visibleSkillRootsForAgents({ cwd, home, agents });
|
|
254
|
+
const seen = new Map();
|
|
255
|
+
const kept = [];
|
|
256
|
+
const removed = [];
|
|
257
|
+
|
|
258
|
+
for (const root of roots) {
|
|
259
|
+
for (const skill of listSkillsInRoot(root)) {
|
|
260
|
+
const previous = seen.get(skill.key);
|
|
261
|
+
if (!previous) {
|
|
262
|
+
seen.set(skill.key, skill);
|
|
263
|
+
kept.push(skill.path);
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const previousReal = safeRealpath(previous.path);
|
|
268
|
+
const currentReal = safeRealpath(skill.path);
|
|
269
|
+
if (previousReal && currentReal && previousReal === currentReal) {
|
|
270
|
+
if (!dryRun) fs.rmSync(skill.path, { force: true, recursive: true });
|
|
271
|
+
removed.push(skill.path);
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (!dryRun) fs.rmSync(skill.path, { force: true, recursive: true });
|
|
276
|
+
removed.push(skill.path);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return { kept: [...new Set(kept)], removed: [...new Set(removed)], roots };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function visibleSkillRootsForAgents({ cwd, home, agents }) {
|
|
284
|
+
const normalizedAgents = normalizeAgentList(agents);
|
|
285
|
+
const roots = [];
|
|
286
|
+
const addShared = () => {
|
|
287
|
+
roots.push(path.join(home, ".agents", "skills"));
|
|
288
|
+
roots.push(path.join(cwd, ".agents", "skills"));
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
addShared();
|
|
292
|
+
if (normalizedAgents.includes("codex")) {
|
|
293
|
+
roots.push(path.join(home, ".codex", "skills"));
|
|
294
|
+
roots.push(path.join(cwd, ".codex", "skills"));
|
|
295
|
+
}
|
|
296
|
+
if (normalizedAgents.includes("claude")) {
|
|
297
|
+
roots.push(path.join(home, ".claude", "skills"));
|
|
298
|
+
roots.push(path.join(cwd, ".claude", "skills"));
|
|
299
|
+
}
|
|
300
|
+
if (normalizedAgents.includes("antigravity")) {
|
|
301
|
+
roots.push(path.join(home, ".gemini", "skills"));
|
|
302
|
+
roots.push(path.join(home, ".gemini", "antigravity", "skills"));
|
|
303
|
+
roots.push(path.join(home, ".gemini", "antigravity-cli", "skills"));
|
|
304
|
+
roots.push(path.join(cwd, ".gemini", "skills"));
|
|
305
|
+
roots.push(path.join(cwd, ".gemini", "antigravity", "skills"));
|
|
306
|
+
roots.push(path.join(cwd, ".gemini", "antigravity-cli", "skills"));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return uniquePaths(roots);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function listSkillsInRoot(root) {
|
|
313
|
+
return findSkillFiles(root)
|
|
314
|
+
.map((skillFile) => {
|
|
315
|
+
const skillDir = path.dirname(skillFile);
|
|
316
|
+
const name = readSkillName(skillFile) || path.basename(skillDir);
|
|
317
|
+
return {
|
|
318
|
+
name,
|
|
319
|
+
key: normalizeSkillName(name),
|
|
320
|
+
path: skillDir,
|
|
321
|
+
root
|
|
322
|
+
};
|
|
323
|
+
})
|
|
324
|
+
.filter((skill) => skill.key)
|
|
325
|
+
.sort((a, b) => a.path.localeCompare(b.path));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function readSkillName(skillFile) {
|
|
329
|
+
try {
|
|
330
|
+
const content = fs.readFileSync(skillFile, "utf8");
|
|
331
|
+
return content.match(/^\s*name:\s*(.+?)\s*$/m)?.[1]
|
|
332
|
+
?.replace(/^["']|["']$/g, "")
|
|
333
|
+
.trim() || "";
|
|
334
|
+
} catch {
|
|
335
|
+
return "";
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function normalizeSkillName(name) {
|
|
340
|
+
return String(name || "")
|
|
341
|
+
.trim()
|
|
342
|
+
.toLowerCase()
|
|
343
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
344
|
+
.replace(/^-+|-+$/g, "");
|
|
345
|
+
}
|
|
346
|
+
|
|
247
347
|
function skillRoots({ cwd, home }) {
|
|
248
348
|
return uniquePaths([
|
|
249
349
|
path.join(home, ".claude", "skills"),
|
|
@@ -503,6 +603,18 @@ export async function syncSkills({
|
|
|
503
603
|
const syncedCount = countSkillFiles(skillshareSourceDir({ home }));
|
|
504
604
|
logger(statusLine("Running skillshare sync...", options.dryRun ? "dry-run" : `✓ ${syncedCount} skills → ${options.agents.join(", ")}`));
|
|
505
605
|
|
|
606
|
+
const deduped = dedupeAgentVisibleSkills({
|
|
607
|
+
cwd,
|
|
608
|
+
home,
|
|
609
|
+
agents: options.agents,
|
|
610
|
+
dryRun: options.dryRun
|
|
611
|
+
});
|
|
612
|
+
if (deduped.removed.length) {
|
|
613
|
+
logger(statusLine("Deduping agent-visible skills...", options.dryRun
|
|
614
|
+
? `dry-run (${deduped.removed.length} duplicates)`
|
|
615
|
+
: `✓ ${deduped.removed.length} duplicates removed`));
|
|
616
|
+
}
|
|
617
|
+
|
|
506
618
|
let embeddings = { count: 0, cachePath: null, skipped: options.dryRun || options.noEmbeddings };
|
|
507
619
|
if (options.noEmbeddings) {
|
|
508
620
|
logger(statusLine("Rebuilding skill embeddings...", "skipped by --no-embeddings"));
|
|
@@ -128,12 +128,14 @@ export async function suggestWorkflows({
|
|
|
128
128
|
workflows = [],
|
|
129
129
|
dataDir,
|
|
130
130
|
limit = DEFAULT_LIMIT,
|
|
131
|
-
timeoutMs = Number(process.env.CONTEXTOS_WORKFLOW_EMBEDDING_TIMEOUT_MS || process.env.CONTEXTOS_EMBEDDING_TIMEOUT_MS || 800)
|
|
131
|
+
timeoutMs = Number(process.env.CONTEXTOS_WORKFLOW_EMBEDDING_TIMEOUT_MS || process.env.CONTEXTOS_EMBEDDING_TIMEOUT_MS || 800),
|
|
132
|
+
embeddingsEnabled = true
|
|
132
133
|
} = {}) {
|
|
133
134
|
if (!String(prompt || "").trim() || !workflows.length) return [];
|
|
134
135
|
const base = scoreWorkflowsByKeyword({ prompt, workflows });
|
|
135
136
|
const embeddingCandidates = selectWorkflowEmbeddingCandidates(base);
|
|
136
137
|
if (!embeddingCandidates.length) return [];
|
|
138
|
+
if (!embeddingsEnabled) return finalizeWorkflowScores(embeddingCandidates, limit);
|
|
137
139
|
|
|
138
140
|
const embedding = await enhanceRuleScoresWithEmbeddings(embeddingCandidates, prompt, {
|
|
139
141
|
dataDir,
|
|
@@ -4,12 +4,32 @@ import { z } from "zod";
|
|
|
4
4
|
import { scoreContext } from "../lib/score-context.js";
|
|
5
5
|
import { scheduleContext } from "../lib/scheduler.js";
|
|
6
6
|
|
|
7
|
-
export function createContextOSMcpServer({ dataDir }) {
|
|
7
|
+
export function createContextOSMcpServer({ dataDir, getHealth = defaultHealth }) {
|
|
8
8
|
const server = new McpServer({
|
|
9
9
|
name: "ctx-mcp",
|
|
10
10
|
version: "0.1.0"
|
|
11
11
|
});
|
|
12
12
|
|
|
13
|
+
server.registerTool("ctx_health", {
|
|
14
|
+
title: "ContextOS health",
|
|
15
|
+
description: "Reports ContextOS MCP bridge and embedding model readiness.",
|
|
16
|
+
inputSchema: {},
|
|
17
|
+
outputSchema: {
|
|
18
|
+
model_cache_ready: z.boolean(),
|
|
19
|
+
embedding_pipeline_loaded: z.boolean(),
|
|
20
|
+
bridge_ready: z.boolean(),
|
|
21
|
+
preload_status: z.string().optional(),
|
|
22
|
+
loaded_at: z.number().optional(),
|
|
23
|
+
error: z.string().optional()
|
|
24
|
+
}
|
|
25
|
+
}, async () => {
|
|
26
|
+
const health = getHealth();
|
|
27
|
+
return {
|
|
28
|
+
content: [{ type: "text", text: JSON.stringify(health) }],
|
|
29
|
+
structuredContent: health
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
|
|
13
33
|
server.registerTool("ctx_score_context", {
|
|
14
34
|
title: "Score ContextOS prompt context",
|
|
15
35
|
description: "Scores AGENTS.md rules and suggests files/skills for an agent prompt.",
|
|
@@ -90,3 +110,11 @@ export function createContextOSMcpServer({ dataDir }) {
|
|
|
90
110
|
return server;
|
|
91
111
|
}
|
|
92
112
|
|
|
113
|
+
function defaultHealth() {
|
|
114
|
+
return {
|
|
115
|
+
model_cache_ready: false,
|
|
116
|
+
embedding_pipeline_loaded: false,
|
|
117
|
+
bridge_ready: false,
|
|
118
|
+
preload_status: "unknown"
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -4,7 +4,7 @@ import net from "node:net";
|
|
|
4
4
|
|
|
5
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
6
|
|
|
7
|
-
import { isModelCacheReady, modelCacheDir } from "../lib/embedding-scorer.js";
|
|
7
|
+
import { isModelCacheReady, modelCacheDir, preloadEmbeddingPipeline } from "../lib/embedding-scorer.js";
|
|
8
8
|
import { scoreContext } from "../lib/score-context.js";
|
|
9
9
|
import { CTX_MCP_BRIDGE_REVISION, ctxMcpSocketPath } from "../lib/ctx-mcp-client.js";
|
|
10
10
|
import { defaultDataRoot } from "../lib/workspace-data.js";
|
|
@@ -12,23 +12,51 @@ import { createContextOSMcpServer } from "./contextos-server.js";
|
|
|
12
12
|
|
|
13
13
|
const dataDir = defaultDataRoot();
|
|
14
14
|
const socketPath = ctxMcpSocketPath(dataDir);
|
|
15
|
+
const modelState = {
|
|
16
|
+
modelCacheReady: false,
|
|
17
|
+
embeddingPipelineLoaded: false,
|
|
18
|
+
bridgeReady: false,
|
|
19
|
+
preloadStatus: "starting",
|
|
20
|
+
loadedAt: null,
|
|
21
|
+
error: null
|
|
22
|
+
};
|
|
15
23
|
|
|
16
24
|
fs.mkdirSync(dataDir, { recursive: true });
|
|
17
25
|
await ensureModelReady();
|
|
18
26
|
if (process.env.CONTEXTOS_DISABLE_BRIDGE !== "1") startBridge();
|
|
27
|
+
preloadEmbeddingModel();
|
|
19
28
|
const keepAlive = setInterval(() => {}, 2 ** 31 - 1);
|
|
20
29
|
|
|
21
|
-
const server = createContextOSMcpServer({ dataDir });
|
|
30
|
+
const server = createContextOSMcpServer({ dataDir, getHealth: bridgeHealth });
|
|
22
31
|
console.error("ctx-mcp ready");
|
|
23
32
|
await server.connect(new StdioServerTransport());
|
|
24
33
|
|
|
25
34
|
async function ensureModelReady() {
|
|
26
35
|
const modelDir = modelCacheDir(dataDir);
|
|
27
|
-
|
|
36
|
+
modelState.modelCacheReady = fs.existsSync(modelDir) && isModelCacheReady(dataDir);
|
|
37
|
+
if (!modelState.modelCacheReady) {
|
|
28
38
|
throw new Error(`ContextOS model cache missing: ${modelDir}. Run ctx install first.`);
|
|
29
39
|
}
|
|
30
40
|
}
|
|
31
41
|
|
|
42
|
+
async function preloadEmbeddingModel() {
|
|
43
|
+
modelState.preloadStatus = "loading";
|
|
44
|
+
const result = await preloadEmbeddingPipeline({
|
|
45
|
+
dataDir,
|
|
46
|
+
allowRemote: false,
|
|
47
|
+
warmText: "contextos warmup"
|
|
48
|
+
});
|
|
49
|
+
modelState.embeddingPipelineLoaded = Boolean(result.loaded);
|
|
50
|
+
modelState.preloadStatus = result.status;
|
|
51
|
+
modelState.loadedAt = result.loaded ? Date.now() : null;
|
|
52
|
+
modelState.error = result.error || null;
|
|
53
|
+
if (result.loaded) {
|
|
54
|
+
console.error(`ctx-mcp embedding model hot (${result.elapsedMs}ms)`);
|
|
55
|
+
} else {
|
|
56
|
+
console.error(`ctx-mcp embedding preload failed: ${result.error || result.status}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
32
60
|
function startBridge() {
|
|
33
61
|
fs.rmSync(socketPath, { force: true });
|
|
34
62
|
const bridge = net.createServer((socket) => {
|
|
@@ -42,9 +70,12 @@ function startBridge() {
|
|
|
42
70
|
});
|
|
43
71
|
});
|
|
44
72
|
bridge.on("error", (error) => {
|
|
73
|
+
modelState.bridgeReady = false;
|
|
45
74
|
console.error(`ctx-mcp bridge disabled: ${error?.message || String(error)}`);
|
|
46
75
|
});
|
|
47
|
-
bridge.listen(socketPath)
|
|
76
|
+
bridge.listen(socketPath, () => {
|
|
77
|
+
modelState.bridgeReady = true;
|
|
78
|
+
});
|
|
48
79
|
process.on("exit", () => {
|
|
49
80
|
clearInterval(keepAlive);
|
|
50
81
|
fs.rmSync(socketPath, { force: true });
|
|
@@ -60,6 +91,10 @@ async function handleBridgeRequest(socket, raw) {
|
|
|
60
91
|
socket.pause();
|
|
61
92
|
try {
|
|
62
93
|
const payload = JSON.parse(raw.trim() || "{}");
|
|
94
|
+
if (payload.type === "health") {
|
|
95
|
+
socket.end(JSON.stringify({ bridgeRevision: CTX_MCP_BRIDGE_REVISION, health: bridgeHealth() }));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
63
98
|
const result = await scoreContext({
|
|
64
99
|
cwd: payload.cwd || process.cwd(),
|
|
65
100
|
prompt: payload.prompt || "",
|
|
@@ -84,3 +119,14 @@ async function handleBridgeRequest(socket, raw) {
|
|
|
84
119
|
}));
|
|
85
120
|
}
|
|
86
121
|
}
|
|
122
|
+
|
|
123
|
+
function bridgeHealth() {
|
|
124
|
+
return {
|
|
125
|
+
model_cache_ready: Boolean(modelState.modelCacheReady),
|
|
126
|
+
embedding_pipeline_loaded: Boolean(modelState.embeddingPipelineLoaded),
|
|
127
|
+
bridge_ready: Boolean(modelState.bridgeReady),
|
|
128
|
+
preload_status: modelState.preloadStatus,
|
|
129
|
+
loaded_at: modelState.loadedAt || undefined,
|
|
130
|
+
error: modelState.error || undefined
|
|
131
|
+
};
|
|
132
|
+
}
|