@minhpnq1807/contextos 0.1.9 → 0.3.1
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 +35 -0
- package/README.md +119 -18
- package/bin/ctx.js +87 -37
- package/package.json +5 -2
- package/plugins/ctx/bin/on-antigravity-preinvocation.js +36 -0
- package/plugins/ctx/bin/on-antigravity-stop.js +28 -0
- package/plugins/ctx/bin/on-prompt.js +6 -5
- package/plugins/ctx/bin/on-session-start.js +5 -4
- package/plugins/ctx/bin/on-stop.js +6 -5
- package/plugins/ctx/lib/analyzer.js +17 -1
- package/plugins/ctx/lib/antigravity-adapter.js +56 -0
- package/plugins/ctx/lib/antigravity-hooks.js +53 -0
- package/plugins/ctx/lib/antigravity-mcp.js +43 -0
- package/plugins/ctx/lib/benchmark.js +72 -0
- package/plugins/ctx/lib/claude-hooks.js +27 -0
- package/plugins/ctx/lib/claude-mcp.js +33 -0
- package/plugins/ctx/lib/embedding-scorer.js +13 -0
- package/plugins/ctx/lib/file-embedding-retriever.js +2 -1
- package/plugins/ctx/lib/hook-io.js +16 -0
- package/plugins/ctx/lib/measure.js +12 -4
- package/plugins/ctx/lib/package-install.js +36 -0
- package/plugins/ctx/lib/prompt-hook.js +2 -1
- package/plugins/ctx/lib/reporter.js +11 -3
- package/plugins/ctx/lib/ruler-sync.js +478 -0
- package/plugins/ctx/lib/stats.js +4 -1
- package/plugins/ctx/lib/stop-hook.js +2 -1
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readStdinJson, writeJson, failOpen, logDebug, pluginRuntimeFile } from "../lib/hook-io.js";
|
|
2
|
+
import { readStdinJson, writeJson, failOpen, logDebug, pluginRuntimeFile, resolveHookCwd } from "../lib/hook-io.js";
|
|
3
3
|
import { handleStopPayload } from "../lib/stop-hook.js";
|
|
4
4
|
import { appendTelemetry } from "../lib/telemetry.js";
|
|
5
5
|
|
|
6
6
|
try {
|
|
7
7
|
const payload = await readStdinJson();
|
|
8
|
-
const cwd = payload
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
const cwd = resolveHookCwd(payload);
|
|
9
|
+
const normalized = { ...payload, cwd };
|
|
10
|
+
logDebug("Stop", normalized);
|
|
11
|
+
appendTelemetry({ telemetryPath: pluginRuntimeFile("telemetry.jsonl", cwd), event: "Stop", payload: normalized });
|
|
12
|
+
writeJson(handleStopPayload(normalized, {
|
|
12
13
|
contextPath: pluginRuntimeFile("last-prompt-context.json", cwd),
|
|
13
14
|
reportPath: pluginRuntimeFile("last-report.json", cwd),
|
|
14
15
|
historyPath: pluginRuntimeFile("report-history.jsonl", cwd),
|
|
@@ -43,7 +43,10 @@ const SEMANTIC_ALIASES = {
|
|
|
43
43
|
thong: ["notification", "notify", "message"],
|
|
44
44
|
bao: ["notification", "notify", "message"],
|
|
45
45
|
"thong-bao": ["notification", "notify", "message"],
|
|
46
|
-
thongbao: ["notification", "notify", "message"]
|
|
46
|
+
thongbao: ["notification", "notify", "message"],
|
|
47
|
+
authen: ["auth", "authentication", "login"],
|
|
48
|
+
authentication: ["auth", "authen", "login"],
|
|
49
|
+
recheck: ["check", "verify", "review"]
|
|
47
50
|
};
|
|
48
51
|
|
|
49
52
|
const MODERATION_TOKENS = new Set(["moderation", "moderate", "content-moderation", "approval", "approved", "reject", "rejected", "needs_review"]);
|
|
@@ -81,6 +84,11 @@ const TOOL_REFERENCE_TOKENS = new Set([
|
|
|
81
84
|
"list_communities"
|
|
82
85
|
]);
|
|
83
86
|
|
|
87
|
+
const ACTION_TOKENS = new Set([
|
|
88
|
+
"add", "avoid", "call", "check", "derive", "ensure", "filter", "follow", "prefer", "run",
|
|
89
|
+
"use", "validate", "verify", "write", "never", "always", "must", "should", "do"
|
|
90
|
+
]);
|
|
91
|
+
|
|
84
92
|
export function tokenize(value) {
|
|
85
93
|
const normalized = String(value || "")
|
|
86
94
|
.toLowerCase()
|
|
@@ -190,6 +198,7 @@ export function isDocumentationOnlyRule(rule) {
|
|
|
190
198
|
if (/^<!--.*-->$/.test(normalized)) return true;
|
|
191
199
|
if (DOCUMENTATION_HEADING_PATTERNS.some((pattern) => pattern.test(normalized))) return true;
|
|
192
200
|
if (isMarkdownTableRule(normalized)) return true;
|
|
201
|
+
if (isGenericHeading(normalized)) return true;
|
|
193
202
|
return false;
|
|
194
203
|
}
|
|
195
204
|
|
|
@@ -209,6 +218,13 @@ function isMarkdownTableRule(content) {
|
|
|
209
218
|
return /\btool\b/.test(lower) && /\buse\s+when\b/.test(lower) && toolReferenceCount >= 2;
|
|
210
219
|
}
|
|
211
220
|
|
|
221
|
+
function isGenericHeading(content) {
|
|
222
|
+
if (content.length > 80 || /[`.:;]/.test(content)) return false;
|
|
223
|
+
const tokens = tokenize(content);
|
|
224
|
+
if (tokens.length > 4) return false;
|
|
225
|
+
return !tokens.some((token) => ACTION_TOKENS.has(token));
|
|
226
|
+
}
|
|
227
|
+
|
|
212
228
|
function dedupeRules(rules) {
|
|
213
229
|
const seen = new Set();
|
|
214
230
|
return rules.filter((rule) => {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { resolveHookCwd } from "./hook-io.js";
|
|
3
|
+
|
|
4
|
+
export function antigravityCwd(payload) {
|
|
5
|
+
return payload.cwd
|
|
6
|
+
|| payload.working_directory
|
|
7
|
+
|| payload.workspacePath
|
|
8
|
+
|| payload.workspacePaths?.[0]
|
|
9
|
+
|| resolveHookCwd(payload);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function textFromValue(value) {
|
|
13
|
+
if (!value) return "";
|
|
14
|
+
if (typeof value === "string") return value;
|
|
15
|
+
if (Array.isArray(value)) return value.map(textFromValue).filter(Boolean).join("\n");
|
|
16
|
+
if (typeof value !== "object") return "";
|
|
17
|
+
if (typeof value.text === "string") return value.text;
|
|
18
|
+
if (typeof value.content === "string") return value.content;
|
|
19
|
+
if (typeof value.message === "string") return value.message;
|
|
20
|
+
if (typeof value.userMessage === "string") return value.userMessage;
|
|
21
|
+
if (Array.isArray(value.parts)) return textFromValue(value.parts);
|
|
22
|
+
if (Array.isArray(value.content)) return textFromValue(value.content);
|
|
23
|
+
return "";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function looksUserAuthored(record) {
|
|
27
|
+
const role = String(record.role || record.author || record.type || record.sender || "").toLowerCase();
|
|
28
|
+
return !role || role.includes("user") || role.includes("human");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function extractPromptFromAntigravityPayload(payload) {
|
|
32
|
+
const direct = payload.prompt || payload.userPrompt || payload.userMessage || payload.message;
|
|
33
|
+
if (direct) return textFromValue(direct);
|
|
34
|
+
|
|
35
|
+
const transcriptPath = payload.transcriptPath || payload.transcript_path;
|
|
36
|
+
if (!transcriptPath || !fs.existsSync(transcriptPath)) return "";
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const lines = fs.readFileSync(transcriptPath, "utf8").trim().split(/\r?\n/).filter(Boolean).slice(-200);
|
|
40
|
+
for (const line of lines.reverse()) {
|
|
41
|
+
let record;
|
|
42
|
+
try {
|
|
43
|
+
record = JSON.parse(line);
|
|
44
|
+
} catch {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (!looksUserAuthored(record)) continue;
|
|
48
|
+
const text = textFromValue(record.prompt || record.userPrompt || record.userMessage || record.message || record.content || record.parts);
|
|
49
|
+
if (text.trim()) return text.trim();
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
return "";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return "";
|
|
56
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
function shellQuote(value) {
|
|
5
|
+
return `'${String(value).replaceAll("'", "'\\''")}'`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function readJsonFile(filePath, fallback) {
|
|
9
|
+
if (!fs.existsSync(filePath)) return fallback;
|
|
10
|
+
const raw = fs.readFileSync(filePath, "utf8").trim();
|
|
11
|
+
if (!raw) return fallback;
|
|
12
|
+
return JSON.parse(raw);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function commandFor(installRoot, scriptName, { injectPromptContext = true } = {}) {
|
|
16
|
+
const envPrefix = scriptName === "on-antigravity-preinvocation.js" && !injectPromptContext ? "CONTEXTOS_INJECT=0 " : "";
|
|
17
|
+
return `${envPrefix}node ${shellQuote(path.join(installRoot, "plugins", "ctx", "bin", scriptName))}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function antigravityHooksPath() {
|
|
21
|
+
return process.env.ANTIGRAVITY_HOOKS_PATH
|
|
22
|
+
|| path.join(process.env.HOME || process.cwd(), ".gemini", "config", "hooks.json");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function buildAntigravityHooksConfig(existingConfig, { installRoot, injectPromptContext = true } = {}) {
|
|
26
|
+
const config = existingConfig && typeof existingConfig === "object" ? structuredClone(existingConfig) : {};
|
|
27
|
+
config.contextos = {
|
|
28
|
+
enabled: true,
|
|
29
|
+
PreInvocation: [
|
|
30
|
+
{
|
|
31
|
+
type: "command",
|
|
32
|
+
command: commandFor(installRoot, "on-antigravity-preinvocation.js", { injectPromptContext }),
|
|
33
|
+
timeout: 10
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
Stop: [
|
|
37
|
+
{
|
|
38
|
+
type: "command",
|
|
39
|
+
command: commandFor(installRoot, "on-antigravity-stop.js"),
|
|
40
|
+
timeout: 10
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
};
|
|
44
|
+
return config;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function installAntigravityHooks({ hooksPath = antigravityHooksPath(), installRoot, injectPromptContext = true } = {}) {
|
|
48
|
+
const existing = readJsonFile(hooksPath, {});
|
|
49
|
+
const next = buildAntigravityHooksConfig(existing, { installRoot, injectPromptContext });
|
|
50
|
+
fs.mkdirSync(path.dirname(hooksPath), { recursive: true });
|
|
51
|
+
fs.writeFileSync(hooksPath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
|
52
|
+
return hooksPath;
|
|
53
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
function readJsonFile(filePath, fallback) {
|
|
5
|
+
if (!fs.existsSync(filePath)) return fallback;
|
|
6
|
+
const raw = fs.readFileSync(filePath, "utf8").trim();
|
|
7
|
+
if (!raw) return fallback;
|
|
8
|
+
return JSON.parse(raw);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function antigravityMcpConfigPaths() {
|
|
12
|
+
if (process.env.ANTIGRAVITY_MCP_CONFIG_PATH) {
|
|
13
|
+
return [process.env.ANTIGRAVITY_MCP_CONFIG_PATH];
|
|
14
|
+
}
|
|
15
|
+
const home = process.env.HOME || process.cwd();
|
|
16
|
+
return [
|
|
17
|
+
path.join(home, ".gemini", "antigravity", "mcp_config.json"),
|
|
18
|
+
path.join(home, ".gemini", "antigravity-cli", "mcp_config.json"),
|
|
19
|
+
path.join(home, ".gemini", "config", "mcp_config.json")
|
|
20
|
+
];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function buildAntigravityMcpConfig(existingConfig, { installRoot } = {}) {
|
|
24
|
+
const config = existingConfig && typeof existingConfig === "object" ? structuredClone(existingConfig) : {};
|
|
25
|
+
if (!config.mcpServers || typeof config.mcpServers !== "object") config.mcpServers = {};
|
|
26
|
+
config.mcpServers["ctx-mcp"] = {
|
|
27
|
+
command: "node",
|
|
28
|
+
args: [path.join(installRoot, "plugins", "ctx", "mcp", "server.js")]
|
|
29
|
+
};
|
|
30
|
+
return config;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function installAntigravityMcp({ configPaths = antigravityMcpConfigPaths(), installRoot } = {}) {
|
|
34
|
+
const written = [];
|
|
35
|
+
for (const configPath of configPaths) {
|
|
36
|
+
const existing = readJsonFile(configPath, {});
|
|
37
|
+
const next = buildAntigravityMcpConfig(existing, { installRoot });
|
|
38
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
39
|
+
fs.writeFileSync(configPath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
|
40
|
+
written.push(configPath);
|
|
41
|
+
}
|
|
42
|
+
return written;
|
|
43
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { parseRules, filterActionableRules, scoreRules } from "./analyzer.js";
|
|
2
|
+
import { readAgentsChain } from "./reader.js";
|
|
3
|
+
import { scheduleContext } from "./scheduler.js";
|
|
4
|
+
|
|
5
|
+
export function benchmarkContext({ markdown, sources = [], task = "", openFiles = [], topK = 8 } = {}) {
|
|
6
|
+
const parsedRules = parseRules(markdown);
|
|
7
|
+
const actionableRules = filterActionableRules(parsedRules);
|
|
8
|
+
const scoredRules = scoreRules(actionableRules, task, openFiles);
|
|
9
|
+
const relevantRules = scoredRules.filter((rule) => Number(rule.score || 0) >= 0.1);
|
|
10
|
+
const scheduled = scheduleContext({ rules: scoredRules, relevantFiles: [] });
|
|
11
|
+
|
|
12
|
+
const originalPositions = new Map(actionableRules.map((rule, index) => [rule.content, index]));
|
|
13
|
+
const middleStart = Math.floor(actionableRules.length * 0.25);
|
|
14
|
+
const middleEnd = Math.ceil(actionableRules.length * 0.75);
|
|
15
|
+
const lostMiddle = relevantRules.filter((rule) => {
|
|
16
|
+
const index = originalPositions.get(rule.content);
|
|
17
|
+
return typeof index === "number" && index >= middleStart && index <= middleEnd;
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
task,
|
|
22
|
+
sources,
|
|
23
|
+
rulesParsed: parsedRules.length,
|
|
24
|
+
actionableRules: actionableRules.length,
|
|
25
|
+
filteredRules: parsedRules.length - actionableRules.length,
|
|
26
|
+
relevantRules: relevantRules.length,
|
|
27
|
+
baseline: {
|
|
28
|
+
relevantRulesInMiddle: lostMiddle.length,
|
|
29
|
+
middleRiskPercent: relevantRules.length ? Math.round((lostMiddle.length / relevantRules.length) * 100) : 0
|
|
30
|
+
},
|
|
31
|
+
contextOS: {
|
|
32
|
+
highRules: scheduled.highRules.length,
|
|
33
|
+
midRules: scheduled.midRules.length,
|
|
34
|
+
topRules: scoredRules.slice(0, topK).map((rule) => ({
|
|
35
|
+
score: rule.score,
|
|
36
|
+
content: rule.content,
|
|
37
|
+
reasons: rule.reasons || []
|
|
38
|
+
})),
|
|
39
|
+
repeatsHighRulesAtEnd: scheduled.highRules.length > 0
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function benchmarkWorkspace({ cwd = process.cwd(), task = "", openFiles = [], topK = 8 } = {}) {
|
|
45
|
+
const merged = readAgentsChain({ cwd });
|
|
46
|
+
return benchmarkContext({
|
|
47
|
+
markdown: merged.content,
|
|
48
|
+
sources: merged.sources,
|
|
49
|
+
task,
|
|
50
|
+
openFiles,
|
|
51
|
+
topK
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function formatBenchmark(result) {
|
|
56
|
+
const lines = [];
|
|
57
|
+
lines.push("ContextOS benchmark");
|
|
58
|
+
lines.push(`Task: ${result.task || "(empty)"}`);
|
|
59
|
+
lines.push(`Rules: ${result.rulesParsed} parsed, ${result.actionableRules} actionable, ${result.filteredRules} filtered`);
|
|
60
|
+
lines.push(`Relevant rules: ${result.relevantRules}`);
|
|
61
|
+
lines.push(`Baseline middle-risk: ${result.baseline.relevantRulesInMiddle}/${result.relevantRules} relevant rules (${result.baseline.middleRiskPercent}%)`);
|
|
62
|
+
lines.push(`ContextOS scheduled: ${result.contextOS.highRules} high, ${result.contextOS.midRules} mid`);
|
|
63
|
+
lines.push(`Recency reminder: ${result.contextOS.repeatsHighRulesAtEnd ? "enabled" : "not needed"}`);
|
|
64
|
+
if (result.contextOS.topRules.length) {
|
|
65
|
+
lines.push("Top rules:");
|
|
66
|
+
for (const rule of result.contextOS.topRules) {
|
|
67
|
+
const reasons = rule.reasons?.length ? ` reasons:${rule.reasons.join(",")}` : "";
|
|
68
|
+
lines.push(`- ${Number(rule.score || 0).toFixed(2)} ${rule.content}${reasons}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return lines.join("\n");
|
|
72
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { buildGlobalHooksConfig } from "./global-hooks.js";
|
|
5
|
+
|
|
6
|
+
function readJsonFile(filePath, fallback) {
|
|
7
|
+
if (!fs.existsSync(filePath)) return fallback;
|
|
8
|
+
const raw = fs.readFileSync(filePath, "utf8").trim();
|
|
9
|
+
if (!raw) return fallback;
|
|
10
|
+
return JSON.parse(raw);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function claudeHome() {
|
|
14
|
+
return process.env.CLAUDE_HOME || path.join(process.env.HOME || process.cwd(), ".claude");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function installClaudeHooks({ claudeHome: home = claudeHome(), installRoot, injectPromptContext = true } = {}) {
|
|
18
|
+
const settingsPath = path.join(home, "settings.json");
|
|
19
|
+
const existing = readJsonFile(settingsPath, {});
|
|
20
|
+
const next = buildGlobalHooksConfig(existing, {
|
|
21
|
+
marketplaceRoot: installRoot,
|
|
22
|
+
injectPromptContext
|
|
23
|
+
});
|
|
24
|
+
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
|
25
|
+
fs.writeFileSync(settingsPath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
|
26
|
+
return settingsPath;
|
|
27
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
function readJsonFile(filePath, fallback) {
|
|
5
|
+
if (!fs.existsSync(filePath)) return fallback;
|
|
6
|
+
const raw = fs.readFileSync(filePath, "utf8").trim();
|
|
7
|
+
if (!raw) return fallback;
|
|
8
|
+
return JSON.parse(raw);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function claudeConfigPath() {
|
|
12
|
+
return process.env.CLAUDE_CONFIG_PATH || path.join(process.env.HOME || process.cwd(), ".claude.json");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function buildClaudeMcpConfig(existingConfig, { installRoot } = {}) {
|
|
16
|
+
const config = existingConfig && typeof existingConfig === "object" ? structuredClone(existingConfig) : {};
|
|
17
|
+
if (!config.mcpServers || typeof config.mcpServers !== "object") config.mcpServers = {};
|
|
18
|
+
config.mcpServers["ctx-mcp"] = {
|
|
19
|
+
type: "stdio",
|
|
20
|
+
command: "node",
|
|
21
|
+
args: [path.join(installRoot, "plugins", "ctx", "mcp", "server.js")],
|
|
22
|
+
env: {}
|
|
23
|
+
};
|
|
24
|
+
return config;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function installClaudeMcp({ configPath = claudeConfigPath(), installRoot } = {}) {
|
|
28
|
+
const existing = readJsonFile(configPath, {});
|
|
29
|
+
const next = buildClaudeMcpConfig(existing, { installRoot });
|
|
30
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
31
|
+
fs.writeFileSync(configPath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
|
32
|
+
return configPath;
|
|
33
|
+
}
|
|
@@ -57,6 +57,9 @@ export async function warmRuleEmbeddings({
|
|
|
57
57
|
sources = [],
|
|
58
58
|
allowRemote = true
|
|
59
59
|
} = {}) {
|
|
60
|
+
if (!allowRemote && !isModelCacheReady(dataDir)) {
|
|
61
|
+
return { count: 0, cachePath: path.join(dataDir, "embeddings.db"), status: "missing-model" };
|
|
62
|
+
}
|
|
60
63
|
const texts = [...new Set([
|
|
61
64
|
task,
|
|
62
65
|
...rules.map((rule) => rule.content || "")
|
|
@@ -131,6 +134,16 @@ export function modelCacheDir(dataDir = defaultDataRoot()) {
|
|
|
131
134
|
return path.join(dataDir, "models");
|
|
132
135
|
}
|
|
133
136
|
|
|
137
|
+
export function isModelCacheReady(dataDir = defaultDataRoot()) {
|
|
138
|
+
const modelDir = path.join(modelCacheDir(dataDir), ...DEFAULT_MODEL.split("/"));
|
|
139
|
+
return [
|
|
140
|
+
"config.json",
|
|
141
|
+
"tokenizer.json",
|
|
142
|
+
"tokenizer_config.json",
|
|
143
|
+
path.join("onnx", "model_quantized.onnx")
|
|
144
|
+
].every((relativePath) => fs.existsSync(path.join(modelDir, relativePath)));
|
|
145
|
+
}
|
|
146
|
+
|
|
134
147
|
async function getCachedEmbedding({ cache, embedder, text, sources }) {
|
|
135
148
|
const key = cacheKey(text, sources);
|
|
136
149
|
const existing = cache.get(key);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { enhanceRuleScoresWithEmbeddings, warmRuleEmbeddings } from "./embedding-scorer.js";
|
|
3
|
+
import { enhanceRuleScoresWithEmbeddings, isModelCacheReady, warmRuleEmbeddings } from "./embedding-scorer.js";
|
|
4
4
|
|
|
5
5
|
const SOURCE_EXTENSIONS = new Set([
|
|
6
6
|
".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".sql", ".md", ".json"
|
|
@@ -65,6 +65,7 @@ export async function warmFileEmbeddings({
|
|
|
65
65
|
maxFiles = Number(process.env.CONTEXTOS_FILE_EMBEDDING_MAX_FILES || DEFAULT_MAX_FILES)
|
|
66
66
|
} = {}) {
|
|
67
67
|
if (!dataDir) return { count: 0, cachePath: null };
|
|
68
|
+
if (!allowRemote && !isModelCacheReady(dataDir)) return { count: 0, cachePath: null, status: "missing-model" };
|
|
68
69
|
const files = listSourceFiles(cwd, { maxFiles });
|
|
69
70
|
const rules = files.map((filePath) => ({ content: fileSearchText(filePath) }));
|
|
70
71
|
return warmRuleEmbeddings({
|
|
@@ -15,6 +15,22 @@ export function writeJson(value) {
|
|
|
15
15
|
process.stdout.write(`${JSON.stringify(value)}\n`);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
export function resolveHookCwd(payload = {}) {
|
|
19
|
+
return payload.cwd
|
|
20
|
+
|| payload.working_directory
|
|
21
|
+
|| payload.workspacePath
|
|
22
|
+
|| payload.workspace_path
|
|
23
|
+
|| payload.workspaceRoot
|
|
24
|
+
|| payload.workspace_root
|
|
25
|
+
|| payload.projectDir
|
|
26
|
+
|| payload.project_dir
|
|
27
|
+
|| payload.workspacePaths?.[0]
|
|
28
|
+
|| payload.workspace_paths?.[0]
|
|
29
|
+
|| process.env.CLAUDE_PROJECT_DIR
|
|
30
|
+
|| process.env.PWD
|
|
31
|
+
|| process.cwd();
|
|
32
|
+
}
|
|
33
|
+
|
|
18
34
|
export function pluginDataDir(fileName = "", cwd = process.cwd()) {
|
|
19
35
|
let root;
|
|
20
36
|
try {
|
|
@@ -183,23 +183,31 @@ export function checkCompliance({ rules = [], addedLines = [], runtimeEvidence =
|
|
|
183
183
|
if (!keywords.length || !addedLines.length) {
|
|
184
184
|
results.push({
|
|
185
185
|
rule,
|
|
186
|
-
status: "unknown",
|
|
186
|
+
status: isRuntimeOnly || !addedLines.length || !keywords.length ? "unmeasurable" : "unknown",
|
|
187
187
|
kind: isRuntimeOnly ? "runtime" : kind,
|
|
188
188
|
keywords,
|
|
189
189
|
evidence: isRuntimeOnly
|
|
190
|
-
? "requires runtime/tool-call telemetry; no
|
|
190
|
+
? "requires runtime/tool-call telemetry; no runtime telemetry source observed"
|
|
191
191
|
: (!addedLines.length ? "no added lines in git diff" : "no concrete compliance keywords found")
|
|
192
192
|
});
|
|
193
193
|
continue;
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
if (isRuntimeOnly) {
|
|
197
|
+
const hasRuntimeSource = Boolean(
|
|
198
|
+
runtimeEvidence.sources?.length ||
|
|
199
|
+
runtimeEvidence.signals?.length ||
|
|
200
|
+
runtimeEvidence.toolSignals?.length ||
|
|
201
|
+
runtimeEvidence.commandSignals?.length
|
|
202
|
+
);
|
|
197
203
|
results.push({
|
|
198
204
|
rule,
|
|
199
|
-
status: "unknown",
|
|
205
|
+
status: hasRuntimeSource ? "unknown" : "unmeasurable",
|
|
200
206
|
kind: "runtime",
|
|
201
207
|
keywords,
|
|
202
|
-
evidence:
|
|
208
|
+
evidence: hasRuntimeSource
|
|
209
|
+
? "requires runtime/tool-call telemetry; no matching runtime signal observed"
|
|
210
|
+
: "requires runtime/tool-call telemetry; no runtime telemetry source observed"
|
|
203
211
|
});
|
|
204
212
|
continue;
|
|
205
213
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export function copyDir(src, dest) {
|
|
5
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
6
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
7
|
+
const srcPath = path.join(src, entry.name);
|
|
8
|
+
const destPath = path.join(dest, entry.name);
|
|
9
|
+
if (entry.isDirectory()) {
|
|
10
|
+
copyDir(srcPath, destPath);
|
|
11
|
+
} else if (entry.isFile()) {
|
|
12
|
+
fs.copyFileSync(srcPath, destPath);
|
|
13
|
+
fs.chmodSync(destPath, fs.statSync(srcPath).mode);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function copyPath(src, dest) {
|
|
19
|
+
const stat = fs.statSync(src);
|
|
20
|
+
if (stat.isDirectory()) {
|
|
21
|
+
copyDir(src, dest);
|
|
22
|
+
} else {
|
|
23
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
24
|
+
fs.copyFileSync(src, dest);
|
|
25
|
+
fs.chmodSync(dest, stat.mode);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function copyPackageRoot({ rootDir, targetRoot }) {
|
|
30
|
+
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
31
|
+
for (const entry of [".agents", "bin", "plugins", "package.json", "package-lock.json", "README.md", "LICENSE", "node_modules"]) {
|
|
32
|
+
const src = path.join(rootDir, entry);
|
|
33
|
+
if (fs.existsSync(src)) copyPath(src, path.join(targetRoot, entry));
|
|
34
|
+
}
|
|
35
|
+
return targetRoot;
|
|
36
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { scheduleContext } from "./scheduler.js";
|
|
2
2
|
import { appendJsonLine, writeJsonFile } from "./fs-utils.js";
|
|
3
3
|
import { callCtxScoreContext } from "./ctx-mcp-client.js";
|
|
4
|
+
import { resolveHookCwd } from "./hook-io.js";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
|
|
6
7
|
export async function handlePromptPayload(
|
|
@@ -16,7 +17,7 @@ export async function handlePromptPayload(
|
|
|
16
17
|
} = {}
|
|
17
18
|
) {
|
|
18
19
|
const prompt = payload.prompt || payload.message || payload.user_prompt || "";
|
|
19
|
-
const cwd = payload
|
|
20
|
+
const cwd = resolveHookCwd(payload);
|
|
20
21
|
const openFiles = payload.openFiles || payload.open_files || payload.files || [];
|
|
21
22
|
const dataDir = dataPath ? path.dirname(dataPath) : undefined;
|
|
22
23
|
|
|
@@ -5,6 +5,7 @@ export function buildReport({ cwd, prompt, relevantFiles, scheduled, gitSnapshot
|
|
|
5
5
|
const followed = actionableCompliance.filter((item) => item.status === "followed");
|
|
6
6
|
const ignored = actionableCompliance.filter((item) => item.status === "ignored");
|
|
7
7
|
const unknown = actionableCompliance.filter((item) => item.status === "unknown");
|
|
8
|
+
const unmeasurable = actionableCompliance.filter((item) => item.status === "unmeasurable");
|
|
8
9
|
const measured = followed.length + ignored.length;
|
|
9
10
|
const efficiencyScore = measured ? Math.round((followed.length / measured) * 100) : null;
|
|
10
11
|
|
|
@@ -20,8 +21,10 @@ export function buildReport({ cwd, prompt, relevantFiles, scheduled, gitSnapshot
|
|
|
20
21
|
followed,
|
|
21
22
|
ignored,
|
|
22
23
|
unknown,
|
|
24
|
+
unmeasurable,
|
|
23
25
|
measuredRuleCount: measured,
|
|
24
26
|
unknownRuleCount: unknown.length,
|
|
27
|
+
unmeasurableRuleCount: unmeasurable.length,
|
|
25
28
|
efficiencyScore
|
|
26
29
|
};
|
|
27
30
|
}
|
|
@@ -32,7 +35,7 @@ export function formatReport(report) {
|
|
|
32
35
|
lines.push("ContextOS report");
|
|
33
36
|
lines.push(`Efficiency: ${report.efficiencyScore == null ? "unknown" : `${report.efficiencyScore}%`}`);
|
|
34
37
|
lines.push(`Injected rules: ${report.injectedRuleCount || 0}`);
|
|
35
|
-
lines.push(`Rule outcomes: ${report.followed?.length || 0} followed, ${report.ignored?.length || 0} ignored, ${report.unknown?.length || 0} unknown`);
|
|
38
|
+
lines.push(`Rule outcomes: ${report.followed?.length || 0} followed, ${report.ignored?.length || 0} ignored, ${report.unknown?.length || 0} unknown, ${report.unmeasurable?.length || 0} unmeasurable`);
|
|
36
39
|
lines.push(`Measured rules: ${report.measuredRuleCount ?? ((report.followed?.length || 0) + (report.ignored?.length || 0))}`);
|
|
37
40
|
lines.push(`Changed files: ${report.changedFiles?.length ? report.changedFiles.join(", ") : "none detected"}`);
|
|
38
41
|
|
|
@@ -48,6 +51,7 @@ export function formatReport(report) {
|
|
|
48
51
|
appendBucket(lines, "Followed", report.followed);
|
|
49
52
|
appendBucket(lines, "Ignored", report.ignored);
|
|
50
53
|
appendBucket(lines, "Unknown", report.unknown);
|
|
54
|
+
appendBucket(lines, "Unmeasurable", report.unmeasurable);
|
|
51
55
|
|
|
52
56
|
if (report.ignored?.length) {
|
|
53
57
|
lines.push(`Suggestion: fix ignored rule evidence first: ${truncate(report.ignored[0].rule?.content || "", 70)}`);
|
|
@@ -71,7 +75,8 @@ export function formatEvidence(report) {
|
|
|
71
75
|
const items = [
|
|
72
76
|
...(report.followed || []).map((item) => ({ ...item, status: "followed" })),
|
|
73
77
|
...(report.ignored || []).map((item) => ({ ...item, status: "ignored" })),
|
|
74
|
-
...(report.unknown || []).map((item) => ({ ...item, status: "unknown" }))
|
|
78
|
+
...(report.unknown || []).map((item) => ({ ...item, status: "unknown" })),
|
|
79
|
+
...(report.unmeasurable || []).map((item) => ({ ...item, status: "unmeasurable" }))
|
|
75
80
|
];
|
|
76
81
|
|
|
77
82
|
if (!items.length) {
|
|
@@ -129,15 +134,18 @@ function sanitizeReport(report = {}) {
|
|
|
129
134
|
const followed = (report.followed || []).filter((item) => !isSystemUserRule(item.rule));
|
|
130
135
|
const ignored = (report.ignored || []).filter((item) => !isSystemUserRule(item.rule));
|
|
131
136
|
const unknown = (report.unknown || []).filter((item) => !isSystemUserRule(item.rule));
|
|
137
|
+
const unmeasurable = (report.unmeasurable || []).filter((item) => !isSystemUserRule(item.rule));
|
|
132
138
|
const measured = followed.length + ignored.length;
|
|
133
139
|
return {
|
|
134
140
|
...report,
|
|
135
|
-
injectedRuleCount: followed.length + ignored.length + unknown.length,
|
|
141
|
+
injectedRuleCount: followed.length + ignored.length + unknown.length + unmeasurable.length,
|
|
136
142
|
followed,
|
|
137
143
|
ignored,
|
|
138
144
|
unknown,
|
|
145
|
+
unmeasurable,
|
|
139
146
|
measuredRuleCount: measured,
|
|
140
147
|
unknownRuleCount: unknown.length,
|
|
148
|
+
unmeasurableRuleCount: unmeasurable.length,
|
|
141
149
|
efficiencyScore: measured ? Math.round((followed.length / measured) * 100) : null
|
|
142
150
|
};
|
|
143
151
|
}
|