agentcache 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,45 @@
1
+ // src/policy/engine.ts
2
+ var HARDCODED_BLOCKS = [
3
+ { pattern: /git\s+push\s+--force\s+(origin\s+)?(main|master)/i, reason: "Force-push to main/master is blocked by Loop policy" },
4
+ { pattern: /rm\s+-rf\s+[\/~]/i, reason: "Destructive rm -rf on root or home is blocked by Loop policy" },
5
+ { pattern: />\s*(.*\.(env|pem|key))/i, reason: "Writing to sensitive files (.env, .pem, .key) is blocked by Loop policy" }
6
+ ];
7
+ function evaluatePolicy(input, enforcedRules) {
8
+ const command = extractCommand(input.tool_name, input.tool_input);
9
+ if (!command) return {};
10
+ for (const block of HARDCODED_BLOCKS) {
11
+ if (block.pattern.test(command)) {
12
+ return { decision: "block", reason: block.reason };
13
+ }
14
+ }
15
+ for (const item of enforcedRules) {
16
+ if (matchesRule(item, input.tool_name, command)) {
17
+ return { decision: "block", reason: `Blocked by enforced rule: ${item.content}` };
18
+ }
19
+ }
20
+ return {};
21
+ }
22
+ function extractCommand(toolName, toolInput) {
23
+ if (toolName === "Bash" && typeof toolInput.command === "string") {
24
+ return toolInput.command;
25
+ }
26
+ if (toolName === "Write" && typeof toolInput.file_path === "string") {
27
+ return `> ${toolInput.file_path}`;
28
+ }
29
+ if (toolName === "Edit" && typeof toolInput.file_path === "string") {
30
+ return `edit ${toolInput.file_path}`;
31
+ }
32
+ return null;
33
+ }
34
+ function matchesRule(item, _toolName, command) {
35
+ const content = item.content.toLowerCase();
36
+ const cmd = command.toLowerCase();
37
+ const keywords = content.replace(/never|always|don't|do not|avoid|must not/gi, "").split(/\s+/).filter((w) => w.length > 3);
38
+ if (keywords.length === 0) return false;
39
+ const matchCount = keywords.filter((k) => cmd.includes(k)).length;
40
+ return matchCount >= Math.ceil(keywords.length * 0.5);
41
+ }
42
+
43
+ export {
44
+ evaluatePolicy
45
+ };
@@ -0,0 +1,173 @@
1
+ import {
2
+ getClaudeTranscriptsDir,
3
+ getContinueSessionsDir
4
+ } from "./chunk-VPEEZXLK.js";
5
+ import {
6
+ __export
7
+ } from "./chunk-MLKGABMK.js";
8
+
9
+ // src/utils/transcript.ts
10
+ import { readdirSync, statSync, existsSync } from "fs";
11
+ import { join } from "path";
12
+
13
+ // src/utils/transcript-parsers/claude-jsonl.ts
14
+ var claude_jsonl_exports = {};
15
+ __export(claude_jsonl_exports, {
16
+ canParse: () => canParse,
17
+ parse: () => parse
18
+ });
19
+ import { readFileSync } from "fs";
20
+ function canParse(path) {
21
+ return path.endsWith(".jsonl");
22
+ }
23
+ function parse(path) {
24
+ const content = readFileSync(path, "utf-8");
25
+ const events = [];
26
+ for (const line of content.split("\n")) {
27
+ if (!line.trim()) continue;
28
+ try {
29
+ const obj = JSON.parse(line);
30
+ if (obj.type === "user" && obj.message?.content) {
31
+ events.push({
32
+ type: "message",
33
+ role: "user",
34
+ content: typeof obj.message.content === "string" ? obj.message.content : JSON.stringify(obj.message.content)
35
+ });
36
+ } else if (obj.type === "assistant" && obj.message?.content) {
37
+ const blocks = Array.isArray(obj.message.content) ? obj.message.content : [{ type: "text", text: obj.message.content }];
38
+ for (const block of blocks) {
39
+ if (block.type === "text" && block.text) {
40
+ events.push({ type: "message", role: "assistant", content: block.text });
41
+ } else if (block.type === "tool_use") {
42
+ events.push({
43
+ type: "tool_use",
44
+ tool_name: block.name,
45
+ tool_input: block.input
46
+ });
47
+ }
48
+ }
49
+ }
50
+ } catch {
51
+ continue;
52
+ }
53
+ }
54
+ return events;
55
+ }
56
+
57
+ // src/utils/transcript-parsers/continue-json.ts
58
+ var continue_json_exports = {};
59
+ __export(continue_json_exports, {
60
+ canParse: () => canParse2,
61
+ parse: () => parse2
62
+ });
63
+ import { readFileSync as readFileSync2 } from "fs";
64
+ function canParse2(path) {
65
+ if (!path.endsWith(".json")) return false;
66
+ try {
67
+ const content = readFileSync2(path, "utf-8");
68
+ const parsed = JSON.parse(content);
69
+ return parsed.history && Array.isArray(parsed.history);
70
+ } catch {
71
+ return false;
72
+ }
73
+ }
74
+ function parse2(path) {
75
+ const content = readFileSync2(path, "utf-8");
76
+ const session = JSON.parse(content);
77
+ const events = [];
78
+ if (!session.history || !Array.isArray(session.history)) return events;
79
+ for (const entry of session.history) {
80
+ const msg = entry.message;
81
+ if (!msg) continue;
82
+ const role = msg.role === "user" ? "user" : msg.role === "assistant" ? "assistant" : null;
83
+ if (!role) continue;
84
+ let text = "";
85
+ if (typeof msg.content === "string") {
86
+ text = msg.content;
87
+ } else if (Array.isArray(msg.content)) {
88
+ text = msg.content.filter((b) => b.type === "text" && b.text).map((b) => b.text).join("\n");
89
+ }
90
+ if (text) {
91
+ events.push({ type: "message", role, content: text });
92
+ }
93
+ }
94
+ return events;
95
+ }
96
+
97
+ // src/utils/transcript-parsers/index.ts
98
+ var parsers = [claude_jsonl_exports, continue_json_exports];
99
+ function parseTranscriptAuto(path) {
100
+ for (const parser of parsers) {
101
+ if (parser.canParse(path)) return parser.parse(path);
102
+ }
103
+ return [];
104
+ }
105
+
106
+ // src/utils/transcript.ts
107
+ function parseTranscript(path) {
108
+ return parseTranscriptAuto(path);
109
+ }
110
+ function findLatestTranscript() {
111
+ const baseDir = getClaudeTranscriptsDir();
112
+ if (!existsSync(baseDir)) return null;
113
+ let latest = null;
114
+ try {
115
+ const dirs = readdirSync(baseDir).map((d) => join(baseDir, d)).filter((d) => statSync(d).isDirectory());
116
+ for (const dir of dirs) {
117
+ try {
118
+ const files = readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
119
+ for (const file of files) {
120
+ const fullPath = join(dir, file);
121
+ const mtime = statSync(fullPath).mtimeMs;
122
+ if (!latest || mtime > latest.mtime) {
123
+ latest = { path: fullPath, mtime };
124
+ }
125
+ }
126
+ } catch {
127
+ continue;
128
+ }
129
+ }
130
+ } catch {
131
+ return null;
132
+ }
133
+ return latest?.path ?? null;
134
+ }
135
+ function findAllClaudeTranscripts() {
136
+ const baseDir = getClaudeTranscriptsDir();
137
+ if (!existsSync(baseDir)) return [];
138
+ const transcripts = [];
139
+ try {
140
+ const dirs = readdirSync(baseDir).map((d) => join(baseDir, d)).filter((d) => statSync(d).isDirectory());
141
+ for (const dir of dirs) {
142
+ try {
143
+ const files = readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
144
+ for (const file of files) {
145
+ const fullPath = join(dir, file);
146
+ if (statSync(fullPath).size > 100) {
147
+ transcripts.push(fullPath);
148
+ }
149
+ }
150
+ } catch {
151
+ continue;
152
+ }
153
+ }
154
+ } catch {
155
+ }
156
+ return transcripts;
157
+ }
158
+ function findAllContinueTranscripts() {
159
+ const dir = getContinueSessionsDir();
160
+ if (!existsSync(dir)) return [];
161
+ try {
162
+ return readdirSync(dir).filter((f) => f.endsWith(".json") && f !== "sessions.json").map((f) => join(dir, f)).filter((f) => statSync(f).size > 100);
163
+ } catch {
164
+ return [];
165
+ }
166
+ }
167
+
168
+ export {
169
+ parseTranscript,
170
+ findLatestTranscript,
171
+ findAllClaudeTranscripts,
172
+ findAllContinueTranscripts
173
+ };
@@ -0,0 +1,129 @@
1
+ // src/utils/ide-detector.ts
2
+ import { existsSync } from "fs";
3
+ import { join } from "path";
4
+ import { homedir } from "os";
5
+ function detectInstalledIdes() {
6
+ const home = homedir();
7
+ return [
8
+ {
9
+ name: "Claude Code",
10
+ detected: existsSync(join(home, ".claude")),
11
+ mcpConfigPath: join(home, ".claude", "settings.json"),
12
+ mcpConfigFormat: "claude-settings"
13
+ },
14
+ {
15
+ name: "Cursor",
16
+ detected: existsSync(join(home, ".cursor")),
17
+ mcpConfigPath: join(home, ".cursor", "mcp.json"),
18
+ mcpConfigFormat: "mcp-json"
19
+ },
20
+ {
21
+ name: "Roo Code",
22
+ detected: existsSync(join(home, ".roo")),
23
+ mcpConfigPath: join(home, ".roo", "mcp.json"),
24
+ mcpConfigFormat: "mcp-json"
25
+ },
26
+ {
27
+ name: "Windsurf",
28
+ detected: existsSync(join(home, ".windsurf")),
29
+ mcpConfigPath: join(home, ".windsurf", "mcp.json"),
30
+ mcpConfigFormat: "mcp-json"
31
+ },
32
+ {
33
+ name: "Continue",
34
+ detected: existsSync(join(home, ".continue")),
35
+ mcpConfigPath: join(home, ".continue", "mcp.json"),
36
+ mcpConfigFormat: "mcp-json"
37
+ },
38
+ {
39
+ name: "Codex",
40
+ detected: existsSync(join(home, ".codex")),
41
+ mcpConfigPath: join(home, ".codex", "mcp.json"),
42
+ mcpConfigFormat: "mcp-json"
43
+ }
44
+ ];
45
+ }
46
+
47
+ // src/utils/ide-registrar.ts
48
+ import { existsSync as existsSync2, mkdirSync, readFileSync, writeFileSync } from "fs";
49
+ import { join as join2, dirname } from "path";
50
+ import { homedir as homedir2 } from "os";
51
+ var MCP_ENTRY = {
52
+ command: "agentcache",
53
+ args: ["serve"]
54
+ };
55
+ function registerMcpServer(ide) {
56
+ if (!ide.detected) return false;
57
+ if (ide.mcpConfigFormat === "claude-settings") {
58
+ let settings = {};
59
+ if (existsSync2(ide.mcpConfigPath)) {
60
+ try {
61
+ settings = JSON.parse(readFileSync(ide.mcpConfigPath, "utf-8"));
62
+ } catch {
63
+ settings = {};
64
+ }
65
+ }
66
+ if (!settings.mcpServers) settings.mcpServers = {};
67
+ if (settings.mcpServers.agentcache) return false;
68
+ settings.mcpServers.agentcache = MCP_ENTRY;
69
+ mkdirSync(dirname(ide.mcpConfigPath), { recursive: true });
70
+ writeFileSync(ide.mcpConfigPath, JSON.stringify(settings, null, 2));
71
+ return true;
72
+ }
73
+ if (ide.mcpConfigFormat === "mcp-json") {
74
+ let config = {};
75
+ if (existsSync2(ide.mcpConfigPath)) {
76
+ try {
77
+ config = JSON.parse(readFileSync(ide.mcpConfigPath, "utf-8"));
78
+ } catch {
79
+ config = {};
80
+ }
81
+ }
82
+ if (!config.mcpServers) config.mcpServers = {};
83
+ if (config.mcpServers.agentcache) return false;
84
+ config.mcpServers.agentcache = MCP_ENTRY;
85
+ mkdirSync(dirname(ide.mcpConfigPath), { recursive: true });
86
+ writeFileSync(ide.mcpConfigPath, JSON.stringify(config, null, 2));
87
+ return true;
88
+ }
89
+ return false;
90
+ }
91
+ function registerClaudeHooks() {
92
+ const settingsPath = join2(homedir2(), ".claude", "settings.json");
93
+ if (!existsSync2(join2(homedir2(), ".claude"))) return false;
94
+ let settings = {};
95
+ if (existsSync2(settingsPath)) {
96
+ try {
97
+ settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
98
+ } catch {
99
+ settings = {};
100
+ }
101
+ }
102
+ if (!settings.hooks) settings.hooks = {};
103
+ const hooks = settings.hooks;
104
+ const loopHooks = {
105
+ Stop: [{ matcher: "", hooks: [{ type: "command", command: "agentcache compile-session" }] }],
106
+ SessionStart: [{ matcher: "", hooks: [{ type: "command", command: "agentcache discover" }] }],
107
+ PreToolUse: [{ matcher: "", hooks: [{ type: "command", command: "agentcache enforce" }] }]
108
+ };
109
+ let registered = false;
110
+ for (const [event, hookConfig] of Object.entries(loopHooks)) {
111
+ if (!hooks[event]) hooks[event] = [];
112
+ const existing = hooks[event];
113
+ const hasLoop = existing.some((h) => h.hooks?.some((hh) => hh.command?.includes("agentcache")));
114
+ if (!hasLoop) {
115
+ hooks[event].push(...hookConfig);
116
+ registered = true;
117
+ }
118
+ }
119
+ if (registered) {
120
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
121
+ }
122
+ return registered;
123
+ }
124
+
125
+ export {
126
+ detectInstalledIdes,
127
+ registerMcpServer,
128
+ registerClaudeHooks
129
+ };
@@ -0,0 +1,68 @@
1
+ // src/utils/paths.ts
2
+ import { existsSync } from "fs";
3
+ import { join } from "path";
4
+ import { homedir } from "os";
5
+ import { createHash } from "crypto";
6
+
7
+ // src/utils/git.ts
8
+ import { execSync } from "child_process";
9
+ function run(cmd, cwd) {
10
+ try {
11
+ return execSync(cmd, { cwd, encoding: "utf-8", timeout: 5e3 }).trim();
12
+ } catch {
13
+ return "";
14
+ }
15
+ }
16
+ function getGitContext(projectRoot) {
17
+ const branch = run("git rev-parse --abbrev-ref HEAD", projectRoot);
18
+ const commit = run("git rev-parse --short HEAD", projectRoot);
19
+ const recentCommits = run("git log --oneline -10", projectRoot).split("\n").filter(Boolean);
20
+ const modifiedFiles = run("git diff --name-only HEAD~5 HEAD", projectRoot).split("\n").filter(Boolean);
21
+ return { branch, commit, recentCommits, modifiedFiles };
22
+ }
23
+ function getGitRoot(cwd) {
24
+ const root = run("git rev-parse --show-toplevel", cwd);
25
+ return root || null;
26
+ }
27
+
28
+ // src/utils/paths.ts
29
+ function getGlobalLoopDir() {
30
+ return join(homedir(), ".loop");
31
+ }
32
+ function getDbPath() {
33
+ return join(getGlobalLoopDir(), "loop.db");
34
+ }
35
+ function isLoopInitialized() {
36
+ return existsSync(getDbPath());
37
+ }
38
+ function findProjectRoot(cwd) {
39
+ const dir = cwd || process.cwd();
40
+ const gitRoot = getGitRoot(dir);
41
+ return gitRoot || dir;
42
+ }
43
+ function getProjectId(projectRoot) {
44
+ const name = projectRoot.split("/").pop() || "unknown";
45
+ const hash = createHash("sha256").update(projectRoot).digest("hex").slice(0, 6);
46
+ return `${name}-${hash}`;
47
+ }
48
+ function getProjectDisplayName(projectRoot) {
49
+ return projectRoot.split("/").pop() || "unknown";
50
+ }
51
+ function getClaudeTranscriptsDir() {
52
+ return join(homedir(), ".claude", "projects");
53
+ }
54
+ function getContinueSessionsDir() {
55
+ return join(homedir(), ".continue", "sessions");
56
+ }
57
+
58
+ export {
59
+ getGitContext,
60
+ getGlobalLoopDir,
61
+ getDbPath,
62
+ isLoopInitialized,
63
+ findProjectRoot,
64
+ getProjectId,
65
+ getProjectDisplayName,
66
+ getClaudeTranscriptsDir,
67
+ getContinueSessionsDir
68
+ };
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.js ADDED
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { Command } from "commander";
5
+ var program = new Command();
6
+ program.name("agentcache").description("Engineering Knowledge Compiler \u2014 universal, zero-config").version("0.3.0");
7
+ program.command("setup").description("Detect IDEs and register Loop (runs automatically on install)").action(async () => {
8
+ const { runSetup } = await import("./setup-QB36C2TH.js");
9
+ await runSetup();
10
+ });
11
+ program.command("serve").description("Start Loop MCP server (spawned by IDEs automatically)").action(async () => {
12
+ const { startMcpServer } = await import("./mcp.js");
13
+ await startMcpServer();
14
+ });
15
+ program.command("compile-session").description("Stop hook: queue transcript for compilation").action(async () => {
16
+ const { handleStop } = await import("./stop-DRL3LXFQ.js");
17
+ let payload;
18
+ try {
19
+ let data = "";
20
+ for await (const chunk of process.stdin) {
21
+ data += chunk;
22
+ }
23
+ if (data.trim()) {
24
+ payload = JSON.parse(data);
25
+ }
26
+ } catch {
27
+ }
28
+ await handleStop(payload);
29
+ });
30
+ program.command("discover").description("SessionStart hook: discover uncompiled transcripts").action(async () => {
31
+ const { handleSessionStart } = await import("./session-start-ILEPFZZC.js");
32
+ await handleSessionStart();
33
+ });
34
+ program.command("enforce").description("PreToolUse hook: policy enforcement").action(async () => {
35
+ const { handlePreToolUse } = await import("./pre-tool-use-2P5P6JWE.js");
36
+ let data = "";
37
+ for await (const chunk of process.stdin) {
38
+ data += chunk;
39
+ }
40
+ try {
41
+ const input = JSON.parse(data);
42
+ const result = handlePreToolUse(input);
43
+ process.stdout.write(JSON.stringify(result));
44
+ } catch {
45
+ process.stdout.write("{}");
46
+ }
47
+ });
48
+ program.command("status").description("Show Loop knowledge stats").action(async () => {
49
+ const { getDbPath, isLoopInitialized, findProjectRoot, getProjectId, getProjectDisplayName } = await import("./paths-TWJ7GAJY.js");
50
+ if (!isLoopInitialized()) {
51
+ console.log("Loop not initialized. Run: agentcache setup");
52
+ return;
53
+ }
54
+ const { SqliteKnowledgeRepository } = await import("./sqlite-5V565IV3.js");
55
+ const repo = new SqliteKnowledgeRepository(getDbPath());
56
+ const projectRoot = findProjectRoot();
57
+ const project = getProjectId(projectRoot);
58
+ const displayName = getProjectDisplayName(projectRoot);
59
+ const items = repo.getKnowledgeForContext(project);
60
+ const rules = items.filter((i) => i.type === "rule");
61
+ const lessons = items.filter((i) => i.type === "lesson");
62
+ const decisions = items.filter((i) => i.type === "decision");
63
+ const context = items.filter((i) => i.type === "context");
64
+ const globalItems = items.filter((i) => i.scope === "global");
65
+ const projectItems = items.filter((i) => i.scope === "project");
66
+ const pending = repo.getPendingCount();
67
+ repo.close();
68
+ console.log(`Loop \u2014 ${displayName} (${project})`);
69
+ console.log(` ${items.length} items (${globalItems.length} global, ${projectItems.length} project)`);
70
+ console.log(` ${rules.length} rules | ${lessons.length} lessons | ${decisions.length} decisions | ${context.length} context`);
71
+ if (pending > 0) console.log(` ${pending} sessions pending compilation`);
72
+ });
73
+ program.parse();
package/dist/mcp.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ declare function startMcpServer(): Promise<void>;
2
+
3
+ export { startMcpServer };