@minhpnq1807/contextos 0.5.46 → 0.5.49
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 +12 -4
- package/bin/ctx.js +43 -21
- package/package.json +1 -1
- package/plugins/ctx/.codex-plugin/plugin.json +1 -1
- package/plugins/ctx/lib/embedding-scorer.js +64 -25
- package/plugins/ctx/lib/graph-strategy.js +1 -0
- package/plugins/ctx/lib/project-profiler.js +207 -0
- package/plugins/ctx/lib/prompt-hook.js +2 -1
- package/plugins/ctx/lib/scheduler.js +2 -1
- package/plugins/ctx/lib/score-context.js +12 -2
- package/plugins/ctx/lib/skill-discoverer.js +97 -369
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.49
|
|
4
|
+
|
|
5
|
+
- **Cold-cache MCP smoke:** `npm run test:mcp` now verifies the MCP tool contract without requiring a pre-downloaded ContextOS embedding model. When the model exists it still runs the semantic/performance smoke; otherwise it asserts cold-cache fallback behavior so publish CI does not fail before model warmup.
|
|
6
|
+
|
|
7
|
+
## 0.5.48
|
|
8
|
+
|
|
9
|
+
- **Publish-safe embedding warm:** `ctx embeddings warm` now skips remote model downloads by default in CI and returns best-effort warm results when local embedding model loading fails, preventing npm publish workflows from failing on Hugging Face 429/rate-limit errors.
|
|
10
|
+
- **CI graph embedding skip:** Code-review-graph embedding refresh is skipped when remote embeddings are disabled, avoiding extra Hugging Face calls during publish jobs.
|
|
11
|
+
|
|
12
|
+
## 0.5.47
|
|
13
|
+
|
|
14
|
+
- **Semantic-only skill discovery:** Replaced taxonomy/keyword skill ranking with fused semantic retrieval. ContextOS now embeds `prompt + project profile` and compares it against cached skill vectors, so mixed Vietnamese/English prompts can use package, language, and recent-file signals without hard-coded categories.
|
|
15
|
+
- **Project profile cache:** Added a bounded project profiler that builds an embeddable string from root/workspace `package.json` dependencies, scripts, detected languages, and recent git files. The cache invalidates when package metadata or git `HEAD` changes.
|
|
16
|
+
- **Skill dedupe and activation format:** Skill catalogs are deduplicated by normalized skill name before indexing/searching, preferring project and Codex roots. Prompt output now renders skill activations as `$skill-name`.
|
|
17
|
+
- **Refresh rebuilds skill index:** `ctx install` and `ctx refresh` now rebuild the skill embedding index by default because prompt-time skill discovery no longer falls back to taxonomy or keyword ranking for large catalogs.
|
|
18
|
+
- **Skill hook timeout split:** Prompt hooks now use a separate skill embedding timeout, so large indexed skill catalogs can still produce suggestions after direct fallback without slowing rule scoring.
|
|
19
|
+
- **Read-only embedding cache close:** Prompt-time embedding reads no longer rewrite the whole `embeddings.db` when no cache entries changed.
|
|
20
|
+
|
|
3
21
|
## 0.5.46
|
|
4
22
|
|
|
5
23
|
- **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.
|
package/README.md
CHANGED
|
@@ -445,7 +445,7 @@ This warning comes from a transitive dependency in the local embedding/WASM stac
|
|
|
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
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
|
-
| `ctx refresh` | Refreshes the active Codex marketplace plugin and rebuilds local indexes. | Local development updates or
|
|
448
|
+
| `ctx refresh` | Refreshes the active Codex marketplace plugin and rebuilds local indexes. | Local development updates or stale file/skill retrieval indexes. | Copies the current package to `$CODEX_HOME/marketplaces/contextos`, rebuilds file-path embeddings, skill embeddings, 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. |
|
|
451
451
|
| `ctx --version` | Prints the installed ContextOS CLI version. | You want to confirm which npm version is being executed. | Prints the version from package metadata. |
|
|
@@ -511,17 +511,23 @@ This keeps the hook fast and local while still using graph semantics when availa
|
|
|
511
511
|
|
|
512
512
|
Prompt scoring does not walk the repository for file candidates or import expansion. `ctx install` and `ctx embeddings warm` rebuild the persisted file-vector index and one-hop import adjacency index by walking source paths once; prompt hooks query those indexes directly. Rules, files, skills, and workflows are scored concurrently with `Promise.all()`.
|
|
513
513
|
|
|
514
|
-
`ctx embeddings warm` automatically refreshes the active Codex marketplace payload before rebuilding indexes. Use `ctx refresh` when you want the same marketplace sync plus install-style file
|
|
514
|
+
`ctx embeddings warm` automatically refreshes the active Codex marketplace payload before rebuilding indexes. Use `ctx refresh` when you want the same marketplace sync plus install-style file, skill, import, and code-review-graph embedding refresh in one command.
|
|
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
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 a comma-separated inline list of basenames without paths, skills show unique
|
|
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 `$skill-name` activations 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
|
|
524
|
+
Skill ranking is semantic-only. ContextOS builds a fused query from the user prompt plus a cached project profile, then compares that vector with cached skill vectors:
|
|
525
|
+
|
|
526
|
+
```text
|
|
527
|
+
embed(prompt + project profile) -> cosine -> embed(skill name + description)
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
The project profile is an embeddable string built from bounded root/workspace `package.json` metadata, dependencies, scripts, detected languages, and recent git files. It is cached under the ContextOS workspace data directory and invalidated when package metadata or git `HEAD` changes. ContextOS does not maintain a skill taxonomy or domain gate list for ranking; if the skill index is cold for a large catalog, prompt hooks fail open instead of falling back to arbitrary keyword matches. Skill catalogs are deduplicated by normalized skill name before indexing and rendering.
|
|
525
531
|
|
|
526
532
|
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
533
|
|
|
@@ -538,6 +544,8 @@ CONTEXTOS_HOOK_DEADLINE_MS=8500 hard fail-open deadline for prompt hooks
|
|
|
538
544
|
CONTEXTOS_DIRECT_FALLBACK_TIMEOUT_MS=6000 direct scoring timeout when the bridge is unavailable
|
|
539
545
|
CONTEXTOS_HOOK_EMBEDDING_TIMEOUT_MS=500 rule embedding timeout during hook direct fallback
|
|
540
546
|
CONTEXTOS_EMBEDDING_TIMEOUT_MS=800 embedding scoring timeout inside ctx-mcp/debug
|
|
547
|
+
CONTEXTOS_HOOK_SKILL_EMBEDDING_TIMEOUT_MS=2000 skill retrieval timeout during hook direct fallback
|
|
548
|
+
CONTEXTOS_SKILL_EMBEDDING_TIMEOUT_MS=2000 skill retrieval timeout inside ctx-mcp/debug
|
|
541
549
|
CONTEXTOS_FILE_EMBEDDINGS=0 disable file-path embedding retrieval
|
|
542
550
|
CONTEXTOS_HOOK_FILE_EMBEDDING_TIMEOUT_MS=500 file retrieval timeout during hook direct fallback
|
|
543
551
|
CONTEXTOS_FILE_EMBEDDING_TIMEOUT_MS=1000 file-path embedding retrieval timeout
|
package/bin/ctx.js
CHANGED
|
@@ -487,6 +487,7 @@ function graphStrategyForInstall() {
|
|
|
487
487
|
async function warmInstallEmbeddings() {
|
|
488
488
|
const dataDir = contextOSDataDir();
|
|
489
489
|
const modelReady = isModelCacheReady(dataDir);
|
|
490
|
+
const allowRemote = shouldAllowRemoteWarm(modelReady);
|
|
490
491
|
const result = await warmRuleEmbeddings({
|
|
491
492
|
rules: [
|
|
492
493
|
{ content: "Always use project rules that are semantically relevant to the user prompt." },
|
|
@@ -496,29 +497,29 @@ async function warmInstallEmbeddings() {
|
|
|
496
497
|
task: "kiểm duyệt upload moderation semantic code search",
|
|
497
498
|
dataDir,
|
|
498
499
|
sources: [],
|
|
499
|
-
allowRemote
|
|
500
|
+
allowRemote
|
|
500
501
|
});
|
|
501
502
|
const fileResult = await warmFileEmbeddings({
|
|
502
503
|
cwd: process.cwd(),
|
|
503
504
|
dataDir,
|
|
504
|
-
allowRemote
|
|
505
|
+
allowRemote
|
|
506
|
+
});
|
|
507
|
+
const skillResult = await warmSkillEmbeddings({
|
|
508
|
+
cwd: process.cwd(),
|
|
509
|
+
dataDir,
|
|
510
|
+
allowRemote
|
|
505
511
|
});
|
|
506
512
|
const warmDiscovery = process.env.CONTEXTOS_INSTALL_WARM_DISCOVERY === "1";
|
|
507
|
-
const skillResult = warmDiscovery
|
|
508
|
-
? await warmSkillEmbeddings({
|
|
509
|
-
cwd: process.cwd(),
|
|
510
|
-
dataDir,
|
|
511
|
-
allowRemote: !modelReady
|
|
512
|
-
})
|
|
513
|
-
: { count: 0 };
|
|
514
513
|
const workflowResult = warmDiscovery
|
|
515
514
|
? await warmWorkflowEmbeddings({
|
|
516
515
|
cwd: process.cwd(),
|
|
517
516
|
dataDir,
|
|
518
|
-
allowRemote
|
|
517
|
+
allowRemote
|
|
519
518
|
})
|
|
520
519
|
: { count: 0 };
|
|
521
|
-
const graphEmbedding =
|
|
520
|
+
const graphEmbedding = allowRemote
|
|
521
|
+
? embedCodeReviewGraph({ cwd: process.cwd() })
|
|
522
|
+
: { status: "skipped", reason: "remote-embedding-disabled" };
|
|
522
523
|
return { ...result, modelAlreadyCached: modelReady, fileCount: fileResult.count, skillCount: skillResult.count, workflowCount: workflowResult.count, graphEmbedding };
|
|
523
524
|
}
|
|
524
525
|
|
|
@@ -663,31 +664,36 @@ async function warmEmbeddings(task, { syncMarketplace = true, quiet = false } =
|
|
|
663
664
|
|
|
664
665
|
async function warmWorkspaceIndexes({ task = "project context" } = {}) {
|
|
665
666
|
const cwd = process.cwd();
|
|
667
|
+
const dataDir = contextOSDataDir();
|
|
668
|
+
const modelReady = isModelCacheReady(dataDir);
|
|
669
|
+
const allowRemote = shouldAllowRemoteWarm(modelReady);
|
|
666
670
|
const merged = readAgentsChain({ cwd });
|
|
667
671
|
const rules = scoreRules(filterActionableRules(parseRules(merged.content)), task, []);
|
|
668
672
|
const result = await warmRuleEmbeddings({
|
|
669
673
|
rules,
|
|
670
674
|
task,
|
|
671
|
-
dataDir
|
|
675
|
+
dataDir,
|
|
672
676
|
sources: merged.sources,
|
|
673
|
-
allowRemote
|
|
677
|
+
allowRemote
|
|
674
678
|
});
|
|
675
679
|
const fileResult = await warmFileEmbeddings({
|
|
676
680
|
cwd,
|
|
677
|
-
dataDir
|
|
678
|
-
allowRemote
|
|
681
|
+
dataDir,
|
|
682
|
+
allowRemote
|
|
679
683
|
});
|
|
680
684
|
const skillResult = await warmSkillEmbeddings({
|
|
681
685
|
cwd,
|
|
682
|
-
dataDir
|
|
683
|
-
allowRemote
|
|
686
|
+
dataDir,
|
|
687
|
+
allowRemote
|
|
684
688
|
});
|
|
685
689
|
const workflowResult = await warmWorkflowEmbeddings({
|
|
686
690
|
cwd,
|
|
687
|
-
dataDir
|
|
688
|
-
allowRemote
|
|
691
|
+
dataDir,
|
|
692
|
+
allowRemote
|
|
689
693
|
});
|
|
690
|
-
const graphEmbedding =
|
|
694
|
+
const graphEmbedding = allowRemote
|
|
695
|
+
? embedCodeReviewGraph({ cwd })
|
|
696
|
+
: { status: "skipped", reason: "remote-embedding-disabled" };
|
|
691
697
|
return {
|
|
692
698
|
ruleCount: result.count,
|
|
693
699
|
fileCount: fileResult.count,
|
|
@@ -698,12 +704,28 @@ async function warmWorkspaceIndexes({ task = "project context" } = {}) {
|
|
|
698
704
|
};
|
|
699
705
|
}
|
|
700
706
|
|
|
707
|
+
function shouldAllowRemoteWarm(modelReady) {
|
|
708
|
+
if (modelReady) return false;
|
|
709
|
+
if (process.env.CONTEXTOS_EMBEDDING_ALLOW_REMOTE !== undefined) {
|
|
710
|
+
return process.env.CONTEXTOS_EMBEDDING_ALLOW_REMOTE === "1";
|
|
711
|
+
}
|
|
712
|
+
return !isCiEnvironment();
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function isCiEnvironment() {
|
|
716
|
+
return process.env.CI === "true"
|
|
717
|
+
|| process.env.GITHUB_ACTIONS === "true"
|
|
718
|
+
|| process.env.CONTINUOUS_INTEGRATION === "true"
|
|
719
|
+
|| process.env.BUILD_ID !== undefined
|
|
720
|
+
|| process.env.RUN_ID !== undefined;
|
|
721
|
+
}
|
|
722
|
+
|
|
701
723
|
async function refresh() {
|
|
702
724
|
const marketplaceSync = syncActiveCodexMarketplace();
|
|
703
725
|
const invalidatedBridge = invalidateCtxMcpSocket(contextOSDataDir());
|
|
704
726
|
const warmResult = await warmInstallEmbeddings();
|
|
705
727
|
console.log(`Marketplace: ${marketplaceSync.synced ? "synced" : "already active"} (${marketplaceSync.targetRoot})`);
|
|
706
|
-
console.log(`Indexes: ${warmResult.fileCount || 0} file paths rebuilt`);
|
|
728
|
+
console.log(`Indexes: ${warmResult.fileCount || 0} file paths rebuilt, ${warmResult.skillCount || 0} skills indexed`);
|
|
707
729
|
console.log(`Graph embeddings: ${formatCodeReviewGraphEmbedding(warmResult.graphEmbedding)}`);
|
|
708
730
|
if (invalidatedBridge) console.log("Bridge: stale private socket invalidated");
|
|
709
731
|
console.log("Restart Codex if ctx-mcp was already running.");
|
package/package.json
CHANGED
|
@@ -65,13 +65,28 @@ export async function warmRuleEmbeddings({
|
|
|
65
65
|
...rules.map((rule) => rule.content || "")
|
|
66
66
|
].filter((text) => String(text).trim()))];
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
await
|
|
68
|
+
let cache;
|
|
69
|
+
try {
|
|
70
|
+
cache = await openEmbeddingCache(dataDir);
|
|
71
|
+
const embedder = await getExtractor({ allowRemote, dataDir });
|
|
72
|
+
for (const text of texts) {
|
|
73
|
+
await getCachedEmbedding({ cache, embedder, text, sources, flush: false });
|
|
74
|
+
}
|
|
75
|
+
cache.close();
|
|
76
|
+
return { count: texts.length, cachePath: cache.path, status: "enabled" };
|
|
77
|
+
} catch (error) {
|
|
78
|
+
try {
|
|
79
|
+
cache?.close();
|
|
80
|
+
} catch {
|
|
81
|
+
// Ignore close failures while reporting a best-effort warm result.
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
count: 0,
|
|
85
|
+
cachePath: path.join(dataDir, "embeddings.db"),
|
|
86
|
+
status: "warm-failed",
|
|
87
|
+
error: error?.message || String(error)
|
|
88
|
+
};
|
|
72
89
|
}
|
|
73
|
-
cache.close();
|
|
74
|
-
return { count: texts.length, cachePath: cache.path };
|
|
75
90
|
}
|
|
76
91
|
|
|
77
92
|
export async function searchIndexedEmbeddings({
|
|
@@ -106,20 +121,35 @@ export async function warmIndexedEmbeddings({
|
|
|
106
121
|
return { count: 0, cachePath: path.join(dataDir, "embeddings.db"), status: "missing-model" };
|
|
107
122
|
}
|
|
108
123
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
124
|
+
let cache;
|
|
125
|
+
try {
|
|
126
|
+
cache = await openEmbeddingCache(dataDir);
|
|
127
|
+
const embedder = await getExtractor({ allowRemote, dataDir });
|
|
128
|
+
if (String(task || "").trim()) await getCachedEmbedding({ cache, embedder, text: task, sources });
|
|
129
|
+
|
|
130
|
+
const indexed = [];
|
|
131
|
+
for (const item of items) {
|
|
132
|
+
const text = String(item.text || "");
|
|
133
|
+
if (!item.id || !text.trim()) continue;
|
|
134
|
+
const vector = await getCachedEmbedding({ cache, embedder, text, sources, flush: false });
|
|
135
|
+
indexed.push({ id: item.id, text, vector });
|
|
136
|
+
}
|
|
137
|
+
cache.replaceIndex(kind, indexed);
|
|
138
|
+
cache.close();
|
|
139
|
+
return { count: indexed.length, cachePath: cache.path, status: "enabled" };
|
|
140
|
+
} catch (error) {
|
|
141
|
+
try {
|
|
142
|
+
cache?.close();
|
|
143
|
+
} catch {
|
|
144
|
+
// Ignore close failures while reporting a best-effort warm result.
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
count: 0,
|
|
148
|
+
cachePath: path.join(dataDir, "embeddings.db"),
|
|
149
|
+
status: "warm-failed",
|
|
150
|
+
error: error?.message || String(error)
|
|
151
|
+
};
|
|
119
152
|
}
|
|
120
|
-
cache.replaceIndex(kind, indexed);
|
|
121
|
-
cache.close();
|
|
122
|
-
return { count: indexed.length, cachePath: cache.path };
|
|
123
153
|
}
|
|
124
154
|
|
|
125
155
|
async function enhanceRuleScores(rules, task, { dataDir, sources, allowRemote }) {
|
|
@@ -187,7 +217,10 @@ async function getExtractor({ allowRemote, dataDir }) {
|
|
|
187
217
|
return transformers.pipeline("feature-extraction", DEFAULT_MODEL, {
|
|
188
218
|
quantized: true
|
|
189
219
|
});
|
|
190
|
-
})())
|
|
220
|
+
})().catch((error) => {
|
|
221
|
+
extractorPromises.delete(key);
|
|
222
|
+
throw error;
|
|
223
|
+
}));
|
|
191
224
|
}
|
|
192
225
|
return extractorPromises.get(key);
|
|
193
226
|
}
|
|
@@ -206,7 +239,7 @@ export function isModelCacheReady(dataDir = defaultDataRoot()) {
|
|
|
206
239
|
].every((relativePath) => fs.existsSync(path.join(modelDir, relativePath)));
|
|
207
240
|
}
|
|
208
241
|
|
|
209
|
-
async function getCachedEmbedding({ cache, embedder, text, sources }) {
|
|
242
|
+
async function getCachedEmbedding({ cache, embedder, text, sources, flush = true }) {
|
|
210
243
|
const key = cacheKey(text, sources);
|
|
211
244
|
const existing = cache.get(key);
|
|
212
245
|
if (existing) return existing;
|
|
@@ -216,7 +249,7 @@ async function getCachedEmbedding({ cache, embedder, text, sources }) {
|
|
|
216
249
|
normalize: true
|
|
217
250
|
});
|
|
218
251
|
const embedding = Array.from(output.data || []);
|
|
219
|
-
cache.set(key, embedding);
|
|
252
|
+
cache.set(key, embedding, { flush });
|
|
220
253
|
return embedding;
|
|
221
254
|
}
|
|
222
255
|
|
|
@@ -225,6 +258,7 @@ export async function openEmbeddingCache(dataDir) {
|
|
|
225
258
|
const cachePath = path.join(dataDir, "embeddings.db");
|
|
226
259
|
const SQL = await getSql();
|
|
227
260
|
const db = initializeEmbeddingDatabase(SQL, cachePath);
|
|
261
|
+
let dirty = false;
|
|
228
262
|
|
|
229
263
|
return {
|
|
230
264
|
path: cachePath,
|
|
@@ -238,12 +272,16 @@ export async function openEmbeddingCache(dataDir) {
|
|
|
238
272
|
stmt.free();
|
|
239
273
|
}
|
|
240
274
|
},
|
|
241
|
-
set(key, vector) {
|
|
275
|
+
set(key, vector, { flush = true } = {}) {
|
|
242
276
|
db.run(
|
|
243
277
|
"INSERT OR REPLACE INTO embeddings (key, model, vector, updated_at) VALUES (?, ?, ?, ?)",
|
|
244
278
|
[key, DEFAULT_MODEL, JSON.stringify(vector), new Date().toISOString()]
|
|
245
279
|
);
|
|
246
|
-
|
|
280
|
+
dirty = true;
|
|
281
|
+
if (flush) {
|
|
282
|
+
writeDatabaseAtomically(cachePath, db);
|
|
283
|
+
dirty = false;
|
|
284
|
+
}
|
|
247
285
|
},
|
|
248
286
|
listIndexed(kind) {
|
|
249
287
|
const stmt = db.prepare("SELECT id, text, vector FROM embedding_index WHERE kind = ? AND model = ?");
|
|
@@ -268,9 +306,10 @@ export async function openEmbeddingCache(dataDir) {
|
|
|
268
306
|
);
|
|
269
307
|
}
|
|
270
308
|
writeDatabaseAtomically(cachePath, db);
|
|
309
|
+
dirty = false;
|
|
271
310
|
},
|
|
272
311
|
close() {
|
|
273
|
-
writeDatabaseAtomically(cachePath, db);
|
|
312
|
+
if (dirty) writeDatabaseAtomically(cachePath, db);
|
|
274
313
|
db.close();
|
|
275
314
|
}
|
|
276
315
|
};
|
|
@@ -91,6 +91,7 @@ export function formatCodeReviewGraphEmbedding(result) {
|
|
|
91
91
|
}
|
|
92
92
|
if (result.reason === "missing-graph-index") return "skipped (no .code-review-graph/graph.db)";
|
|
93
93
|
if (result.reason === "missing-code-review-graph-python") return "skipped (code-review-graph Python unavailable)";
|
|
94
|
+
if (result.reason === "remote-embedding-disabled") return "skipped (remote embedding disabled)";
|
|
94
95
|
return `skipped (${result.error || result.reason || "unavailable"})`;
|
|
95
96
|
}
|
|
96
97
|
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
const PROFILE_CACHE_FILE = "project-profile.json";
|
|
6
|
+
const MAX_DEPENDENCIES = 80;
|
|
7
|
+
const MAX_SCRIPTS = 30;
|
|
8
|
+
const MAX_RECENT_FILES = 20;
|
|
9
|
+
|
|
10
|
+
export function projectProfile({ cwd = process.cwd(), dataDir } = {}) {
|
|
11
|
+
const fingerprint = projectFingerprint(cwd);
|
|
12
|
+
const cachePath = dataDir ? path.join(dataDir, PROFILE_CACHE_FILE) : null;
|
|
13
|
+
const cached = cachePath ? readCachedProfile(cachePath, fingerprint) : null;
|
|
14
|
+
if (cached) return cached;
|
|
15
|
+
|
|
16
|
+
const packagePaths = workspacePackagePaths(cwd);
|
|
17
|
+
const packageTexts = packagePaths.map((packagePath) => packageSignal(cwd, packagePath)).filter(Boolean);
|
|
18
|
+
const recentFiles = recentGitFiles(cwd, MAX_RECENT_FILES);
|
|
19
|
+
const languages = languageSignal({ cwd, packagePaths, recentFiles });
|
|
20
|
+
const embeddableString = [
|
|
21
|
+
packageTexts.length ? `[project packages: ${packageTexts.join(" | ")}]` : "",
|
|
22
|
+
languages.length ? `[project languages: ${languages.join(" ")}]` : "",
|
|
23
|
+
recentFiles.length ? `[recent files: ${recentFiles.join(", ")}]` : ""
|
|
24
|
+
].filter(Boolean).join(" ");
|
|
25
|
+
|
|
26
|
+
const profile = { fingerprint, embeddableString };
|
|
27
|
+
if (cachePath) writeCachedProfile(cachePath, profile);
|
|
28
|
+
return profile;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function fusedProjectQuery({ prompt = "", cwd = process.cwd(), dataDir } = {}) {
|
|
32
|
+
const profile = projectProfile({ cwd, dataDir });
|
|
33
|
+
return [String(prompt || "").trim(), profile.embeddableString].filter(Boolean).join("\n");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function readCachedProfile(cachePath, fingerprint) {
|
|
37
|
+
try {
|
|
38
|
+
const cached = JSON.parse(fs.readFileSync(cachePath, "utf8"));
|
|
39
|
+
if (cached?.fingerprint === fingerprint && typeof cached.embeddableString === "string") return cached;
|
|
40
|
+
} catch {
|
|
41
|
+
// Cache miss.
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function writeCachedProfile(cachePath, profile) {
|
|
47
|
+
try {
|
|
48
|
+
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
49
|
+
fs.writeFileSync(cachePath, JSON.stringify(profile, null, 2));
|
|
50
|
+
} catch {
|
|
51
|
+
// The profile is an optimization; prompt scoring can continue without a cache write.
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function projectFingerprint(cwd) {
|
|
56
|
+
const parts = [];
|
|
57
|
+
for (const packagePath of workspacePackagePaths(cwd)) {
|
|
58
|
+
try {
|
|
59
|
+
const stat = fs.statSync(packagePath);
|
|
60
|
+
parts.push(`${path.relative(cwd, packagePath)}:${stat.mtimeMs}:${stat.size}`);
|
|
61
|
+
} catch {
|
|
62
|
+
parts.push(`${path.relative(cwd, packagePath)}:missing`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
parts.push(`git:${gitHead(cwd)}`);
|
|
66
|
+
return parts.join("|");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function packageSignal(cwd, packagePath) {
|
|
70
|
+
const packageJson = readJson(packagePath);
|
|
71
|
+
if (!packageJson) return "";
|
|
72
|
+
const packageDir = path.dirname(packagePath);
|
|
73
|
+
const dependencies = [
|
|
74
|
+
...Object.keys(packageJson.dependencies || {}),
|
|
75
|
+
...Object.keys(packageJson.devDependencies || {}),
|
|
76
|
+
...Object.keys(packageJson.peerDependencies || {})
|
|
77
|
+
].slice(0, MAX_DEPENDENCIES);
|
|
78
|
+
const scripts = Object.keys(packageJson.scripts || {}).slice(0, MAX_SCRIPTS);
|
|
79
|
+
const configFiles = ["app.json", "app.config.js", "app.config.ts", "eas.json", "tsconfig.json", "Dockerfile"]
|
|
80
|
+
.filter((fileName) => fs.existsSync(path.join(packageDir, fileName)));
|
|
81
|
+
return [
|
|
82
|
+
path.relative(cwd, packagePath) || "package.json",
|
|
83
|
+
packageJson.name,
|
|
84
|
+
packageJson.description,
|
|
85
|
+
Array.isArray(packageJson.keywords) ? packageJson.keywords.join(" ") : "",
|
|
86
|
+
dependencies.join(" "),
|
|
87
|
+
scripts.length ? `scripts ${scripts.join(" ")}` : "",
|
|
88
|
+
configFiles.join(" ")
|
|
89
|
+
].filter(Boolean).join(" ");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function languageSignal({ cwd, packagePaths, recentFiles }) {
|
|
93
|
+
const values = new Set();
|
|
94
|
+
for (const packagePath of packagePaths) {
|
|
95
|
+
const packageDir = path.dirname(packagePath);
|
|
96
|
+
const packageJson = readJson(packagePath);
|
|
97
|
+
const deps = normalize(Object.keys({
|
|
98
|
+
...(packageJson?.dependencies || {}),
|
|
99
|
+
...(packageJson?.devDependencies || {})
|
|
100
|
+
}).join(" "));
|
|
101
|
+
if (deps.includes("typescript") || fs.existsSync(path.join(packageDir, "tsconfig.json"))) values.add("TypeScript");
|
|
102
|
+
if (deps.includes("python")) values.add("Python");
|
|
103
|
+
if (deps.includes("go")) values.add("Go");
|
|
104
|
+
if (deps.includes("react")) values.add("React");
|
|
105
|
+
if (deps.includes("next")) values.add("Next.js");
|
|
106
|
+
if (deps.includes("expo") || deps.includes("react native")) values.add("React Native");
|
|
107
|
+
}
|
|
108
|
+
for (const file of recentFiles) {
|
|
109
|
+
const ext = path.extname(file);
|
|
110
|
+
if ([".ts", ".tsx"].includes(ext)) values.add("TypeScript");
|
|
111
|
+
if ([".js", ".jsx", ".mjs", ".cjs"].includes(ext)) values.add("JavaScript");
|
|
112
|
+
if (ext === ".py") values.add("Python");
|
|
113
|
+
if (ext === ".go") values.add("Go");
|
|
114
|
+
if (ext === ".rs") values.add("Rust");
|
|
115
|
+
if (ext === ".java") values.add("Java");
|
|
116
|
+
}
|
|
117
|
+
if (!values.size && fs.existsSync(path.join(cwd, "package.json"))) values.add("JavaScript");
|
|
118
|
+
return [...values];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function recentGitFiles(cwd, limit) {
|
|
122
|
+
try {
|
|
123
|
+
const output = execFileSync("git", ["log", "--name-only", "--pretty=format:", "-n", "30"], {
|
|
124
|
+
cwd,
|
|
125
|
+
encoding: "utf8",
|
|
126
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
127
|
+
timeout: 300
|
|
128
|
+
});
|
|
129
|
+
const seen = new Set();
|
|
130
|
+
for (const line of output.split(/\r?\n/)) {
|
|
131
|
+
const file = line.trim();
|
|
132
|
+
if (!file || seen.has(file)) continue;
|
|
133
|
+
seen.add(file);
|
|
134
|
+
if (seen.size >= limit) break;
|
|
135
|
+
}
|
|
136
|
+
return [...seen];
|
|
137
|
+
} catch {
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function gitHead(cwd) {
|
|
143
|
+
try {
|
|
144
|
+
return execFileSync("git", ["rev-parse", "HEAD"], {
|
|
145
|
+
cwd,
|
|
146
|
+
encoding: "utf8",
|
|
147
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
148
|
+
timeout: 300
|
|
149
|
+
}).trim();
|
|
150
|
+
} catch {
|
|
151
|
+
return "nogit";
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function workspacePackagePaths(cwd) {
|
|
156
|
+
const rootPackagePath = path.join(cwd, "package.json");
|
|
157
|
+
const rootPackage = readJson(rootPackagePath);
|
|
158
|
+
const paths = new Set(fs.existsSync(rootPackagePath) ? [rootPackagePath] : []);
|
|
159
|
+
for (const workspace of workspacePatterns(rootPackage?.workspaces)) {
|
|
160
|
+
for (const packagePath of expandWorkspacePattern({ cwd, pattern: workspace })) {
|
|
161
|
+
paths.add(packagePath);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return [...paths];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function workspacePatterns(workspaces) {
|
|
168
|
+
if (Array.isArray(workspaces)) return workspaces.filter((item) => typeof item === "string");
|
|
169
|
+
if (Array.isArray(workspaces?.packages)) return workspaces.packages.filter((item) => typeof item === "string");
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function expandWorkspacePattern({ cwd, pattern }) {
|
|
174
|
+
const normalizedPattern = String(pattern || "").replace(/\\/g, "/").replace(/\/+$/g, "");
|
|
175
|
+
if (!normalizedPattern || normalizedPattern.startsWith("..") || path.isAbsolute(normalizedPattern)) return [];
|
|
176
|
+
if (!normalizedPattern.includes("*")) {
|
|
177
|
+
const packagePath = path.join(cwd, normalizedPattern, "package.json");
|
|
178
|
+
return fs.existsSync(packagePath) ? [packagePath] : [];
|
|
179
|
+
}
|
|
180
|
+
const parts = normalizedPattern.split("/");
|
|
181
|
+
const starIndex = parts.indexOf("*");
|
|
182
|
+
if (starIndex < 0 || parts.includes("**")) return [];
|
|
183
|
+
const baseDir = path.join(cwd, ...parts.slice(0, starIndex));
|
|
184
|
+
const suffix = parts.slice(starIndex + 1);
|
|
185
|
+
let entries = [];
|
|
186
|
+
try {
|
|
187
|
+
entries = fs.readdirSync(baseDir, { withFileTypes: true });
|
|
188
|
+
} catch {
|
|
189
|
+
return [];
|
|
190
|
+
}
|
|
191
|
+
return entries
|
|
192
|
+
.filter((entry) => entry.isDirectory() && !entry.name.startsWith("."))
|
|
193
|
+
.map((entry) => path.join(baseDir, entry.name, ...suffix, "package.json"))
|
|
194
|
+
.filter((packagePath) => fs.existsSync(packagePath));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function readJson(filePath) {
|
|
198
|
+
try {
|
|
199
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
200
|
+
} catch {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function normalize(value) {
|
|
206
|
+
return String(value || "").toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
|
|
207
|
+
}
|
|
@@ -56,7 +56,8 @@ export async function handlePromptPayload(
|
|
|
56
56
|
maxWorkflows: promptLimits.workflows,
|
|
57
57
|
dataDir: mcpDataDir || dataDir,
|
|
58
58
|
embeddingTimeoutMs: Number(process.env.CONTEXTOS_HOOK_EMBEDDING_TIMEOUT_MS || 500),
|
|
59
|
-
fileEmbeddingTimeoutMs: Number(process.env.CONTEXTOS_HOOK_FILE_EMBEDDING_TIMEOUT_MS || 1000)
|
|
59
|
+
fileEmbeddingTimeoutMs: Number(process.env.CONTEXTOS_HOOK_FILE_EMBEDDING_TIMEOUT_MS || 1000),
|
|
60
|
+
skillEmbeddingTimeoutMs: Number(process.env.CONTEXTOS_HOOK_SKILL_EMBEDDING_TIMEOUT_MS || 2000)
|
|
60
61
|
}), directFallbackTimeoutMs, "direct fallback scoring");
|
|
61
62
|
scored.telemetry = {
|
|
62
63
|
...(scored.telemetry || {}),
|
|
@@ -97,7 +97,8 @@ function formatFile(file, basenameCounts) {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
function formatSkill(skill) {
|
|
100
|
-
|
|
100
|
+
const name = String(skill.name || "").trim();
|
|
101
|
+
return name.startsWith("$") ? name : `$${name}`;
|
|
101
102
|
}
|
|
102
103
|
|
|
103
104
|
function formatWorkflow(workflow) {
|
|
@@ -17,7 +17,9 @@ export async function scoreContext({
|
|
|
17
17
|
skills = null,
|
|
18
18
|
workflows = null,
|
|
19
19
|
embeddingTimeoutMs = 5000,
|
|
20
|
-
fileEmbeddingTimeoutMs = Number(process.env.CONTEXTOS_FILE_EMBEDDING_TIMEOUT_MS || 1000)
|
|
20
|
+
fileEmbeddingTimeoutMs = Number(process.env.CONTEXTOS_FILE_EMBEDDING_TIMEOUT_MS || 1000),
|
|
21
|
+
skillEmbeddingTimeoutMs = Number(process.env.CONTEXTOS_SKILL_EMBEDDING_TIMEOUT_MS || embeddingTimeoutMs),
|
|
22
|
+
skillSearchOptions = {}
|
|
21
23
|
} = {}) {
|
|
22
24
|
const started = Date.now();
|
|
23
25
|
const ruleInputsPromise = Promise.resolve().then(() => {
|
|
@@ -59,7 +61,15 @@ export async function scoreContext({
|
|
|
59
61
|
const catalog = Array.isArray(skills) ? skills : scanSkills({ cwd });
|
|
60
62
|
return {
|
|
61
63
|
catalog,
|
|
62
|
-
suggestions: await suggestSkills({
|
|
64
|
+
suggestions: await suggestSkills({
|
|
65
|
+
cwd,
|
|
66
|
+
prompt,
|
|
67
|
+
skills: catalog,
|
|
68
|
+
dataDir,
|
|
69
|
+
limit: maxSkills,
|
|
70
|
+
timeoutMs: skillEmbeddingTimeoutMs,
|
|
71
|
+
...skillSearchOptions
|
|
72
|
+
})
|
|
63
73
|
};
|
|
64
74
|
});
|
|
65
75
|
|
|
@@ -2,29 +2,20 @@ import fs from "node:fs";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
enhanceRuleScoresWithEmbeddings,
|
|
7
|
+
searchIndexedEmbeddings,
|
|
8
|
+
warmIndexedEmbeddings
|
|
9
|
+
} from "./embedding-scorer.js";
|
|
10
|
+
import { fusedProjectQuery, workspacePackagePaths } from "./project-profiler.js";
|
|
6
11
|
|
|
7
12
|
const DEFAULT_LIMIT = 3;
|
|
8
13
|
const DEFAULT_MAX_SKILLS = 2000;
|
|
9
14
|
const DEFAULT_EMBEDDING_CANDIDATES = 120;
|
|
10
|
-
const DEFAULT_SEMANTIC_CATALOG_LIMIT = 300;
|
|
11
15
|
const SCAN_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
12
16
|
const MAX_DESCRIPTION_CHARS = 500;
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
"environment", "file", "files", "graph", "install", "integration", "local", "node", "package",
|
|
16
|
-
"project", "refresh", "rebuild", "setup", "skill", "skills", "sync", "tool", "tools", "using",
|
|
17
|
-
"build", "can", "not", "production", "show", "something", "https", "http", "com", "www",
|
|
18
|
-
"a", "an", "and", "are", "as", "at", "be", "before", "after", "both", "by", "from", "for",
|
|
19
|
-
"if", "in", "into", "is", "must", "of", "on", "or", "the", "then", "this", "to", "user",
|
|
20
|
-
"users", "when", "where", "whether", "with"
|
|
21
|
-
]);
|
|
22
|
-
const SPECIALIZED_SKILL_TOKENS = new Set([
|
|
23
|
-
"android", "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"
|
|
27
|
-
]);
|
|
17
|
+
const SKILL_EMBEDDING_THRESHOLD = 0.45;
|
|
18
|
+
const DEFAULT_SKILL_TIMEOUT_MS = 2000;
|
|
28
19
|
|
|
29
20
|
const scanCache = new Map();
|
|
30
21
|
|
|
@@ -124,8 +115,9 @@ function monotonicNow() {
|
|
|
124
115
|
}
|
|
125
116
|
|
|
126
117
|
function cacheAndReturnSkills(cacheKey, skills) {
|
|
127
|
-
|
|
128
|
-
|
|
118
|
+
const deduped = dedupeSkills(skills);
|
|
119
|
+
scanCache.set(cacheKey, { createdAt: monotonicNow(), skills: deduped });
|
|
120
|
+
return deduped;
|
|
129
121
|
}
|
|
130
122
|
|
|
131
123
|
function findSkillFiles(root) {
|
|
@@ -171,18 +163,40 @@ export async function suggestSkills({
|
|
|
171
163
|
dataDir,
|
|
172
164
|
cwd = process.cwd(),
|
|
173
165
|
limit = DEFAULT_LIMIT,
|
|
174
|
-
timeoutMs = Number(process.env.CONTEXTOS_SKILL_EMBEDDING_TIMEOUT_MS || process.env.CONTEXTOS_EMBEDDING_TIMEOUT_MS ||
|
|
166
|
+
timeoutMs = Number(process.env.CONTEXTOS_SKILL_EMBEDDING_TIMEOUT_MS || process.env.CONTEXTOS_EMBEDDING_TIMEOUT_MS || DEFAULT_SKILL_TIMEOUT_MS),
|
|
167
|
+
indexedSearcher = searchIndexedEmbeddings,
|
|
168
|
+
embeddingEnhancer = enhanceRuleScoresWithEmbeddings
|
|
175
169
|
} = {}) {
|
|
176
170
|
if (!String(prompt || "").trim() || !skills.length) return [];
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
171
|
+
const catalog = dedupeSkills(skills);
|
|
172
|
+
const query = fusedProjectQuery({ prompt, cwd, dataDir });
|
|
173
|
+
const byId = new Map(catalog.map((skill) => [skillIndexId(skill), skill]));
|
|
174
|
+
|
|
175
|
+
if (dataDir) {
|
|
176
|
+
const indexed = await indexedSearcher({
|
|
177
|
+
kind: skillIndexKind(cwd),
|
|
178
|
+
task: query,
|
|
179
|
+
dataDir,
|
|
180
|
+
timeoutMs,
|
|
181
|
+
allowRemote: false
|
|
182
|
+
});
|
|
183
|
+
if (indexed.status === "enabled" && indexed.items.length) {
|
|
184
|
+
return finalizeSkillScores(indexed.items
|
|
185
|
+
.map((item) => {
|
|
186
|
+
const skill = byId.get(item.id);
|
|
187
|
+
if (!skill) return null;
|
|
188
|
+
return skillScoreFromEmbedding(skill, item.embeddingScore, [`embedding:${Number(item.embeddingScore || 0).toFixed(2)}`]);
|
|
189
|
+
})
|
|
190
|
+
.filter(Boolean), limit);
|
|
191
|
+
}
|
|
180
192
|
}
|
|
181
193
|
|
|
182
|
-
|
|
194
|
+
if (catalog.length > DEFAULT_EMBEDDING_CANDIDATES) return [];
|
|
195
|
+
|
|
196
|
+
const embeddingCandidates = catalog.map((skill, index) => skillRule({ skill, index }));
|
|
183
197
|
if (!embeddingCandidates.length) return [];
|
|
184
198
|
|
|
185
|
-
const embedding = await
|
|
199
|
+
const embedding = await embeddingEnhancer(embeddingCandidates, query, {
|
|
186
200
|
dataDir,
|
|
187
201
|
sources: embeddingCandidates.map((skill) => skill.path).filter(Boolean),
|
|
188
202
|
timeoutMs,
|
|
@@ -192,26 +206,20 @@ export async function suggestSkills({
|
|
|
192
206
|
return finalizeSkillScores(embedding.rules, limit);
|
|
193
207
|
}
|
|
194
208
|
|
|
195
|
-
function finalizeSkillScores(skills, limit
|
|
209
|
+
function finalizeSkillScores(skills, limit) {
|
|
196
210
|
const ranked = skills
|
|
197
|
-
.filter((rule) => rule.domainEligible !== false)
|
|
198
211
|
.map((rule) => ({
|
|
199
212
|
name: rule.name,
|
|
200
213
|
description: rule.description,
|
|
201
214
|
path: rule.path,
|
|
202
215
|
scope: rule.scope,
|
|
203
|
-
keywordScore: rule.keywordScore,
|
|
204
216
|
score: Math.min(1, Number(rule.score || 0)),
|
|
205
217
|
embeddingScore: rule.embeddingScore,
|
|
206
|
-
|
|
207
|
-
rankScore: Math.min(1, Number(rule.score || 0)) + Number(rule.relevancePriority || 0) / 100,
|
|
218
|
+
rankScore: Math.min(1, Number(rule.score || 0)),
|
|
208
219
|
reasons: rule.reasons || []
|
|
209
220
|
}))
|
|
210
|
-
.filter((skill) => Number(skill.
|
|
211
|
-
|| Number(skill.embeddingScore || 0) >= 0.62
|
|
212
|
-
|| Number(skill.relevancePriority || 0) >= 50)
|
|
221
|
+
.filter((skill) => Number(skill.embeddingScore || skill.score || 0) >= SKILL_EMBEDDING_THRESHOLD)
|
|
213
222
|
.sort((a, b) => b.rankScore - a.rankScore
|
|
214
|
-
|| b.relevancePriority - a.relevancePriority
|
|
215
223
|
|| b.score - a.score
|
|
216
224
|
|| scopePriority(b.scope) - scopePriority(a.scope)
|
|
217
225
|
|| a.name.localeCompare(b.name));
|
|
@@ -230,14 +238,6 @@ function scopePriority(scope) {
|
|
|
230
238
|
return scope === "project" ? 1 : 0;
|
|
231
239
|
}
|
|
232
240
|
|
|
233
|
-
function selectEmbeddingCandidates(skills) {
|
|
234
|
-
if (skills.length <= DEFAULT_EMBEDDING_CANDIDATES) return skills;
|
|
235
|
-
return [...skills]
|
|
236
|
-
.filter((skill) => Number(skill.keywordScore || 0) > 0)
|
|
237
|
-
.sort((a, b) => Number(b.keywordScore || 0) - Number(a.keywordScore || 0) || a.name.localeCompare(b.name))
|
|
238
|
-
.slice(0, DEFAULT_EMBEDDING_CANDIDATES);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
241
|
export async function warmSkillEmbeddings({
|
|
242
242
|
cwd = process.cwd(),
|
|
243
243
|
dataDir,
|
|
@@ -245,304 +245,81 @@ export async function warmSkillEmbeddings({
|
|
|
245
245
|
skills = scanSkills({ cwd })
|
|
246
246
|
} = {}) {
|
|
247
247
|
if (!dataDir || !skills.length) return { count: 0, cachePath: null };
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
248
|
+
const catalog = dedupeSkills(skills);
|
|
249
|
+
return warmIndexedEmbeddings({
|
|
250
|
+
kind: skillIndexKind(cwd),
|
|
251
|
+
items: catalog.map((skill) => ({
|
|
252
|
+
id: skillIndexId(skill),
|
|
253
|
+
text: skillEmbeddingText(skill)
|
|
254
|
+
})),
|
|
255
|
+
task: fusedProjectQuery({ prompt: "skill discovery semantic retrieval", cwd, dataDir }),
|
|
251
256
|
dataDir,
|
|
252
|
-
sources:
|
|
257
|
+
sources: catalog.map((skill) => skill.path).filter(Boolean),
|
|
253
258
|
allowRemote
|
|
254
259
|
});
|
|
255
260
|
}
|
|
256
261
|
|
|
257
|
-
function
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
);
|
|
270
|
-
const projectMatches = enriched.searchTokens.filter((token) => projectTokens.has(token) && SPECIALIZED_SKILL_TOKENS.has(token));
|
|
271
|
-
const normalizedName = enriched.normalizedName;
|
|
272
|
-
const nameTokens = enriched.nameTokens;
|
|
273
|
-
const nameHit = normalizedPrompt.includes(normalizedName);
|
|
274
|
-
const nameTokenHit = nameTokens.length > 1 && nameTokens.every((token) => promptTokens.has(token));
|
|
275
|
-
const scopeBonus = enriched.scope === "project" ? 0.08 : 0;
|
|
276
|
-
const intentBonus = skillIntentBonus(normalizedPrompt, enriched, projectTokens);
|
|
277
|
-
const relevancePriority = skillRelevancePriority(normalizedPrompt, enriched, projectTokens);
|
|
278
|
-
const domainEligible = isSkillDomainEligible(normalizedPrompt, enriched, projectTokens);
|
|
279
|
-
const matchScore = matches.reduce((sum, token) => sum + (SPECIALIZED_SKILL_TOKENS.has(token) ? 0.2 : 0.08), 0);
|
|
280
|
-
const projectBonus = intentBonus ? Math.min(0.16, projectMatches.length * 0.04) : 0;
|
|
281
|
-
const score = Math.min(1, (matches.length ? 0.25 + matchScore : 0) + projectBonus + intentBonus + (nameHit ? 0.2 : 0) + (nameTokenHit ? 0.18 : 0) + scopeBonus);
|
|
282
|
-
return {
|
|
283
|
-
id: `skill-${index + 1}`,
|
|
284
|
-
name,
|
|
285
|
-
description,
|
|
286
|
-
path: enriched.path,
|
|
287
|
-
scope: enriched.scope,
|
|
288
|
-
content,
|
|
289
|
-
score,
|
|
290
|
-
keywordScore: score,
|
|
291
|
-
relevancePriority,
|
|
292
|
-
domainEligible,
|
|
293
|
-
reasons: [
|
|
294
|
-
...(matches.length ? [`keyword:${matches.slice(0, 4).join(",")}`] : []),
|
|
295
|
-
...(projectBonus ? [`project:${projectMatches.slice(0, 4).join(",")}`] : []),
|
|
296
|
-
...(intentBonus ? ["intent-match"] : []),
|
|
297
|
-
...(nameHit || nameTokenHit ? ["name-match"] : [])
|
|
298
|
-
],
|
|
299
|
-
originalOrder: index
|
|
300
|
-
};
|
|
301
|
-
});
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
function filterSkillMatches(matches, { normalizedPrompt, enriched }) {
|
|
305
|
-
if (!/\beas\b/.test(normalizedPrompt)) return matches;
|
|
306
|
-
const skillText = normalize(`${enriched.name} ${enriched.description}`);
|
|
307
|
-
if (/\b(eas|expo|cicd)\b/.test(skillText)) return matches;
|
|
308
|
-
return matches.filter((token) => token !== "android" && token !== "ios");
|
|
262
|
+
function skillRule({ skill, index }) {
|
|
263
|
+
const enriched = skill.searchTokens ? skill : enrichSkill(skill);
|
|
264
|
+
return {
|
|
265
|
+
id: skillIndexId(enriched),
|
|
266
|
+
name: enriched.name,
|
|
267
|
+
description: enriched.description,
|
|
268
|
+
path: enriched.path,
|
|
269
|
+
scope: enriched.scope,
|
|
270
|
+
content: skillEmbeddingText(enriched),
|
|
271
|
+
score: 0,
|
|
272
|
+
originalOrder: index
|
|
273
|
+
};
|
|
309
274
|
}
|
|
310
275
|
|
|
311
|
-
function
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
276
|
+
function skillScoreFromEmbedding(skill, embeddingScore, reasons = []) {
|
|
277
|
+
const score = Math.min(1, Number(embeddingScore || 0));
|
|
278
|
+
return {
|
|
279
|
+
name: skill.name,
|
|
280
|
+
description: skill.description,
|
|
281
|
+
path: skill.path,
|
|
282
|
+
scope: skill.scope,
|
|
283
|
+
score,
|
|
284
|
+
embeddingScore: score,
|
|
285
|
+
reasons
|
|
286
|
+
};
|
|
321
287
|
}
|
|
322
288
|
|
|
323
|
-
function
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
}
|
|
358
|
-
if (/\beas\b/.test(normalizedPrompt)
|
|
359
|
-
&& /\b(eas|expo)\b/.test(skillText)
|
|
360
|
-
&& /\b(cicd|workflow|workflows|build|deploy|deployment|pipeline|pipelines)\b/.test(skillText)) {
|
|
361
|
-
return 0.28;
|
|
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;
|
|
289
|
+
function dedupeSkills(skills) {
|
|
290
|
+
const byName = new Map();
|
|
291
|
+
for (const skill of skills || []) {
|
|
292
|
+
const enriched = skill.searchTokens ? skill : enrichSkill(skill);
|
|
293
|
+
const key = normalize(enriched.name);
|
|
294
|
+
if (!key) continue;
|
|
295
|
+
const existing = byName.get(key);
|
|
296
|
+
if (!existing || skillSourcePriority(enriched) > skillSourcePriority(existing)) {
|
|
297
|
+
byName.set(key, enriched);
|
|
298
|
+
}
|
|
370
299
|
}
|
|
371
|
-
return
|
|
300
|
+
return [...byName.values()];
|
|
372
301
|
}
|
|
373
302
|
|
|
374
|
-
function
|
|
375
|
-
const skillText = normalize(`${enriched.name} ${enriched.description}`);
|
|
376
|
-
const skillName = normalize(enriched.name);
|
|
303
|
+
function skillSourcePriority(skill) {
|
|
377
304
|
let priority = 0;
|
|
378
|
-
if (
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
-
}
|
|
305
|
+
if (skill.scope === "project") priority += 100;
|
|
306
|
+
const skillPath = String(skill.path || "");
|
|
307
|
+
if (skillPath.includes(`${path.sep}.codex${path.sep}skills${path.sep}`)) priority += 30;
|
|
308
|
+
if (skillPath.includes(`${path.sep}.config${path.sep}skillshare${path.sep}skills${path.sep}`)) priority += 20;
|
|
309
|
+
if (skillPath.includes(`${path.sep}.agents${path.sep}skills${path.sep}`)) priority += 10;
|
|
441
310
|
return priority;
|
|
442
311
|
}
|
|
443
312
|
|
|
444
|
-
function
|
|
445
|
-
return
|
|
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);
|
|
313
|
+
function skillIndexKind(cwd) {
|
|
314
|
+
return `skill:${path.resolve(cwd)}`;
|
|
466
315
|
}
|
|
467
316
|
|
|
468
|
-
function
|
|
469
|
-
return
|
|
317
|
+
function skillIndexId(skill) {
|
|
318
|
+
return normalize(skill.name);
|
|
470
319
|
}
|
|
471
320
|
|
|
472
|
-
function
|
|
473
|
-
return
|
|
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;
|
|
321
|
+
function skillEmbeddingText(skill) {
|
|
322
|
+
return [skill.name, skill.description].filter(Boolean).join("\n");
|
|
546
323
|
}
|
|
547
324
|
|
|
548
325
|
export function projectSkillHints({ cwd = process.cwd() } = {}) {
|
|
@@ -567,48 +344,6 @@ export function projectSkillHints({ cwd = process.cwd() } = {}) {
|
|
|
567
344
|
return [...hints];
|
|
568
345
|
}
|
|
569
346
|
|
|
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
|
-
|
|
612
347
|
function readJson(filePath) {
|
|
613
348
|
try {
|
|
614
349
|
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
@@ -639,10 +374,3 @@ function enrichSkill(skill) {
|
|
|
639
374
|
function normalize(value) {
|
|
640
375
|
return String(value || "").toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
|
|
641
376
|
}
|
|
642
|
-
|
|
643
|
-
function normalizePrompt(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"));
|
|
648
|
-
}
|