@minhpnq1807/contextos 0.1.3 → 0.1.6
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 +15 -0
- package/README.md +6 -2
- package/bin/ctx.js +5 -2
- package/package.json +1 -1
- package/plugins/ctx/.codex-plugin/plugin.json +1 -1
- package/plugins/ctx/lib/analyzer.js +25 -0
- package/plugins/ctx/lib/mcp-proxy-install.js +118 -0
- package/plugins/ctx/lib/reporter.js +25 -3
- package/plugins/ctx/lib/score-context.js +4 -2
- package/plugins/ctx/lib/stop-hook.js +15 -2
- package/plugins/ctx/mcp/proxy.js +92 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.6
|
|
4
|
+
|
|
5
|
+
- Adds a transparent stdio MCP telemetry proxy that records `tools/call` events while forwarding requests to the original MCP server.
|
|
6
|
+
- `ctx install` now wraps supported local MCP servers, including `code-review-graph` and `agentmemory`, so runtime-only tool rules can be measured from real MCP usage.
|
|
7
|
+
|
|
8
|
+
## 0.1.5
|
|
9
|
+
|
|
10
|
+
- Sanitizes stale Stop reports at display time so previously recorded system-user rules no longer appear in `ctx report` or `ctx evidence` after upgrading.
|
|
11
|
+
- Filters system-user rules again inside the Stop hook to protect reports created from older prompt contexts.
|
|
12
|
+
|
|
13
|
+
## 0.1.4
|
|
14
|
+
|
|
15
|
+
- Filters host/session user rules such as `sudo -u user`, `sudo su - user`, and "commands must run as user X" before scoring and injection.
|
|
16
|
+
- Prevents system-user setup instructions from inflating `unknown` outcomes or skewing ContextOS efficiency reports.
|
|
17
|
+
|
|
3
18
|
## 0.1.3
|
|
4
19
|
|
|
5
20
|
- Separates runtime prompt/report/stats files per workspace under `~/.ctx/contextos/workspaces/<workspace-id>`.
|
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ With ContextOS, each prompt gets a compact block:
|
|
|
53
53
|
```text
|
|
54
54
|
## Critical ContextOS rules
|
|
55
55
|
- Use code-review-graph before reading files.
|
|
56
|
-
-
|
|
56
|
+
- Check upload moderation flows before editing approval code.
|
|
57
57
|
|
|
58
58
|
## Suggested files to check
|
|
59
59
|
- services/content-service/src/infrastructure/services/content-moderation.service.ts
|
|
@@ -66,6 +66,7 @@ With ContextOS, each prompt gets a compact block:
|
|
|
66
66
|
- Registers a `ctx-mcp` MCP server that owns model loading and semantic scoring.
|
|
67
67
|
- Reads the active `AGENTS.md` chain for the current workspace.
|
|
68
68
|
- Scores rules by relevance to the user prompt.
|
|
69
|
+
- Filters host/session setup rules such as "run commands as user X" or `sudo -u user` because they are environment instructions, not project guidance.
|
|
69
70
|
- Finds likely relevant files with a hybrid retriever:
|
|
70
71
|
- first, local prompt/file heuristics create seed candidates;
|
|
71
72
|
- then, if `.code-review-graph/graph.db` exists, ContextOS queries `code-review-graph` semantic search and re-ranks graph-backed matches;
|
|
@@ -104,6 +105,7 @@ node bin/ctx.js install
|
|
|
104
105
|
3. Downloads and caches the required local MiniLM embedding model under `~/.ctx/contextos/models`.
|
|
105
106
|
4. Warms `~/.ctx/contextos/embeddings.db` for AGENTS rules and project file paths.
|
|
106
107
|
5. Registers the `ctx-mcp` MCP server and merges ContextOS global hooks into `$CODEX_HOME/hooks.json`.
|
|
108
|
+
6. Wraps supported local MCP servers, currently `code-review-graph` and `agentmemory`, with a transparent telemetry proxy so `tools/call` events can be measured.
|
|
107
109
|
|
|
108
110
|
Restart Codex after installing.
|
|
109
111
|
|
|
@@ -288,7 +290,9 @@ unknown = the rule was relevant, but the diff does not prove either way
|
|
|
288
290
|
|
|
289
291
|
For runtime-only rules, ContextOS also checks `telemetry.jsonl` for hook-visible tool names, MCP server names, and command metadata. A rule like "use code-review-graph before reading files" can be marked `followed` when telemetry contains a matching `code-review-graph` signal.
|
|
290
292
|
|
|
291
|
-
|
|
293
|
+
`ctx install` wraps supported stdio MCP servers with a transparent proxy. The proxy forwards MCP JSON-RPC unchanged, but records `tools/call` requests such as `code-review-graph.detect_changes_tool` to workspace telemetry. This is what lets ContextOS prove runtime rules that cannot be seen in git diff.
|
|
294
|
+
|
|
295
|
+
Host/session setup rules such as "run shell commands as user X", `sudo su - user`, `sudo -i -u user`, and `sudo -u user` are filtered before scoring. They are not injected and do not count toward `unknown` outcomes because they describe the agent runtime environment rather than project behavior.
|
|
292
296
|
|
|
293
297
|
## Development
|
|
294
298
|
|
package/bin/ctx.js
CHANGED
|
@@ -5,7 +5,7 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
import { execFileSync } from "node:child_process";
|
|
6
6
|
|
|
7
7
|
import { readAgentsChain } from "../plugins/ctx/lib/reader.js";
|
|
8
|
-
import { parseRules, scoreRules } from "../plugins/ctx/lib/analyzer.js";
|
|
8
|
+
import { filterActionableRules, parseRules, scoreRules } from "../plugins/ctx/lib/analyzer.js";
|
|
9
9
|
import { scheduleContext } from "../plugins/ctx/lib/scheduler.js";
|
|
10
10
|
import { formatEvidence, formatReport } from "../plugins/ctx/lib/reporter.js";
|
|
11
11
|
import { installGlobalHooks } from "../plugins/ctx/lib/global-hooks.js";
|
|
@@ -14,6 +14,7 @@ import { modelCacheDir, warmRuleEmbeddings } from "../plugins/ctx/lib/embedding-
|
|
|
14
14
|
import { warmFileEmbeddings } from "../plugins/ctx/lib/file-embedding-retriever.js";
|
|
15
15
|
import { scoreContext } from "../plugins/ctx/lib/score-context.js";
|
|
16
16
|
import { defaultDataRoot, workspaceDataDir, workspaceMarkerPath } from "../plugins/ctx/lib/workspace-data.js";
|
|
17
|
+
import { installMcpTelemetryProxies } from "../plugins/ctx/lib/mcp-proxy-install.js";
|
|
17
18
|
|
|
18
19
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
19
20
|
const rootDir = path.resolve(__dirname, "..");
|
|
@@ -101,6 +102,7 @@ async function install({ copy = false, inject = true } = {}) {
|
|
|
101
102
|
runCodex(["plugin", "marketplace", "add", marketplaceRoot]);
|
|
102
103
|
runCodex(["plugin", "add", "ctx@contextos"]);
|
|
103
104
|
runCodex(["mcp", "add", "ctx-mcp", "--", "node", path.join(marketplaceRoot, "plugins", "ctx", "mcp", "server.js")]);
|
|
105
|
+
const proxyResult = installMcpTelemetryProxies({ codexHome: codexHome(), marketplaceRoot });
|
|
104
106
|
const hooksPath = installGlobalHooks({ codexHome: codexHome(), marketplaceRoot, injectPromptContext: inject });
|
|
105
107
|
|
|
106
108
|
console.log("Preparing required local embedding model...");
|
|
@@ -109,6 +111,7 @@ async function install({ copy = false, inject = true } = {}) {
|
|
|
109
111
|
console.log(`Stable marketplace root: ${marketplaceRoot}`);
|
|
110
112
|
console.log(`Installed ContextOS global hooks to ${hooksPath}`);
|
|
111
113
|
console.log("Installed ctx-mcp MCP server.");
|
|
114
|
+
console.log(`MCP telemetry proxies: ${proxyResult.wrapped.length ? proxyResult.wrapped.map((item) => item.name).join(", ") : "none changed"}`);
|
|
112
115
|
console.log(`Embedding model cache: ${modelCacheDir(contextOSDataDir())}`);
|
|
113
116
|
console.log(`Embedding vectors cache: ${warmResult.cachePath}`);
|
|
114
117
|
console.log(`File path embeddings warmed: ${warmResult.fileCount || 0}`);
|
|
@@ -222,7 +225,7 @@ async function debug(task) {
|
|
|
222
225
|
async function warmEmbeddings(task) {
|
|
223
226
|
const cwd = process.cwd();
|
|
224
227
|
const merged = readAgentsChain({ cwd });
|
|
225
|
-
const rules = scoreRules(parseRules(merged.content), task, []);
|
|
228
|
+
const rules = scoreRules(filterActionableRules(parseRules(merged.content)), task, []);
|
|
226
229
|
const result = await warmRuleEmbeddings({
|
|
227
230
|
rules,
|
|
228
231
|
task,
|
package/package.json
CHANGED
|
@@ -48,6 +48,20 @@ const SEMANTIC_ALIASES = {
|
|
|
48
48
|
|
|
49
49
|
const MODERATION_TOKENS = new Set(["moderation", "moderate", "content-moderation", "approval", "approved", "reject", "rejected", "needs_review"]);
|
|
50
50
|
|
|
51
|
+
const SYSTEM_USER_RULE_PATTERNS = [
|
|
52
|
+
/\ball\s+shell\s+commands?\s+must\s+run\s+as\b/i,
|
|
53
|
+
/\bcommands?\s+must\s+run\s+as\b/i,
|
|
54
|
+
/\bstrictly\s+follow\s+this\s+sequence\b/i,
|
|
55
|
+
/\bswitch\s+the\s+user\s+context\b/i,
|
|
56
|
+
/\bdo\s+not\s+prefix\b.*\bsudo\s+-u\b/i,
|
|
57
|
+
/\bsudo\s+su\s+-\s*[a-z_][a-z0-9_-]*\b/i,
|
|
58
|
+
/\bsudo\s+-i\s+-u\s+[a-z_][a-z0-9_-]*\b/i,
|
|
59
|
+
/\bsudo\s+-u\s+[a-z_][a-z0-9_-]*\b/i,
|
|
60
|
+
/\bsu\s+-\s+[a-z_][a-z0-9_-]*\b/i,
|
|
61
|
+
/[/\\]\.codex[/\\]RTK\.md\b/i,
|
|
62
|
+
/\bminh_dev\b/i
|
|
63
|
+
];
|
|
64
|
+
|
|
51
65
|
export function tokenize(value) {
|
|
52
66
|
const normalized = String(value || "")
|
|
53
67
|
.toLowerCase()
|
|
@@ -138,6 +152,17 @@ export function parseRules(markdown) {
|
|
|
138
152
|
return dedupeRules(rules);
|
|
139
153
|
}
|
|
140
154
|
|
|
155
|
+
export function filterActionableRules(rules = []) {
|
|
156
|
+
return rules
|
|
157
|
+
.filter((rule) => !isSystemUserRule(rule))
|
|
158
|
+
.map((rule, index) => ({ ...rule, id: `r${index + 1}`, originalOrder: index }));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function isSystemUserRule(rule) {
|
|
162
|
+
const content = typeof rule === "string" ? rule : rule?.content;
|
|
163
|
+
return SYSTEM_USER_RULE_PATTERNS.some((pattern) => pattern.test(String(content || "")));
|
|
164
|
+
}
|
|
165
|
+
|
|
141
166
|
function dedupeRules(rules) {
|
|
142
167
|
const seen = new Set();
|
|
143
168
|
return rules.filter((rule) => {
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_TARGETS = new Set(["code-review-graph", "agentmemory"]);
|
|
5
|
+
|
|
6
|
+
export function installMcpTelemetryProxies({ codexHome, marketplaceRoot, targets = DEFAULT_TARGETS } = {}) {
|
|
7
|
+
const configPath = path.join(codexHome, "config.toml");
|
|
8
|
+
if (!fs.existsSync(configPath)) return { wrapped: [], skipped: [], configPath };
|
|
9
|
+
|
|
10
|
+
const original = fs.readFileSync(configPath, "utf8");
|
|
11
|
+
const proxyPath = path.join(marketplaceRoot, "plugins", "ctx", "mcp", "proxy.js");
|
|
12
|
+
const result = rewriteMcpTelemetryProxies(original, { proxyPath, targets });
|
|
13
|
+
if (result.content !== original) {
|
|
14
|
+
fs.writeFileSync(configPath, result.content, "utf8");
|
|
15
|
+
}
|
|
16
|
+
return { ...result, configPath };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function rewriteMcpTelemetryProxies(toml, { proxyPath, targets = DEFAULT_TARGETS } = {}) {
|
|
20
|
+
const lines = String(toml || "").split(/\r?\n/);
|
|
21
|
+
const sections = findMcpServerSections(lines);
|
|
22
|
+
const wrapped = [];
|
|
23
|
+
const skipped = [];
|
|
24
|
+
|
|
25
|
+
for (const section of sections.reverse()) {
|
|
26
|
+
if (!targets.has(section.name)) {
|
|
27
|
+
skipped.push({ name: section.name, reason: "not-targeted" });
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const body = lines.slice(section.start + 1, section.end);
|
|
32
|
+
const command = findStringValue(body, "command");
|
|
33
|
+
const args = findArrayValue(body, "args") || [];
|
|
34
|
+
if (!command) {
|
|
35
|
+
skipped.push({ name: section.name, reason: "missing-command" });
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (command === "node" && args[0] === proxyPath) {
|
|
39
|
+
skipped.push({ name: section.name, reason: "already-wrapped" });
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const nextBody = replaceOrInsertServerField(
|
|
44
|
+
replaceOrInsertServerField(body, "command", tomlString("node")),
|
|
45
|
+
"args",
|
|
46
|
+
tomlArray([proxyPath, "--name", section.name, "--", command, ...args])
|
|
47
|
+
);
|
|
48
|
+
lines.splice(section.start + 1, section.end - section.start - 1, ...nextBody);
|
|
49
|
+
wrapped.push({ name: section.name, command, args });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return { content: lines.join("\n"), wrapped: wrapped.reverse(), skipped: skipped.reverse() };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function findMcpServerSections(lines) {
|
|
56
|
+
const sections = [];
|
|
57
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
58
|
+
const match = lines[index].match(/^\[mcp_servers\.([^\].]+)\]\s*$/);
|
|
59
|
+
if (!match) continue;
|
|
60
|
+
let end = lines.length;
|
|
61
|
+
for (let cursor = index + 1; cursor < lines.length; cursor += 1) {
|
|
62
|
+
if (/^\[/.test(lines[cursor])) {
|
|
63
|
+
end = cursor;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
sections.push({ name: unquoteTomlKey(match[1]), start: index, end });
|
|
68
|
+
}
|
|
69
|
+
return sections;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function replaceOrInsertServerField(body, key, value) {
|
|
73
|
+
const next = [...body];
|
|
74
|
+
const index = next.findIndex((line) => new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`).test(line));
|
|
75
|
+
const line = `${key} = ${value}`;
|
|
76
|
+
if (index >= 0) next[index] = line;
|
|
77
|
+
else next.unshift(line);
|
|
78
|
+
return next;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function findStringValue(lines, key) {
|
|
82
|
+
const line = lines.find((item) => new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`).test(item));
|
|
83
|
+
if (!line) return null;
|
|
84
|
+
const match = line.match(/=\s*"((?:\\.|[^"\\])*)"/);
|
|
85
|
+
return match ? unescapeTomlString(match[1]) : null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function findArrayValue(lines, key) {
|
|
89
|
+
const line = lines.find((item) => new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`).test(item));
|
|
90
|
+
if (!line) return null;
|
|
91
|
+
const arrayMatch = line.match(/=\s*\[(.*)\]\s*$/);
|
|
92
|
+
if (!arrayMatch) return null;
|
|
93
|
+
const values = [];
|
|
94
|
+
const pattern = /"((?:\\.|[^"\\])*)"/g;
|
|
95
|
+
let match;
|
|
96
|
+
while ((match = pattern.exec(arrayMatch[1]))) values.push(unescapeTomlString(match[1]));
|
|
97
|
+
return values;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function tomlArray(values) {
|
|
101
|
+
return `[${values.map(tomlString).join(", ")}]`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function tomlString(value) {
|
|
105
|
+
return `"${String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function unescapeTomlString(value) {
|
|
109
|
+
return String(value).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function unquoteTomlKey(value) {
|
|
113
|
+
return value.replace(/^"|"$/g, "");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function escapeRegExp(value) {
|
|
117
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
118
|
+
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { isSystemUserRule } from "./analyzer.js";
|
|
2
|
+
|
|
1
3
|
export function buildReport({ cwd, prompt, relevantFiles, scheduled, gitSnapshot, compliance, runtimeEvidence }) {
|
|
2
|
-
const
|
|
3
|
-
const
|
|
4
|
-
const
|
|
4
|
+
const actionableCompliance = compliance.filter((item) => !isSystemUserRule(item.rule));
|
|
5
|
+
const followed = actionableCompliance.filter((item) => item.status === "followed");
|
|
6
|
+
const ignored = actionableCompliance.filter((item) => item.status === "ignored");
|
|
7
|
+
const unknown = actionableCompliance.filter((item) => item.status === "unknown");
|
|
5
8
|
const measured = followed.length + ignored.length;
|
|
6
9
|
const efficiencyScore = measured ? Math.round((followed.length / measured) * 100) : null;
|
|
7
10
|
|
|
@@ -24,6 +27,7 @@ export function buildReport({ cwd, prompt, relevantFiles, scheduled, gitSnapshot
|
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
export function formatReport(report) {
|
|
30
|
+
report = sanitizeReport(report);
|
|
27
31
|
const lines = [];
|
|
28
32
|
lines.push("ContextOS report");
|
|
29
33
|
lines.push(`Efficiency: ${report.efficiencyScore == null ? "unknown" : `${report.efficiencyScore}%`}`);
|
|
@@ -55,6 +59,7 @@ export function formatReport(report) {
|
|
|
55
59
|
}
|
|
56
60
|
|
|
57
61
|
export function formatEvidence(report) {
|
|
62
|
+
report = sanitizeReport(report);
|
|
58
63
|
const lines = [];
|
|
59
64
|
lines.push("ContextOS evidence");
|
|
60
65
|
lines.push(`Prompt: ${report.prompt || "(empty)"}`);
|
|
@@ -119,3 +124,20 @@ function summarizeRuntimeEvidence(runtimeEvidence = {}) {
|
|
|
119
124
|
sources: (runtimeEvidence.sources || []).slice(0, 10)
|
|
120
125
|
};
|
|
121
126
|
}
|
|
127
|
+
|
|
128
|
+
function sanitizeReport(report = {}) {
|
|
129
|
+
const followed = (report.followed || []).filter((item) => !isSystemUserRule(item.rule));
|
|
130
|
+
const ignored = (report.ignored || []).filter((item) => !isSystemUserRule(item.rule));
|
|
131
|
+
const unknown = (report.unknown || []).filter((item) => !isSystemUserRule(item.rule));
|
|
132
|
+
const measured = followed.length + ignored.length;
|
|
133
|
+
return {
|
|
134
|
+
...report,
|
|
135
|
+
injectedRuleCount: followed.length + ignored.length + unknown.length,
|
|
136
|
+
followed,
|
|
137
|
+
ignored,
|
|
138
|
+
unknown,
|
|
139
|
+
measuredRuleCount: measured,
|
|
140
|
+
unknownRuleCount: unknown.length,
|
|
141
|
+
efficiencyScore: measured ? Math.round((followed.length / measured) * 100) : null
|
|
142
|
+
};
|
|
143
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
|
|
3
3
|
import { readAgentsChain } from "./reader.js";
|
|
4
|
-
import { parseRules, scoreRules, findRelevantFiles } from "./analyzer.js";
|
|
4
|
+
import { filterActionableRules, parseRules, scoreRules, findRelevantFiles } from "./analyzer.js";
|
|
5
5
|
import { enhanceRuleScoresWithEmbeddings } from "./embedding-scorer.js";
|
|
6
6
|
|
|
7
7
|
export async function scoreContext({
|
|
@@ -15,7 +15,8 @@ export async function scoreContext({
|
|
|
15
15
|
} = {}) {
|
|
16
16
|
const started = Date.now();
|
|
17
17
|
const merged = readAgentsChain({ cwd });
|
|
18
|
-
const
|
|
18
|
+
const rawRules = parseRules(merged.content);
|
|
19
|
+
const parsedRules = filterActionableRules(rawRules);
|
|
19
20
|
const baseScoredRules = scoreRules(parsedRules, prompt, openFiles);
|
|
20
21
|
const embedding = await enhanceRuleScoresWithEmbeddings(baseScoredRules, prompt, {
|
|
21
22
|
dataDir,
|
|
@@ -47,6 +48,7 @@ export async function scoreContext({
|
|
|
47
48
|
model: embedding.model,
|
|
48
49
|
cachePath: embedding.cachePath,
|
|
49
50
|
rulesParsed: parsedRules.length,
|
|
51
|
+
rulesFiltered: rawRules.length - parsedRules.length,
|
|
50
52
|
rulesInjected: scoredRules.filter((rule) => Number(rule.score || 0) >= 0.1).length,
|
|
51
53
|
filesSuggested: suggestedFiles.length,
|
|
52
54
|
sources: merged.sources.map((source) => path.relative(cwd, source))
|
|
@@ -4,14 +4,27 @@ import { appendJsonLine, readJsonFile, writeJsonFile } from "./fs-utils.js";
|
|
|
4
4
|
import { readGitSnapshot, checkCompliance } from "./measure.js";
|
|
5
5
|
import { buildReport, formatReport } from "./reporter.js";
|
|
6
6
|
import { loadRuntimeEvidence } from "./telemetry.js";
|
|
7
|
+
import { filterActionableRules } from "./analyzer.js";
|
|
7
8
|
|
|
8
9
|
export function handleStopPayload(payload, { contextPath, reportPath, historyPath, telemetryPath } = {}) {
|
|
9
10
|
const cwd = payload.cwd || payload.working_directory || process.cwd();
|
|
10
11
|
const promptContext = contextPath && fs.existsSync(contextPath) ? readJsonFile(contextPath) : null;
|
|
11
|
-
const
|
|
12
|
+
const rawScheduledRules = [
|
|
12
13
|
...(promptContext?.scheduled?.highRules || []),
|
|
13
14
|
...(promptContext?.scheduled?.midRules || [])
|
|
14
15
|
];
|
|
16
|
+
const scheduledRules = filterActionableRules(rawScheduledRules);
|
|
17
|
+
const scheduled = promptContext?.scheduled
|
|
18
|
+
? {
|
|
19
|
+
...promptContext.scheduled,
|
|
20
|
+
highRules: filterActionableRules(promptContext.scheduled.highRules || []),
|
|
21
|
+
midRules: filterActionableRules(promptContext.scheduled.midRules || []),
|
|
22
|
+
droppedRules: [
|
|
23
|
+
...(promptContext.scheduled.droppedRules || []),
|
|
24
|
+
...rawScheduledRules.filter((rule) => !scheduledRules.some((item) => item.content === rule.content && item.sourcePath === rule.sourcePath))
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
: null;
|
|
15
28
|
const gitSnapshot = readGitSnapshot({ cwd });
|
|
16
29
|
const runtimeEvidence = loadRuntimeEvidence({
|
|
17
30
|
telemetryPath,
|
|
@@ -28,7 +41,7 @@ export function handleStopPayload(payload, { contextPath, reportPath, historyPat
|
|
|
28
41
|
cwd,
|
|
29
42
|
prompt: promptContext?.prompt || "",
|
|
30
43
|
relevantFiles: promptContext?.relevantFiles || [],
|
|
31
|
-
scheduled
|
|
44
|
+
scheduled,
|
|
32
45
|
gitSnapshot,
|
|
33
46
|
compliance,
|
|
34
47
|
runtimeEvidence
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { appendTelemetry } from "../lib/telemetry.js";
|
|
6
|
+
import { workspaceDataDir } from "../lib/workspace-data.js";
|
|
7
|
+
|
|
8
|
+
const { serverName, command, args } = parseArgs(process.argv.slice(2));
|
|
9
|
+
const cwd = process.cwd();
|
|
10
|
+
const telemetryPath = path.join(workspaceDataDir({ cwd }), "telemetry.jsonl");
|
|
11
|
+
let inspectBuffer = "";
|
|
12
|
+
|
|
13
|
+
const child = spawn(command, args, {
|
|
14
|
+
cwd,
|
|
15
|
+
env: process.env,
|
|
16
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
process.stdin.on("data", (chunk) => {
|
|
20
|
+
inspectClientChunk(chunk);
|
|
21
|
+
child.stdin.write(chunk);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
process.stdin.on("end", () => {
|
|
25
|
+
child.stdin.end();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
child.stdout.on("data", (chunk) => {
|
|
29
|
+
process.stdout.write(chunk);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
child.stderr.on("data", (chunk) => {
|
|
33
|
+
process.stderr.write(chunk);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
child.on("error", (error) => {
|
|
37
|
+
process.stderr.write(`contextos mcp proxy failed to start ${serverName}: ${error?.message || String(error)}\n`);
|
|
38
|
+
process.exitCode = 1;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
child.on("exit", (code, signal) => {
|
|
42
|
+
if (signal) process.kill(process.pid, signal);
|
|
43
|
+
else process.exit(code ?? 0);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
function inspectClientChunk(chunk) {
|
|
47
|
+
inspectBuffer += chunk.toString("utf8");
|
|
48
|
+
const lines = inspectBuffer.split(/\r?\n/);
|
|
49
|
+
inspectBuffer = lines.pop() || "";
|
|
50
|
+
for (const line of lines.filter(Boolean)) {
|
|
51
|
+
let message;
|
|
52
|
+
try {
|
|
53
|
+
message = JSON.parse(line);
|
|
54
|
+
} catch {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (message?.method !== "tools/call") continue;
|
|
58
|
+
const toolName = message.params?.name || "unknown";
|
|
59
|
+
appendTelemetry({
|
|
60
|
+
telemetryPath,
|
|
61
|
+
event: "McpToolCall",
|
|
62
|
+
payload: {
|
|
63
|
+
cwd,
|
|
64
|
+
mcp: serverName,
|
|
65
|
+
server: serverName,
|
|
66
|
+
toolName: `${serverName}.${toolName}`,
|
|
67
|
+
tool: toolName,
|
|
68
|
+
method: message.method
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function parseArgs(argv) {
|
|
75
|
+
const separator = argv.indexOf("--");
|
|
76
|
+
if (separator < 0) usage();
|
|
77
|
+
|
|
78
|
+
const before = argv.slice(0, separator);
|
|
79
|
+
const after = argv.slice(separator + 1);
|
|
80
|
+
const nameIndex = before.indexOf("--name");
|
|
81
|
+
const serverName = nameIndex >= 0 ? before[nameIndex + 1] : null;
|
|
82
|
+
const command = after[0];
|
|
83
|
+
const args = after.slice(1);
|
|
84
|
+
|
|
85
|
+
if (!serverName || !command) usage();
|
|
86
|
+
return { serverName, command, args };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function usage() {
|
|
90
|
+
process.stderr.write("Usage: node proxy.js --name <mcp-server-name> -- <command> [...args]\n");
|
|
91
|
+
process.exit(2);
|
|
92
|
+
}
|