ai-hist 0.3.7 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +100 -2
  2. package/dist/cloud-client.d.ts +1 -0
  3. package/dist/cloud-client.d.ts.map +1 -1
  4. package/dist/cloud-client.js +2 -0
  5. package/dist/cloud-client.js.map +1 -1
  6. package/dist/cloud-push.d.ts +51 -0
  7. package/dist/cloud-push.d.ts.map +1 -0
  8. package/dist/cloud-push.js +113 -0
  9. package/dist/cloud-push.js.map +1 -0
  10. package/dist/cloud-push.test.d.ts +2 -0
  11. package/dist/cloud-push.test.d.ts.map +1 -0
  12. package/dist/cloud-push.test.js +82 -0
  13. package/dist/cloud-push.test.js.map +1 -0
  14. package/dist/index.d.ts +119 -6
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +304 -10
  17. package/dist/index.js.map +1 -1
  18. package/dist/jsonl-sources.d.ts +4 -6
  19. package/dist/jsonl-sources.d.ts.map +1 -1
  20. package/dist/jsonl-sources.js +9 -12
  21. package/dist/jsonl-sources.js.map +1 -1
  22. package/dist/mcp-server.js +116 -1
  23. package/dist/mcp-server.js.map +1 -1
  24. package/dist/mcp-smoke.test.js +194 -44
  25. package/dist/mcp-smoke.test.js.map +1 -1
  26. package/dist/session-catalog.d.ts +257 -0
  27. package/dist/session-catalog.d.ts.map +1 -0
  28. package/dist/session-catalog.js +340 -0
  29. package/dist/session-catalog.js.map +1 -0
  30. package/dist/session-catalog.test.d.ts +2 -0
  31. package/dist/session-catalog.test.d.ts.map +1 -0
  32. package/dist/session-catalog.test.js +710 -0
  33. package/dist/session-catalog.test.js.map +1 -0
  34. package/dist/session-events.test.d.ts +2 -0
  35. package/dist/session-events.test.d.ts.map +1 -0
  36. package/dist/session-events.test.js +177 -0
  37. package/dist/session-events.test.js.map +1 -0
  38. package/package.json +1 -1
package/README.md CHANGED
@@ -49,7 +49,7 @@ npm run build
49
49
  node dist/mcp-server.js
50
50
  ```
51
51
 
52
- The MCP server exposes tools for search, recent history, session lookup, temporal context, evidence packing, stats, trajectory search, and task WHY lookup over stdio. It runs on the user's machine and uses the same data-opening behavior as the SDK: SQLite first, then local fallback scanning.
52
+ The MCP server exposes tools for search, recent history, the session catalog, session lookup, temporal context, evidence packing, stats, trajectory search, and task WHY lookup over stdio. It runs on the user's machine and uses the same data-opening behavior as the SDK: SQLite first, then local fallback scanning.
53
53
 
54
54
  To expose only one project through MCP, pass a project scope when launching the server. Exact project matches and child paths are included.
55
55
 
@@ -67,6 +67,57 @@ Contract tools:
67
67
  - `stats()`
68
68
  - `search_trajectories(query, limit?)`
69
69
  - `why_for_task(query)`
70
+ - `list_sessions(sources?, limit?, before_ms?, after?)`
71
+
72
+ ## Session catalog
73
+
74
+ `sessions` is a materialized catalog of every coding-agent session ai-hist knows about — one row per `(source, session_id)`, with cwd, git branch, first/last activity, the first prompt, observed models, originator, agent version, and repo identity. It is populated by shallow discovery (cheap, metadata only) and upgraded in place by a full `ai-hist sync`. `discoveryState` says which you are looking at: `'shallow'` for a catalog row whose transcript has not been ingested, `'full'` once it has (rows written before the catalog existed store `NULL` and read as `'full'`).
75
+
76
+ Reading it is one indexed query over a single table — no provider transcript is opened and no prompt history is scanned — so it is the fast path for "which sessions exist?" on first paint:
77
+
78
+ ```ts
79
+ import { openAiHist, discoverSessions, SESSION_CATALOG_CONTRACT_VERSION } from 'ai-hist';
80
+
81
+ const hist = await openAiHist();
82
+ for (const s of hist.listSessionCatalog({ limit: 20 })) {
83
+ console.log(s.lastActivityMs, s.source, s.sessionId, s.cwd, s.firstPrompt);
84
+ }
85
+ ```
86
+
87
+ The catalog's total order is `(lastActivityMs DESC, source ASC, sessionId ASC)`, with rows of unknown recency last. Recency alone is not a key: one discovery pass can stamp many sessions with the same mtime-derived millisecond, so paginate with `listSessionCatalogPage` and follow its cursor, which carries all three columns:
88
+
89
+ ```ts
90
+ let after;
91
+ for (;;) {
92
+ const page = hist.listSessionCatalogPage({ limit: 20, after });
93
+ render(page.sessions);
94
+ if (!page.nextCursor) break; // null once the catalog is exhausted
95
+ after = page.nextCursor; // { lastActivityMs, source, sessionId }
96
+ }
97
+ ```
98
+
99
+ `nextCursor` is non-null only when the page filled its limit, so the loop terminates on an empty catalog and on a null-timestamp tail alike — both cases where a `beforeMs`-only walk either spins forever or silently drops the rows tied with a page boundary. `beforeMs` survives as a *coarse* cutoff ("anything before last Tuesday") and is ignored when `after` is set. The same contract is available from the CLI as the `next_cursor` field of `sessions list --json`, fed back as `--after-source` + `--after-session-id` (both required together, and `--after-ms` alone is refused — a timestamp is not a cursor). Omit `--after-ms` to continue through the undated tail.
100
+
101
+ `trajectory` rows are excluded — trajectories are derived records, not sessions. A configured `projectScope` constrains rows by `cwd`, so sources with no working directory (relay) drop out of a scoped listing. A negative `limit` throws (SQLite would read it as "unlimited"). In JSONL fallback mode (no SQLite database) the catalog is empty and the methods return no rows; only the native discovery engine writes it.
102
+
103
+ To populate or refresh the catalog, run shallow discovery. It scans the known provider locations with bounded reads, orders candidates globally by recency before applying the limit, and skips sources whose bytes have not changed since the last run:
104
+
105
+ ```ts
106
+ const { sessions, diagnostics, summary } = await discoverSessions({
107
+ sources: ['claude', 'codex'], // omit for every discoverable source
108
+ limit: 50, // global across providers, by recency
109
+ onSession: (s) => render(s), // streams as rows arrive
110
+ });
111
+
112
+ console.log(summary.discovered, summary.skippedUnchanged, summary.counters.bytesRead);
113
+ for (const d of diagnostics) console.warn(`[${d.source}] ${d.locator ?? ''}: ${d.error}`);
114
+ ```
115
+
116
+ `discoverSessions` is a top-level function, not an `AiHist` method: it drives `ai-hist sessions discover --json` (binary discovery is `$AI_HIST_RUST_BIN` → the install.sh location → `ai-hist` on `PATH`, the same as `pushToCloud`), and that writes the on-disk database. An `AiHist` instance is an in-memory snapshot taken at open time, so re-open it before listing to see freshly discovered rows.
117
+
118
+ Failure behavior matches the CLI's: one provider blowing up yields a diagnostic and the run still resolves. The promise rejects with a `DiscoveryError` when the binary cannot be run at all (the message names `AI_HIST_RUST_BIN`), when the run exits non-zero because every selected provider failed, or when the closing summary is missing or announces a contract version this SDK does not implement — the version is checked for you against `SESSION_CATALOG_CONTRACT_VERSION` rather than left as a field to remember. The native command writes its diagnostics and summary trailer even on an all-provider failure, so a `DiscoveryError` carries `diagnostics`, `summary`, `stderr`, and `exitCode`. Unparseable or unrecognized JSONL lines are skipped rather than failing the run.
119
+
120
+ An exception thrown by `onSession` or `onDiagnostic` aborts the run: the child process is killed and the promise rejects with your error, instead of escaping the stream handler as an uncaught exception in the host.
70
121
 
71
122
  ## API
72
123
 
@@ -83,8 +134,12 @@ hist.sourceKind: 'sqlite' | 'jsonl'
83
134
  hist.projectScope: string | undefined
84
135
 
85
136
  hist.recent(opts?): HistoryEntry[] // newest prompts first
86
- hist.listSessions(opts?): SessionSummary[] // grouped by session_id, last activity DESC
137
+ hist.listSessions(opts?): SessionSummary[] // grouped from history by session_id, last activity DESC
138
+ hist.listSessionCatalog(opts?): CatalogSession[] // the sessions catalog: cache-only, one indexed query
139
+ hist.listSessionCatalogPage(opts?): SessionCatalogPage // the same page plus its nextCursor
87
140
  hist.getSession(sessionId): HistoryEntry[] // all prompts in a session, oldest first
141
+ hist.getSessionEvents(sessionId): SessionEvent[] // full transcript: text, thinking, tool calls/results, token usage
142
+ hist.getToolCalls(sessionId): SessionToolCall[] // the session's tool invocations
88
143
  hist.getEntry(id): HistoryEntry | null
89
144
  hist.getInTimeWindow(timestampMs, windowMs): HistoryEntry[]
90
145
  hist.search(query, opts?): HistoryEntry[] // literal substring search, recent matches first
@@ -95,9 +150,14 @@ hist.stats(): Stats // counts + date range
95
150
 
96
151
  All list-style methods accept `{ source?, project?, limit?, beforeMs? }`. `beforeMs` is the cursor for paginating older results.
97
152
 
153
+ The catalog methods take `{ sources?, limit?, beforeMs?, after? }` instead — they filter on the catalog's own `source` column, not on project, and `after` is the cursor for paging.
154
+
98
155
  ```ts
99
156
  resumeCommand(entry): string | null // shell command per source; null for relay
100
157
  defaultDbPath(): string // resolve env / OS default
158
+ discoverSessions(opts?): Promise<DiscoverResult> // drives `ai-hist sessions discover --json`
159
+ SESSION_CATALOG_CONTRACT_VERSION: number // native session-catalog output contract (1)
160
+ DiscoveryError // thrown by discoverSessions; carries diagnostics + summary
101
161
  ```
102
162
 
103
163
  ## Trajectories
@@ -160,8 +220,46 @@ CREATE TABLE history (
160
220
  );
161
221
 
162
222
  CREATE VIRTUAL TABLE history_fts USING fts5(prompt, project, content='history', content_rowid='id');
223
+
224
+ CREATE TABLE sessions (
225
+ session_id TEXT NOT NULL,
226
+ source TEXT NOT NULL,
227
+ cwd TEXT,
228
+ git_branch TEXT,
229
+ first_activity_ms INTEGER,
230
+ last_activity_ms INTEGER,
231
+ last_assistant_text TEXT,
232
+ raw_path TEXT,
233
+ parser_version INTEGER NOT NULL DEFAULT 1,
234
+ first_prompt TEXT,
235
+ models_json TEXT,
236
+ originator TEXT,
237
+ agent_version TEXT,
238
+ repo_url TEXT,
239
+ initial_commit TEXT,
240
+ workspace_roots_json TEXT,
241
+ source_stamp TEXT,
242
+ discovery_state TEXT,
243
+ PRIMARY KEY (session_id, source)
244
+ );
245
+
246
+ CREATE INDEX idx_sessions_recency ON sessions(last_activity_ms DESC, source, session_id);
247
+ CREATE INDEX idx_sessions_source_recency ON sessions(source, last_activity_ms DESC, session_id);
248
+ CREATE INDEX idx_sessions_source_last ON sessions(source, last_activity_ms DESC);
249
+ CREATE INDEX idx_sessions_raw_path ON sessions(source, raw_path);
250
+
251
+ CREATE TABLE discovery_skips (
252
+ source TEXT NOT NULL,
253
+ locator TEXT NOT NULL,
254
+ stamp TEXT NOT NULL,
255
+ reason TEXT,
256
+ updated_ms INTEGER,
257
+ PRIMARY KEY (source, locator)
258
+ );
163
259
  ```
164
260
 
261
+ `models_json` and `workspace_roots_json` hold JSON string arrays or `NULL` (never `[]`); the SDK parses both into arrays. The catalog columns were added after 0.5.0, so when the SDK opens an older database file it backfills the missing columns on its in-memory copy with `ALTER TABLE` — the file on disk is not touched, and the absent values read as `NULL`.
262
+
165
263
  Trajectory sync also creates a structured `trajectories` table and inserts each per-run compact file into `history` with `source='trajectory'`, so general history search and WHY-specific lookup both work.
166
264
 
167
265
  ## License
@@ -1,3 +1,4 @@
1
+ export { pushToCloud, resolveAiHistBinary, type PushReport, type PushOptions } from './cloud-push.js';
1
2
  export interface RelayhistoryAuth {
2
3
  baseUrl: string;
3
4
  accessToken: string;
@@ -1 +1 @@
1
- {"version":3,"file":"cloud-client.d.ts","sourceRoot":"","sources":["../src/cloud-client.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,MAAM,gBAAgB,GACxB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,gBAAgB,CAAA;CAAE,GACpC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAajC,wBAAsB,0BAA0B,IAAI,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CASnF;AAED,wBAAsB,UAAU,CAC9B,gBAAgB,EAAE,MAAM,EACxB,IAAI,GAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAO,GAC9C,OAAO,CAAC,gBAAgB,CAAC,CAkD3B"}
1
+ {"version":3,"file":"cloud-client.d.ts","sourceRoot":"","sources":["../src/cloud-client.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,KAAK,UAAU,EAAE,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEtG,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,MAAM,gBAAgB,GACxB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,gBAAgB,CAAA;CAAE,GACpC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAajC,wBAAsB,0BAA0B,IAAI,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CASnF;AAED,wBAAsB,UAAU,CAC9B,gBAAgB,EAAE,MAAM,EACxB,IAAI,GAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAO,GAC9C,OAAO,CAAC,gBAAgB,CAAC,CAkD3B"}
@@ -1,6 +1,8 @@
1
1
  import { readFile, writeFile, mkdir } from 'node:fs/promises';
2
2
  import { homedir } from 'node:os';
3
3
  import { join, dirname } from 'node:path';
4
+ // Re-export the in-process cloud push so `ai-hist/cloud` exposes auth + push.
5
+ export { pushToCloud, resolveAiHistBinary } from './cloud-push.js';
4
6
  function authPath() {
5
7
  const configDir = process.env.AI_HIST_CONFIG_DIR ?? join(homedir(), '.config', 'ai-hist');
6
8
  return join(configDir, 'auth.json');
@@ -1 +1 @@
1
- {"version":3,"file":"cloud-client.js","sourceRoot":"","sources":["../src/cloud-client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cloud-client.js","sourceRoot":"","sources":["../src/cloud-client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * In-process cloud push — a thin wrapper over the `ai-hist` Rust binary.
3
+ *
4
+ * Rather than re-implement the push pipeline (outbox batching, cursor, wire
5
+ * envelopes, dedup hashing) in TypeScript — which would have to stay byte-for-
6
+ * byte compatible with the Rust client forever — this spawns the real
7
+ * `ai-hist push --json` and parses its result. The Rust binary is the single
8
+ * source of truth for the push logic; this is just an ergonomic SDK surface
9
+ * over it, so hosts (e.g. the Agent Relay runtime) can sync without shelling
10
+ * out to a CLI by hand.
11
+ */
12
+ import { spawn } from 'node:child_process';
13
+ export interface PushReport {
14
+ sent: number;
15
+ accepted: number;
16
+ batchId: string | null;
17
+ cursor?: unknown;
18
+ /** Accepted per-source record limit after any capacity retry. */
19
+ batchLimit?: number;
20
+ /** Number of HTTP ingest attempts made by this push. */
21
+ attempts?: number;
22
+ }
23
+ export interface PushOptions {
24
+ /** Explicit path to the ai-hist binary (overrides discovery). */
25
+ binPath?: string;
26
+ /** Rows scanned per source (forwarded as `--limit`). */
27
+ limit?: number;
28
+ /** Session/trajectory ids to exclude (forwarded as repeated `--incognito`). */
29
+ incognito?: Iterable<string>;
30
+ /** Extra environment for the spawned binary (merged over `process.env`). */
31
+ env?: NodeJS.ProcessEnv;
32
+ /** Injectable spawn for testing. */
33
+ spawnFn?: typeof spawn;
34
+ }
35
+ /**
36
+ * Resolve the ai-hist binary. An explicit override is **authoritative** — it is
37
+ * returned verbatim without falling through to discovery, so the caller always
38
+ * runs the binary it asked for (an unusable path surfaces later in `pushToCloud`
39
+ * as a no-op push, never a silent switch to a different binary). With no
40
+ * override: `$AI_HIST_RUST_BIN` → the install.sh location → the `ai-hist`
41
+ * wrapper on `PATH`. Always returns a string.
42
+ */
43
+ export declare function resolveAiHistBinary(explicit?: string): string;
44
+ /**
45
+ * Push new local history to relayhistory-cloud by driving `ai-hist push --json`.
46
+ * Resolves `null` when the binary isn't installed or the user isn't
47
+ * authenticated (both non-fatal for a background loop); rejects on other
48
+ * non-zero exits or unparseable output.
49
+ */
50
+ export declare function pushToCloud(opts?: PushOptions): Promise<PushReport | null>;
51
+ //# sourceMappingURL=cloud-push.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cloud-push.d.ts","sourceRoot":"","sources":["../src/cloud-push.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,KAAK,EAAqB,MAAM,oBAAoB,CAAC;AAK9D,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,iEAAiE;IACjE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,wDAAwD;IACxD,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,iEAAiE;IACjE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,wDAAwD;IACxD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,+EAA+E;IAC/E,SAAS,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC7B,4EAA4E;IAC5E,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,oCAAoC;IACpC,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;CACxB;AAaD;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAW7D;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,IAAI,GAAE,WAAgB,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CA+D9E"}
@@ -0,0 +1,113 @@
1
+ /**
2
+ * In-process cloud push — a thin wrapper over the `ai-hist` Rust binary.
3
+ *
4
+ * Rather than re-implement the push pipeline (outbox batching, cursor, wire
5
+ * envelopes, dedup hashing) in TypeScript — which would have to stay byte-for-
6
+ * byte compatible with the Rust client forever — this spawns the real
7
+ * `ai-hist push --json` and parses its result. The Rust binary is the single
8
+ * source of truth for the push logic; this is just an ergonomic SDK surface
9
+ * over it, so hosts (e.g. the Agent Relay runtime) can sync without shelling
10
+ * out to a CLI by hand.
11
+ */
12
+ import { spawn } from 'node:child_process';
13
+ import { accessSync, constants } from 'node:fs';
14
+ import { homedir } from 'node:os';
15
+ import { join } from 'node:path';
16
+ const AI_HIST_RUST_BIN_ENV = 'AI_HIST_RUST_BIN';
17
+ function isExecutable(path) {
18
+ try {
19
+ accessSync(path, constants.X_OK);
20
+ return true;
21
+ }
22
+ catch {
23
+ return false;
24
+ }
25
+ }
26
+ /**
27
+ * Resolve the ai-hist binary. An explicit override is **authoritative** — it is
28
+ * returned verbatim without falling through to discovery, so the caller always
29
+ * runs the binary it asked for (an unusable path surfaces later in `pushToCloud`
30
+ * as a no-op push, never a silent switch to a different binary). With no
31
+ * override: `$AI_HIST_RUST_BIN` → the install.sh location → the `ai-hist`
32
+ * wrapper on `PATH`. Always returns a string.
33
+ */
34
+ export function resolveAiHistBinary(explicit) {
35
+ if (typeof explicit === 'string' && explicit.length > 0)
36
+ return explicit;
37
+ const known = [
38
+ process.env[AI_HIST_RUST_BIN_ENV],
39
+ join(homedir(), '.local', 'share', 'ai-hist', 'ai-hist-rust-bin'),
40
+ ].filter((c) => typeof c === 'string' && c.length > 0);
41
+ for (const candidate of known) {
42
+ if (isExecutable(candidate))
43
+ return candidate;
44
+ }
45
+ // Let PATH resolution (and ENOENT handling) take over.
46
+ return 'ai-hist';
47
+ }
48
+ /**
49
+ * Push new local history to relayhistory-cloud by driving `ai-hist push --json`.
50
+ * Resolves `null` when the binary isn't installed or the user isn't
51
+ * authenticated (both non-fatal for a background loop); rejects on other
52
+ * non-zero exits or unparseable output.
53
+ */
54
+ export function pushToCloud(opts = {}) {
55
+ const spawnFn = opts.spawnFn ?? spawn;
56
+ const bin = resolveAiHistBinary(opts.binPath);
57
+ const args = ['push', '--json'];
58
+ if (opts.limit != null)
59
+ args.push('--limit', String(opts.limit));
60
+ for (const id of opts.incognito ?? [])
61
+ args.push('--incognito', id);
62
+ const spawnOpts = {
63
+ env: { ...process.env, ...opts.env },
64
+ stdio: ['ignore', 'pipe', 'pipe'],
65
+ };
66
+ return new Promise((resolve, reject) => {
67
+ const child = spawnFn(bin, args, spawnOpts);
68
+ let stdout = '';
69
+ let stderr = '';
70
+ child.stdout?.on('data', (chunk) => {
71
+ stdout += String(chunk);
72
+ });
73
+ child.stderr?.on('data', (chunk) => {
74
+ stderr += String(chunk);
75
+ });
76
+ child.on('error', (err) => {
77
+ // Binary unavailable — missing (ENOENT), not executable (EACCES/EPERM), or
78
+ // a directory (ENOTDIR/EISDIR). All mean "nothing to sync", the same
79
+ // no-op contract a background loop relies on.
80
+ if (['ENOENT', 'EACCES', 'EPERM', 'ENOTDIR', 'EISDIR'].includes(err.code ?? '')) {
81
+ resolve(null);
82
+ return;
83
+ }
84
+ reject(err);
85
+ });
86
+ child.on('close', (code) => {
87
+ if (code !== 0) {
88
+ // Not logged in yet is expected before `reflex on` completes.
89
+ if (/not authenticated|no relayhistory auth|run `?ai-hist login/i.test(stderr)) {
90
+ resolve(null);
91
+ return;
92
+ }
93
+ reject(new Error(`ai-hist push failed (exit ${code}): ${stderr.trim().slice(0, 300)}`));
94
+ return;
95
+ }
96
+ try {
97
+ const parsed = (stdout.trim() ? JSON.parse(stdout) : {});
98
+ resolve({
99
+ sent: typeof parsed.sent === 'number' ? parsed.sent : 0,
100
+ accepted: typeof parsed.accepted === 'number' ? parsed.accepted : 0,
101
+ batchId: typeof parsed.batchId === 'string' ? parsed.batchId : null,
102
+ cursor: parsed.cursor,
103
+ ...(typeof parsed.batchLimit === 'number' ? { batchLimit: parsed.batchLimit } : {}),
104
+ ...(typeof parsed.attempts === 'number' ? { attempts: parsed.attempts } : {}),
105
+ });
106
+ }
107
+ catch (err) {
108
+ reject(new Error(`could not parse ai-hist push output: ${err instanceof Error ? err.message : String(err)}`));
109
+ }
110
+ });
111
+ });
112
+ }
113
+ //# sourceMappingURL=cloud-push.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cloud-push.js","sourceRoot":"","sources":["../src/cloud-push.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=cloud-push.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cloud-push.test.d.ts","sourceRoot":"","sources":["../src/cloud-push.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,82 @@
1
+ import assert from 'node:assert/strict';
2
+ import { EventEmitter } from 'node:events';
3
+ import { test } from 'node:test';
4
+ import { pushToCloud } from './cloud-push.js';
5
+ /** Minimal fake child process that scripts stdout/stderr/exit for one run. */
6
+ function fakeSpawn(script) {
7
+ const calls = [];
8
+ const spawnFn = ((bin, args) => {
9
+ calls.push({ bin, args });
10
+ const child = new EventEmitter();
11
+ child.stdout = new EventEmitter();
12
+ child.stderr = new EventEmitter();
13
+ // Emit asynchronously so the caller has attached its listeners.
14
+ setImmediate(() => {
15
+ if (script.errorCode) {
16
+ const err = new Error('spawn failed');
17
+ err.code = script.errorCode;
18
+ child.emit('error', err);
19
+ return;
20
+ }
21
+ if (script.stdout)
22
+ child.stdout.emit('data', script.stdout);
23
+ if (script.stderr)
24
+ child.stderr.emit('data', script.stderr);
25
+ child.emit('close', script.code ?? 0);
26
+ });
27
+ return child;
28
+ });
29
+ return { spawnFn, calls };
30
+ }
31
+ test('pushToCloud parses ai-hist push --json output into a report', async () => {
32
+ const { spawnFn, calls } = fakeSpawn({
33
+ stdout: JSON.stringify({
34
+ sent: 3,
35
+ accepted: 3,
36
+ batchId: 'b_abc',
37
+ cursor: { history_id: 9 },
38
+ batchLimit: 100,
39
+ attempts: 2,
40
+ }),
41
+ });
42
+ const report = await pushToCloud({ binPath: '/bin/echo', spawnFn, limit: 200, incognito: ['s1', 's2'] });
43
+ assert.deepEqual(report, {
44
+ sent: 3,
45
+ accepted: 3,
46
+ batchId: 'b_abc',
47
+ cursor: { history_id: 9 },
48
+ batchLimit: 100,
49
+ attempts: 2,
50
+ });
51
+ // Forwards the flags to the binary.
52
+ assert.deepEqual(calls[0].args, ['push', '--json', '--limit', '200', '--incognito', 's1', '--incognito', 's2']);
53
+ });
54
+ test('pushToCloud returns null when the binary is not installed (ENOENT)', async () => {
55
+ const { spawnFn } = fakeSpawn({ errorCode: 'ENOENT' });
56
+ const report = await pushToCloud({ binPath: '/bin/echo', spawnFn });
57
+ assert.equal(report, null);
58
+ });
59
+ test('pushToCloud returns null when the binary is not executable (EACCES)', async () => {
60
+ const { spawnFn } = fakeSpawn({ errorCode: 'EACCES' });
61
+ const report = await pushToCloud({ binPath: '/some/dir', spawnFn });
62
+ assert.equal(report, null);
63
+ });
64
+ test('pushToCloud returns null when not authenticated', async () => {
65
+ const { spawnFn } = fakeSpawn({ code: 1, stderr: 'error: not authenticated — run `ai-hist login`' });
66
+ const report = await pushToCloud({ binPath: '/bin/echo', spawnFn });
67
+ assert.equal(report, null);
68
+ });
69
+ test('pushToCloud rejects on other non-zero exits', async () => {
70
+ const { spawnFn } = fakeSpawn({ code: 2, stderr: 'boom: server exploded' });
71
+ await assert.rejects(pushToCloud({ binPath: '/bin/echo', spawnFn }), /ai-hist push failed \(exit 2\).*boom/);
72
+ });
73
+ test('pushToCloud treats empty stdout as a zero-record push', async () => {
74
+ const { spawnFn } = fakeSpawn({ stdout: '' });
75
+ const report = await pushToCloud({ binPath: '/bin/echo', spawnFn });
76
+ assert.deepEqual(report, { sent: 0, accepted: 0, batchId: null, cursor: undefined });
77
+ });
78
+ test('pushToCloud rejects when the binary prints malformed JSON', async () => {
79
+ const { spawnFn } = fakeSpawn({ code: 0, stdout: '{ truncated' });
80
+ await assert.rejects(pushToCloud({ binPath: '/bin/echo', spawnFn }), /could not parse ai-hist push output/);
81
+ });
82
+ //# sourceMappingURL=cloud-push.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cloud-push.test.js","sourceRoot":"","sources":["../src/cloud-push.test.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -13,6 +13,8 @@
13
13
  */
14
14
  import { type Database } from 'sql.js';
15
15
  import { type TrajectoryDecision, type TrajectoryRetrospective } from './trajectory-sources.js';
16
+ import { type CatalogSession, type ListCatalogOptions, type SessionCatalogPage } from './session-catalog.js';
17
+ export { SESSION_CATALOG_CONTRACT_VERSION, DiscoveryError, discoverSessions, type CatalogSession, type CatalogCursor, type SessionCatalogPage, type ListCatalogOptions, type DiscoverSessionsOptions, type DiscoverResult, type DiscoverySummary, type DiscoveryDiagnostic, type DiscoveryCounters, type ProviderDiscoverySummary, type SourceExemption, } from './session-catalog.js';
16
18
  export type Source = 'claude' | 'codex' | 'cursor' | 'grok' | 'relay' | 'trajectory' | 'opencode';
17
19
  export interface HistoryEntry {
18
20
  id: number;
@@ -23,6 +25,12 @@ export interface HistoryEntry {
23
25
  timestampMs: number;
24
26
  gitBranch: string | null;
25
27
  }
28
+ /**
29
+ * The pre-catalog `sessions` row shape, kept for backward compatibility.
30
+ * New code wants {@link CatalogSession} (via `listSessionCatalog`), which
31
+ * covers the whole catalog row: first prompt, models, originator, repo
32
+ * identity, and discovery state.
33
+ */
26
34
  export interface SessionMeta {
27
35
  sessionId: string;
28
36
  source: Source;
@@ -73,6 +81,44 @@ export interface SessionSummary {
73
81
  firstActivityMs: number;
74
82
  promptCount: number;
75
83
  }
84
+ /**
85
+ * One normalized transcript event from the `session_events` table: user and
86
+ * assistant text, thinking, tool calls, and tool results, in order.
87
+ * `tokenUsage` is the parsed `token_json` blob — Claude's per-message
88
+ * `usage` shape, or a Codex per-request delta (`input_tokens` inclusive of
89
+ * `cached_input_tokens`). Claude repeats the same usage on every content
90
+ * block of a message, so sum per distinct `messageId`, not per row.
91
+ */
92
+ export interface SessionEvent {
93
+ id: number;
94
+ source: Source;
95
+ sessionId: string;
96
+ project: string | null;
97
+ cwd: string | null;
98
+ gitBranch: string | null;
99
+ messageId: string | null;
100
+ parentId: string | null;
101
+ tsMs: number;
102
+ role: 'user' | 'assistant' | 'tool_result';
103
+ kind: 'text' | 'thinking' | 'tool_use' | 'tool_result';
104
+ text: string | null;
105
+ model: string | null;
106
+ tokenUsage: Record<string, unknown> | null;
107
+ eventUid: string;
108
+ }
109
+ /** One tool invocation from the `tool_calls` table. */
110
+ export interface SessionToolCall {
111
+ id: number;
112
+ source: Source;
113
+ sessionId: string;
114
+ messageId: string | null;
115
+ toolUseId: string;
116
+ name: string;
117
+ target: string | null;
118
+ argsJson: string | null;
119
+ isError: boolean | null;
120
+ tsMs: number | null;
121
+ }
76
122
  export interface ListOptions {
77
123
  source?: Source;
78
124
  project?: string;
@@ -144,8 +190,8 @@ export interface OpenOptions {
144
190
  /**
145
191
  * What to do when the SQLite DB is missing:
146
192
  * - `'jsonl'` (default): scan local Claude/Codex/Cursor/Grok history files
147
- * directly into an in-memory SQLite — works without the Python
148
- * `ai-hist sync` tool installed.
193
+ * directly into an in-memory SQLite — works without the native
194
+ * `ai-hist` CLI installed.
149
195
  * - `'error'`: throw with an install hint (legacy 0.1.x behavior).
150
196
  */
151
197
  fallback?: 'jsonl' | 'error';
@@ -177,7 +223,7 @@ export declare class AiHist {
177
223
  * mode: a comma-separated list of the scanned source paths.
178
224
  */
179
225
  get dbPath(): string;
180
- /** Which data path was used: `'sqlite'` (Python tool) or `'jsonl'` (fallback). */
226
+ /** Which data path was used: `'sqlite'` (on-disk SQLite database) or `'jsonl'` (fallback). */
181
227
  get sourceKind(): 'sqlite' | 'jsonl';
182
228
  /** Server/client-wide project scope applied to every read, if configured. */
183
229
  get projectScope(): string | undefined;
@@ -206,20 +252,87 @@ export declare class AiHist {
206
252
  * Group history into sessions, ordered by last activity (newest first).
207
253
  * Sessions without a `session_id` are skipped.
208
254
  *
255
+ * This derives sessions from the `history` table, so it only ever sees
256
+ * sessions a full `ai-hist sync` has ingested, and it pays a window-function
257
+ * pass over history to do it. For the fast and complete path — every session
258
+ * the catalog knows, including ones only shallow discovery has seen, with
259
+ * cwd, branch, models, originator and repo identity attached — use
260
+ * {@link AiHist.listSessionCatalog}. This method stays as-is for callers
261
+ * that want prompt counts and the project-derived grouping.
262
+ *
209
263
  * Implementation note: this used to use a correlated scalar subquery
210
264
  * to pick `first_prompt`, which ran in O(sessions × rows) — ~19s on a
211
265
  * 35K-row DB. Switched to `ROW_NUMBER() OVER (PARTITION BY session_id
212
266
  * ORDER BY timestamp_ms)` so first-prompt picking is a single pass
213
267
  * over the table (~300ms on the same DB). Plus the index ensure step
214
- * in `openAiHist` keeps it fast even when the DB was written by the
215
- * older Python CLI that didn't create `idx_history_session`.
268
+ * in `openAiHist` keeps it fast even when the DB was written by an
269
+ * older CLI that didn't create `idx_history_session`.
216
270
  */
217
271
  listSessions(opts?: ListOptions): SessionSummary[];
272
+ /**
273
+ * The session catalog, newest first — one indexed query over `sessions` and
274
+ * nothing else.
275
+ *
276
+ * No provider transcript is opened and neither `history` nor
277
+ * `session_events` is touched, so this stays fast on first paint even with
278
+ * thousands of historical sessions. It is the read side of the native
279
+ * `ai-hist sessions list`; populate the catalog with `discoverSessions()`
280
+ * (or a full `ai-hist sync`).
281
+ *
282
+ * Details worth knowing:
283
+ * - `trajectory` rows are excluded defensively — trajectories are derived
284
+ * records, not sessions, and must never appear in a session list.
285
+ * - The catalog's total order is
286
+ * `(last_activity_ms DESC, source ASC, session_id ASC)`. SQLite sorts
287
+ * NULL lowest, so rows of unknown recency land last under `DESC` without
288
+ * a helper expression. Recency alone is not a key: one discovery pass can
289
+ * stamp many sessions with the same mtime-derived millisecond, which is
290
+ * why the identity columns are part of the order.
291
+ * - To page, use {@link AiHist.listSessionCatalogPage} and follow its
292
+ * `nextCursor`. `beforeMs` is only a *coarse* cutoff ("anything before
293
+ * last Tuesday"): it cannot separate rows sharing a millisecond, so a
294
+ * walk built on it drops every row tied with a page boundary. It is
295
+ * ignored when `after` is set.
296
+ * - A configured `projectScope` constrains rows by `cwd`, like
297
+ * `getHandoff`. Sources with no working directory (relay) therefore drop
298
+ * out of a scoped listing.
299
+ * - In JSONL fallback mode (no SQLite database) the catalog is empty and
300
+ * this returns `[]`: the fallback scan builds `history` rows only, and
301
+ * only the native discovery engine writes the catalog.
302
+ */
303
+ listSessionCatalog(opts?: ListCatalogOptions): CatalogSession[];
304
+ /**
305
+ * {@link AiHist.listSessionCatalog} plus the cursor that continues it.
306
+ *
307
+ * Follow `nextCursor` until it comes back `null` to walk the whole catalog:
308
+ * no skipped and no duplicated rows, even when a whole page shares one
309
+ * millisecond, and the undated tail stays reachable from a dated cursor.
310
+ * `nextCursor` is non-null only when the page filled its limit, mirroring
311
+ * the native `list_session_catalog_page` and the CLI's `next_cursor`.
312
+ */
313
+ listSessionCatalogPage(opts?: ListCatalogOptions): SessionCatalogPage;
218
314
  /** All prompts in a session, ordered oldest → newest. */
219
315
  getSession(sessionId: string, opts?: Pick<ListOptions, 'source' | 'tag'>): HistoryEntry[];
316
+ /**
317
+ * All normalized events for a session, oldest first — messages, thinking,
318
+ * tool calls, and tool results with per-event token usage where the
319
+ * transcript provides it. Returns `[]` when the database predates the
320
+ * `session_events` table (or in JSONL fallback mode, which has no events).
321
+ *
322
+ * Size caveat: this SDK loads the whole database file into memory, so on
323
+ * large event-bearing databases prefer streaming from the native CLI
324
+ * (`ai-hist events <session-id> --json`).
325
+ */
326
+ getSessionEvents(sessionId: string, opts?: {
327
+ source?: Source;
328
+ }): SessionEvent[];
329
+ /** All tool calls for a session, oldest first; `[]` on pre-events databases. */
330
+ getToolCalls(sessionId: string, opts?: {
331
+ source?: Source;
332
+ }): SessionToolCall[];
220
333
  /**
221
334
  * Substring search across prompt + project, case-insensitive, recent
222
- * matches first. The Python CLI uses FTS5; this SDK uses LIKE because
335
+ * matches first. The native CLI uses FTS5; this SDK uses LIKE because
223
336
  * sql.js's default WASM build doesn't ship the FTS5 module. Plenty fast
224
337
  * for the ai-hist scale (~tens of thousands of rows); revisit if a
225
338
  * future consumer needs phrase/boolean queries.