agentel 0.2.3 → 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 +1 -1
- package/docs/history-source-handling.md +24 -24
- package/docs/release.md +1 -1
- package/package.json +1 -2
- package/src/importers/providers.js +1 -1
- package/src/importers.js +91 -17
- package/src/search.js +51 -25
- package/agentlog-spec.md +0 -558
package/README.md
CHANGED
|
@@ -183,30 +183,30 @@ package-prefixed scheme.
|
|
|
183
183
|
|
|
184
184
|
| Source type | Version |
|
|
185
185
|
| --- | --- |
|
|
186
|
-
| `codex-cli-history` | `0.2.
|
|
187
|
-
| `codex-desktop-history` | `0.2.
|
|
188
|
-
| `cli-history` | `0.2.
|
|
189
|
-
| `claude-sdk-history` | `0.2.
|
|
190
|
-
| `claude-code-desktop-metadata` | `0.2.
|
|
191
|
-
| `claude-workspace-desktop` | `0.2.
|
|
192
|
-
| `cursor-workspace-sqlite` | `0.2.
|
|
193
|
-
| `cursor-global-sqlite` | `0.2.
|
|
194
|
-
| `cursor-raw-sqlite-salvage` | `0.2.
|
|
195
|
-
| `cursor-agent-transcripts` | `0.2.
|
|
196
|
-
| `devin-cli-history` | `0.2.
|
|
197
|
-
| `gemini-cli-history` | `0.2.
|
|
198
|
-
| `cline-task-history` | `0.2.
|
|
199
|
-
| `opencode-history` | `0.2.
|
|
200
|
-
| `opencode-sqlite-history` | `0.2.
|
|
201
|
-
| `aider-chat-history` | `0.2.
|
|
202
|
-
| `antigravity-history` | `0.2.
|
|
203
|
-
| `antigravity-trajectory-summary` | `0.2.
|
|
204
|
-
| `windsurf-trajectory-export` | `0.2.
|
|
205
|
-
| `web-chat-export` | `0.2.
|
|
206
|
-
| `chatgpt-export` | `0.2.
|
|
207
|
-
| `claude-web-export` | `0.2.
|
|
208
|
-
| `claude-web-memory` | `0.2.
|
|
209
|
-
| `import` | `0.2.
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentel",
|
|
3
|
-
"version": "0.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
|
],
|
|
@@ -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
|
@@ -4149,6 +4149,7 @@ const CURSOR_RAW_ASSISTANT_MERGE_MIN_SCORE = 32;
|
|
|
4149
4149
|
const CURSOR_RAW_ASSISTANT_MERGE_MIN_OVERLAP = 2;
|
|
4150
4150
|
const CURSOR_ABSOLUTE_PATH_RE = /(?:file:\/\/)?\/(?:Users|home|Volumes|private|tmp|var)\/[^\s"'`<>{}|]+/g;
|
|
4151
4151
|
const SQLITE_QUERY_TIMEOUT_MS = 30 * 1000;
|
|
4152
|
+
const OPENCODE_SQLITE_BATCH_SIZE = 100;
|
|
4152
4153
|
|
|
4153
4154
|
function readCursorRawSqliteSalvageSessionsFromDb(dbPath, options = {}) {
|
|
4154
4155
|
const files = cursorRawSqliteFilesForDb(dbPath);
|
|
@@ -6544,7 +6545,18 @@ function readOpenCodeSessions(env = process.env, options = {}) {
|
|
|
6544
6545
|
const sessions = [];
|
|
6545
6546
|
reportDiscoveryProgress(options, { current: 0, total: dbs.length, message: "reading OpenCode SQLite stores" });
|
|
6546
6547
|
for (let index = 0; index < dbs.length; index++) {
|
|
6547
|
-
|
|
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
|
+
}
|
|
6548
6560
|
sessions.push(...dbSessions);
|
|
6549
6561
|
reportDiscoveryProgress(options, {
|
|
6550
6562
|
current: index + 1,
|
|
@@ -6656,12 +6668,14 @@ function openCodeMessageSessionIds(root) {
|
|
|
6656
6668
|
.sort((a, b) => a.localeCompare(b));
|
|
6657
6669
|
}
|
|
6658
6670
|
|
|
6659
|
-
function readOpenCodeSqliteSessionsFromDb(dbPath) {
|
|
6671
|
+
function readOpenCodeSqliteSessionsFromDb(dbPath, options = {}) {
|
|
6660
6672
|
if (!safeStat(dbPath)) return [];
|
|
6661
6673
|
if (!sqliteTableExists(dbPath, "session") || !sqliteTableExists(dbPath, "message") || !sqliteTableExists(dbPath, "part")) return [];
|
|
6662
|
-
const sessionRows = readOpenCodeSqliteSessionRows(dbPath);
|
|
6663
|
-
|
|
6664
|
-
const
|
|
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"]);
|
|
6665
6679
|
const messagesBySession = groupRowsBy(messageRows, "session_id");
|
|
6666
6680
|
const partsByMessage = groupRowsBy(partRows, "message_id");
|
|
6667
6681
|
const storageRoot = path.join(path.dirname(dbPath), "storage");
|
|
@@ -6709,11 +6723,12 @@ function readOpenCodeSqliteSessionsFromDb(dbPath) {
|
|
|
6709
6723
|
return sessions;
|
|
6710
6724
|
}
|
|
6711
6725
|
|
|
6712
|
-
function readOpenCodeSqliteSessionRows(dbPath) {
|
|
6726
|
+
function readOpenCodeSqliteSessionRows(dbPath, options = {}) {
|
|
6713
6727
|
const sessionColumns = sqliteTableColumns(dbPath, "session");
|
|
6714
6728
|
if (!sessionColumns.has("id")) return [];
|
|
6715
6729
|
const projectColumns = sqliteTableExists(dbPath, "project") ? sqliteTableColumns(dbPath, "project") : new Set();
|
|
6716
6730
|
const canJoinProject = sessionColumns.has("project_id") && projectColumns.has("id");
|
|
6731
|
+
const timestampExpr = openCodeSqliteSessionTimestampExpr(sessionColumns);
|
|
6717
6732
|
const selects = [
|
|
6718
6733
|
"s.id",
|
|
6719
6734
|
sqliteSelectMaybe(sessionColumns, "s", "project_id"),
|
|
@@ -6735,7 +6750,11 @@ function readOpenCodeSqliteSessionRows(dbPath) {
|
|
|
6735
6750
|
];
|
|
6736
6751
|
const queryParts = [`select ${selects.join(", ")}`, "from session s"];
|
|
6737
6752
|
if (canJoinProject) queryParts.push("left join project p on p.id = s.project_id");
|
|
6738
|
-
|
|
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 ")}`);
|
|
6739
6758
|
const orderColumns = [];
|
|
6740
6759
|
if (sessionColumns.has("time_updated")) orderColumns.push("s.time_updated desc");
|
|
6741
6760
|
if (sessionColumns.has("time_created")) orderColumns.push("s.time_created desc");
|
|
@@ -6744,7 +6763,23 @@ function readOpenCodeSqliteSessionRows(dbPath) {
|
|
|
6744
6763
|
return readSqliteJson(dbPath, queryParts.join(" "), "OpenCode SQLite sessions");
|
|
6745
6764
|
}
|
|
6746
6765
|
|
|
6747
|
-
function
|
|
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 = []) {
|
|
6748
6783
|
const columns = sqliteTableColumns(dbPath, "message");
|
|
6749
6784
|
if (!columns.has("id") || !columns.has("session_id")) return [];
|
|
6750
6785
|
const selects = [
|
|
@@ -6754,13 +6789,16 @@ function readOpenCodeSqliteMessageRows(dbPath) {
|
|
|
6754
6789
|
sqliteSelectMaybe(columns, "message", "time_updated"),
|
|
6755
6790
|
sqliteSelectMaybe(columns, "message", "data")
|
|
6756
6791
|
];
|
|
6757
|
-
|
|
6758
|
-
|
|
6759
|
-
|
|
6760
|
-
|
|
6792
|
+
return readOpenCodeSqliteRowsForSessionIds(
|
|
6793
|
+
dbPath,
|
|
6794
|
+
"message",
|
|
6795
|
+
selects,
|
|
6796
|
+
sessionIds,
|
|
6797
|
+
"OpenCode SQLite messages"
|
|
6798
|
+
);
|
|
6761
6799
|
}
|
|
6762
6800
|
|
|
6763
|
-
function readOpenCodeSqlitePartRows(dbPath) {
|
|
6801
|
+
function readOpenCodeSqlitePartRows(dbPath, sessionIds = []) {
|
|
6764
6802
|
const columns = sqliteTableColumns(dbPath, "part");
|
|
6765
6803
|
if (!columns.has("id") || !columns.has("message_id") || !columns.has("session_id")) return [];
|
|
6766
6804
|
const selects = [
|
|
@@ -6771,10 +6809,46 @@ function readOpenCodeSqlitePartRows(dbPath) {
|
|
|
6771
6809
|
sqliteSelectMaybe(columns, "part", "time_updated"),
|
|
6772
6810
|
sqliteSelectMaybe(columns, "part", "data")
|
|
6773
6811
|
];
|
|
6774
|
-
|
|
6775
|
-
|
|
6776
|
-
|
|
6777
|
-
|
|
6812
|
+
return readOpenCodeSqliteRowsForSessionIds(
|
|
6813
|
+
dbPath,
|
|
6814
|
+
"part",
|
|
6815
|
+
selects,
|
|
6816
|
+
sessionIds,
|
|
6817
|
+
"OpenCode SQLite parts"
|
|
6818
|
+
);
|
|
6819
|
+
}
|
|
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 || ""));
|
|
6778
6852
|
}
|
|
6779
6853
|
|
|
6780
6854
|
function openCodeSqliteMessagesFromRow(row, partRows, index) {
|
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
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
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 (
|
|
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.
|