@minhpnq1807/contextos 0.5.50 → 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 +7 -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 +49 -10
- 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,12 @@
|
|
|
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
|
+
|
|
3
10
|
## 0.5.50
|
|
4
11
|
|
|
5
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.
|
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
|
|
|
@@ -167,22 +167,18 @@ export async function suggestSkills({
|
|
|
167
167
|
limit = DEFAULT_LIMIT,
|
|
168
168
|
timeoutMs = Number(process.env.CONTEXTOS_SKILL_EMBEDDING_TIMEOUT_MS || process.env.CONTEXTOS_EMBEDDING_TIMEOUT_MS || DEFAULT_SKILL_TIMEOUT_MS),
|
|
169
169
|
indexedSearcher = searchIndexedEmbeddings,
|
|
170
|
-
embeddingEnhancer = enhanceRuleScoresWithEmbeddings
|
|
170
|
+
embeddingEnhancer = enhanceRuleScoresWithEmbeddings,
|
|
171
|
+
embeddingsEnabled = true
|
|
171
172
|
} = {}) {
|
|
172
173
|
if (!String(prompt || "").trim() || !skills.length) return [];
|
|
173
174
|
const catalog = dedupeSkills(skills);
|
|
174
|
-
const query =
|
|
175
|
+
const query = skillQuery({ prompt, cwd, dataDir });
|
|
175
176
|
const byId = new Map(catalog.map((skill) => [skillIndexId(skill), skill]));
|
|
176
177
|
const explicitSkills = explicitSkillSuggestions({ prompt, byId });
|
|
178
|
+
if (!embeddingsEnabled) return finalizeSkillScores(explicitSkills, limit);
|
|
177
179
|
|
|
178
180
|
if (dataDir) {
|
|
179
|
-
const indexed = await
|
|
180
|
-
kind: skillIndexKind(cwd),
|
|
181
|
-
task: query,
|
|
182
|
-
dataDir,
|
|
183
|
-
timeoutMs,
|
|
184
|
-
allowRemote: false
|
|
185
|
-
});
|
|
181
|
+
const indexed = await searchSkillIndexes({ cwd, query, dataDir, timeoutMs, indexedSearcher });
|
|
186
182
|
if (indexed.status === "enabled" && indexed.items.length) {
|
|
187
183
|
return finalizeSkillScores([
|
|
188
184
|
...explicitSkills,
|
|
@@ -212,6 +208,12 @@ export async function suggestSkills({
|
|
|
212
208
|
return finalizeSkillScores([...explicitSkills, ...embedding.rules], limit);
|
|
213
209
|
}
|
|
214
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
|
+
|
|
215
217
|
function explicitSkillSuggestions({ prompt = "", byId = new Map() } = {}) {
|
|
216
218
|
const names = extractExplicitSkillNames(prompt);
|
|
217
219
|
return names
|
|
@@ -275,7 +277,7 @@ export async function warmSkillEmbeddings({
|
|
|
275
277
|
} = {}) {
|
|
276
278
|
if (!dataDir || !skills.length) return { count: 0, cachePath: null };
|
|
277
279
|
const catalog = dedupeSkills(skills);
|
|
278
|
-
|
|
280
|
+
const workspaceResult = await warmIndexedEmbeddings({
|
|
279
281
|
kind: skillIndexKind(cwd),
|
|
280
282
|
items: catalog.map((skill) => ({
|
|
281
283
|
id: skillIndexId(skill),
|
|
@@ -286,6 +288,19 @@ export async function warmSkillEmbeddings({
|
|
|
286
288
|
sources: catalog.map((skill) => skill.path).filter(Boolean),
|
|
287
289
|
allowRemote
|
|
288
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;
|
|
289
304
|
}
|
|
290
305
|
|
|
291
306
|
function skillRule({ skill, index }) {
|
|
@@ -343,6 +358,30 @@ function skillIndexKind(cwd) {
|
|
|
343
358
|
return `skill:${path.resolve(cwd)}`;
|
|
344
359
|
}
|
|
345
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
|
+
|
|
346
385
|
function skillIndexId(skill) {
|
|
347
386
|
return normalize(skill.name);
|
|
348
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,
|