@minhpnq1807/contextos 0.5.44 → 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 +10 -0
- package/bin/ctx.js +4 -3
- package/package.json +1 -1
- package/plugins/ctx/.codex-plugin/plugin.json +1 -1
- package/plugins/ctx/lib/analyzer.js +188 -4
- package/plugins/ctx/lib/prompt-hook.js +69 -7
- package/plugins/ctx/lib/scheduler.js +18 -3
- package/plugins/ctx/lib/skill-discoverer.js +240 -18
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.45
|
|
4
|
+
|
|
5
|
+
- **Project-aware MCP skill suggestions:** Skill ranking now reads `package.json` keywords and dependencies such as `@modelcontextprotocol/sdk`. MCP projects can recommend `mcp-builder`, `mcp-management`, `mcp-tool-developer`, and `agent-memory-mcp` for context retrieval, scorer, hook, and prompt-injection debugging tasks even when the prompt does not explicitly say `mcp`.
|
|
6
|
+
- **Domain-safe skill ranking:** Added common-language stopwords and bounded domain gates so generic prompt overlap no longer revives unrelated MCP, offensive-security, or platform-commerce skills. Purchase prompts prioritize payment, billing, frontend API integration, and backend skills without assuming Stripe, PayPal, WooCommerce, or another provider unless named.
|
|
7
|
+
- **Purchase-flow file retrieval:** File embedding and graph retrieval queries now expand purchase, wallet, checkout, content-access, library, and notification prompts with focused retrieval hints while keeping repository walking out of the prompt hot path.
|
|
8
|
+
- **Seven-item prompt summaries:** Increased suggested file and skill limits from three to seven in prompt hooks and `ctx debug`. Suggested files now render as a compact comma-separated inline summary while duplicate basenames remain disambiguated with relative paths.
|
|
9
|
+
- **Target-workspace prompt scoring:** Prompt hooks can score an explicitly named sibling workspace such as `../philo-mind`, allowing repo-specific manifest and skill recommendations when debugging another project from the current shell.
|
|
10
|
+
- **Monorepo manifest awareness:** Project skill hints and run/connect file suggestions now read bounded root and workspace `package.json` metadata, including workspace arrays, `{ packages: [...] }`, and one-level globs.
|
|
11
|
+
- **Fallback file retrieval budget:** Raised direct hook fallback file-vector timeout to 1000ms so indexed file suggestions remain available when the private MCP bridge is unavailable.
|
|
12
|
+
|
|
3
13
|
## 0.5.44
|
|
4
14
|
|
|
5
15
|
- **Robust MCP TOML handling:** Added `smol-toml` parsing for Codex MCP config while preserving comments, ordering, multiline arrays, and nested tool approval sections during telemetry proxy rewrites.
|
package/bin/ctx.js
CHANGED
|
@@ -590,12 +590,13 @@ async function debug(task) {
|
|
|
590
590
|
cwd,
|
|
591
591
|
prompt: task,
|
|
592
592
|
dataDir: contextOSDataDir(),
|
|
593
|
-
maxFiles:
|
|
593
|
+
maxFiles: 7,
|
|
594
|
+
maxSkills: 7,
|
|
594
595
|
embeddingTimeoutMs: Number(process.env.CONTEXTOS_EMBEDDING_DEBUG_TIMEOUT_MS || 5000)
|
|
595
596
|
});
|
|
596
597
|
const rules = scored.scoredRules;
|
|
597
|
-
const relevantFiles = scored.suggestedFiles.slice(0,
|
|
598
|
-
const suggestedSkills = (scored.suggestedSkills || []).slice(0,
|
|
598
|
+
const relevantFiles = scored.suggestedFiles.slice(0, 7);
|
|
599
|
+
const suggestedSkills = (scored.suggestedSkills || []).slice(0, 7);
|
|
599
600
|
const suggestedWorkflows = (scored.suggestedWorkflows || []).slice(0, 2);
|
|
600
601
|
const scheduled = scheduleContext({ rules, relevantFiles, suggestedSkills, suggestedWorkflows });
|
|
601
602
|
|
package/package.json
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import { findGraphRelevantFiles, mergeRelevantFiles } from "./graph-retriever.js";
|
|
3
4
|
import { expandImportGraph } from "./import-graph.js";
|
|
@@ -282,9 +283,12 @@ export async function findRelevantFiles({
|
|
|
282
283
|
} = {}) {
|
|
283
284
|
if (!String(task || "").trim()) return [];
|
|
284
285
|
|
|
286
|
+
const retrievalTask = expandFileRetrievalTask(task);
|
|
287
|
+
const explicitFiles = findExplicitPromptFiles({ cwd, task, limit: Math.max(limit * 2, 6) });
|
|
288
|
+
const manifestFiles = findProjectManifestFiles({ cwd, task, limit: Math.max(limit * 2, 6) });
|
|
285
289
|
const embeddingFiles = await embeddingFileFinder({
|
|
286
290
|
cwd,
|
|
287
|
-
task,
|
|
291
|
+
task: retrievalTask,
|
|
288
292
|
dataDir,
|
|
289
293
|
timeoutMs: fileEmbeddingTimeoutMs,
|
|
290
294
|
embeddingOptions: fileEmbeddingOptions,
|
|
@@ -292,16 +296,16 @@ export async function findRelevantFiles({
|
|
|
292
296
|
});
|
|
293
297
|
const importGraphFiles = expandImportGraph({
|
|
294
298
|
cwd,
|
|
295
|
-
seedFiles: embeddingFiles.slice(0, limit),
|
|
299
|
+
seedFiles: [...explicitFiles, ...manifestFiles, ...embeddingFiles].slice(0, limit),
|
|
296
300
|
dataDir,
|
|
297
301
|
limit: Math.max(limit * 2, 6)
|
|
298
302
|
});
|
|
299
|
-
const seedFiles = mergeLocalFileCandidates([...embeddingFiles, ...importGraphFiles])
|
|
303
|
+
const seedFiles = mergeLocalFileCandidates([...explicitFiles, ...manifestFiles, ...embeddingFiles, ...importGraphFiles])
|
|
300
304
|
.slice(0, Math.max(limit * 3, 9));
|
|
301
305
|
|
|
302
306
|
const graphFiles = findGraphRelevantFiles({
|
|
303
307
|
cwd,
|
|
304
|
-
task,
|
|
308
|
+
task: retrievalTask,
|
|
305
309
|
rules,
|
|
306
310
|
seedFiles,
|
|
307
311
|
limit: Math.max(limit * 2, 6)
|
|
@@ -310,6 +314,186 @@ export async function findRelevantFiles({
|
|
|
310
314
|
return mergeRelevantFiles({ graphFiles, heuristicFiles: seedFiles, limit });
|
|
311
315
|
}
|
|
312
316
|
|
|
317
|
+
export function findProjectManifestFiles({ cwd = process.cwd(), task = "", limit = 6 } = {}) {
|
|
318
|
+
const tokens = new Set(tokenize(task));
|
|
319
|
+
if (!isManifestRelevantTask(tokens)) return [];
|
|
320
|
+
const manifests = workspacePackageManifests(cwd, tokens);
|
|
321
|
+
return manifests.slice(0, limit).map((filePath, index) => ({
|
|
322
|
+
path: filePath,
|
|
323
|
+
score: manifestScore(filePath, tokens, index),
|
|
324
|
+
source: "manifest",
|
|
325
|
+
reasons: ["project-manifest"]
|
|
326
|
+
}));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function manifestScore(manifest, taskTokens, index) {
|
|
330
|
+
if (manifest === "package.json") return 50;
|
|
331
|
+
const parts = manifest.split(/[\\/]+/).filter(Boolean);
|
|
332
|
+
const workspaceName = parts.at(-2);
|
|
333
|
+
return (taskTokens.has(workspaceName) ? 35 : 20) - index * 0.01;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function isManifestRelevantTask(tokens) {
|
|
337
|
+
const runIntent = ["run", "start", "connect", "qr", "install", "build", "script", "scripts"].some((token) => tokens.has(token));
|
|
338
|
+
const projectIntent = ["webapp", "frontend", "expo", "native", "app", "package", "workspace"].some((token) => tokens.has(token));
|
|
339
|
+
return runIntent && projectIntent;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function workspacePackageManifests(cwd, taskTokens = new Set()) {
|
|
343
|
+
const rootManifest = path.join(cwd, "package.json");
|
|
344
|
+
const manifests = [];
|
|
345
|
+
if (fs.existsSync(rootManifest)) manifests.push("package.json");
|
|
346
|
+
const rootPackage = readJson(rootManifest);
|
|
347
|
+
for (const pattern of workspacePatterns(rootPackage?.workspaces)) {
|
|
348
|
+
for (const manifest of expandWorkspacePattern({ cwd, pattern })) {
|
|
349
|
+
manifests.push(path.relative(cwd, manifest));
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return [...new Set(manifests)].sort((a, b) => manifestPriority(b, taskTokens) - manifestPriority(a, taskTokens) || a.localeCompare(b));
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function manifestPriority(manifest, taskTokens) {
|
|
356
|
+
if (manifest === "package.json") return 100;
|
|
357
|
+
const parts = manifest.split(/[\\/]+/).filter(Boolean);
|
|
358
|
+
const workspaceName = parts.at(-2);
|
|
359
|
+
return taskTokens.has(workspaceName) ? 80 : 0;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function workspacePatterns(workspaces) {
|
|
363
|
+
if (Array.isArray(workspaces)) return workspaces.filter((item) => typeof item === "string");
|
|
364
|
+
if (Array.isArray(workspaces?.packages)) return workspaces.packages.filter((item) => typeof item === "string");
|
|
365
|
+
return [];
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function expandWorkspacePattern({ cwd, pattern }) {
|
|
369
|
+
const normalized = String(pattern || "").replace(/\\/g, "/").replace(/\/+$/g, "");
|
|
370
|
+
if (!normalized || normalized.startsWith("..") || path.isAbsolute(normalized)) return [];
|
|
371
|
+
if (!normalized.includes("*")) {
|
|
372
|
+
const manifest = path.join(cwd, normalized, "package.json");
|
|
373
|
+
return fs.existsSync(manifest) ? [manifest] : [];
|
|
374
|
+
}
|
|
375
|
+
const parts = normalized.split("/");
|
|
376
|
+
const starIndex = parts.indexOf("*");
|
|
377
|
+
if (starIndex < 0 || parts.includes("**")) return [];
|
|
378
|
+
const baseDir = path.join(cwd, ...parts.slice(0, starIndex));
|
|
379
|
+
const suffix = parts.slice(starIndex + 1);
|
|
380
|
+
let entries = [];
|
|
381
|
+
try {
|
|
382
|
+
entries = fs.readdirSync(baseDir, { withFileTypes: true });
|
|
383
|
+
} catch {
|
|
384
|
+
return [];
|
|
385
|
+
}
|
|
386
|
+
return entries
|
|
387
|
+
.filter((entry) => entry.isDirectory() && !entry.name.startsWith("."))
|
|
388
|
+
.map((entry) => path.join(baseDir, entry.name, ...suffix, "package.json"))
|
|
389
|
+
.filter((manifest) => fs.existsSync(manifest));
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function readJson(filePath) {
|
|
393
|
+
try {
|
|
394
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
395
|
+
} catch {
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function expandFileRetrievalTask(task) {
|
|
401
|
+
const tokens = new Set(tokenize(task));
|
|
402
|
+
const additions = new Set();
|
|
403
|
+
if (hasAny(tokens, ["purchase", "purchased", "buy", "buyer", "seller", "payment", "pay", "checkout"])) {
|
|
404
|
+
addAll(additions, [
|
|
405
|
+
"purchase", "payment", "checkout", "billing", "wallet", "balance", "top up",
|
|
406
|
+
"transaction", "order", "invoice"
|
|
407
|
+
]);
|
|
408
|
+
}
|
|
409
|
+
if (hasAny(tokens, ["wallet", "balance", "topup", "top", "funded"])) {
|
|
410
|
+
addAll(additions, ["wallet", "balance", "top up", "billing"]);
|
|
411
|
+
}
|
|
412
|
+
if (hasAny(tokens, ["library", "access", "permissions", "permission", "resources", "tutorials", "collections"])) {
|
|
413
|
+
addAll(additions, [
|
|
414
|
+
"content access", "content-access-service", "access permissions", "library",
|
|
415
|
+
"resource", "resources", "tutorial", "tutorials", "collections"
|
|
416
|
+
]);
|
|
417
|
+
}
|
|
418
|
+
if (hasAny(tokens, ["notification", "notifications", "notify", "buyer", "seller"])) {
|
|
419
|
+
addAll(additions, ["notification", "notifications", "notify", "buyer", "seller"]);
|
|
420
|
+
}
|
|
421
|
+
if (!additions.size) return task;
|
|
422
|
+
return `${task}\n\nContextOS retrieval hints: ${[...additions].join(", ")}`;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function hasAny(tokens, values) {
|
|
426
|
+
return values.some((value) => tokens.has(value));
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function addAll(target, values) {
|
|
430
|
+
for (const value of values) target.add(value);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export function findExplicitPromptFiles({ cwd = process.cwd(), task = "", limit = 6 } = {}) {
|
|
434
|
+
const candidates = new Set();
|
|
435
|
+
const normalizedTask = String(task || "").replace(/\/\s+/g, "/");
|
|
436
|
+
const matches = normalizedTask.match(/[A-Za-z0-9_.()[\]@~:-]+(?:\/[A-Za-z0-9_.()[\]@~:-]+)+/g) || [];
|
|
437
|
+
for (const match of matches) {
|
|
438
|
+
const cleaned = match.replace(/[),.;:]+$/g, "");
|
|
439
|
+
for (const filePath of resolvePromptPathCandidates({ cwd, promptPath: cleaned })) {
|
|
440
|
+
candidates.add(filePath);
|
|
441
|
+
if (candidates.size >= limit) break;
|
|
442
|
+
}
|
|
443
|
+
if (candidates.size >= limit) break;
|
|
444
|
+
}
|
|
445
|
+
return [...candidates].map((filePath, index) => ({
|
|
446
|
+
path: filePath,
|
|
447
|
+
score: 12 - index * 0.01,
|
|
448
|
+
source: "prompt-path",
|
|
449
|
+
reasons: ["prompt-path"]
|
|
450
|
+
}));
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function resolvePromptPathCandidates({ cwd, promptPath }) {
|
|
454
|
+
if (!promptPath || promptPath.includes("://")) return [];
|
|
455
|
+
const relative = promptPath.replace(/^\.?\//, "");
|
|
456
|
+
if (relative.startsWith("..")) return [];
|
|
457
|
+
const absolute = path.resolve(cwd, relative);
|
|
458
|
+
if (!isInsidePath(absolute, cwd)) return [];
|
|
459
|
+
const resolved = [];
|
|
460
|
+
if (isSourceFile(absolute)) resolved.push(path.relative(cwd, absolute));
|
|
461
|
+
if (isDirectory(absolute)) {
|
|
462
|
+
for (const fileName of ["page.tsx", "page.ts", "page.jsx", "page.js", "layout.tsx", "index.tsx", "index.ts"]) {
|
|
463
|
+
const candidate = path.join(absolute, fileName);
|
|
464
|
+
if (isSourceFile(candidate)) resolved.push(path.relative(cwd, candidate));
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
if (!path.extname(relative)) {
|
|
468
|
+
for (const extension of [".tsx", ".ts", ".jsx", ".js", ".md", ".json"]) {
|
|
469
|
+
const candidate = `${absolute}${extension}`;
|
|
470
|
+
if (isSourceFile(candidate)) resolved.push(path.relative(cwd, candidate));
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return resolved;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function isInsidePath(filePath, parentPath) {
|
|
477
|
+
const relative = path.relative(path.resolve(parentPath), path.resolve(filePath));
|
|
478
|
+
return relative && !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function isDirectory(filePath) {
|
|
482
|
+
try {
|
|
483
|
+
return fs.statSync(filePath).isDirectory();
|
|
484
|
+
} catch {
|
|
485
|
+
return false;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function isSourceFile(filePath) {
|
|
490
|
+
try {
|
|
491
|
+
return fs.statSync(filePath).isFile();
|
|
492
|
+
} catch {
|
|
493
|
+
return false;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
313
497
|
function mergeLocalFileCandidates(files) {
|
|
314
498
|
const byPath = new Map();
|
|
315
499
|
for (const file of files) {
|
|
@@ -5,8 +5,13 @@ import { callCtxScoreContext } from "./ctx-mcp-client.js";
|
|
|
5
5
|
import { resolveHookCwd } from "./hook-io.js";
|
|
6
6
|
import { loadOutputConfig } from "./output-config.js";
|
|
7
7
|
import { scoreContext as scoreContextDirect } from "./score-context.js";
|
|
8
|
+
import fs from "node:fs";
|
|
8
9
|
import path from "node:path";
|
|
9
10
|
|
|
11
|
+
const PROMPT_FILE_LIMIT = 7;
|
|
12
|
+
const PROMPT_SKILL_LIMIT = 7;
|
|
13
|
+
const PROMPT_WORKFLOW_LIMIT = 2;
|
|
14
|
+
|
|
10
15
|
export async function handlePromptPayload(
|
|
11
16
|
payload,
|
|
12
17
|
{
|
|
@@ -24,7 +29,8 @@ export async function handlePromptPayload(
|
|
|
24
29
|
} = {}
|
|
25
30
|
) {
|
|
26
31
|
const prompt = payload.prompt || payload.message || payload.user_prompt || "";
|
|
27
|
-
const
|
|
32
|
+
const hookCwd = resolveHookCwd(payload);
|
|
33
|
+
const cwd = resolvePromptTargetCwd({ cwd: hookCwd, prompt });
|
|
28
34
|
const openFiles = payload.openFiles || payload.open_files || payload.files || [];
|
|
29
35
|
const dataDir = dataPath ? path.dirname(dataPath) : undefined;
|
|
30
36
|
|
|
@@ -34,7 +40,9 @@ export async function handlePromptPayload(
|
|
|
34
40
|
cwd,
|
|
35
41
|
prompt,
|
|
36
42
|
openFiles,
|
|
37
|
-
maxFiles:
|
|
43
|
+
maxFiles: PROMPT_FILE_LIMIT,
|
|
44
|
+
maxSkills: PROMPT_SKILL_LIMIT,
|
|
45
|
+
maxWorkflows: PROMPT_WORKFLOW_LIMIT
|
|
38
46
|
}, {
|
|
39
47
|
dataDir: mcpDataDir || dataDir,
|
|
40
48
|
timeoutMs: Number(process.env.CONTEXTOS_MCP_BRIDGE_TIMEOUT_MS || 2000)
|
|
@@ -45,10 +53,12 @@ export async function handlePromptPayload(
|
|
|
45
53
|
cwd,
|
|
46
54
|
prompt,
|
|
47
55
|
openFiles,
|
|
48
|
-
maxFiles:
|
|
56
|
+
maxFiles: PROMPT_FILE_LIMIT,
|
|
57
|
+
maxSkills: PROMPT_SKILL_LIMIT,
|
|
58
|
+
maxWorkflows: PROMPT_WORKFLOW_LIMIT,
|
|
49
59
|
dataDir: mcpDataDir || dataDir,
|
|
50
60
|
embeddingTimeoutMs: Number(process.env.CONTEXTOS_HOOK_EMBEDDING_TIMEOUT_MS || 500),
|
|
51
|
-
fileEmbeddingTimeoutMs: Number(process.env.CONTEXTOS_HOOK_FILE_EMBEDDING_TIMEOUT_MS ||
|
|
61
|
+
fileEmbeddingTimeoutMs: Number(process.env.CONTEXTOS_HOOK_FILE_EMBEDDING_TIMEOUT_MS || 1000)
|
|
52
62
|
}), directFallbackTimeoutMs, "direct fallback scoring");
|
|
53
63
|
scored.telemetry = {
|
|
54
64
|
...(scored.telemetry || {}),
|
|
@@ -66,9 +76,9 @@ export async function handlePromptPayload(
|
|
|
66
76
|
|
|
67
77
|
if (scored.error) throw new Error(scored.error);
|
|
68
78
|
const scoredRules = scored.scoredRules || [];
|
|
69
|
-
const relevantFiles = (scored.suggestedFiles || []).slice(0,
|
|
70
|
-
const suggestedSkills = (scored.suggestedSkills || []).slice(0,
|
|
71
|
-
const suggestedWorkflows = (scored.suggestedWorkflows || []).slice(0,
|
|
79
|
+
const relevantFiles = (scored.suggestedFiles || []).slice(0, PROMPT_FILE_LIMIT);
|
|
80
|
+
const suggestedSkills = (scored.suggestedSkills || []).slice(0, PROMPT_SKILL_LIMIT);
|
|
81
|
+
const suggestedWorkflows = (scored.suggestedWorkflows || []).slice(0, PROMPT_WORKFLOW_LIMIT);
|
|
72
82
|
const effectiveOutputConfig = outputConfig || loadOutputConfig();
|
|
73
83
|
const scheduled = scheduleContext({ rules: scoredRules, relevantFiles, suggestedSkills, suggestedWorkflows, outputConfig: effectiveOutputConfig });
|
|
74
84
|
const contextEmptyReason = emptyContextReason({ scheduled, outputConfig: effectiveOutputConfig, injectContext });
|
|
@@ -127,6 +137,58 @@ export async function handlePromptPayload(
|
|
|
127
137
|
return output;
|
|
128
138
|
}
|
|
129
139
|
|
|
140
|
+
export function resolvePromptTargetCwd({ cwd = process.cwd(), prompt = "" } = {}) {
|
|
141
|
+
const current = path.resolve(cwd);
|
|
142
|
+
const candidates = targetPathCandidates(prompt);
|
|
143
|
+
for (const candidate of candidates) {
|
|
144
|
+
const resolved = path.resolve(current, candidate);
|
|
145
|
+
if (!isAllowedTargetCwd({ current, resolved })) continue;
|
|
146
|
+
if (isWorkspaceRoot(resolved)) return resolved;
|
|
147
|
+
}
|
|
148
|
+
return current;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function targetPathCandidates(prompt) {
|
|
152
|
+
const text = String(prompt || "");
|
|
153
|
+
const patterns = [
|
|
154
|
+
/\b(?:tr[eê]n|in|inside|under|repo|workspace|cwd)\s+([.~A-Za-z0-9_/@.-]+(?:\/[A-Za-z0-9_@().-]+)*)/gi,
|
|
155
|
+
/\b(?:debug|test|check|run)\s+(?:on|tr[eê]n)\s+([.~A-Za-z0-9_/@.-]+(?:\/[A-Za-z0-9_@().-]+)*)/gi
|
|
156
|
+
];
|
|
157
|
+
const results = [];
|
|
158
|
+
for (const pattern of patterns) {
|
|
159
|
+
let match;
|
|
160
|
+
while ((match = pattern.exec(text))) {
|
|
161
|
+
const value = cleanPromptPath(match[1]);
|
|
162
|
+
if (value) results.push(value);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return results;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function cleanPromptPath(value) {
|
|
169
|
+
const cleaned = String(value || "").trim().replace(/[),.;:]+$/g, "");
|
|
170
|
+
if (!cleaned || cleaned.includes("://")) return null;
|
|
171
|
+
return cleaned;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function isAllowedTargetCwd({ current, resolved }) {
|
|
175
|
+
const parent = path.dirname(current);
|
|
176
|
+
const relative = path.relative(parent, resolved);
|
|
177
|
+
return relative && !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function isWorkspaceRoot(directory) {
|
|
181
|
+
try {
|
|
182
|
+
const stat = fs.statSync(directory);
|
|
183
|
+
if (!stat.isDirectory()) return false;
|
|
184
|
+
} catch {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
return fs.existsSync(path.join(directory, "package.json"))
|
|
188
|
+
|| fs.existsSync(path.join(directory, "AGENTS.md"))
|
|
189
|
+
|| fs.existsSync(path.join(directory, ".git"));
|
|
190
|
+
}
|
|
191
|
+
|
|
130
192
|
function emptyContextReason({ scheduled, outputConfig, injectContext }) {
|
|
131
193
|
if (!injectContext) return "injection-disabled";
|
|
132
194
|
if (scheduled.additionalContext) return null;
|
|
@@ -22,7 +22,7 @@ export function scheduleContext({
|
|
|
22
22
|
sections.push(section("Critical ContextOS rules", high.slice(0, 5).map(formatRule)));
|
|
23
23
|
}
|
|
24
24
|
if (outputConfig.sections.files && relevantFiles.length) {
|
|
25
|
-
sections.push(
|
|
25
|
+
sections.push(commaSection("Suggested files to check", formatFiles(relevantFiles)));
|
|
26
26
|
}
|
|
27
27
|
if (outputConfig.sections.skills && suggestedSkills.length) {
|
|
28
28
|
sections.push(inlineSection("Skills to activate for this task", suggestedSkills.map(formatSkill)));
|
|
@@ -72,13 +72,28 @@ function inlineSection(title, values) {
|
|
|
72
72
|
return `## ${title}: ${uniqueValues.join(", ")}`;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
function commaSection(title, values) {
|
|
76
|
+
const uniqueValues = [...new Set(values)];
|
|
77
|
+
if (!uniqueValues.length) return "";
|
|
78
|
+
return `## ${title}, ${uniqueValues.join(", ")}`;
|
|
79
|
+
}
|
|
75
80
|
|
|
76
81
|
function formatRule(rule) {
|
|
77
82
|
return `- ${rule.content}`;
|
|
78
83
|
}
|
|
79
84
|
|
|
80
|
-
function
|
|
81
|
-
|
|
85
|
+
function formatFiles(files) {
|
|
86
|
+
const counts = new Map();
|
|
87
|
+
for (const file of files) {
|
|
88
|
+
const name = path.basename(file.path);
|
|
89
|
+
counts.set(name, (counts.get(name) || 0) + 1);
|
|
90
|
+
}
|
|
91
|
+
return files.map((file) => formatFile(file, counts));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function formatFile(file, basenameCounts) {
|
|
95
|
+
const name = path.basename(file.path);
|
|
96
|
+
return basenameCounts.get(name) > 1 ? file.path : name;
|
|
82
97
|
}
|
|
83
98
|
|
|
84
99
|
function formatSkill(skill) {
|
|
@@ -14,10 +14,15 @@ const GENERIC_SKILL_TOKENS = new Set([
|
|
|
14
14
|
"active", "agent", "agents", "code", "config", "configuration", "create", "development",
|
|
15
15
|
"environment", "file", "files", "graph", "install", "integration", "local", "node", "package",
|
|
16
16
|
"project", "refresh", "rebuild", "setup", "skill", "skills", "sync", "tool", "tools", "using",
|
|
17
|
-
"build", "production", "https", "http", "com", "www"
|
|
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"
|
|
18
21
|
]);
|
|
19
22
|
const SPECIALIZED_SKILL_TOKENS = new Set([
|
|
20
|
-
"android", "cicd", "eas", "expo", "
|
|
23
|
+
"android", "authorization", "cicd", "eas", "expo", "frontend", "ios", "next", "nextjs",
|
|
24
|
+
"mcp", "modelcontextprotocol", "postgres", "postgresql", "react", "react-native", "tailwind",
|
|
25
|
+
"typescript", "ui"
|
|
21
26
|
]);
|
|
22
27
|
|
|
23
28
|
const scanCache = new Map();
|
|
@@ -29,9 +34,9 @@ export function skillSearchRoots({ cwd = process.cwd(), home = os.homedir() } =
|
|
|
29
34
|
path.join(cwd, ".gemini", "skills"),
|
|
30
35
|
path.join(cwd, ".gemini", "antigravity", "skills"),
|
|
31
36
|
path.join(cwd, ".gemini", "antigravity-cli", "skills"),
|
|
32
|
-
path.join(home, ".config", "skillshare", "skills"),
|
|
33
37
|
path.join(home, ".codex", "skills"),
|
|
34
38
|
path.join(home, ".claude", "skills"),
|
|
39
|
+
path.join(home, ".config", "skillshare", "skills"),
|
|
35
40
|
path.join(home, ".gemini", "skills"),
|
|
36
41
|
path.join(home, ".gemini", "antigravity", "skills"),
|
|
37
42
|
path.join(home, ".gemini", "antigravity-cli", "skills")
|
|
@@ -197,10 +202,18 @@ function finalizeSkillScores(skills, limit, { minimumKeywordScore = 0.35 } = {})
|
|
|
197
202
|
keywordScore: rule.keywordScore,
|
|
198
203
|
score: Math.min(1, Number(rule.score || 0)),
|
|
199
204
|
embeddingScore: rule.embeddingScore,
|
|
205
|
+
relevancePriority: Number(rule.relevancePriority || 0),
|
|
206
|
+
rankScore: Math.min(1, Number(rule.score || 0)) + Number(rule.relevancePriority || 0) / 100,
|
|
200
207
|
reasons: rule.reasons || []
|
|
201
208
|
}))
|
|
202
|
-
.filter((skill) => Number(skill.keywordScore || 0) >= minimumKeywordScore
|
|
203
|
-
|
|
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));
|
|
204
217
|
const seen = new Set();
|
|
205
218
|
return ranked
|
|
206
219
|
.filter((skill) => {
|
|
@@ -259,10 +272,11 @@ function scoreSkillsByKeyword({ prompt, skills, projectHints = [] }) {
|
|
|
259
272
|
const nameHit = normalizedPrompt.includes(normalizedName);
|
|
260
273
|
const nameTokenHit = nameTokens.length > 1 && nameTokens.every((token) => promptTokens.has(token));
|
|
261
274
|
const scopeBonus = enriched.scope === "project" ? 0.08 : 0;
|
|
262
|
-
const intentBonus = skillIntentBonus(normalizedPrompt, enriched);
|
|
263
|
-
const
|
|
275
|
+
const intentBonus = skillIntentBonus(normalizedPrompt, enriched, projectTokens);
|
|
276
|
+
const relevancePriority = skillRelevancePriority(normalizedPrompt, enriched, projectTokens);
|
|
277
|
+
const domainEligible = isSkillDomainEligible(normalizedPrompt, enriched, projectTokens);
|
|
264
278
|
const matchScore = matches.reduce((sum, token) => sum + (SPECIALIZED_SKILL_TOKENS.has(token) ? 0.2 : 0.08), 0);
|
|
265
|
-
const projectBonus =
|
|
279
|
+
const projectBonus = intentBonus ? Math.min(0.16, projectMatches.length * 0.04) : 0;
|
|
266
280
|
const score = Math.min(1, (matches.length ? 0.25 + matchScore : 0) + projectBonus + intentBonus + (nameHit ? 0.2 : 0) + (nameTokenHit ? 0.18 : 0) + scopeBonus);
|
|
267
281
|
return {
|
|
268
282
|
id: `skill-${index + 1}`,
|
|
@@ -273,6 +287,7 @@ function scoreSkillsByKeyword({ prompt, skills, projectHints = [] }) {
|
|
|
273
287
|
content,
|
|
274
288
|
score,
|
|
275
289
|
keywordScore: score,
|
|
290
|
+
relevancePriority,
|
|
276
291
|
domainEligible,
|
|
277
292
|
reasons: [
|
|
278
293
|
...(matches.length ? [`keyword:${matches.slice(0, 4).join(",")}`] : []),
|
|
@@ -292,31 +307,191 @@ function filterSkillMatches(matches, { normalizedPrompt, enriched }) {
|
|
|
292
307
|
return matches.filter((token) => token !== "android" && token !== "ios");
|
|
293
308
|
}
|
|
294
309
|
|
|
295
|
-
function isSkillDomainEligible(normalizedPrompt, enriched) {
|
|
296
|
-
if (!/\beas\b/.test(normalizedPrompt)) return true;
|
|
310
|
+
function isSkillDomainEligible(normalizedPrompt, enriched, projectTokens = new Set()) {
|
|
297
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;
|
|
298
316
|
if (!/\b(android|ios)\b/.test(skillText)) return true;
|
|
299
317
|
return /\b(eas|expo|cicd)\b/.test(skillText);
|
|
300
318
|
}
|
|
301
319
|
|
|
302
|
-
function skillIntentBonus(normalizedPrompt, enriched) {
|
|
320
|
+
function skillIntentBonus(normalizedPrompt, enriched, projectTokens = new Set()) {
|
|
303
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
|
+
}
|
|
304
351
|
if (/\beas\b/.test(normalizedPrompt)
|
|
305
352
|
&& /\b(eas|expo)\b/.test(skillText)
|
|
306
353
|
&& /\b(cicd|workflow|workflows|build|deploy|deployment|pipeline|pipelines)\b/.test(skillText)) {
|
|
307
354
|
return 0.28;
|
|
308
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
|
+
}
|
|
309
364
|
return 0;
|
|
310
365
|
}
|
|
311
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
|
+
|
|
312
492
|
export function projectSkillHints({ cwd = process.cwd() } = {}) {
|
|
313
493
|
const hints = new Set();
|
|
314
|
-
const packagePaths =
|
|
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
|
-
}
|
|
494
|
+
const packagePaths = workspacePackagePaths(cwd);
|
|
320
495
|
|
|
321
496
|
for (const packagePath of packagePaths) {
|
|
322
497
|
const packageDir = path.dirname(packagePath);
|
|
@@ -324,6 +499,8 @@ export function projectSkillHints({ cwd = process.cwd() } = {}) {
|
|
|
324
499
|
addHintText(hints, JSON.stringify({
|
|
325
500
|
name: packageJson?.name,
|
|
326
501
|
description: packageJson?.description,
|
|
502
|
+
keywords: packageJson?.keywords || [],
|
|
503
|
+
scripts: packageJson?.scripts || {},
|
|
327
504
|
dependencies: Object.keys(packageJson?.dependencies || {}),
|
|
328
505
|
devDependencies: Object.keys(packageJson?.devDependencies || {})
|
|
329
506
|
}));
|
|
@@ -334,6 +511,48 @@ export function projectSkillHints({ cwd = process.cwd() } = {}) {
|
|
|
334
511
|
return [...hints];
|
|
335
512
|
}
|
|
336
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
|
+
|
|
337
556
|
function readJson(filePath) {
|
|
338
557
|
try {
|
|
339
558
|
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
@@ -366,5 +585,8 @@ function normalize(value) {
|
|
|
366
585
|
}
|
|
367
586
|
|
|
368
587
|
function normalizePrompt(value) {
|
|
369
|
-
return normalize(String(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"));
|
|
370
592
|
}
|