ai-hist 0.1.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.
- package/README.md +73 -0
- package/dist/index.d.ts +102 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +267 -0
- package/dist/index.js.map +1 -0
- package/package.json +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# ai-hist (TypeScript SDK)
|
|
2
|
+
|
|
3
|
+
Thin TypeScript SDK for the [ai-hist](../README.md) SQLite database. Reads the same file the Python `ai-hist sync` tool writes — this package never writes, sync stays the Python tool's job.
|
|
4
|
+
|
|
5
|
+
Built to let Electron / Node consumers (e.g. the `pear` desktop app) render a Codex/Claude-style "previous conversations" list backed by your local ai-hist history.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install ai-hist
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`better-sqlite3` is a native dep — when bundling into an Electron app, run `electron-rebuild` (or your bundler's equivalent) after install.
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { AiHist, resumeCommand } from 'ai-hist';
|
|
19
|
+
|
|
20
|
+
const hist = new AiHist(); // uses $AI_HIST_DB or ~/.local/share/ai-hist/ai-history.db
|
|
21
|
+
try {
|
|
22
|
+
const sessions = hist.listSessions({ limit: 20 });
|
|
23
|
+
for (const s of sessions) {
|
|
24
|
+
console.log(`[${s.source}] ${s.firstPrompt.slice(0, 60)} (${s.promptCount} prompts)`);
|
|
25
|
+
console.log(' resume:', resumeCommand(s));
|
|
26
|
+
}
|
|
27
|
+
} finally {
|
|
28
|
+
hist.close();
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## API
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
new AiHist(opts?: { dbPath?: string }) // readonly open
|
|
36
|
+
hist.close()
|
|
37
|
+
hist.dbPath: string
|
|
38
|
+
|
|
39
|
+
hist.recent(opts?): HistoryEntry[] // newest prompts first
|
|
40
|
+
hist.listSessions(opts?): SessionSummary[] // grouped by session_id, last-activity DESC
|
|
41
|
+
hist.getSession(sessionId): HistoryEntry[] // all prompts in a session, oldest first
|
|
42
|
+
hist.search(query, opts?): HistoryEntry[] // FTS5 query, recent matches first
|
|
43
|
+
hist.stats(): Stats // counts + date range
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
All list-style methods accept `{ source?, project?, limit?, beforeMs? }`. `beforeMs` is the cursor for paginating older results.
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
resumeCommand(entry): string | null // shell command per source; null for relay
|
|
50
|
+
defaultDbPath(): string // resolve env / OS default
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Schema (canonical, owned by the Python tool)
|
|
54
|
+
|
|
55
|
+
```sql
|
|
56
|
+
CREATE TABLE history (
|
|
57
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
58
|
+
source TEXT NOT NULL, -- 'claude' | 'codex' | 'cursor' | 'relay'
|
|
59
|
+
session_id TEXT,
|
|
60
|
+
project TEXT,
|
|
61
|
+
prompt TEXT NOT NULL,
|
|
62
|
+
timestamp_ms INTEGER NOT NULL,
|
|
63
|
+
UNIQUE(source, timestamp_ms, prompt)
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
CREATE VIRTUAL TABLE history_fts USING fts5(prompt, project, content='history', content_rowid='id');
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
If the Python sync tool's schema changes, bump this SDK's major version. Consumers pin the SDK version they were built against.
|
|
70
|
+
|
|
71
|
+
## License
|
|
72
|
+
|
|
73
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript SDK for reading the ai-hist SQLite database.
|
|
3
|
+
*
|
|
4
|
+
* Backed by sql.js (WASM SQLite) so the package has zero native build
|
|
5
|
+
* requirements — works in Electron, Node, and browser contexts without
|
|
6
|
+
* needing electron-rebuild. The SDK reads the same file the Python CLI
|
|
7
|
+
* maintains (default `~/.local/share/ai-hist/ai-history.db`, or
|
|
8
|
+
* `$AI_HIST_DB`); the Python tool stays the canonical sync engine.
|
|
9
|
+
*
|
|
10
|
+
* Trade-off vs better-sqlite3: sql.js loads the whole DB file into
|
|
11
|
+
* memory. Fine for the ai-hist scale (tens of thousands of rows, MBs
|
|
12
|
+
* of data); revisit if anyone hits millions.
|
|
13
|
+
*/
|
|
14
|
+
import { type Database } from 'sql.js';
|
|
15
|
+
export type Source = 'claude' | 'codex' | 'cursor' | 'relay';
|
|
16
|
+
export interface HistoryEntry {
|
|
17
|
+
id: number;
|
|
18
|
+
source: Source;
|
|
19
|
+
sessionId: string | null;
|
|
20
|
+
project: string | null;
|
|
21
|
+
prompt: string;
|
|
22
|
+
timestampMs: number;
|
|
23
|
+
}
|
|
24
|
+
export interface SessionSummary {
|
|
25
|
+
sessionId: string;
|
|
26
|
+
source: Source;
|
|
27
|
+
project: string | null;
|
|
28
|
+
firstPrompt: string;
|
|
29
|
+
lastActivityMs: number;
|
|
30
|
+
firstActivityMs: number;
|
|
31
|
+
promptCount: number;
|
|
32
|
+
}
|
|
33
|
+
export interface ListOptions {
|
|
34
|
+
source?: Source;
|
|
35
|
+
project?: string;
|
|
36
|
+
/** Default 50. */
|
|
37
|
+
limit?: number;
|
|
38
|
+
/**
|
|
39
|
+
* Paginate older than this timestamp (exclusive). Use the
|
|
40
|
+
* `lastActivityMs` / `timestampMs` of the last item from the previous page.
|
|
41
|
+
*/
|
|
42
|
+
beforeMs?: number;
|
|
43
|
+
}
|
|
44
|
+
export type SearchOptions = ListOptions;
|
|
45
|
+
export interface Stats {
|
|
46
|
+
total: number;
|
|
47
|
+
bySource: Partial<Record<Source, number>>;
|
|
48
|
+
byProject: Array<{
|
|
49
|
+
project: string;
|
|
50
|
+
count: number;
|
|
51
|
+
}>;
|
|
52
|
+
firstTimestampMs: number | null;
|
|
53
|
+
lastTimestampMs: number | null;
|
|
54
|
+
}
|
|
55
|
+
/** Resolve the SQLite path the Python CLI writes to. */
|
|
56
|
+
export declare function defaultDbPath(): string;
|
|
57
|
+
/**
|
|
58
|
+
* Open an `AiHist` reader. Async because sql.js initializes its WASM
|
|
59
|
+
* runtime lazily. Each call reads the DB file from disk; the resulting
|
|
60
|
+
* instance is a snapshot — to see new prompts written by the Python sync,
|
|
61
|
+
* call `reload()` (or open a fresh instance).
|
|
62
|
+
*/
|
|
63
|
+
export declare function openAiHist(opts?: {
|
|
64
|
+
dbPath?: string;
|
|
65
|
+
}): Promise<AiHist>;
|
|
66
|
+
export declare class AiHist {
|
|
67
|
+
private readonly db;
|
|
68
|
+
private readonly _dbPath;
|
|
69
|
+
private closed;
|
|
70
|
+
/** @internal — use `openAiHist({ dbPath })` to construct. */
|
|
71
|
+
constructor(db: Database, dbPath: string);
|
|
72
|
+
get dbPath(): string;
|
|
73
|
+
close(): void;
|
|
74
|
+
/** Most recent prompts, newest first. */
|
|
75
|
+
recent(opts?: ListOptions): HistoryEntry[];
|
|
76
|
+
/**
|
|
77
|
+
* Group history into sessions, ordered by last activity (newest first).
|
|
78
|
+
* Sessions without a `session_id` are skipped.
|
|
79
|
+
*/
|
|
80
|
+
listSessions(opts?: ListOptions): SessionSummary[];
|
|
81
|
+
/** All prompts in a session, ordered oldest → newest. */
|
|
82
|
+
getSession(sessionId: string): HistoryEntry[];
|
|
83
|
+
/**
|
|
84
|
+
* Substring search across prompt + project, case-insensitive, recent
|
|
85
|
+
* matches first. The Python CLI uses FTS5; this SDK uses LIKE because
|
|
86
|
+
* sql.js's default WASM build doesn't ship the FTS5 module. Plenty fast
|
|
87
|
+
* for the ai-hist scale (~tens of thousands of rows); revisit if a
|
|
88
|
+
* future consumer needs phrase/boolean queries.
|
|
89
|
+
*
|
|
90
|
+
* The query is matched literally — `%` and `_` are escaped so users can
|
|
91
|
+
* search for them. Empty queries return an empty array.
|
|
92
|
+
*/
|
|
93
|
+
search(query: string, opts?: SearchOptions): HistoryEntry[];
|
|
94
|
+
/** Counts + date range, mirroring `ai-hist stats`. */
|
|
95
|
+
stats(): Stats;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Resume command for an entry/session, matching what `ai-hist show` prints.
|
|
99
|
+
* Returns `null` for sources that don't have a resume affordance (relay).
|
|
100
|
+
*/
|
|
101
|
+
export declare function resumeCommand(entry: Pick<HistoryEntry, 'source' | 'sessionId' | 'project'>): string | null;
|
|
102
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAkB,EAAE,KAAK,QAAQ,EAAoB,MAAM,QAAQ,CAAC;AAKpE,MAAM,MAAM,MAAM,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE7D,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kBAAkB;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,MAAM,aAAa,GAAG,WAAW,CAAC;AAExC,MAAM,WAAW,KAAK;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC1C,SAAS,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACrD,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAED,wDAAwD;AACxD,wBAAgB,aAAa,IAAI,MAAM,CAItC;AAUD;;;;;GAKG;AACH,wBAAsB,UAAU,CAAC,IAAI,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAgBhF;AAyDD,qBAAa,MAAM;IACjB,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAW;IAC9B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,MAAM,CAAS;IAEvB,6DAA6D;gBACjD,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM;IAKxC,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED,KAAK,IAAI,IAAI;IAMb,yCAAyC;IACzC,MAAM,CAAC,IAAI,GAAE,WAAgB,GAAG,YAAY,EAAE;IAc9C;;;OAGG;IACH,YAAY,CAAC,IAAI,GAAE,WAAgB,GAAG,cAAc,EAAE;IAsDtD,yDAAyD;IACzD,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,YAAY,EAAE;IAW7C;;;;;;;;;OASG;IACH,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,GAAE,aAAkB,GAAG,YAAY,EAAE;IAiC/D,sDAAsD;IACtD,KAAK,IAAI,KAAK;CAgCf;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,IAAI,CAAC,YAAY,EAAE,QAAQ,GAAG,WAAW,GAAG,SAAS,CAAC,GAC5D,MAAM,GAAG,IAAI,CAkBf"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript SDK for reading the ai-hist SQLite database.
|
|
3
|
+
*
|
|
4
|
+
* Backed by sql.js (WASM SQLite) so the package has zero native build
|
|
5
|
+
* requirements — works in Electron, Node, and browser contexts without
|
|
6
|
+
* needing electron-rebuild. The SDK reads the same file the Python CLI
|
|
7
|
+
* maintains (default `~/.local/share/ai-hist/ai-history.db`, or
|
|
8
|
+
* `$AI_HIST_DB`); the Python tool stays the canonical sync engine.
|
|
9
|
+
*
|
|
10
|
+
* Trade-off vs better-sqlite3: sql.js loads the whole DB file into
|
|
11
|
+
* memory. Fine for the ai-hist scale (tens of thousands of rows, MBs
|
|
12
|
+
* of data); revisit if anyone hits millions.
|
|
13
|
+
*/
|
|
14
|
+
import initSqlJs from 'sql.js';
|
|
15
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
16
|
+
import { homedir } from 'node:os';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
/** Resolve the SQLite path the Python CLI writes to. */
|
|
19
|
+
export function defaultDbPath() {
|
|
20
|
+
const fromEnv = process.env.AI_HIST_DB;
|
|
21
|
+
if (fromEnv && fromEnv.trim().length > 0)
|
|
22
|
+
return fromEnv;
|
|
23
|
+
return join(homedir(), '.local', 'share', 'ai-hist', 'ai-history.db');
|
|
24
|
+
}
|
|
25
|
+
let _sqlPromise = null;
|
|
26
|
+
function getSqlJs() {
|
|
27
|
+
if (!_sqlPromise) {
|
|
28
|
+
_sqlPromise = initSqlJs();
|
|
29
|
+
}
|
|
30
|
+
return _sqlPromise;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Open an `AiHist` reader. Async because sql.js initializes its WASM
|
|
34
|
+
* runtime lazily. Each call reads the DB file from disk; the resulting
|
|
35
|
+
* instance is a snapshot — to see new prompts written by the Python sync,
|
|
36
|
+
* call `reload()` (or open a fresh instance).
|
|
37
|
+
*/
|
|
38
|
+
export async function openAiHist(opts = {}) {
|
|
39
|
+
const dbPath = opts.dbPath ?? defaultDbPath();
|
|
40
|
+
// Surface a clearer error than sql.js's bare "file not found" when the
|
|
41
|
+
// user hasn't run the Python sync at least once yet.
|
|
42
|
+
try {
|
|
43
|
+
statSync(dbPath);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
throw new Error(`ai-hist database not found at ${dbPath}. Run \`ai-hist sync\` first ` +
|
|
47
|
+
`(see https://github.com/khaliqgant/ai-hist).`);
|
|
48
|
+
}
|
|
49
|
+
const SQL = await getSqlJs();
|
|
50
|
+
const fileBuffer = readFileSync(dbPath);
|
|
51
|
+
const db = new SQL.Database(fileBuffer);
|
|
52
|
+
return new AiHist(db, dbPath);
|
|
53
|
+
}
|
|
54
|
+
function rowToEntry(row) {
|
|
55
|
+
return {
|
|
56
|
+
id: row.id,
|
|
57
|
+
source: row.source,
|
|
58
|
+
sessionId: row.session_id,
|
|
59
|
+
project: row.project,
|
|
60
|
+
prompt: row.prompt,
|
|
61
|
+
timestampMs: row.timestamp_ms,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function buildFilters(opts) {
|
|
65
|
+
const clauses = [];
|
|
66
|
+
const params = [];
|
|
67
|
+
if (opts.source) {
|
|
68
|
+
clauses.push('source = ?');
|
|
69
|
+
params.push(opts.source);
|
|
70
|
+
}
|
|
71
|
+
if (opts.project) {
|
|
72
|
+
clauses.push('project = ?');
|
|
73
|
+
params.push(opts.project);
|
|
74
|
+
}
|
|
75
|
+
if (typeof opts.beforeMs === 'number') {
|
|
76
|
+
clauses.push('timestamp_ms < ?');
|
|
77
|
+
params.push(opts.beforeMs);
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
sql: clauses.length > 0 ? ` AND ${clauses.join(' AND ')}` : '',
|
|
81
|
+
params,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function runQuery(db, sql, params) {
|
|
85
|
+
const stmt = db.prepare(sql);
|
|
86
|
+
try {
|
|
87
|
+
stmt.bind(params);
|
|
88
|
+
const rows = [];
|
|
89
|
+
while (stmt.step()) {
|
|
90
|
+
rows.push(stmt.getAsObject());
|
|
91
|
+
}
|
|
92
|
+
return rows;
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
stmt.free();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
export class AiHist {
|
|
99
|
+
db;
|
|
100
|
+
_dbPath;
|
|
101
|
+
closed = false;
|
|
102
|
+
/** @internal — use `openAiHist({ dbPath })` to construct. */
|
|
103
|
+
constructor(db, dbPath) {
|
|
104
|
+
this.db = db;
|
|
105
|
+
this._dbPath = dbPath;
|
|
106
|
+
}
|
|
107
|
+
get dbPath() {
|
|
108
|
+
return this._dbPath;
|
|
109
|
+
}
|
|
110
|
+
close() {
|
|
111
|
+
if (this.closed)
|
|
112
|
+
return;
|
|
113
|
+
this.db.close();
|
|
114
|
+
this.closed = true;
|
|
115
|
+
}
|
|
116
|
+
/** Most recent prompts, newest first. */
|
|
117
|
+
recent(opts = {}) {
|
|
118
|
+
const limit = opts.limit ?? 50;
|
|
119
|
+
const { sql, params } = buildFilters(opts);
|
|
120
|
+
return runQuery(this.db, `SELECT id, source, session_id, project, prompt, timestamp_ms
|
|
121
|
+
FROM history
|
|
122
|
+
WHERE 1=1${sql}
|
|
123
|
+
ORDER BY timestamp_ms DESC
|
|
124
|
+
LIMIT ?`, [...params, limit]).map(rowToEntry);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Group history into sessions, ordered by last activity (newest first).
|
|
128
|
+
* Sessions without a `session_id` are skipped.
|
|
129
|
+
*/
|
|
130
|
+
listSessions(opts = {}) {
|
|
131
|
+
const limit = opts.limit ?? 50;
|
|
132
|
+
const { sql, params } = buildFilters(opts);
|
|
133
|
+
const rows = runQuery(this.db, `WITH filtered AS (
|
|
134
|
+
SELECT id, source, session_id, project, prompt, timestamp_ms
|
|
135
|
+
FROM history
|
|
136
|
+
WHERE session_id IS NOT NULL AND session_id != ''${sql}
|
|
137
|
+
)
|
|
138
|
+
SELECT
|
|
139
|
+
session_id,
|
|
140
|
+
source,
|
|
141
|
+
project,
|
|
142
|
+
prompt_count,
|
|
143
|
+
first_activity_ms,
|
|
144
|
+
last_activity_ms,
|
|
145
|
+
(SELECT prompt FROM filtered f2
|
|
146
|
+
WHERE f2.session_id = grouped.session_id
|
|
147
|
+
ORDER BY f2.timestamp_ms ASC LIMIT 1) AS first_prompt
|
|
148
|
+
FROM (
|
|
149
|
+
SELECT
|
|
150
|
+
session_id,
|
|
151
|
+
source,
|
|
152
|
+
project,
|
|
153
|
+
COUNT(*) AS prompt_count,
|
|
154
|
+
MIN(timestamp_ms) AS first_activity_ms,
|
|
155
|
+
MAX(timestamp_ms) AS last_activity_ms
|
|
156
|
+
FROM filtered
|
|
157
|
+
GROUP BY session_id, source, project
|
|
158
|
+
) AS grouped
|
|
159
|
+
ORDER BY last_activity_ms DESC
|
|
160
|
+
LIMIT ?`, [...params, limit]);
|
|
161
|
+
return rows.map((row) => ({
|
|
162
|
+
sessionId: row.session_id,
|
|
163
|
+
source: row.source,
|
|
164
|
+
project: row.project,
|
|
165
|
+
firstPrompt: row.first_prompt,
|
|
166
|
+
lastActivityMs: row.last_activity_ms,
|
|
167
|
+
firstActivityMs: row.first_activity_ms,
|
|
168
|
+
promptCount: row.prompt_count,
|
|
169
|
+
}));
|
|
170
|
+
}
|
|
171
|
+
/** All prompts in a session, ordered oldest → newest. */
|
|
172
|
+
getSession(sessionId) {
|
|
173
|
+
return runQuery(this.db, `SELECT id, source, session_id, project, prompt, timestamp_ms
|
|
174
|
+
FROM history
|
|
175
|
+
WHERE session_id = ?
|
|
176
|
+
ORDER BY timestamp_ms ASC`, [sessionId]).map(rowToEntry);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Substring search across prompt + project, case-insensitive, recent
|
|
180
|
+
* matches first. The Python CLI uses FTS5; this SDK uses LIKE because
|
|
181
|
+
* sql.js's default WASM build doesn't ship the FTS5 module. Plenty fast
|
|
182
|
+
* for the ai-hist scale (~tens of thousands of rows); revisit if a
|
|
183
|
+
* future consumer needs phrase/boolean queries.
|
|
184
|
+
*
|
|
185
|
+
* The query is matched literally — `%` and `_` are escaped so users can
|
|
186
|
+
* search for them. Empty queries return an empty array.
|
|
187
|
+
*/
|
|
188
|
+
search(query, opts = {}) {
|
|
189
|
+
const trimmed = query.trim();
|
|
190
|
+
if (!trimmed)
|
|
191
|
+
return [];
|
|
192
|
+
const limit = opts.limit ?? 50;
|
|
193
|
+
const escaped = trimmed.replace(/\\/g, '\\\\').replace(/%/g, '\\%').replace(/_/g, '\\_');
|
|
194
|
+
const pattern = `%${escaped}%`;
|
|
195
|
+
const clauses = [
|
|
196
|
+
`(LOWER(prompt) LIKE LOWER(?) ESCAPE '\\' OR LOWER(COALESCE(project, '')) LIKE LOWER(?) ESCAPE '\\')`,
|
|
197
|
+
];
|
|
198
|
+
const params = [pattern, pattern];
|
|
199
|
+
if (opts.source) {
|
|
200
|
+
clauses.push('source = ?');
|
|
201
|
+
params.push(opts.source);
|
|
202
|
+
}
|
|
203
|
+
if (opts.project) {
|
|
204
|
+
clauses.push('project = ?');
|
|
205
|
+
params.push(opts.project);
|
|
206
|
+
}
|
|
207
|
+
if (typeof opts.beforeMs === 'number') {
|
|
208
|
+
clauses.push('timestamp_ms < ?');
|
|
209
|
+
params.push(opts.beforeMs);
|
|
210
|
+
}
|
|
211
|
+
return runQuery(this.db, `SELECT id, source, session_id, project, prompt, timestamp_ms
|
|
212
|
+
FROM history
|
|
213
|
+
WHERE ${clauses.join(' AND ')}
|
|
214
|
+
ORDER BY timestamp_ms DESC
|
|
215
|
+
LIMIT ?`, [...params, limit]).map(rowToEntry);
|
|
216
|
+
}
|
|
217
|
+
/** Counts + date range, mirroring `ai-hist stats`. */
|
|
218
|
+
stats() {
|
|
219
|
+
const total = runQuery(this.db, 'SELECT COUNT(*) AS c FROM history', [])[0]?.c ?? 0;
|
|
220
|
+
const bySourceRows = runQuery(this.db, 'SELECT source, COUNT(*) AS c FROM history GROUP BY source', []);
|
|
221
|
+
const bySource = {};
|
|
222
|
+
for (const row of bySourceRows) {
|
|
223
|
+
bySource[row.source] = row.c;
|
|
224
|
+
}
|
|
225
|
+
const byProject = runQuery(this.db, `SELECT project, COUNT(*) AS c FROM history
|
|
226
|
+
WHERE project IS NOT NULL AND project != ''
|
|
227
|
+
GROUP BY project ORDER BY c DESC LIMIT 10`, []).map((row) => ({ project: row.project, count: row.c }));
|
|
228
|
+
const range = runQuery(this.db, 'SELECT MIN(timestamp_ms) AS mn, MAX(timestamp_ms) AS mx FROM history', [])[0];
|
|
229
|
+
return {
|
|
230
|
+
total,
|
|
231
|
+
bySource,
|
|
232
|
+
byProject,
|
|
233
|
+
firstTimestampMs: range?.mn ?? null,
|
|
234
|
+
lastTimestampMs: range?.mx ?? null,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Resume command for an entry/session, matching what `ai-hist show` prints.
|
|
240
|
+
* Returns `null` for sources that don't have a resume affordance (relay).
|
|
241
|
+
*/
|
|
242
|
+
export function resumeCommand(entry) {
|
|
243
|
+
if (!entry.sessionId)
|
|
244
|
+
return null;
|
|
245
|
+
switch (entry.source) {
|
|
246
|
+
case 'claude':
|
|
247
|
+
return entry.project
|
|
248
|
+
? `cd ${shellQuote(entry.project)} && claude --resume ${shellQuote(entry.sessionId)}`
|
|
249
|
+
: `claude --resume ${shellQuote(entry.sessionId)}`;
|
|
250
|
+
case 'codex':
|
|
251
|
+
return `codex resume ${shellQuote(entry.sessionId)}`;
|
|
252
|
+
case 'cursor':
|
|
253
|
+
return entry.project
|
|
254
|
+
? `cd ${shellQuote(entry.project)} && cursor-agent --resume=${shellQuote(entry.sessionId)}`
|
|
255
|
+
: `cursor-agent --resume=${shellQuote(entry.sessionId)}`;
|
|
256
|
+
case 'relay':
|
|
257
|
+
return null;
|
|
258
|
+
default:
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
function shellQuote(value) {
|
|
263
|
+
if (/^[A-Za-z0-9_\-./]+$/.test(value))
|
|
264
|
+
return value;
|
|
265
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
266
|
+
}
|
|
267
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,SAA8C,MAAM,QAAQ,CAAC;AACpE,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACjD,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AA6CjC,wDAAwD;AACxD,MAAM,UAAU,aAAa;IAC3B,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC;IACvC,IAAI,OAAO,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,OAAO,CAAC;IACzD,OAAO,IAAI,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,eAAe,CAAC,CAAC;AACxE,CAAC;AAED,IAAI,WAAW,GAAgC,IAAI,CAAC;AACpD,SAAS,QAAQ;IACf,IAAI,CAAC,WAAW,EAAE,CAAC;QACjB,WAAW,GAAG,SAAS,EAAE,CAAC;IAC5B,CAAC;IACD,OAAO,WAAW,CAAC;AACrB,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,OAA4B,EAAE;IAC7D,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,aAAa,EAAE,CAAC;IAC9C,uEAAuE;IACvE,qDAAqD;IACrD,IAAI,CAAC;QACH,QAAQ,CAAC,MAAM,CAAC,CAAC;IACnB,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CACb,iCAAiC,MAAM,+BAA+B;YACpE,8CAA8C,CACjD,CAAC;IACJ,CAAC;IACD,MAAM,GAAG,GAAG,MAAM,QAAQ,EAAE,CAAC;IAC7B,MAAM,UAAU,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;IACxC,MAAM,EAAE,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;IACxC,OAAO,IAAI,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;AAChC,CAAC;AAWD,SAAS,UAAU,CAAC,GAAkB;IACpC,OAAO;QACL,EAAE,EAAE,GAAG,CAAC,EAAE;QACV,MAAM,EAAE,GAAG,CAAC,MAAgB;QAC5B,SAAS,EAAE,GAAG,CAAC,UAAU;QACzB,OAAO,EAAE,GAAG,CAAC,OAAO;QACpB,MAAM,EAAE,GAAG,CAAC,MAAM;QAClB,WAAW,EAAE,GAAG,CAAC,YAAY;KAC9B,CAAC;AACJ,CAAC;AAED,SAAS,YAAY,CAAC,IAAiB;IACrC,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,MAAM,MAAM,GAAc,EAAE,CAAC;IAC7B,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;QAChB,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QAC3B,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC3B,CAAC;IACD,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;QACjB,OAAO,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;QAC5B,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC5B,CAAC;IACD,IAAI,OAAO,IAAI,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;QACtC,OAAO,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC;QACjC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC7B,CAAC;IACD,OAAO;QACL,GAAG,EAAE,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE;QAC9D,MAAM;KACP,CAAC;AACJ,CAAC;AAED,SAAS,QAAQ,CAAI,EAAY,EAAE,GAAW,EAAE,MAAiB;IAC/D,MAAM,IAAI,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC7B,IAAI,CAAC;QACH,IAAI,CAAC,IAAI,CAAC,MAAiB,CAAC,CAAC;QAC7B,MAAM,IAAI,GAAQ,EAAE,CAAC;QACrB,OAAO,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;YACnB,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,EAAO,CAAC,CAAC;QACrC,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;YAAS,CAAC;QACT,IAAI,CAAC,IAAI,EAAE,CAAC;IACd,CAAC;AACH,CAAC;AAED,MAAM,OAAO,MAAM;IACA,EAAE,CAAW;IACb,OAAO,CAAS;IACzB,MAAM,GAAG,KAAK,CAAC;IAEvB,6DAA6D;IAC7D,YAAY,EAAY,EAAE,MAAc;QACtC,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC;IACxB,CAAC;IAED,IAAI,MAAM;QACR,OAAO,IAAI,CAAC,OAAO,CAAC;IACtB,CAAC;IAED,KAAK;QACH,IAAI,IAAI,CAAC,MAAM;YAAE,OAAO;QACxB,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC;QAChB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;IACrB,CAAC;IAED,yCAAyC;IACzC,MAAM,CAAC,OAAoB,EAAE;QAC3B,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;QAC/B,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;QAC3C,OAAO,QAAQ,CACb,IAAI,CAAC,EAAE,EACP;;kBAEY,GAAG;;eAEN,EACT,CAAC,GAAG,MAAM,EAAE,KAAK,CAAC,CACnB,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;IACpB,CAAC;IAED;;;OAGG;IACH,YAAY,CAAC,OAAoB,EAAE;QACjC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;QAC/B,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;QAC3C,MAAM,IAAI,GAAG,QAAQ,CASnB,IAAI,CAAC,EAAE,EACP;;;2DAGqD,GAAG;;;;;;;;;;;;;;;;;;;;;;;;cAwBhD,EACR,CAAC,GAAG,MAAM,EAAE,KAAK,CAAC,CACnB,CAAC;QACF,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;YACxB,SAAS,EAAE,GAAG,CAAC,UAAU;YACzB,MAAM,EAAE,GAAG,CAAC,MAAgB;YAC5B,OAAO,EAAE,GAAG,CAAC,OAAO;YACpB,WAAW,EAAE,GAAG,CAAC,YAAY;YAC7B,cAAc,EAAE,GAAG,CAAC,gBAAgB;YACpC,eAAe,EAAE,GAAG,CAAC,iBAAiB;YACtC,WAAW,EAAE,GAAG,CAAC,YAAY;SAC9B,CAAC,CAAC,CAAC;IACN,CAAC;IAED,yDAAyD;IACzD,UAAU,CAAC,SAAiB;QAC1B,OAAO,QAAQ,CACb,IAAI,CAAC,EAAE,EACP;;;iCAG2B,EAC3B,CAAC,SAAS,CAAC,CACZ,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;IACpB,CAAC;IAED;;;;;;;;;OASG;IACH,MAAM,CAAC,KAAa,EAAE,OAAsB,EAAE;QAC5C,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;QAC7B,IAAI,CAAC,OAAO;YAAE,OAAO,EAAE,CAAC;QACxB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;QAC/B,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QACzF,MAAM,OAAO,GAAG,IAAI,OAAO,GAAG,CAAC;QAC/B,MAAM,OAAO,GAAa;YACxB,qGAAqG;SACtG,CAAC;QACF,MAAM,MAAM,GAAc,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAC7C,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAChB,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;YAC3B,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC3B,CAAC;QACD,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,OAAO,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;YAC5B,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC5B,CAAC;QACD,IAAI,OAAO,IAAI,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;YACtC,OAAO,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC;YACjC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC7B,CAAC;QACD,OAAO,QAAQ,CACb,IAAI,CAAC,EAAE,EACP;;eAES,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC;;eAErB,EACT,CAAC,GAAG,MAAM,EAAE,KAAK,CAAC,CACnB,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;IACpB,CAAC;IAED,sDAAsD;IACtD,KAAK;QACH,MAAM,KAAK,GACT,QAAQ,CAAgB,IAAI,CAAC,EAAE,EAAE,mCAAmC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;QACvF,MAAM,YAAY,GAAG,QAAQ,CAC3B,IAAI,CAAC,EAAE,EACP,2DAA2D,EAC3D,EAAE,CACH,CAAC;QACF,MAAM,QAAQ,GAAoC,EAAE,CAAC;QACrD,KAAK,MAAM,GAAG,IAAI,YAAY,EAAE,CAAC;YAC/B,QAAQ,CAAC,GAAG,CAAC,MAAgB,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC;QACzC,CAAC;QACD,MAAM,SAAS,GAAG,QAAQ,CACxB,IAAI,CAAC,EAAE,EACP;;iDAE2C,EAC3C,EAAE,CACH,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACzD,MAAM,KAAK,GAAG,QAAQ,CACpB,IAAI,CAAC,EAAE,EACP,sEAAsE,EACtE,EAAE,CACH,CAAC,CAAC,CAAC,CAAC;QACL,OAAO;YACL,KAAK;YACL,QAAQ;YACR,SAAS;YACT,gBAAgB,EAAE,KAAK,EAAE,EAAE,IAAI,IAAI;YACnC,eAAe,EAAE,KAAK,EAAE,EAAE,IAAI,IAAI;SACnC,CAAC;IACJ,CAAC;CACF;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAC3B,KAA6D;IAE7D,IAAI,CAAC,KAAK,CAAC,SAAS;QAAE,OAAO,IAAI,CAAC;IAClC,QAAQ,KAAK,CAAC,MAAM,EAAE,CAAC;QACrB,KAAK,QAAQ;YACX,OAAO,KAAK,CAAC,OAAO;gBAClB,CAAC,CAAC,MAAM,UAAU,CAAC,KAAK,CAAC,OAAO,CAAC,uBAAuB,UAAU,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE;gBACrF,CAAC,CAAC,mBAAmB,UAAU,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,CAAC;QACvD,KAAK,OAAO;YACV,OAAO,gBAAgB,UAAU,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,CAAC;QACvD,KAAK,QAAQ;YACX,OAAO,KAAK,CAAC,OAAO;gBAClB,CAAC,CAAC,MAAM,UAAU,CAAC,KAAK,CAAC,OAAO,CAAC,6BAA6B,UAAU,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE;gBAC3F,CAAC,CAAC,yBAAyB,UAAU,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,CAAC;QAC7D,KAAK,OAAO;YACV,OAAO,IAAI,CAAC;QACd;YACE,OAAO,IAAI,CAAC;IAChB,CAAC;AACH,CAAC;AAED,SAAS,UAAU,CAAC,KAAa;IAC/B,IAAI,qBAAqB,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACpD,OAAO,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC;AAC7C,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ai-hist",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "TypeScript SDK for reading the ai-hist SQLite database (recent prompts, sessions, search).",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"private": false,
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "dist/index.js",
|
|
9
|
+
"types": "dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./package.json": "./package.json"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"README.md",
|
|
20
|
+
"package.json"
|
|
21
|
+
],
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/AgentWorkforce/ai-hist",
|
|
25
|
+
"directory": "sdk-ts"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsc -p tsconfig.json",
|
|
32
|
+
"dev": "tsc -p tsconfig.json --watch --preserveWatchOutput",
|
|
33
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
34
|
+
"test": "tsc -p tsconfig.json && node --test dist/*.test.js",
|
|
35
|
+
"lint": "tsc -p tsconfig.json --noEmit",
|
|
36
|
+
"prepare": "npm run build"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"sql.js": "^1.13.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/node": "^22.18.0",
|
|
43
|
+
"@types/sql.js": "^1.4.9",
|
|
44
|
+
"typescript": "^5.9.2"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=18"
|
|
48
|
+
}
|
|
49
|
+
}
|