@minhpnq1807/contextos 0.5.42 → 0.5.44
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 +26 -0
- package/README.md +41 -10
- package/bin/ctx.js +136 -39
- 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 +12 -94
- 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 +95 -20
- package/plugins/ctx/lib/ruler-sync.js +9 -71
- package/plugins/ctx/lib/scheduler.js +33 -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 +110 -10
- 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
|
@@ -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
|
-
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { writeJsonFile } from "./fs-utils.js";
|
|
5
|
+
import { defaultDataRoot } from "./workspace-data.js";
|
|
6
|
+
|
|
7
|
+
const CONFIG_FILE = "output-config.json";
|
|
8
|
+
|
|
9
|
+
export const OUTPUT_SECTION_OPTIONS = [
|
|
10
|
+
{ value: "rules", label: "Critical ContextOS rules", hint: "Include critical and additional relevant AGENTS.md rules." },
|
|
11
|
+
{ value: "files", label: "Suggested files to check", hint: "Include semantic, import-graph, and code-review-graph file suggestions." },
|
|
12
|
+
{ value: "skills", label: "Suggested skills for this task", hint: "Include matching local skill recommendations." },
|
|
13
|
+
{ value: "workflows", label: "Suggested workflow for this task", hint: "Include matching workflow recommendations." }
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
export function defaultOutputConfig() {
|
|
17
|
+
return {
|
|
18
|
+
sections: Object.fromEntries(OUTPUT_SECTION_OPTIONS.map((option) => [option.value, true]))
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function outputConfigPath(dataRoot = defaultDataRoot()) {
|
|
23
|
+
return path.join(dataRoot, CONFIG_FILE);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function loadOutputConfig({ dataRoot = defaultDataRoot() } = {}) {
|
|
27
|
+
try {
|
|
28
|
+
return normalizeOutputConfig(JSON.parse(fs.readFileSync(outputConfigPath(dataRoot), "utf8")));
|
|
29
|
+
} catch {
|
|
30
|
+
return defaultOutputConfig();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function saveOutputConfig(config, { dataRoot = defaultDataRoot() } = {}) {
|
|
35
|
+
const normalized = normalizeOutputConfig(config);
|
|
36
|
+
writeJsonFile(outputConfigPath(dataRoot), normalized);
|
|
37
|
+
return normalized;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function enabledOutputSections(config = loadOutputConfig()) {
|
|
41
|
+
const normalized = normalizeOutputConfig(config);
|
|
42
|
+
return OUTPUT_SECTION_OPTIONS
|
|
43
|
+
.filter((option) => normalized.sections[option.value])
|
|
44
|
+
.map((option) => option.value);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function enabledOutputSectionsLabel(config = loadOutputConfig()) {
|
|
48
|
+
const enabled = enabledOutputSections(config);
|
|
49
|
+
return enabled.length ? enabled.join(", ") : "(none)";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function configureOutputSections({
|
|
53
|
+
dataRoot = defaultDataRoot(),
|
|
54
|
+
select,
|
|
55
|
+
logger = console.log
|
|
56
|
+
} = {}) {
|
|
57
|
+
if (typeof select !== "function") throw new Error("configureOutputSections requires a multi-select function");
|
|
58
|
+
const current = loadOutputConfig({ dataRoot });
|
|
59
|
+
const selected = await select({
|
|
60
|
+
message: "Select ContextOS prompt sections to show:",
|
|
61
|
+
options: OUTPUT_SECTION_OPTIONS.map((option) => ({
|
|
62
|
+
...option,
|
|
63
|
+
selected: current.sections[option.value]
|
|
64
|
+
}))
|
|
65
|
+
});
|
|
66
|
+
const selectedSet = new Set(selected);
|
|
67
|
+
const saved = saveOutputConfig({
|
|
68
|
+
sections: Object.fromEntries(OUTPUT_SECTION_OPTIONS.map((option) => [option.value, selectedSet.has(option.value)]))
|
|
69
|
+
}, { dataRoot });
|
|
70
|
+
logger(`│ Saved ContextOS prompt section config: ${outputConfigPath(dataRoot)}`);
|
|
71
|
+
logger(`│ Enabled sections: ${enabledOutputSectionsLabel(saved)}`);
|
|
72
|
+
return saved;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function normalizeOutputConfig(config = {}) {
|
|
76
|
+
const defaults = defaultOutputConfig();
|
|
77
|
+
return {
|
|
78
|
+
sections: Object.fromEntries(OUTPUT_SECTION_OPTIONS.map((option) => [
|
|
79
|
+
option.value,
|
|
80
|
+
typeof config.sections?.[option.value] === "boolean"
|
|
81
|
+
? config.sections[option.value]
|
|
82
|
+
: defaults.sections[option.value]
|
|
83
|
+
]))
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -34,3 +34,11 @@ export function copyPackageRoot({ rootDir, targetRoot }) {
|
|
|
34
34
|
}
|
|
35
35
|
return targetRoot;
|
|
36
36
|
}
|
|
37
|
+
|
|
38
|
+
export function syncPackageRoot({ rootDir, targetRoot }) {
|
|
39
|
+
if (path.resolve(rootDir) === path.resolve(targetRoot)) {
|
|
40
|
+
return { targetRoot, synced: false };
|
|
41
|
+
}
|
|
42
|
+
copyPackageRoot({ rootDir, targetRoot });
|
|
43
|
+
return { targetRoot, synced: true };
|
|
44
|
+
}
|