agentel 0.2.8 → 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.
@@ -0,0 +1,293 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const os = require("os");
5
+ const path = require("path");
6
+
7
+ // Filesystem events are triggers, not data: an event only marks its source
8
+ // dirty, so watcher memory stays O(sources) no matter how fast agents write.
9
+ // The first event for a source opens a fixed-delay coalesce window and later
10
+ // events fold into it, so a continuously writing session cannot starve its
11
+ // own import (a trailing-edge debounce would).
12
+ const DEFAULT_COALESCE_MS = 3 * 1000;
13
+ // SQLite-backed stores churn their WAL while the app is open, including for
14
+ // state unrelated to conversations, so they get a wider window.
15
+ const SQLITE_COALESCE_MS = 20 * 1000;
16
+ // Roots that don't exist yet (tool not installed) or whose watcher died are
17
+ // re-checked on this cadence.
18
+ const WATCH_RETRY_MS = 5 * 60 * 1000;
19
+
20
+ function homeDir(env = process.env) {
21
+ return env.HOME || env.USERPROFILE || os.homedir();
22
+ }
23
+
24
+ function geminiHome(env, home) {
25
+ return env.AGENTLOG_GEMINI_HOME_DIR || env.GEMINI_HOME || path.join(home, ".gemini");
26
+ }
27
+
28
+ function claudeAppSupportRoot(env, home) {
29
+ return env.CLAUDE_APP_SUPPORT || path.join(home, "Library", "Application Support", "Claude");
30
+ }
31
+
32
+ function root(dir, options = {}) {
33
+ return {
34
+ dir: path.resolve(dir),
35
+ recursive: options.recursive !== false,
36
+ filter: options.filter || null,
37
+ coalesceMs: options.coalesceMs || DEFAULT_COALESCE_MS
38
+ };
39
+ }
40
+
41
+ function sqliteStateDbFilter(name) {
42
+ return path.basename(name).startsWith("state.vscdb");
43
+ }
44
+
45
+ // Watch roots per import source, mirroring the importer path resolution in
46
+ // src/importers.js (including its env overrides) without loading that module:
47
+ // the supervisor process should stay small, and a root that drifts slightly
48
+ // from what the importer reads only costs a no-op import or falls back to the
49
+ // heartbeat poll. Sources with no entry (aider scans whole project trees;
50
+ // watching those would fire on every git operation in every repo) stay on the
51
+ // polling cadence.
52
+ function watchRootsForSource(source, env = process.env) {
53
+ const home = homeDir(env);
54
+ switch (source) {
55
+ case "codex-cli":
56
+ case "codex-desktop":
57
+ case "codex-sdk": {
58
+ const codexHome = env.CODEX_HOME || path.join(home, ".codex");
59
+ return [root(path.join(codexHome, "sessions")), root(path.join(codexHome, "archived_sessions"))];
60
+ }
61
+ case "claude":
62
+ return [root(path.join(home, ".claude", "projects"))];
63
+ case "claude-code-desktop":
64
+ return [root(path.join(claudeAppSupportRoot(env, home), "claude-code-sessions"))];
65
+ case "claude-cowork":
66
+ return [root(path.join(claudeAppSupportRoot(env, home), "local-agent-mode-sessions"))];
67
+ case "gemini-cli":
68
+ // Antigravity homes live inside the Gemini home; exclude them so an
69
+ // Antigravity session doesn't ping the Gemini importer.
70
+ return [root(geminiHome(env, home), { filter: (name) => !name.startsWith("antigravity") })];
71
+ case "antigravity":
72
+ return [root(env.AGENTLOG_ANTIGRAVITY_HOME_DIR || path.join(geminiHome(env, home), "antigravity"))];
73
+ case "antigravity-cli":
74
+ return [root(env.AGENTLOG_ANTIGRAVITY_CLI_HOME_DIR || path.join(geminiHome(env, home), "antigravity-cli"))];
75
+ case "antigravity-ide":
76
+ return [root(env.AGENTLOG_ANTIGRAVITY_IDE_HOME_DIR || path.join(geminiHome(env, home), "antigravity-ide"))];
77
+ case "devin-cli": {
78
+ const db = env.AGENTLOG_DEVIN_SESSIONS_DB || path.join(home, ".local", "share", "devin", "cli", "sessions.db");
79
+ return [root(path.dirname(db), { recursive: false, coalesceMs: SQLITE_COALESCE_MS })];
80
+ }
81
+ case "devin-desktop": {
82
+ const appRoots = [
83
+ path.join(home, "Library", "Application Support", "Devin"),
84
+ path.join(home, ".config", "Devin")
85
+ ];
86
+ return appRoots.flatMap((appRoot) => [
87
+ root(path.join(appRoot, "User", "acp-events")),
88
+ root(path.join(appRoot, "User", "globalStorage"), {
89
+ recursive: false,
90
+ filter: sqliteStateDbFilter,
91
+ coalesceMs: SQLITE_COALESCE_MS
92
+ })
93
+ ]);
94
+ }
95
+ case "windsurf": {
96
+ const windsurfHome =
97
+ env.AGENTLOG_WINDSURF_HOME_DIR ||
98
+ env.AGENTLOG_CODEIUM_WINDSURF_HOME_DIR ||
99
+ path.join(env.AGENTLOG_CODEIUM_HOME_DIR || path.join(home, ".codeium"), "windsurf");
100
+ return [root(windsurfHome)];
101
+ }
102
+ case "copilot-cli": {
103
+ if (env.AGENTLOG_COPILOT_SESSION_STATE_DIR) return [root(env.AGENTLOG_COPILOT_SESSION_STATE_DIR)];
104
+ const copilotHome = env.AGENTLOG_COPILOT_HOME || env.COPILOT_HOME || path.join(home, ".copilot");
105
+ return [root(path.join(copilotHome, "session-state"))];
106
+ }
107
+ case "factory": {
108
+ if (env.AGENTLOG_FACTORY_SESSIONS_DIR) return [root(env.AGENTLOG_FACTORY_SESSIONS_DIR)];
109
+ return [root(path.join(env.FACTORY_HOME_OVERRIDE || home, ".factory", "sessions"))];
110
+ }
111
+ case "grok-build": {
112
+ if (env.AGENTLOG_GROK_SESSIONS_DIR) return [root(env.AGENTLOG_GROK_SESSIONS_DIR)];
113
+ const grokHome = env.AGENTLOG_GROK_HOME || env.GROK_HOME || path.join(home, ".grok");
114
+ return [root(path.join(grokHome, "sessions"))];
115
+ }
116
+ case "pi": {
117
+ const explicit = env.AGENTLOG_PI_SESSION_DIR || env.PI_CODING_AGENT_SESSION_DIR;
118
+ if (explicit) return [root(explicit)];
119
+ const agentDir = env.PI_CODING_AGENT_DIR || path.join(home, ".pi", "agent");
120
+ return [root(path.join(agentDir, "sessions"))];
121
+ }
122
+ case "cursor": {
123
+ const globalStorage =
124
+ env.AGENTLOG_CURSOR_GLOBAL_STORAGE_DIR ||
125
+ path.join(home, "Library", "Application Support", "Cursor", "User", "globalStorage");
126
+ const workspaceStorage =
127
+ env.AGENTLOG_CURSOR_WORKSPACE_STORAGE_DIR ||
128
+ path.join(home, "Library", "Application Support", "Cursor", "User", "workspaceStorage");
129
+ return [
130
+ root(globalStorage, { recursive: false, filter: sqliteStateDbFilter, coalesceMs: SQLITE_COALESCE_MS }),
131
+ root(workspaceStorage, { filter: sqliteStateDbFilter, coalesceMs: SQLITE_COALESCE_MS })
132
+ ];
133
+ }
134
+ case "cline": {
135
+ // VS Code variants only; JetBrains installs rely on the heartbeat poll.
136
+ const appData = env.APPDATA || env.AppData;
137
+ return [
138
+ path.join(home, "Library", "Application Support", "Code", "User", "globalStorage", "saoudrizwan.claude-dev"),
139
+ path.join(home, "Library", "Application Support", "Code - Insiders", "User", "globalStorage", "saoudrizwan.claude-dev"),
140
+ path.join(home, ".config", "Code", "User", "globalStorage", "saoudrizwan.claude-dev"),
141
+ appData ? path.join(appData, "Code", "User", "globalStorage", "saoudrizwan.claude-dev") : ""
142
+ ]
143
+ .filter(Boolean)
144
+ .map((dir) => root(dir));
145
+ }
146
+ case "opencode-cli":
147
+ case "opencode-desktop":
148
+ case "opencode-web": {
149
+ const configured = env.AGENTLOG_OPENCODE_DATA_DIR || env.OPENCODE_DATA_DIR;
150
+ if (configured) return [root(configured)];
151
+ const cliRoot = path.join(home, ".local", "share", "opencode");
152
+ if (source === "opencode-cli") return [root(cliRoot)];
153
+ const desktopRoots = [
154
+ path.join(home, "Library", "Application Support", "ai.opencode.desktop"),
155
+ path.join(home, "Library", "Application Support", "opencode"),
156
+ path.join(home, ".local", "share", "ai.opencode.app"),
157
+ path.join(home, "Library", "Application Support", "ai.opencode.app")
158
+ ];
159
+ const roots = source === "opencode-web" ? [cliRoot, ...desktopRoots] : desktopRoots;
160
+ return roots.map((dir) => root(dir));
161
+ }
162
+ default:
163
+ return [];
164
+ }
165
+ }
166
+
167
+ // Watches the source roots and reports dirty sources after their coalesce
168
+ // window. Roots are deduped so sources sharing a directory (codex-cli and
169
+ // codex-desktop both read ~/.codex/sessions) share one OS watch. Returns a
170
+ // handle: isWatched(source) tells the supervisor whether the source can rely
171
+ // on events (true while roots are merely missing — nothing to import then
172
+ // either — false once a watch attempt errors), refresh() retries missing or
173
+ // dead watches, close() tears everything down.
174
+ function startSourceWatchers(sources, env, onSourceDirty, options = {}) {
175
+ const coalesceOverrideMs = options.coalesceMs || 0;
176
+ const retryMs = options.retryMs || WATCH_RETRY_MS;
177
+ const onLog = typeof options.onLog === "function" ? options.onLog : () => {};
178
+
179
+ const byDir = new Map();
180
+ const sourcesWithRoots = new Set();
181
+ const failedSources = new Set();
182
+ for (const source of sources || []) {
183
+ for (const entry of watchRootsForSource(source, env)) {
184
+ sourcesWithRoots.add(source);
185
+ const existing = byDir.get(entry.dir);
186
+ if (existing) {
187
+ existing.recursive = existing.recursive || entry.recursive;
188
+ existing.subscribers.push({ source, filter: entry.filter, coalesceMs: entry.coalesceMs });
189
+ } else {
190
+ byDir.set(entry.dir, {
191
+ dir: entry.dir,
192
+ recursive: entry.recursive,
193
+ watcher: null,
194
+ subscribers: [{ source, filter: entry.filter, coalesceMs: entry.coalesceMs }]
195
+ });
196
+ }
197
+ }
198
+ }
199
+
200
+ const timers = new Map();
201
+ let closed = false;
202
+
203
+ function markDirty(source, coalesceMs) {
204
+ if (closed || timers.has(source)) return;
205
+ const timer = setTimeout(() => {
206
+ timers.delete(source);
207
+ if (!closed) onSourceDirty(source);
208
+ }, coalesceOverrideMs || coalesceMs);
209
+ if (typeof timer.unref === "function") timer.unref();
210
+ timers.set(source, timer);
211
+ }
212
+
213
+ function handleEvent(entry, filename) {
214
+ const name = typeof filename === "string" ? filename : "";
215
+ for (const subscriber of entry.subscribers) {
216
+ // A null filename means the platform couldn't say what changed; treat
217
+ // it as a match rather than risk missing activity.
218
+ if (subscriber.filter && name && !subscriber.filter(name)) continue;
219
+ markDirty(subscriber.source, subscriber.coalesceMs);
220
+ }
221
+ }
222
+
223
+ function markEntryFailed(entry, error) {
224
+ for (const subscriber of entry.subscribers) failedSources.add(subscriber.source);
225
+ onLog(`watch failed for ${entry.dir}: ${error.message}`);
226
+ }
227
+
228
+ function attach(entry) {
229
+ if (entry.watcher || closed) return;
230
+ let stat = null;
231
+ try {
232
+ stat = fs.statSync(entry.dir);
233
+ } catch {
234
+ return; // Missing root: nothing to import from it yet; refresh retries.
235
+ }
236
+ if (!stat.isDirectory()) return;
237
+ try {
238
+ const watcher = fs.watch(entry.dir, { recursive: entry.recursive, persistent: false });
239
+ watcher.on("change", (eventType, filename) => handleEvent(entry, filename));
240
+ watcher.on("error", () => {
241
+ // Watches die when the root is replaced; refresh re-attaches.
242
+ try {
243
+ watcher.close();
244
+ } catch {}
245
+ entry.watcher = null;
246
+ });
247
+ entry.watcher = watcher;
248
+ } catch (error) {
249
+ markEntryFailed(entry, error);
250
+ }
251
+ }
252
+
253
+ function refresh() {
254
+ if (closed) return;
255
+ for (const entry of byDir.values()) attach(entry);
256
+ }
257
+
258
+ refresh();
259
+ const retryTimer = setInterval(refresh, retryMs);
260
+ if (typeof retryTimer.unref === "function") retryTimer.unref();
261
+
262
+ return {
263
+ isWatched(source) {
264
+ return sourcesWithRoots.has(source) && !failedSources.has(source);
265
+ },
266
+ activeWatchCount() {
267
+ let count = 0;
268
+ for (const entry of byDir.values()) if (entry.watcher) count += 1;
269
+ return count;
270
+ },
271
+ refresh,
272
+ close() {
273
+ closed = true;
274
+ clearInterval(retryTimer);
275
+ for (const timer of timers.values()) clearTimeout(timer);
276
+ timers.clear();
277
+ for (const entry of byDir.values()) {
278
+ if (!entry.watcher) continue;
279
+ try {
280
+ entry.watcher.close();
281
+ } catch {}
282
+ entry.watcher = null;
283
+ }
284
+ }
285
+ };
286
+ }
287
+
288
+ module.exports = {
289
+ DEFAULT_COALESCE_MS,
290
+ SQLITE_COALESCE_MS,
291
+ startSourceWatchers,
292
+ watchRootsForSource
293
+ };
package/src/sources.js CHANGED
@@ -15,7 +15,7 @@ const SOURCE_GROUPS = [
15
15
  sources: [
16
16
  { source: "claude", provider: "claude_code", label: "Claude Code CLI" },
17
17
  { source: "claude-code-desktop", provider: "claude_desktop", sourceType: "claude-code-desktop-metadata", label: "Claude Code Desktop" },
18
- { source: "claude-workspace", provider: "claude_desktop", sourceType: "claude-workspace-desktop", label: "Claude Workspace" },
18
+ { source: "claude-cowork", provider: "claude_desktop", sourceType: "claude-workspace-desktop", label: "Claude Cowork" },
19
19
  { source: "claude-web", provider: "claude_web", label: "Claude.ai" },
20
20
  { source: "claude-sdk", provider: "claude_sdk", label: "Claude SDK jobs" }
21
21
  ]
@@ -24,19 +24,42 @@ const SOURCE_GROUPS = [
24
24
  group: "Google",
25
25
  sources: [
26
26
  { source: "gemini-cli", provider: "gemini_cli", label: "Gemini CLI" },
27
- { source: "antigravity", provider: "antigravity", label: "Antigravity" }
27
+ { source: "antigravity-cli", provider: "antigravity_cli", sourceType: "antigravity-cli-transcript-log", label: "Antigravity CLI" },
28
+ { source: "antigravity", provider: "antigravity", label: "Antigravity 2.0" },
29
+ { source: "antigravity-ide", provider: "antigravity_ide", sourceType: "antigravity-ide-transcript-log", label: "Antigravity IDE" }
28
30
  ]
29
31
  },
30
32
  {
31
33
  group: "Cognition",
32
34
  sources: [
33
- { source: "devin-cli", provider: "devin", sourceType: "devin-cli-history", label: "Devin CLI" }
35
+ { source: "devin-cli", provider: "devin", sourceType: "devin-cli-history", label: "Devin CLI" },
36
+ { source: "devin-desktop", provider: "devin", sourceType: "devin-desktop-acp-events", label: "Devin Desktop" },
37
+ { source: "windsurf", provider: "windsurf", sourceType: "windsurf-cascade-brain", label: "Windsurf" }
38
+ ]
39
+ },
40
+ {
41
+ group: "GitHub",
42
+ sources: [
43
+ { source: "copilot-cli", provider: "copilot", sourceType: "copilot-cli-history", label: "GitHub Copilot CLI" }
44
+ ]
45
+ },
46
+ {
47
+ group: "Factory",
48
+ sources: [
49
+ { source: "factory", provider: "factory", sourceType: "factory-droid-history", label: "Factory Droid" }
50
+ ]
51
+ },
52
+ {
53
+ group: "xAI",
54
+ sources: [
55
+ { source: "grok-build", provider: "grok", sourceType: "grok-build-history", label: "Grok Build" }
34
56
  ]
35
57
  },
36
58
  {
37
59
  group: "Other",
38
60
  sources: [
39
61
  { source: "cursor", provider: "cursor", label: "Cursor" },
62
+ { source: "pi", provider: "pi", sourceType: "pi-cli-history", label: "pi" },
40
63
  { source: "cline", provider: "cline", sourceType: "cline-task-history", label: "Cline" },
41
64
  { source: "opencode-cli", provider: "opencode", sourceType: "opencode-cli-history", label: "OpenCode CLI" },
42
65
  { source: "opencode-desktop", provider: "opencode", sourceType: "opencode-desktop-history", label: "OpenCode Desktop" },
@@ -46,23 +69,28 @@ const SOURCE_GROUPS = [
46
69
  }
47
70
  ];
48
71
 
49
- const DISABLED_IMPORT_SOURCES = new Set(["windsurf"]);
72
+ const DISABLED_IMPORT_SOURCES = new Set();
50
73
 
51
- const DISABLED_IMPORT_SOURCE_MESSAGES = {
52
- windsurf:
53
- "Windsurf import is disabled because current Cascade transcripts are encrypted binary stores. agentlog can detect those sessions, but cannot archive readable conversation text yet."
54
- };
74
+ const DISABLED_IMPORT_SOURCE_MESSAGES = {};
55
75
 
56
76
  const IMPORT_SOURCE_ORDER = [
57
77
  "codex-cli",
58
78
  "codex-desktop",
59
79
  "claude",
60
80
  "claude-code-desktop",
61
- "claude-workspace",
81
+ "claude-cowork",
62
82
  "gemini-cli",
83
+ "antigravity-cli",
63
84
  "antigravity",
85
+ "antigravity-ide",
64
86
  "devin-cli",
87
+ "devin-desktop",
88
+ "windsurf",
89
+ "copilot-cli",
90
+ "factory",
91
+ "grok-build",
65
92
  "cursor",
93
+ "pi",
66
94
  "cline",
67
95
  "opencode-cli",
68
96
  "opencode-desktop",
@@ -70,17 +98,38 @@ const IMPORT_SOURCE_ORDER = [
70
98
  "aider"
71
99
  ];
72
100
 
101
+ const IMPORT_SOURCE_ALIASES = {
102
+ "claude-workspace": "claude-cowork",
103
+ claude_workspace: "claude-cowork",
104
+ claude_cowork: "claude-cowork"
105
+ };
106
+
73
107
  const HISTORY_PROVIDER_OPTIONS = SOURCE_GROUPS.flatMap((group) => group.sources);
74
108
 
109
+ function canonicalImportSource(source) {
110
+ const value = String(source || "").trim();
111
+ return IMPORT_SOURCE_ALIASES[value] || value;
112
+ }
113
+
75
114
  function enabledImportSources(sources) {
76
- return (sources || []).filter((source) => !DISABLED_IMPORT_SOURCES.has(source));
115
+ const seen = new Set();
116
+ const result = [];
117
+ for (const rawSource of sources || []) {
118
+ const source = canonicalImportSource(rawSource);
119
+ if (!source || seen.has(source) || DISABLED_IMPORT_SOURCES.has(source)) continue;
120
+ seen.add(source);
121
+ result.push(source);
122
+ }
123
+ return result;
77
124
  }
78
125
 
79
126
  function sourceLabel(source) {
80
- return HISTORY_PROVIDER_OPTIONS.find((item) => item.source === source)?.label || { opencode: "OpenCode" }[source] || source;
127
+ const canonical = canonicalImportSource(source);
128
+ return HISTORY_PROVIDER_OPTIONS.find((item) => item.source === canonical)?.label || { opencode: "OpenCode" }[canonical] || canonical;
81
129
  }
82
130
 
83
131
  module.exports = {
132
+ canonicalImportSource,
84
133
  DISABLED_IMPORT_SOURCE_MESSAGES,
85
134
  DISABLED_IMPORT_SOURCES,
86
135
  HISTORY_PROVIDER_OPTIONS,