agentel 0.2.2 → 0.2.4

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/README.md CHANGED
@@ -41,7 +41,7 @@ ref for repeatable installs:
41
41
  ```sh
42
42
  npm install -g brianlzhou/agentlog
43
43
  # or
44
- npm install -g brianlzhou/agentlog#v0.2.2
44
+ npm install -g brianlzhou/agentlog#v0.2.4
45
45
  agentlog init
46
46
  ```
47
47
 
@@ -183,30 +183,30 @@ package-prefixed scheme.
183
183
 
184
184
  | Source type | Version |
185
185
  | --- | --- |
186
- | `codex-cli-history` | `0.2.2.0` |
187
- | `codex-desktop-history` | `0.2.2.0` |
188
- | `cli-history` | `0.2.2.0` |
189
- | `claude-sdk-history` | `0.2.2.0` |
190
- | `claude-code-desktop-metadata` | `0.2.2.0` |
191
- | `claude-workspace-desktop` | `0.2.2.0` |
192
- | `cursor-workspace-sqlite` | `0.2.2.0` |
193
- | `cursor-global-sqlite` | `0.2.2.0` |
194
- | `cursor-raw-sqlite-salvage` | `0.2.2.0` |
195
- | `cursor-agent-transcripts` | `0.2.2.0` |
196
- | `devin-cli-history` | `0.2.2.0` |
197
- | `gemini-cli-history` | `0.2.2.0` |
198
- | `cline-task-history` | `0.2.2.0` |
199
- | `opencode-history` | `0.2.2.0` |
200
- | `opencode-sqlite-history` | `0.2.2.0` |
201
- | `aider-chat-history` | `0.2.2.0` |
202
- | `antigravity-history` | `0.2.2.0` |
203
- | `antigravity-trajectory-summary` | `0.2.2.0` |
204
- | `windsurf-trajectory-export` | `0.2.2.0` |
205
- | `web-chat-export` | `0.2.2.0` |
206
- | `chatgpt-export` | `0.2.2.0` |
207
- | `claude-web-export` | `0.2.2.0` |
208
- | `claude-web-memory` | `0.2.2.0` |
209
- | `import` | `0.2.2.0` |
186
+ | `codex-cli-history` | `0.2.4.0` |
187
+ | `codex-desktop-history` | `0.2.4.0` |
188
+ | `cli-history` | `0.2.4.0` |
189
+ | `claude-sdk-history` | `0.2.4.0` |
190
+ | `claude-code-desktop-metadata` | `0.2.4.0` |
191
+ | `claude-workspace-desktop` | `0.2.4.0` |
192
+ | `cursor-workspace-sqlite` | `0.2.4.0` |
193
+ | `cursor-global-sqlite` | `0.2.4.0` |
194
+ | `cursor-raw-sqlite-salvage` | `0.2.4.0` |
195
+ | `cursor-agent-transcripts` | `0.2.4.0` |
196
+ | `devin-cli-history` | `0.2.4.0` |
197
+ | `gemini-cli-history` | `0.2.4.0` |
198
+ | `cline-task-history` | `0.2.4.0` |
199
+ | `opencode-history` | `0.2.4.0` |
200
+ | `opencode-sqlite-history` | `0.2.4.0` |
201
+ | `aider-chat-history` | `0.2.4.0` |
202
+ | `antigravity-history` | `0.2.4.0` |
203
+ | `antigravity-trajectory-summary` | `0.2.4.0` |
204
+ | `windsurf-trajectory-export` | `0.2.4.0` |
205
+ | `web-chat-export` | `0.2.4.0` |
206
+ | `chatgpt-export` | `0.2.4.0` |
207
+ | `claude-web-export` | `0.2.4.0` |
208
+ | `claude-web-memory` | `0.2.4.0` |
209
+ | `import` | `0.2.4.0` |
210
210
 
211
211
  `cursor-sqlite-history` and `antigravity-brain` are compatibility aliases for
212
212
  older labels. Fingerprints include the parser version prefix, so changing the
package/docs/release.md CHANGED
@@ -65,5 +65,5 @@ After tagging and pushing the release, sanity-check both public install forms:
65
65
 
66
66
  ```sh
67
67
  npm install -g agentel
68
- npm install -g brianlzhou/agentlog#v0.2.2
68
+ npm install -g brianlzhou/agentlog#v0.2.4
69
69
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentel",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Local-first archive and recall layer for agent coding sessions.",
5
5
  "type": "commonjs",
6
6
  "license": "MIT",
@@ -39,7 +39,6 @@
39
39
  "docs/code-reference.md",
40
40
  "docs/history-source-handling.md",
41
41
  "docs/release.md",
42
- "agentlog-spec.md",
43
42
  "README.md",
44
43
  "LICENSE"
45
44
  ],
package/src/archive.js CHANGED
@@ -138,7 +138,8 @@ function writeSession(input, env = process.env) {
138
138
  "conversationKind",
139
139
  "pinned",
140
140
  "timeStatus",
141
- "composerId"
141
+ "composerId",
142
+ "parentComposerId"
142
143
  ]) {
143
144
  if (input[key] !== undefined) session[key] = input[key];
144
145
  }
@@ -79,7 +79,7 @@ const PROVIDER_ADAPTERS = [
79
79
  sourceType: "opencode-history",
80
80
  label: "OpenCode",
81
81
  run: ({ helpers, since, options, env }) =>
82
- helpers.importStructuredProvider("opencode", helpers.readOpenCodeSessions(env, options), since, options, env)
82
+ helpers.importStructuredProvider("opencode", helpers.readOpenCodeSessions(env, { ...options, since }), since, options, env)
83
83
  },
84
84
  {
85
85
  source: "aider",
package/src/importers.js CHANGED
@@ -455,6 +455,7 @@ function importCursorProvider(provider, since, options = {}, env = process.env)
455
455
  sourceType,
456
456
  title: session.title,
457
457
  composerId: cursorSessionComposerId(session) || undefined,
458
+ parentComposerId: session.parentComposerId || undefined,
458
459
  sharedRawFiles: cursorSessionUsesSharedRawFiles(sourceType),
459
460
  replaceSourcePathCopies: sourceType === "cursor-agent-transcripts"
460
461
  },
@@ -2967,6 +2968,26 @@ function sqliteTableExists(dbPath, tableName) {
2967
2968
  }
2968
2969
  }
2969
2970
 
2971
+ function sqliteTableColumns(dbPath, tableName) {
2972
+ try {
2973
+ return new Set(
2974
+ readSqliteJson(dbPath, `select name from pragma_table_info(${sqlQuote(tableName)})`, `${tableName} column check`)
2975
+ .map((row) => String(row.name || ""))
2976
+ .filter(Boolean)
2977
+ );
2978
+ } catch {
2979
+ return new Set();
2980
+ }
2981
+ }
2982
+
2983
+ function sqliteSelectMaybe(columns, tableAlias, columnName, outputName = columnName) {
2984
+ if (columns.has(columnName)) {
2985
+ const value = `${tableAlias}.${columnName}`;
2986
+ return outputName === columnName ? value : `${value} as ${outputName}`;
2987
+ }
2988
+ return `null as ${outputName}`;
2989
+ }
2990
+
2970
2991
  function codexStateDb(env = process.env) {
2971
2992
  return env.CODEX_STATE_DB || path.join(codexHome(env), "state_5.sqlite");
2972
2993
  }
@@ -4128,6 +4149,7 @@ const CURSOR_RAW_ASSISTANT_MERGE_MIN_SCORE = 32;
4128
4149
  const CURSOR_RAW_ASSISTANT_MERGE_MIN_OVERLAP = 2;
4129
4150
  const CURSOR_ABSOLUTE_PATH_RE = /(?:file:\/\/)?\/(?:Users|home|Volumes|private|tmp|var)\/[^\s"'`<>{}|]+/g;
4130
4151
  const SQLITE_QUERY_TIMEOUT_MS = 30 * 1000;
4152
+ const OPENCODE_SQLITE_BATCH_SIZE = 100;
4131
4153
 
4132
4154
  function readCursorRawSqliteSalvageSessionsFromDb(dbPath, options = {}) {
4133
4155
  const files = cursorRawSqliteFilesForDb(dbPath);
@@ -6523,7 +6545,18 @@ function readOpenCodeSessions(env = process.env, options = {}) {
6523
6545
  const sessions = [];
6524
6546
  reportDiscoveryProgress(options, { current: 0, total: dbs.length, message: "reading OpenCode SQLite stores" });
6525
6547
  for (let index = 0; index < dbs.length; index++) {
6526
- const dbSessions = readOpenCodeSqliteSessionsFromDb(dbs[index]);
6548
+ let dbSessions = [];
6549
+ try {
6550
+ dbSessions = readOpenCodeSqliteSessionsFromDb(dbs[index], options);
6551
+ } catch (error) {
6552
+ reportDiscoveryProgress(options, {
6553
+ current: index + 1,
6554
+ total: dbs.length,
6555
+ message: `SQLite skipped: ${error.message}`,
6556
+ path: dbs[index]
6557
+ });
6558
+ continue;
6559
+ }
6527
6560
  sessions.push(...dbSessions);
6528
6561
  reportDiscoveryProgress(options, {
6529
6562
  current: index + 1,
@@ -6635,12 +6668,14 @@ function openCodeMessageSessionIds(root) {
6635
6668
  .sort((a, b) => a.localeCompare(b));
6636
6669
  }
6637
6670
 
6638
- function readOpenCodeSqliteSessionsFromDb(dbPath) {
6671
+ function readOpenCodeSqliteSessionsFromDb(dbPath, options = {}) {
6639
6672
  if (!safeStat(dbPath)) return [];
6640
6673
  if (!sqliteTableExists(dbPath, "session") || !sqliteTableExists(dbPath, "message") || !sqliteTableExists(dbPath, "part")) return [];
6641
- const sessionRows = readOpenCodeSqliteSessionRows(dbPath);
6642
- const messageRows = readOpenCodeSqliteMessageRows(dbPath);
6643
- const partRows = readOpenCodeSqlitePartRows(dbPath);
6674
+ const sessionRows = readOpenCodeSqliteSessionRows(dbPath, options);
6675
+ if (!sessionRows.length) return [];
6676
+ const sessionIds = sessionRows.map((row) => row.id).filter(Boolean);
6677
+ const messageRows = sortOpenCodeSqliteRows(readOpenCodeSqliteMessageRows(dbPath, sessionIds), ["session_id", "time_created", "id"]);
6678
+ const partRows = sortOpenCodeSqliteRows(readOpenCodeSqlitePartRows(dbPath, sessionIds), ["session_id", "message_id", "time_created", "id"]);
6644
6679
  const messagesBySession = groupRowsBy(messageRows, "session_id");
6645
6680
  const partsByMessage = groupRowsBy(partRows, "message_id");
6646
6681
  const storageRoot = path.join(path.dirname(dbPath), "storage");
@@ -6688,37 +6723,134 @@ function readOpenCodeSqliteSessionsFromDb(dbPath) {
6688
6723
  return sessions;
6689
6724
  }
6690
6725
 
6691
- function readOpenCodeSqliteSessionRows(dbPath) {
6692
- return readSqliteJson(
6693
- dbPath,
6694
- [
6695
- "select s.id, s.project_id, s.parent_id, s.slug, s.directory, s.title, s.version,",
6696
- "s.share_url, s.time_created, s.time_updated, s.time_archived, s.workspace_id, s.path, s.agent, s.model,",
6697
- "p.worktree as project_worktree, p.name as project_name",
6698
- "from session s left join project p on p.id = s.project_id",
6699
- "where coalesce(s.time_archived, 0) = 0",
6700
- "order by s.time_updated desc, s.id"
6701
- ].join(" "),
6702
- "OpenCode SQLite sessions"
6703
- );
6704
- }
6705
-
6706
- function readOpenCodeSqliteMessageRows(dbPath) {
6707
- return readSqliteJson(
6726
+ function readOpenCodeSqliteSessionRows(dbPath, options = {}) {
6727
+ const sessionColumns = sqliteTableColumns(dbPath, "session");
6728
+ if (!sessionColumns.has("id")) return [];
6729
+ const projectColumns = sqliteTableExists(dbPath, "project") ? sqliteTableColumns(dbPath, "project") : new Set();
6730
+ const canJoinProject = sessionColumns.has("project_id") && projectColumns.has("id");
6731
+ const timestampExpr = openCodeSqliteSessionTimestampExpr(sessionColumns);
6732
+ const selects = [
6733
+ "s.id",
6734
+ sqliteSelectMaybe(sessionColumns, "s", "project_id"),
6735
+ sqliteSelectMaybe(sessionColumns, "s", "parent_id"),
6736
+ sqliteSelectMaybe(sessionColumns, "s", "slug"),
6737
+ sqliteSelectMaybe(sessionColumns, "s", "directory"),
6738
+ sqliteSelectMaybe(sessionColumns, "s", "title"),
6739
+ sqliteSelectMaybe(sessionColumns, "s", "version"),
6740
+ sqliteSelectMaybe(sessionColumns, "s", "share_url"),
6741
+ sqliteSelectMaybe(sessionColumns, "s", "time_created"),
6742
+ sqliteSelectMaybe(sessionColumns, "s", "time_updated"),
6743
+ sqliteSelectMaybe(sessionColumns, "s", "time_archived"),
6744
+ sqliteSelectMaybe(sessionColumns, "s", "workspace_id"),
6745
+ sqliteSelectMaybe(sessionColumns, "s", "path"),
6746
+ sqliteSelectMaybe(sessionColumns, "s", "agent"),
6747
+ sqliteSelectMaybe(sessionColumns, "s", "model"),
6748
+ canJoinProject && projectColumns.has("worktree") ? "p.worktree as project_worktree" : "null as project_worktree",
6749
+ canJoinProject && projectColumns.has("name") ? "p.name as project_name" : "null as project_name"
6750
+ ];
6751
+ const queryParts = [`select ${selects.join(", ")}`, "from session s"];
6752
+ if (canJoinProject) queryParts.push("left join project p on p.id = s.project_id");
6753
+ const where = [];
6754
+ if (sessionColumns.has("time_archived")) where.push("coalesce(s.time_archived, 0) = 0");
6755
+ const sinceCondition = openCodeSqliteSinceCondition(timestampExpr, options.since);
6756
+ if (sinceCondition) where.push(sinceCondition);
6757
+ if (where.length) queryParts.push(`where ${where.join(" and ")}`);
6758
+ const orderColumns = [];
6759
+ if (sessionColumns.has("time_updated")) orderColumns.push("s.time_updated desc");
6760
+ if (sessionColumns.has("time_created")) orderColumns.push("s.time_created desc");
6761
+ orderColumns.push("s.id");
6762
+ queryParts.push(`order by ${orderColumns.join(", ")}`);
6763
+ return readSqliteJson(dbPath, queryParts.join(" "), "OpenCode SQLite sessions");
6764
+ }
6765
+
6766
+ function openCodeSqliteSessionTimestampExpr(sessionColumns) {
6767
+ const candidates = ["time_updated", "time_created"].filter((column) => sessionColumns.has(column)).map((column) => `s.${column}`);
6768
+ if (!candidates.length) return "";
6769
+ return candidates.length === 1 ? candidates[0] : `coalesce(${candidates.join(", ")})`;
6770
+ }
6771
+
6772
+ function openCodeSqliteSinceCondition(timestampExpr, since) {
6773
+ if (!timestampExpr || !since) return "";
6774
+ const sinceTime = since instanceof Date ? since.getTime() : Date.parse(since);
6775
+ if (!Number.isFinite(sinceTime)) return "";
6776
+ const sinceMs = Math.floor(sinceTime);
6777
+ const sinceSeconds = Math.floor(sinceTime / 1000);
6778
+ const sinceIso = new Date(sinceTime).toISOString();
6779
+ return `((${timestampExpr} is not null) and ((abs(${timestampExpr}) > 1000000000000 and ${timestampExpr} >= ${sinceMs}) or (abs(${timestampExpr}) <= 1000000000000 and ${timestampExpr} >= ${sinceSeconds}) or (typeof(${timestampExpr}) = 'text' and datetime(${timestampExpr}) >= datetime(${sqlQuote(sinceIso)}))))`;
6780
+ }
6781
+
6782
+ function readOpenCodeSqliteMessageRows(dbPath, sessionIds = []) {
6783
+ const columns = sqliteTableColumns(dbPath, "message");
6784
+ if (!columns.has("id") || !columns.has("session_id")) return [];
6785
+ const selects = [
6786
+ "id",
6787
+ "session_id",
6788
+ sqliteSelectMaybe(columns, "message", "time_created"),
6789
+ sqliteSelectMaybe(columns, "message", "time_updated"),
6790
+ sqliteSelectMaybe(columns, "message", "data")
6791
+ ];
6792
+ return readOpenCodeSqliteRowsForSessionIds(
6708
6793
  dbPath,
6709
- "select id, session_id, time_created, time_updated, data from message order by session_id, time_created, id",
6794
+ "message",
6795
+ selects,
6796
+ sessionIds,
6710
6797
  "OpenCode SQLite messages"
6711
6798
  );
6712
6799
  }
6713
6800
 
6714
- function readOpenCodeSqlitePartRows(dbPath) {
6715
- return readSqliteJson(
6801
+ function readOpenCodeSqlitePartRows(dbPath, sessionIds = []) {
6802
+ const columns = sqliteTableColumns(dbPath, "part");
6803
+ if (!columns.has("id") || !columns.has("message_id") || !columns.has("session_id")) return [];
6804
+ const selects = [
6805
+ "id",
6806
+ "message_id",
6807
+ "session_id",
6808
+ sqliteSelectMaybe(columns, "part", "time_created"),
6809
+ sqliteSelectMaybe(columns, "part", "time_updated"),
6810
+ sqliteSelectMaybe(columns, "part", "data")
6811
+ ];
6812
+ return readOpenCodeSqliteRowsForSessionIds(
6716
6813
  dbPath,
6717
- "select id, message_id, session_id, time_created, time_updated, data from part order by session_id, message_id, time_created, id",
6814
+ "part",
6815
+ selects,
6816
+ sessionIds,
6718
6817
  "OpenCode SQLite parts"
6719
6818
  );
6720
6819
  }
6721
6820
 
6821
+ function readOpenCodeSqliteRowsForSessionIds(dbPath, tableName, selects, sessionIds, label) {
6822
+ const ids = [...new Set((sessionIds || []).map((id) => String(id || "")).filter(Boolean))];
6823
+ if (!ids.length) return [];
6824
+ const rows = [];
6825
+ for (let index = 0; index < ids.length; index += OPENCODE_SQLITE_BATCH_SIZE) {
6826
+ const batch = ids.slice(index, index + OPENCODE_SQLITE_BATCH_SIZE);
6827
+ const query = [
6828
+ `select ${selects.join(", ")}`,
6829
+ `from ${tableName}`,
6830
+ `where session_id in (${batch.map(sqlQuote).join(",")})`
6831
+ ].join(" ");
6832
+ rows.push(...readSqliteJson(dbPath, query, label));
6833
+ }
6834
+ return rows;
6835
+ }
6836
+
6837
+ function sortOpenCodeSqliteRows(rows, keys) {
6838
+ return rows.sort((left, right) => {
6839
+ for (const key of keys) {
6840
+ const result = compareOpenCodeSqliteValues(left?.[key], right?.[key]);
6841
+ if (result) return result;
6842
+ }
6843
+ return 0;
6844
+ });
6845
+ }
6846
+
6847
+ function compareOpenCodeSqliteValues(left, right) {
6848
+ const leftNumber = Number(left);
6849
+ const rightNumber = Number(right);
6850
+ if (Number.isFinite(leftNumber) && Number.isFinite(rightNumber) && leftNumber !== rightNumber) return leftNumber - rightNumber;
6851
+ return String(left || "").localeCompare(String(right || ""));
6852
+ }
6853
+
6722
6854
  function openCodeSqliteMessagesFromRow(row, partRows, index) {
6723
6855
  const data = parseJsonObject(row.data);
6724
6856
  const parts = (partRows || []).map((partRow) => ({
package/src/search.js CHANGED
@@ -17,6 +17,8 @@ const SQLITE_BUILD_BATCH_SIZE = 100;
17
17
  const RIPGREP_SEARCH_TIMEOUT_MS = 8000;
18
18
  const RIPGREP_BATCH_FILE_COUNT = 200;
19
19
  const MARKDOWN_MATCHES_PER_FILE = 3;
20
+ const FTS_SEARCH_BATCH_SIZE = 250;
21
+ const FTS_MAX_SCAN_ROWS = 5000;
20
22
  const _indexCache = {
21
23
  path: "",
22
24
  mtimeMs: 0,
@@ -671,33 +673,29 @@ function searchFtsSessions(query, queryTokens, context, env = process.env) {
671
673
  if (!ftsIndexAvailable(env, { noStaleCheck: Boolean(context.options.noRebuild || context.options.allowStaleFts) })) return null;
672
674
  const matchQuery = ftsMatchQuery(query);
673
675
  if (!matchQuery) return [];
674
- const candidateLimit = Math.max(context.limit * 8, 80);
675
- const rows = sqliteJson(
676
- ftsPath,
677
- [
678
- "SELECT",
679
- " d.doc_id, d.session_id, d.provider, d.source_type, d.repo_canonical, d.repo_display,",
680
- " d.scope_canonical, d.cwd, d.title, d.started_at, d.occurred_at, d.role,",
681
- " d.event_id, d.event_kind, d.message_index, d.path, d.matched_text,",
682
- " snippet(docs_fts, 0, '', '', '...', 32) AS excerpt,",
683
- " bm25(docs_fts) AS rank",
684
- "FROM docs_fts",
685
- "JOIN docs d ON d.rowid = docs_fts.rowid",
686
- `WHERE docs_fts MATCH ${sqliteString(matchQuery)}`,
687
- "ORDER BY rank ASC, d.occurred_at DESC, d.started_at DESC",
688
- `LIMIT ${candidateLimit};`
689
- ].join("\n")
690
- );
691
- if (!rows) return null;
692
676
  const bySession = new Map();
693
- for (const row of rows) {
694
- const doc = ftsRowToDoc(row);
695
- if (!matchesSessionFilter(doc, { ...context.filter, includeWebChats: context.includeWebChats, since: context.since })) continue;
696
- if (!context.options.repo && context.repo && doc.repoCanonical === context.repo) {
697
- row.rank = Number(row.rank || 0) - 0.05;
677
+ const batchSize = Math.max(context.limit * 20, FTS_SEARCH_BATCH_SIZE);
678
+ const maxScanRows = Math.max(batchSize, Math.min(FTS_MAX_SCAN_ROWS, context.limit * 500));
679
+ let offset = 0;
680
+ while (offset < maxScanRows && bySession.size < context.limit) {
681
+ const rows = ftsSearchRows(ftsPath, matchQuery, {
682
+ limit: Math.min(batchSize, maxScanRows - offset),
683
+ offset,
684
+ context
685
+ });
686
+ if (!rows) return null;
687
+ if (!rows.length) break;
688
+ offset += rows.length;
689
+ for (const row of rows) {
690
+ const doc = ftsRowToDoc(row);
691
+ if (!matchesSessionFilter(doc, { ...context.filter, includeWebChats: context.includeWebChats, since: context.since })) continue;
692
+ if (!context.options.repo && context.repo && doc.repoCanonical === context.repo) {
693
+ row.rank = Number(row.rank || 0) - 0.05;
694
+ }
695
+ if (!bySession.has(doc.sessionId)) bySession.set(doc.sessionId, { doc, row });
696
+ if (bySession.size >= context.limit) break;
698
697
  }
699
- if (!bySession.has(doc.sessionId)) bySession.set(doc.sessionId, { doc, row });
700
- if (bySession.size >= context.limit) break;
698
+ if (rows.length < batchSize) break;
701
699
  }
702
700
  return [...bySession.values()].slice(0, context.limit).map(({ doc, row }) => ({
703
701
  session_id: doc.sessionId,
@@ -720,6 +718,34 @@ function searchFtsSessions(query, queryTokens, context, env = process.env) {
720
718
  }));
721
719
  }
722
720
 
721
+ function ftsSearchRows(ftsPath, matchQuery, options) {
722
+ const clauses = [`docs_fts MATCH ${sqliteString(matchQuery)}`];
723
+ const filter = options.context?.filter || {};
724
+ if (filter.provider) clauses.push(`d.provider = ${sqliteString(filter.provider)}`);
725
+ const sourceTypes = filter.sourceTypes?.length ? filter.sourceTypes : filter.sourceType ? [filter.sourceType] : [];
726
+ if (sourceTypes.length) clauses.push(`d.source_type IN (${sourceTypes.map(sqliteString).join(", ")})`);
727
+ if (options.context?.since) {
728
+ const since = sqliteString(options.context.since.toISOString());
729
+ clauses.push(`(d.started_at >= ${since} OR (d.started_at = '' AND d.occurred_at >= ${since}))`);
730
+ }
731
+ return sqliteJson(
732
+ ftsPath,
733
+ [
734
+ "SELECT",
735
+ " d.doc_id, d.session_id, d.provider, d.source_type, d.repo_canonical, d.repo_display,",
736
+ " d.scope_canonical, d.cwd, d.title, d.started_at, d.occurred_at, d.role,",
737
+ " d.event_id, d.event_kind, d.message_index, d.path, d.matched_text,",
738
+ " snippet(docs_fts, 0, '', '', '...', 32) AS excerpt,",
739
+ " bm25(docs_fts) AS rank",
740
+ "FROM docs_fts",
741
+ "JOIN docs d ON d.rowid = docs_fts.rowid",
742
+ `WHERE ${clauses.join(" AND ")}`,
743
+ "ORDER BY rank ASC, d.occurred_at DESC, d.started_at DESC",
744
+ `LIMIT ${Math.max(1, Number(options.limit) || FTS_SEARCH_BATCH_SIZE)} OFFSET ${Math.max(0, Number(options.offset) || 0)};`
745
+ ].join("\n")
746
+ );
747
+ }
748
+
723
749
  function ftsRowToDoc(row) {
724
750
  return {
725
751
  id: row.doc_id || "",
package/agentlog-spec.md DELETED
@@ -1,558 +0,0 @@
1
- # agentlog — spec v0.2
2
-
3
- A weekend-buildable archive and recall layer for agent coding sessions across Codex, ChatGPT exports, Claude, Gemini, Antigravity, Devin, Cursor, and Windsurf trajectory exports. Local-first, optionally cloud-backed via any S3-compatible storage. Web chats from ChatGPT and Claude.ai are importable via their official export flows. Windsurf's encrypted local Cascade cache remains disabled, but downloaded trajectory Markdown is importable.
4
-
5
- ## What it does
6
-
7
- Agentlog optimizes for four product constraints:
8
-
9
- 1. **Preserve the source history as-is.** Import copies raw source files into the archive before normalizing them, so agentlog can re-parse sessions as provider formats change.
10
- 2. **Create a durable recall substrate.** Readable markdown plus canonical event JSONL is the source of truth for `/recall` skills, MCP tools, coding agents, and standardization across providers.
11
- 3. **Show full histories clearly.** The CLI and local web viewer must make complete conversation histories easy to browse, search, export, and inspect without hiding tool calls or context.
12
- 4. **Sync across machines.** Local storage remains canonical, but every archive object should be syncable to S3-compatible storage or another cloud target for backup, restore, and multi-device recall.
13
-
14
- Three layers, separable, in priority order:
15
-
16
- 1. **Archive** — every agent session is captured as readable markdown plus raw transcripts, redacted at ingest, written to S3-compatible storage keyed by canonical repo identity.
17
- 2. **Recall** — an MCP server exposing one tool, `search_past_sessions(query, repo, limit)`, that searches canonical event JSONL first and falls back to markdown/transcript retrieval. Available to any MCP-capable agent and wrapped by installable agent commands/skills.
18
- 3. **Notify** — optional `/buzz`-style Slack skill that posts session summaries on completion. Cut from v0; ships separately as `agentlog-slack`.
19
-
20
- ## Architecture
21
-
22
- ```
23
- ┌─────────────────────────────────────────────────┐
24
- │ Agents (Claude Code, Codex, Devin, Cursor) │
25
- └──────────────┬──────────────────────────────────┘
26
-
27
- ┌──────┴───────┐
28
- │ │
29
- OTel push File tail / poll
30
- (Claude Code) (Codex JSONL,
31
- (Cowork) Cursor SQLite)
32
- │ │
33
- └──────┬───────┘
34
-
35
- ┌───────────────────────┐ ┌──────────────────┐
36
- │ agentlog supervisor │◄────────│ Web chat import │
37
- │ ├─ collector │ │ (Claude.ai, │
38
- │ ├─ openobserve │ │ ChatGPT export │
39
- │ ├─ codex-watcher │ │ files) │
40
- │ ├─ cursor-poller │ └──────────────────┘
41
- │ ├─ indexer │
42
- │ └─ importer │
43
- └──────────┬────────────┘
44
-
45
- ┌───────────────────────┐
46
- │ S3-compatible bucket │
47
- │ (R2 / S3 / B2 / │
48
- │ MinIO / etc) │
49
- └──────────┬────────────┘
50
-
51
- ┌───────────────────────┐
52
- │ agentlog-recall MCP │ ┌─────────────────────┐
53
- │ (stdio, on-demand) │ │ agentlog history │
54
- │ search_past_sessions │ │ (cchv-based) │
55
- └───────────────────────┘ └─────────────────────┘
56
- ```
57
-
58
- ## Process model
59
-
60
- **One supervisor, several workers.** The supervisor is the only process the user thinks about. It manages child processes, handles restarts with backoff, exposes a control socket at `~/.agentlog/control.sock`, and unifies logging.
61
-
62
- **Always-on workers (run inside the supervisor):**
63
-
64
- - OTel collector — receives Claude Code's OTLP pushes, ~40MB RAM
65
- - OpenObserve — local-mode storage and query, ~100MB RAM (skipped in remote/team mode)
66
- - Codex watcher — `fsnotify` on `~/.codex/sessions/`, ~10MB RAM, idle when no Codex activity
67
- - Devin/Cursor pollers — SQLite/transcript scans for configured local sources
68
- - Indexer — runs every 10 minutes if there are unindexed sessions; pauses on battery
69
- - Importer — runs at low priority during backfill operations; idle otherwise
70
-
71
- **On-demand workers (launched by their caller):**
72
-
73
- - `agentlog-recall` MCP server — spawned by the agent client over stdio when a session starts; killed when the session ends. No always-on cost.
74
-
75
- **Total always-on footprint, solo install:** ~150MB RAM, near-zero idle CPU. Comparable to a menu-bar chat app.
76
-
77
- **Team mode inverts this:** developer machines run only the collector and watchers (~50MB total) and forward OTLP to a team server that runs OpenObserve, the indexer, and the recall HTTP endpoint.
78
-
79
- ## Auto-start at login
80
-
81
- `init` offers to install one platform-native login item for the local watcher. Default: yes, with explicit opt-out.
82
-
83
- **Prompt during init:**
84
-
85
- ```
86
- Background Watcher
87
-
88
- The supervisor is agentlog's local watcher: it imports new history,
89
- refreshes indexes, and runs scheduled cloud sync.
90
- Leave this checked to install one login item. Uncheck it for manual-only mode.
91
-
92
- 1 [x] Start watcher at login
93
- installs one user-level login item; unchecked means no continuous
94
- watching until you run agentlog watcher start
95
- ```
96
-
97
- **Per-platform implementation:**
98
-
99
- - **macOS:** `~/Library/LaunchAgents/com.agentlog.supervisor.plist`. `RunAtLoad: true`, `KeepAlive: { SuccessfulExit: false }` (restart on crash, not on clean shutdown). Logs to `~/Library/Logs/agentlog/`. Loaded with `launchctl load -w`.
100
- - **Linux:** systemd user unit at `~/.config/systemd/user/agentlog.service`. `Type=simple`, `Restart=on-failure`, `RestartSec=5`. Enabled with `systemctl --user enable --now`. Init detects whether `loginctl enable-linger` is needed and prompts separately: "Keep agentlog running when you're logged out? [y/N]" — default no.
101
- - **Windows:** Scheduled Task triggered at logon (`schtasks /create /tn "agentlog" /tr ... /sc onlogon`). Service-based install is a v1 enhancement.
102
-
103
- **Critical detail:** the launch agent runs `agentlog watcher start --foreground`, not `agentlog watcher start`. Foreground mode keeps the OS supervisor (launchd/systemd/Task Scheduler) as the parent — detaching breaks process tracking and crash restart.
104
-
105
- **Lifecycle commands:**
106
-
107
- ```
108
- agentlog watcher login enable # writes the launch agent/unit/task
109
- agentlog watcher login disable # removes auto-start, keeps agentlog installed
110
- agentlog watcher login status # shows current state
111
- agentlog uninstall [--keep-data]
112
- ```
113
-
114
- `uninstall` is exhaustively tested: removes the launch agent, the config, the binaries, optionally the data (with confirmation). For a tool handling sensitive data, "remove all traces" must actually work.
115
-
116
- ## Resource awareness
117
-
118
- The supervisor respects laptop realities:
119
-
120
- - **Power state.** On battery, indexer interval drops from 10 to 30 minutes; compaction skipped; speculative pre-fetching disabled.
121
- - **Network state.** If storage backend is remote and we're offline, writes spool to `~/.agentlog/spool/` and flush on reconnect. The OTLP collector already does this for spans; the same pattern extends to S3 writes.
122
- - **Sleep/wake.** On wake, supervisor verifies child health and restarts any that died during sleep.
123
- - **Cursor presence.** Cursor poller checks `pgrep -x Cursor` before each poll cycle; sleeps entirely when Cursor is not running.
124
-
125
- These are v0 features, not v1, because they're the difference between a tool people keep installed and a tool people uninstall after a week.
126
-
127
- ## Storage layer
128
-
129
- **Backend: anything S3-compatible.** Backend choice is a config matter, not a code matter. Supported in `init`:
130
-
131
- - **Local** (default first-run) — `~/.agentlog/data/`, no cloud account
132
- - **R2** (recommended for personal/small team) — Cloudflare, no egress fees, free 10GB tier
133
- - **S3** (recommended for AWS-native teams)
134
- - **Custom endpoint** — covers B2, MinIO, Wasabi, Tigris, Hetzner, etc.
135
-
136
- **Bucket layout:**
137
-
138
- ```
139
- s3://<bucket>/agentlog/
140
- devices/
141
- <device-name>/
142
- sessions/
143
- repo=<canonical-repo-key>/
144
- provider=<claude_code|codex|cursor|devin>/
145
- year=2026/month=04/day=26/
146
- session=<session_id>.conversation.md
147
- session=<session_id>.transcript.jsonl
148
- session=<session_id>.metadata.json
149
- session=<session_id>.events.jsonl
150
- scope=<claude-web|chatgpt>/
151
- year=2026/month=04/day=26/
152
- session=<session_id>.conversation.md
153
- session=<session_id>.transcript.jsonl
154
- session=<session_id>.metadata.json
155
- session=<session_id>.events.jsonl
156
- indexes/
157
- bm25/... # local keyword/BM25-style index over events/transcripts
158
- snapshots/
159
- 20260504T173000Z/
160
- <device-name>/
161
- sessions/...
162
- ```
163
-
164
- Markdown conversations are the primary human-readable representation because
165
- agents and humans can inspect them with ordinary filesystem tools. Raw
166
- transcripts are stored alongside as immutable JSONL for provenance and
167
- re-indexing. `events.jsonl` is the canonical machine-readable recall substrate:
168
- one provider-independent JSONL event stream with `session.started`,
169
- `prompt.submitted`, `response.generated`, `tool.called`, and `tool.completed`.
170
- Structured analytics artifacts such as Parquet/OTel spans are optional siblings,
171
- not the default recall substrate.
172
-
173
- Every importer has a centralized semantic parser version in
174
- `src/parser-versions.js`. Parser versions are included in archive metadata and
175
- import fingerprints. The first npm release uses `1.0.0` as the baseline for
176
- every source type. After release, when parser output changes for the same raw
177
- input, the source-type version must be bumped in the same change so stale
178
- archives can be replaced.
179
-
180
- **Migration:** `agentlog sync configure` records the remote target through an interactive picker in terminals, while non-interactive scripts can still pass `--target`, `--endpoint`, and credentials. Choosing an existing remote opens useful next actions instead of only echoing the current config. `agentlog sync` uploads the same markdown-primary object layout to any S3-compatible target under `agentlog/devices/<device-name>/...`; terminal runs pick a remote, preview the upload-only plan, and confirm before writing. Local→R2 is a one-shot upload and then an incremental supervisor upload. Normal sync does not delete remote objects. `agentlog sync replace` is the explicit repair path: it previews the selected remote, requires typed confirmation, deletes only the current device namespace, and then uploads the current local archive. `agentlog sync wipe` is delete-only, asks for remote and scope, previews the chosen target and prefix, requires typed confirmation, and is followed by `agentlog sync` when the user wants to rebuild a remote copy from local state. Wipe scopes include the current device namespace, one snapshot, all snapshots, the configured prefix, and the bucket/root. Receive-only and two-way sync should read other device namespaces and merge normalized archive metadata without interpreting absence on one device as a delete. `agentlog sync snapshot` lists existing snapshots, asks for a name, confirms, and writes redundant point-in-time copies under `agentlog/snapshots/<timestamp>/<device-name>/...`.
181
-
182
- ## Repo keying
183
-
184
- Every span and record gets `agentlog.repo.canonical`, derived in this order:
185
-
186
- 1. `git config --get remote.origin.url` from the session's `cwd`, normalized: lowercase host, strip protocol, strip `.git`, strip trailing slash. `git@github.com:User/Repo.git` → `github.com/user/repo`.
187
- 2. First-commit SHA fallback: `git rev-list --max-parents=0 HEAD` → `firstcommit:<sha>`.
188
- 3. Non-git fallback: content hash of cwd path normalized to home-relative → `path:<sha256>`.
189
-
190
- Repo-level override at `.agentlog.yaml`:
191
-
192
- ```yaml
193
- canonical_repo: github.com/myorg/private-name
194
- aliases:
195
- - github.com/myorg/old-name
196
- ```
197
-
198
- Web chat imports use `agentlog.scope.canonical` instead (e.g. `claude-web`, `chatgpt`) — see Web chat import section.
199
-
200
- ## Redaction (at ingest, not at query)
201
-
202
- Three layers in the collector before anything hits storage:
203
-
204
- 1. **Built-in patterns** (always on):
205
- - AWS keys, OpenAI/Anthropic keys, GitHub tokens, Slack tokens
206
- - JWT-shaped strings, private key blocks
207
- - High-entropy strings >32 chars in `KEY=value` shapes
208
-
209
- 2. **Env-var value scrubbing.** If `.env` files are read in a session, configured variable values are scrubbed wherever they appear in transcripts.
210
-
211
- 3. **User-defined patterns** in `~/.agentlog/redaction.yaml`:
212
-
213
- ```yaml
214
- patterns:
215
- - name: internal_api
216
- regex: 'https://[a-z]+\.internal\.acme\.com/[^\s]+'
217
- env_vars: [API_KEY, DATABASE_URL, STRIPE_SECRET]
218
- allowlist_repos:
219
- - github.com/acme/public-docs
220
- ```
221
-
222
- Each session gets a `redaction_summary` span: counts by category, no content. Users can audit "did this leak anything" without seeing what leaked.
223
-
224
- `agentlog show <session-id> --unredacted` re-renders un-redacted from local cache. Local-only — never works on remote/team archives.
225
-
226
- **Honesty about limits:** pattern-based redaction catches credentials. It does not catch personal/sensitive content (medical, legal, financial conversations). This matters especially for web chat imports, which is why those default to local-only storage.
227
-
228
- ## Per-provider capture
229
-
230
- ### Claude Code & Claude Cowork
231
-
232
- Native OTel. `init` merges into `~/.claude/settings.json`:
233
-
234
- ```json
235
- {
236
- "env": {
237
- "CLAUDE_CODE_ENABLE_TELEMETRY": "1",
238
- "OTEL_METRICS_EXPORTER": "otlp",
239
- "OTEL_LOGS_EXPORTER": "otlp",
240
- "OTEL_EXPORTER_OTLP_PROTOCOL": "http/json",
241
- "OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:4318",
242
- "OTEL_LOG_USER_PROMPTS": "1",
243
- "OTEL_RESOURCE_ATTRIBUTES": "service.name=claude-code,agentlog.user=<user>"
244
- }
245
- }
246
- ```
247
-
248
- ### Codex CLI
249
-
250
- `fsnotify` watcher on `~/.codex/sessions/YYYY/MM/DD/`. Decompresses `.jsonl.zst`, normalizes to OTel `gen_ai.*` spans, posts to local collector. State (file offsets) in `~/.agentlog/state/codex-cursor.db` SQLite. ~200 lines of Go. Deleted when OpenAI ships native OTel.
251
-
252
- ### Cursor
253
-
254
- SQLite/transcript poller, 30-second interval, only active when `Cursor` process detected. Reads older `~/Library/Application Support/Cursor/User/workspaceStorage/*/state.vscdb` stores and newer `~/.cursor/projects/<project>/agent-transcripts/` JSON/JSONL transcripts on macOS/Linux. macOS Full Disk Access prompt documented with screenshots.
255
-
256
- ### Devin
257
-
258
- SQLite importer for Devin for Terminal. Reads
259
- `~/.local/share/devin/cli/sessions.db`, reconstructs the visible message branch
260
- from `sessions.main_chain_id` plus `message_nodes.parent_node_id`, skips Devin's
261
- injected context messages, and archives user, assistant, and tool messages under
262
- provider `devin`. `AGENTLOG_DEVIN_SESSIONS_DB` can point at an alternate
263
- database.
264
-
265
- ### Web chat import (Claude.ai, ChatGPT)
266
-
267
- Web chats don't have a real-time hook — Anthropic and OpenAI provide periodic export files only. agentlog imports these as one-shot operations per export.
268
-
269
- **Flow:**
270
-
271
- 1. User exports from Claude.ai (Settings → Privacy → Export) or ChatGPT (Settings → Data Controls → Export)
272
- 2. Email arrives with download link
273
- 3. User runs: `agentlog import claude-web --file <downloaded file>` or `agentlog import chatgpt --file <downloaded file>`
274
-
275
- **Storage scope:** local-only by default, **even if the agentlog instance is configured for a team backend.** Web chats often contain personal content; opt-in required to share with team. Override with `--scope team`.
276
-
277
- **Repo keying:** web chats are stored under `scope=claude-web` or `scope=chatgpt` rather than a repo key. Heuristic repo inference (matching code blocks and error messages against known repos) deferred to v1.
278
-
279
- **Recall behavior:** excluded from agent-initiated `search_past_sessions` calls by default. Included in human-initiated `agentlog history` searches. Override via `--include-web-chats` flag on recall queries.
280
-
281
- **Frequency:** designed for periodic re-import, not continuous capture. Realistic cadence is "monthly or when the user remembers." Detection of new exports in `~/Downloads/` and prompting is a v1 enhancement.
282
-
283
- **Confirmation prompt for team-configured installs:**
284
-
285
- ```
286
- $ agentlog import claude-web --file ...
287
- Storage backend: team (s3://acme-agentlog/)
288
- Web chats often contain personal content. By default they'll be
289
- stored only in your local archive, not the team archive.
290
-
291
- 1) Local only (recommended)
292
- 2) Team archive (your conversations will be visible to teammates)
293
- 3) Cancel
294
- ```
295
-
296
- ## Importing existing CLI history
297
-
298
- `init` scans for existing CLI conversations and offers to import them. The default scope is "last 30 days" — recent enough to be useful for recall, narrow enough to avoid surprises in archives users may have forgotten the contents of.
299
-
300
- **Discovery during init:**
301
-
302
- ```
303
- Scanning for existing conversations...
304
- ✓ Codex CLI: 89 sessions across 12 projects (oldest: 2025-09-15)
305
- ✓ Codex Desktop: 14 sessions across 3 projects (oldest: 2026-02-01)
306
- ✓ Claude Code CLI: 247 sessions across 18 projects (oldest: 2025-11-03)
307
- ✓ Claude Code Desktop: 4 sessions (oldest: 2026-02-04)
308
- ✓ Claude Workspace: 7 sessions (oldest: 2026-02-04)
309
- ✓ Gemini CLI: 2 sessions (oldest: 2026-03-01)
310
- ✓ Antigravity: 2 sessions (oldest: 2025-11-19)
311
- ✓ Devin CLI: 3 sessions (oldest: 2026-04-28)
312
- ✓ Cursor: 31 sessions across 4 workspaces (oldest: 2026-01-02)
313
-
314
- Import existing history?
315
- 1) Last 30 days (default)
316
- 2) Everything
317
- 3) Choose specific repos
318
- 4) Skip for now
319
- ```
320
-
321
- Import runs as a background worker at lower priority than live ingestion. Progress visible via `agentlog import status`. Idempotent on re-run (tracks imported session IDs in state DB). Sessions whose `cwd` no longer exists fall back to path-hash repo keying.
322
-
323
- **Standalone command:**
324
-
325
- ```
326
- agentlog import [--source codex-cli|codex-desktop|claude|claude-code-desktop|claude-workspace|gemini-cli|antigravity|devin-cli|cursor|all]
327
- [--since 30d|all]
328
- [--repos <list>]
329
- [--dry-run]
330
- ```
331
-
332
- `--dry-run` shows what would be imported without doing it.
333
-
334
- **Web chat import is a separate command** — it requires a file argument and has different default storage scope. Not part of the init flow because exports take time to generate.
335
-
336
- ## Collector
337
-
338
- A single Go binary wrapping the upstream OTel collector with three custom processors:
339
-
340
- 1. `repokeyprocessor` — derives canonical repo key from `cwd`, or scope key for web chats
341
- 2. `redactionprocessor` — runs the three redaction layers
342
- 3. `agentnormalizer` — for file-tail providers and import sources, converts ingested events into OTel spans matching `gen_ai.*` semantic conventions
343
-
344
- Exporters: OTLP→OpenObserve for spans/metrics/logs; direct `s3exporter` for raw transcripts. Both share S3 credentials.
345
-
346
- ## Recall layer
347
-
348
- Separate binary, `agentlog-recall`. Spawned by agent clients over stdio (preferred) or run as HTTP server for team mode.
349
-
350
- **One MCP tool:**
351
-
352
- ```
353
- search_past_sessions(query: string, repo?: string, limit?: int = 10,
354
- include_web_chats?: bool = false)
355
- → list of message excerpts with session links
356
- ```
357
-
358
- **Recall pipeline:**
359
-
360
- - Builds a local keyword/BM25-style index over `events.jsonl` when present
361
- - Indexes prompt, response, tool-call, and tool-result event text independently
362
- - Aggregates event hits back to sessions for CLI/MCP compatibility
363
- - Returns optional `event_id`, `event_kind`, `message_index`, and matched text
364
- - Falls back to transcript/markdown search for legacy archives without events
365
-
366
- **Retrieval:** event-first over canonical JSONL. `repo` parameter is a hard filter.
367
- Without it, results are weighted toward the calling agent's current `cwd` repo.
368
- Web chats are excluded unless `include_web_chats=true`.
369
-
370
- **No memory promotion in v0.** Raw evidence with good retrieval beats lossy summarization. Add summarization in v1 only if v0 retrieval proves insufficient.
371
-
372
- **Adding to agents:**
373
-
374
- ```
375
- agentlog integrations add-to claude # writes ~/.claude/mcp.json, /commands/recall.md, and /skills/agentlog-recall/SKILL.md
376
- agentlog integrations add-to cursor # writes ~/.cursor/mcp.json
377
- agentlog integrations add-to codex # writes ~/.codex/config.toml
378
- ```
379
-
380
- Generated recall commands and skills should let the agent choose the first
381
- `agentlog history` query from the user's request. They should prefer concise,
382
- distinctive search terms over blindly passing the full `/recall` argument string
383
- through to the CLI. Skill-style files should include a concise command table,
384
- workflow, query-selection guidance, archive/filter hints, important rules, and
385
- troubleshooting. Archive hints should note that sessions live under
386
- `~/.agentlog/data/agentlog/sessions/repo=<repo-or-path-key>/provider=<provider>/...`,
387
- git repos use canonical keys like `github.com/org/repo`, non-git directories may
388
- use stable `path:<hash>` keys, and `agentlog history --repo "<repo-or-path>"`
389
- matches canonical repo keys, local `cwd`, display labels, web scopes, and path
390
- fragments.
391
-
392
- ## History viewer
393
-
394
- v0 ships a dependency-free local viewer behind `agentlog web`. It lists sessions in a repo tree sorted by last updated time, pages large folders with a load-more control, searches the same event-first recall index, filters by repo/provider/date, and opens full conversations through the CLI API. The static viewer follows shadcn/ui-style tokens and compact button/input/select/sidebar patterns without requiring a frontend build step. Stable `path:<hash>` keys remain valid archive identifiers for folders without git identity, but the viewer displays the local folder path. The transcript pane defaults to readable chat bubbles for user, assistant, system, and tool messages, with a markdown toggle for the canonical archive file. Tool rendering reads canonical events or normalized metadata first, uses category/icon/target fields for consistent Bash/edit/read/search/web/task/skill/MCP cards, and uses raw text patterns only for legacy archives.
395
-
396
- **Commands:**
397
-
398
- ```
399
- agentlog history # native app pointed at archive
400
- agentlog web # web UI on localhost:7824
401
- agentlog history "query" --provider codex-cli
402
- agentlog history --repo github.com/org/repo
403
- agentlog history --since 7d
404
- agentlog history --include-web-chats
405
- agentlog show <session-id>
406
- ```
407
-
408
- The viewer's search hits the same retrieval endpoint as the recall MCP server — humans and agents see the same world (with the human-vs-agent default scope difference for web chats).
409
-
410
- **For headless/server contexts** (SSH'd into a dev box), `--web` mode serves the UI on a local port with a bearer-token-in-URL auth pattern.
411
-
412
- **Team mode** serves cchv as a web service at the same endpoint as the OTLP collector, gated by the same auth. v0 team mode: "everyone sees everything," documented as such. Per-user filtering and permissions in v1.
413
-
414
- ## CLI surface
415
-
416
- Complete user-facing commands:
417
-
418
- ```
419
- # Setup and lifecycle
420
- agentlog init [--storage local|r2|s3|custom] [--remote URL]
421
- agentlog watcher start [--foreground]
422
- agentlog watcher stop
423
- agentlog watcher logs [--follow]
424
- agentlog watcher login <enable|disable|status>
425
- agentlog status
426
- agentlog config <show|path|get|set|setup|sources> [args]
427
- agentlog sync configure
428
- agentlog sync [--endpoint <url>] [--bucket <name>] [--access-key-id <id>] [--secret-access-key <key>] [--prefix agentlog] [--yes|--dry-run]
429
- agentlog sync snapshot [--name <label>] [--yes|--dry-run]
430
- agentlog sync replace
431
- agentlog sync wipe [--scope device|snapshot|snapshots|prefix|bucket] [--snapshot-name <name>] [--yes|--dry-run]
432
- agentlog doctor [--json]
433
- agentlog uninstall [--keep-data]
434
-
435
- # Capture management
436
- agentlog show <session-id> --unredacted
437
- agentlog redact reapply
438
- agentlog index <pause|resume|status>
439
-
440
- # Import
441
- agentlog import [--source codex-cli|codex-desktop|claude|claude-code-desktop|claude-workspace|claude-sdk|gemini-cli|antigravity|devin-cli|cursor|all] [--since 30d|all] [--repos <list>] [--dry-run]
442
- agentlog import claude-web --file <path> [--scope local|team]
443
- agentlog import chatgpt --file <path> [--scope local|team]
444
- agentlog import status
445
-
446
- # Recall
447
- agentlog mcp serve
448
- agentlog integrations add-to <codex|claude|gemini|antigravity|cursor>
449
- agentlog integrations recall [target]
450
- agentlog index rebuild
451
-
452
- # Viewing
453
- agentlog history [query] [--repo <repo>] [--provider <provider>] [--since <duration>] [--include-web-chats]
454
- agentlog web [--port <port>] [--no-open]
455
- agentlog show <session-id> [--json|--path|--open]
456
-
457
- # Team mode
458
- agentlog mcp serve
459
- ```
460
-
461
- ## Setup flows
462
-
463
- ### Solo, local only (~90 seconds)
464
-
465
- ```
466
- brew install agentlog
467
- agentlog init # picks local storage, prompts for the login watcher
468
- # → "Start watcher at login"
469
- # → scans for existing history
470
- # → "Import last 30 days? [Y/n]"
471
- # → if checked, writes launch agent and starts supervisor
472
- # → import runs in background
473
- ```
474
-
475
- After `init` completes, prints:
476
-
477
- ```
478
- ✓ Launch agent installed at ~/Library/LaunchAgents/com.agentlog.supervisor.plist
479
- ✓ Service started (PID 47291)
480
- ✓ Collector listening on localhost:4318
481
- ✓ Claude Code config updated at ~/.claude/settings.json
482
- ✓ Background import started: 247 sessions queued (~25min)
483
-
484
- View your history: `agentlog history`
485
- Try it out: open Claude Code, have a quick conversation, then run
486
- `agentlog status` to see it captured.
487
- ```
488
-
489
- ### Solo, R2-backed (~5 minutes)
490
-
491
- ```
492
- brew install agentlog
493
- agentlog init --storage r2
494
- # → opens dash.cloudflare.com/?to=/:account/r2/api-tokens
495
- # → user pastes credentials back into CLI
496
- # → agentlog creates bucket if needed, validates write
497
- # → same auto-start and import prompts as above
498
- ```
499
-
500
- ### Team (afternoon for operator, ~1 minute per developer)
501
-
502
- ```
503
- # Operator, once:
504
- terraform apply # ships agentlog/deploy-aws or deploy-cloudflare
505
- # outputs OTLP endpoint and bootstrap token
506
-
507
- # Each developer:
508
- agentlog init --remote https://agentlog.myteam.com --token <token>
509
- # auto-start prompt; import scoped to team policy;
510
- # no local OpenObserve (team server runs it)
511
- ```
512
-
513
- Companion deployment modules: `agentlog/deploy-aws` (ECS + ALB + S3), `agentlog/deploy-cloudflare` (R2 + Workers for auth proxy), `agentlog/deploy-fly` (Fly.io single-region). The Terraform module is the unsexy linchpin for team adoption.
514
-
515
- ## Privacy and data handling commitments
516
-
517
- Codified because they shape every other decision:
518
-
519
- 1. **No phone-home telemetry.** agentlog itself ships zero usage analytics anywhere. Any future opt-in metrics live on a separate channel.
520
- 2. **No filesystem scanning.** Only specific known paths: `~/.codex/`, `~/.claude/`, `~/.gemini/`, `~/.local/share/devin/cli/sessions.db`, Cursor's storage, `~/.agentlog/`, plus user-specified import file paths. Windsurf encrypted cache paths are excluded while Cascade transcripts remain encrypted; user-selected downloaded trajectory Markdown files are allowed.
521
- 3. **No process inspection beyond `pgrep`.** We check whether Cursor is running. We don't introspect what it's doing.
522
- 4. **Redaction at ingest, not query.** Pattern-matching credentials never land in storage in the first place.
523
- 5. **Reveal is local-only.** Un-redacted content is never reconstructible from team/remote archives.
524
- 6. **Web chats default to local-only.** Even on team-configured installs. Explicit override required to share.
525
- 7. **Agent recall excludes web chats by default.** Human-initiated history viewing includes them.
526
- 8. **Redaction limits are stated honestly.** Pattern-based redaction catches credentials, not sensitive personal content.
527
- 9. **Uninstall removes everything.** Tested.
528
-
529
- ## What's deferred
530
-
531
- - **Web UI beyond cchv** — v0.2 if cchv proves insufficient
532
- - **Slack notify skill** (`agentlog-slack`) — separate repo, consumes from archive; v0.2
533
- - **Other web chat sources** (Gemini, Perplexity, Grok) — v1, same pattern, different parsers
534
- - **Heuristic repo inference for web chats** — v1
535
- - **Auto-detection of new export files in `~/Downloads/`** — v1
536
- - **Summary generation / "revival packets"** — v1, only if raw retrieval proves insufficient
537
- - **Cross-machine session linking** — v1, depends on canonical repo keying landing solid
538
- - **SSO** — v1, enterprise tier
539
- - **Per-user permissions for team viewing** — v1
540
- - **Cursor extension for richer capture** — v1 if SQLite proves too lossy
541
- - **Windows Service install** — v1, currently Scheduled Task only
542
- - **Direct integration with Claude.ai/ChatGPT desktop app local stores** — probably never; respect the boundary
543
-
544
- ## What this is not
545
-
546
- - Not a memory-curation product. Memorix and Hindsight occupy that space; agentlog can be their substrate.
547
- - Not an enterprise observability tool. SigNoz/Datadog/Honeycomb are better and accept the same OTel feeds.
548
- - Not a session-replay tool.
549
- - Not a Slack bot. Slack integration is downstream of the archive, intentionally.
550
- - Not a real-time capture tool for web chats. Those are import-only by design.
551
-
552
- ## The differentiator
553
-
554
- A redaction-first, repo-keyed, S3-compatible archive substrate that the agent itself can query via MCP, with backfill of existing CLI history and import paths for web chats. Every piece exists separately. The value is in composing them under one CLI with a setup flow that doesn't take an afternoon and a privacy story that doesn't require trusting a vendor.
555
-
556
- If it's good, it disappears: the user forgets it's running, their agents quietly get smarter at their repos over time, and accumulated debugging knowledge stops evaporating between sessions.
557
-
558
- That's v0.2.