@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.
- package/CHANGELOG.md +36 -0
- package/README.md +41 -10
- package/bin/ctx.js +140 -42
- package/package.json +2 -1
- package/plugins/ctx/.codex-plugin/plugin.json +1 -1
- package/plugins/ctx/bin/on-prompt.js +12 -9
- package/plugins/ctx/lib/analyzer.js +193 -91
- package/plugins/ctx/lib/auto-warm.js +74 -0
- package/plugins/ctx/lib/ctx-mcp-client.js +58 -9
- package/plugins/ctx/lib/embedding-scorer.js +109 -7
- package/plugins/ctx/lib/file-embedding-retriever.js +20 -23
- package/plugins/ctx/lib/global-hooks.js +20 -0
- package/plugins/ctx/lib/graph-retriever.js +82 -15
- package/plugins/ctx/lib/graph-strategy.js +107 -0
- package/plugins/ctx/lib/hook-io.js +29 -1
- package/plugins/ctx/lib/import-graph.js +37 -40
- package/plugins/ctx/lib/mcp-proxy-install.js +18 -90
- package/plugins/ctx/lib/output-config.js +85 -0
- package/plugins/ctx/lib/package-install.js +8 -0
- package/plugins/ctx/lib/prompt-hook.js +162 -25
- package/plugins/ctx/lib/ruler-sync.js +9 -71
- package/plugins/ctx/lib/scheduler.js +48 -21
- package/plugins/ctx/lib/score-context.js +60 -32
- package/plugins/ctx/lib/setup-wizard.js +5 -2
- package/plugins/ctx/lib/shell-runner.js +88 -0
- package/plugins/ctx/lib/skill-discoverer.js +333 -11
- package/plugins/ctx/lib/skillshare-sync.js +51 -2
- package/plugins/ctx/lib/stop-hook.js +2 -3
- package/plugins/ctx/lib/toml-config.js +116 -0
- package/plugins/ctx/mcp/server.js +6 -2
|
@@ -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
|
-
|
|
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) >=
|
|
192
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
}
|