@minhpnq1807/contextos 0.1.5 → 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 CHANGED
@@ -1,5 +1,10 @@
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
+
3
8
  ## 0.1.5
4
9
 
5
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.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minhpnq1807/contextos",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Task-aware AGENTS.md context injection and compliance reporting for Codex.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ctx",
3
- "version": "0.1.0",
3
+ "version": "0.1.6",
4
4
  "description": "Inject task-relevant AGENTS.md rules into Codex through plugin hooks.",
5
5
  "author": {
6
6
  "name": "ContextOS"
@@ -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
+ }