@minhpnq1807/contextos 0.5.42 → 0.5.45
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 +36 -0
- package/README.md +41 -10
- package/bin/ctx.js +140 -42
- 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 +193 -91
- 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 +162 -25
- package/plugins/ctx/lib/ruler-sync.js +9 -71
- package/plugins/ctx/lib/scheduler.js +48 -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 +333 -11
- 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,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { isModelCacheReady, searchIndexedEmbeddings, warmIndexedEmbeddings } from "./embedding-scorer.js";
|
|
4
|
+
import { rebuildImportGraphIndex } from "./import-graph.js";
|
|
4
5
|
|
|
5
6
|
const SOURCE_EXTENSIONS = new Set([
|
|
6
7
|
".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".sql", ".md", ".json"
|
|
@@ -8,7 +9,7 @@ const SOURCE_EXTENSIONS = new Set([
|
|
|
8
9
|
const IGNORE_DIRS = new Set([
|
|
9
10
|
".git", ".next", ".turbo", "coverage", "dist", "build", "node_modules", "vendor"
|
|
10
11
|
]);
|
|
11
|
-
const DEFAULT_TIMEOUT_MS =
|
|
12
|
+
const DEFAULT_TIMEOUT_MS = 1000;
|
|
12
13
|
const DEFAULT_MAX_FILES = 1200;
|
|
13
14
|
|
|
14
15
|
export async function findEmbeddingRelevantFiles({
|
|
@@ -18,27 +19,17 @@ export async function findEmbeddingRelevantFiles({
|
|
|
18
19
|
limit = 10,
|
|
19
20
|
timeoutMs = Number(process.env.CONTEXTOS_FILE_EMBEDDING_TIMEOUT_MS || DEFAULT_TIMEOUT_MS),
|
|
20
21
|
maxFiles = Number(process.env.CONTEXTOS_FILE_EMBEDDING_MAX_FILES || DEFAULT_MAX_FILES),
|
|
21
|
-
embeddingOptions = {}
|
|
22
|
+
embeddingOptions = {},
|
|
23
|
+
indexedSearcher = searchIndexedEmbeddings
|
|
22
24
|
} = {}) {
|
|
23
25
|
if (process.env.CONTEXTOS_FILE_EMBEDDINGS === "0") return [];
|
|
24
26
|
if (!dataDir) return [];
|
|
25
27
|
if (!String(task || "").trim()) return [];
|
|
26
28
|
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const fileRules = files.map((filePath, index) => ({
|
|
31
|
-
id: `f${index + 1}`,
|
|
32
|
-
content: fileSearchText(filePath),
|
|
33
|
-
path: filePath,
|
|
34
|
-
score: 0,
|
|
35
|
-
reasons: [],
|
|
36
|
-
originalOrder: index
|
|
37
|
-
}));
|
|
38
|
-
|
|
39
|
-
const result = await enhanceRuleScoresWithEmbeddings(fileRules, task, {
|
|
29
|
+
const result = await indexedSearcher({
|
|
30
|
+
kind: fileIndexKind(cwd),
|
|
31
|
+
task,
|
|
40
32
|
dataDir,
|
|
41
|
-
sources: [path.join(cwd, "AGENTS.md")],
|
|
42
33
|
timeoutMs,
|
|
43
34
|
allowRemote: false,
|
|
44
35
|
...embeddingOptions
|
|
@@ -46,12 +37,12 @@ export async function findEmbeddingRelevantFiles({
|
|
|
46
37
|
|
|
47
38
|
if (result.status !== "enabled") return [];
|
|
48
39
|
|
|
49
|
-
return result.
|
|
40
|
+
return result.items
|
|
50
41
|
.filter((rule) => Number(rule.embeddingScore || 0) >= 0.45)
|
|
51
|
-
.sort((a, b) => Number(b.embeddingScore || 0) - Number(a.embeddingScore || 0) || a.
|
|
42
|
+
.sort((a, b) => Number(b.embeddingScore || 0) - Number(a.embeddingScore || 0) || a.id.localeCompare(b.id))
|
|
52
43
|
.slice(0, limit)
|
|
53
44
|
.map((rule) => ({
|
|
54
|
-
path: rule.
|
|
45
|
+
path: rule.id,
|
|
55
46
|
score: Math.round(Number(rule.embeddingScore || 0) * 10),
|
|
56
47
|
source: "embedding",
|
|
57
48
|
reasons: [`file-embedding:${Number(rule.embeddingScore || 0).toFixed(2)}`]
|
|
@@ -67,9 +58,11 @@ export async function warmFileEmbeddings({
|
|
|
67
58
|
if (!dataDir) return { count: 0, cachePath: null };
|
|
68
59
|
if (!allowRemote && !isModelCacheReady(dataDir)) return { count: 0, cachePath: null, status: "missing-model" };
|
|
69
60
|
const files = listSourceFiles(cwd, { maxFiles });
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
61
|
+
rebuildImportGraphIndex({ cwd, files, dataDir });
|
|
62
|
+
const items = files.map((filePath) => ({ id: filePath, text: fileSearchText(filePath) }));
|
|
63
|
+
return warmIndexedEmbeddings({
|
|
64
|
+
kind: fileIndexKind(cwd),
|
|
65
|
+
items,
|
|
73
66
|
task: "project file semantic retrieval",
|
|
74
67
|
dataDir,
|
|
75
68
|
sources: [path.join(cwd, "AGENTS.md")],
|
|
@@ -77,6 +70,10 @@ export async function warmFileEmbeddings({
|
|
|
77
70
|
});
|
|
78
71
|
}
|
|
79
72
|
|
|
73
|
+
function fileIndexKind(cwd) {
|
|
74
|
+
return `file:${path.resolve(cwd)}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
80
77
|
function listSourceFiles(cwd, { maxFiles }) {
|
|
81
78
|
const files = [];
|
|
82
79
|
walkFiles(cwd, (filePath) => {
|
|
@@ -4,6 +4,8 @@ import path from "node:path";
|
|
|
4
4
|
const CONTEXTOS_COMMAND_MARKER = "/contextos/plugins/ctx/bin/on-";
|
|
5
5
|
const QUIET_CODE_REVIEW_GRAPH_STATUS_COMMAND =
|
|
6
6
|
"git rev-parse --git-dir >/dev/null 2>&1 && code-review-graph status >/dev/null 2>&1 || true";
|
|
7
|
+
const DRAINED_CODE_REVIEW_GRAPH_UPDATE_COMMAND =
|
|
8
|
+
"cat >/dev/null; git rev-parse --git-dir >/dev/null 2>&1 && code-review-graph update --skip-flows || true";
|
|
7
9
|
|
|
8
10
|
function shellQuote(value) {
|
|
9
11
|
const s = String(value);
|
|
@@ -52,6 +54,23 @@ function quietCodeReviewGraphSessionStart(entries = []) {
|
|
|
52
54
|
}));
|
|
53
55
|
}
|
|
54
56
|
|
|
57
|
+
function drainCodeReviewGraphPostToolUse(entries = []) {
|
|
58
|
+
return entries.map((entry) => ({
|
|
59
|
+
...entry,
|
|
60
|
+
hooks: (entry.hooks || []).map((hook) => {
|
|
61
|
+
if (typeof hook.command === "string" && hook.command.includes("code-review-graph update --skip-flows")) {
|
|
62
|
+
return {
|
|
63
|
+
...hook,
|
|
64
|
+
command: hook.command.includes("cat >/dev/null")
|
|
65
|
+
? hook.command
|
|
66
|
+
: DRAINED_CODE_REVIEW_GRAPH_UPDATE_COMMAND
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return hook;
|
|
70
|
+
})
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
|
|
55
74
|
function commandFor(marketplaceRoot, scriptName, { injectPromptContext = true } = {}) {
|
|
56
75
|
const envPrefix = scriptName === "on-prompt.js" && !injectPromptContext ? "CONTEXTOS_INJECT=0 " : "";
|
|
57
76
|
return `${envPrefix}node ${shellQuote(path.join(marketplaceRoot, "plugins", "ctx", "bin", scriptName))}`;
|
|
@@ -105,6 +124,7 @@ export function buildGlobalHooksConfig(existingConfig, { marketplaceRoot, inject
|
|
|
105
124
|
}
|
|
106
125
|
|
|
107
126
|
config.hooks.SessionStart = quietCodeReviewGraphSessionStart(config.hooks.SessionStart);
|
|
127
|
+
config.hooks.PostToolUse = drainCodeReviewGraphPostToolUse(config.hooks.PostToolUse);
|
|
108
128
|
|
|
109
129
|
return config;
|
|
110
130
|
}
|
|
@@ -3,6 +3,9 @@ import os from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { execFileSync } from "node:child_process";
|
|
5
5
|
|
|
6
|
+
import { appendTelemetry } from "./telemetry.js";
|
|
7
|
+
import { workspaceDataDir } from "./workspace-data.js";
|
|
8
|
+
|
|
6
9
|
const DEFAULT_TIMEOUT_MS = 80;
|
|
7
10
|
const MAX_GRAPH_QUERIES = 10;
|
|
8
11
|
const DEFAULT_CRG_PYTHON = path.join(
|
|
@@ -25,17 +28,47 @@ export function findGraphRelevantFiles({
|
|
|
25
28
|
rules = [],
|
|
26
29
|
seedFiles = [],
|
|
27
30
|
limit = 6,
|
|
28
|
-
timeoutMs = Number(process.env.CONTEXTOS_GRAPH_TIMEOUT_MS || DEFAULT_TIMEOUT_MS)
|
|
31
|
+
timeoutMs = Number(process.env.CONTEXTOS_GRAPH_TIMEOUT_MS || DEFAULT_TIMEOUT_MS),
|
|
32
|
+
python = process.env.CONTEXTOS_CRG_PYTHON || DEFAULT_CRG_PYTHON,
|
|
33
|
+
telemetryPath,
|
|
34
|
+
graphSearch = runCodeReviewGraphSearch,
|
|
35
|
+
appendTelemetryEvent = appendTelemetry
|
|
29
36
|
} = {}) {
|
|
30
37
|
if (process.env.CONTEXTOS_GRAPH_RETRIEVAL === "0") return [];
|
|
31
38
|
if (!hasGraphIndex(cwd)) return [];
|
|
32
39
|
|
|
33
|
-
const python = process.env.CONTEXTOS_CRG_PYTHON || DEFAULT_CRG_PYTHON;
|
|
34
40
|
if (!fs.existsSync(python)) return [];
|
|
35
41
|
|
|
36
42
|
const queries = buildGraphQueries({ task, rules, seedFiles });
|
|
37
43
|
if (!queries.length) return [];
|
|
38
44
|
|
|
45
|
+
try {
|
|
46
|
+
const raw = graphSearch({ cwd, python, queries, limit, timeoutMs });
|
|
47
|
+
const files = mergeGraphResults({ cwd, results: raw, limit });
|
|
48
|
+
recordGraphRetrievalTelemetry({
|
|
49
|
+
cwd,
|
|
50
|
+
telemetryPath,
|
|
51
|
+
appendTelemetryEvent,
|
|
52
|
+
queryCount: queries.length,
|
|
53
|
+
resultCount: files.length,
|
|
54
|
+
status: "ok"
|
|
55
|
+
});
|
|
56
|
+
return files;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
recordGraphRetrievalTelemetry({
|
|
59
|
+
cwd,
|
|
60
|
+
telemetryPath,
|
|
61
|
+
appendTelemetryEvent,
|
|
62
|
+
queryCount: queries.length,
|
|
63
|
+
resultCount: 0,
|
|
64
|
+
status: "error",
|
|
65
|
+
error
|
|
66
|
+
});
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function runCodeReviewGraphSearch({ cwd, python, queries, limit, timeoutMs }) {
|
|
39
72
|
const script = `
|
|
40
73
|
import json
|
|
41
74
|
import sys
|
|
@@ -79,22 +112,56 @@ for query in queries:
|
|
|
79
112
|
print(json.dumps(results))
|
|
80
113
|
`;
|
|
81
114
|
|
|
115
|
+
const output = execFileSync(python, ["-c", script], {
|
|
116
|
+
cwd,
|
|
117
|
+
input: JSON.stringify({ repoRoot: cwd, queries, limit }),
|
|
118
|
+
encoding: "utf8",
|
|
119
|
+
timeout: timeoutMs,
|
|
120
|
+
env: {
|
|
121
|
+
...process.env,
|
|
122
|
+
MPLCONFIGDIR: process.env.MPLCONFIGDIR || path.join(os.tmpdir(), "contextos-mpl")
|
|
123
|
+
},
|
|
124
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
125
|
+
});
|
|
126
|
+
return JSON.parse(output || "[]");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function recordGraphRetrievalTelemetry({
|
|
130
|
+
cwd,
|
|
131
|
+
telemetryPath,
|
|
132
|
+
appendTelemetryEvent = appendTelemetry,
|
|
133
|
+
queryCount = 0,
|
|
134
|
+
resultCount = 0,
|
|
135
|
+
status,
|
|
136
|
+
error
|
|
137
|
+
}) {
|
|
82
138
|
try {
|
|
83
|
-
|
|
84
|
-
cwd,
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
139
|
+
appendTelemetryEvent({
|
|
140
|
+
telemetryPath: telemetryPath || path.join(workspaceDataDir({ cwd }), "telemetry.jsonl"),
|
|
141
|
+
event: "InternalGraphRetrieval",
|
|
142
|
+
payload: {
|
|
143
|
+
cwd,
|
|
144
|
+
source: "graph-retriever",
|
|
145
|
+
method: "internal",
|
|
146
|
+
...(status === "ok"
|
|
147
|
+
? {
|
|
148
|
+
mcp: "code-review-graph",
|
|
149
|
+
toolName: "code-review-graph.semantic_search_nodes"
|
|
150
|
+
}
|
|
151
|
+
: {})
|
|
91
152
|
},
|
|
92
|
-
|
|
153
|
+
extra: {
|
|
154
|
+
source: "graph-retriever",
|
|
155
|
+
backend: "code-review-graph",
|
|
156
|
+
method: "internal",
|
|
157
|
+
status,
|
|
158
|
+
queryCount,
|
|
159
|
+
resultCount,
|
|
160
|
+
...(error ? { error: String(error.message || error).slice(0, 200) } : {})
|
|
161
|
+
}
|
|
93
162
|
});
|
|
94
|
-
const raw = JSON.parse(output || "[]");
|
|
95
|
-
return mergeGraphResults({ cwd, results: raw, limit });
|
|
96
163
|
} catch {
|
|
97
|
-
|
|
164
|
+
// Graph retrieval must remain fail-open when telemetry storage is unavailable.
|
|
98
165
|
}
|
|
99
166
|
}
|
|
100
167
|
|
|
@@ -165,7 +232,7 @@ function mergeGraphResults({ cwd, results, limit }) {
|
|
|
165
232
|
source: "graph",
|
|
166
233
|
reasons: []
|
|
167
234
|
};
|
|
168
|
-
existing.score += 1;
|
|
235
|
+
existing.score += 1 + Number(result.score || 0);
|
|
169
236
|
existing.reasons.push(`graph:${result.query}`);
|
|
170
237
|
byPath.set(normalized, existing);
|
|
171
238
|
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
const DEFAULT_CRG_PYTHON = path.join(
|
|
7
|
+
os.homedir(),
|
|
8
|
+
".local/share/pipx/venvs/code-review-graph/bin/python"
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
export function detectGraphStrategy({
|
|
12
|
+
cwd = process.cwd(),
|
|
13
|
+
pathEnv = process.env.PATH || "",
|
|
14
|
+
mcpServerNames = []
|
|
15
|
+
} = {}) {
|
|
16
|
+
const serverNames = new Set(mcpServerNames);
|
|
17
|
+
const codeReviewGraph = fs.existsSync(path.join(cwd, ".code-review-graph", "graph.db"))
|
|
18
|
+
|| hasExecutable("code-review-graph", pathEnv)
|
|
19
|
+
|| serverNames.has("code-review-graph");
|
|
20
|
+
const codegraph = hasExecutable("codegraph", pathEnv)
|
|
21
|
+
|| serverNames.has("codegraph");
|
|
22
|
+
|
|
23
|
+
const strategy = codeReviewGraph && codegraph
|
|
24
|
+
? "hybrid-adapter-pending"
|
|
25
|
+
: codegraph
|
|
26
|
+
? "codegraph-detected-adapter-pending"
|
|
27
|
+
: codeReviewGraph
|
|
28
|
+
? "code-review-graph"
|
|
29
|
+
: "none";
|
|
30
|
+
|
|
31
|
+
return { strategy, codeReviewGraph, codegraph };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function formatGraphStrategy(result) {
|
|
35
|
+
const detected = [
|
|
36
|
+
result.codeReviewGraph ? "code-review-graph" : null,
|
|
37
|
+
result.codegraph ? "codegraph" : null
|
|
38
|
+
].filter(Boolean);
|
|
39
|
+
return `${result.strategy}${detected.length ? ` (${detected.join(", ")})` : ""}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function embedCodeReviewGraph({
|
|
43
|
+
cwd = process.cwd(),
|
|
44
|
+
python = process.env.CONTEXTOS_CRG_PYTHON || DEFAULT_CRG_PYTHON,
|
|
45
|
+
run = execFileSync
|
|
46
|
+
} = {}) {
|
|
47
|
+
if (!fs.existsSync(path.join(cwd, ".code-review-graph", "graph.db"))) {
|
|
48
|
+
return { status: "skipped", reason: "missing-graph-index" };
|
|
49
|
+
}
|
|
50
|
+
if (!fs.existsSync(python)) {
|
|
51
|
+
return { status: "skipped", reason: "missing-code-review-graph-python" };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const script = `
|
|
55
|
+
import json
|
|
56
|
+
|
|
57
|
+
from code_review_graph.tools.docs import embed_graph
|
|
58
|
+
|
|
59
|
+
print(json.dumps(embed_graph(repo_root=${JSON.stringify(cwd)}, provider="local")))
|
|
60
|
+
`;
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const output = run(python, ["-c", script], {
|
|
64
|
+
cwd,
|
|
65
|
+
encoding: "utf8",
|
|
66
|
+
timeout: Number(process.env.CONTEXTOS_CRG_EMBED_TIMEOUT_MS || 120_000),
|
|
67
|
+
env: {
|
|
68
|
+
...process.env,
|
|
69
|
+
MPLCONFIGDIR: process.env.MPLCONFIGDIR || path.join(os.tmpdir(), "contextos-mpl")
|
|
70
|
+
},
|
|
71
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
72
|
+
});
|
|
73
|
+
const response = JSON.parse(output || "{}");
|
|
74
|
+
return {
|
|
75
|
+
status: "embedded",
|
|
76
|
+
newlyEmbedded: Number(response.newly_embedded || 0),
|
|
77
|
+
totalEmbeddings: Number(response.total_embeddings || response.embeddings_count || 0)
|
|
78
|
+
};
|
|
79
|
+
} catch (error) {
|
|
80
|
+
return {
|
|
81
|
+
status: "skipped",
|
|
82
|
+
reason: "embed-failed",
|
|
83
|
+
error: String(error.stderr || error.message || error).trim().slice(0, 200)
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function formatCodeReviewGraphEmbedding(result) {
|
|
89
|
+
if (result.status === "embedded") {
|
|
90
|
+
return `${result.totalEmbeddings} nodes (${result.newlyEmbedded} new)`;
|
|
91
|
+
}
|
|
92
|
+
if (result.reason === "missing-graph-index") return "skipped (no .code-review-graph/graph.db)";
|
|
93
|
+
if (result.reason === "missing-code-review-graph-python") return "skipped (code-review-graph Python unavailable)";
|
|
94
|
+
return `skipped (${result.error || result.reason || "unavailable"})`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function hasExecutable(command, pathEnv) {
|
|
98
|
+
return String(pathEnv || "").split(path.delimiter).some((directory) => {
|
|
99
|
+
if (!directory) return false;
|
|
100
|
+
try {
|
|
101
|
+
fs.accessSync(path.join(directory, command), fs.constants.X_OK);
|
|
102
|
+
return true;
|
|
103
|
+
} catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
@@ -12,7 +12,35 @@ export async function readStdinJson() {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export function writeJson(value) {
|
|
15
|
-
|
|
15
|
+
try {
|
|
16
|
+
process.stdout.write(`${JSON.stringify(value)}\n`);
|
|
17
|
+
} catch (error) {
|
|
18
|
+
if (error?.code !== "EPIPE") throw error;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function exitAfterStdout(code = 0) {
|
|
23
|
+
if (process.stdout.writableNeedDrain) {
|
|
24
|
+
process.stdout.once("drain", () => process.exit(code));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
setImmediate(() => process.exit(code));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function armHookDeadline(event, fallback, {
|
|
31
|
+
timeoutMs = Number(process.env.CONTEXTOS_HOOK_DEADLINE_MS || 8500)
|
|
32
|
+
} = {}) {
|
|
33
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return { clear() {} };
|
|
34
|
+
const timer = setTimeout(() => {
|
|
35
|
+
logError(event, new Error(`${event} hook deadline exceeded after ${timeoutMs}ms`));
|
|
36
|
+
writeJson(fallback);
|
|
37
|
+
exitAfterStdout(0);
|
|
38
|
+
}, timeoutMs);
|
|
39
|
+
return {
|
|
40
|
+
clear() {
|
|
41
|
+
clearTimeout(timer);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
16
44
|
}
|
|
17
45
|
|
|
18
46
|
export function resolveHookCwd(payload = {}) {
|
|
@@ -4,27 +4,14 @@ import path from "node:path";
|
|
|
4
4
|
const JS_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
|
|
5
5
|
const IMPORT_RE = /\bimport\s+(?:[^'"]+\s+from\s+)?['"]([^'"]+)['"]|\brequire\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
6
6
|
|
|
7
|
-
export function expandImportGraph({ cwd = process.cwd(), seedFiles = [], limit = 6 } = {}) {
|
|
7
|
+
export function expandImportGraph({ cwd = process.cwd(), seedFiles = [], dataDir, limit = 6 } = {}) {
|
|
8
8
|
const seeds = new Set(seedFiles.map((file) => normalizeRel(file.path)).filter(Boolean));
|
|
9
9
|
if (!seeds.size) return [];
|
|
10
10
|
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const fileSet = new Set(files);
|
|
16
|
-
const outgoing = new Map();
|
|
17
|
-
const incoming = new Map();
|
|
18
|
-
|
|
19
|
-
for (const rel of files) {
|
|
20
|
-
const imports = resolveImports({ cwd, rel, fileSet });
|
|
21
|
-
outgoing.set(rel, imports);
|
|
22
|
-
for (const target of imports) {
|
|
23
|
-
const importers = incoming.get(target) || new Set();
|
|
24
|
-
importers.add(rel);
|
|
25
|
-
incoming.set(target, importers);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
11
|
+
const index = readImportGraphIndex({ cwd, dataDir });
|
|
12
|
+
if (!index) return [];
|
|
13
|
+
const outgoing = objectToMap(index.outgoing);
|
|
14
|
+
const incoming = objectToMap(index.incoming);
|
|
28
15
|
|
|
29
16
|
const candidates = new Map();
|
|
30
17
|
for (const seed of seeds) {
|
|
@@ -42,15 +29,27 @@ export function expandImportGraph({ cwd = process.cwd(), seedFiles = [], limit =
|
|
|
42
29
|
.slice(0, limit);
|
|
43
30
|
}
|
|
44
31
|
|
|
45
|
-
function
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
32
|
+
export function rebuildImportGraphIndex({ cwd = process.cwd(), files = [], dataDir } = {}) {
|
|
33
|
+
if (!dataDir) return { count: 0, path: null };
|
|
34
|
+
const normalizedFiles = [...new Set(files.map(normalizeRel).filter(Boolean))];
|
|
35
|
+
const fileSet = new Set(normalizedFiles);
|
|
36
|
+
const outgoing = {};
|
|
37
|
+
const incoming = {};
|
|
38
|
+
|
|
39
|
+
for (const rel of normalizedFiles) {
|
|
40
|
+
const imports = resolveImports({ cwd, rel, fileSet });
|
|
41
|
+
outgoing[rel] = imports;
|
|
42
|
+
for (const target of imports) {
|
|
43
|
+
incoming[target] = [...new Set([...(incoming[target] || []), rel])];
|
|
44
|
+
}
|
|
52
45
|
}
|
|
53
|
-
|
|
46
|
+
|
|
47
|
+
const indexPath = importGraphIndexPath(dataDir);
|
|
48
|
+
fs.mkdirSync(path.dirname(indexPath), { recursive: true });
|
|
49
|
+
const tmpPath = `${indexPath}.tmp-${process.pid}-${Date.now()}`;
|
|
50
|
+
fs.writeFileSync(tmpPath, `${JSON.stringify({ cwd: path.resolve(cwd), outgoing, incoming })}\n`, "utf8");
|
|
51
|
+
fs.renameSync(tmpPath, indexPath);
|
|
52
|
+
return { count: normalizedFiles.length, path: indexPath };
|
|
54
53
|
}
|
|
55
54
|
|
|
56
55
|
function addImportCandidate(candidates, filePath, reason, score) {
|
|
@@ -103,22 +102,20 @@ function normalizeRel(filePath) {
|
|
|
103
102
|
return normalized;
|
|
104
103
|
}
|
|
105
104
|
|
|
106
|
-
function
|
|
107
|
-
if (
|
|
108
|
-
let entries = [];
|
|
105
|
+
function readImportGraphIndex({ cwd, dataDir }) {
|
|
106
|
+
if (!dataDir) return null;
|
|
109
107
|
try {
|
|
110
|
-
|
|
108
|
+
const index = JSON.parse(fs.readFileSync(importGraphIndexPath(dataDir), "utf8"));
|
|
109
|
+
return index.cwd === path.resolve(cwd) ? index : null;
|
|
111
110
|
} catch {
|
|
112
|
-
return;
|
|
111
|
+
return null;
|
|
113
112
|
}
|
|
113
|
+
}
|
|
114
114
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
onFile(fullPath);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
115
|
+
function importGraphIndexPath(dataDir) {
|
|
116
|
+
return path.join(dataDir, "import-graph.json");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function objectToMap(value = {}) {
|
|
120
|
+
return new Map(Object.entries(value));
|
|
124
121
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
|
+
import { readMcpServersFromToml, updateMcpServerFields } from "./toml-config.js";
|
|
5
|
+
|
|
4
6
|
const DEFAULT_EXCLUDES = new Set(["ctx-mcp"]);
|
|
5
7
|
|
|
6
8
|
export function installMcpTelemetryProxies({ codexHome, marketplaceRoot, targets = null, excludes = DEFAULT_EXCLUDES } = {}) {
|
|
@@ -17,55 +19,46 @@ export function installMcpTelemetryProxies({ codexHome, marketplaceRoot, targets
|
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
export function rewriteMcpTelemetryProxies(toml, { proxyPath, targets = null, excludes = DEFAULT_EXCLUDES } = {}) {
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
+
let content = String(toml || "");
|
|
23
|
+
const servers = readMcpServersFromToml(content);
|
|
22
24
|
const wrapped = [];
|
|
23
25
|
const skipped = [];
|
|
24
26
|
|
|
25
|
-
for (const
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
const args = findArrayValue(body, "args") || [];
|
|
29
|
-
const shouldProxy = shouldProxyServer(section.name, { targets, excludes });
|
|
27
|
+
for (const server of servers) {
|
|
28
|
+
const { name, command, args } = server;
|
|
29
|
+
const shouldProxy = shouldProxyServer(name, { targets, excludes });
|
|
30
30
|
|
|
31
31
|
if (command === "node" && args[0] === proxyPath && !shouldProxy) {
|
|
32
32
|
const original = unwrapProxyArgs(args);
|
|
33
33
|
if (original) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
"args",
|
|
37
|
-
tomlArray(original.args)
|
|
38
|
-
);
|
|
39
|
-
lines.splice(section.start + 1, section.end - section.start - 1, ...nextBody);
|
|
40
|
-
skipped.push({ name: section.name, reason: "unwrapped-non-target" });
|
|
34
|
+
content = updateMcpServerFields(content, name, original);
|
|
35
|
+
skipped.push({ name, reason: "unwrapped-non-target" });
|
|
41
36
|
continue;
|
|
42
37
|
}
|
|
43
38
|
}
|
|
44
39
|
|
|
45
40
|
if (!shouldProxy) {
|
|
46
|
-
skipped.push({ name
|
|
41
|
+
skipped.push({ name, reason: "not-targeted" });
|
|
47
42
|
continue;
|
|
48
43
|
}
|
|
49
44
|
|
|
50
45
|
if (!command) {
|
|
51
|
-
skipped.push({ name
|
|
46
|
+
skipped.push({ name, reason: "missing-command" });
|
|
52
47
|
continue;
|
|
53
48
|
}
|
|
54
49
|
if (command === "node" && args[0] === proxyPath) {
|
|
55
|
-
skipped.push({ name
|
|
50
|
+
skipped.push({ name, reason: "already-wrapped" });
|
|
56
51
|
continue;
|
|
57
52
|
}
|
|
58
53
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
);
|
|
64
|
-
lines.splice(section.start + 1, section.end - section.start - 1, ...nextBody);
|
|
65
|
-
wrapped.push({ name: section.name, command, args });
|
|
54
|
+
content = updateMcpServerFields(content, name, {
|
|
55
|
+
command: "node",
|
|
56
|
+
args: [proxyPath, "--name", name, "--", command, ...args]
|
|
57
|
+
});
|
|
58
|
+
wrapped.push({ name, command, args });
|
|
66
59
|
}
|
|
67
60
|
|
|
68
|
-
return { content
|
|
61
|
+
return { content, wrapped, skipped };
|
|
69
62
|
}
|
|
70
63
|
|
|
71
64
|
function shouldProxyServer(name, { targets, excludes }) {
|
|
@@ -82,68 +75,3 @@ function unwrapProxyArgs(args) {
|
|
|
82
75
|
args: args.slice(separator + 2)
|
|
83
76
|
};
|
|
84
77
|
}
|
|
85
|
-
|
|
86
|
-
function findMcpServerSections(lines) {
|
|
87
|
-
const sections = [];
|
|
88
|
-
for (let index = 0; index < lines.length; index += 1) {
|
|
89
|
-
const match = lines[index].match(/^\[mcp_servers\.([^\].]+)\]\s*$/);
|
|
90
|
-
if (!match) continue;
|
|
91
|
-
let end = lines.length;
|
|
92
|
-
for (let cursor = index + 1; cursor < lines.length; cursor += 1) {
|
|
93
|
-
if (/^\[/.test(lines[cursor])) {
|
|
94
|
-
end = cursor;
|
|
95
|
-
break;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
sections.push({ name: unquoteTomlKey(match[1]), start: index, end });
|
|
99
|
-
}
|
|
100
|
-
return sections;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function replaceOrInsertServerField(body, key, value) {
|
|
104
|
-
const next = [...body];
|
|
105
|
-
const index = next.findIndex((line) => new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`).test(line));
|
|
106
|
-
const line = `${key} = ${value}`;
|
|
107
|
-
if (index >= 0) next[index] = line;
|
|
108
|
-
else next.unshift(line);
|
|
109
|
-
return next;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function findStringValue(lines, key) {
|
|
113
|
-
const line = lines.find((item) => new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`).test(item));
|
|
114
|
-
if (!line) return null;
|
|
115
|
-
const match = line.match(/=\s*"((?:\\.|[^"\\])*)"/);
|
|
116
|
-
return match ? unescapeTomlString(match[1]) : null;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function findArrayValue(lines, key) {
|
|
120
|
-
const line = lines.find((item) => new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`).test(item));
|
|
121
|
-
if (!line) return null;
|
|
122
|
-
const arrayMatch = line.match(/=\s*\[(.*)\]\s*$/);
|
|
123
|
-
if (!arrayMatch) return null;
|
|
124
|
-
const values = [];
|
|
125
|
-
const pattern = /"((?:\\.|[^"\\])*)"/g;
|
|
126
|
-
let match;
|
|
127
|
-
while ((match = pattern.exec(arrayMatch[1]))) values.push(unescapeTomlString(match[1]));
|
|
128
|
-
return values;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function tomlArray(values) {
|
|
132
|
-
return `[${values.map(tomlString).join(", ")}]`;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function tomlString(value) {
|
|
136
|
-
return `"${String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function unescapeTomlString(value) {
|
|
140
|
-
return String(value).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function unquoteTomlKey(value) {
|
|
144
|
-
return value.replace(/^"|"$/g, "");
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function escapeRegExp(value) {
|
|
148
|
-
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
149
|
-
}
|