@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.
@@ -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
- return skills
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) >= 0.35 || Number(skill.embeddingScore || 0) >= 0.62)
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 = normalize(prompt);
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 = enriched.searchTokens.filter((token) => promptTokens.has(token) && token.length > 2);
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 score = Math.min(1, (matches.length ? 0.25 + matches.length * 0.08 : 0) + (nameHit ? 0.2 : 0) + (nameTokenHit ? 0.18 : 0) + scopeBonus);
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
- await spawnShellStreaming("sh", ["-c", `curl -fsSL ${INSTALL_SH_URL} | sh`]);
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, formatReport } from "./reporter.js";
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: [],