@minhpnq1807/contextos 0.5.42 → 0.5.45

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.
@@ -0,0 +1,88 @@
1
+ import fs from "node:fs";
2
+ import { spawn } from "node:child_process";
3
+
4
+ export function shellInvocation(command, { platform = process.platform, env = process.env } = {}) {
5
+ if (platform === "win32") {
6
+ return {
7
+ command: env.ComSpec || env.COMSPEC || "cmd.exe",
8
+ args: ["/d", "/s", "/c", command]
9
+ };
10
+ }
11
+ return {
12
+ command: fs.existsSync("/bin/sh") ? "/bin/sh" : "sh",
13
+ args: ["-c", command]
14
+ };
15
+ }
16
+
17
+ export function runPrefixedCommand(commandText, {
18
+ spawnFn = spawn,
19
+ stdout = process.stdout,
20
+ stderr = process.stderr,
21
+ stdin = "inherit",
22
+ platform = process.platform,
23
+ env = process.env,
24
+ prefix = "\x1B[2m│\x1B[0m "
25
+ } = {}) {
26
+ const shell = shellInvocation(commandText, { platform, env });
27
+ return new Promise((resolve, reject) => {
28
+ let settled = false;
29
+ const fail = (error) => {
30
+ if (settled) return;
31
+ settled = true;
32
+ reject(error);
33
+ };
34
+
35
+ let child;
36
+ try {
37
+ child = spawnFn(shell.command, shell.args, {
38
+ stdio: [stdin, "pipe", "pipe"],
39
+ windowsHide: true
40
+ });
41
+ } catch (error) {
42
+ fail(error);
43
+ return;
44
+ }
45
+
46
+ pipePrefixed(child.stdout, stdout, prefix);
47
+ pipePrefixed(child.stderr, stderr, prefix);
48
+
49
+ child.on("error", (error) => {
50
+ if (error?.code === "ENOENT") {
51
+ fail(new Error([
52
+ `Unable to start shell '${shell.command}' for installer command.`,
53
+ `Original command: ${commandText}`,
54
+ platform === "win32"
55
+ ? "Fix: ensure cmd.exe is available through ComSpec/COMSPEC, or run ContextOS from a normal Command Prompt, PowerShell, or Windows Terminal session."
56
+ : "Fix: ensure /bin/sh exists, or install a POSIX shell before running ContextOS installers."
57
+ ].join("\n")));
58
+ return;
59
+ }
60
+ fail(error);
61
+ });
62
+
63
+ child.on("close", (code) => {
64
+ if (settled) return;
65
+ settled = true;
66
+ if (code === 0) resolve();
67
+ else reject(new Error(`Installer command exited with code ${code}: ${commandText}`));
68
+ });
69
+ });
70
+ }
71
+
72
+ function pipePrefixed(stream, target, prefix) {
73
+ if (!stream) return;
74
+ let needPrefix = true;
75
+ stream.on("data", (buf) => {
76
+ const str = buf.toString();
77
+ let out = "";
78
+ for (const ch of str) {
79
+ if (needPrefix) {
80
+ out += prefix;
81
+ needPrefix = false;
82
+ }
83
+ out += ch;
84
+ if (ch === "\n") needPrefix = true;
85
+ }
86
+ target.write(out);
87
+ });
88
+ }
@@ -10,6 +10,20 @@ 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", "can", "not", "production", "show", "something", "https", "http", "com", "www",
18
+ "a", "an", "and", "are", "as", "at", "be", "before", "after", "both", "by", "from", "for",
19
+ "if", "in", "into", "is", "must", "of", "on", "or", "the", "then", "this", "to", "user",
20
+ "users", "when", "where", "whether", "with"
21
+ ]);
22
+ const SPECIALIZED_SKILL_TOKENS = new Set([
23
+ "android", "authorization", "cicd", "eas", "expo", "frontend", "ios", "next", "nextjs",
24
+ "mcp", "modelcontextprotocol", "postgres", "postgresql", "react", "react-native", "tailwind",
25
+ "typescript", "ui"
26
+ ]);
13
27
 
14
28
  const scanCache = new Map();
15
29
 
@@ -20,9 +34,9 @@ export function skillSearchRoots({ cwd = process.cwd(), home = os.homedir() } =
20
34
  path.join(cwd, ".gemini", "skills"),
21
35
  path.join(cwd, ".gemini", "antigravity", "skills"),
22
36
  path.join(cwd, ".gemini", "antigravity-cli", "skills"),
23
- path.join(home, ".config", "skillshare", "skills"),
24
37
  path.join(home, ".codex", "skills"),
25
38
  path.join(home, ".claude", "skills"),
39
+ path.join(home, ".config", "skillshare", "skills"),
26
40
  path.join(home, ".gemini", "skills"),
27
41
  path.join(home, ".gemini", "antigravity", "skills"),
28
42
  path.join(home, ".gemini", "antigravity-cli", "skills")
@@ -154,13 +168,14 @@ export async function suggestSkills({
154
168
  prompt = "",
155
169
  skills = [],
156
170
  dataDir,
171
+ cwd = process.cwd(),
157
172
  limit = DEFAULT_LIMIT,
158
173
  timeoutMs = Number(process.env.CONTEXTOS_SKILL_EMBEDDING_TIMEOUT_MS || process.env.CONTEXTOS_EMBEDDING_TIMEOUT_MS || 800)
159
174
  } = {}) {
160
175
  if (!String(prompt || "").trim() || !skills.length) return [];
161
- const base = scoreSkillsByKeyword({ prompt, skills });
176
+ const base = scoreSkillsByKeyword({ prompt, skills, projectHints: projectSkillHints({ cwd }) });
162
177
  if (skills.length > DEFAULT_SEMANTIC_CATALOG_LIMIT) {
163
- return finalizeSkillScores(base, limit);
178
+ return finalizeSkillScores(base, limit, { minimumKeywordScore: 0.5 });
164
179
  }
165
180
 
166
181
  const embeddingCandidates = selectEmbeddingCandidates(base);
@@ -176,8 +191,9 @@ export async function suggestSkills({
176
191
  return finalizeSkillScores(embedding.rules, limit);
177
192
  }
178
193
 
179
- function finalizeSkillScores(skills, limit) {
180
- return skills
194
+ function finalizeSkillScores(skills, limit, { minimumKeywordScore = 0.35 } = {}) {
195
+ const ranked = skills
196
+ .filter((rule) => rule.domainEligible !== false)
181
197
  .map((rule) => ({
182
198
  name: rule.name,
183
199
  description: rule.description,
@@ -186,13 +202,33 @@ function finalizeSkillScores(skills, limit) {
186
202
  keywordScore: rule.keywordScore,
187
203
  score: Math.min(1, Number(rule.score || 0)),
188
204
  embeddingScore: rule.embeddingScore,
205
+ relevancePriority: Number(rule.relevancePriority || 0),
206
+ rankScore: Math.min(1, Number(rule.score || 0)) + Number(rule.relevancePriority || 0) / 100,
189
207
  reasons: rule.reasons || []
190
208
  }))
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))
209
+ .filter((skill) => Number(skill.keywordScore || 0) >= minimumKeywordScore
210
+ || Number(skill.embeddingScore || 0) >= 0.62
211
+ || Number(skill.relevancePriority || 0) >= 50)
212
+ .sort((a, b) => b.rankScore - a.rankScore
213
+ || b.relevancePriority - a.relevancePriority
214
+ || b.score - a.score
215
+ || scopePriority(b.scope) - scopePriority(a.scope)
216
+ || a.name.localeCompare(b.name));
217
+ const seen = new Set();
218
+ return ranked
219
+ .filter((skill) => {
220
+ const key = normalize(skill.name);
221
+ if (seen.has(key)) return false;
222
+ seen.add(key);
223
+ return true;
224
+ })
193
225
  .slice(0, limit);
194
226
  }
195
227
 
228
+ function scopePriority(scope) {
229
+ return scope === "project" ? 1 : 0;
230
+ }
231
+
196
232
  function selectEmbeddingCandidates(skills) {
197
233
  if (skills.length <= DEFAULT_EMBEDDING_CANDIDATES) return skills;
198
234
  return [...skills]
@@ -217,21 +253,31 @@ export async function warmSkillEmbeddings({
217
253
  });
218
254
  }
219
255
 
220
- function scoreSkillsByKeyword({ prompt, skills }) {
221
- const normalizedPrompt = normalize(prompt);
256
+ function scoreSkillsByKeyword({ prompt, skills, projectHints = [] }) {
257
+ const normalizedPrompt = normalizePrompt(prompt);
222
258
  const promptTokens = new Set(normalizedPrompt.split(/\s+/).filter(Boolean));
259
+ const projectTokens = new Set(projectHints);
223
260
  return skills.map((skill, index) => {
224
261
  const enriched = skill.searchTokens ? skill : enrichSkill(skill);
225
262
  const name = String(enriched.name || "");
226
263
  const description = truncateDescription(enriched.description || "");
227
264
  const content = `${name} ${description}`;
228
- const matches = enriched.searchTokens.filter((token) => promptTokens.has(token) && token.length > 2);
265
+ const matches = filterSkillMatches(
266
+ enriched.searchTokens.filter((token) => promptTokens.has(token) && token.length > 2 && !GENERIC_SKILL_TOKENS.has(token)),
267
+ { normalizedPrompt, enriched }
268
+ );
269
+ const projectMatches = enriched.searchTokens.filter((token) => projectTokens.has(token) && SPECIALIZED_SKILL_TOKENS.has(token));
229
270
  const normalizedName = enriched.normalizedName;
230
271
  const nameTokens = enriched.nameTokens;
231
272
  const nameHit = normalizedPrompt.includes(normalizedName);
232
273
  const nameTokenHit = nameTokens.length > 1 && nameTokens.every((token) => promptTokens.has(token));
233
274
  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);
275
+ const intentBonus = skillIntentBonus(normalizedPrompt, enriched, projectTokens);
276
+ const relevancePriority = skillRelevancePriority(normalizedPrompt, enriched, projectTokens);
277
+ const domainEligible = isSkillDomainEligible(normalizedPrompt, enriched, projectTokens);
278
+ const matchScore = matches.reduce((sum, token) => sum + (SPECIALIZED_SKILL_TOKENS.has(token) ? 0.2 : 0.08), 0);
279
+ const projectBonus = intentBonus ? Math.min(0.16, projectMatches.length * 0.04) : 0;
280
+ const score = Math.min(1, (matches.length ? 0.25 + matchScore : 0) + projectBonus + intentBonus + (nameHit ? 0.2 : 0) + (nameTokenHit ? 0.18 : 0) + scopeBonus);
235
281
  return {
236
282
  id: `skill-${index + 1}`,
237
283
  name,
@@ -241,8 +287,12 @@ function scoreSkillsByKeyword({ prompt, skills }) {
241
287
  content,
242
288
  score,
243
289
  keywordScore: score,
290
+ relevancePriority,
291
+ domainEligible,
244
292
  reasons: [
245
293
  ...(matches.length ? [`keyword:${matches.slice(0, 4).join(",")}`] : []),
294
+ ...(projectBonus ? [`project:${projectMatches.slice(0, 4).join(",")}`] : []),
295
+ ...(intentBonus ? ["intent-match"] : []),
246
296
  ...(nameHit || nameTokenHit ? ["name-match"] : [])
247
297
  ],
248
298
  originalOrder: index
@@ -250,6 +300,271 @@ function scoreSkillsByKeyword({ prompt, skills }) {
250
300
  });
251
301
  }
252
302
 
303
+ function filterSkillMatches(matches, { normalizedPrompt, enriched }) {
304
+ if (!/\beas\b/.test(normalizedPrompt)) return matches;
305
+ const skillText = normalize(`${enriched.name} ${enriched.description}`);
306
+ if (/\b(eas|expo|cicd)\b/.test(skillText)) return matches;
307
+ return matches.filter((token) => token !== "android" && token !== "ios");
308
+ }
309
+
310
+ function isSkillDomainEligible(normalizedPrompt, enriched, projectTokens = new Set()) {
311
+ const skillText = normalize(`${enriched.name} ${enriched.description}`);
312
+ if (isMcpSkill(skillText) && !isMcpRelevantTask(normalizedPrompt, projectTokens)) return false;
313
+ if (isOffensiveSecuritySkill(skillText) && !isSecurityTask(normalizedPrompt)) return false;
314
+ if (isPlatformCommerceSkill(skillText) && !isPlatformCommerceTask(normalizedPrompt, skillText)) return false;
315
+ if (!/\beas\b/.test(normalizedPrompt)) return true;
316
+ if (!/\b(android|ios)\b/.test(skillText)) return true;
317
+ return /\b(eas|expo|cicd)\b/.test(skillText);
318
+ }
319
+
320
+ function skillIntentBonus(normalizedPrompt, enriched, projectTokens = new Set()) {
321
+ const skillText = normalize(`${enriched.name} ${enriched.description}`);
322
+ if (isMcpRelevantTask(normalizedPrompt, projectTokens)
323
+ && /\b(mcp|model context protocol|modelcontextprotocol|agent memory|tool developer|tool builder)\b/.test(skillText)) {
324
+ return 0.48;
325
+ }
326
+ if (isCommerceTask(normalizedPrompt)
327
+ && /\b(payment|payments|checkout|billing|bill|invoice|wallet|balance|stripe|paypal|commerce|monetization)\b/.test(skillText)) {
328
+ return 0.46;
329
+ }
330
+ if (isContentAccessTask(normalizedPrompt)
331
+ && /\b(api|endpoint|backend|service|services|auth|authorization|permission|permissions|access|rbac|frontend api)\b/.test(skillText)) {
332
+ return 0.34;
333
+ }
334
+ if (isNotificationTask(normalizedPrompt)
335
+ && /\b(notification|notifications|notify|message|sms|email|event|webhook)\b/.test(skillText)) {
336
+ return 0.3;
337
+ }
338
+ if (isFrontendCheckoutTask(normalizedPrompt)
339
+ && /\b(frontend|react|next|nextjs|ui|component|modal|api integration)\b/.test(skillText)) {
340
+ return 0.32;
341
+ }
342
+ if (isExpoRuntimeTask(normalizedPrompt, projectTokens)
343
+ && /\b(expo|eas|nativewind|react native|tailwind)\b/.test(skillText)) {
344
+ return 0.46;
345
+ }
346
+ if (isNextAppRouterTask(normalizedPrompt)
347
+ && /\b(next|nextjs)\b/.test(skillText)
348
+ && /\b(app router|router|routing|server components)\b/.test(skillText)) {
349
+ return 0.5;
350
+ }
351
+ if (/\beas\b/.test(normalizedPrompt)
352
+ && /\b(eas|expo)\b/.test(skillText)
353
+ && /\b(cicd|workflow|workflows|build|deploy|deployment|pipeline|pipelines)\b/.test(skillText)) {
354
+ return 0.28;
355
+ }
356
+ if (/\b(webapp|frontend|ui|dashboard|button|page|component|app|router)\b/.test(normalizedPrompt)
357
+ && /\b(frontend|react|next|nextjs|ui|component|tailwind|app router)\b/.test(skillText)) {
358
+ return 0.36;
359
+ }
360
+ if (/\b(role|admin|creator|permission|permissions|authorization|access)\b/.test(normalizedPrompt)
361
+ && /\b(auth|authentication|authorization|permission|permissions|access|rbac)\b/.test(skillText)) {
362
+ return 0.32;
363
+ }
364
+ return 0;
365
+ }
366
+
367
+ function skillRelevancePriority(normalizedPrompt, enriched, projectTokens = new Set()) {
368
+ const skillText = normalize(`${enriched.name} ${enriched.description}`);
369
+ const skillName = normalize(enriched.name);
370
+ let priority = 0;
371
+ if (isMcpRelevantTask(normalizedPrompt, projectTokens)) {
372
+ if (skillName === "mcp builder") priority += 760;
373
+ if (skillName === "mcp management") priority += 740;
374
+ if (skillName === "mcp tool developer") priority += 720;
375
+ if (skillName === "agent memory mcp") priority += 700;
376
+ if (skillName === "agent tool builder" || skillName === "context agent") priority += 260;
377
+ if (/\b(mcp|model context protocol|modelcontextprotocol)\b/.test(skillText)) priority += 160;
378
+ }
379
+ if (isCommerceTask(normalizedPrompt)) {
380
+ if (/\b(payment integration|stripe integration|paypal integration)\b/.test(skillText)) priority += 520;
381
+ if (/\bbilling automation\b/.test(skillText)) priority += 430;
382
+ if (/\b(payment|payments|checkout|billing|wallet|balance|stripe|paypal|commerce|monetization)\b/.test(skillText)) priority += 160;
383
+ if (!/\bstripe\b/.test(normalizedPrompt) && /\bstripe\b/.test(skillText)) priority -= 520;
384
+ if (!/\bpaypal\b/.test(normalizedPrompt) && /\bpaypal\b/.test(skillText)) priority -= 520;
385
+ if (!/\bsquare\b/.test(normalizedPrompt) && /\bsquare\b/.test(skillText)) priority -= 440;
386
+ if (/\b(mcp|metasploit|penetration|exploit|bug bounty)\b/.test(skillText)) priority -= 500;
387
+ }
388
+ if (isContentAccessTask(normalizedPrompt)) {
389
+ if (/\b(api endpoint builder|backend development|backend architect|frontend api integration patterns)\b/.test(skillText)) priority += 260;
390
+ if (/\b(auth implementation patterns|authorization|permission|permissions|access|rbac)\b/.test(skillText)) priority += 120;
391
+ }
392
+ if (isNotificationTask(normalizedPrompt)) {
393
+ if (/\bsendblue notify\b/.test(skillText)) priority += 140;
394
+ if (/\b(notification|notifications|notify|message|sms|email|event|webhook)\b/.test(skillText)) priority += 90;
395
+ }
396
+ if (isFrontendCheckoutTask(normalizedPrompt)) {
397
+ if (/\bfrontend api integration patterns\b/.test(skillText)) priority += 220;
398
+ if (/\breact nextjs development|nextjs best practices|nextjs app router patterns|frontend developer\b/.test(skillText)) priority += 90;
399
+ }
400
+ if (isExpoRuntimeTask(normalizedPrompt, projectTokens)) {
401
+ if (/\bexpo deployment\b/.test(skillText)) priority += 900;
402
+ if (/\bbuilding native ui\b/.test(skillText)) priority += 760;
403
+ if (/\bexpo tailwind setup\b/.test(skillText)) priority += 620;
404
+ if (/\bexpo\b/.test(skillText) && /\b(qr|expo go|run|running|start|connect|eas|deployment|build)\b/.test(skillText)) priority += 220;
405
+ if (/\bnativewind|tailwind\b/.test(skillText) && projectTokens.has("nativewind")) priority += 120;
406
+ if (/\b(next|nextjs|frontend designer|dark themed|glassmorphism|framer motion)\b/.test(skillText)) priority -= 160;
407
+ }
408
+ if (isNextAppRouterTask(normalizedPrompt)) {
409
+ if (/\bnextjs app router patterns\b/.test(skillText)) priority += 600;
410
+ if (/\bnextjs best practices\b/.test(skillText)) priority += 560;
411
+ if (/\breact nextjs development\b/.test(skillText)) priority += 420;
412
+ if (/\b(next|nextjs)\b/.test(skillText) && /\b(app router|router|routing|server components)\b/.test(skillText)) priority += 100;
413
+ if (/\b(next|nextjs)\b/.test(skillText) && /\breact\b/.test(skillText)) priority += 70;
414
+ if (/\b(glassmorphism|dark themed|dark theme|framer motion)\b/.test(skillText)) priority -= 40;
415
+ }
416
+ if (/\b(role|admin|creator|permission|permissions|authorization|access)\b/.test(normalizedPrompt)
417
+ && /\b(auth|authentication|authorization|permission|permissions|access|rbac)\b/.test(skillText)) {
418
+ priority += 55;
419
+ }
420
+ return priority;
421
+ }
422
+
423
+ function isNextAppRouterTask(normalizedPrompt) {
424
+ return /\bwebapp\b.*\bsrc\b.*\bapp\b/.test(normalizedPrompt)
425
+ || /\b(next|nextjs)\b.*\b(app router|router|routing)\b/.test(normalizedPrompt)
426
+ || /\bapp router\b/.test(normalizedPrompt);
427
+ }
428
+
429
+ function isExpoRuntimeTask(normalizedPrompt, projectTokens = new Set()) {
430
+ const expoProject = projectTokens.has("expo") || projectTokens.has("nativewind") || projectTokens.has("eas");
431
+ if (!expoProject) return false;
432
+ return /\b(qr|connect|run|start|expo go|device|metro|tunnel|lan)\b/.test(normalizedPrompt);
433
+ }
434
+
435
+ function isCommerceTask(normalizedPrompt) {
436
+ return /\b(purchase|purchased|buy|buyer|seller|payment|pay|checkout|wallet|balance|top up|topup|funded|billing|invoice)\b/.test(normalizedPrompt);
437
+ }
438
+
439
+ function isContentAccessTask(normalizedPrompt) {
440
+ return /\b(content access service|content access|access permissions|grant access|permissions|library|resources|tutorials|collections)\b/.test(normalizedPrompt);
441
+ }
442
+
443
+ function isNotificationTask(normalizedPrompt) {
444
+ return /\b(notification|notifications|notify|buyer|seller)\b/.test(normalizedPrompt);
445
+ }
446
+
447
+ function isFrontendCheckoutTask(normalizedPrompt) {
448
+ return /\b(modal|display|show|checkout|library|frontend|webapp|page|button)\b/.test(normalizedPrompt);
449
+ }
450
+
451
+ function isMcpTask(normalizedPrompt) {
452
+ return /\b(mcp|model context protocol|tool server|tools server|server tool|bridge|proxy)\b/.test(normalizedPrompt);
453
+ }
454
+
455
+ function isMcpRelevantTask(normalizedPrompt, projectTokens = new Set()) {
456
+ return isMcpTask(normalizedPrompt)
457
+ || (isMcpProject(projectTokens) && isContextRetrievalTask(normalizedPrompt));
458
+ }
459
+
460
+ function isMcpProject(projectTokens = new Set()) {
461
+ return projectTokens.has("mcp") || projectTokens.has("modelcontextprotocol");
462
+ }
463
+
464
+ function isContextRetrievalTask(normalizedPrompt) {
465
+ return /\b(suggest|suggested|suggestion|skills|files|context|retrieval|retrieve|scorer|scoring|match|matching|prompt|hook|inject|injection)\b/.test(normalizedPrompt);
466
+ }
467
+
468
+ function isSecurityTask(normalizedPrompt) {
469
+ return /\b(security|pentest|penetration|exploit|vulnerability|metasploit|bug bounty|owasp|xss|csrf|attack|audit)\b/.test(normalizedPrompt);
470
+ }
471
+
472
+ function isMcpSkill(skillText) {
473
+ return /\bmcp\b|\bmodel context protocol\b/.test(skillText);
474
+ }
475
+
476
+ function isOffensiveSecuritySkill(skillText) {
477
+ return /\b(metasploit|penetration testing|bug bounty|exploit|exploitation|privilege escalation|ethical hacking|web fuzzing|security assessment)\b/.test(skillText);
478
+ }
479
+
480
+ function isPlatformCommerceSkill(skillText) {
481
+ return /\b(wordpress|woocommerce|shopify|odoo)\b/.test(skillText);
482
+ }
483
+
484
+ function isPlatformCommerceTask(normalizedPrompt, skillText) {
485
+ if (/\bwordpress\b/.test(skillText)) return /\bwordpress\b/.test(normalizedPrompt);
486
+ if (/\bwoocommerce\b/.test(skillText)) return /\bwoocommerce\b/.test(normalizedPrompt);
487
+ if (/\bshopify\b/.test(skillText)) return /\bshopify\b/.test(normalizedPrompt);
488
+ if (/\bodoo\b/.test(skillText)) return /\bodoo\b/.test(normalizedPrompt);
489
+ return true;
490
+ }
491
+
492
+ export function projectSkillHints({ cwd = process.cwd() } = {}) {
493
+ const hints = new Set();
494
+ const packagePaths = workspacePackagePaths(cwd);
495
+
496
+ for (const packagePath of packagePaths) {
497
+ const packageDir = path.dirname(packagePath);
498
+ const packageJson = readJson(packagePath);
499
+ addHintText(hints, JSON.stringify({
500
+ name: packageJson?.name,
501
+ description: packageJson?.description,
502
+ keywords: packageJson?.keywords || [],
503
+ scripts: packageJson?.scripts || {},
504
+ dependencies: Object.keys(packageJson?.dependencies || {}),
505
+ devDependencies: Object.keys(packageJson?.devDependencies || {})
506
+ }));
507
+ for (const fileName of ["app.json", "app.config.js", "app.config.ts", "eas.json"]) {
508
+ if (fs.existsSync(path.join(packageDir, fileName))) addHintText(hints, fileName);
509
+ }
510
+ }
511
+ return [...hints];
512
+ }
513
+
514
+ function workspacePackagePaths(cwd) {
515
+ const rootPackagePath = path.join(cwd, "package.json");
516
+ const rootPackage = readJson(rootPackagePath);
517
+ const paths = new Set([rootPackagePath]);
518
+ for (const workspace of workspacePatterns(rootPackage?.workspaces)) {
519
+ for (const packagePath of expandWorkspacePattern({ cwd, pattern: workspace })) {
520
+ paths.add(packagePath);
521
+ }
522
+ }
523
+ return [...paths];
524
+ }
525
+
526
+ function workspacePatterns(workspaces) {
527
+ if (Array.isArray(workspaces)) return workspaces.filter((item) => typeof item === "string");
528
+ if (Array.isArray(workspaces?.packages)) return workspaces.packages.filter((item) => typeof item === "string");
529
+ return [];
530
+ }
531
+
532
+ function expandWorkspacePattern({ cwd, pattern }) {
533
+ const normalized = String(pattern || "").replace(/\\/g, "/").replace(/\/+$/g, "");
534
+ if (!normalized || normalized.startsWith("..") || path.isAbsolute(normalized)) return [];
535
+ if (!normalized.includes("*")) {
536
+ const packagePath = path.join(cwd, normalized, "package.json");
537
+ return fs.existsSync(packagePath) ? [packagePath] : [];
538
+ }
539
+ const parts = normalized.split("/");
540
+ const starIndex = parts.indexOf("*");
541
+ if (starIndex < 0 || parts.includes("**")) return [];
542
+ const baseDir = path.join(cwd, ...parts.slice(0, starIndex));
543
+ const suffix = parts.slice(starIndex + 1);
544
+ let entries = [];
545
+ try {
546
+ entries = fs.readdirSync(baseDir, { withFileTypes: true });
547
+ } catch {
548
+ return [];
549
+ }
550
+ return entries
551
+ .filter((entry) => entry.isDirectory() && !entry.name.startsWith("."))
552
+ .map((entry) => path.join(baseDir, entry.name, ...suffix, "package.json"))
553
+ .filter((packagePath) => fs.existsSync(packagePath));
554
+ }
555
+
556
+ function readJson(filePath) {
557
+ try {
558
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
559
+ } catch {
560
+ return null;
561
+ }
562
+ }
563
+
564
+ function addHintText(hints, value) {
565
+ for (const token of normalize(value).split(/\s+/).filter(Boolean)) hints.add(token);
566
+ }
567
+
253
568
  function enrichSkill(skill) {
254
569
  const name = String(skill.name || "");
255
570
  const description = truncateDescription(skill.description || "");
@@ -268,3 +583,10 @@ function enrichSkill(skill) {
268
583
  function normalize(value) {
269
584
  return String(value || "").toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
270
585
  }
586
+
587
+ function normalizePrompt(value) {
588
+ return normalize(String(value || "")
589
+ .replace(/https?:\/\/\S+/gi, " ")
590
+ .replace(/giao\s+di[eệ]n/gi, "frontend ui")
591
+ .replace(/phan\s+quyen/gi, "authorization role"));
592
+ }
@@ -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
  }