@minhpnq1807/contextos 0.3.1 → 0.5.1
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 +21 -0
- package/README.md +32 -1
- package/bin/ctx.js +45 -3
- package/package.json +2 -2
- package/plugins/ctx/lib/prompt-hook.js +5 -2
- package/plugins/ctx/lib/scheduler.js +11 -1
- package/plugins/ctx/lib/score-context.js +13 -0
- package/plugins/ctx/lib/skill-discoverer.js +232 -0
- package/plugins/ctx/lib/skillshare-sync.js +237 -0
- package/plugins/ctx/mcp/contextos-server.js +13 -3
- package/plugins/ctx/mcp/server.js +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.1
|
|
4
|
+
|
|
5
|
+
- Fixes `ctx sync --skills` first-run ordering by running `skillshare init` before `skillshare backup`, matching skillshare's config requirement.
|
|
6
|
+
|
|
7
|
+
## 0.5.0
|
|
8
|
+
|
|
9
|
+
- Adds `ctx sync --skills` for skillshare-backed skill sync across Codex, Claude Code, and Antigravity.
|
|
10
|
+
- Detects existing global/project skill directories, backs them up, optionally collects them into skillshare, runs sync, and rebuilds skill embeddings.
|
|
11
|
+
- Adds `~/.config/skillshare/skills` to skill discovery roots so ContextOS ranks the shared source of truth after sync.
|
|
12
|
+
|
|
13
|
+
## 0.4.1
|
|
14
|
+
|
|
15
|
+
- Adds Antigravity skill discovery roots for `.gemini/skills`, `.gemini/antigravity/skills`, and `.gemini/antigravity-cli/skills`.
|
|
16
|
+
- Raises the skill catalog scan cap to cover large Antigravity skill catalogs before ranking.
|
|
17
|
+
|
|
18
|
+
## 0.4.0
|
|
19
|
+
|
|
20
|
+
- Adds prompt-aware skill discovery to `ctx_score_context`, returning `suggestedSkills` alongside rules and files.
|
|
21
|
+
- Scans project/global `.codex/skills`, `.claude/skills`, and Antigravity `.gemini/**/skills` catalogs, ranks skill `name` + `description`, and injects top skill hints into prompt context.
|
|
22
|
+
- Warms skill embeddings during `ctx install` and `ctx embeddings warm`.
|
|
23
|
+
|
|
3
24
|
## 0.3.0
|
|
4
25
|
|
|
5
26
|
- Adds `ctx sync --rules` for Ruler-backed project rule/MCP sync across Codex, Claude Code, and Antigravity.
|
package/README.md
CHANGED
|
@@ -77,6 +77,7 @@ With ContextOS, each prompt gets a compact block:
|
|
|
77
77
|
- Registers a `ctx-mcp` MCP server that owns model loading and semantic scoring.
|
|
78
78
|
- Reads the active `AGENTS.md` chain for the current workspace.
|
|
79
79
|
- Scores rules by relevance to the user prompt.
|
|
80
|
+
- Scans project/global `.codex/skills`, `.claude/skills`, and Antigravity `.gemini/**/skills`, ranks skill descriptions by task relevance, and injects top skill hints.
|
|
80
81
|
- Filters host/session setup rules such as "run commands as user X" or `sudo -u user` because they are environment instructions, not project guidance.
|
|
81
82
|
- Finds likely relevant files with a hybrid retriever:
|
|
82
83
|
- first, local prompt/file heuristics create seed candidates;
|
|
@@ -168,6 +169,32 @@ npm exec --yes --package=@minhpnq1807/contextos@latest -- ctx --version
|
|
|
168
169
|
npm exec --yes --package=@minhpnq1807/contextos@latest -- ctx debug -- "Recheck authen flow"
|
|
169
170
|
```
|
|
170
171
|
|
|
172
|
+
## Skill Sync
|
|
173
|
+
|
|
174
|
+
Use skillshare when you want Codex, Claude Code, and Antigravity to share one skills catalog:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
ctx sync --skills
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
ContextOS checks for `skillshare`, initializes it when needed, backs up existing skills before collection, runs `skillshare collect --all` unless `--no-collect` is provided, then runs `skillshare sync`. After sync, ContextOS rebuilds skill embeddings so prompt-time skill discovery can rank the shared source immediately.
|
|
181
|
+
|
|
182
|
+
The shared source is:
|
|
183
|
+
|
|
184
|
+
```text
|
|
185
|
+
~/.config/skillshare/skills/
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Useful variants:
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
ctx sync --skills --dry-run
|
|
192
|
+
ctx sync --skills --no-collect
|
|
193
|
+
ctx sync --skills --agents codex,claude
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
After this, `ctx debug -- "task"` and prompt hooks can suggest skills from `~/.config/skillshare/skills/` plus agent-specific skill folders.
|
|
197
|
+
|
|
171
198
|
## Modes
|
|
172
199
|
|
|
173
200
|
Injection mode is the default:
|
|
@@ -287,7 +314,11 @@ This warning comes from a transitive dependency in the local embedding/WASM stac
|
|
|
287
314
|
| `ctx sync --rules --dry-run` | Previews Ruler sync without writing files or running apply. | You want to inspect behavior before changing project config. | Prints the same flow with dry-run status. |
|
|
288
315
|
| `ctx sync --rules --force` | Rewrites ContextOS-owned Ruler sections. | You changed the ContextOS install path or need to refresh `ctx-mcp`. | Removes and re-adds ContextOS-owned `mcp`, `mcp_servers.ctx-mcp`, and selected agent sections. |
|
|
289
316
|
| `ctx sync --rules --no-import-codex-mcp` | Skips Codex MCP import. | You only want ContextOS' own `ctx-mcp` in Ruler. | Does not read `~/.codex/config.toml`. |
|
|
290
|
-
| `ctx
|
|
317
|
+
| `ctx sync --skills` | Syncs agent skills through skillshare. | You want Codex, Claude Code, and Antigravity to share one skill source. | Installs or verifies `skillshare`, initializes it if needed, backs up and collects existing skills unless skipped, runs `skillshare sync`, and rebuilds ContextOS skill embeddings. |
|
|
318
|
+
| `ctx sync --skills --agents <list>` | Syncs skills only for selected agents. | You want to target a subset such as `codex,claude`. | Runs `skillshare sync --agents <list>` and refreshes skill embeddings. |
|
|
319
|
+
| `ctx sync --skills --dry-run` | Previews skillshare sync. | You want to inspect behavior before changing skill directories. | Runs `skillshare sync --dry-run` and skips embedding rebuild. |
|
|
320
|
+
| `ctx sync --skills --no-collect` | Skips collecting existing agent skills into skillshare. | You already manage `~/.config/skillshare/skills` and only want to push it out. | Initializes/syncs skillshare without running `skillshare backup` or `skillshare collect --all`. |
|
|
321
|
+
| `ctx embeddings warm -- "task"` | Prepares local semantic embedding caches. | First install, CI smoke checks, or after changing AGENTS.md/project files/skills. | Loads/downloads `Xenova/all-MiniLM-L6-v2` and writes rule, file-path, and skill vectors to `~/.ctx/contextos/embeddings.db`. |
|
|
291
322
|
| `ctx --version` | Prints the installed ContextOS CLI version. | You want to confirm which npm version is being executed. | Prints the version from package metadata. |
|
|
292
323
|
|
|
293
324
|
## Runtime Files
|
package/bin/ctx.js
CHANGED
|
@@ -22,6 +22,8 @@ import { installClaudeMcp } from "../plugins/ctx/lib/claude-mcp.js";
|
|
|
22
22
|
import { installAntigravityHooks } from "../plugins/ctx/lib/antigravity-hooks.js";
|
|
23
23
|
import { installAntigravityMcp } from "../plugins/ctx/lib/antigravity-mcp.js";
|
|
24
24
|
import { syncRules } from "../plugins/ctx/lib/ruler-sync.js";
|
|
25
|
+
import { syncSkills } from "../plugins/ctx/lib/skillshare-sync.js";
|
|
26
|
+
import { scanSkills, warmSkillEmbeddings } from "../plugins/ctx/lib/skill-discoverer.js";
|
|
25
27
|
|
|
26
28
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
27
29
|
const rootDir = path.resolve(__dirname, "..");
|
|
@@ -48,6 +50,10 @@ Usage:
|
|
|
48
50
|
ctx sync --rules --agents codex,claude,antigravity
|
|
49
51
|
ctx sync --rules --dry-run
|
|
50
52
|
ctx sync --rules --no-import-codex-mcp
|
|
53
|
+
ctx sync --skills
|
|
54
|
+
ctx sync --skills --dry-run
|
|
55
|
+
ctx sync --skills --no-collect
|
|
56
|
+
ctx sync --skills --agents codex,claude,antigravity
|
|
51
57
|
ctx embeddings warm -- "task"
|
|
52
58
|
ctx --version
|
|
53
59
|
`;
|
|
@@ -96,6 +102,7 @@ async function install({ copy = false, inject = true, agent = "codex" } = {}) {
|
|
|
96
102
|
console.log(`Embedding model cache: ${modelCacheDir(contextOSDataDir())}`);
|
|
97
103
|
console.log(`Embedding vectors cache: ${warmResult.cachePath}`);
|
|
98
104
|
console.log(`File path embeddings warmed: ${warmResult.fileCount || 0}`);
|
|
105
|
+
console.log(`Skill embeddings warmed: ${warmResult.skillCount || 0}`);
|
|
99
106
|
console.log(`Prompt context injection: ${inject ? "enabled" : "quiet logging only"}`);
|
|
100
107
|
console.log("Restart Claude Code if it was already running, then submit a task to trigger ContextOS.");
|
|
101
108
|
return;
|
|
@@ -113,6 +120,7 @@ async function install({ copy = false, inject = true, agent = "codex" } = {}) {
|
|
|
113
120
|
console.log(`Embedding model cache: ${modelCacheDir(contextOSDataDir())}`);
|
|
114
121
|
console.log(`Embedding vectors cache: ${warmResult.cachePath}`);
|
|
115
122
|
console.log(`File path embeddings warmed: ${warmResult.fileCount || 0}`);
|
|
123
|
+
console.log(`Skill embeddings warmed: ${warmResult.skillCount || 0}`);
|
|
116
124
|
console.log(`Prompt context injection: ${inject ? "enabled" : "quiet logging only"}`);
|
|
117
125
|
console.log("Restart Antigravity or agy if it was already running, then submit a task to trigger ContextOS.");
|
|
118
126
|
return;
|
|
@@ -143,6 +151,7 @@ async function install({ copy = false, inject = true, agent = "codex" } = {}) {
|
|
|
143
151
|
console.log(`Embedding model cache: ${modelCacheDir(contextOSDataDir())}`);
|
|
144
152
|
console.log(`Embedding vectors cache: ${warmResult.cachePath}`);
|
|
145
153
|
console.log(`File path embeddings warmed: ${warmResult.fileCount || 0}`);
|
|
154
|
+
console.log(`Skill embeddings warmed: ${warmResult.skillCount || 0}`);
|
|
146
155
|
console.log(`Prompt context injection: ${inject ? "enabled" : "quiet logging only"}`);
|
|
147
156
|
console.log("Restart Codex if it was already running, then submit a task to trigger ContextOS.");
|
|
148
157
|
}
|
|
@@ -169,7 +178,12 @@ async function warmInstallEmbeddings() {
|
|
|
169
178
|
dataDir,
|
|
170
179
|
allowRemote: !modelReady
|
|
171
180
|
});
|
|
172
|
-
|
|
181
|
+
const skillResult = await warmSkillEmbeddings({
|
|
182
|
+
cwd: process.cwd(),
|
|
183
|
+
dataDir,
|
|
184
|
+
allowRemote: !modelReady
|
|
185
|
+
});
|
|
186
|
+
return { ...result, modelAlreadyCached: modelReady, fileCount: fileResult.count, skillCount: skillResult.count };
|
|
173
187
|
}
|
|
174
188
|
|
|
175
189
|
function tryRunCodex(args) {
|
|
@@ -226,7 +240,8 @@ async function debug(task) {
|
|
|
226
240
|
});
|
|
227
241
|
const rules = scored.scoredRules;
|
|
228
242
|
const relevantFiles = scored.suggestedFiles.slice(0, 3);
|
|
229
|
-
const
|
|
243
|
+
const suggestedSkills = (scored.suggestedSkills || []).slice(0, 3);
|
|
244
|
+
const scheduled = scheduleContext({ rules, relevantFiles, suggestedSkills });
|
|
230
245
|
|
|
231
246
|
console.log("ContextOS debug");
|
|
232
247
|
console.log(`cwd: ${cwd}`);
|
|
@@ -250,6 +265,14 @@ async function debug(task) {
|
|
|
250
265
|
}
|
|
251
266
|
if (!relevantFiles.length) console.log("(none)");
|
|
252
267
|
console.log("");
|
|
268
|
+
console.log("Suggested skills:");
|
|
269
|
+
for (const skill of suggestedSkills) {
|
|
270
|
+
const score = Number(skill.score || 0).toFixed(2);
|
|
271
|
+
const location = skill.path ? ` path:${skill.path}` : "";
|
|
272
|
+
console.log(`${score} ${skill.name}${location}`);
|
|
273
|
+
}
|
|
274
|
+
if (!suggestedSkills.length) console.log("(none)");
|
|
275
|
+
console.log("");
|
|
253
276
|
console.log("Final additionalContext:");
|
|
254
277
|
console.log(scheduled.additionalContext || "(empty)");
|
|
255
278
|
}
|
|
@@ -270,8 +293,14 @@ async function warmEmbeddings(task) {
|
|
|
270
293
|
dataDir: contextOSDataDir(),
|
|
271
294
|
allowRemote: true
|
|
272
295
|
});
|
|
296
|
+
const skillResult = await warmSkillEmbeddings({
|
|
297
|
+
cwd,
|
|
298
|
+
dataDir: contextOSDataDir(),
|
|
299
|
+
allowRemote: true
|
|
300
|
+
});
|
|
273
301
|
console.log(`Warmed ${result.count} embeddings`);
|
|
274
302
|
console.log(`Warmed ${fileResult.count} file path embeddings`);
|
|
303
|
+
console.log(`Warmed ${skillResult.count} skill embeddings`);
|
|
275
304
|
console.log(`Cache: ${result.cachePath}`);
|
|
276
305
|
}
|
|
277
306
|
|
|
@@ -321,7 +350,20 @@ try {
|
|
|
321
350
|
if (!task.trim()) throw new Error('Usage: ctx benchmark -- "task"');
|
|
322
351
|
console.log(formatBenchmark(benchmarkWorkspace({ cwd: process.cwd(), task })));
|
|
323
352
|
} else if (command === "sync") {
|
|
324
|
-
|
|
353
|
+
if (args.includes("--skills")) {
|
|
354
|
+
await syncSkills({
|
|
355
|
+
cwd: process.cwd(),
|
|
356
|
+
args: args.slice(1),
|
|
357
|
+
rebuildSkillEmbeddings: async ({ cwd, sourceDir }) => warmSkillEmbeddings({
|
|
358
|
+
cwd,
|
|
359
|
+
dataDir: contextOSDataDir(),
|
|
360
|
+
allowRemote: !isModelCacheReady(contextOSDataDir()),
|
|
361
|
+
skills: scanSkills({ cwd, roots: [sourceDir] })
|
|
362
|
+
})
|
|
363
|
+
});
|
|
364
|
+
} else {
|
|
365
|
+
await syncRules({ cwd: process.cwd(), rootDir, args: args.slice(1) });
|
|
366
|
+
}
|
|
325
367
|
} else {
|
|
326
368
|
throw new Error(`Unknown command: ${command}\n\n${usage()}`);
|
|
327
369
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@minhpnq1807/contextos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Task-aware AGENTS.md context injection and compliance reporting for Codex, Claude Code, and Antigravity.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"CHANGELOG.md"
|
|
17
17
|
],
|
|
18
18
|
"scripts": {
|
|
19
|
-
"test": "vitest run",
|
|
19
|
+
"test": "vitest run test",
|
|
20
20
|
"build": "node bin/ctx.js --version",
|
|
21
21
|
"validate:plugin": "node test/validate-plugin.js",
|
|
22
22
|
"test:mcp": "node test/mcp-protocol-smoke.js"
|
|
@@ -34,7 +34,8 @@ export async function handlePromptPayload(
|
|
|
34
34
|
if (scored.error) throw new Error(scored.error);
|
|
35
35
|
const scoredRules = scored.scoredRules || [];
|
|
36
36
|
const relevantFiles = (scored.suggestedFiles || []).slice(0, 3);
|
|
37
|
-
const
|
|
37
|
+
const suggestedSkills = (scored.suggestedSkills || []).slice(0, 3);
|
|
38
|
+
const scheduled = scheduleContext({ rules: scoredRules, relevantFiles, suggestedSkills });
|
|
38
39
|
|
|
39
40
|
const runtime = {
|
|
40
41
|
at: now.toISOString(),
|
|
@@ -46,10 +47,12 @@ export async function handlePromptPayload(
|
|
|
46
47
|
mcp: scored.telemetry || {}
|
|
47
48
|
},
|
|
48
49
|
relevantFiles,
|
|
50
|
+
suggestedSkills,
|
|
49
51
|
telemetry: {
|
|
50
52
|
...(scored.telemetry || {}),
|
|
51
53
|
rulesInjected: (scheduled.highRules?.length || 0) + (scheduled.midRules?.length || 0),
|
|
52
|
-
filesSuggested: relevantFiles.length
|
|
54
|
+
filesSuggested: relevantFiles.length,
|
|
55
|
+
skillsSuggested: suggestedSkills.length
|
|
53
56
|
},
|
|
54
57
|
scheduled,
|
|
55
58
|
injected: injectContext,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const MAX_CONTEXT_CHARS = 4000;
|
|
2
2
|
|
|
3
|
-
export function scheduleContext({ rules = [], relevantFiles = [], maxChars = MAX_CONTEXT_CHARS } = {}) {
|
|
3
|
+
export function scheduleContext({ rules = [], relevantFiles = [], suggestedSkills = [], maxChars = MAX_CONTEXT_CHARS } = {}) {
|
|
4
4
|
const high = rules.filter((rule) => rule.score >= 0.5);
|
|
5
5
|
const mid = rules.filter((rule) => rule.score >= 0.1 && rule.score < 0.5);
|
|
6
6
|
const dropped = rules.filter((rule) => rule.score < 0.1);
|
|
@@ -12,6 +12,9 @@ export function scheduleContext({ rules = [], relevantFiles = [], maxChars = MAX
|
|
|
12
12
|
if (relevantFiles.length) {
|
|
13
13
|
sections.push(section("Suggested files to check", relevantFiles.map((file) => `- ${file.path}`)));
|
|
14
14
|
}
|
|
15
|
+
if (suggestedSkills.length) {
|
|
16
|
+
sections.push(section("Skills to activate for this task", suggestedSkills.map(formatSkill)));
|
|
17
|
+
}
|
|
15
18
|
if (mid.length) {
|
|
16
19
|
sections.push(section("Additional relevant rules", mid.slice(0, 8).map(formatRule)));
|
|
17
20
|
}
|
|
@@ -25,6 +28,7 @@ export function scheduleContext({ rules = [], relevantFiles = [], maxChars = MAX
|
|
|
25
28
|
midRules: mid,
|
|
26
29
|
droppedRules: dropped,
|
|
27
30
|
relevantFiles,
|
|
31
|
+
suggestedSkills,
|
|
28
32
|
additionalContext
|
|
29
33
|
};
|
|
30
34
|
}
|
|
@@ -39,6 +43,12 @@ function formatRule(rule) {
|
|
|
39
43
|
return `- ${rule.content}${source}`;
|
|
40
44
|
}
|
|
41
45
|
|
|
46
|
+
function formatSkill(skill) {
|
|
47
|
+
const description = skill.description ? `: ${skill.description}` : "";
|
|
48
|
+
const location = skill.path ? ` (${skill.path})` : "";
|
|
49
|
+
return `- ${skill.name}${description}${location}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
42
52
|
function trimToLimit(value, maxChars) {
|
|
43
53
|
if (value.length <= maxChars) return value;
|
|
44
54
|
return `${value.slice(0, Math.max(0, maxChars - 80)).trimEnd()}\n\n[ContextOS truncated context to ${maxChars} chars]`;
|
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { readAgentsChain } from "./reader.js";
|
|
4
4
|
import { filterActionableRules, parseRules, scoreRules, findRelevantFiles } from "./analyzer.js";
|
|
5
5
|
import { enhanceRuleScoresWithEmbeddings } from "./embedding-scorer.js";
|
|
6
|
+
import { scanSkills, suggestSkills } from "./skill-discoverer.js";
|
|
6
7
|
|
|
7
8
|
export async function scoreContext({
|
|
8
9
|
cwd = process.cwd(),
|
|
@@ -10,6 +11,8 @@ export async function scoreContext({
|
|
|
10
11
|
openFiles = [],
|
|
11
12
|
dataDir,
|
|
12
13
|
maxFiles = 5,
|
|
14
|
+
maxSkills = 3,
|
|
15
|
+
skills = null,
|
|
13
16
|
embeddingTimeoutMs = 5000,
|
|
14
17
|
fileEmbeddingTimeoutMs = Number(process.env.CONTEXTOS_FILE_EMBEDDING_TIMEOUT_MS || 80)
|
|
15
18
|
} = {}) {
|
|
@@ -36,12 +39,20 @@ export async function scoreContext({
|
|
|
36
39
|
allowRemote: false
|
|
37
40
|
}
|
|
38
41
|
});
|
|
42
|
+
const skillCatalog = Array.isArray(skills) ? skills : scanSkills({ cwd });
|
|
43
|
+
const suggestedSkills = await suggestSkills({
|
|
44
|
+
prompt,
|
|
45
|
+
skills: skillCatalog,
|
|
46
|
+
dataDir,
|
|
47
|
+
limit: maxSkills
|
|
48
|
+
});
|
|
39
49
|
|
|
40
50
|
return {
|
|
41
51
|
cwd,
|
|
42
52
|
prompt,
|
|
43
53
|
scoredRules,
|
|
44
54
|
suggestedFiles,
|
|
55
|
+
suggestedSkills,
|
|
45
56
|
telemetry: {
|
|
46
57
|
elapsedMs: Date.now() - started,
|
|
47
58
|
modelStatus: embedding.status,
|
|
@@ -51,6 +62,8 @@ export async function scoreContext({
|
|
|
51
62
|
rulesFiltered: rawRules.length - parsedRules.length,
|
|
52
63
|
rulesInjected: scoredRules.filter((rule) => Number(rule.score || 0) >= 0.1).length,
|
|
53
64
|
filesSuggested: suggestedFiles.length,
|
|
65
|
+
skillsScanned: skillCatalog.length,
|
|
66
|
+
skillsSuggested: suggestedSkills.length,
|
|
54
67
|
sources: merged.sources.map((source) => path.relative(cwd, source))
|
|
55
68
|
}
|
|
56
69
|
};
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { enhanceRuleScoresWithEmbeddings, warmRuleEmbeddings } from "./embedding-scorer.js";
|
|
6
|
+
|
|
7
|
+
const DEFAULT_LIMIT = 3;
|
|
8
|
+
const DEFAULT_MAX_SKILLS = 2000;
|
|
9
|
+
const DEFAULT_EMBEDDING_CANDIDATES = 120;
|
|
10
|
+
const DEFAULT_SEMANTIC_CATALOG_LIMIT = 300;
|
|
11
|
+
|
|
12
|
+
export function skillSearchRoots({ cwd = process.cwd(), home = os.homedir() } = {}) {
|
|
13
|
+
return [
|
|
14
|
+
path.join(cwd, ".codex", "skills"),
|
|
15
|
+
path.join(cwd, ".claude", "skills"),
|
|
16
|
+
path.join(cwd, ".gemini", "skills"),
|
|
17
|
+
path.join(cwd, ".gemini", "antigravity", "skills"),
|
|
18
|
+
path.join(cwd, ".gemini", "antigravity-cli", "skills"),
|
|
19
|
+
path.join(home, ".config", "skillshare", "skills"),
|
|
20
|
+
path.join(home, ".codex", "skills"),
|
|
21
|
+
path.join(home, ".claude", "skills"),
|
|
22
|
+
path.join(home, ".gemini", "skills"),
|
|
23
|
+
path.join(home, ".gemini", "antigravity", "skills"),
|
|
24
|
+
path.join(home, ".gemini", "antigravity-cli", "skills")
|
|
25
|
+
];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function parseSkillFrontmatter(content = "", { fallbackName = "", skillPath = "" } = {}) {
|
|
29
|
+
const text = String(content || "");
|
|
30
|
+
const frontmatter = text.match(/^---\s*\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)/);
|
|
31
|
+
const fields = frontmatter ? parseYamlishFields(frontmatter[1]) : {};
|
|
32
|
+
const body = frontmatter ? text.slice(frontmatter[0].length) : text;
|
|
33
|
+
const fallbackDescription = firstParagraph(body);
|
|
34
|
+
return {
|
|
35
|
+
name: fields.name || fallbackName || path.basename(path.dirname(skillPath)),
|
|
36
|
+
description: fields.description || fallbackDescription,
|
|
37
|
+
path: skillPath
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function parseYamlishFields(frontmatter) {
|
|
42
|
+
const fields = {};
|
|
43
|
+
const lines = String(frontmatter || "").split(/\r?\n/);
|
|
44
|
+
for (const line of lines) {
|
|
45
|
+
const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
|
|
46
|
+
if (!match) continue;
|
|
47
|
+
const key = match[1];
|
|
48
|
+
let value = match[2].trim();
|
|
49
|
+
value = value.replace(/^["']|["']$/g, "");
|
|
50
|
+
fields[key] = value;
|
|
51
|
+
}
|
|
52
|
+
return fields;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function firstParagraph(body) {
|
|
56
|
+
return String(body || "")
|
|
57
|
+
.split(/\n\s*\n/)
|
|
58
|
+
.map((part) => part.replace(/^#+\s*/gm, "").replace(/\s+/g, " ").trim())
|
|
59
|
+
.find(Boolean) || "";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function scanSkills({ cwd = process.cwd(), roots = skillSearchRoots({ cwd }), maxSkills = DEFAULT_MAX_SKILLS } = {}) {
|
|
63
|
+
const skills = [];
|
|
64
|
+
const seen = new Set();
|
|
65
|
+
for (const root of roots) {
|
|
66
|
+
for (const skillPath of findSkillFiles(root)) {
|
|
67
|
+
if (skills.length >= maxSkills) return skills;
|
|
68
|
+
const realPath = safeRealpath(skillPath) || skillPath;
|
|
69
|
+
if (seen.has(realPath)) continue;
|
|
70
|
+
seen.add(realPath);
|
|
71
|
+
let content = "";
|
|
72
|
+
try {
|
|
73
|
+
content = fs.readFileSync(skillPath, "utf8");
|
|
74
|
+
} catch {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const skill = parseSkillFrontmatter(content, {
|
|
78
|
+
fallbackName: path.basename(path.dirname(skillPath)),
|
|
79
|
+
skillPath
|
|
80
|
+
});
|
|
81
|
+
if (!skill.name || !skill.description) continue;
|
|
82
|
+
skills.push({
|
|
83
|
+
...skill,
|
|
84
|
+
root,
|
|
85
|
+
scope: isInsidePath(skillPath, cwd) ? "project" : "global",
|
|
86
|
+
relativePath: path.relative(cwd, skillPath)
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return skills;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function findSkillFiles(root) {
|
|
94
|
+
const files = [];
|
|
95
|
+
walk(root, 0, files);
|
|
96
|
+
return files;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function walk(directory, depth, files) {
|
|
100
|
+
if (depth > 4) return;
|
|
101
|
+
let entries = [];
|
|
102
|
+
try {
|
|
103
|
+
entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
104
|
+
} catch {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
for (const entry of entries) {
|
|
108
|
+
const fullPath = path.join(directory, entry.name);
|
|
109
|
+
if (entry.isDirectory()) {
|
|
110
|
+
walk(fullPath, depth + 1, files);
|
|
111
|
+
} else if (entry.isFile() && entry.name === "SKILL.md") {
|
|
112
|
+
files.push(fullPath);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function safeRealpath(filePath) {
|
|
118
|
+
try {
|
|
119
|
+
return fs.realpathSync(filePath);
|
|
120
|
+
} catch {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function isInsidePath(filePath, parentPath) {
|
|
126
|
+
const relative = path.relative(path.resolve(parentPath), path.resolve(filePath));
|
|
127
|
+
return relative && !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function suggestSkills({
|
|
131
|
+
prompt = "",
|
|
132
|
+
skills = [],
|
|
133
|
+
dataDir,
|
|
134
|
+
limit = DEFAULT_LIMIT,
|
|
135
|
+
timeoutMs = Number(process.env.CONTEXTOS_SKILL_EMBEDDING_TIMEOUT_MS || process.env.CONTEXTOS_EMBEDDING_TIMEOUT_MS || 800)
|
|
136
|
+
} = {}) {
|
|
137
|
+
if (!String(prompt || "").trim() || !skills.length) return [];
|
|
138
|
+
const base = scoreSkillsByKeyword({ prompt, skills });
|
|
139
|
+
if (skills.length > DEFAULT_SEMANTIC_CATALOG_LIMIT) {
|
|
140
|
+
return finalizeSkillScores(base, limit);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const embeddingCandidates = selectEmbeddingCandidates(base);
|
|
144
|
+
if (!embeddingCandidates.length) return [];
|
|
145
|
+
|
|
146
|
+
const embedding = await enhanceRuleScoresWithEmbeddings(embeddingCandidates, prompt, {
|
|
147
|
+
dataDir,
|
|
148
|
+
sources: embeddingCandidates.map((skill) => skill.path).filter(Boolean),
|
|
149
|
+
timeoutMs,
|
|
150
|
+
allowRemote: false
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
return finalizeSkillScores(embedding.rules, limit);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function finalizeSkillScores(skills, limit) {
|
|
157
|
+
return skills
|
|
158
|
+
.map((rule) => ({
|
|
159
|
+
name: rule.name,
|
|
160
|
+
description: rule.description,
|
|
161
|
+
path: rule.path,
|
|
162
|
+
scope: rule.scope,
|
|
163
|
+
keywordScore: rule.keywordScore,
|
|
164
|
+
score: Math.min(1, Number(rule.score || 0)),
|
|
165
|
+
embeddingScore: rule.embeddingScore,
|
|
166
|
+
reasons: rule.reasons || []
|
|
167
|
+
}))
|
|
168
|
+
.filter((skill) => Number(skill.keywordScore || 0) >= 0.35 || Number(skill.embeddingScore || 0) >= 0.62)
|
|
169
|
+
.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name))
|
|
170
|
+
.slice(0, limit);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function selectEmbeddingCandidates(skills) {
|
|
174
|
+
if (skills.length <= DEFAULT_EMBEDDING_CANDIDATES) return skills;
|
|
175
|
+
return [...skills]
|
|
176
|
+
.filter((skill) => Number(skill.keywordScore || 0) > 0)
|
|
177
|
+
.sort((a, b) => Number(b.keywordScore || 0) - Number(a.keywordScore || 0) || a.name.localeCompare(b.name))
|
|
178
|
+
.slice(0, DEFAULT_EMBEDDING_CANDIDATES);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export async function warmSkillEmbeddings({
|
|
182
|
+
cwd = process.cwd(),
|
|
183
|
+
dataDir,
|
|
184
|
+
allowRemote = true,
|
|
185
|
+
skills = scanSkills({ cwd })
|
|
186
|
+
} = {}) {
|
|
187
|
+
if (!dataDir || !skills.length) return { count: 0, cachePath: null };
|
|
188
|
+
return warmRuleEmbeddings({
|
|
189
|
+
rules: skills.map((skill) => ({ content: `${skill.name} ${skill.description}` })),
|
|
190
|
+
task: "skill discovery semantic retrieval",
|
|
191
|
+
dataDir,
|
|
192
|
+
sources: skills.map((skill) => skill.path).filter(Boolean),
|
|
193
|
+
allowRemote
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function scoreSkillsByKeyword({ prompt, skills }) {
|
|
198
|
+
const normalizedPrompt = normalize(prompt);
|
|
199
|
+
const promptTokens = new Set(normalizedPrompt.split(/\s+/).filter(Boolean));
|
|
200
|
+
return skills.map((skill, index) => {
|
|
201
|
+
const name = String(skill.name || "");
|
|
202
|
+
const description = String(skill.description || "");
|
|
203
|
+
const content = `${name} ${description}`;
|
|
204
|
+
const skillTokens = new Set(normalize(content).split(/\s+/).filter(Boolean));
|
|
205
|
+
const matches = [...skillTokens].filter((token) => promptTokens.has(token) && token.length > 2);
|
|
206
|
+
const normalizedName = normalize(name);
|
|
207
|
+
const nameTokens = normalizedName.split(/\s+/).filter((token) => token.length > 2);
|
|
208
|
+
const nameHit = normalizedPrompt.includes(normalizedName);
|
|
209
|
+
const nameTokenHit = nameTokens.length > 1 && nameTokens.every((token) => promptTokens.has(token));
|
|
210
|
+
const scopeBonus = skill.scope === "project" ? 0.08 : 0;
|
|
211
|
+
const score = Math.min(1, (matches.length ? 0.25 + matches.length * 0.08 : 0) + (nameHit ? 0.2 : 0) + (nameTokenHit ? 0.18 : 0) + scopeBonus);
|
|
212
|
+
return {
|
|
213
|
+
id: `skill-${index + 1}`,
|
|
214
|
+
name,
|
|
215
|
+
description,
|
|
216
|
+
path: skill.path,
|
|
217
|
+
scope: skill.scope,
|
|
218
|
+
content,
|
|
219
|
+
score,
|
|
220
|
+
keywordScore: score,
|
|
221
|
+
reasons: [
|
|
222
|
+
...(matches.length ? [`keyword:${matches.slice(0, 4).join(",")}`] : []),
|
|
223
|
+
...(nameHit || nameTokenHit ? ["name-match"] : [])
|
|
224
|
+
],
|
|
225
|
+
originalOrder: index
|
|
226
|
+
};
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function normalize(value) {
|
|
231
|
+
return String(value || "").toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
|
|
232
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import readline from "node:readline/promises";
|
|
5
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
6
|
+
import { execFileSync, execSync } from "node:child_process";
|
|
7
|
+
|
|
8
|
+
const DEFAULT_AGENTS = ["codex", "claude", "antigravity"];
|
|
9
|
+
const INSTALL_SH_URL = "https://raw.githubusercontent.com/runkids/skillshare/main/install.sh";
|
|
10
|
+
const INSTALL_PS_URL = "https://raw.githubusercontent.com/runkids/skillshare/main/install.ps1";
|
|
11
|
+
|
|
12
|
+
function statusLine(label, value) {
|
|
13
|
+
return `[ctx] ${label.padEnd(38)} ${value}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function runCommand(command, args = [], { cwd = process.cwd(), stdio = "pipe", dryRun = false } = {}) {
|
|
17
|
+
if (dryRun) return { stdout: "", skipped: true };
|
|
18
|
+
const stdout = execFileSync(command, args, { cwd, stdio, encoding: "utf8" });
|
|
19
|
+
return { stdout: stdout || "" };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function runShell(command, { cwd = process.cwd(), stdio = "inherit", dryRun = false } = {}) {
|
|
23
|
+
if (dryRun) return { stdout: "", skipped: true };
|
|
24
|
+
const stdout = execSync(command, { cwd, stdio, encoding: "utf8" });
|
|
25
|
+
return { stdout: stdout || "" };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function parseSyncSkillsArgs(args = []) {
|
|
29
|
+
const agentsFlag = args.indexOf("--agents");
|
|
30
|
+
const agents = agentsFlag >= 0
|
|
31
|
+
? String(args[agentsFlag + 1] || "").split(",").map((item) => item.trim()).filter(Boolean)
|
|
32
|
+
: DEFAULT_AGENTS;
|
|
33
|
+
return {
|
|
34
|
+
skills: args.includes("--skills"),
|
|
35
|
+
agents,
|
|
36
|
+
dryRun: args.includes("--dry-run"),
|
|
37
|
+
noCollect: args.includes("--no-collect"),
|
|
38
|
+
yes: args.includes("--yes") || args.includes("-y")
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function detectOS(platform = process.platform) {
|
|
43
|
+
if (platform === "darwin") return "mac";
|
|
44
|
+
if (platform === "win32") return "windows";
|
|
45
|
+
return "linux";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function skillshareConfigDir({ home = os.homedir() } = {}) {
|
|
49
|
+
return path.join(home, ".config", "skillshare");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function skillshareSourceDir({ home = os.homedir() } = {}) {
|
|
53
|
+
return path.join(skillshareConfigDir({ home }), "skills");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function checkSkillshareInstalled({ run = runCommand } = {}) {
|
|
57
|
+
try {
|
|
58
|
+
const result = run("skillshare", ["--version"]);
|
|
59
|
+
return { installed: true, version: result.stdout.trim() || "installed" };
|
|
60
|
+
} catch {
|
|
61
|
+
return { installed: false, version: "" };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function shouldInstallSkillshare({ yes = false } = {}) {
|
|
66
|
+
if (yes) return true;
|
|
67
|
+
if (!process.stdin.isTTY) return false;
|
|
68
|
+
const rl = readline.createInterface({ input, output });
|
|
69
|
+
try {
|
|
70
|
+
const answer = await rl.question("[ctx] skillshare is not installed. Install now? [Y/n] ");
|
|
71
|
+
return !/^n(o)?$/i.test(answer.trim());
|
|
72
|
+
} finally {
|
|
73
|
+
rl.close();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function installSkillshare({
|
|
78
|
+
run = runCommand,
|
|
79
|
+
runShellCommand = runShell,
|
|
80
|
+
yes = false,
|
|
81
|
+
dryRun = false,
|
|
82
|
+
platform = process.platform
|
|
83
|
+
} = {}) {
|
|
84
|
+
const accepted = dryRun || await shouldInstallSkillshare({ yes });
|
|
85
|
+
if (!accepted) {
|
|
86
|
+
throw new Error("skillshare is required for ctx sync --skills. Install it manually with `curl -fsSL https://raw.githubusercontent.com/runkids/skillshare/main/install.sh | sh` or rerun with --yes.");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const osName = detectOS(platform);
|
|
90
|
+
if (osName === "windows") {
|
|
91
|
+
run("powershell", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", `irm ${INSTALL_PS_URL} | iex`], { stdio: "inherit", dryRun });
|
|
92
|
+
} else {
|
|
93
|
+
runShellCommand(`curl -fsSL ${INSTALL_SH_URL} | sh`, { stdio: "inherit", dryRun });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const check = checkSkillshareInstalled({ run });
|
|
97
|
+
if (!dryRun && !check.installed) {
|
|
98
|
+
throw new Error("skillshare install finished but `skillshare --version` still failed. Check PATH or install skillshare manually.");
|
|
99
|
+
}
|
|
100
|
+
return check;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function detectExistingSkills({ cwd = process.cwd(), home = os.homedir() } = {}) {
|
|
104
|
+
return skillRoots({ cwd, home })
|
|
105
|
+
.map((root) => ({ path: root, count: countSkillFiles(root) }))
|
|
106
|
+
.filter((entry) => entry.count > 0);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function skillRoots({ cwd, home }) {
|
|
110
|
+
return [
|
|
111
|
+
path.join(home, ".claude", "skills"),
|
|
112
|
+
path.join(home, ".codex", "skills"),
|
|
113
|
+
path.join(home, ".gemini", "antigravity", "skills"),
|
|
114
|
+
path.join(home, ".gemini", "antigravity-cli", "skills"),
|
|
115
|
+
path.join(cwd, ".claude", "skills"),
|
|
116
|
+
path.join(cwd, ".codex", "skills"),
|
|
117
|
+
path.join(cwd, ".gemini", "antigravity", "skills"),
|
|
118
|
+
path.join(cwd, ".gemini", "antigravity-cli", "skills")
|
|
119
|
+
];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function countSkillFiles(root) {
|
|
123
|
+
return findSkillFiles(root).length;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function findSkillFiles(root) {
|
|
127
|
+
const files = [];
|
|
128
|
+
walk(root, 0, files);
|
|
129
|
+
return files;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function walk(directory, depth, files) {
|
|
133
|
+
if (depth > 4) return;
|
|
134
|
+
let entries = [];
|
|
135
|
+
try {
|
|
136
|
+
entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
137
|
+
} catch {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
for (const entry of entries) {
|
|
141
|
+
const fullPath = path.join(directory, entry.name);
|
|
142
|
+
if (entry.isSymbolicLink()) {
|
|
143
|
+
const stat = safeStat(fullPath);
|
|
144
|
+
if (stat?.isDirectory()) walk(fullPath, depth + 1, files);
|
|
145
|
+
} else if (entry.isDirectory()) {
|
|
146
|
+
walk(fullPath, depth + 1, files);
|
|
147
|
+
} else if (entry.isFile() && entry.name === "SKILL.md") {
|
|
148
|
+
files.push(fullPath);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function safeStat(filePath) {
|
|
154
|
+
try {
|
|
155
|
+
return fs.statSync(filePath);
|
|
156
|
+
} catch {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function isSkillshareInitialized({ home = os.homedir() } = {}) {
|
|
162
|
+
return fs.existsSync(skillshareConfigDir({ home }));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export async function syncSkills({
|
|
166
|
+
cwd = process.cwd(),
|
|
167
|
+
home = os.homedir(),
|
|
168
|
+
args = [],
|
|
169
|
+
run = runCommand,
|
|
170
|
+
runShellCommand = runShell,
|
|
171
|
+
logger = console.log,
|
|
172
|
+
rebuildSkillEmbeddings = async () => ({ count: 0, cachePath: null })
|
|
173
|
+
} = {}) {
|
|
174
|
+
const options = parseSyncSkillsArgs(args);
|
|
175
|
+
if (!options.skills) throw new Error("Usage: ctx sync --skills [--dry-run] [--no-collect] [--agents codex,claude,antigravity]");
|
|
176
|
+
|
|
177
|
+
const installed = checkSkillshareInstalled({ run });
|
|
178
|
+
logger(statusLine("Checking skillshare installation...", installed.installed ? `✓ ${installed.version}` : "not found"));
|
|
179
|
+
if (!installed.installed) {
|
|
180
|
+
logger("");
|
|
181
|
+
logger("skillshare is required to sync skills across agents.");
|
|
182
|
+
const postInstall = await installSkillshare({
|
|
183
|
+
run,
|
|
184
|
+
runShellCommand,
|
|
185
|
+
yes: options.yes,
|
|
186
|
+
dryRun: options.dryRun,
|
|
187
|
+
platform: process.platform
|
|
188
|
+
});
|
|
189
|
+
logger(statusLine("Installing skillshare...", options.dryRun ? "dry-run" : `✓ ${postInstall.version}`));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const initialized = isSkillshareInitialized({ home });
|
|
193
|
+
logger(statusLine("Checking skillshare config...", initialized ? "✓ initialized" : "not initialized"));
|
|
194
|
+
|
|
195
|
+
if (!initialized) {
|
|
196
|
+
const existing = detectExistingSkills({ cwd, home });
|
|
197
|
+
if (existing.length) {
|
|
198
|
+
logger("[ctx] Found existing skills:");
|
|
199
|
+
for (const entry of existing) {
|
|
200
|
+
logger(` ${entry.path.padEnd(44)} ${entry.count} skills`);
|
|
201
|
+
}
|
|
202
|
+
} else {
|
|
203
|
+
logger("[ctx] No existing skills found.");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
run("skillshare", ["init"], { cwd, stdio: "inherit", dryRun: options.dryRun });
|
|
207
|
+
logger(statusLine("Initializing skillshare...", options.dryRun ? "dry-run" : "✓ initialized"));
|
|
208
|
+
|
|
209
|
+
if (existing.length && !options.noCollect) {
|
|
210
|
+
run("skillshare", ["backup"], { cwd, stdio: "inherit", dryRun: options.dryRun });
|
|
211
|
+
logger(statusLine("Backing up...", options.dryRun ? "dry-run" : "✓ backup created"));
|
|
212
|
+
run("skillshare", ["collect", "--all"], { cwd, stdio: "inherit", dryRun: options.dryRun });
|
|
213
|
+
const collected = countSkillFiles(skillshareSourceDir({ home }));
|
|
214
|
+
logger(statusLine("Collecting from all agents...", options.dryRun ? "dry-run" : `✓ ${collected} skills collected`));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const syncArgs = ["sync"];
|
|
219
|
+
if (options.dryRun) syncArgs.push("--dry-run");
|
|
220
|
+
if (options.agents.length) syncArgs.push("--agents", options.agents.join(","));
|
|
221
|
+
run("skillshare", syncArgs, { cwd, stdio: "inherit", dryRun: false });
|
|
222
|
+
const syncedCount = countSkillFiles(skillshareSourceDir({ home }));
|
|
223
|
+
logger(statusLine("Running skillshare sync...", options.dryRun ? "dry-run" : `✓ ${syncedCount} skills → ${options.agents.join(", ")}`));
|
|
224
|
+
|
|
225
|
+
let embeddings = { count: 0, cachePath: null, skipped: options.dryRun };
|
|
226
|
+
if (!options.dryRun) {
|
|
227
|
+
embeddings = await rebuildSkillEmbeddings({ cwd, home, sourceDir: skillshareSourceDir({ home }) });
|
|
228
|
+
logger(statusLine("Rebuilding skill embeddings...", `✓ ${embeddings.count || 0} skills indexed`));
|
|
229
|
+
} else {
|
|
230
|
+
logger(statusLine("Rebuilding skill embeddings...", "skipped in dry-run"));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
logger("");
|
|
234
|
+
logger("Done. Skills are now synced.");
|
|
235
|
+
logger(`Source: ${skillshareSourceDir({ home })}`);
|
|
236
|
+
return { options, initialized, sourceDir: skillshareSourceDir({ home }), syncedCount, embeddings };
|
|
237
|
+
}
|
|
@@ -11,16 +11,23 @@ export function createContextOSMcpServer({ dataDir }) {
|
|
|
11
11
|
|
|
12
12
|
server.registerTool("ctx_score_context", {
|
|
13
13
|
title: "Score ContextOS prompt context",
|
|
14
|
-
description: "Scores AGENTS.md rules and suggests files for
|
|
14
|
+
description: "Scores AGENTS.md rules and suggests files/skills for an agent prompt.",
|
|
15
15
|
inputSchema: {
|
|
16
16
|
cwd: z.string().optional(),
|
|
17
17
|
prompt: z.string(),
|
|
18
18
|
openFiles: z.array(z.string()).optional(),
|
|
19
|
-
maxFiles: z.number().int().positive().max(20).optional()
|
|
19
|
+
maxFiles: z.number().int().positive().max(20).optional(),
|
|
20
|
+
maxSkills: z.number().int().positive().max(10).optional(),
|
|
21
|
+
skills: z.array(z.object({
|
|
22
|
+
name: z.string(),
|
|
23
|
+
description: z.string(),
|
|
24
|
+
path: z.string().optional()
|
|
25
|
+
})).optional()
|
|
20
26
|
},
|
|
21
27
|
outputSchema: {
|
|
22
28
|
scoredRules: z.array(z.any()),
|
|
23
29
|
suggestedFiles: z.array(z.any()),
|
|
30
|
+
suggestedSkills: z.array(z.any()),
|
|
24
31
|
telemetry: z.record(z.string(), z.any())
|
|
25
32
|
}
|
|
26
33
|
}, async (args) => {
|
|
@@ -29,7 +36,9 @@ export function createContextOSMcpServer({ dataDir }) {
|
|
|
29
36
|
prompt: args.prompt || "",
|
|
30
37
|
openFiles: args.openFiles || [],
|
|
31
38
|
dataDir,
|
|
32
|
-
maxFiles: args.maxFiles || 5
|
|
39
|
+
maxFiles: args.maxFiles || 5,
|
|
40
|
+
maxSkills: args.maxSkills || 3,
|
|
41
|
+
skills: args.skills
|
|
33
42
|
});
|
|
34
43
|
return {
|
|
35
44
|
content: [
|
|
@@ -41,6 +50,7 @@ export function createContextOSMcpServer({ dataDir }) {
|
|
|
41
50
|
structuredContent: {
|
|
42
51
|
scoredRules: result.scoredRules,
|
|
43
52
|
suggestedFiles: result.suggestedFiles,
|
|
53
|
+
suggestedSkills: result.suggestedSkills,
|
|
44
54
|
telemetry: result.telemetry
|
|
45
55
|
}
|
|
46
56
|
};
|
|
@@ -68,7 +68,9 @@ async function handleBridgeRequest(socket, raw) {
|
|
|
68
68
|
prompt: payload.prompt || "",
|
|
69
69
|
openFiles: payload.openFiles || [],
|
|
70
70
|
dataDir,
|
|
71
|
-
maxFiles: payload.maxFiles || 5
|
|
71
|
+
maxFiles: payload.maxFiles || 5,
|
|
72
|
+
maxSkills: payload.maxSkills || 3,
|
|
73
|
+
skills: payload.skills
|
|
72
74
|
});
|
|
73
75
|
socket.end(JSON.stringify(result));
|
|
74
76
|
} catch (error) {
|
|
@@ -76,6 +78,7 @@ async function handleBridgeRequest(socket, raw) {
|
|
|
76
78
|
error: error?.message || String(error),
|
|
77
79
|
scoredRules: [],
|
|
78
80
|
suggestedFiles: [],
|
|
81
|
+
suggestedSkills: [],
|
|
79
82
|
telemetry: { elapsedMs: 0, modelStatus: "error" }
|
|
80
83
|
}));
|
|
81
84
|
}
|