@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.
Files changed (58) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +114 -9
  3. package/bin/ctx.js +64 -8
  4. package/eval/skill-routing/cases.yaml +366 -0
  5. package/eval/skill-routing/fixtures/docker-node/Dockerfile +4 -0
  6. package/eval/skill-routing/fixtures/docker-node/docker-compose.yml +5 -0
  7. package/eval/skill-routing/fixtures/docker-node/package.json +6 -0
  8. package/eval/skill-routing/fixtures/expo-eas/.github/workflows/eas.yml +1 -0
  9. package/eval/skill-routing/fixtures/expo-eas/app.json +5 -0
  10. package/eval/skill-routing/fixtures/expo-eas/eas.json +6 -0
  11. package/eval/skill-routing/fixtures/expo-eas/package.json +11 -0
  12. package/eval/skill-routing/fixtures/expo-with-vercel-json/app.json +6 -0
  13. package/eval/skill-routing/fixtures/expo-with-vercel-json/eas.json +5 -0
  14. package/eval/skill-routing/fixtures/expo-with-vercel-json/package.json +8 -0
  15. package/eval/skill-routing/fixtures/expo-with-vercel-json/vercel.json +3 -0
  16. package/eval/skill-routing/fixtures/express-mongo-jwt/package.json +8 -0
  17. package/eval/skill-routing/fixtures/firebase-hosting/firebase.json +11 -0
  18. package/eval/skill-routing/fixtures/firebase-hosting/package.json +6 -0
  19. package/eval/skill-routing/fixtures/flutter-firebase/pubspec.yaml +5 -0
  20. package/eval/skill-routing/fixtures/frontend-only-next/package.json +8 -0
  21. package/eval/skill-routing/fixtures/integration-test/jest.config.js +3 -0
  22. package/eval/skill-routing/fixtures/integration-test/package.json +10 -0
  23. package/eval/skill-routing/fixtures/jest-project/jest.config.js +3 -0
  24. package/eval/skill-routing/fixtures/jest-project/package.json +7 -0
  25. package/eval/skill-routing/fixtures/nest-prisma/package.json +10 -0
  26. package/eval/skill-routing/fixtures/nest-prisma/prisma/schema.prisma +4 -0
  27. package/eval/skill-routing/fixtures/next-vercel/.github/workflows/deploy.yml +1 -0
  28. package/eval/skill-routing/fixtures/next-vercel/package.json +8 -0
  29. package/eval/skill-routing/fixtures/next-vercel/vercel.json +3 -0
  30. package/eval/skill-routing/fixtures/oauth-google/.env.example +3 -0
  31. package/eval/skill-routing/fixtures/oauth-google/package.json +9 -0
  32. package/eval/skill-routing/fixtures/password-reset/package.json +8 -0
  33. package/eval/skill-routing/fixtures/playwright-project/package.json +6 -0
  34. package/eval/skill-routing/fixtures/playwright-project/playwright.config.ts +5 -0
  35. package/eval/skill-routing/fixtures/railway-render/package.json +6 -0
  36. package/eval/skill-routing/fixtures/railway-render/railway.json +6 -0
  37. package/eval/skill-routing/fixtures/railway-render/render.yaml +5 -0
  38. package/eval/skill-routing/fixtures/rbac-api/package.json +8 -0
  39. package/eval/skill-routing/fixtures/redis-cache/package.json +7 -0
  40. package/eval/skill-routing/fixtures/static-docs/README.md +3 -0
  41. package/eval/skill-routing/run-eval.js +278 -0
  42. package/package.json +3 -1
  43. package/plugins/ctx/.codex-plugin/plugin.json +1 -1
  44. package/plugins/ctx/lib/analyzer.js +17 -2
  45. package/plugins/ctx/lib/auto-warm.js +1 -0
  46. package/plugins/ctx/lib/ctx-mcp-client.js +21 -0
  47. package/plugins/ctx/lib/embedding-scorer.js +34 -0
  48. package/plugins/ctx/lib/hook-io.js +11 -1
  49. package/plugins/ctx/lib/package-install.js +1 -1
  50. package/plugins/ctx/lib/project-profiler.js +5 -1
  51. package/plugins/ctx/lib/prompt-hook.js +17 -2
  52. package/plugins/ctx/lib/score-context.js +13 -2
  53. package/plugins/ctx/lib/setup-wizard.js +8 -3
  54. package/plugins/ctx/lib/skill-discoverer.js +480 -27
  55. package/plugins/ctx/lib/skillshare-sync.js +112 -0
  56. package/plugins/ctx/lib/workflow-discoverer.js +3 -1
  57. package/plugins/ctx/mcp/contextos-server.js +29 -1
  58. 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
- if (!fs.existsSync(modelDir) || !isModelCacheReady(dataDir)) {
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
+ }