@minhpnq1807/contextos 0.5.44 → 0.5.46
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 +18 -0
- package/README.md +4 -4
- package/bin/ctx.js +29 -8
- 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/output-config.js +37 -2
- package/plugins/ctx/lib/prompt-hook.js +68 -9
- package/plugins/ctx/lib/scheduler.js +18 -3
- package/plugins/ctx/lib/setup-wizard.js +3 -1
- package/plugins/ctx/lib/skill-discoverer.js +296 -18
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.46
|
|
4
|
+
|
|
5
|
+
- **Configurable prompt suggestion limits:** `ctx --config` and interactive `ctx setup` now let users choose how many suggested files, skills, and workflows appear in prompt context. Defaults are five each, with caps of 20 files, 10 skills, and 5 workflows.
|
|
6
|
+
- **Limit-aware prompt hooks and debug:** `UserPromptSubmit` hooks, direct fallback scoring, the private `ctx-mcp` bridge request, and `ctx debug` now all honor the saved suggestion limits instead of using hard-coded counts.
|
|
7
|
+
- **Document authoring skill intent:** Prompts that create, edit, update, or maintain documents, workspace docs, README files, wiki pages, manuals, guides, specs, or ADRs now prioritize documentation skills such as `doc-coauthoring`, `documentation`, `docs-architect`, `readme`, and `wiki-page-writer`.
|
|
8
|
+
- **Safer document skill gating:** Document-processing and workspace-automation skills such as Azure Document Intelligence, DocuSign, Asana, Slack, Google Docs, and Notion no longer win generic document-writing prompts unless the provider or processing task is explicitly named.
|
|
9
|
+
- **Setup summary clarity:** The setup wizard summary now reports the saved prompt suggestion limits alongside the enabled prompt sections so users can review output volume immediately.
|
|
10
|
+
|
|
11
|
+
## 0.5.45
|
|
12
|
+
|
|
13
|
+
- **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`.
|
|
14
|
+
- **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.
|
|
15
|
+
- **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.
|
|
16
|
+
- **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.
|
|
17
|
+
- **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.
|
|
18
|
+
- **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.
|
|
19
|
+
- **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.
|
|
20
|
+
|
|
3
21
|
## 0.5.44
|
|
4
22
|
|
|
5
23
|
- **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/README.md
CHANGED
|
@@ -444,7 +444,7 @@ This warning comes from a transitive dependency in the local embedding/WASM stac
|
|
|
444
444
|
| `ctx sync --workflows --dry-run` | Previews workflow sync without writing files. | You want to inspect source workflows and target roots first. | Prints planned sync/index output and skips copying target files. |
|
|
445
445
|
| `ctx skills` | Installs community skill libraries. | You want curated skills without running the full setup wizard. | Opens the community installer, uses a portable shell on Windows/Linux/macOS, repairs unsafe skill symlinks, and syncs installed skills to selected agents. |
|
|
446
446
|
| `ctx embeddings warm -- "task"` | Prepares local semantic embedding caches. | First install, CI smoke checks, or after changing AGENTS.md/project files/skills/workflows. | Loads/downloads `Xenova/all-MiniLM-L6-v2` and writes rule, file-path, skill, and workflow vectors to `~/.ctx/contextos/embeddings.db`. |
|
|
447
|
-
| `ctx --config` | Opens an interactive
|
|
447
|
+
| `ctx --config` | Opens an interactive panel for prompt sections and suggestion limits. | You want to reduce ContextOS prompt output noise. | Toggles critical rules, suggested files, suggested skills, and suggested workflows globally under `~/.ctx/contextos/output-config.json`, then lets you set suggestion counts for files, skills, and workflows. |
|
|
448
448
|
| `ctx refresh` | Refreshes the active Codex marketplace plugin and rebuilds local indexes. | Local development updates or a stale file retrieval index. | Copies the current package to `$CODEX_HOME/marketplaces/contextos`, rebuilds file-path embeddings and import adjacency, and refreshes code-review-graph embeddings when available. |
|
|
449
449
|
| `ctx ruler -- <args>` | Forwards args to the installed `ruler` CLI. | You need native Ruler commands such as `init`, `apply`, or `revert`. | Preserves Ruler stdout/stderr and exit status. |
|
|
450
450
|
| `ctx skillshare -- <args>` | Forwards args to the installed `skillshare` CLI. | You need native skillshare commands such as `status`, `target list`, `doctor`, `push`, or `pull`. | Preserves skillshare stdout/stderr and exit status. |
|
|
@@ -515,13 +515,13 @@ Prompt scoring does not walk the repository for file candidates or import expans
|
|
|
515
515
|
|
|
516
516
|
If a prompt has no usable context candidates, the hook fails open without emitting an empty `hook context` block, records `emptyContextReason` in the workspace runtime file, and starts a detached `autowarm` rebuild with a cooldown. That background rebuild refreshes file vectors, skill/workflow vectors, import adjacency, and available code-review-graph node embeddings for the next prompt while keeping repository walking out of the current prompt hot path.
|
|
517
517
|
|
|
518
|
-
Use `ctx --config` to choose which prompt sections ContextOS injects. Interactive `ctx setup`
|
|
518
|
+
Use `ctx --config` to choose which prompt sections ContextOS injects and how many suggestions each section may show. Interactive `ctx setup` includes the same section picker and limit prompts, while `ctx setup --yes` keeps the current saved config for automation. The panel supports multiple selection with `Space` and persists the global choice in `~/.ctx/contextos/output-config.json`. Defaults are five suggested files, five skills, and five workflows; caps are 20 files, 10 skills, and 5 workflows. Disabling rules hides both critical and additional relevant rule sections; compliance metadata remains available for reports.
|
|
519
519
|
|
|
520
|
-
Injected prompt sections are intentionally compact: rules show only detected rule text, files show basenames without paths, skills show unique names as a comma-separated inline list without descriptions, and workflows show names with their agent chain. Stop hooks persist reports silently; run `ctx report` or `ctx evidence` when you want the detailed compliance output.
|
|
520
|
+
Injected prompt sections are intentionally compact: rules show only detected rule text, files show a comma-separated inline list of basenames without paths, skills show unique names as a comma-separated inline list without descriptions, and workflows show names with their agent chain. Stop hooks persist reports silently; run `ctx report` or `ctx evidence` when you want the detailed compliance output.
|
|
521
521
|
|
|
522
522
|
Codex may flatten newlines in its `UserPromptSubmit hook (completed)` preview. The injected `additionalContext` payload remains multiline; this is a Codex preview display limitation.
|
|
523
523
|
|
|
524
|
-
Skill ranking uses bounded project hints from root/workspace `package.json` files and known mobile config files such as `app.json`, `app.config.*`, and `eas.json`. This lets Expo/EAS tasks activate specialized skills without walking the source tree on every prompt.
|
|
524
|
+
Skill ranking uses bounded project hints from root/workspace `package.json` files and known mobile config files such as `app.json`, `app.config.*`, and `eas.json`. This lets Expo/EAS and MCP tasks activate specialized skills without walking the source tree on every prompt. Document-authoring prompts also get explicit intent handling for README, wiki, workspace documentation, guides, specs, and ADR work, while document-processing or workspace-automation providers only rank highly when the prompt actually names that provider or processing task.
|
|
525
525
|
|
|
526
526
|
After `ctx refresh`, ContextOS invalidates the private hook bridge socket so prompts fall back to direct scoring until Codex restarts the long-running `ctx-mcp` process. Hook clients also discard a same-inode socket if an older bridge revision is detected.
|
|
527
527
|
|
package/bin/ctx.js
CHANGED
|
@@ -34,7 +34,7 @@ import { scanSkills, warmSkillEmbeddings } from "../plugins/ctx/lib/skill-discov
|
|
|
34
34
|
import { parsePassthroughArgs, runPassthrough } from "../plugins/ctx/lib/passthrough.js";
|
|
35
35
|
import { parseAgentList, parseSetupArgs, setupSummaryLines } from "../plugins/ctx/lib/setup-wizard.js";
|
|
36
36
|
import { multiSelect } from "../plugins/ctx/lib/multi-select.js";
|
|
37
|
-
import { configureOutputSections, enabledOutputSectionsLabel, loadOutputConfig } from "../plugins/ctx/lib/output-config.js";
|
|
37
|
+
import { configureOutputSections, enabledOutputSectionsLabel, loadOutputConfig, outputConfigLimits, outputConfigLimitsLabel } from "../plugins/ctx/lib/output-config.js";
|
|
38
38
|
import { syncWorkflows, warmWorkflowEmbeddings } from "../plugins/ctx/lib/workflow-discoverer.js";
|
|
39
39
|
import { checkForUpdate } from "../plugins/ctx/lib/update-notifier.js";
|
|
40
40
|
import { fetchSkillsForAgents, printSkillRecommendations, getAllLibraries, getInstallCommands } from "../plugins/ctx/lib/skill-library.js";
|
|
@@ -586,17 +586,20 @@ function contextOSWorkspaceDataDir(cwd = process.cwd()) {
|
|
|
586
586
|
|
|
587
587
|
async function debug(task) {
|
|
588
588
|
const cwd = process.cwd();
|
|
589
|
+
const limits = outputConfigLimits(loadOutputConfig({ dataRoot: contextOSDataDir() }));
|
|
589
590
|
const scored = await scoreContext({
|
|
590
591
|
cwd,
|
|
591
592
|
prompt: task,
|
|
592
593
|
dataDir: contextOSDataDir(),
|
|
593
|
-
maxFiles:
|
|
594
|
+
maxFiles: limits.files,
|
|
595
|
+
maxSkills: limits.skills,
|
|
596
|
+
maxWorkflows: limits.workflows,
|
|
594
597
|
embeddingTimeoutMs: Number(process.env.CONTEXTOS_EMBEDDING_DEBUG_TIMEOUT_MS || 5000)
|
|
595
598
|
});
|
|
596
599
|
const rules = scored.scoredRules;
|
|
597
|
-
const relevantFiles = scored.suggestedFiles.slice(0,
|
|
598
|
-
const suggestedSkills = (scored.suggestedSkills || []).slice(0,
|
|
599
|
-
const suggestedWorkflows = (scored.suggestedWorkflows || []).slice(0,
|
|
600
|
+
const relevantFiles = scored.suggestedFiles.slice(0, limits.files);
|
|
601
|
+
const suggestedSkills = (scored.suggestedSkills || []).slice(0, limits.skills);
|
|
602
|
+
const suggestedWorkflows = (scored.suggestedWorkflows || []).slice(0, limits.workflows);
|
|
600
603
|
const scheduled = scheduleContext({ rules, relevantFiles, suggestedSkills, suggestedWorkflows });
|
|
601
604
|
|
|
602
605
|
console.log("ContextOS debug");
|
|
@@ -727,6 +730,21 @@ async function askSetupYesNo(rl, question, defaultValue = true) {
|
|
|
727
730
|
return !/^n(o)?$/i.test(answer.trim());
|
|
728
731
|
}
|
|
729
732
|
|
|
733
|
+
async function askOutputLimit({ option, currentValue }) {
|
|
734
|
+
if (!process.stdin.isTTY) return currentValue;
|
|
735
|
+
const rl = readline.createInterface({ input, output });
|
|
736
|
+
try {
|
|
737
|
+
const answer = await rl.question(`◇ ${option.label} limit (0-${option.max}, current ${currentValue}): `);
|
|
738
|
+
const trimmed = answer.trim();
|
|
739
|
+
if (!trimmed) return currentValue;
|
|
740
|
+
const value = Number(trimmed);
|
|
741
|
+
if (!Number.isFinite(value)) return currentValue;
|
|
742
|
+
return Math.max(0, Math.min(option.max, Math.trunc(value)));
|
|
743
|
+
} finally {
|
|
744
|
+
rl.close();
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
730
748
|
async function setup({ args = [], cwd = process.cwd() } = {}) {
|
|
731
749
|
const options = parseSetupArgs(args);
|
|
732
750
|
const interactive = !options.yes && process.stdin.isTTY;
|
|
@@ -776,7 +794,8 @@ async function setup({ args = [], cwd = process.cwd() } = {}) {
|
|
|
776
794
|
console.log("◇ Configure prompt output:");
|
|
777
795
|
outputConfig = await configureOutputSections({
|
|
778
796
|
dataRoot: contextOSDataDir(),
|
|
779
|
-
select: multiSelect
|
|
797
|
+
select: multiSelect,
|
|
798
|
+
askLimit: askOutputLimit
|
|
780
799
|
});
|
|
781
800
|
}
|
|
782
801
|
|
|
@@ -785,7 +804,8 @@ async function setup({ args = [], cwd = process.cwd() } = {}) {
|
|
|
785
804
|
for (const line of setupSummaryLines({
|
|
786
805
|
cwd,
|
|
787
806
|
...options,
|
|
788
|
-
promptSections: enabledOutputSectionsLabel(outputConfig)
|
|
807
|
+
promptSections: enabledOutputSectionsLabel(outputConfig),
|
|
808
|
+
promptLimits: outputConfigLimitsLabel(outputConfig)
|
|
789
809
|
})) console.log(`│ ${line}`);
|
|
790
810
|
console.log("");
|
|
791
811
|
|
|
@@ -870,7 +890,8 @@ try {
|
|
|
870
890
|
} else if (command === "--config" || command === "config") {
|
|
871
891
|
await configureOutputSections({
|
|
872
892
|
dataRoot: contextOSDataDir(),
|
|
873
|
-
select: multiSelect
|
|
893
|
+
select: multiSelect,
|
|
894
|
+
askLimit: askOutputLimit
|
|
874
895
|
});
|
|
875
896
|
} else if (command === "install") {
|
|
876
897
|
const copy = args.includes("--copy");
|
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) {
|
|
@@ -13,9 +13,16 @@ export const OUTPUT_SECTION_OPTIONS = [
|
|
|
13
13
|
{ value: "workflows", label: "Suggested workflow for this task", hint: "Include matching workflow recommendations." }
|
|
14
14
|
];
|
|
15
15
|
|
|
16
|
+
export const OUTPUT_LIMIT_OPTIONS = [
|
|
17
|
+
{ value: "files", label: "Suggested files", defaultValue: 5, max: 20 },
|
|
18
|
+
{ value: "skills", label: "Suggested skills", defaultValue: 5, max: 10 },
|
|
19
|
+
{ value: "workflows", label: "Suggested workflows", defaultValue: 5, max: 5 }
|
|
20
|
+
];
|
|
21
|
+
|
|
16
22
|
export function defaultOutputConfig() {
|
|
17
23
|
return {
|
|
18
|
-
sections: Object.fromEntries(OUTPUT_SECTION_OPTIONS.map((option) => [option.value, true]))
|
|
24
|
+
sections: Object.fromEntries(OUTPUT_SECTION_OPTIONS.map((option) => [option.value, true])),
|
|
25
|
+
limits: Object.fromEntries(OUTPUT_LIMIT_OPTIONS.map((option) => [option.value, option.defaultValue]))
|
|
19
26
|
};
|
|
20
27
|
}
|
|
21
28
|
|
|
@@ -49,9 +56,19 @@ export function enabledOutputSectionsLabel(config = loadOutputConfig()) {
|
|
|
49
56
|
return enabled.length ? enabled.join(", ") : "(none)";
|
|
50
57
|
}
|
|
51
58
|
|
|
59
|
+
export function outputConfigLimits(config = loadOutputConfig()) {
|
|
60
|
+
return normalizeOutputConfig(config).limits;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function outputConfigLimitsLabel(config = loadOutputConfig()) {
|
|
64
|
+
const limits = outputConfigLimits(config);
|
|
65
|
+
return OUTPUT_LIMIT_OPTIONS.map((option) => `${option.value}: ${limits[option.value]}`).join(", ");
|
|
66
|
+
}
|
|
67
|
+
|
|
52
68
|
export async function configureOutputSections({
|
|
53
69
|
dataRoot = defaultDataRoot(),
|
|
54
70
|
select,
|
|
71
|
+
askLimit,
|
|
55
72
|
logger = console.log
|
|
56
73
|
} = {}) {
|
|
57
74
|
if (typeof select !== "function") throw new Error("configureOutputSections requires a multi-select function");
|
|
@@ -64,11 +81,19 @@ export async function configureOutputSections({
|
|
|
64
81
|
}))
|
|
65
82
|
});
|
|
66
83
|
const selectedSet = new Set(selected);
|
|
84
|
+
const limits = {};
|
|
85
|
+
for (const option of OUTPUT_LIMIT_OPTIONS) {
|
|
86
|
+
limits[option.value] = typeof askLimit === "function"
|
|
87
|
+
? await askLimit({ option, currentValue: current.limits[option.value] })
|
|
88
|
+
: current.limits[option.value];
|
|
89
|
+
}
|
|
67
90
|
const saved = saveOutputConfig({
|
|
68
|
-
sections: Object.fromEntries(OUTPUT_SECTION_OPTIONS.map((option) => [option.value, selectedSet.has(option.value)]))
|
|
91
|
+
sections: Object.fromEntries(OUTPUT_SECTION_OPTIONS.map((option) => [option.value, selectedSet.has(option.value)])),
|
|
92
|
+
limits
|
|
69
93
|
}, { dataRoot });
|
|
70
94
|
logger(`│ Saved ContextOS prompt section config: ${outputConfigPath(dataRoot)}`);
|
|
71
95
|
logger(`│ Enabled sections: ${enabledOutputSectionsLabel(saved)}`);
|
|
96
|
+
logger(`│ Suggest limits: ${outputConfigLimitsLabel(saved)}`);
|
|
72
97
|
return saved;
|
|
73
98
|
}
|
|
74
99
|
|
|
@@ -80,6 +105,16 @@ function normalizeOutputConfig(config = {}) {
|
|
|
80
105
|
typeof config.sections?.[option.value] === "boolean"
|
|
81
106
|
? config.sections[option.value]
|
|
82
107
|
: defaults.sections[option.value]
|
|
108
|
+
])),
|
|
109
|
+
limits: Object.fromEntries(OUTPUT_LIMIT_OPTIONS.map((option) => [
|
|
110
|
+
option.value,
|
|
111
|
+
normalizeLimit(config.limits?.[option.value], option)
|
|
83
112
|
]))
|
|
84
113
|
};
|
|
85
114
|
}
|
|
115
|
+
|
|
116
|
+
function normalizeLimit(value, option) {
|
|
117
|
+
const number = Number(value);
|
|
118
|
+
if (!Number.isFinite(number)) return option.defaultValue;
|
|
119
|
+
return Math.max(0, Math.min(option.max, Math.trunc(number)));
|
|
120
|
+
}
|
|
@@ -3,8 +3,9 @@ import { appendJsonLine, writeJsonFile } from "./fs-utils.js";
|
|
|
3
3
|
import { maybeAutoWarmWorkspace } from "./auto-warm.js";
|
|
4
4
|
import { callCtxScoreContext } from "./ctx-mcp-client.js";
|
|
5
5
|
import { resolveHookCwd } from "./hook-io.js";
|
|
6
|
-
import { loadOutputConfig } from "./output-config.js";
|
|
6
|
+
import { loadOutputConfig, outputConfigLimits } 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
|
|
|
10
11
|
export async function handlePromptPayload(
|
|
@@ -24,9 +25,12 @@ export async function handlePromptPayload(
|
|
|
24
25
|
} = {}
|
|
25
26
|
) {
|
|
26
27
|
const prompt = payload.prompt || payload.message || payload.user_prompt || "";
|
|
27
|
-
const
|
|
28
|
+
const hookCwd = resolveHookCwd(payload);
|
|
29
|
+
const cwd = resolvePromptTargetCwd({ cwd: hookCwd, prompt });
|
|
28
30
|
const openFiles = payload.openFiles || payload.open_files || payload.files || [];
|
|
29
31
|
const dataDir = dataPath ? path.dirname(dataPath) : undefined;
|
|
32
|
+
const effectiveOutputConfig = outputConfig || loadOutputConfig();
|
|
33
|
+
const promptLimits = outputConfigLimits(effectiveOutputConfig);
|
|
30
34
|
|
|
31
35
|
let scored;
|
|
32
36
|
try {
|
|
@@ -34,7 +38,9 @@ export async function handlePromptPayload(
|
|
|
34
38
|
cwd,
|
|
35
39
|
prompt,
|
|
36
40
|
openFiles,
|
|
37
|
-
maxFiles:
|
|
41
|
+
maxFiles: promptLimits.files,
|
|
42
|
+
maxSkills: promptLimits.skills,
|
|
43
|
+
maxWorkflows: promptLimits.workflows
|
|
38
44
|
}, {
|
|
39
45
|
dataDir: mcpDataDir || dataDir,
|
|
40
46
|
timeoutMs: Number(process.env.CONTEXTOS_MCP_BRIDGE_TIMEOUT_MS || 2000)
|
|
@@ -45,10 +51,12 @@ export async function handlePromptPayload(
|
|
|
45
51
|
cwd,
|
|
46
52
|
prompt,
|
|
47
53
|
openFiles,
|
|
48
|
-
maxFiles:
|
|
54
|
+
maxFiles: promptLimits.files,
|
|
55
|
+
maxSkills: promptLimits.skills,
|
|
56
|
+
maxWorkflows: promptLimits.workflows,
|
|
49
57
|
dataDir: mcpDataDir || dataDir,
|
|
50
58
|
embeddingTimeoutMs: Number(process.env.CONTEXTOS_HOOK_EMBEDDING_TIMEOUT_MS || 500),
|
|
51
|
-
fileEmbeddingTimeoutMs: Number(process.env.CONTEXTOS_HOOK_FILE_EMBEDDING_TIMEOUT_MS ||
|
|
59
|
+
fileEmbeddingTimeoutMs: Number(process.env.CONTEXTOS_HOOK_FILE_EMBEDDING_TIMEOUT_MS || 1000)
|
|
52
60
|
}), directFallbackTimeoutMs, "direct fallback scoring");
|
|
53
61
|
scored.telemetry = {
|
|
54
62
|
...(scored.telemetry || {}),
|
|
@@ -66,10 +74,9 @@ export async function handlePromptPayload(
|
|
|
66
74
|
|
|
67
75
|
if (scored.error) throw new Error(scored.error);
|
|
68
76
|
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,
|
|
72
|
-
const effectiveOutputConfig = outputConfig || loadOutputConfig();
|
|
77
|
+
const relevantFiles = (scored.suggestedFiles || []).slice(0, promptLimits.files);
|
|
78
|
+
const suggestedSkills = (scored.suggestedSkills || []).slice(0, promptLimits.skills);
|
|
79
|
+
const suggestedWorkflows = (scored.suggestedWorkflows || []).slice(0, promptLimits.workflows);
|
|
73
80
|
const scheduled = scheduleContext({ rules: scoredRules, relevantFiles, suggestedSkills, suggestedWorkflows, outputConfig: effectiveOutputConfig });
|
|
74
81
|
const contextEmptyReason = emptyContextReason({ scheduled, outputConfig: effectiveOutputConfig, injectContext });
|
|
75
82
|
const autoWarm = autoWarmWorkspace({
|
|
@@ -127,6 +134,58 @@ export async function handlePromptPayload(
|
|
|
127
134
|
return output;
|
|
128
135
|
}
|
|
129
136
|
|
|
137
|
+
export function resolvePromptTargetCwd({ cwd = process.cwd(), prompt = "" } = {}) {
|
|
138
|
+
const current = path.resolve(cwd);
|
|
139
|
+
const candidates = targetPathCandidates(prompt);
|
|
140
|
+
for (const candidate of candidates) {
|
|
141
|
+
const resolved = path.resolve(current, candidate);
|
|
142
|
+
if (!isAllowedTargetCwd({ current, resolved })) continue;
|
|
143
|
+
if (isWorkspaceRoot(resolved)) return resolved;
|
|
144
|
+
}
|
|
145
|
+
return current;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function targetPathCandidates(prompt) {
|
|
149
|
+
const text = String(prompt || "");
|
|
150
|
+
const patterns = [
|
|
151
|
+
/\b(?:tr[eê]n|in|inside|under|repo|workspace|cwd)\s+([.~A-Za-z0-9_/@.-]+(?:\/[A-Za-z0-9_@().-]+)*)/gi,
|
|
152
|
+
/\b(?:debug|test|check|run)\s+(?:on|tr[eê]n)\s+([.~A-Za-z0-9_/@.-]+(?:\/[A-Za-z0-9_@().-]+)*)/gi
|
|
153
|
+
];
|
|
154
|
+
const results = [];
|
|
155
|
+
for (const pattern of patterns) {
|
|
156
|
+
let match;
|
|
157
|
+
while ((match = pattern.exec(text))) {
|
|
158
|
+
const value = cleanPromptPath(match[1]);
|
|
159
|
+
if (value) results.push(value);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return results;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function cleanPromptPath(value) {
|
|
166
|
+
const cleaned = String(value || "").trim().replace(/[),.;:]+$/g, "");
|
|
167
|
+
if (!cleaned || cleaned.includes("://")) return null;
|
|
168
|
+
return cleaned;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function isAllowedTargetCwd({ current, resolved }) {
|
|
172
|
+
const parent = path.dirname(current);
|
|
173
|
+
const relative = path.relative(parent, resolved);
|
|
174
|
+
return relative && !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function isWorkspaceRoot(directory) {
|
|
178
|
+
try {
|
|
179
|
+
const stat = fs.statSync(directory);
|
|
180
|
+
if (!stat.isDirectory()) return false;
|
|
181
|
+
} catch {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
return fs.existsSync(path.join(directory, "package.json"))
|
|
185
|
+
|| fs.existsSync(path.join(directory, "AGENTS.md"))
|
|
186
|
+
|| fs.existsSync(path.join(directory, ".git"));
|
|
187
|
+
}
|
|
188
|
+
|
|
130
189
|
function emptyContextReason({ scheduled, outputConfig, injectContext }) {
|
|
131
190
|
if (!injectContext) return "injection-disabled";
|
|
132
191
|
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) {
|
|
@@ -37,7 +37,8 @@ export function setupSummaryLines({
|
|
|
37
37
|
agents = DEFAULT_AGENTS,
|
|
38
38
|
syncRules = true,
|
|
39
39
|
syncSkills = true,
|
|
40
|
-
promptSections = null
|
|
40
|
+
promptSections = null,
|
|
41
|
+
promptLimits = null
|
|
41
42
|
} = {}) {
|
|
42
43
|
const lines = [
|
|
43
44
|
`Installation directory: ${cwd}`,
|
|
@@ -47,5 +48,6 @@ export function setupSummaryLines({
|
|
|
47
48
|
`skillshare skill sync: ${syncSkills ? "enabled" : "skipped"}`
|
|
48
49
|
];
|
|
49
50
|
if (promptSections !== null) lines.push(`Prompt sections shown: ${promptSections}`);
|
|
51
|
+
if (promptLimits !== null) lines.push(`Prompt suggest limits: ${promptLimits}`);
|
|
50
52
|
return lines;
|
|
51
53
|
}
|
|
@@ -14,10 +14,16 @@ 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", "
|
|
23
|
+
"android", "architecture", "authorization", "cicd", "documentation", "docs", "document",
|
|
24
|
+
"eas", "expo", "frontend", "ios", "next", "nextjs", "mcp", "modelcontextprotocol",
|
|
25
|
+
"postgres", "postgresql", "react", "react-native", "readme", "tailwind", "typescript",
|
|
26
|
+
"ui", "wiki", "writer"
|
|
21
27
|
]);
|
|
22
28
|
|
|
23
29
|
const scanCache = new Map();
|
|
@@ -29,9 +35,9 @@ export function skillSearchRoots({ cwd = process.cwd(), home = os.homedir() } =
|
|
|
29
35
|
path.join(cwd, ".gemini", "skills"),
|
|
30
36
|
path.join(cwd, ".gemini", "antigravity", "skills"),
|
|
31
37
|
path.join(cwd, ".gemini", "antigravity-cli", "skills"),
|
|
32
|
-
path.join(home, ".config", "skillshare", "skills"),
|
|
33
38
|
path.join(home, ".codex", "skills"),
|
|
34
39
|
path.join(home, ".claude", "skills"),
|
|
40
|
+
path.join(home, ".config", "skillshare", "skills"),
|
|
35
41
|
path.join(home, ".gemini", "skills"),
|
|
36
42
|
path.join(home, ".gemini", "antigravity", "skills"),
|
|
37
43
|
path.join(home, ".gemini", "antigravity-cli", "skills")
|
|
@@ -197,10 +203,18 @@ function finalizeSkillScores(skills, limit, { minimumKeywordScore = 0.35 } = {})
|
|
|
197
203
|
keywordScore: rule.keywordScore,
|
|
198
204
|
score: Math.min(1, Number(rule.score || 0)),
|
|
199
205
|
embeddingScore: rule.embeddingScore,
|
|
206
|
+
relevancePriority: Number(rule.relevancePriority || 0),
|
|
207
|
+
rankScore: Math.min(1, Number(rule.score || 0)) + Number(rule.relevancePriority || 0) / 100,
|
|
200
208
|
reasons: rule.reasons || []
|
|
201
209
|
}))
|
|
202
|
-
.filter((skill) => Number(skill.keywordScore || 0) >= minimumKeywordScore
|
|
203
|
-
|
|
210
|
+
.filter((skill) => Number(skill.keywordScore || 0) >= minimumKeywordScore
|
|
211
|
+
|| Number(skill.embeddingScore || 0) >= 0.62
|
|
212
|
+
|| Number(skill.relevancePriority || 0) >= 50)
|
|
213
|
+
.sort((a, b) => b.rankScore - a.rankScore
|
|
214
|
+
|| b.relevancePriority - a.relevancePriority
|
|
215
|
+
|| b.score - a.score
|
|
216
|
+
|| scopePriority(b.scope) - scopePriority(a.scope)
|
|
217
|
+
|| a.name.localeCompare(b.name));
|
|
204
218
|
const seen = new Set();
|
|
205
219
|
return ranked
|
|
206
220
|
.filter((skill) => {
|
|
@@ -259,10 +273,11 @@ function scoreSkillsByKeyword({ prompt, skills, projectHints = [] }) {
|
|
|
259
273
|
const nameHit = normalizedPrompt.includes(normalizedName);
|
|
260
274
|
const nameTokenHit = nameTokens.length > 1 && nameTokens.every((token) => promptTokens.has(token));
|
|
261
275
|
const scopeBonus = enriched.scope === "project" ? 0.08 : 0;
|
|
262
|
-
const intentBonus = skillIntentBonus(normalizedPrompt, enriched);
|
|
263
|
-
const
|
|
276
|
+
const intentBonus = skillIntentBonus(normalizedPrompt, enriched, projectTokens);
|
|
277
|
+
const relevancePriority = skillRelevancePriority(normalizedPrompt, enriched, projectTokens);
|
|
278
|
+
const domainEligible = isSkillDomainEligible(normalizedPrompt, enriched, projectTokens);
|
|
264
279
|
const matchScore = matches.reduce((sum, token) => sum + (SPECIALIZED_SKILL_TOKENS.has(token) ? 0.2 : 0.08), 0);
|
|
265
|
-
const projectBonus =
|
|
280
|
+
const projectBonus = intentBonus ? Math.min(0.16, projectMatches.length * 0.04) : 0;
|
|
266
281
|
const score = Math.min(1, (matches.length ? 0.25 + matchScore : 0) + projectBonus + intentBonus + (nameHit ? 0.2 : 0) + (nameTokenHit ? 0.18 : 0) + scopeBonus);
|
|
267
282
|
return {
|
|
268
283
|
id: `skill-${index + 1}`,
|
|
@@ -273,6 +288,7 @@ function scoreSkillsByKeyword({ prompt, skills, projectHints = [] }) {
|
|
|
273
288
|
content,
|
|
274
289
|
score,
|
|
275
290
|
keywordScore: score,
|
|
291
|
+
relevancePriority,
|
|
276
292
|
domainEligible,
|
|
277
293
|
reasons: [
|
|
278
294
|
...(matches.length ? [`keyword:${matches.slice(0, 4).join(",")}`] : []),
|
|
@@ -292,31 +308,246 @@ function filterSkillMatches(matches, { normalizedPrompt, enriched }) {
|
|
|
292
308
|
return matches.filter((token) => token !== "android" && token !== "ios");
|
|
293
309
|
}
|
|
294
310
|
|
|
295
|
-
function isSkillDomainEligible(normalizedPrompt, enriched) {
|
|
296
|
-
if (!/\beas\b/.test(normalizedPrompt)) return true;
|
|
311
|
+
function isSkillDomainEligible(normalizedPrompt, enriched, projectTokens = new Set()) {
|
|
297
312
|
const skillText = normalize(`${enriched.name} ${enriched.description}`);
|
|
313
|
+
if (isMcpSkill(skillText) && !isMcpRelevantTask(normalizedPrompt, projectTokens)) return false;
|
|
314
|
+
if (isOffensiveSecuritySkill(skillText) && !isSecurityTask(normalizedPrompt)) return false;
|
|
315
|
+
if (isPlatformCommerceSkill(skillText) && !isPlatformCommerceTask(normalizedPrompt, skillText)) return false;
|
|
316
|
+
if (isDocumentProcessingSkill(skillText) && !isDocumentProcessingTask(normalizedPrompt, skillText)) return false;
|
|
317
|
+
if (isWorkspaceAutomationSkill(skillText) && !isWorkspaceAutomationTask(normalizedPrompt, skillText)) return false;
|
|
318
|
+
if (!/\beas\b/.test(normalizedPrompt)) return true;
|
|
298
319
|
if (!/\b(android|ios)\b/.test(skillText)) return true;
|
|
299
320
|
return /\b(eas|expo|cicd)\b/.test(skillText);
|
|
300
321
|
}
|
|
301
322
|
|
|
302
|
-
function skillIntentBonus(normalizedPrompt, enriched) {
|
|
323
|
+
function skillIntentBonus(normalizedPrompt, enriched, projectTokens = new Set()) {
|
|
303
324
|
const skillText = normalize(`${enriched.name} ${enriched.description}`);
|
|
325
|
+
if (isDocumentAuthoringTask(normalizedPrompt)
|
|
326
|
+
&& /\b(documentation|document|docs|doc|readme|wiki|writer|writing|coauthor|technical documentation|architecture documentation|onboarding|office productivity)\b/.test(skillText)) {
|
|
327
|
+
return 0.48;
|
|
328
|
+
}
|
|
329
|
+
if (isMcpRelevantTask(normalizedPrompt, projectTokens)
|
|
330
|
+
&& /\b(mcp|model context protocol|modelcontextprotocol|agent memory|tool developer|tool builder)\b/.test(skillText)) {
|
|
331
|
+
return 0.48;
|
|
332
|
+
}
|
|
333
|
+
if (isCommerceTask(normalizedPrompt)
|
|
334
|
+
&& /\b(payment|payments|checkout|billing|bill|invoice|wallet|balance|stripe|paypal|commerce|monetization)\b/.test(skillText)) {
|
|
335
|
+
return 0.46;
|
|
336
|
+
}
|
|
337
|
+
if (isContentAccessTask(normalizedPrompt)
|
|
338
|
+
&& /\b(api|endpoint|backend|service|services|auth|authorization|permission|permissions|access|rbac|frontend api)\b/.test(skillText)) {
|
|
339
|
+
return 0.34;
|
|
340
|
+
}
|
|
341
|
+
if (isNotificationTask(normalizedPrompt)
|
|
342
|
+
&& /\b(notification|notifications|notify|message|sms|email|event|webhook)\b/.test(skillText)) {
|
|
343
|
+
return 0.3;
|
|
344
|
+
}
|
|
345
|
+
if (isFrontendCheckoutTask(normalizedPrompt)
|
|
346
|
+
&& /\b(frontend|react|next|nextjs|ui|component|modal|api integration)\b/.test(skillText)) {
|
|
347
|
+
return 0.32;
|
|
348
|
+
}
|
|
349
|
+
if (isExpoRuntimeTask(normalizedPrompt, projectTokens)
|
|
350
|
+
&& /\b(expo|eas|nativewind|react native|tailwind)\b/.test(skillText)) {
|
|
351
|
+
return 0.46;
|
|
352
|
+
}
|
|
353
|
+
if (isNextAppRouterTask(normalizedPrompt)
|
|
354
|
+
&& /\b(next|nextjs)\b/.test(skillText)
|
|
355
|
+
&& /\b(app router|router|routing|server components)\b/.test(skillText)) {
|
|
356
|
+
return 0.5;
|
|
357
|
+
}
|
|
304
358
|
if (/\beas\b/.test(normalizedPrompt)
|
|
305
359
|
&& /\b(eas|expo)\b/.test(skillText)
|
|
306
360
|
&& /\b(cicd|workflow|workflows|build|deploy|deployment|pipeline|pipelines)\b/.test(skillText)) {
|
|
307
361
|
return 0.28;
|
|
308
362
|
}
|
|
363
|
+
if (/\b(webapp|frontend|ui|dashboard|button|page|component|app|router)\b/.test(normalizedPrompt)
|
|
364
|
+
&& /\b(frontend|react|next|nextjs|ui|component|tailwind|app router)\b/.test(skillText)) {
|
|
365
|
+
return 0.36;
|
|
366
|
+
}
|
|
367
|
+
if (/\b(role|admin|creator|permission|permissions|authorization|access)\b/.test(normalizedPrompt)
|
|
368
|
+
&& /\b(auth|authentication|authorization|permission|permissions|access|rbac)\b/.test(skillText)) {
|
|
369
|
+
return 0.32;
|
|
370
|
+
}
|
|
309
371
|
return 0;
|
|
310
372
|
}
|
|
311
373
|
|
|
374
|
+
function skillRelevancePriority(normalizedPrompt, enriched, projectTokens = new Set()) {
|
|
375
|
+
const skillText = normalize(`${enriched.name} ${enriched.description}`);
|
|
376
|
+
const skillName = normalize(enriched.name);
|
|
377
|
+
let priority = 0;
|
|
378
|
+
if (isDocumentAuthoringTask(normalizedPrompt)) {
|
|
379
|
+
if (skillName === "doc coauthoring") priority += 1300;
|
|
380
|
+
if (skillName === "documentation") priority += 720;
|
|
381
|
+
if (skillName === "docs architect") priority += 700;
|
|
382
|
+
if (skillName === "readme") priority += 660;
|
|
383
|
+
if (skillName === "wiki page writer") priority += 640;
|
|
384
|
+
if (skillName === "wiki architect") priority += 620;
|
|
385
|
+
if (skillName === "wiki onboarding") priority += 600;
|
|
386
|
+
if (skillName === "writer" || skillName === "docx" || skillName === "office productivity") priority += 560;
|
|
387
|
+
if (skillName === "agents md") priority += 420;
|
|
388
|
+
if (/\b(code documentation doc generate|documentation generation doc generate|api documentation|api documenter|reference builder|architecture)\b/.test(skillText)) priority += 320;
|
|
389
|
+
if (/\b(documentation|document|docs|doc|readme|wiki|writer|writing|coauthor|technical documentation)\b/.test(skillText)) priority += 130;
|
|
390
|
+
if (/\b(mcp|model context protocol|metasploit|penetration|exploit)\b/.test(skillText)) priority -= 220;
|
|
391
|
+
}
|
|
392
|
+
if (isMcpRelevantTask(normalizedPrompt, projectTokens)) {
|
|
393
|
+
if (skillName === "mcp builder") priority += 760;
|
|
394
|
+
if (skillName === "mcp management") priority += 740;
|
|
395
|
+
if (skillName === "mcp tool developer") priority += 720;
|
|
396
|
+
if (skillName === "agent memory mcp") priority += 700;
|
|
397
|
+
if (skillName === "agent tool builder" || skillName === "context agent") priority += 260;
|
|
398
|
+
if (/\b(mcp|model context protocol|modelcontextprotocol)\b/.test(skillText)) priority += 160;
|
|
399
|
+
}
|
|
400
|
+
if (isCommerceTask(normalizedPrompt)) {
|
|
401
|
+
if (/\b(payment integration|stripe integration|paypal integration)\b/.test(skillText)) priority += 520;
|
|
402
|
+
if (/\bbilling automation\b/.test(skillText)) priority += 430;
|
|
403
|
+
if (/\b(payment|payments|checkout|billing|wallet|balance|stripe|paypal|commerce|monetization)\b/.test(skillText)) priority += 160;
|
|
404
|
+
if (!/\bstripe\b/.test(normalizedPrompt) && /\bstripe\b/.test(skillText)) priority -= 520;
|
|
405
|
+
if (!/\bpaypal\b/.test(normalizedPrompt) && /\bpaypal\b/.test(skillText)) priority -= 520;
|
|
406
|
+
if (!/\bsquare\b/.test(normalizedPrompt) && /\bsquare\b/.test(skillText)) priority -= 440;
|
|
407
|
+
if (/\b(mcp|metasploit|penetration|exploit|bug bounty)\b/.test(skillText)) priority -= 500;
|
|
408
|
+
}
|
|
409
|
+
if (isContentAccessTask(normalizedPrompt)) {
|
|
410
|
+
if (/\b(api endpoint builder|backend development|backend architect|frontend api integration patterns)\b/.test(skillText)) priority += 260;
|
|
411
|
+
if (/\b(auth implementation patterns|authorization|permission|permissions|access|rbac)\b/.test(skillText)) priority += 120;
|
|
412
|
+
}
|
|
413
|
+
if (isNotificationTask(normalizedPrompt)) {
|
|
414
|
+
if (/\bsendblue notify\b/.test(skillText)) priority += 140;
|
|
415
|
+
if (/\b(notification|notifications|notify|message|sms|email|event|webhook)\b/.test(skillText)) priority += 90;
|
|
416
|
+
}
|
|
417
|
+
if (isFrontendCheckoutTask(normalizedPrompt)) {
|
|
418
|
+
if (/\bfrontend api integration patterns\b/.test(skillText)) priority += 220;
|
|
419
|
+
if (/\breact nextjs development|nextjs best practices|nextjs app router patterns|frontend developer\b/.test(skillText)) priority += 90;
|
|
420
|
+
}
|
|
421
|
+
if (isExpoRuntimeTask(normalizedPrompt, projectTokens)) {
|
|
422
|
+
if (/\bexpo deployment\b/.test(skillText)) priority += 900;
|
|
423
|
+
if (/\bbuilding native ui\b/.test(skillText)) priority += 760;
|
|
424
|
+
if (/\bexpo tailwind setup\b/.test(skillText)) priority += 620;
|
|
425
|
+
if (/\bexpo\b/.test(skillText) && /\b(qr|expo go|run|running|start|connect|eas|deployment|build)\b/.test(skillText)) priority += 220;
|
|
426
|
+
if (/\bnativewind|tailwind\b/.test(skillText) && projectTokens.has("nativewind")) priority += 120;
|
|
427
|
+
if (/\b(next|nextjs|frontend designer|dark themed|glassmorphism|framer motion)\b/.test(skillText)) priority -= 160;
|
|
428
|
+
}
|
|
429
|
+
if (isNextAppRouterTask(normalizedPrompt)) {
|
|
430
|
+
if (/\bnextjs app router patterns\b/.test(skillText)) priority += 600;
|
|
431
|
+
if (/\bnextjs best practices\b/.test(skillText)) priority += 560;
|
|
432
|
+
if (/\breact nextjs development\b/.test(skillText)) priority += 420;
|
|
433
|
+
if (/\b(next|nextjs)\b/.test(skillText) && /\b(app router|router|routing|server components)\b/.test(skillText)) priority += 100;
|
|
434
|
+
if (/\b(next|nextjs)\b/.test(skillText) && /\breact\b/.test(skillText)) priority += 70;
|
|
435
|
+
if (/\b(glassmorphism|dark themed|dark theme|framer motion)\b/.test(skillText)) priority -= 40;
|
|
436
|
+
}
|
|
437
|
+
if (/\b(role|admin|creator|permission|permissions|authorization|access)\b/.test(normalizedPrompt)
|
|
438
|
+
&& /\b(auth|authentication|authorization|permission|permissions|access|rbac)\b/.test(skillText)) {
|
|
439
|
+
priority += 55;
|
|
440
|
+
}
|
|
441
|
+
return priority;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function isNextAppRouterTask(normalizedPrompt) {
|
|
445
|
+
return /\bwebapp\b.*\bsrc\b.*\bapp\b/.test(normalizedPrompt)
|
|
446
|
+
|| /\b(next|nextjs)\b.*\b(app router|router|routing)\b/.test(normalizedPrompt)
|
|
447
|
+
|| /\bapp router\b/.test(normalizedPrompt);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function isExpoRuntimeTask(normalizedPrompt, projectTokens = new Set()) {
|
|
451
|
+
const expoProject = projectTokens.has("expo") || projectTokens.has("nativewind") || projectTokens.has("eas");
|
|
452
|
+
if (!expoProject) return false;
|
|
453
|
+
return /\b(qr|connect|run|start|expo go|device|metro|tunnel|lan)\b/.test(normalizedPrompt);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function isCommerceTask(normalizedPrompt) {
|
|
457
|
+
return /\b(purchase|purchased|buy|buyer|seller|payment|pay|checkout|wallet|balance|top up|topup|funded|billing|invoice)\b/.test(normalizedPrompt);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function isContentAccessTask(normalizedPrompt) {
|
|
461
|
+
return /\b(content access service|content access|access permissions|grant access|permissions|library|resources|tutorials|collections)\b/.test(normalizedPrompt);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function isNotificationTask(normalizedPrompt) {
|
|
465
|
+
return /\b(notification|notifications|notify|buyer|seller)\b/.test(normalizedPrompt);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function isFrontendCheckoutTask(normalizedPrompt) {
|
|
469
|
+
return /\b(modal|display|show|checkout|library|frontend|webapp|page|button)\b/.test(normalizedPrompt);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function isDocumentAuthoringTask(normalizedPrompt) {
|
|
473
|
+
return /\b(create|write|edit|update|draft|generate|author|maintain|work on|produce)\b.*\b(document|documents|documentation|docs|doc|readme|wiki|workspace|workspaces|manual|guide|onboarding|spec|adr)\b/.test(normalizedPrompt)
|
|
474
|
+
|| /\b(document|documents|documentation|docs|doc|readme|wiki|workspace|workspaces|manual|guide|onboarding|spec|adr)\b.*\b(create|write|edit|update|draft|generate|author|maintain|work on|produce)\b/.test(normalizedPrompt);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function isMcpTask(normalizedPrompt) {
|
|
478
|
+
return /\b(mcp|model context protocol|tool server|tools server|server tool|bridge|proxy)\b/.test(normalizedPrompt);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function isMcpRelevantTask(normalizedPrompt, projectTokens = new Set()) {
|
|
482
|
+
return isMcpTask(normalizedPrompt)
|
|
483
|
+
|| (isMcpProject(projectTokens) && isContextRetrievalTask(normalizedPrompt));
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function isMcpProject(projectTokens = new Set()) {
|
|
487
|
+
return projectTokens.has("mcp") || projectTokens.has("modelcontextprotocol");
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function isContextRetrievalTask(normalizedPrompt) {
|
|
491
|
+
return /\b(suggest|suggested|suggestion|skills|files|context|retrieval|retrieve|scorer|scoring|match|matching|prompt|hook|inject|injection)\b/.test(normalizedPrompt);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function isSecurityTask(normalizedPrompt) {
|
|
495
|
+
return /\b(security|pentest|penetration|exploit|vulnerability|metasploit|bug bounty|owasp|xss|csrf|attack|audit)\b/.test(normalizedPrompt);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function isMcpSkill(skillText) {
|
|
499
|
+
return /\bmcp\b|\bmodel context protocol\b/.test(skillText);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function isOffensiveSecuritySkill(skillText) {
|
|
503
|
+
return /\b(metasploit|penetration testing|bug bounty|exploit|exploitation|privilege escalation|ethical hacking|web fuzzing|security assessment)\b/.test(skillText);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function isPlatformCommerceSkill(skillText) {
|
|
507
|
+
return /\b(wordpress|woocommerce|shopify|odoo)\b/.test(skillText);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function isPlatformCommerceTask(normalizedPrompt, skillText) {
|
|
511
|
+
if (/\bwordpress\b/.test(skillText)) return /\bwordpress\b/.test(normalizedPrompt);
|
|
512
|
+
if (/\bwoocommerce\b/.test(skillText)) return /\bwoocommerce\b/.test(normalizedPrompt);
|
|
513
|
+
if (/\bshopify\b/.test(skillText)) return /\bshopify\b/.test(normalizedPrompt);
|
|
514
|
+
if (/\bodoo\b/.test(skillText)) return /\bodoo\b/.test(normalizedPrompt);
|
|
515
|
+
return true;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function isDocumentProcessingSkill(skillText) {
|
|
519
|
+
return /\b(azure ai document|document intelligence|formrecognizer|document translation|cosmos db|azure cosmos|search documents|docusign)\b/.test(skillText);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function isDocumentProcessingTask(normalizedPrompt, skillText) {
|
|
523
|
+
if (/\bdocusign\b/.test(skillText)) return /\bdocusign|signature|envelope|sign\b/.test(normalizedPrompt);
|
|
524
|
+
if (/\bcosmos db|azure cosmos\b/.test(skillText)) return /\bcosmos|database|nosql|query|container\b/.test(normalizedPrompt);
|
|
525
|
+
if (/\bsearch documents\b/.test(skillText)) return /\bazure search|vector search|semantic search|index\b/.test(normalizedPrompt);
|
|
526
|
+
return /\bextract|ocr|analyze|translate|translation|form recognizer|document intelligence|azure\b/.test(normalizedPrompt);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function isWorkspaceAutomationSkill(skillText) {
|
|
530
|
+
return /\b(asana|bitbucket|slack|coda|google docs|google drive|google sheets|google slides|notion|telegram)\b/.test(skillText)
|
|
531
|
+
&& /\b(automation|automate|workspace|workspaces|manage docs|documents)\b/.test(skillText);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function isWorkspaceAutomationTask(normalizedPrompt, skillText) {
|
|
535
|
+
if (/\basana\b/.test(skillText)) return /\basana\b/.test(normalizedPrompt);
|
|
536
|
+
if (/\bbitbucket\b/.test(skillText)) return /\bbitbucket\b/.test(normalizedPrompt);
|
|
537
|
+
if (/\bslack\b/.test(skillText)) return /\bslack\b/.test(normalizedPrompt);
|
|
538
|
+
if (/\bcoda\b/.test(skillText)) return /\bcoda\b/.test(normalizedPrompt);
|
|
539
|
+
if (/\bgoogle docs\b/.test(skillText)) return /\bgoogle docs\b/.test(normalizedPrompt);
|
|
540
|
+
if (/\bgoogle drive\b/.test(skillText)) return /\bgoogle drive\b/.test(normalizedPrompt);
|
|
541
|
+
if (/\bgoogle sheets\b/.test(skillText)) return /\bgoogle sheets\b/.test(normalizedPrompt);
|
|
542
|
+
if (/\bgoogle slides\b/.test(skillText)) return /\bgoogle slides\b/.test(normalizedPrompt);
|
|
543
|
+
if (/\bnotion\b/.test(skillText)) return /\bnotion\b/.test(normalizedPrompt);
|
|
544
|
+
if (/\btelegram\b/.test(skillText)) return /\btelegram\b/.test(normalizedPrompt);
|
|
545
|
+
return true;
|
|
546
|
+
}
|
|
547
|
+
|
|
312
548
|
export function projectSkillHints({ cwd = process.cwd() } = {}) {
|
|
313
549
|
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
|
-
}
|
|
550
|
+
const packagePaths = workspacePackagePaths(cwd);
|
|
320
551
|
|
|
321
552
|
for (const packagePath of packagePaths) {
|
|
322
553
|
const packageDir = path.dirname(packagePath);
|
|
@@ -324,6 +555,8 @@ export function projectSkillHints({ cwd = process.cwd() } = {}) {
|
|
|
324
555
|
addHintText(hints, JSON.stringify({
|
|
325
556
|
name: packageJson?.name,
|
|
326
557
|
description: packageJson?.description,
|
|
558
|
+
keywords: packageJson?.keywords || [],
|
|
559
|
+
scripts: packageJson?.scripts || {},
|
|
327
560
|
dependencies: Object.keys(packageJson?.dependencies || {}),
|
|
328
561
|
devDependencies: Object.keys(packageJson?.devDependencies || {})
|
|
329
562
|
}));
|
|
@@ -334,6 +567,48 @@ export function projectSkillHints({ cwd = process.cwd() } = {}) {
|
|
|
334
567
|
return [...hints];
|
|
335
568
|
}
|
|
336
569
|
|
|
570
|
+
function workspacePackagePaths(cwd) {
|
|
571
|
+
const rootPackagePath = path.join(cwd, "package.json");
|
|
572
|
+
const rootPackage = readJson(rootPackagePath);
|
|
573
|
+
const paths = new Set([rootPackagePath]);
|
|
574
|
+
for (const workspace of workspacePatterns(rootPackage?.workspaces)) {
|
|
575
|
+
for (const packagePath of expandWorkspacePattern({ cwd, pattern: workspace })) {
|
|
576
|
+
paths.add(packagePath);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return [...paths];
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function workspacePatterns(workspaces) {
|
|
583
|
+
if (Array.isArray(workspaces)) return workspaces.filter((item) => typeof item === "string");
|
|
584
|
+
if (Array.isArray(workspaces?.packages)) return workspaces.packages.filter((item) => typeof item === "string");
|
|
585
|
+
return [];
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function expandWorkspacePattern({ cwd, pattern }) {
|
|
589
|
+
const normalized = String(pattern || "").replace(/\\/g, "/").replace(/\/+$/g, "");
|
|
590
|
+
if (!normalized || normalized.startsWith("..") || path.isAbsolute(normalized)) return [];
|
|
591
|
+
if (!normalized.includes("*")) {
|
|
592
|
+
const packagePath = path.join(cwd, normalized, "package.json");
|
|
593
|
+
return fs.existsSync(packagePath) ? [packagePath] : [];
|
|
594
|
+
}
|
|
595
|
+
const parts = normalized.split("/");
|
|
596
|
+
const starIndex = parts.indexOf("*");
|
|
597
|
+
if (starIndex < 0 || parts.includes("**")) return [];
|
|
598
|
+
const baseDir = path.join(cwd, ...parts.slice(0, starIndex));
|
|
599
|
+
const suffix = parts.slice(starIndex + 1);
|
|
600
|
+
let entries = [];
|
|
601
|
+
try {
|
|
602
|
+
entries = fs.readdirSync(baseDir, { withFileTypes: true });
|
|
603
|
+
} catch {
|
|
604
|
+
return [];
|
|
605
|
+
}
|
|
606
|
+
return entries
|
|
607
|
+
.filter((entry) => entry.isDirectory() && !entry.name.startsWith("."))
|
|
608
|
+
.map((entry) => path.join(baseDir, entry.name, ...suffix, "package.json"))
|
|
609
|
+
.filter((packagePath) => fs.existsSync(packagePath));
|
|
610
|
+
}
|
|
611
|
+
|
|
337
612
|
function readJson(filePath) {
|
|
338
613
|
try {
|
|
339
614
|
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
@@ -366,5 +641,8 @@ function normalize(value) {
|
|
|
366
641
|
}
|
|
367
642
|
|
|
368
643
|
function normalizePrompt(value) {
|
|
369
|
-
return normalize(String(value || "")
|
|
644
|
+
return normalize(String(value || "")
|
|
645
|
+
.replace(/https?:\/\/\S+/gi, " ")
|
|
646
|
+
.replace(/giao\s+di[eệ]n/gi, "frontend ui")
|
|
647
|
+
.replace(/phan\s+quyen/gi, "authorization role"));
|
|
370
648
|
}
|