@minhpnq1807/contextos 0.2.0 → 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 +29 -0
- package/README.md +117 -18
- package/bin/ctx.js +80 -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 +4 -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/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/package-install.js +36 -0
- package/plugins/ctx/lib/prompt-hook.js +2 -1
- package/plugins/ctx/lib/ruler-sync.js +478 -0
- package/plugins/ctx/lib/stop-hook.js +2 -1
|
@@ -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,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 {
|
|
@@ -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
|
|