@minhpnq1807/contextos 0.1.0

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.
@@ -0,0 +1,127 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ import { safeReadText } from "./fs-utils.js";
5
+
6
+ function readJsonLines(filePath) {
7
+ return safeReadText(filePath)
8
+ .split(/\r?\n/)
9
+ .map((line) => line.trim())
10
+ .filter(Boolean)
11
+ .map((line) => {
12
+ try {
13
+ return JSON.parse(line);
14
+ } catch {
15
+ return null;
16
+ }
17
+ })
18
+ .filter(Boolean);
19
+ }
20
+
21
+ function readJsonIfExists(filePath) {
22
+ try {
23
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+
29
+ function average(values) {
30
+ const nums = values.filter((value) => typeof value === "number" && Number.isFinite(value));
31
+ if (!nums.length) return null;
32
+ return Math.round(nums.reduce((sum, value) => sum + value, 0) / nums.length);
33
+ }
34
+
35
+ function percent(numerator, denominator) {
36
+ if (!denominator) return 0;
37
+ return Math.round((numerator / denominator) * 100);
38
+ }
39
+
40
+ export function loadStats(dataDir) {
41
+ const prompts = readJsonLines(path.join(dataDir, "prompt-history.jsonl"));
42
+ const reportsFromHistory = readJsonLines(path.join(dataDir, "report-history.jsonl"));
43
+ const lastPrompt = readJsonIfExists(path.join(dataDir, "last-prompt-context.json"));
44
+ const lastReport = readJsonIfExists(path.join(dataDir, "last-report.json"));
45
+ const events = readJsonLines(path.join(dataDir, "debug.log"));
46
+ const reports = reportsFromHistory.length ? reportsFromHistory : [lastReport].filter(Boolean);
47
+
48
+ const byEvent = new Map();
49
+ for (const item of events) {
50
+ byEvent.set(item.event, (byEvent.get(item.event) || 0) + 1);
51
+ }
52
+
53
+ const promptCount = prompts.length || (lastPrompt ? 1 : 0);
54
+ const injectedCount = prompts.filter((item) => item.injected).length + (!prompts.length && lastPrompt?.injected ? 1 : 0);
55
+ const analyzedPrompts = prompts.length ? prompts : [lastPrompt].filter(Boolean);
56
+ const knownEfficiency = reports.map((report) => report.efficiencyScore).filter((score) => score != null);
57
+ const followed = reports.reduce((sum, report) => sum + (report.followed?.length || 0), 0);
58
+ const ignored = reports.reduce((sum, report) => sum + (report.ignored?.length || 0), 0);
59
+ const unknown = reports.reduce((sum, report) => sum + (report.unknown?.length || 0), 0);
60
+
61
+ return {
62
+ dataDir,
63
+ events: Object.fromEntries([...byEvent.entries()].sort()),
64
+ promptCount,
65
+ reportCount: reports.length,
66
+ injectedCount,
67
+ quietCount: Math.max(0, promptCount - injectedCount),
68
+ injectionRate: percent(injectedCount, promptCount),
69
+ averagePromptMs: average(analyzedPrompts.map((item) => item.elapsedMs)),
70
+ averageEfficiency: average(knownEfficiency),
71
+ followed,
72
+ ignored,
73
+ unknown,
74
+ lastPrompt: analyzedPrompts.at(-1) || null,
75
+ lastReport: reports.at(-1) || null
76
+ };
77
+ }
78
+
79
+ export function formatStats(stats) {
80
+ const lines = [];
81
+ lines.push("ContextOS stats");
82
+ lines.push(`Data dir: ${stats.dataDir}`);
83
+ lines.push(`Prompts analyzed: ${stats.promptCount}`);
84
+ lines.push(`Reports generated: ${stats.reportCount}`);
85
+ lines.push(`Prompt mode: ${stats.injectedCount} injected, ${stats.quietCount} quiet (${stats.injectionRate}% injected)`);
86
+ lines.push(`Average prompt analysis: ${stats.averagePromptMs == null ? "unknown" : `${stats.averagePromptMs}ms`}`);
87
+ lines.push(`Average efficiency: ${formatAverageEfficiency(stats)}`);
88
+ lines.push(`Rule outcomes: ${stats.followed} followed, ${stats.ignored} ignored, ${stats.unknown} unknown`);
89
+
90
+ const eventSummary = Object.entries(stats.events)
91
+ .map(([event, count]) => `${event}:${count}`)
92
+ .join(", ");
93
+ lines.push(`Hook events: ${eventSummary || "none"}`);
94
+
95
+ if (stats.lastPrompt) {
96
+ lines.push(`Last prompt: ${truncateLine(stats.lastPrompt.prompt || "", 100) || "(empty)"}`);
97
+ lines.push(`Last scheduled rules: ${scheduledRuleCount(stats.lastPrompt)}`);
98
+ const files = (stats.lastPrompt.relevantFiles || []).map((file) => file.path).join(", ");
99
+ if (files) lines.push(`Last suggested files: ${files}`);
100
+ }
101
+
102
+ if (stats.lastReport) {
103
+ lines.push(`Last report efficiency: ${stats.lastReport.efficiencyScore == null ? "unknown" : `${stats.lastReport.efficiencyScore}%`}`);
104
+ lines.push(`Last report measured rules: ${stats.lastReport.measuredRuleCount ?? ((stats.lastReport.followed?.length || 0) + (stats.lastReport.ignored?.length || 0))}`);
105
+ lines.push(`Last report unknown rules: ${stats.lastReport.unknownRuleCount ?? (stats.lastReport.unknown?.length || 0)}`);
106
+ const changed = stats.lastReport.changedFiles?.join(", ");
107
+ if (changed) lines.push(`Last changed files: ${changed}`);
108
+ }
109
+
110
+ return lines.join("\n");
111
+ }
112
+
113
+ function formatAverageEfficiency(stats) {
114
+ if (stats.averageEfficiency != null) return `${stats.averageEfficiency}%`;
115
+ if (!stats.reportCount) return "unknown (no Stop reports yet)";
116
+ if (stats.followed + stats.ignored === 0) return "unknown (no measurable followed/ignored rule evidence yet)";
117
+ return "unknown";
118
+ }
119
+
120
+ function scheduledRuleCount(prompt) {
121
+ return (prompt.scheduled?.highRules?.length || 0) + (prompt.scheduled?.midRules?.length || 0);
122
+ }
123
+
124
+ function truncateLine(value, max) {
125
+ const normalized = String(value).replace(/\s+/g, " ").trim();
126
+ return normalized.length > max ? `${normalized.slice(0, max - 3)}...` : normalized;
127
+ }
@@ -0,0 +1,32 @@
1
+ import fs from "node:fs";
2
+
3
+ import { appendJsonLine, readJsonFile, writeJsonFile } from "./fs-utils.js";
4
+ import { readGitSnapshot, checkCompliance } from "./measure.js";
5
+ import { buildReport, formatReport } from "./reporter.js";
6
+
7
+ export function handleStopPayload(payload, { contextPath, reportPath, historyPath } = {}) {
8
+ const cwd = payload.cwd || payload.working_directory || process.cwd();
9
+ const promptContext = contextPath && fs.existsSync(contextPath) ? readJsonFile(contextPath) : null;
10
+ const scheduledRules = [
11
+ ...(promptContext?.scheduled?.highRules || []),
12
+ ...(promptContext?.scheduled?.midRules || [])
13
+ ];
14
+ const gitSnapshot = readGitSnapshot({ cwd });
15
+ const compliance = checkCompliance({ rules: scheduledRules, addedLines: gitSnapshot.addedLines });
16
+ const report = buildReport({
17
+ cwd,
18
+ prompt: promptContext?.prompt || "",
19
+ relevantFiles: promptContext?.relevantFiles || [],
20
+ scheduled: promptContext?.scheduled || null,
21
+ gitSnapshot,
22
+ compliance
23
+ });
24
+
25
+ if (reportPath) writeJsonFile(reportPath, report);
26
+ if (historyPath) appendJsonLine(historyPath, report);
27
+
28
+ return {
29
+ continue: true,
30
+ systemMessage: formatReport(report)
31
+ };
32
+ }
@@ -0,0 +1,50 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+
4
+ import { scoreContext } from "../lib/score-context.js";
5
+
6
+ export function createContextOSMcpServer({ dataDir }) {
7
+ const server = new McpServer({
8
+ name: "ctx-mcp",
9
+ version: "0.1.0"
10
+ });
11
+
12
+ server.registerTool("ctx_score_context", {
13
+ title: "Score ContextOS prompt context",
14
+ description: "Scores AGENTS.md rules and suggests files for a Codex prompt.",
15
+ inputSchema: {
16
+ cwd: z.string().optional(),
17
+ prompt: z.string(),
18
+ openFiles: z.array(z.string()).optional(),
19
+ maxFiles: z.number().int().positive().max(20).optional()
20
+ },
21
+ outputSchema: {
22
+ scoredRules: z.array(z.any()),
23
+ suggestedFiles: z.array(z.any()),
24
+ telemetry: z.record(z.string(), z.any())
25
+ }
26
+ }, async (args) => {
27
+ const result = await scoreContext({
28
+ cwd: args.cwd || process.cwd(),
29
+ prompt: args.prompt || "",
30
+ openFiles: args.openFiles || [],
31
+ dataDir,
32
+ maxFiles: args.maxFiles || 5
33
+ });
34
+ return {
35
+ content: [
36
+ {
37
+ type: "text",
38
+ text: JSON.stringify(result.telemetry)
39
+ }
40
+ ],
41
+ structuredContent: {
42
+ scoredRules: result.scoredRules,
43
+ suggestedFiles: result.suggestedFiles,
44
+ telemetry: result.telemetry
45
+ }
46
+ };
47
+ });
48
+
49
+ return server;
50
+ }
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import net from "node:net";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+
7
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
+
9
+ import { modelCacheDir, warmRuleEmbeddings } from "../lib/embedding-scorer.js";
10
+ import { scoreContext } from "../lib/score-context.js";
11
+ import { ctxMcpSocketPath } from "../lib/ctx-mcp-client.js";
12
+ import { createContextOSMcpServer } from "./contextos-server.js";
13
+
14
+ const dataDir = process.env.PLUGIN_DATA || path.join(process.env.CODEX_HOME || path.join(os.homedir(), ".codex"), "contextos");
15
+ const socketPath = ctxMcpSocketPath(dataDir);
16
+
17
+ fs.mkdirSync(dataDir, { recursive: true });
18
+ await ensureModelReady();
19
+ if (process.env.CONTEXTOS_DISABLE_BRIDGE !== "1") startBridge();
20
+ const keepAlive = setInterval(() => {}, 2 ** 31 - 1);
21
+
22
+ const server = createContextOSMcpServer({ dataDir });
23
+ console.error("ctx-mcp ready");
24
+ await server.connect(new StdioServerTransport());
25
+
26
+ async function ensureModelReady() {
27
+ const modelDir = modelCacheDir(dataDir);
28
+ if (!fs.existsSync(modelDir)) {
29
+ throw new Error(`ContextOS model cache missing: ${modelDir}. Run ctx install first.`);
30
+ }
31
+ await warmRuleEmbeddings({
32
+ task: "contextos mcp model ready",
33
+ rules: [{ content: "ContextOS semantic scorer is ready." }],
34
+ dataDir,
35
+ allowRemote: false
36
+ });
37
+ }
38
+
39
+ function startBridge() {
40
+ fs.rmSync(socketPath, { force: true });
41
+ const bridge = net.createServer((socket) => {
42
+ let raw = "";
43
+ socket.on("data", (chunk) => {
44
+ raw += chunk.toString("utf8");
45
+ if (raw.includes("\n")) handleBridgeRequest(socket, raw);
46
+ });
47
+ });
48
+ bridge.on("error", (error) => {
49
+ console.error(`ctx-mcp bridge disabled: ${error?.message || String(error)}`);
50
+ });
51
+ bridge.listen(socketPath);
52
+ process.on("exit", () => {
53
+ clearInterval(keepAlive);
54
+ fs.rmSync(socketPath, { force: true });
55
+ });
56
+ process.on("SIGTERM", () => {
57
+ clearInterval(keepAlive);
58
+ fs.rmSync(socketPath, { force: true });
59
+ process.exit(0);
60
+ });
61
+ }
62
+
63
+ async function handleBridgeRequest(socket, raw) {
64
+ socket.pause();
65
+ try {
66
+ const payload = JSON.parse(raw.trim() || "{}");
67
+ const result = await scoreContext({
68
+ cwd: payload.cwd || process.cwd(),
69
+ prompt: payload.prompt || "",
70
+ openFiles: payload.openFiles || [],
71
+ dataDir,
72
+ maxFiles: payload.maxFiles || 5
73
+ });
74
+ socket.end(JSON.stringify(result));
75
+ } catch (error) {
76
+ socket.end(JSON.stringify({
77
+ error: error?.message || String(error),
78
+ scoredRules: [],
79
+ suggestedFiles: [],
80
+ telemetry: { elapsedMs: 0, modelStatus: "error" }
81
+ }));
82
+ }
83
+ }