agentel 0.2.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.
package/src/mcp.js ADDED
@@ -0,0 +1,148 @@
1
+ "use strict";
2
+
3
+ const { searchPastSessions } = require("./search");
4
+ const { version } = require("./version");
5
+
6
+ async function runMcpServer(input = process.stdin, output = process.stdout, env = process.env) {
7
+ const transport = new McpStdioTransport(input, output);
8
+ transport.onMessage(async (message) => {
9
+ if (!message || !message.method) return;
10
+ try {
11
+ const result = await handleRequest(message, env);
12
+ if ("id" in message && result !== undefined) transport.send({ jsonrpc: "2.0", id: message.id, result });
13
+ } catch (error) {
14
+ if ("id" in message) {
15
+ transport.send({
16
+ jsonrpc: "2.0",
17
+ id: message.id,
18
+ error: { code: -32000, message: error.message || String(error) }
19
+ });
20
+ }
21
+ }
22
+ });
23
+ await transport.start();
24
+ }
25
+
26
+ async function handleRequest(message, env = process.env) {
27
+ if (message.method === "initialize") {
28
+ return {
29
+ protocolVersion: message.params?.protocolVersion || "2024-11-05",
30
+ capabilities: { tools: {} },
31
+ serverInfo: { name: "agentlog-recall", version }
32
+ };
33
+ }
34
+ if (message.method === "tools/list") {
35
+ return {
36
+ tools: [
37
+ {
38
+ name: "search_past_sessions",
39
+ description: "Search redacted, repo-keyed conversation markdown archives for relevant excerpts.",
40
+ inputSchema: {
41
+ type: "object",
42
+ properties: {
43
+ query: { type: "string" },
44
+ repo: { type: "string" },
45
+ limit: { type: "integer", default: 10, minimum: 1, maximum: 50 },
46
+ include_web_chats: { type: "boolean", default: false }
47
+ },
48
+ required: ["query"]
49
+ }
50
+ }
51
+ ]
52
+ };
53
+ }
54
+ if (message.method === "tools/call") {
55
+ const name = message.params?.name;
56
+ if (name !== "search_past_sessions") throw new Error(`unknown tool: ${name}`);
57
+ const args = message.params?.arguments || {};
58
+ const results = searchPastSessions(
59
+ args.query || "",
60
+ {
61
+ repo: args.repo,
62
+ limit: args.limit || 10,
63
+ includeWebChats: args.include_web_chats === true
64
+ },
65
+ env
66
+ );
67
+ return {
68
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
69
+ structuredContent: { results }
70
+ };
71
+ }
72
+ if (message.method === "ping") return {};
73
+ if (message.method.startsWith("notifications/")) return undefined;
74
+ throw new Error(`unsupported MCP method: ${message.method}`);
75
+ }
76
+
77
+ class McpStdioTransport {
78
+ constructor(input, output) {
79
+ this.input = input;
80
+ this.output = output;
81
+ this.buffer = "";
82
+ this.handlers = [];
83
+ }
84
+
85
+ onMessage(handler) {
86
+ this.handlers.push(handler);
87
+ }
88
+
89
+ async start() {
90
+ this.input.setEncoding("utf8");
91
+ this.input.on("data", (chunk) => {
92
+ this.buffer += chunk;
93
+ this.drain();
94
+ });
95
+ return new Promise((resolve) => {
96
+ this.input.on("end", resolve);
97
+ });
98
+ }
99
+
100
+ drain() {
101
+ for (;;) {
102
+ const headerEnd = this.buffer.indexOf("\r\n\r\n");
103
+ if (this.buffer.startsWith("Content-Length:") && headerEnd >= 0) {
104
+ const header = this.buffer.slice(0, headerEnd);
105
+ const match = header.match(/Content-Length:\s*(\d+)/i);
106
+ if (!match) {
107
+ this.buffer = "";
108
+ return;
109
+ }
110
+ const length = Number(match[1]);
111
+ const bodyStart = headerEnd + 4;
112
+ if (this.buffer.length < bodyStart + length) return;
113
+ const body = this.buffer.slice(bodyStart, bodyStart + length);
114
+ this.buffer = this.buffer.slice(bodyStart + length);
115
+ this.emit(body);
116
+ continue;
117
+ }
118
+
119
+ const newline = this.buffer.indexOf("\n");
120
+ if (newline < 0) return;
121
+ const line = this.buffer.slice(0, newline).trim();
122
+ this.buffer = this.buffer.slice(newline + 1);
123
+ if (line) this.emit(line);
124
+ }
125
+ }
126
+
127
+ emit(body) {
128
+ let message;
129
+ try {
130
+ message = JSON.parse(body);
131
+ } catch (error) {
132
+ console.error(`agentlog-recall: invalid JSON-RPC message: ${error.message}`);
133
+ return;
134
+ }
135
+ for (const handler of this.handlers) handler(message);
136
+ }
137
+
138
+ send(message) {
139
+ const body = JSON.stringify(message);
140
+ this.output.write(`Content-Length: ${Buffer.byteLength(body, "utf8")}\r\n\r\n${body}`);
141
+ }
142
+ }
143
+
144
+ module.exports = {
145
+ McpStdioTransport,
146
+ handleRequest,
147
+ runMcpServer
148
+ };
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+
3
+ const SOURCE_TYPE_ALIASES = {
4
+ "cursor-sqlite-history": "cursor-workspace-sqlite",
5
+ "antigravity-brain": "antigravity-history"
6
+ };
7
+
8
+ const PARSER_VERSIONS = {
9
+ "codex-cli-history": "1.0.0",
10
+ "codex-desktop-history": "1.0.0",
11
+ "cli-history": "1.0.0",
12
+ "claude-sdk-history": "1.0.0",
13
+ "claude-code-desktop-metadata": "1.0.0",
14
+ "claude-workspace-desktop": "1.0.0",
15
+ "cursor-workspace-sqlite": "1.0.0",
16
+ "cursor-global-sqlite": "1.0.0",
17
+ "cursor-raw-sqlite-salvage": "1.0.0",
18
+ "cursor-agent-transcripts": "1.0.0",
19
+ "devin-cli-history": "1.0.0",
20
+ "gemini-cli-history": "1.0.0",
21
+ "cline-task-history": "1.0.0",
22
+ "opencode-history": "1.0.0",
23
+ "aider-chat-history": "1.0.0",
24
+ "antigravity-history": "1.0.0",
25
+ "web-chat-export": "1.0.0",
26
+ "chatgpt-export": "1.0.0",
27
+ "claude-web-export": "1.0.0",
28
+ "claude-web-memory": "1.0.0",
29
+ import: "1.0.0"
30
+ };
31
+
32
+ function canonicalSourceType(sourceType) {
33
+ const key = String(sourceType || "import").trim() || "import";
34
+ return SOURCE_TYPE_ALIASES[key] || key;
35
+ }
36
+
37
+ function parserVersionForSource(sourceType) {
38
+ const key = canonicalSourceType(sourceType);
39
+ return PARSER_VERSIONS[key] || null;
40
+ }
41
+
42
+ function fingerprintPrefix(sourceType) {
43
+ const key = canonicalSourceType(sourceType);
44
+ const version = parserVersionForSource(key);
45
+ if (!version) throw new Error(`unknown parser source type: ${sourceType || "(empty)"}`);
46
+ return `${key}-v${version}`;
47
+ }
48
+
49
+ function assertKnownSourceType(sourceType) {
50
+ const key = canonicalSourceType(sourceType);
51
+ if (!parserVersionForSource(key)) throw new Error(`unknown parser source type: ${sourceType || "(empty)"}`);
52
+ return key;
53
+ }
54
+
55
+ module.exports = {
56
+ PARSER_VERSIONS,
57
+ SOURCE_TYPE_ALIASES,
58
+ assertKnownSourceType,
59
+ canonicalSourceType,
60
+ fingerprintPrefix,
61
+ parserVersionForSource
62
+ };
package/src/paths.js ADDED
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const os = require("os");
5
+ const path = require("path");
6
+
7
+ function homeDir(env = process.env) {
8
+ return env.AGENTLOG_HOME || path.join(os.homedir(), ".agentlog");
9
+ }
10
+
11
+ function paths(env = process.env) {
12
+ const home = homeDir(env);
13
+ return {
14
+ home,
15
+ config: path.join(home, "config.json"),
16
+ redaction: path.join(home, "redaction.yaml"),
17
+ data: path.join(home, "data"),
18
+ state: path.join(home, "state"),
19
+ spool: path.join(home, "spool"),
20
+ cache: path.join(home, "cache"),
21
+ logs: path.join(home, "logs"),
22
+ pid: path.join(home, "state", "supervisor.pid"),
23
+ imports: path.join(home, "state", "imports.json"),
24
+ webAccounts: path.join(home, "state", "web-accounts.json"),
25
+ index: path.join(home, "data", "agentlog", "indexes", "bm25", "index.json"),
26
+ revealCache: path.join(home, "cache", "unredacted")
27
+ };
28
+ }
29
+
30
+ function ensureDir(dir) {
31
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
32
+ }
33
+
34
+ function ensureBaseDirs(p = paths()) {
35
+ for (const dir of [p.home, p.data, p.state, p.spool, p.cache, p.logs, p.revealCache]) {
36
+ ensureDir(dir);
37
+ }
38
+ }
39
+
40
+ function readJson(file, fallback) {
41
+ try {
42
+ return JSON.parse(fs.readFileSync(file, "utf8"));
43
+ } catch (error) {
44
+ if (error.code === "ENOENT") return fallback;
45
+ throw error;
46
+ }
47
+ }
48
+
49
+ function writeJson(file, value) {
50
+ ensureDir(path.dirname(file));
51
+ fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
52
+ }
53
+
54
+ module.exports = {
55
+ ensureBaseDirs,
56
+ ensureDir,
57
+ homeDir,
58
+ paths,
59
+ readJson,
60
+ writeJson
61
+ };
@@ -0,0 +1,228 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const { paths } = require("./paths");
6
+
7
+ const BUILT_INS = [
8
+ { name: "aws_key", regex: /\bAKIA[0-9A-Z]{16}\b/g },
9
+ { name: "anthropic_key", regex: /\bsk-ant-[A-Za-z0-9_-]{20,}\b/g },
10
+ { name: "openai_key", regex: /\bsk-[A-Za-z0-9_-]{20,}\b/g },
11
+ { name: "github_token", regex: /\bgh[pousr]_[A-Za-z0-9_]{30,}\b/g },
12
+ { name: "slack_token", regex: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g },
13
+ { name: "jwt", regex: /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g },
14
+ {
15
+ name: "private_key",
16
+ regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g
17
+ }
18
+ ];
19
+
20
+ const REDACTION_MARKER_RE = /\[REDACTED:([^\]\n]+)\]/g;
21
+
22
+ function loadRedactionConfig(env = process.env) {
23
+ const file = paths(env).redaction;
24
+ try {
25
+ return parseRedactionYaml(fs.readFileSync(file, "utf8"));
26
+ } catch (error) {
27
+ if (error.code === "ENOENT") return { patterns: [], envVars: [], allowlistRepos: [] };
28
+ throw error;
29
+ }
30
+ }
31
+
32
+ function parseRedactionYaml(text) {
33
+ const patterns = [];
34
+ const envVars = [];
35
+ const allowlistRepos = [];
36
+ const lines = text.split(/\r?\n/);
37
+ let section = "";
38
+ let currentPattern = null;
39
+
40
+ for (const raw of lines) {
41
+ const line = raw.replace(/\s+#.*$/, "");
42
+ const trimmed = line.trim();
43
+ if (!trimmed) continue;
44
+
45
+ const top = trimmed.match(/^([a-zA-Z_]+)\s*:\s*(.*)$/);
46
+ if (top && !line.startsWith(" ")) {
47
+ section = top[1];
48
+ const inline = top[2].trim();
49
+ if (section === "env_vars" && inline.startsWith("[")) {
50
+ envVars.push(...parseInlineList(inline));
51
+ } else if (section === "allowlist_repos" && inline.startsWith("[")) {
52
+ allowlistRepos.push(...parseInlineList(inline));
53
+ }
54
+ continue;
55
+ }
56
+
57
+ if (section === "patterns") {
58
+ const name = trimmed.match(/^-\s*name\s*:\s*["']?([^"']+)["']?$/);
59
+ if (name) {
60
+ currentPattern = { name: name[1].trim(), regex: "" };
61
+ patterns.push(currentPattern);
62
+ continue;
63
+ }
64
+ const regex = trimmed.match(/^regex\s*:\s*(.*)$/);
65
+ if (regex && currentPattern) {
66
+ currentPattern.regex = stripYamlString(regex[1].trim());
67
+ }
68
+ } else if (section === "env_vars") {
69
+ const item = trimmed.match(/^-\s*(.+)$/);
70
+ if (item) envVars.push(stripYamlString(item[1].trim()));
71
+ } else if (section === "allowlist_repos") {
72
+ const item = trimmed.match(/^-\s*(.+)$/);
73
+ if (item) allowlistRepos.push(stripYamlString(item[1].trim()).toLowerCase());
74
+ }
75
+ }
76
+
77
+ return {
78
+ patterns: patterns.filter((pattern) => pattern.name && pattern.regex),
79
+ envVars: [...new Set(envVars.filter(Boolean))],
80
+ allowlistRepos: [...new Set(allowlistRepos.filter(Boolean))]
81
+ };
82
+ }
83
+
84
+ function parseInlineList(value) {
85
+ return value
86
+ .replace(/^\[/, "")
87
+ .replace(/\]$/, "")
88
+ .split(",")
89
+ .map((item) => stripYamlString(item.trim()))
90
+ .filter(Boolean);
91
+ }
92
+
93
+ function stripYamlString(value) {
94
+ return value.replace(/^['"]/, "").replace(/['"]$/, "");
95
+ }
96
+
97
+ function loadEnvValues(cwd, names = [], env = process.env) {
98
+ const values = [];
99
+ for (const name of names) {
100
+ if (env[name]) values.push({ name, value: env[name] });
101
+ }
102
+ for (const file of envFilesFor(cwd)) {
103
+ try {
104
+ const text = fs.readFileSync(file, "utf8");
105
+ for (const raw of text.split(/\r?\n/)) {
106
+ const line = raw.trim();
107
+ if (!line || line.startsWith("#")) continue;
108
+ const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
109
+ if (!match || !names.includes(match[1])) continue;
110
+ const value = stripYamlString(match[2].trim());
111
+ if (value) values.push({ name: match[1], value });
112
+ }
113
+ } catch {
114
+ // Ignore unreadable .env files; import should continue.
115
+ }
116
+ }
117
+ return values.filter((item) => item.value && item.value.length >= 4);
118
+ }
119
+
120
+ function envFilesFor(cwd) {
121
+ const files = [];
122
+ let dir = path.resolve(cwd || process.cwd());
123
+ for (;;) {
124
+ files.push(path.join(dir, ".env"));
125
+ const parent = path.dirname(dir);
126
+ if (parent === dir) return files;
127
+ if (fs.existsSync(path.join(dir, ".git"))) return files;
128
+ dir = parent;
129
+ }
130
+ }
131
+
132
+ function redactText(input, options = {}) {
133
+ let text = String(input ?? "");
134
+ const summary = {};
135
+ const repo = options.repoCanonical || "";
136
+ const cfg = options.config || loadRedactionConfig(options.env);
137
+
138
+ if (repo && cfg.allowlistRepos.includes(repo.toLowerCase())) {
139
+ return { text, summary };
140
+ }
141
+
142
+ for (const pattern of BUILT_INS) {
143
+ text = replaceMatches(text, pattern.regex, `[REDACTED:${pattern.name}]`, summary, pattern.name);
144
+ }
145
+
146
+ const envValues = options.envValues || loadEnvValues(options.cwd, cfg.envVars, options.env);
147
+ for (const item of envValues) {
148
+ const escaped = escapeRegExp(item.value);
149
+ if (!escaped) continue;
150
+ text = replaceMatches(text, new RegExp(escaped, "g"), `[REDACTED:env:${item.name}]`, summary, `env:${item.name}`);
151
+ }
152
+
153
+ text = text.replace(
154
+ /(\b[A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|PWD)[A-Z0-9_]*\s*=\s*)(["']?)([A-Za-z0-9+/=_-]{32,})(\2)/gi,
155
+ (_, prefix, quote, value, suffix) => {
156
+ if (!looksHighEntropy(value)) return `${prefix}${quote}${value}${suffix}`;
157
+ increment(summary, "high_entropy_env");
158
+ return `${prefix}${quote}[REDACTED:high_entropy_env]${suffix}`;
159
+ }
160
+ );
161
+
162
+ for (const pattern of cfg.patterns) {
163
+ try {
164
+ text = replaceMatches(text, new RegExp(pattern.regex, "g"), `[REDACTED:${pattern.name}]`, summary, pattern.name);
165
+ } catch {
166
+ increment(summary, "invalid_user_pattern");
167
+ }
168
+ }
169
+
170
+ return { text, summary };
171
+ }
172
+
173
+ function styleRedactionMarkersForMarkdown(input) {
174
+ return String(input ?? "").replace(REDACTION_MARKER_RE, (_, rawKind) => {
175
+ const kind = safeRedactionKind(rawKind);
176
+ return `<mark class="agentlog-redaction" title="agentlog redacted ${escapeHtml(kind)}">[REDACTED:${escapeHtml(kind)}]</mark>`;
177
+ });
178
+ }
179
+
180
+ function safeRedactionKind(value) {
181
+ return String(value || "unknown").replace(/[^\w:.-]/g, "_").slice(0, 120) || "unknown";
182
+ }
183
+
184
+ function escapeHtml(value) {
185
+ return String(value ?? "").replace(/[&<>"']/g, (ch) => ({
186
+ "&": "&amp;",
187
+ "<": "&lt;",
188
+ ">": "&gt;",
189
+ "\"": "&quot;",
190
+ "'": "&#39;"
191
+ })[ch]);
192
+ }
193
+
194
+ function replaceMatches(text, regex, replacement, summary, name) {
195
+ return text.replace(regex, () => {
196
+ increment(summary, name);
197
+ return replacement;
198
+ });
199
+ }
200
+
201
+ function looksHighEntropy(value) {
202
+ const unique = new Set(value.split(""));
203
+ return unique.size >= 12 && /[A-Z]/i.test(value) && /\d/.test(value);
204
+ }
205
+
206
+ function increment(summary, key, amount = 1) {
207
+ summary[key] = (summary[key] || 0) + amount;
208
+ }
209
+
210
+ function mergeSummaries(target, source) {
211
+ for (const [key, value] of Object.entries(source || {})) {
212
+ increment(target, key, value);
213
+ }
214
+ return target;
215
+ }
216
+
217
+ function escapeRegExp(value) {
218
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
219
+ }
220
+
221
+ module.exports = {
222
+ loadEnvValues,
223
+ loadRedactionConfig,
224
+ mergeSummaries,
225
+ parseRedactionYaml,
226
+ redactText,
227
+ styleRedactionMarkersForMarkdown
228
+ };
package/src/repo.js ADDED
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+
3
+ const crypto = require("crypto");
4
+ const fs = require("fs");
5
+ const os = require("os");
6
+ const path = require("path");
7
+ const { execFileSync } = require("child_process");
8
+
9
+ function canonicalizeRemote(remote) {
10
+ if (!remote || typeof remote !== "string") return "";
11
+ let value = remote.trim();
12
+ if (!value) return "";
13
+
14
+ const scpLike = value.match(/^(?:[^@/:]+@)?([^/:]+):(.+)$/);
15
+ if (scpLike && !value.includes("://")) {
16
+ return normalizeHostPath(scpLike[1], scpLike[2]);
17
+ }
18
+
19
+ try {
20
+ const parsed = new URL(value);
21
+ return normalizeHostPath(parsed.hostname, parsed.pathname);
22
+ } catch {
23
+ const withoutProtocol = value.replace(/^[a-z]+:\/\//i, "");
24
+ const firstSlash = withoutProtocol.indexOf("/");
25
+ if (firstSlash === -1) return stripGit(withoutProtocol).toLowerCase();
26
+ return normalizeHostPath(withoutProtocol.slice(0, firstSlash), withoutProtocol.slice(firstSlash + 1));
27
+ }
28
+ }
29
+
30
+ function normalizeHostPath(host, repoPath) {
31
+ const cleanedHost = host.replace(/^.*@/, "").toLowerCase();
32
+ const cleanedPath = stripGit(repoPath.replace(/^\/+/, "").replace(/\/+$/, ""))
33
+ .toLowerCase();
34
+ if (!cleanedHost || !cleanedPath) return "";
35
+ return `${cleanedHost}/${cleanedPath}`;
36
+ }
37
+
38
+ function stripGit(value) {
39
+ return value.replace(/\.git$/i, "");
40
+ }
41
+
42
+ function canonicalRepo(cwd = process.cwd()) {
43
+ const override = readRepoOverride(cwd);
44
+ if (override) return { key: override, source: ".agentlog.yaml" };
45
+
46
+ const remote = runGit(cwd, ["config", "--get", "remote.origin.url"]);
47
+ const remoteKey = canonicalizeRemote(remote);
48
+ if (remoteKey) return { key: remoteKey, source: "git-remote" };
49
+
50
+ const firstCommit = runGit(cwd, ["rev-list", "--max-parents=0", "HEAD"]);
51
+ if (firstCommit) return { key: `firstcommit:${firstCommit.trim()}`, source: "git-first-commit" };
52
+
53
+ const normalized = normalizePathForHash(cwd);
54
+ const hash = crypto.createHash("sha256").update(normalized).digest("hex").slice(0, 32);
55
+ return { key: `path:${hash}`, source: "path-hash" };
56
+ }
57
+
58
+ function runGit(cwd, args) {
59
+ try {
60
+ return execFileSync("git", ["-C", cwd, ...args], {
61
+ encoding: "utf8",
62
+ stdio: ["ignore", "pipe", "ignore"]
63
+ }).trim();
64
+ } catch {
65
+ return "";
66
+ }
67
+ }
68
+
69
+ function normalizePathForHash(cwd) {
70
+ const resolved = path.resolve(cwd);
71
+ const home = os.homedir();
72
+ if (resolved === home) return "~";
73
+ if (resolved.startsWith(`${home}${path.sep}`)) return `~/${resolved.slice(home.length + 1)}`;
74
+ return resolved;
75
+ }
76
+
77
+ function readRepoOverride(cwd) {
78
+ const file = findUp(cwd, ".agentlog.yaml");
79
+ if (!file) return "";
80
+ try {
81
+ const text = fs.readFileSync(file, "utf8");
82
+ const match = text.match(/^\s*canonical_repo\s*:\s*["']?([^"'\n#]+)["']?\s*(?:#.*)?$/m);
83
+ return match ? match[1].trim().toLowerCase() : "";
84
+ } catch {
85
+ return "";
86
+ }
87
+ }
88
+
89
+ function findUp(start, filename) {
90
+ let dir = path.resolve(start || process.cwd());
91
+ for (;;) {
92
+ const candidate = path.join(dir, filename);
93
+ if (fs.existsSync(candidate)) return candidate;
94
+ const parent = path.dirname(dir);
95
+ if (parent === dir) return "";
96
+ if (fs.existsSync(path.join(dir, ".git"))) return "";
97
+ dir = parent;
98
+ }
99
+ }
100
+
101
+ module.exports = {
102
+ canonicalRepo,
103
+ canonicalizeRemote,
104
+ normalizePathForHash,
105
+ readRepoOverride
106
+ };