@minhpnq1807/contextos 0.5.49 → 0.5.51
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 +12 -0
- package/bin/ctx.js +5 -1
- package/package.json +1 -1
- package/plugins/ctx/.codex-plugin/plugin.json +1 -1
- package/plugins/ctx/lib/analyzer.js +17 -2
- package/plugins/ctx/lib/auto-warm.js +1 -0
- package/plugins/ctx/lib/ctx-mcp-client.js +2 -0
- package/plugins/ctx/lib/hook-io.js +11 -1
- package/plugins/ctx/lib/project-profiler.js +5 -1
- package/plugins/ctx/lib/prompt-hook.js +5 -1
- package/plugins/ctx/lib/score-context.js +13 -2
- package/plugins/ctx/lib/skill-discoverer.js +83 -15
- package/plugins/ctx/lib/skillshare-sync.js +112 -0
- package/plugins/ctx/lib/workflow-discoverer.js +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.51
|
|
4
|
+
|
|
5
|
+
- **Faster prompt fallback:** Direct prompt-hook fallback now skips embedding work and uses a shorter timeout, so context injection can still return deterministic rule, file, skill, and workflow candidates when MCP or semantic scoring is unavailable.
|
|
6
|
+
- **Shared skill index fallback:** Skill discovery now warms a shared global skill index and searches it when the workspace-specific skill index has no matches, improving reuse across projects.
|
|
7
|
+
- **Agent-visible skill dedupe:** Community skill installs and skill sync now remove duplicate skills visible through shared, Codex, and Antigravity roots while preserving unique agent-specific skills.
|
|
8
|
+
- **Workspace prompt path detection:** Explicit file paths in prompts now tolerate line and column suffixes and can resolve files from workspace packages, improving suggested-file accuracy in monorepos.
|
|
9
|
+
|
|
10
|
+
## 0.5.50
|
|
11
|
+
|
|
12
|
+
- **Explicit skill activation:** Prompt skills named with `$skill-name` are now preserved and ranked before semantic suggestions, so user-requested skills such as `$threejs` or `$design-taste-frontend` appear in prompt context even when semantic ranking would not select them.
|
|
13
|
+
- **Agents skill root discovery:** Skill discovery now scans project and global `.agents/skills` roots in addition to Codex, Claude, Gemini, and skillshare roots.
|
|
14
|
+
|
|
3
15
|
## 0.5.49
|
|
4
16
|
|
|
5
17
|
- **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.
|
package/bin/ctx.js
CHANGED
|
@@ -29,7 +29,7 @@ import { installCopilotMcp } from "../plugins/ctx/lib/copilot-mcp.js";
|
|
|
29
29
|
import { readCodexMcpServers, syncRules } from "../plugins/ctx/lib/ruler-sync.js";
|
|
30
30
|
import { detectGraphStrategy, embedCodeReviewGraph, formatCodeReviewGraphEmbedding, formatGraphStrategy } from "../plugins/ctx/lib/graph-strategy.js";
|
|
31
31
|
import { writeInnerGitignore, ensureRootGitignore } from "../plugins/ctx/lib/gitignore.js";
|
|
32
|
-
import { repairSkillSymlinks, syncSkills, detectExistingSkills } from "../plugins/ctx/lib/skillshare-sync.js";
|
|
32
|
+
import { dedupeAgentVisibleSkills, repairSkillSymlinks, syncSkills, detectExistingSkills } from "../plugins/ctx/lib/skillshare-sync.js";
|
|
33
33
|
import { scanSkills, warmSkillEmbeddings } from "../plugins/ctx/lib/skill-discoverer.js";
|
|
34
34
|
import { parsePassthroughArgs, runPassthrough } from "../plugins/ctx/lib/passthrough.js";
|
|
35
35
|
import { parseAgentList, parseSetupArgs, setupSummaryLines } from "../plugins/ctx/lib/setup-wizard.js";
|
|
@@ -143,6 +143,10 @@ async function runCommunitySkillInstaller(agents = []) {
|
|
|
143
143
|
if (afterRepair.repaired.length || afterRepair.removedBroken.length) {
|
|
144
144
|
console.log(`${DIM}│${RESET} Repaired ${afterRepair.repaired.length} skill links after install.`);
|
|
145
145
|
}
|
|
146
|
+
const deduped = dedupeAgentVisibleSkills({ cwd: process.cwd(), home: os.homedir(), agents });
|
|
147
|
+
if (deduped.removed.length) {
|
|
148
|
+
console.log(`${DIM}│${RESET} Removed ${deduped.removed.length} duplicate agent-visible skills.`);
|
|
149
|
+
}
|
|
146
150
|
successCount++;
|
|
147
151
|
|
|
148
152
|
if (installInfo.verify) {
|
package/package.json
CHANGED
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { findGraphRelevantFiles, mergeRelevantFiles } from "./graph-retriever.js";
|
|
4
4
|
import { expandImportGraph } from "./import-graph.js";
|
|
5
5
|
import { findEmbeddingRelevantFiles } from "./file-embedding-retriever.js";
|
|
6
|
+
import { workspacePackagePaths } from "./project-profiler.js";
|
|
6
7
|
|
|
7
8
|
const STOP_WORDS = new Set([
|
|
8
9
|
"a", "an", "and", "are", "as", "at", "be", "by", "cho", "co", "cua", "do", "fix", "for",
|
|
@@ -433,9 +434,9 @@ function addAll(target, values) {
|
|
|
433
434
|
export function findExplicitPromptFiles({ cwd = process.cwd(), task = "", limit = 6 } = {}) {
|
|
434
435
|
const candidates = new Set();
|
|
435
436
|
const normalizedTask = String(task || "").replace(/\/\s+/g, "/");
|
|
436
|
-
const matches = normalizedTask.match(/[A-Za-z0-9_.()[\]
|
|
437
|
+
const matches = normalizedTask.match(/[A-Za-z0-9_.()[\]@~:,-]+(?:\/[A-Za-z0-9_.()[\]@~:,-]+)+/g) || [];
|
|
437
438
|
for (const match of matches) {
|
|
438
|
-
const cleaned = match
|
|
439
|
+
const cleaned = cleanPromptFilePath(match);
|
|
439
440
|
for (const filePath of resolvePromptPathCandidates({ cwd, promptPath: cleaned })) {
|
|
440
441
|
candidates.add(filePath);
|
|
441
442
|
if (candidates.size >= limit) break;
|
|
@@ -450,6 +451,13 @@ export function findExplicitPromptFiles({ cwd = process.cwd(), task = "", limit
|
|
|
450
451
|
}));
|
|
451
452
|
}
|
|
452
453
|
|
|
454
|
+
function cleanPromptFilePath(value) {
|
|
455
|
+
return String(value || "")
|
|
456
|
+
.replace(/\.(tsx?|jsx?|mjs|cjs|json|md|sql|py)\(\d+(?:,\d+)?\)[),.;:]*$/i, ".$1")
|
|
457
|
+
.replace(/\.(tsx?|jsx?|mjs|cjs|json|md|sql|py):\d+(?::\d+)?[),.;:]*$/i, ".$1")
|
|
458
|
+
.replace(/[),.;:]+$/g, "");
|
|
459
|
+
}
|
|
460
|
+
|
|
453
461
|
function resolvePromptPathCandidates({ cwd, promptPath }) {
|
|
454
462
|
if (!promptPath || promptPath.includes("://")) return [];
|
|
455
463
|
const relative = promptPath.replace(/^\.?\//, "");
|
|
@@ -470,6 +478,13 @@ function resolvePromptPathCandidates({ cwd, promptPath }) {
|
|
|
470
478
|
if (isSourceFile(candidate)) resolved.push(path.relative(cwd, candidate));
|
|
471
479
|
}
|
|
472
480
|
}
|
|
481
|
+
if (!resolved.length && !relative.startsWith("..")) {
|
|
482
|
+
for (const packagePath of workspacePackagePaths(cwd).slice(1)) {
|
|
483
|
+
const packageDir = path.dirname(packagePath);
|
|
484
|
+
const candidate = path.join(packageDir, relative);
|
|
485
|
+
if (isSourceFile(candidate)) resolved.push(path.relative(cwd, candidate));
|
|
486
|
+
}
|
|
487
|
+
}
|
|
473
488
|
return resolved;
|
|
474
489
|
}
|
|
475
490
|
|
|
@@ -57,6 +57,7 @@ export function maybeAutoWarmWorkspace({
|
|
|
57
57
|
function shouldAutoWarm(reason) {
|
|
58
58
|
if (reason === "no-context-candidates") return true;
|
|
59
59
|
if (reason === "enabled-sections-empty-after-formatting") return true;
|
|
60
|
+
if (String(reason || "").startsWith("enabled-sections-missing-candidates:")) return true;
|
|
60
61
|
return false;
|
|
61
62
|
}
|
|
62
63
|
|
|
@@ -40,6 +40,7 @@ export async function callCtxScoreContext(payload, {
|
|
|
40
40
|
let raw = "";
|
|
41
41
|
let responseTimer;
|
|
42
42
|
const connectTimer = setTimeout(() => {
|
|
43
|
+
invalidateSocketIfUnchanged(socketPath, socketIdentity);
|
|
43
44
|
client.destroy();
|
|
44
45
|
reject(new Error(`ctx-mcp bridge connect timed out after ${connectTimeoutMs}ms`));
|
|
45
46
|
}, connectTimeoutMs);
|
|
@@ -47,6 +48,7 @@ export async function callCtxScoreContext(payload, {
|
|
|
47
48
|
client.on("connect", () => {
|
|
48
49
|
clearTimeout(connectTimer);
|
|
49
50
|
responseTimer = setTimeout(() => {
|
|
51
|
+
invalidateSocketIfUnchanged(socketPath, socketIdentity);
|
|
50
52
|
client.destroy();
|
|
51
53
|
reject(new Error(`ctx-mcp bridge timed out after ${timeoutMs}ms`));
|
|
52
54
|
}, timeoutMs);
|
|
@@ -3,6 +3,8 @@ import path from "node:path";
|
|
|
3
3
|
import { writeJsonFile } from "./fs-utils.js";
|
|
4
4
|
import { defaultDataRoot, workspaceDataDir } from "./workspace-data.js";
|
|
5
5
|
|
|
6
|
+
let pendingStdoutWrites = 0;
|
|
7
|
+
|
|
6
8
|
export async function readStdinJson() {
|
|
7
9
|
const chunks = [];
|
|
8
10
|
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
@@ -13,13 +15,21 @@ export async function readStdinJson() {
|
|
|
13
15
|
|
|
14
16
|
export function writeJson(value) {
|
|
15
17
|
try {
|
|
16
|
-
|
|
18
|
+
pendingStdoutWrites += 1;
|
|
19
|
+
process.stdout.write(`${JSON.stringify(value)}\n`, () => {
|
|
20
|
+
pendingStdoutWrites = Math.max(0, pendingStdoutWrites - 1);
|
|
21
|
+
});
|
|
17
22
|
} catch (error) {
|
|
23
|
+
pendingStdoutWrites = Math.max(0, pendingStdoutWrites - 1);
|
|
18
24
|
if (error?.code !== "EPIPE") throw error;
|
|
19
25
|
}
|
|
20
26
|
}
|
|
21
27
|
|
|
22
28
|
export function exitAfterStdout(code = 0) {
|
|
29
|
+
if (pendingStdoutWrites > 0) {
|
|
30
|
+
setImmediate(() => exitAfterStdout(code));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
23
33
|
if (process.stdout.writableNeedDrain) {
|
|
24
34
|
process.stdout.once("drain", () => process.exit(code));
|
|
25
35
|
return;
|
|
@@ -6,6 +6,7 @@ const PROFILE_CACHE_FILE = "project-profile.json";
|
|
|
6
6
|
const MAX_DEPENDENCIES = 80;
|
|
7
7
|
const MAX_SCRIPTS = 30;
|
|
8
8
|
const MAX_RECENT_FILES = 20;
|
|
9
|
+
const MAX_FUSED_PROFILE_CHARS = 500;
|
|
9
10
|
|
|
10
11
|
export function projectProfile({ cwd = process.cwd(), dataDir } = {}) {
|
|
11
12
|
const fingerprint = projectFingerprint(cwd);
|
|
@@ -30,7 +31,10 @@ export function projectProfile({ cwd = process.cwd(), dataDir } = {}) {
|
|
|
30
31
|
|
|
31
32
|
export function fusedProjectQuery({ prompt = "", cwd = process.cwd(), dataDir } = {}) {
|
|
32
33
|
const profile = projectProfile({ cwd, dataDir });
|
|
33
|
-
|
|
34
|
+
const profileSignal = profile.embeddableString
|
|
35
|
+
? profile.embeddableString.slice(0, MAX_FUSED_PROFILE_CHARS)
|
|
36
|
+
: "";
|
|
37
|
+
return [String(prompt || "").trim(), profileSignal].filter(Boolean).join("\n");
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
function readCachedProfile(cachePath, fingerprint) {
|
|
@@ -21,7 +21,7 @@ export async function handlePromptPayload(
|
|
|
21
21
|
autoWarmWorkspace = maybeAutoWarmWorkspace,
|
|
22
22
|
mcpDataDir,
|
|
23
23
|
outputConfig,
|
|
24
|
-
directFallbackTimeoutMs = Number(process.env.CONTEXTOS_DIRECT_FALLBACK_TIMEOUT_MS ||
|
|
24
|
+
directFallbackTimeoutMs = Number(process.env.CONTEXTOS_DIRECT_FALLBACK_TIMEOUT_MS || 2500)
|
|
25
25
|
} = {}
|
|
26
26
|
) {
|
|
27
27
|
const prompt = payload.prompt || payload.message || payload.user_prompt || "";
|
|
@@ -55,6 +55,7 @@ export async function handlePromptPayload(
|
|
|
55
55
|
maxSkills: promptLimits.skills,
|
|
56
56
|
maxWorkflows: promptLimits.workflows,
|
|
57
57
|
dataDir: mcpDataDir || dataDir,
|
|
58
|
+
allowEmbeddings: false,
|
|
58
59
|
embeddingTimeoutMs: Number(process.env.CONTEXTOS_HOOK_EMBEDDING_TIMEOUT_MS || 500),
|
|
59
60
|
fileEmbeddingTimeoutMs: Number(process.env.CONTEXTOS_HOOK_FILE_EMBEDDING_TIMEOUT_MS || 1000),
|
|
60
61
|
skillEmbeddingTimeoutMs: Number(process.env.CONTEXTOS_HOOK_SKILL_EMBEDDING_TIMEOUT_MS || 2000)
|
|
@@ -197,6 +198,9 @@ function emptyContextReason({ scheduled, outputConfig, injectContext }) {
|
|
|
197
198
|
if (scheduled.suggestedSkills?.length) available.push("skills");
|
|
198
199
|
if (scheduled.suggestedWorkflows?.length) available.push("workflows");
|
|
199
200
|
if (!available.length) return "no-context-candidates";
|
|
201
|
+
const enabledMissing = ["rules", "files", "skills", "workflows"]
|
|
202
|
+
.filter((section) => sections[section] !== false && !available.includes(section));
|
|
203
|
+
if (enabledMissing.length) return `enabled-sections-missing-candidates:${enabledMissing.join(",")}`;
|
|
200
204
|
const enabled = available.filter((section) => sections[section] !== false);
|
|
201
205
|
return enabled.length ? "enabled-sections-empty-after-formatting" : `available-sections-disabled:${available.join(",")}`;
|
|
202
206
|
}
|
|
@@ -19,7 +19,8 @@ export async function scoreContext({
|
|
|
19
19
|
embeddingTimeoutMs = 5000,
|
|
20
20
|
fileEmbeddingTimeoutMs = Number(process.env.CONTEXTOS_FILE_EMBEDDING_TIMEOUT_MS || 1000),
|
|
21
21
|
skillEmbeddingTimeoutMs = Number(process.env.CONTEXTOS_SKILL_EMBEDDING_TIMEOUT_MS || embeddingTimeoutMs),
|
|
22
|
-
skillSearchOptions = {}
|
|
22
|
+
skillSearchOptions = {},
|
|
23
|
+
allowEmbeddings = true
|
|
23
24
|
} = {}) {
|
|
24
25
|
const started = Date.now();
|
|
25
26
|
const ruleInputsPromise = Promise.resolve().then(() => {
|
|
@@ -35,6 +36,14 @@ export async function scoreContext({
|
|
|
35
36
|
});
|
|
36
37
|
|
|
37
38
|
const rulesPromise = ruleInputsPromise.then(({ merged, baseScoredRules }) => {
|
|
39
|
+
if (!allowEmbeddings) {
|
|
40
|
+
return {
|
|
41
|
+
rules: baseScoredRules,
|
|
42
|
+
status: "disabled",
|
|
43
|
+
model: null,
|
|
44
|
+
cachePath: dataDir
|
|
45
|
+
};
|
|
46
|
+
}
|
|
38
47
|
return enhanceRuleScoresWithEmbeddings(baseScoredRules, prompt, {
|
|
39
48
|
dataDir,
|
|
40
49
|
sources: merged.sources,
|
|
@@ -52,6 +61,7 @@ export async function scoreContext({
|
|
|
52
61
|
limit: maxFiles,
|
|
53
62
|
fileEmbeddingTimeoutMs,
|
|
54
63
|
fileEmbeddingOptions: {
|
|
64
|
+
enabled: allowEmbeddings,
|
|
55
65
|
allowRemote: false
|
|
56
66
|
}
|
|
57
67
|
});
|
|
@@ -68,6 +78,7 @@ export async function scoreContext({
|
|
|
68
78
|
dataDir,
|
|
69
79
|
limit: maxSkills,
|
|
70
80
|
timeoutMs: skillEmbeddingTimeoutMs,
|
|
81
|
+
embeddingsEnabled: allowEmbeddings,
|
|
71
82
|
...skillSearchOptions
|
|
72
83
|
})
|
|
73
84
|
};
|
|
@@ -77,7 +88,7 @@ export async function scoreContext({
|
|
|
77
88
|
const catalog = Array.isArray(workflows) ? workflows : scanWorkflows({ cwd });
|
|
78
89
|
return {
|
|
79
90
|
catalog,
|
|
80
|
-
suggestions: await suggestWorkflows({ prompt, workflows: catalog, dataDir, limit: maxWorkflows })
|
|
91
|
+
suggestions: await suggestWorkflows({ prompt, workflows: catalog, dataDir, limit: maxWorkflows, embeddingsEnabled: allowEmbeddings })
|
|
81
92
|
};
|
|
82
93
|
});
|
|
83
94
|
|
|
@@ -22,11 +22,13 @@ const scanCache = new Map();
|
|
|
22
22
|
export function skillSearchRoots({ cwd = process.cwd(), home = os.homedir() } = {}) {
|
|
23
23
|
return [
|
|
24
24
|
path.join(cwd, ".codex", "skills"),
|
|
25
|
+
path.join(cwd, ".agents", "skills"),
|
|
25
26
|
path.join(cwd, ".claude", "skills"),
|
|
26
27
|
path.join(cwd, ".gemini", "skills"),
|
|
27
28
|
path.join(cwd, ".gemini", "antigravity", "skills"),
|
|
28
29
|
path.join(cwd, ".gemini", "antigravity-cli", "skills"),
|
|
29
30
|
path.join(home, ".codex", "skills"),
|
|
31
|
+
path.join(home, ".agents", "skills"),
|
|
30
32
|
path.join(home, ".claude", "skills"),
|
|
31
33
|
path.join(home, ".config", "skillshare", "skills"),
|
|
32
34
|
path.join(home, ".gemini", "skills"),
|
|
@@ -165,36 +167,36 @@ export async function suggestSkills({
|
|
|
165
167
|
limit = DEFAULT_LIMIT,
|
|
166
168
|
timeoutMs = Number(process.env.CONTEXTOS_SKILL_EMBEDDING_TIMEOUT_MS || process.env.CONTEXTOS_EMBEDDING_TIMEOUT_MS || DEFAULT_SKILL_TIMEOUT_MS),
|
|
167
169
|
indexedSearcher = searchIndexedEmbeddings,
|
|
168
|
-
embeddingEnhancer = enhanceRuleScoresWithEmbeddings
|
|
170
|
+
embeddingEnhancer = enhanceRuleScoresWithEmbeddings,
|
|
171
|
+
embeddingsEnabled = true
|
|
169
172
|
} = {}) {
|
|
170
173
|
if (!String(prompt || "").trim() || !skills.length) return [];
|
|
171
174
|
const catalog = dedupeSkills(skills);
|
|
172
|
-
const query =
|
|
175
|
+
const query = skillQuery({ prompt, cwd, dataDir });
|
|
173
176
|
const byId = new Map(catalog.map((skill) => [skillIndexId(skill), skill]));
|
|
177
|
+
const explicitSkills = explicitSkillSuggestions({ prompt, byId });
|
|
178
|
+
if (!embeddingsEnabled) return finalizeSkillScores(explicitSkills, limit);
|
|
174
179
|
|
|
175
180
|
if (dataDir) {
|
|
176
|
-
const indexed = await
|
|
177
|
-
kind: skillIndexKind(cwd),
|
|
178
|
-
task: query,
|
|
179
|
-
dataDir,
|
|
180
|
-
timeoutMs,
|
|
181
|
-
allowRemote: false
|
|
182
|
-
});
|
|
181
|
+
const indexed = await searchSkillIndexes({ cwd, query, dataDir, timeoutMs, indexedSearcher });
|
|
183
182
|
if (indexed.status === "enabled" && indexed.items.length) {
|
|
184
|
-
return finalizeSkillScores(
|
|
183
|
+
return finalizeSkillScores([
|
|
184
|
+
...explicitSkills,
|
|
185
|
+
...indexed.items
|
|
185
186
|
.map((item) => {
|
|
186
187
|
const skill = byId.get(item.id);
|
|
187
188
|
if (!skill) return null;
|
|
188
189
|
return skillScoreFromEmbedding(skill, item.embeddingScore, [`embedding:${Number(item.embeddingScore || 0).toFixed(2)}`]);
|
|
189
190
|
})
|
|
190
|
-
.filter(Boolean)
|
|
191
|
+
.filter(Boolean)
|
|
192
|
+
], limit);
|
|
191
193
|
}
|
|
192
194
|
}
|
|
193
195
|
|
|
194
|
-
if (catalog.length > DEFAULT_EMBEDDING_CANDIDATES) return
|
|
196
|
+
if (catalog.length > DEFAULT_EMBEDDING_CANDIDATES) return finalizeSkillScores(explicitSkills, limit);
|
|
195
197
|
|
|
196
198
|
const embeddingCandidates = catalog.map((skill, index) => skillRule({ skill, index }));
|
|
197
|
-
if (!embeddingCandidates.length) return
|
|
199
|
+
if (!embeddingCandidates.length) return finalizeSkillScores(explicitSkills, limit);
|
|
198
200
|
|
|
199
201
|
const embedding = await embeddingEnhancer(embeddingCandidates, query, {
|
|
200
202
|
dataDir,
|
|
@@ -203,7 +205,36 @@ export async function suggestSkills({
|
|
|
203
205
|
allowRemote: false
|
|
204
206
|
});
|
|
205
207
|
|
|
206
|
-
return finalizeSkillScores(embedding.rules, limit);
|
|
208
|
+
return finalizeSkillScores([...explicitSkills, ...embedding.rules], limit);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function skillQuery({ prompt = "", cwd = process.cwd(), dataDir } = {}) {
|
|
212
|
+
const focusedPrompt = String(prompt || "").trim();
|
|
213
|
+
const fused = fusedProjectQuery({ prompt, cwd, dataDir });
|
|
214
|
+
return [focusedPrompt, focusedPrompt, fused].filter(Boolean).join("\n");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function explicitSkillSuggestions({ prompt = "", byId = new Map() } = {}) {
|
|
218
|
+
const names = extractExplicitSkillNames(prompt);
|
|
219
|
+
return names
|
|
220
|
+
.map((name, index) => ({ skill: byId.get(normalize(name)), index }))
|
|
221
|
+
.filter(({ skill }) => Boolean(skill))
|
|
222
|
+
.map(({ skill, index }) => skillScoreFromEmbedding(skill, 1 - index * 0.0001, ["explicit-skill"]));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function extractExplicitSkillNames(prompt = "") {
|
|
226
|
+
const names = [];
|
|
227
|
+
const seen = new Set();
|
|
228
|
+
const pattern = /(?:^|[\s([{,])\$([A-Za-z0-9][A-Za-z0-9_.:-]*)/g;
|
|
229
|
+
let match;
|
|
230
|
+
while ((match = pattern.exec(String(prompt || "")))) {
|
|
231
|
+
const name = match[1];
|
|
232
|
+
const key = normalize(name);
|
|
233
|
+
if (!key || seen.has(key)) continue;
|
|
234
|
+
seen.add(key);
|
|
235
|
+
names.push(name);
|
|
236
|
+
}
|
|
237
|
+
return names;
|
|
207
238
|
}
|
|
208
239
|
|
|
209
240
|
function finalizeSkillScores(skills, limit) {
|
|
@@ -246,7 +277,7 @@ export async function warmSkillEmbeddings({
|
|
|
246
277
|
} = {}) {
|
|
247
278
|
if (!dataDir || !skills.length) return { count: 0, cachePath: null };
|
|
248
279
|
const catalog = dedupeSkills(skills);
|
|
249
|
-
|
|
280
|
+
const workspaceResult = await warmIndexedEmbeddings({
|
|
250
281
|
kind: skillIndexKind(cwd),
|
|
251
282
|
items: catalog.map((skill) => ({
|
|
252
283
|
id: skillIndexId(skill),
|
|
@@ -257,6 +288,19 @@ export async function warmSkillEmbeddings({
|
|
|
257
288
|
sources: catalog.map((skill) => skill.path).filter(Boolean),
|
|
258
289
|
allowRemote
|
|
259
290
|
});
|
|
291
|
+
if (workspaceResult.status === "missing-model" || workspaceResult.status === "warm-failed") return workspaceResult;
|
|
292
|
+
await warmIndexedEmbeddings({
|
|
293
|
+
kind: sharedSkillIndexKind(),
|
|
294
|
+
items: catalog.map((skill) => ({
|
|
295
|
+
id: skillIndexId(skill),
|
|
296
|
+
text: skillEmbeddingText(skill)
|
|
297
|
+
})),
|
|
298
|
+
task: fusedProjectQuery({ prompt: "skill discovery semantic retrieval", cwd, dataDir }),
|
|
299
|
+
dataDir,
|
|
300
|
+
sources: catalog.map((skill) => skill.path).filter(Boolean),
|
|
301
|
+
allowRemote
|
|
302
|
+
});
|
|
303
|
+
return workspaceResult;
|
|
260
304
|
}
|
|
261
305
|
|
|
262
306
|
function skillRule({ skill, index }) {
|
|
@@ -314,6 +358,30 @@ function skillIndexKind(cwd) {
|
|
|
314
358
|
return `skill:${path.resolve(cwd)}`;
|
|
315
359
|
}
|
|
316
360
|
|
|
361
|
+
function sharedSkillIndexKind() {
|
|
362
|
+
return "skill:global";
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async function searchSkillIndexes({ cwd, query, dataDir, timeoutMs, indexedSearcher }) {
|
|
366
|
+
const workspace = await indexedSearcher({
|
|
367
|
+
kind: skillIndexKind(cwd),
|
|
368
|
+
task: query,
|
|
369
|
+
dataDir,
|
|
370
|
+
timeoutMs,
|
|
371
|
+
allowRemote: false
|
|
372
|
+
});
|
|
373
|
+
if (workspace.status === "enabled" && workspace.items.length) return workspace;
|
|
374
|
+
const shared = await indexedSearcher({
|
|
375
|
+
kind: sharedSkillIndexKind(),
|
|
376
|
+
task: query,
|
|
377
|
+
dataDir,
|
|
378
|
+
timeoutMs,
|
|
379
|
+
allowRemote: false
|
|
380
|
+
});
|
|
381
|
+
if (shared.status === "enabled" && shared.items.length) return shared;
|
|
382
|
+
return workspace.status === "enabled" ? workspace : shared;
|
|
383
|
+
}
|
|
384
|
+
|
|
317
385
|
function skillIndexId(skill) {
|
|
318
386
|
return normalize(skill.name);
|
|
319
387
|
}
|
|
@@ -244,6 +244,106 @@ export function repairSkillSymlinks({
|
|
|
244
244
|
return { repaired: [...new Set(repaired)], removedBroken: [...new Set(removedBroken)] };
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
+
export function dedupeAgentVisibleSkills({
|
|
248
|
+
cwd = process.cwd(),
|
|
249
|
+
home = os.homedir(),
|
|
250
|
+
agents = DEFAULT_AGENTS,
|
|
251
|
+
dryRun = false
|
|
252
|
+
} = {}) {
|
|
253
|
+
const roots = visibleSkillRootsForAgents({ cwd, home, agents });
|
|
254
|
+
const seen = new Map();
|
|
255
|
+
const kept = [];
|
|
256
|
+
const removed = [];
|
|
257
|
+
|
|
258
|
+
for (const root of roots) {
|
|
259
|
+
for (const skill of listSkillsInRoot(root)) {
|
|
260
|
+
const previous = seen.get(skill.key);
|
|
261
|
+
if (!previous) {
|
|
262
|
+
seen.set(skill.key, skill);
|
|
263
|
+
kept.push(skill.path);
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const previousReal = safeRealpath(previous.path);
|
|
268
|
+
const currentReal = safeRealpath(skill.path);
|
|
269
|
+
if (previousReal && currentReal && previousReal === currentReal) {
|
|
270
|
+
if (!dryRun) fs.rmSync(skill.path, { force: true, recursive: true });
|
|
271
|
+
removed.push(skill.path);
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (!dryRun) fs.rmSync(skill.path, { force: true, recursive: true });
|
|
276
|
+
removed.push(skill.path);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return { kept: [...new Set(kept)], removed: [...new Set(removed)], roots };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function visibleSkillRootsForAgents({ cwd, home, agents }) {
|
|
284
|
+
const normalizedAgents = normalizeAgentList(agents);
|
|
285
|
+
const roots = [];
|
|
286
|
+
const addShared = () => {
|
|
287
|
+
roots.push(path.join(home, ".agents", "skills"));
|
|
288
|
+
roots.push(path.join(cwd, ".agents", "skills"));
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
addShared();
|
|
292
|
+
if (normalizedAgents.includes("codex")) {
|
|
293
|
+
roots.push(path.join(home, ".codex", "skills"));
|
|
294
|
+
roots.push(path.join(cwd, ".codex", "skills"));
|
|
295
|
+
}
|
|
296
|
+
if (normalizedAgents.includes("claude")) {
|
|
297
|
+
roots.push(path.join(home, ".claude", "skills"));
|
|
298
|
+
roots.push(path.join(cwd, ".claude", "skills"));
|
|
299
|
+
}
|
|
300
|
+
if (normalizedAgents.includes("antigravity")) {
|
|
301
|
+
roots.push(path.join(home, ".gemini", "skills"));
|
|
302
|
+
roots.push(path.join(home, ".gemini", "antigravity", "skills"));
|
|
303
|
+
roots.push(path.join(home, ".gemini", "antigravity-cli", "skills"));
|
|
304
|
+
roots.push(path.join(cwd, ".gemini", "skills"));
|
|
305
|
+
roots.push(path.join(cwd, ".gemini", "antigravity", "skills"));
|
|
306
|
+
roots.push(path.join(cwd, ".gemini", "antigravity-cli", "skills"));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return uniquePaths(roots);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function listSkillsInRoot(root) {
|
|
313
|
+
return findSkillFiles(root)
|
|
314
|
+
.map((skillFile) => {
|
|
315
|
+
const skillDir = path.dirname(skillFile);
|
|
316
|
+
const name = readSkillName(skillFile) || path.basename(skillDir);
|
|
317
|
+
return {
|
|
318
|
+
name,
|
|
319
|
+
key: normalizeSkillName(name),
|
|
320
|
+
path: skillDir,
|
|
321
|
+
root
|
|
322
|
+
};
|
|
323
|
+
})
|
|
324
|
+
.filter((skill) => skill.key)
|
|
325
|
+
.sort((a, b) => a.path.localeCompare(b.path));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function readSkillName(skillFile) {
|
|
329
|
+
try {
|
|
330
|
+
const content = fs.readFileSync(skillFile, "utf8");
|
|
331
|
+
return content.match(/^\s*name:\s*(.+?)\s*$/m)?.[1]
|
|
332
|
+
?.replace(/^["']|["']$/g, "")
|
|
333
|
+
.trim() || "";
|
|
334
|
+
} catch {
|
|
335
|
+
return "";
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function normalizeSkillName(name) {
|
|
340
|
+
return String(name || "")
|
|
341
|
+
.trim()
|
|
342
|
+
.toLowerCase()
|
|
343
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
344
|
+
.replace(/^-+|-+$/g, "");
|
|
345
|
+
}
|
|
346
|
+
|
|
247
347
|
function skillRoots({ cwd, home }) {
|
|
248
348
|
return uniquePaths([
|
|
249
349
|
path.join(home, ".claude", "skills"),
|
|
@@ -503,6 +603,18 @@ export async function syncSkills({
|
|
|
503
603
|
const syncedCount = countSkillFiles(skillshareSourceDir({ home }));
|
|
504
604
|
logger(statusLine("Running skillshare sync...", options.dryRun ? "dry-run" : `✓ ${syncedCount} skills → ${options.agents.join(", ")}`));
|
|
505
605
|
|
|
606
|
+
const deduped = dedupeAgentVisibleSkills({
|
|
607
|
+
cwd,
|
|
608
|
+
home,
|
|
609
|
+
agents: options.agents,
|
|
610
|
+
dryRun: options.dryRun
|
|
611
|
+
});
|
|
612
|
+
if (deduped.removed.length) {
|
|
613
|
+
logger(statusLine("Deduping agent-visible skills...", options.dryRun
|
|
614
|
+
? `dry-run (${deduped.removed.length} duplicates)`
|
|
615
|
+
: `✓ ${deduped.removed.length} duplicates removed`));
|
|
616
|
+
}
|
|
617
|
+
|
|
506
618
|
let embeddings = { count: 0, cachePath: null, skipped: options.dryRun || options.noEmbeddings };
|
|
507
619
|
if (options.noEmbeddings) {
|
|
508
620
|
logger(statusLine("Rebuilding skill embeddings...", "skipped by --no-embeddings"));
|
|
@@ -128,12 +128,14 @@ export async function suggestWorkflows({
|
|
|
128
128
|
workflows = [],
|
|
129
129
|
dataDir,
|
|
130
130
|
limit = DEFAULT_LIMIT,
|
|
131
|
-
timeoutMs = Number(process.env.CONTEXTOS_WORKFLOW_EMBEDDING_TIMEOUT_MS || process.env.CONTEXTOS_EMBEDDING_TIMEOUT_MS || 800)
|
|
131
|
+
timeoutMs = Number(process.env.CONTEXTOS_WORKFLOW_EMBEDDING_TIMEOUT_MS || process.env.CONTEXTOS_EMBEDDING_TIMEOUT_MS || 800),
|
|
132
|
+
embeddingsEnabled = true
|
|
132
133
|
} = {}) {
|
|
133
134
|
if (!String(prompt || "").trim() || !workflows.length) return [];
|
|
134
135
|
const base = scoreWorkflowsByKeyword({ prompt, workflows });
|
|
135
136
|
const embeddingCandidates = selectWorkflowEmbeddingCandidates(base);
|
|
136
137
|
if (!embeddingCandidates.length) return [];
|
|
138
|
+
if (!embeddingsEnabled) return finalizeWorkflowScores(embeddingCandidates, limit);
|
|
137
139
|
|
|
138
140
|
const embedding = await enhanceRuleScoresWithEmbeddings(embeddingCandidates, prompt, {
|
|
139
141
|
dataDir,
|