@minhpnq1807/contextos 0.5.42 → 0.5.45

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,85 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ import { writeJsonFile } from "./fs-utils.js";
5
+ import { defaultDataRoot } from "./workspace-data.js";
6
+
7
+ const CONFIG_FILE = "output-config.json";
8
+
9
+ export const OUTPUT_SECTION_OPTIONS = [
10
+ { value: "rules", label: "Critical ContextOS rules", hint: "Include critical and additional relevant AGENTS.md rules." },
11
+ { value: "files", label: "Suggested files to check", hint: "Include semantic, import-graph, and code-review-graph file suggestions." },
12
+ { value: "skills", label: "Suggested skills for this task", hint: "Include matching local skill recommendations." },
13
+ { value: "workflows", label: "Suggested workflow for this task", hint: "Include matching workflow recommendations." }
14
+ ];
15
+
16
+ export function defaultOutputConfig() {
17
+ return {
18
+ sections: Object.fromEntries(OUTPUT_SECTION_OPTIONS.map((option) => [option.value, true]))
19
+ };
20
+ }
21
+
22
+ export function outputConfigPath(dataRoot = defaultDataRoot()) {
23
+ return path.join(dataRoot, CONFIG_FILE);
24
+ }
25
+
26
+ export function loadOutputConfig({ dataRoot = defaultDataRoot() } = {}) {
27
+ try {
28
+ return normalizeOutputConfig(JSON.parse(fs.readFileSync(outputConfigPath(dataRoot), "utf8")));
29
+ } catch {
30
+ return defaultOutputConfig();
31
+ }
32
+ }
33
+
34
+ export function saveOutputConfig(config, { dataRoot = defaultDataRoot() } = {}) {
35
+ const normalized = normalizeOutputConfig(config);
36
+ writeJsonFile(outputConfigPath(dataRoot), normalized);
37
+ return normalized;
38
+ }
39
+
40
+ export function enabledOutputSections(config = loadOutputConfig()) {
41
+ const normalized = normalizeOutputConfig(config);
42
+ return OUTPUT_SECTION_OPTIONS
43
+ .filter((option) => normalized.sections[option.value])
44
+ .map((option) => option.value);
45
+ }
46
+
47
+ export function enabledOutputSectionsLabel(config = loadOutputConfig()) {
48
+ const enabled = enabledOutputSections(config);
49
+ return enabled.length ? enabled.join(", ") : "(none)";
50
+ }
51
+
52
+ export async function configureOutputSections({
53
+ dataRoot = defaultDataRoot(),
54
+ select,
55
+ logger = console.log
56
+ } = {}) {
57
+ if (typeof select !== "function") throw new Error("configureOutputSections requires a multi-select function");
58
+ const current = loadOutputConfig({ dataRoot });
59
+ const selected = await select({
60
+ message: "Select ContextOS prompt sections to show:",
61
+ options: OUTPUT_SECTION_OPTIONS.map((option) => ({
62
+ ...option,
63
+ selected: current.sections[option.value]
64
+ }))
65
+ });
66
+ const selectedSet = new Set(selected);
67
+ const saved = saveOutputConfig({
68
+ sections: Object.fromEntries(OUTPUT_SECTION_OPTIONS.map((option) => [option.value, selectedSet.has(option.value)]))
69
+ }, { dataRoot });
70
+ logger(`│ Saved ContextOS prompt section config: ${outputConfigPath(dataRoot)}`);
71
+ logger(`│ Enabled sections: ${enabledOutputSectionsLabel(saved)}`);
72
+ return saved;
73
+ }
74
+
75
+ function normalizeOutputConfig(config = {}) {
76
+ const defaults = defaultOutputConfig();
77
+ return {
78
+ sections: Object.fromEntries(OUTPUT_SECTION_OPTIONS.map((option) => [
79
+ option.value,
80
+ typeof config.sections?.[option.value] === "boolean"
81
+ ? config.sections[option.value]
82
+ : defaults.sections[option.value]
83
+ ]))
84
+ };
85
+ }
@@ -34,3 +34,11 @@ export function copyPackageRoot({ rootDir, targetRoot }) {
34
34
  }
35
35
  return targetRoot;
36
36
  }
37
+
38
+ export function syncPackageRoot({ rootDir, targetRoot }) {
39
+ if (path.resolve(rootDir) === path.resolve(targetRoot)) {
40
+ return { targetRoot, synced: false };
41
+ }
42
+ copyPackageRoot({ rootDir, targetRoot });
43
+ return { targetRoot, synced: true };
44
+ }
@@ -1,10 +1,17 @@
1
1
  import { scheduleContext } from "./scheduler.js";
2
2
  import { appendJsonLine, writeJsonFile } from "./fs-utils.js";
3
+ import { maybeAutoWarmWorkspace } from "./auto-warm.js";
3
4
  import { callCtxScoreContext } from "./ctx-mcp-client.js";
4
5
  import { resolveHookCwd } from "./hook-io.js";
6
+ import { loadOutputConfig } from "./output-config.js";
5
7
  import { scoreContext as scoreContextDirect } from "./score-context.js";
8
+ import fs from "node:fs";
6
9
  import path from "node:path";
7
10
 
11
+ const PROMPT_FILE_LIMIT = 7;
12
+ const PROMPT_SKILL_LIMIT = 7;
13
+ const PROMPT_WORKFLOW_LIMIT = 2;
14
+
8
15
  export async function handlePromptPayload(
9
16
  payload,
10
17
  {
@@ -14,11 +21,16 @@ export async function handlePromptPayload(
14
21
  started = Date.now(),
15
22
  injectContext = process.env.CONTEXTOS_INJECT !== "0",
16
23
  scoreContextClient = callCtxScoreContext,
17
- mcpDataDir
24
+ scoreContextDirectClient = scoreContextDirect,
25
+ autoWarmWorkspace = maybeAutoWarmWorkspace,
26
+ mcpDataDir,
27
+ outputConfig,
28
+ directFallbackTimeoutMs = Number(process.env.CONTEXTOS_DIRECT_FALLBACK_TIMEOUT_MS || 6000)
18
29
  } = {}
19
30
  ) {
20
31
  const prompt = payload.prompt || payload.message || payload.user_prompt || "";
21
- const cwd = resolveHookCwd(payload);
32
+ const hookCwd = resolveHookCwd(payload);
33
+ const cwd = resolvePromptTargetCwd({ cwd: hookCwd, prompt });
22
34
  const openFiles = payload.openFiles || payload.open_files || payload.files || [];
23
35
  const dataDir = dataPath ? path.dirname(dataPath) : undefined;
24
36
 
@@ -28,32 +40,55 @@ export async function handlePromptPayload(
28
40
  cwd,
29
41
  prompt,
30
42
  openFiles,
31
- maxFiles: 3
43
+ maxFiles: PROMPT_FILE_LIMIT,
44
+ maxSkills: PROMPT_SKILL_LIMIT,
45
+ maxWorkflows: PROMPT_WORKFLOW_LIMIT
32
46
  }, {
33
47
  dataDir: mcpDataDir || dataDir,
34
- timeoutMs: Number(process.env.CONTEXTOS_MCP_BRIDGE_TIMEOUT_MS || 1000)
48
+ timeoutMs: Number(process.env.CONTEXTOS_MCP_BRIDGE_TIMEOUT_MS || 2000)
35
49
  });
36
50
  } catch (error) {
37
- scored = await scoreContextDirect({
38
- cwd,
39
- prompt,
40
- openFiles,
41
- maxFiles: 3,
42
- dataDir: mcpDataDir || dataDir
43
- });
44
- scored.telemetry = {
45
- ...(scored.telemetry || {}),
46
- bridgeStatus: "fallback",
47
- bridgeError: error?.message || String(error)
48
- };
51
+ try {
52
+ scored = await withTimeout(scoreContextDirectClient({
53
+ cwd,
54
+ prompt,
55
+ openFiles,
56
+ maxFiles: PROMPT_FILE_LIMIT,
57
+ maxSkills: PROMPT_SKILL_LIMIT,
58
+ maxWorkflows: PROMPT_WORKFLOW_LIMIT,
59
+ dataDir: mcpDataDir || dataDir,
60
+ embeddingTimeoutMs: Number(process.env.CONTEXTOS_HOOK_EMBEDDING_TIMEOUT_MS || 500),
61
+ fileEmbeddingTimeoutMs: Number(process.env.CONTEXTOS_HOOK_FILE_EMBEDDING_TIMEOUT_MS || 1000)
62
+ }), directFallbackTimeoutMs, "direct fallback scoring");
63
+ scored.telemetry = {
64
+ ...(scored.telemetry || {}),
65
+ bridgeStatus: "fallback",
66
+ bridgeError: error?.message || String(error)
67
+ };
68
+ } catch (directError) {
69
+ scored = emptyScore({
70
+ bridgeStatus: "fallback-failed",
71
+ bridgeError: error?.message || String(error),
72
+ directFallbackError: directError?.message || String(directError)
73
+ });
74
+ }
49
75
  }
50
76
 
51
77
  if (scored.error) throw new Error(scored.error);
52
78
  const scoredRules = scored.scoredRules || [];
53
- const relevantFiles = (scored.suggestedFiles || []).slice(0, 3);
54
- const suggestedSkills = (scored.suggestedSkills || []).slice(0, 3);
55
- const suggestedWorkflows = (scored.suggestedWorkflows || []).slice(0, 2);
56
- const scheduled = scheduleContext({ rules: scoredRules, relevantFiles, suggestedSkills, suggestedWorkflows });
79
+ const relevantFiles = (scored.suggestedFiles || []).slice(0, PROMPT_FILE_LIMIT);
80
+ const suggestedSkills = (scored.suggestedSkills || []).slice(0, PROMPT_SKILL_LIMIT);
81
+ const suggestedWorkflows = (scored.suggestedWorkflows || []).slice(0, PROMPT_WORKFLOW_LIMIT);
82
+ const effectiveOutputConfig = outputConfig || loadOutputConfig();
83
+ const scheduled = scheduleContext({ rules: scoredRules, relevantFiles, suggestedSkills, suggestedWorkflows, outputConfig: effectiveOutputConfig });
84
+ const contextEmptyReason = emptyContextReason({ scheduled, outputConfig: effectiveOutputConfig, injectContext });
85
+ const autoWarm = autoWarmWorkspace({
86
+ cwd,
87
+ prompt,
88
+ dataDir,
89
+ reason: contextEmptyReason,
90
+ now: now.getTime()
91
+ });
57
92
 
58
93
  const runtime = {
59
94
  at: now.toISOString(),
@@ -72,7 +107,9 @@ export async function handlePromptPayload(
72
107
  rulesInjected: (scheduled.highRules?.length || 0) + (scheduled.midRules?.length || 0),
73
108
  filesSuggested: relevantFiles.length,
74
109
  skillsSuggested: suggestedSkills.length,
75
- workflowsSuggested: suggestedWorkflows.length
110
+ workflowsSuggested: suggestedWorkflows.length,
111
+ emptyContextReason: contextEmptyReason,
112
+ autoWarm
76
113
  },
77
114
  scheduled,
78
115
  injected: injectContext,
@@ -86,12 +123,112 @@ export async function handlePromptPayload(
86
123
  // Context injection is the critical path; diagnostics are best-effort.
87
124
  }
88
125
 
89
- return {
126
+ const additionalContext = injectContext ? scheduled.additionalContext : "";
127
+ const output = {
90
128
  continue: true,
91
- suppressOutput: true,
92
- hookSpecificOutput: {
129
+ suppressOutput: true
130
+ };
131
+ if (additionalContext) {
132
+ output.hookSpecificOutput = {
93
133
  hookEventName: "UserPromptSubmit",
94
- additionalContext: injectContext ? scheduled.additionalContext : ""
134
+ additionalContext
135
+ };
136
+ }
137
+ return output;
138
+ }
139
+
140
+ export function resolvePromptTargetCwd({ cwd = process.cwd(), prompt = "" } = {}) {
141
+ const current = path.resolve(cwd);
142
+ const candidates = targetPathCandidates(prompt);
143
+ for (const candidate of candidates) {
144
+ const resolved = path.resolve(current, candidate);
145
+ if (!isAllowedTargetCwd({ current, resolved })) continue;
146
+ if (isWorkspaceRoot(resolved)) return resolved;
147
+ }
148
+ return current;
149
+ }
150
+
151
+ function targetPathCandidates(prompt) {
152
+ const text = String(prompt || "");
153
+ const patterns = [
154
+ /\b(?:tr[eê]n|in|inside|under|repo|workspace|cwd)\s+([.~A-Za-z0-9_/@.-]+(?:\/[A-Za-z0-9_@().-]+)*)/gi,
155
+ /\b(?:debug|test|check|run)\s+(?:on|tr[eê]n)\s+([.~A-Za-z0-9_/@.-]+(?:\/[A-Za-z0-9_@().-]+)*)/gi
156
+ ];
157
+ const results = [];
158
+ for (const pattern of patterns) {
159
+ let match;
160
+ while ((match = pattern.exec(text))) {
161
+ const value = cleanPromptPath(match[1]);
162
+ if (value) results.push(value);
163
+ }
164
+ }
165
+ return results;
166
+ }
167
+
168
+ function cleanPromptPath(value) {
169
+ const cleaned = String(value || "").trim().replace(/[),.;:]+$/g, "");
170
+ if (!cleaned || cleaned.includes("://")) return null;
171
+ return cleaned;
172
+ }
173
+
174
+ function isAllowedTargetCwd({ current, resolved }) {
175
+ const parent = path.dirname(current);
176
+ const relative = path.relative(parent, resolved);
177
+ return relative && !relative.startsWith("..") && !path.isAbsolute(relative);
178
+ }
179
+
180
+ function isWorkspaceRoot(directory) {
181
+ try {
182
+ const stat = fs.statSync(directory);
183
+ if (!stat.isDirectory()) return false;
184
+ } catch {
185
+ return false;
186
+ }
187
+ return fs.existsSync(path.join(directory, "package.json"))
188
+ || fs.existsSync(path.join(directory, "AGENTS.md"))
189
+ || fs.existsSync(path.join(directory, ".git"));
190
+ }
191
+
192
+ function emptyContextReason({ scheduled, outputConfig, injectContext }) {
193
+ if (!injectContext) return "injection-disabled";
194
+ if (scheduled.additionalContext) return null;
195
+ const sections = outputConfig?.sections || {};
196
+ const available = [];
197
+ if ((scheduled.highRules?.length || 0) || (scheduled.midRules?.length || 0)) available.push("rules");
198
+ if (scheduled.relevantFiles?.length) available.push("files");
199
+ if (scheduled.suggestedSkills?.length) available.push("skills");
200
+ if (scheduled.suggestedWorkflows?.length) available.push("workflows");
201
+ if (!available.length) return "no-context-candidates";
202
+ const enabled = available.filter((section) => sections[section] !== false);
203
+ return enabled.length ? "enabled-sections-empty-after-formatting" : `available-sections-disabled:${available.join(",")}`;
204
+ }
205
+
206
+ function emptyScore(telemetry = {}) {
207
+ return {
208
+ scoredRules: [],
209
+ suggestedFiles: [],
210
+ suggestedSkills: [],
211
+ suggestedWorkflows: [],
212
+ telemetry: {
213
+ elapsedMs: 0,
214
+ modelStatus: "skipped",
215
+ rulesParsed: 0,
216
+ rulesInjected: 0,
217
+ filesSuggested: 0,
218
+ skillsSuggested: 0,
219
+ workflowsSuggested: 0,
220
+ ...telemetry
95
221
  }
96
222
  };
97
223
  }
224
+
225
+ function withTimeout(promise, timeoutMs, label) {
226
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return promise;
227
+ let timer;
228
+ return Promise.race([
229
+ promise,
230
+ new Promise((_, reject) => {
231
+ timer = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs);
232
+ })
233
+ ]).finally(() => clearTimeout(timer));
234
+ }
@@ -6,6 +6,7 @@ import { stdin as input, stdout as output } from "node:process";
6
6
  import { execFileSync, spawn } from "node:child_process";
7
7
 
8
8
  import { defaultDataRoot } from "./workspace-data.js";
9
+ import { formatTomlValue, readMcpServersFromToml } from "./toml-config.js";
9
10
 
10
11
  const DEFAULT_AGENTS = ["codex", "claude", "antigravity", "copilot"];
11
12
  const CTX_MCP_NAME = "ctx-mcp";
@@ -171,60 +172,12 @@ function hasTomlSection(content, sectionName) {
171
172
  return new RegExp(`^\\[${sectionName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\]\\s*$`, "m").test(content);
172
173
  }
173
174
 
174
- function findMcpServerSections(content) {
175
- const lines = String(content || "").split(/\r?\n/);
176
- const sections = [];
177
- for (let index = 0; index < lines.length; index += 1) {
178
- const match = lines[index].match(/^\[mcp_servers\.([^\].]+)\]\s*$/);
179
- if (!match) continue;
180
- let end = lines.length;
181
- for (let cursor = index + 1; cursor < lines.length; cursor += 1) {
182
- if (/^\[/.test(lines[cursor])) {
183
- end = cursor;
184
- break;
185
- }
186
- }
187
- sections.push({
188
- name: unquoteTomlKey(match[1]),
189
- body: lines.slice(index + 1, end)
190
- });
191
- }
192
- return sections;
193
- }
194
-
195
- function findStringValue(lines, key) {
196
- const line = lines.find((item) => new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`).test(item));
197
- if (!line) return null;
198
- const match = line.match(/=\s*"((?:\\.|[^"\\])*)"/);
199
- return match ? unescapeTomlString(match[1]) : null;
200
- }
201
-
202
- function findArrayValue(lines, key) {
203
- const line = lines.find((item) => new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`).test(item));
204
- if (!line) return [];
205
- const arrayMatch = line.match(/=\s*\[(.*)\]\s*$/);
206
- if (!arrayMatch) return [];
207
- const values = [];
208
- const pattern = /"((?:\\.|[^"\\])*)"/g;
209
- let match;
210
- while ((match = pattern.exec(arrayMatch[1]))) values.push(unescapeTomlString(match[1]));
211
- return values;
212
- }
213
-
214
175
  function tomlString(value) {
215
- return `"${String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
176
+ return formatTomlValue(value);
216
177
  }
217
178
 
218
179
  function tomlArray(values = []) {
219
- return `[${values.map(tomlString).join(", ")}]`;
220
- }
221
-
222
- function unescapeTomlString(value) {
223
- return String(value).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
224
- }
225
-
226
- function unquoteTomlKey(value) {
227
- return value.replace(/^"|"$/g, "");
180
+ return formatTomlValue(values);
228
181
  }
229
182
 
230
183
  function escapeRegExp(value) {
@@ -246,19 +199,14 @@ function unwrapContextOSProxy(command, args = []) {
246
199
  export function readCodexMcpServers({ configPath = codexConfigPath() } = {}) {
247
200
  if (!fs.existsSync(configPath)) return [];
248
201
  const content = fs.readFileSync(configPath, "utf8");
249
- const servers = [];
250
- for (const section of findMcpServerSections(content)) {
251
- const command = findStringValue(section.body, "command");
252
- if (!command) continue;
253
- const args = findArrayValue(section.body, "args");
202
+ return readMcpServersFromToml(content).map(({ name, command, args }) => {
254
203
  const unwrapped = unwrapContextOSProxy(command, args);
255
- servers.push({
256
- name: section.name,
204
+ return {
205
+ name,
257
206
  command: unwrapped.command,
258
207
  args: unwrapped.args
259
- });
260
- }
261
- return servers;
208
+ };
209
+ });
262
210
  }
263
211
 
264
212
  export function readProjectMcpJsonServers({ cwd = process.cwd(), configPath = path.join(cwd, ".mcp.json") } = {}) {
@@ -302,17 +250,7 @@ function mergeMcpServers(...groups) {
302
250
  function readRulerMcpServers({ tomlPath } = {}) {
303
251
  if (!tomlPath || !fs.existsSync(tomlPath)) return [];
304
252
  const content = fs.readFileSync(tomlPath, "utf8");
305
- const servers = [];
306
- for (const section of findMcpServerSections(content)) {
307
- const command = findStringValue(section.body, "command");
308
- if (!command) continue;
309
- servers.push({
310
- name: section.name,
311
- command,
312
- args: findArrayValue(section.body, "args")
313
- });
314
- }
315
- return servers;
253
+ return readMcpServersFromToml(content);
316
254
  }
317
255
 
318
256
  function readRulerMcpServer({ tomlPath, name } = {}) {
@@ -1,26 +1,36 @@
1
+ import path from "node:path";
2
+
3
+ import { loadOutputConfig } from "./output-config.js";
1
4
 
2
5
  const MAX_CONTEXT_CHARS = 4000;
3
6
 
4
- export function scheduleContext({ rules = [], relevantFiles = [], suggestedSkills = [], suggestedWorkflows = [], maxChars = MAX_CONTEXT_CHARS } = {}) {
7
+ export function scheduleContext({
8
+ rules = [],
9
+ relevantFiles = [],
10
+ suggestedSkills = [],
11
+ suggestedWorkflows = [],
12
+ maxChars = MAX_CONTEXT_CHARS,
13
+ outputConfig = loadOutputConfig()
14
+ } = {}) {
5
15
  const orderedRules = [...rules].sort(compareRulesForContext);
6
16
  const high = orderedRules.filter((rule) => rule.score >= 0.5);
7
17
  const mid = orderedRules.filter((rule) => rule.score >= 0.1 && rule.score < 0.5);
8
18
  const dropped = orderedRules.filter((rule) => rule.score < 0.1);
9
19
 
10
20
  const sections = [];
11
- if (high.length) {
21
+ if (outputConfig.sections.rules && high.length) {
12
22
  sections.push(section("Critical ContextOS rules", high.slice(0, 5).map(formatRule)));
13
23
  }
14
- if (relevantFiles.length) {
15
- sections.push(section("Suggested files to check", relevantFiles.map((file) => `- ${file.path}`)));
24
+ if (outputConfig.sections.files && relevantFiles.length) {
25
+ sections.push(commaSection("Suggested files to check", formatFiles(relevantFiles)));
16
26
  }
17
- if (suggestedSkills.length) {
18
- sections.push(section("Skills to activate for this task", suggestedSkills.map(formatSkill)));
27
+ if (outputConfig.sections.skills && suggestedSkills.length) {
28
+ sections.push(inlineSection("Skills to activate for this task", suggestedSkills.map(formatSkill)));
19
29
  }
20
- if (suggestedWorkflows.length) {
30
+ if (outputConfig.sections.workflows && suggestedWorkflows.length) {
21
31
  sections.push(section("Suggested workflow for this task", suggestedWorkflows.map(formatWorkflow)));
22
32
  }
23
- if (mid.length) {
33
+ if (outputConfig.sections.rules && mid.length) {
24
34
  sections.push(section("Additional relevant rules", mid.slice(0, 5).map(formatRule)));
25
35
  }
26
36
 
@@ -51,32 +61,49 @@ function rulePriority(rule) {
51
61
  }
52
62
 
53
63
  function section(title, lines) {
54
- if (!lines.length) return "";
55
- return `## ${title}\n${lines.join("\n")}`;
64
+ const uniqueLines = [...new Set(lines)];
65
+ if (!uniqueLines.length) return "";
66
+ return `## ${title}\n${uniqueLines.join("\n")}`;
67
+ }
68
+
69
+ function inlineSection(title, values) {
70
+ const uniqueValues = [...new Set(values)];
71
+ if (!uniqueValues.length) return "";
72
+ return `## ${title}: ${uniqueValues.join(", ")}`;
56
73
  }
57
74
 
75
+ function commaSection(title, values) {
76
+ const uniqueValues = [...new Set(values)];
77
+ if (!uniqueValues.length) return "";
78
+ return `## ${title}, ${uniqueValues.join(", ")}`;
79
+ }
58
80
 
59
81
  function formatRule(rule) {
60
82
  return `- ${rule.content}`;
61
83
  }
62
84
 
85
+ function formatFiles(files) {
86
+ const counts = new Map();
87
+ for (const file of files) {
88
+ const name = path.basename(file.path);
89
+ counts.set(name, (counts.get(name) || 0) + 1);
90
+ }
91
+ return files.map((file) => formatFile(file, counts));
92
+ }
93
+
94
+ function formatFile(file, basenameCounts) {
95
+ const name = path.basename(file.path);
96
+ return basenameCounts.get(name) > 1 ? file.path : name;
97
+ }
98
+
63
99
  function formatSkill(skill) {
64
- const desc = skill.description
65
- ? `: ${truncate(skill.description, 80)}`
66
- : "";
67
- return `- ${skill.name}${desc}`;
100
+ return skill.name;
68
101
  }
69
102
 
70
103
  function formatWorkflow(workflow) {
71
104
  const name = workflow.title || workflow.name;
72
- const hint = workflow.hint ? `: ${workflow.hint}` : "";
73
105
  const chain = workflow.chain?.length ? `\n chain: ${workflow.chain.join(" -> ")}` : "";
74
- return `- ${name}${hint}${chain}`;
75
- }
76
-
77
- function truncate(text, maxLen) {
78
- if (text.length <= maxLen) return text;
79
- return `${text.slice(0, maxLen - 1)}…`;
106
+ return `- ${name}${chain}`;
80
107
  }
81
108
 
82
109
  function trimToLimit(value, maxChars) {
@@ -17,46 +17,74 @@ export async function scoreContext({
17
17
  skills = null,
18
18
  workflows = null,
19
19
  embeddingTimeoutMs = 5000,
20
- fileEmbeddingTimeoutMs = Number(process.env.CONTEXTOS_FILE_EMBEDDING_TIMEOUT_MS || 80)
20
+ fileEmbeddingTimeoutMs = Number(process.env.CONTEXTOS_FILE_EMBEDDING_TIMEOUT_MS || 1000)
21
21
  } = {}) {
22
22
  const started = Date.now();
23
- const merged = readAgentsChain({ cwd });
24
- const rawRules = parseRules(merged.content);
25
- const parsedRules = filterActionableRules(rawRules);
26
- const baseScoredRules = scoreRules(parsedRules, prompt, openFiles);
27
- const embedding = await enhanceRuleScoresWithEmbeddings(baseScoredRules, prompt, {
28
- dataDir,
29
- sources: merged.sources,
30
- timeoutMs: embeddingTimeoutMs,
31
- allowRemote: false
23
+ const ruleInputsPromise = Promise.resolve().then(() => {
24
+ const merged = readAgentsChain({ cwd });
25
+ const rawRules = parseRules(merged.content);
26
+ const parsedRules = filterActionableRules(rawRules);
27
+ return {
28
+ merged,
29
+ rawRules,
30
+ parsedRules,
31
+ baseScoredRules: scoreRules(parsedRules, prompt, openFiles)
32
+ };
32
33
  });
33
- const scoredRules = embedding.rules;
34
- const suggestedFiles = await findRelevantFiles({
35
- cwd,
36
- task: prompt,
37
- rules: scoredRules,
38
- dataDir,
39
- limit: maxFiles,
40
- fileEmbeddingTimeoutMs,
41
- fileEmbeddingOptions: {
34
+
35
+ const rulesPromise = ruleInputsPromise.then(({ merged, baseScoredRules }) => {
36
+ return enhanceRuleScoresWithEmbeddings(baseScoredRules, prompt, {
37
+ dataDir,
38
+ sources: merged.sources,
39
+ timeoutMs: embeddingTimeoutMs,
42
40
  allowRemote: false
43
- }
41
+ });
44
42
  });
45
- const skillCatalog = Array.isArray(skills) ? skills : scanSkills({ cwd });
46
- const suggestedSkills = await suggestSkills({
47
- prompt,
48
- skills: skillCatalog,
49
- dataDir,
50
- limit: maxSkills
43
+
44
+ const filesPromise = ruleInputsPromise.then(({ baseScoredRules }) => {
45
+ return findRelevantFiles({
46
+ cwd,
47
+ task: prompt,
48
+ rules: baseScoredRules,
49
+ dataDir,
50
+ limit: maxFiles,
51
+ fileEmbeddingTimeoutMs,
52
+ fileEmbeddingOptions: {
53
+ allowRemote: false
54
+ }
55
+ });
51
56
  });
52
- const workflowCatalog = Array.isArray(workflows) ? workflows : scanWorkflows({ cwd });
53
- const suggestedWorkflows = await suggestWorkflows({
54
- prompt,
55
- workflows: workflowCatalog,
56
- dataDir,
57
- limit: maxWorkflows
57
+
58
+ const skillsPromise = Promise.resolve().then(async () => {
59
+ const catalog = Array.isArray(skills) ? skills : scanSkills({ cwd });
60
+ return {
61
+ catalog,
62
+ suggestions: await suggestSkills({ cwd, prompt, skills: catalog, dataDir, limit: maxSkills })
63
+ };
64
+ });
65
+
66
+ const workflowsPromise = Promise.resolve().then(async () => {
67
+ const catalog = Array.isArray(workflows) ? workflows : scanWorkflows({ cwd });
68
+ return {
69
+ catalog,
70
+ suggestions: await suggestWorkflows({ prompt, workflows: catalog, dataDir, limit: maxWorkflows })
71
+ };
58
72
  });
59
73
 
74
+ const [ruleInputs, embedding, suggestedFiles, skillResult, workflowResult] = await Promise.all([
75
+ ruleInputsPromise,
76
+ rulesPromise,
77
+ filesPromise,
78
+ skillsPromise,
79
+ workflowsPromise
80
+ ]);
81
+ const { merged, rawRules, parsedRules } = ruleInputs;
82
+ const scoredRules = embedding.rules;
83
+ const skillCatalog = skillResult.catalog;
84
+ const suggestedSkills = skillResult.suggestions;
85
+ const workflowCatalog = workflowResult.catalog;
86
+ const suggestedWorkflows = workflowResult.suggestions;
87
+
60
88
  return {
61
89
  cwd,
62
90
  prompt,
@@ -36,13 +36,16 @@ export function setupSummaryLines({
36
36
  cwd = process.cwd(),
37
37
  agents = DEFAULT_AGENTS,
38
38
  syncRules = true,
39
- syncSkills = true
39
+ syncSkills = true,
40
+ promptSections = null
40
41
  } = {}) {
41
- return [
42
+ const lines = [
42
43
  `Installation directory: ${cwd}`,
43
44
  `Agents: ${agents.join(", ") || "(none)"}`,
44
45
  `Prompt context injection: always enabled`,
45
46
  `Ruler rule/MCP sync: ${syncRules ? "enabled" : "skipped"}`,
46
47
  `skillshare skill sync: ${syncSkills ? "enabled" : "skipped"}`
47
48
  ];
49
+ if (promptSections !== null) lines.push(`Prompt sections shown: ${promptSections}`);
50
+ return lines;
48
51
  }