@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 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 multi-select panel for prompt sections. | 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`. |
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` now includes the same multi-select step, 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`. Disabling rules hides both critical and additional relevant rule sections; compliance metadata remains available for reports.
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: 3,
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, 3);
598
- const suggestedSkills = (scored.suggestedSkills || []).slice(0, 3);
599
- const suggestedWorkflows = (scored.suggestedWorkflows || []).slice(0, 2);
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@minhpnq1807/contextos",
3
- "version": "0.5.44",
3
+ "version": "0.5.46",
4
4
  "description": "Task-aware AGENTS.md context injection and compliance reporting for Codex, Claude Code, and Antigravity.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ctx",
3
- "version": "0.5.44",
3
+ "version": "0.5.46",
4
4
  "description": "Inject task-relevant AGENTS.md rules into Codex through plugin hooks.",
5
5
  "author": {
6
6
  "name": "ContextOS"
@@ -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 cwd = resolveHookCwd(payload);
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: 3
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: 3,
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 || 500)
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, 3);
70
- const suggestedSkills = (scored.suggestedSkills || []).slice(0, 3);
71
- const suggestedWorkflows = (scored.suggestedWorkflows || []).slice(0, 2);
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(section("Suggested files to check", relevantFiles.map(formatFile)));
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 formatFile(file) {
81
- return `- ${path.basename(file.path)}`;
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", "cicd", "eas", "expo", "ios", "postgres", "postgresql", "react-native"
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 || Number(skill.embeddingScore || 0) >= 0.62)
203
- .sort((a, b) => b.score - a.score || scopePriority(b.scope) - scopePriority(a.scope) || a.name.localeCompare(b.name));
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 domainEligible = isSkillDomainEligible(normalizedPrompt, enriched);
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 = matches.length && intentBonus ? Math.min(0.16, projectMatches.length * 0.04) : 0;
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 = [path.join(cwd, "package.json")];
315
- const rootPackage = readJson(path.join(cwd, "package.json"));
316
- for (const workspace of rootPackage?.workspaces || []) {
317
- if (typeof workspace !== "string" || workspace.includes("*")) continue;
318
- packagePaths.push(path.join(cwd, workspace, "package.json"));
319
- }
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 || "").replace(/https?:\/\/\S+/gi, " "));
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
  }