@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.
- package/CHANGELOG.md +36 -0
- package/README.md +41 -10
- package/bin/ctx.js +140 -42
- package/package.json +2 -1
- package/plugins/ctx/.codex-plugin/plugin.json +1 -1
- package/plugins/ctx/bin/on-prompt.js +12 -9
- package/plugins/ctx/lib/analyzer.js +193 -91
- package/plugins/ctx/lib/auto-warm.js +74 -0
- package/plugins/ctx/lib/ctx-mcp-client.js +58 -9
- package/plugins/ctx/lib/embedding-scorer.js +109 -7
- package/plugins/ctx/lib/file-embedding-retriever.js +20 -23
- package/plugins/ctx/lib/global-hooks.js +20 -0
- package/plugins/ctx/lib/graph-retriever.js +82 -15
- package/plugins/ctx/lib/graph-strategy.js +107 -0
- package/plugins/ctx/lib/hook-io.js +29 -1
- package/plugins/ctx/lib/import-graph.js +37 -40
- package/plugins/ctx/lib/mcp-proxy-install.js +18 -90
- package/plugins/ctx/lib/output-config.js +85 -0
- package/plugins/ctx/lib/package-install.js +8 -0
- package/plugins/ctx/lib/prompt-hook.js +162 -25
- package/plugins/ctx/lib/ruler-sync.js +9 -71
- package/plugins/ctx/lib/scheduler.js +48 -21
- package/plugins/ctx/lib/score-context.js +60 -32
- package/plugins/ctx/lib/setup-wizard.js +5 -2
- package/plugins/ctx/lib/shell-runner.js +88 -0
- package/plugins/ctx/lib/skill-discoverer.js +333 -11
- package/plugins/ctx/lib/skillshare-sync.js +51 -2
- package/plugins/ctx/lib/stop-hook.js +2 -3
- package/plugins/ctx/lib/toml-config.js +116 -0
- package/plugins/ctx/mcp/server.js +6 -2
|
@@ -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
|
-
|
|
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
|
|
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:
|
|
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 ||
|
|
48
|
+
timeoutMs: Number(process.env.CONTEXTOS_MCP_BRIDGE_TIMEOUT_MS || 2000)
|
|
35
49
|
});
|
|
36
50
|
} catch (error) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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,
|
|
54
|
-
const suggestedSkills = (scored.suggestedSkills || []).slice(0,
|
|
55
|
-
const suggestedWorkflows = (scored.suggestedWorkflows || []).slice(0,
|
|
56
|
-
const
|
|
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
|
-
|
|
126
|
+
const additionalContext = injectContext ? scheduled.additionalContext : "";
|
|
127
|
+
const output = {
|
|
90
128
|
continue: true,
|
|
91
|
-
suppressOutput: true
|
|
92
|
-
|
|
129
|
+
suppressOutput: true
|
|
130
|
+
};
|
|
131
|
+
if (additionalContext) {
|
|
132
|
+
output.hookSpecificOutput = {
|
|
93
133
|
hookEventName: "UserPromptSubmit",
|
|
94
|
-
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
|
|
176
|
+
return formatTomlValue(value);
|
|
216
177
|
}
|
|
217
178
|
|
|
218
179
|
function tomlArray(values = []) {
|
|
219
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
256
|
-
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
|
-
|
|
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({
|
|
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(
|
|
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(
|
|
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
|
-
|
|
55
|
-
return
|
|
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
|
-
|
|
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}${
|
|
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 ||
|
|
20
|
+
fileEmbeddingTimeoutMs = Number(process.env.CONTEXTOS_FILE_EMBEDDING_TIMEOUT_MS || 1000)
|
|
21
21
|
} = {}) {
|
|
22
22
|
const started = Date.now();
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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
|
}
|