@minhpnq1807/contextos 0.1.5 → 0.1.7
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 +10 -0
- package/README.md +3 -0
- package/bin/ctx.js +3 -0
- package/package.json +1 -1
- package/plugins/ctx/.codex-plugin/plugin.json +1 -1
- package/plugins/ctx/lib/analyzer.js +46 -0
- package/plugins/ctx/lib/mcp-proxy-install.js +118 -0
- package/plugins/ctx/mcp/proxy.js +92 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.7
|
|
4
|
+
|
|
5
|
+
- Filters documentation-only AGENTS entries such as MCP tool headings, HTML comments, generic "Key Tools" headings, and tool reference tables before scoring.
|
|
6
|
+
- Keeps actionable tool instructions, for example `Use detect_changes for code review`, measurable through MCP telemetry.
|
|
7
|
+
|
|
8
|
+
## 0.1.6
|
|
9
|
+
|
|
10
|
+
- Adds a transparent stdio MCP telemetry proxy that records `tools/call` events while forwarding requests to the original MCP server.
|
|
11
|
+
- `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.
|
|
12
|
+
|
|
3
13
|
## 0.1.5
|
|
4
14
|
|
|
5
15
|
- Sanitizes stale Stop reports at display time so previously recorded system-user rules no longer appear in `ctx report` or `ctx evidence` after upgrading.
|
package/README.md
CHANGED
|
@@ -105,6 +105,7 @@ node bin/ctx.js install
|
|
|
105
105
|
3. Downloads and caches the required local MiniLM embedding model under `~/.ctx/contextos/models`.
|
|
106
106
|
4. Warms `~/.ctx/contextos/embeddings.db` for AGENTS rules and project file paths.
|
|
107
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.
|
|
108
109
|
|
|
109
110
|
Restart Codex after installing.
|
|
110
111
|
|
|
@@ -289,6 +290,8 @@ unknown = the rule was relevant, but the diff does not prove either way
|
|
|
289
290
|
|
|
290
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.
|
|
291
292
|
|
|
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
|
+
|
|
292
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.
|
|
293
296
|
|
|
294
297
|
## Development
|
package/bin/ctx.js
CHANGED
|
@@ -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}`);
|
package/package.json
CHANGED
|
@@ -62,6 +62,25 @@ const SYSTEM_USER_RULE_PATTERNS = [
|
|
|
62
62
|
/\bminh_dev\b/i
|
|
63
63
|
];
|
|
64
64
|
|
|
65
|
+
const DOCUMENTATION_HEADING_PATTERNS = [
|
|
66
|
+
/^mcp\s+tools?\s*:/i,
|
|
67
|
+
/^key\s+tools?$/i,
|
|
68
|
+
/^workflow$/i,
|
|
69
|
+
/^tools?$/i
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
const TOOL_REFERENCE_TOKENS = new Set([
|
|
73
|
+
"detect_changes",
|
|
74
|
+
"get_review_context",
|
|
75
|
+
"get_impact_radius",
|
|
76
|
+
"get_affected_flows",
|
|
77
|
+
"query_graph",
|
|
78
|
+
"semantic_search_nodes",
|
|
79
|
+
"get_architecture_overview",
|
|
80
|
+
"refactor_tool",
|
|
81
|
+
"list_communities"
|
|
82
|
+
]);
|
|
83
|
+
|
|
65
84
|
export function tokenize(value) {
|
|
66
85
|
const normalized = String(value || "")
|
|
67
86
|
.toLowerCase()
|
|
@@ -155,6 +174,7 @@ export function parseRules(markdown) {
|
|
|
155
174
|
export function filterActionableRules(rules = []) {
|
|
156
175
|
return rules
|
|
157
176
|
.filter((rule) => !isSystemUserRule(rule))
|
|
177
|
+
.filter((rule) => !isDocumentationOnlyRule(rule))
|
|
158
178
|
.map((rule, index) => ({ ...rule, id: `r${index + 1}`, originalOrder: index }));
|
|
159
179
|
}
|
|
160
180
|
|
|
@@ -163,6 +183,32 @@ export function isSystemUserRule(rule) {
|
|
|
163
183
|
return SYSTEM_USER_RULE_PATTERNS.some((pattern) => pattern.test(String(content || "")));
|
|
164
184
|
}
|
|
165
185
|
|
|
186
|
+
export function isDocumentationOnlyRule(rule) {
|
|
187
|
+
const content = String(typeof rule === "string" ? rule : rule?.content || "").trim();
|
|
188
|
+
const normalized = stripMarkdownEmphasis(content);
|
|
189
|
+
if (!normalized) return true;
|
|
190
|
+
if (/^<!--.*-->$/.test(normalized)) return true;
|
|
191
|
+
if (DOCUMENTATION_HEADING_PATTERNS.some((pattern) => pattern.test(normalized))) return true;
|
|
192
|
+
if (isMarkdownTableRule(normalized)) return true;
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function stripMarkdownEmphasis(content) {
|
|
197
|
+
return String(content || "")
|
|
198
|
+
.replace(/^#+\s+/, "")
|
|
199
|
+
.replace(/^\*\*(.*)\*\*$/, "$1")
|
|
200
|
+
.trim();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function isMarkdownTableRule(content) {
|
|
204
|
+
if (!content.includes("|")) return false;
|
|
205
|
+
const pipeCount = (content.match(/\|/g) || []).length;
|
|
206
|
+
if (pipeCount < 4) return false;
|
|
207
|
+
const lower = content.toLowerCase();
|
|
208
|
+
const toolReferenceCount = [...TOOL_REFERENCE_TOKENS].filter((token) => lower.includes(token)).length;
|
|
209
|
+
return /\btool\b/.test(lower) && /\buse\s+when\b/.test(lower) && toolReferenceCount >= 2;
|
|
210
|
+
}
|
|
211
|
+
|
|
166
212
|
function dedupeRules(rules) {
|
|
167
213
|
const seen = new Set();
|
|
168
214
|
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
|
+
}
|
|
@@ -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
|
+
}
|