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.
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const http = require("http");
5
+ const path = require("path");
6
+ const { archiveRoot } = require("./archive");
7
+ const { loadConfig } = require("./config");
8
+ const { ensureDir, writeJson } = require("./paths");
9
+
10
+ function startCollector(env = process.env, options = {}) {
11
+ const cfg = loadConfig(env);
12
+ if (!cfg.collector?.enabled && !options.force) return null;
13
+ const endpoint = new URL(cfg.collector?.otlpEndpoint || "http://localhost:4318");
14
+ const host = endpoint.hostname || "127.0.0.1";
15
+ const port = Number(endpoint.port || 4318);
16
+ const server = http.createServer((req, res) => {
17
+ if (req.method === "GET" && req.url === "/healthz") {
18
+ writeJsonResponse(res, { ok: true, collector: "agentlog" });
19
+ return;
20
+ }
21
+ if (req.method !== "POST" || !isOtlpPath(req.url || "")) {
22
+ res.writeHead(404, { "content-type": "text/plain" });
23
+ res.end("not found\n");
24
+ return;
25
+ }
26
+ collectBody(req)
27
+ .then((body) => {
28
+ const record = writeTelemetryPayload(req, body, env);
29
+ writeJsonResponse(res, { partialSuccess: {}, agentlog: { stored: record.file } });
30
+ })
31
+ .catch((error) => {
32
+ writeJsonResponse(res, { error: error.message }, 500);
33
+ });
34
+ });
35
+ server.on("error", (error) => {
36
+ if (typeof options.onError === "function") options.onError(error);
37
+ });
38
+ server.listen(port, host);
39
+ return server;
40
+ }
41
+
42
+ function isOtlpPath(url) {
43
+ return ["/v1/logs", "/v1/traces", "/v1/metrics"].some((prefix) => String(url).startsWith(prefix));
44
+ }
45
+
46
+ function writeTelemetryPayload(req, body, env = process.env) {
47
+ const contentType = String(req.headers["content-type"] || "");
48
+ const kind = req.url.includes("/traces") ? "traces" : req.url.includes("/metrics") ? "metrics" : "logs";
49
+ const now = new Date();
50
+ const dir = path.join(
51
+ archiveRoot(env),
52
+ "telemetry",
53
+ `year=${now.getUTCFullYear()}`,
54
+ `month=${String(now.getUTCMonth() + 1).padStart(2, "0")}`,
55
+ `day=${String(now.getUTCDate()).padStart(2, "0")}`,
56
+ kind
57
+ );
58
+ ensureDir(dir);
59
+ const id = `${now.toISOString().replace(/[:.]/g, "-")}-${Math.random().toString(36).slice(2, 10)}`;
60
+ const isJson = contentType.includes("json") || looksLikeJson(body);
61
+ const payloadFile = path.join(dir, `event=${id}.${isJson ? "json" : "otlp"}`);
62
+ if (isJson) {
63
+ const parsed = safeJson(body);
64
+ writeJson(payloadFile, {
65
+ receivedAt: now.toISOString(),
66
+ path: req.url,
67
+ contentType,
68
+ payload: parsed
69
+ });
70
+ } else {
71
+ fs.writeFileSync(payloadFile, body, { mode: 0o600 });
72
+ writeJson(path.join(dir, `event=${id}.metadata.json`), {
73
+ receivedAt: now.toISOString(),
74
+ path: req.url,
75
+ contentType,
76
+ bytes: body.length,
77
+ note: "Binary OTLP payload stored raw. Configure OTEL_EXPORTER_OTLP_PROTOCOL=http/json for readable payloads."
78
+ });
79
+ }
80
+ return { file: payloadFile };
81
+ }
82
+
83
+ function collectBody(req) {
84
+ return new Promise((resolve, reject) => {
85
+ const chunks = [];
86
+ req.on("data", (chunk) => chunks.push(chunk));
87
+ req.on("end", () => resolve(Buffer.concat(chunks)));
88
+ req.on("error", reject);
89
+ });
90
+ }
91
+
92
+ function writeJsonResponse(res, payload, status = 200) {
93
+ res.writeHead(status, { "content-type": "application/json" });
94
+ res.end(JSON.stringify(payload));
95
+ }
96
+
97
+ function safeJson(body) {
98
+ try {
99
+ return JSON.parse(body.toString("utf8"));
100
+ } catch {
101
+ return { raw: body.toString("utf8") };
102
+ }
103
+ }
104
+
105
+ function looksLikeJson(body) {
106
+ const text = body.toString("utf8", 0, Math.min(body.length, 32)).trim();
107
+ return text.startsWith("{") || text.startsWith("[");
108
+ }
109
+
110
+ module.exports = {
111
+ startCollector,
112
+ writeTelemetryPayload
113
+ };
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const { paths } = require("../paths");
6
+
7
+ function logsCommand(flags, env) {
8
+ const file = path.join(paths(env).logs, flags.file || "supervisor.log");
9
+ if (!flags.follow) {
10
+ try {
11
+ process.stdout.write(fs.readFileSync(file, "utf8"));
12
+ } catch (error) {
13
+ if (error.code === "ENOENT") console.log("No logs yet.");
14
+ else throw error;
15
+ }
16
+ return;
17
+ }
18
+
19
+ let offset = 0;
20
+ try {
21
+ const content = fs.readFileSync(file);
22
+ process.stdout.write(content);
23
+ offset = content.length;
24
+ } catch (error) {
25
+ if (error.code !== "ENOENT") throw error;
26
+ // Continue into follow mode.
27
+ }
28
+
29
+ fs.watchFile(file, { interval: 1000 }, (current) => {
30
+ try {
31
+ if (!current.isFile()) return;
32
+ if (current.size < offset) offset = 0;
33
+ if (current.size === offset) return;
34
+ const fd = fs.openSync(file, "r");
35
+ try {
36
+ const buffer = Buffer.alloc(current.size - offset);
37
+ fs.readSync(fd, buffer, 0, buffer.length, offset);
38
+ process.stdout.write(buffer);
39
+ offset = current.size;
40
+ } finally {
41
+ fs.closeSync(fd);
42
+ }
43
+ } catch {
44
+ // No-op.
45
+ }
46
+ });
47
+ }
48
+
49
+ module.exports = {
50
+ logsCommand
51
+ };
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+
3
+ const { runMcpServer } = require("../mcp");
4
+
5
+ function serverCommand(flags, env, { runMcpServer: startMcpServer = runMcpServer, input = process.stdin, output = process.stdout } = {}) {
6
+ return startMcpServer(input, output, env);
7
+ }
8
+
9
+ module.exports = {
10
+ serverCommand
11
+ };
package/src/config.js ADDED
@@ -0,0 +1,240 @@
1
+ "use strict";
2
+
3
+ const crypto = require("crypto");
4
+ const os = require("os");
5
+ const path = require("path");
6
+ const { ensureBaseDirs, paths, readJson, writeJson } = require("./paths");
7
+ const { IMPORT_SOURCE_ORDER, enabledImportSources } = require("./sources");
8
+
9
+ function defaultConfig(env = process.env) {
10
+ const p = paths(env);
11
+ return {
12
+ version: 1,
13
+ storage: {
14
+ backend: "local",
15
+ root: p.data
16
+ },
17
+ collector: {
18
+ otlpEndpoint: "http://localhost:4318",
19
+ protocol: "http/json",
20
+ enabled: true,
21
+ note: "Local HTTP OTLP collector writes telemetry payloads into the archive when the supervisor is running."
22
+ },
23
+ remote: {
24
+ type: "",
25
+ endpoint: "",
26
+ bucket: "",
27
+ region: "auto",
28
+ prefix: "agentlog",
29
+ accessKeyId: "",
30
+ secretAccessKey: ""
31
+ },
32
+ device: {
33
+ id: defaultDeviceId(env),
34
+ name: defaultDeviceName(env),
35
+ slug: defaultDeviceSlug(env)
36
+ },
37
+ sync: {
38
+ mode: "upload",
39
+ intervalMinutes: 30
40
+ },
41
+ index: {
42
+ paused: false,
43
+ intervalMinutes: 10,
44
+ batteryIntervalMinutes: 30
45
+ },
46
+ imports: {
47
+ defaultSinceDays: 30,
48
+ sources: IMPORT_SOURCE_ORDER,
49
+ autoDiscoverSources: true
50
+ },
51
+ privacy: {
52
+ webChatsDefaultScope: "local",
53
+ revealCache: true
54
+ },
55
+ createdAt: new Date().toISOString()
56
+ };
57
+ }
58
+
59
+ function loadConfig(env = process.env) {
60
+ const p = paths(env);
61
+ const cfg = readJson(p.config, null);
62
+ if (!cfg) return defaultConfig(env);
63
+ const defaults = defaultConfig(env);
64
+ const merged = {
65
+ ...defaults,
66
+ ...cfg,
67
+ storage: { ...defaults.storage, ...(cfg.storage || {}) },
68
+ collector: { ...defaults.collector, ...(cfg.collector || {}) },
69
+ remote: { ...defaults.remote, ...(cfg.remote || {}) },
70
+ device: { ...defaults.device, ...(cfg.device || {}) },
71
+ sync: { ...defaults.sync, ...(cfg.sync || {}) },
72
+ index: { ...defaults.index, ...(cfg.index || {}) },
73
+ imports: { ...defaults.imports, ...(cfg.imports || {}) },
74
+ privacy: { ...defaults.privacy, ...(cfg.privacy || {}) }
75
+ };
76
+ merged.imports.sources = enabledImportSources(merged.imports.sources);
77
+ return merged;
78
+ }
79
+
80
+ function effectiveImportSources(config) {
81
+ const imports = config?.imports || {};
82
+ const configured = enabledImportSources(Array.isArray(imports.sources) ? imports.sources : []);
83
+ if (imports.autoDiscoverSources === false) return configured.length ? configured : undefined;
84
+ return enabledImportSources([...new Set([...configured, ...IMPORT_SOURCE_ORDER])]);
85
+ }
86
+
87
+ function saveConfig(config, env = process.env) {
88
+ const p = paths(env);
89
+ ensureBaseDirs(p);
90
+ writeJson(p.config, config);
91
+ }
92
+
93
+ function initConfig(options = {}, env = process.env) {
94
+ const p = paths(env);
95
+ ensureBaseDirs(p);
96
+ const cfg = loadConfig(env);
97
+ const storage = options.storage || cfg.storage.backend || "local";
98
+ cfg.storage.backend = storage;
99
+ cfg.storage.root = path.resolve(options.root || cfg.storage.root || p.data);
100
+ const explicitDeviceName = options.deviceName || options.device;
101
+ const deviceName = explicitDeviceName || cfg.device?.name || defaultDeviceName(env);
102
+ cfg.device = {
103
+ ...(cfg.device || {}),
104
+ id: options.deviceId || cfg.device?.id || defaultDeviceId(env),
105
+ name: deviceName,
106
+ slug: options.deviceSlug || options["device-slug"] || (explicitDeviceName ? slugifyDeviceName(deviceName) : cfg.device?.slug || slugifyDeviceName(deviceName))
107
+ };
108
+ cfg.sync = {
109
+ ...(cfg.sync || {}),
110
+ mode: normalizeSyncMode(options.syncMode || options["sync-mode"] || options.mode || cfg.sync?.mode || "upload"),
111
+ intervalMinutes: normalizeSyncInterval(options.syncIntervalMinutes || options["sync-interval-minutes"] || options.syncInterval || cfg.sync?.intervalMinutes || 30)
112
+ };
113
+ if (options.remote) {
114
+ const normalizedRemote = normalizeRemoteEndpointInput(options.remote, options.bucket || cfg.remote?.bucket || "");
115
+ cfg.remote = {
116
+ ...(cfg.remote || {}),
117
+ type: storage === "local" ? cfg.remote?.type || "" : storage,
118
+ endpoint: normalizedRemote.endpoint,
119
+ url: normalizedRemote.endpoint,
120
+ bucket: normalizedRemote.bucket,
121
+ region: options.region || cfg.remote?.region || (storage === "r2" ? "auto" : "us-east-1"),
122
+ prefix: options.prefix || cfg.remote?.prefix || "agentlog",
123
+ accessKeyId: options.accessKeyId || cfg.remote?.accessKeyId || "",
124
+ secretAccessKey: options.secretAccessKey || options.token || cfg.remote?.secretAccessKey || ""
125
+ };
126
+ }
127
+ cfg.updatedAt = new Date().toISOString();
128
+ saveConfig(cfg, env);
129
+ return cfg;
130
+ }
131
+
132
+ function defaultDeviceName(env = process.env) {
133
+ return String(env.AGENTLOG_DEVICE_NAME || os.hostname() || "this-device").split(".")[0] || "this-device";
134
+ }
135
+
136
+ function defaultDeviceSlug(env = process.env) {
137
+ return slugifyDeviceName(defaultDeviceName(env));
138
+ }
139
+
140
+ function defaultDeviceId(env = process.env) {
141
+ if (env.AGENTLOG_DEVICE_ID) return String(env.AGENTLOG_DEVICE_ID).trim();
142
+ return crypto.createHash("sha256").update(`${os.hostname()}|${paths(env).home}`).digest("hex").slice(0, 12);
143
+ }
144
+
145
+ function slugifyDeviceName(value) {
146
+ return String(value || "this-device")
147
+ .trim()
148
+ .toLowerCase()
149
+ .replace(/[^a-z0-9]+/g, "-")
150
+ .replace(/^-+|-+$/g, "")
151
+ .slice(0, 64) || "this-device";
152
+ }
153
+
154
+ function normalizeSyncMode(value) {
155
+ const normalized = String(value || "upload").trim().toLowerCase();
156
+ return {
157
+ "upload-only": "upload",
158
+ upload: "upload",
159
+ push: "upload",
160
+ "two-way": "two-way",
161
+ twoway: "two-way",
162
+ bidirectional: "two-way",
163
+ receive: "receive",
164
+ "receive-only": "receive",
165
+ pull: "receive"
166
+ }[normalized] || normalized;
167
+ }
168
+
169
+ function normalizeSyncInterval(value) {
170
+ if (String(value || "").trim().toLowerCase() === "manual") return 0;
171
+ const number = Number(value);
172
+ if (!Number.isFinite(number) || number < 0) return 30;
173
+ return Math.round(number);
174
+ }
175
+
176
+ function normalizeRemoteEndpointInput(endpoint, bucket = "") {
177
+ const input = String(endpoint || "").trim();
178
+ const existingBucket = String(bucket || "").trim();
179
+ if (!input || input.startsWith("file://")) return { endpoint: input, bucket: existingBucket };
180
+ let url;
181
+ try {
182
+ url = new URL(input);
183
+ } catch {
184
+ return { endpoint: input, bucket: existingBucket };
185
+ }
186
+ if (url.protocol !== "http:" && url.protocol !== "https:") return { endpoint: input, bucket: existingBucket };
187
+ const segments = url.pathname.split("/").map((part) => decodeURIComponent(part)).filter(Boolean);
188
+ if (!segments.length) return { endpoint: url.origin, bucket: existingBucket };
189
+ const inferredBucket = existingBucket || segments[0];
190
+ return { endpoint: url.origin, bucket: inferredBucket };
191
+ }
192
+
193
+ function setConfigKey(key, value, env = process.env) {
194
+ const cfg = loadConfig(env);
195
+ const parts = key.split(".").filter(Boolean);
196
+ if (parts.length === 0) throw new Error("config key is required");
197
+ let cursor = cfg;
198
+ for (const part of parts.slice(0, -1)) {
199
+ if (!cursor[part] || typeof cursor[part] !== "object") cursor[part] = {};
200
+ cursor = cursor[part];
201
+ }
202
+ cursor[parts[parts.length - 1]] = parseConfigValue(value);
203
+ cfg.updatedAt = new Date().toISOString();
204
+ saveConfig(cfg, env);
205
+ return cfg;
206
+ }
207
+
208
+ function getConfigKey(key, env = process.env) {
209
+ const cfg = loadConfig(env);
210
+ if (!key) return cfg;
211
+ let cursor = cfg;
212
+ for (const part of key.split(".").filter(Boolean)) {
213
+ if (!cursor || typeof cursor !== "object" || !(part in cursor)) return undefined;
214
+ cursor = cursor[part];
215
+ }
216
+ return cursor;
217
+ }
218
+
219
+ function parseConfigValue(value) {
220
+ if (value === "true") return true;
221
+ if (value === "false") return false;
222
+ if (value === "null") return null;
223
+ if (/^-?\d+(\.\d+)?$/.test(value)) return Number(value);
224
+ try {
225
+ return JSON.parse(value);
226
+ } catch {
227
+ return value;
228
+ }
229
+ }
230
+
231
+ module.exports = {
232
+ defaultConfig,
233
+ effectiveImportSources,
234
+ getConfigKey,
235
+ initConfig,
236
+ loadConfig,
237
+ normalizeRemoteEndpointInput,
238
+ saveConfig,
239
+ setConfigKey
240
+ };
package/src/doctor.js ADDED
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const os = require("os");
5
+ const path = require("path");
6
+ const { spawnSync } = require("child_process");
7
+ const { archiveRoot, countSessions } = require("./archive");
8
+ const { autostartStatus, recallInvocation } = require("./autostart");
9
+ const { loadConfig } = require("./config");
10
+ const { discoverCliHistory } = require("./importers");
11
+ const { paths, readJson } = require("./paths");
12
+ const { hasRemoteTarget } = require("./sync");
13
+
14
+ function runDoctor(env = process.env) {
15
+ const cfg = loadConfig(env);
16
+ const checks = [];
17
+ add(checks, "config", fs.existsSync(paths(env).config), paths(env).config, "Run `agentlog init` to create config.");
18
+ add(checks, "archive", fs.existsSync(archiveRoot(env)), archiveRoot(env), "Run `agentlog import --source all --since 30d` to create the archive.");
19
+ add(checks, "rg", commandExists("rg"), "ripgrep search", "`agentlog history` will fall back to a slower JS scan.");
20
+ add(checks, "sqlite3", commandExists("sqlite3"), "Codex, Devin, and Cursor SQLite imports", "Install sqlite3 for Codex, Devin, and Cursor state database imports.");
21
+ add(checks, "collector", Boolean(cfg.collector?.enabled), cfg.collector?.otlpEndpoint || "", "Enable collector with `agentlog config set collector.enabled true`.");
22
+ add(checks, "remote sync", hasRemoteTarget(cfg, env), remoteTargetLabel(cfg), "Configure with `agentlog sync --endpoint ... --bucket ...`.");
23
+ const auto = autostartStatus();
24
+ add(checks, "autostart", Boolean(auto.enabled), auto.file || auto.note || "", "Run `agentlog autostart enable` to start at login.");
25
+ const invocation = recallInvocation();
26
+ add(checks, "recall command", Boolean(invocation.command), `${invocation.command} ${invocation.args.join(" ")}`, "Install recall with `agentlog recall add-to codex` or `agentlog init`.");
27
+
28
+ const discovery = discoverCliHistory(env);
29
+ const coverage = sourceCoverage(discovery, cfg);
30
+ const sessions = countSessions(env);
31
+ const importState = readJson(paths(env).imports, { files: {}, sessions: {} });
32
+ return {
33
+ ok: checks.every((check) => check.status === "ok"),
34
+ home: paths(env).home,
35
+ archive: archiveRoot(env),
36
+ checks,
37
+ sessions,
38
+ imports: {
39
+ configuredSources: cfg.imports?.sources || [],
40
+ trackedSessions: Object.keys(importState.sessions || {}).length,
41
+ trackedFiles: Object.keys(importState.files || {}).length
42
+ },
43
+ coverage
44
+ };
45
+ }
46
+
47
+ function sourceCoverage(discovery, cfg) {
48
+ const configured = new Set(cfg.imports?.sources || []);
49
+ const rows = [
50
+ coverageRow("codex-cli", "Codex CLI", discovery.codexCli, configured),
51
+ coverageRow("codex-desktop", "Codex Desktop", discovery.codexDesktop, configured),
52
+ coverageRow("claude", "Claude Code CLI", discovery.claude, configured),
53
+ coverageRow("claude-code-desktop", "Claude Code Desktop", discovery.claudeCodeDesktop, configured),
54
+ coverageRow("claude-workspace", "Claude Workspace", discovery.claudeWorkspace, configured),
55
+ coverageRow("claude-sdk", "Claude SDK jobs", discovery.claudeSdk, configured),
56
+ coverageRow("gemini-cli", "Gemini CLI", discovery.geminiCli, configured),
57
+ coverageRow("antigravity", "Antigravity", discovery.antigravity, configured),
58
+ coverageRow("devin-cli", "Devin CLI", discovery.devinCli, configured),
59
+ coverageRow("cursor", "Cursor", discovery.cursor, configured)
60
+ ];
61
+ return rows.map((row) => ({
62
+ ...row,
63
+ status: row.sessions ? (row.configured ? "captured" : "found but disabled") : "none found"
64
+ }));
65
+ }
66
+
67
+ function coverageRow(source, label, result = {}, configured) {
68
+ return {
69
+ source,
70
+ label,
71
+ configured: configured.has(source) || (source === "claude-code-desktop" && configured.has("claude-desktop")) || (source === "claude-workspace" && configured.has("claude-desktop")),
72
+ sessions: result?.sessions || 0,
73
+ oldest: result?.oldest || "",
74
+ details: result?.details || {},
75
+ note: result?.note || ""
76
+ };
77
+ }
78
+
79
+ function add(checks, name, ok, detail, remediation) {
80
+ checks.push({
81
+ name,
82
+ status: ok ? "ok" : "warn",
83
+ detail: detail || "",
84
+ remediation: ok ? "" : remediation
85
+ });
86
+ }
87
+
88
+ function commandExists(command) {
89
+ const result = spawnSync(command, ["--version"], { stdio: "ignore" });
90
+ return !result.error && result.status === 0;
91
+ }
92
+
93
+ function remoteTargetLabel(cfg) {
94
+ const remote = cfg.remote || {};
95
+ if (!remote.endpoint && !remote.url) return "";
96
+ const device = cfg.device?.slug ? ` device=${cfg.device.slug}` : "";
97
+ return `${remote.type || cfg.storage?.backend || "remote"} ${remote.endpoint || remote.url}${remote.bucket ? `/${remote.bucket}` : ""}${device}`;
98
+ }
99
+
100
+ module.exports = {
101
+ runDoctor
102
+ };