@minhpnq1807/contextos 0.2.0 → 0.5.0

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.
@@ -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,239 @@
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
+ if (!options.noCollect) {
203
+ run("skillshare", ["backup"], { cwd, stdio: "inherit", dryRun: options.dryRun });
204
+ logger(statusLine("Backing up...", options.dryRun ? "dry-run" : "✓ backup created"));
205
+ }
206
+ } else {
207
+ logger("[ctx] No existing skills found.");
208
+ }
209
+
210
+ run("skillshare", ["init"], { cwd, stdio: "inherit", dryRun: options.dryRun });
211
+ logger(statusLine("Initializing skillshare...", options.dryRun ? "dry-run" : "✓ initialized"));
212
+
213
+ if (existing.length && !options.noCollect) {
214
+ run("skillshare", ["collect", "--all"], { cwd, stdio: "inherit", dryRun: options.dryRun });
215
+ const collected = countSkillFiles(skillshareSourceDir({ home }));
216
+ logger(statusLine("Collecting from all agents...", options.dryRun ? "dry-run" : `✓ ${collected} skills collected`));
217
+ }
218
+ }
219
+
220
+ const syncArgs = ["sync"];
221
+ if (options.dryRun) syncArgs.push("--dry-run");
222
+ if (options.agents.length) syncArgs.push("--agents", options.agents.join(","));
223
+ run("skillshare", syncArgs, { cwd, stdio: "inherit", dryRun: false });
224
+ const syncedCount = countSkillFiles(skillshareSourceDir({ home }));
225
+ logger(statusLine("Running skillshare sync...", options.dryRun ? "dry-run" : `✓ ${syncedCount} skills → ${options.agents.join(", ")}`));
226
+
227
+ let embeddings = { count: 0, cachePath: null, skipped: options.dryRun };
228
+ if (!options.dryRun) {
229
+ embeddings = await rebuildSkillEmbeddings({ cwd, home, sourceDir: skillshareSourceDir({ home }) });
230
+ logger(statusLine("Rebuilding skill embeddings...", `✓ ${embeddings.count || 0} skills indexed`));
231
+ } else {
232
+ logger(statusLine("Rebuilding skill embeddings...", "skipped in dry-run"));
233
+ }
234
+
235
+ logger("");
236
+ logger("Done. Skills are now synced.");
237
+ logger(`Source: ${skillshareSourceDir({ home })}`);
238
+ return { options, initialized, sourceDir: skillshareSourceDir({ home }), syncedCount, embeddings };
239
+ }
@@ -5,9 +5,10 @@ import { readGitSnapshot, checkCompliance } from "./measure.js";
5
5
  import { buildReport, formatReport } from "./reporter.js";
6
6
  import { loadRuntimeEvidence } from "./telemetry.js";
7
7
  import { filterActionableRules } from "./analyzer.js";
8
+ import { resolveHookCwd } from "./hook-io.js";
8
9
 
9
10
  export function handleStopPayload(payload, { contextPath, reportPath, historyPath, telemetryPath } = {}) {
10
- const cwd = payload.cwd || payload.working_directory || process.cwd();
11
+ const cwd = resolveHookCwd(payload);
11
12
  const promptContext = contextPath && fs.existsSync(contextPath) ? readJsonFile(contextPath) : null;
12
13
  const rawScheduledRules = [
13
14
  ...(promptContext?.scheduled?.highRules || []),
@@ -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 a Codex prompt.",
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
  }