@minhpnq1807/contextos 0.5.42 → 0.5.44
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 +26 -0
- package/README.md +41 -10
- package/bin/ctx.js +136 -39
- 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 +12 -94
- 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 +95 -20
- package/plugins/ctx/lib/ruler-sync.js +9 -71
- package/plugins/ctx/lib/scheduler.js +33 -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 +110 -10
- 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
|
@@ -1,7 +1,9 @@
|
|
|
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";
|
|
6
8
|
import path from "node:path";
|
|
7
9
|
|
|
@@ -14,7 +16,11 @@ export async function handlePromptPayload(
|
|
|
14
16
|
started = Date.now(),
|
|
15
17
|
injectContext = process.env.CONTEXTOS_INJECT !== "0",
|
|
16
18
|
scoreContextClient = callCtxScoreContext,
|
|
17
|
-
|
|
19
|
+
scoreContextDirectClient = scoreContextDirect,
|
|
20
|
+
autoWarmWorkspace = maybeAutoWarmWorkspace,
|
|
21
|
+
mcpDataDir,
|
|
22
|
+
outputConfig,
|
|
23
|
+
directFallbackTimeoutMs = Number(process.env.CONTEXTOS_DIRECT_FALLBACK_TIMEOUT_MS || 6000)
|
|
18
24
|
} = {}
|
|
19
25
|
) {
|
|
20
26
|
const prompt = payload.prompt || payload.message || payload.user_prompt || "";
|
|
@@ -31,21 +37,31 @@ export async function handlePromptPayload(
|
|
|
31
37
|
maxFiles: 3
|
|
32
38
|
}, {
|
|
33
39
|
dataDir: mcpDataDir || dataDir,
|
|
34
|
-
timeoutMs: Number(process.env.CONTEXTOS_MCP_BRIDGE_TIMEOUT_MS ||
|
|
40
|
+
timeoutMs: Number(process.env.CONTEXTOS_MCP_BRIDGE_TIMEOUT_MS || 2000)
|
|
35
41
|
});
|
|
36
42
|
} catch (error) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
43
|
+
try {
|
|
44
|
+
scored = await withTimeout(scoreContextDirectClient({
|
|
45
|
+
cwd,
|
|
46
|
+
prompt,
|
|
47
|
+
openFiles,
|
|
48
|
+
maxFiles: 3,
|
|
49
|
+
dataDir: mcpDataDir || dataDir,
|
|
50
|
+
embeddingTimeoutMs: Number(process.env.CONTEXTOS_HOOK_EMBEDDING_TIMEOUT_MS || 500),
|
|
51
|
+
fileEmbeddingTimeoutMs: Number(process.env.CONTEXTOS_HOOK_FILE_EMBEDDING_TIMEOUT_MS || 500)
|
|
52
|
+
}), directFallbackTimeoutMs, "direct fallback scoring");
|
|
53
|
+
scored.telemetry = {
|
|
54
|
+
...(scored.telemetry || {}),
|
|
55
|
+
bridgeStatus: "fallback",
|
|
56
|
+
bridgeError: error?.message || String(error)
|
|
57
|
+
};
|
|
58
|
+
} catch (directError) {
|
|
59
|
+
scored = emptyScore({
|
|
60
|
+
bridgeStatus: "fallback-failed",
|
|
61
|
+
bridgeError: error?.message || String(error),
|
|
62
|
+
directFallbackError: directError?.message || String(directError)
|
|
63
|
+
});
|
|
64
|
+
}
|
|
49
65
|
}
|
|
50
66
|
|
|
51
67
|
if (scored.error) throw new Error(scored.error);
|
|
@@ -53,7 +69,16 @@ export async function handlePromptPayload(
|
|
|
53
69
|
const relevantFiles = (scored.suggestedFiles || []).slice(0, 3);
|
|
54
70
|
const suggestedSkills = (scored.suggestedSkills || []).slice(0, 3);
|
|
55
71
|
const suggestedWorkflows = (scored.suggestedWorkflows || []).slice(0, 2);
|
|
56
|
-
const
|
|
72
|
+
const effectiveOutputConfig = outputConfig || loadOutputConfig();
|
|
73
|
+
const scheduled = scheduleContext({ rules: scoredRules, relevantFiles, suggestedSkills, suggestedWorkflows, outputConfig: effectiveOutputConfig });
|
|
74
|
+
const contextEmptyReason = emptyContextReason({ scheduled, outputConfig: effectiveOutputConfig, injectContext });
|
|
75
|
+
const autoWarm = autoWarmWorkspace({
|
|
76
|
+
cwd,
|
|
77
|
+
prompt,
|
|
78
|
+
dataDir,
|
|
79
|
+
reason: contextEmptyReason,
|
|
80
|
+
now: now.getTime()
|
|
81
|
+
});
|
|
57
82
|
|
|
58
83
|
const runtime = {
|
|
59
84
|
at: now.toISOString(),
|
|
@@ -72,7 +97,9 @@ export async function handlePromptPayload(
|
|
|
72
97
|
rulesInjected: (scheduled.highRules?.length || 0) + (scheduled.midRules?.length || 0),
|
|
73
98
|
filesSuggested: relevantFiles.length,
|
|
74
99
|
skillsSuggested: suggestedSkills.length,
|
|
75
|
-
workflowsSuggested: suggestedWorkflows.length
|
|
100
|
+
workflowsSuggested: suggestedWorkflows.length,
|
|
101
|
+
emptyContextReason: contextEmptyReason,
|
|
102
|
+
autoWarm
|
|
76
103
|
},
|
|
77
104
|
scheduled,
|
|
78
105
|
injected: injectContext,
|
|
@@ -86,12 +113,60 @@ export async function handlePromptPayload(
|
|
|
86
113
|
// Context injection is the critical path; diagnostics are best-effort.
|
|
87
114
|
}
|
|
88
115
|
|
|
89
|
-
|
|
116
|
+
const additionalContext = injectContext ? scheduled.additionalContext : "";
|
|
117
|
+
const output = {
|
|
90
118
|
continue: true,
|
|
91
|
-
suppressOutput: true
|
|
92
|
-
|
|
119
|
+
suppressOutput: true
|
|
120
|
+
};
|
|
121
|
+
if (additionalContext) {
|
|
122
|
+
output.hookSpecificOutput = {
|
|
93
123
|
hookEventName: "UserPromptSubmit",
|
|
94
|
-
additionalContext
|
|
124
|
+
additionalContext
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
return output;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function emptyContextReason({ scheduled, outputConfig, injectContext }) {
|
|
131
|
+
if (!injectContext) return "injection-disabled";
|
|
132
|
+
if (scheduled.additionalContext) return null;
|
|
133
|
+
const sections = outputConfig?.sections || {};
|
|
134
|
+
const available = [];
|
|
135
|
+
if ((scheduled.highRules?.length || 0) || (scheduled.midRules?.length || 0)) available.push("rules");
|
|
136
|
+
if (scheduled.relevantFiles?.length) available.push("files");
|
|
137
|
+
if (scheduled.suggestedSkills?.length) available.push("skills");
|
|
138
|
+
if (scheduled.suggestedWorkflows?.length) available.push("workflows");
|
|
139
|
+
if (!available.length) return "no-context-candidates";
|
|
140
|
+
const enabled = available.filter((section) => sections[section] !== false);
|
|
141
|
+
return enabled.length ? "enabled-sections-empty-after-formatting" : `available-sections-disabled:${available.join(",")}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function emptyScore(telemetry = {}) {
|
|
145
|
+
return {
|
|
146
|
+
scoredRules: [],
|
|
147
|
+
suggestedFiles: [],
|
|
148
|
+
suggestedSkills: [],
|
|
149
|
+
suggestedWorkflows: [],
|
|
150
|
+
telemetry: {
|
|
151
|
+
elapsedMs: 0,
|
|
152
|
+
modelStatus: "skipped",
|
|
153
|
+
rulesParsed: 0,
|
|
154
|
+
rulesInjected: 0,
|
|
155
|
+
filesSuggested: 0,
|
|
156
|
+
skillsSuggested: 0,
|
|
157
|
+
workflowsSuggested: 0,
|
|
158
|
+
...telemetry
|
|
95
159
|
}
|
|
96
160
|
};
|
|
97
161
|
}
|
|
162
|
+
|
|
163
|
+
function withTimeout(promise, timeoutMs, label) {
|
|
164
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return promise;
|
|
165
|
+
let timer;
|
|
166
|
+
return Promise.race([
|
|
167
|
+
promise,
|
|
168
|
+
new Promise((_, reject) => {
|
|
169
|
+
timer = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
170
|
+
})
|
|
171
|
+
]).finally(() => clearTimeout(timer));
|
|
172
|
+
}
|
|
@@ -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(section("Suggested files to check", relevantFiles.map(
|
|
24
|
+
if (outputConfig.sections.files && relevantFiles.length) {
|
|
25
|
+
sections.push(section("Suggested files to check", relevantFiles.map(formatFile)));
|
|
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,8 +61,15 @@ 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
|
|
|
58
75
|
|
|
@@ -60,23 +77,18 @@ function formatRule(rule) {
|
|
|
60
77
|
return `- ${rule.content}`;
|
|
61
78
|
}
|
|
62
79
|
|
|
80
|
+
function formatFile(file) {
|
|
81
|
+
return `- ${path.basename(file.path)}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
63
84
|
function formatSkill(skill) {
|
|
64
|
-
|
|
65
|
-
? `: ${truncate(skill.description, 80)}`
|
|
66
|
-
: "";
|
|
67
|
-
return `- ${skill.name}${desc}`;
|
|
85
|
+
return skill.name;
|
|
68
86
|
}
|
|
69
87
|
|
|
70
88
|
function formatWorkflow(workflow) {
|
|
71
89
|
const name = workflow.title || workflow.name;
|
|
72
|
-
const hint = workflow.hint ? `: ${workflow.hint}` : "";
|
|
73
90
|
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)}…`;
|
|
91
|
+
return `- ${name}${chain}`;
|
|
80
92
|
}
|
|
81
93
|
|
|
82
94
|
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
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
|
|
4
|
+
export function shellInvocation(command, { platform = process.platform, env = process.env } = {}) {
|
|
5
|
+
if (platform === "win32") {
|
|
6
|
+
return {
|
|
7
|
+
command: env.ComSpec || env.COMSPEC || "cmd.exe",
|
|
8
|
+
args: ["/d", "/s", "/c", command]
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
return {
|
|
12
|
+
command: fs.existsSync("/bin/sh") ? "/bin/sh" : "sh",
|
|
13
|
+
args: ["-c", command]
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function runPrefixedCommand(commandText, {
|
|
18
|
+
spawnFn = spawn,
|
|
19
|
+
stdout = process.stdout,
|
|
20
|
+
stderr = process.stderr,
|
|
21
|
+
stdin = "inherit",
|
|
22
|
+
platform = process.platform,
|
|
23
|
+
env = process.env,
|
|
24
|
+
prefix = "\x1B[2m│\x1B[0m "
|
|
25
|
+
} = {}) {
|
|
26
|
+
const shell = shellInvocation(commandText, { platform, env });
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
let settled = false;
|
|
29
|
+
const fail = (error) => {
|
|
30
|
+
if (settled) return;
|
|
31
|
+
settled = true;
|
|
32
|
+
reject(error);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
let child;
|
|
36
|
+
try {
|
|
37
|
+
child = spawnFn(shell.command, shell.args, {
|
|
38
|
+
stdio: [stdin, "pipe", "pipe"],
|
|
39
|
+
windowsHide: true
|
|
40
|
+
});
|
|
41
|
+
} catch (error) {
|
|
42
|
+
fail(error);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
pipePrefixed(child.stdout, stdout, prefix);
|
|
47
|
+
pipePrefixed(child.stderr, stderr, prefix);
|
|
48
|
+
|
|
49
|
+
child.on("error", (error) => {
|
|
50
|
+
if (error?.code === "ENOENT") {
|
|
51
|
+
fail(new Error([
|
|
52
|
+
`Unable to start shell '${shell.command}' for installer command.`,
|
|
53
|
+
`Original command: ${commandText}`,
|
|
54
|
+
platform === "win32"
|
|
55
|
+
? "Fix: ensure cmd.exe is available through ComSpec/COMSPEC, or run ContextOS from a normal Command Prompt, PowerShell, or Windows Terminal session."
|
|
56
|
+
: "Fix: ensure /bin/sh exists, or install a POSIX shell before running ContextOS installers."
|
|
57
|
+
].join("\n")));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
fail(error);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
child.on("close", (code) => {
|
|
64
|
+
if (settled) return;
|
|
65
|
+
settled = true;
|
|
66
|
+
if (code === 0) resolve();
|
|
67
|
+
else reject(new Error(`Installer command exited with code ${code}: ${commandText}`));
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function pipePrefixed(stream, target, prefix) {
|
|
73
|
+
if (!stream) return;
|
|
74
|
+
let needPrefix = true;
|
|
75
|
+
stream.on("data", (buf) => {
|
|
76
|
+
const str = buf.toString();
|
|
77
|
+
let out = "";
|
|
78
|
+
for (const ch of str) {
|
|
79
|
+
if (needPrefix) {
|
|
80
|
+
out += prefix;
|
|
81
|
+
needPrefix = false;
|
|
82
|
+
}
|
|
83
|
+
out += ch;
|
|
84
|
+
if (ch === "\n") needPrefix = true;
|
|
85
|
+
}
|
|
86
|
+
target.write(out);
|
|
87
|
+
});
|
|
88
|
+
}
|