@ftarganski/omni-ai 1.1.10 → 1.2.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 +12 -1
- package/dist/cli/bin.js +1043 -155
- package/dist/history.d.ts +1 -0
- package/dist/history.js +545 -0
- package/dist/index.d.ts +1 -0
- package/package.json +10 -5
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '@omni-ai/history';
|
package/dist/history.js
ADDED
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
// ../../packages/history/dist/parsers/claude-code.js
|
|
2
|
+
import { readdir, readFile, stat } from "fs/promises";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { glob } from "glob";
|
|
6
|
+
function extractContent(message) {
|
|
7
|
+
const raw = message?.content;
|
|
8
|
+
if (typeof raw === "string")
|
|
9
|
+
return { content: raw };
|
|
10
|
+
if (Array.isArray(raw)) {
|
|
11
|
+
const blocks = raw;
|
|
12
|
+
const text = blocks.map((b) => {
|
|
13
|
+
if (b.type === "text")
|
|
14
|
+
return b.text ?? "";
|
|
15
|
+
if (b.type === "tool_use")
|
|
16
|
+
return `[tool_use:${b.name}] ${JSON.stringify(b.input ?? {})}`;
|
|
17
|
+
if (b.type === "tool_result")
|
|
18
|
+
return typeof b.content === "string" ? b.content : JSON.stringify(b.content);
|
|
19
|
+
return "";
|
|
20
|
+
}).filter(Boolean).join("\n");
|
|
21
|
+
const toolUse = blocks.find((b) => b.type === "tool_use");
|
|
22
|
+
return { content: text, toolName: toolUse?.name };
|
|
23
|
+
}
|
|
24
|
+
return { content: "" };
|
|
25
|
+
}
|
|
26
|
+
function defaultClaudeCodeRoot() {
|
|
27
|
+
return join(homedir(), ".claude", "projects");
|
|
28
|
+
}
|
|
29
|
+
async function parseTranscriptFile(file, provider, scope) {
|
|
30
|
+
const raw = await readFile(file, "utf-8");
|
|
31
|
+
const lines = raw.split("\n").filter((l) => l.trim().length > 0);
|
|
32
|
+
const events = [];
|
|
33
|
+
const citations = [];
|
|
34
|
+
let ordinal = 0;
|
|
35
|
+
lines.forEach((line, lineIndex) => {
|
|
36
|
+
let entry;
|
|
37
|
+
try {
|
|
38
|
+
entry = JSON.parse(line);
|
|
39
|
+
} catch {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const type = typeof entry.type === "string" ? entry.type : void 0;
|
|
43
|
+
const message = entry.message;
|
|
44
|
+
if (!message || type !== "user" && type !== "assistant")
|
|
45
|
+
return;
|
|
46
|
+
const { content, toolName } = extractContent(message);
|
|
47
|
+
if (!content)
|
|
48
|
+
return;
|
|
49
|
+
const timestamp = typeof entry.timestamp === "string" ? Date.parse(entry.timestamp) : Number.NaN;
|
|
50
|
+
events.push({
|
|
51
|
+
role: message.role ?? type ?? "user",
|
|
52
|
+
content,
|
|
53
|
+
toolName,
|
|
54
|
+
ordinal: ordinal++,
|
|
55
|
+
timestamp: Number.isNaN(timestamp) ? Date.now() : timestamp
|
|
56
|
+
});
|
|
57
|
+
citations.push({ sourcePath: file, sourceLine: lineIndex + 1 });
|
|
58
|
+
});
|
|
59
|
+
if (events.length === 0)
|
|
60
|
+
return null;
|
|
61
|
+
const sourceSessionId = file.replace(/\.jsonl$/, "").split(/[/\\]/).pop() ?? file;
|
|
62
|
+
return {
|
|
63
|
+
session: {
|
|
64
|
+
id: `${provider}:${scope ?? ""}:${sourceSessionId}`,
|
|
65
|
+
provider,
|
|
66
|
+
sourceSessionId,
|
|
67
|
+
importedAt: Date.now(),
|
|
68
|
+
scope
|
|
69
|
+
},
|
|
70
|
+
events,
|
|
71
|
+
citations
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
var ClaudeCodeHistoryParser = class {
|
|
75
|
+
provider = "claude-code";
|
|
76
|
+
root;
|
|
77
|
+
constructor(options = {}) {
|
|
78
|
+
this.root = options.root ?? defaultClaudeCodeRoot();
|
|
79
|
+
}
|
|
80
|
+
async discover() {
|
|
81
|
+
const id = `${this.provider}:${this.root}`;
|
|
82
|
+
const base = {
|
|
83
|
+
id,
|
|
84
|
+
provider: this.provider,
|
|
85
|
+
path: this.root,
|
|
86
|
+
nativeImport: true
|
|
87
|
+
};
|
|
88
|
+
let entries;
|
|
89
|
+
try {
|
|
90
|
+
const info = await stat(this.root);
|
|
91
|
+
if (!info.isDirectory()) {
|
|
92
|
+
return [{ ...base, importable: false, reason: "path is not a directory" }];
|
|
93
|
+
}
|
|
94
|
+
entries = await readdir(this.root, { withFileTypes: true });
|
|
95
|
+
} catch {
|
|
96
|
+
return [{ ...base, importable: false, reason: "history directory not found" }];
|
|
97
|
+
}
|
|
98
|
+
const sources = [];
|
|
99
|
+
const looseFiles = await glob("*.jsonl", { cwd: this.root });
|
|
100
|
+
if (looseFiles.length > 0) {
|
|
101
|
+
sources.push({ ...base, importable: true });
|
|
102
|
+
}
|
|
103
|
+
for (const entry of entries) {
|
|
104
|
+
if (!entry.isDirectory())
|
|
105
|
+
continue;
|
|
106
|
+
const dir = join(this.root, entry.name);
|
|
107
|
+
const files = await glob("**/*.jsonl", { cwd: dir });
|
|
108
|
+
if (files.length === 0)
|
|
109
|
+
continue;
|
|
110
|
+
sources.push({
|
|
111
|
+
id: `${this.provider}:${dir}`,
|
|
112
|
+
provider: this.provider,
|
|
113
|
+
path: dir,
|
|
114
|
+
nativeImport: true,
|
|
115
|
+
importable: true,
|
|
116
|
+
scope: entry.name
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
if (sources.length === 0) {
|
|
120
|
+
return [{ ...base, importable: false, reason: "no .jsonl transcripts found" }];
|
|
121
|
+
}
|
|
122
|
+
return sources;
|
|
123
|
+
}
|
|
124
|
+
async import(source) {
|
|
125
|
+
const files = await glob("**/*.jsonl", { cwd: source.path, absolute: true });
|
|
126
|
+
const results = [];
|
|
127
|
+
for (const file of files) {
|
|
128
|
+
const result = await parseTranscriptFile(file, source.provider, source.scope);
|
|
129
|
+
if (result)
|
|
130
|
+
results.push(result);
|
|
131
|
+
}
|
|
132
|
+
return results;
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// ../../packages/history/dist/parsers/codex.js
|
|
137
|
+
import { readFile as readFile2, stat as stat2 } from "fs/promises";
|
|
138
|
+
import { homedir as homedir2 } from "os";
|
|
139
|
+
import { join as join2 } from "path";
|
|
140
|
+
import { glob as glob2 } from "glob";
|
|
141
|
+
|
|
142
|
+
// ../../packages/history/dist/store.js
|
|
143
|
+
import { mkdirSync } from "fs";
|
|
144
|
+
import { dirname } from "path";
|
|
145
|
+
import Database from "better-sqlite3";
|
|
146
|
+
var sourceFromRow = (r) => ({
|
|
147
|
+
id: r.id,
|
|
148
|
+
provider: r.provider,
|
|
149
|
+
path: r.path,
|
|
150
|
+
nativeImport: Boolean(r.native_import),
|
|
151
|
+
importable: Boolean(r.importable),
|
|
152
|
+
reason: r.reason ?? void 0,
|
|
153
|
+
scope: r.scope || void 0
|
|
154
|
+
});
|
|
155
|
+
var sessionFromRow = (r) => ({
|
|
156
|
+
id: r.id,
|
|
157
|
+
provider: r.provider,
|
|
158
|
+
sourceSessionId: r.source_session_id,
|
|
159
|
+
importedAt: r.imported_at,
|
|
160
|
+
scope: r.scope || void 0
|
|
161
|
+
});
|
|
162
|
+
var eventFromRow = (r) => ({
|
|
163
|
+
id: r.id,
|
|
164
|
+
sessionId: r.session_id,
|
|
165
|
+
role: r.role,
|
|
166
|
+
content: r.content,
|
|
167
|
+
toolName: r.tool_name ?? void 0,
|
|
168
|
+
ordinal: r.ordinal,
|
|
169
|
+
timestamp: r.ts
|
|
170
|
+
});
|
|
171
|
+
var HistoryStore = class {
|
|
172
|
+
db;
|
|
173
|
+
constructor(options = {}) {
|
|
174
|
+
const path = options.path ?? "./omni-ai-history.db";
|
|
175
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
176
|
+
this.db = new Database(path);
|
|
177
|
+
this.db.pragma("journal_mode = WAL");
|
|
178
|
+
this.migrate();
|
|
179
|
+
}
|
|
180
|
+
migrate() {
|
|
181
|
+
this.db.exec(`
|
|
182
|
+
CREATE TABLE IF NOT EXISTS sources (
|
|
183
|
+
id TEXT PRIMARY KEY,
|
|
184
|
+
provider TEXT NOT NULL,
|
|
185
|
+
path TEXT NOT NULL,
|
|
186
|
+
native_import INTEGER NOT NULL,
|
|
187
|
+
importable INTEGER NOT NULL,
|
|
188
|
+
reason TEXT,
|
|
189
|
+
scope TEXT NOT NULL DEFAULT ''
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
193
|
+
id TEXT PRIMARY KEY,
|
|
194
|
+
provider TEXT NOT NULL,
|
|
195
|
+
source_session_id TEXT NOT NULL,
|
|
196
|
+
imported_at INTEGER NOT NULL,
|
|
197
|
+
scope TEXT NOT NULL DEFAULT '',
|
|
198
|
+
UNIQUE (provider, scope, source_session_id)
|
|
199
|
+
);
|
|
200
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_scope ON sessions (scope);
|
|
201
|
+
|
|
202
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
203
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
204
|
+
session_id TEXT NOT NULL REFERENCES sessions (id),
|
|
205
|
+
role TEXT NOT NULL,
|
|
206
|
+
content TEXT NOT NULL,
|
|
207
|
+
tool_name TEXT,
|
|
208
|
+
ordinal INTEGER NOT NULL,
|
|
209
|
+
ts INTEGER NOT NULL
|
|
210
|
+
);
|
|
211
|
+
CREATE INDEX IF NOT EXISTS idx_events_session ON events (session_id, ordinal);
|
|
212
|
+
|
|
213
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS events_fts USING fts5(
|
|
214
|
+
content,
|
|
215
|
+
content='events',
|
|
216
|
+
content_rowid='id'
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
CREATE TRIGGER IF NOT EXISTS events_ai AFTER INSERT ON events BEGIN
|
|
220
|
+
INSERT INTO events_fts (rowid, content) VALUES (new.id, new.content);
|
|
221
|
+
END;
|
|
222
|
+
|
|
223
|
+
CREATE TABLE IF NOT EXISTS citations (
|
|
224
|
+
event_id INTEGER NOT NULL REFERENCES events (id),
|
|
225
|
+
source_path TEXT NOT NULL,
|
|
226
|
+
source_line INTEGER,
|
|
227
|
+
PRIMARY KEY (event_id)
|
|
228
|
+
);
|
|
229
|
+
`);
|
|
230
|
+
}
|
|
231
|
+
upsertSource(source) {
|
|
232
|
+
this.db.prepare(`INSERT INTO sources (id, provider, path, native_import, importable, reason, scope)
|
|
233
|
+
VALUES (@id, @provider, @path, @nativeImport, @importable, @reason, @scope)
|
|
234
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
235
|
+
provider = excluded.provider,
|
|
236
|
+
path = excluded.path,
|
|
237
|
+
native_import = excluded.native_import,
|
|
238
|
+
importable = excluded.importable,
|
|
239
|
+
reason = excluded.reason,
|
|
240
|
+
scope = excluded.scope`).run({
|
|
241
|
+
id: source.id,
|
|
242
|
+
provider: source.provider,
|
|
243
|
+
path: source.path,
|
|
244
|
+
nativeImport: source.nativeImport ? 1 : 0,
|
|
245
|
+
importable: source.importable ? 1 : 0,
|
|
246
|
+
reason: source.reason ?? null,
|
|
247
|
+
scope: source.scope ?? ""
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
listSources(provider) {
|
|
251
|
+
const rows = provider ? this.db.prepare(`SELECT * FROM sources WHERE provider = ? ORDER BY id`).all(provider) : this.db.prepare(`SELECT * FROM sources ORDER BY provider, id`).all();
|
|
252
|
+
return rows.map(sourceFromRow);
|
|
253
|
+
}
|
|
254
|
+
/** Persists one imported session and its events/citations. Idempotent per (provider, scope, sourceSessionId). */
|
|
255
|
+
saveImportResult(provider, sessionId, result) {
|
|
256
|
+
const upsertSession = this.db.prepare(`INSERT INTO sessions (id, provider, source_session_id, imported_at, scope)
|
|
257
|
+
VALUES (@id, @provider, @sourceSessionId, @importedAt, @scope)
|
|
258
|
+
ON CONFLICT (provider, scope, source_session_id) DO UPDATE SET imported_at = excluded.imported_at`);
|
|
259
|
+
const insertEvent = this.db.prepare(`INSERT INTO events (session_id, role, content, tool_name, ordinal, ts)
|
|
260
|
+
VALUES (@sessionId, @role, @content, @toolName, @ordinal, @ts)`);
|
|
261
|
+
const insertCitation = this.db.prepare(`INSERT INTO citations (event_id, source_path, source_line)
|
|
262
|
+
VALUES (@eventId, @sourcePath, @sourceLine)
|
|
263
|
+
ON CONFLICT (event_id) DO UPDATE SET source_path = excluded.source_path, source_line = excluded.source_line`);
|
|
264
|
+
const deleteCitations = this.db.prepare(`DELETE FROM citations WHERE event_id IN (SELECT id FROM events WHERE session_id = ?)`);
|
|
265
|
+
const deleteEvents = this.db.prepare(`DELETE FROM events WHERE session_id = ?`);
|
|
266
|
+
const run = this.db.transaction(() => {
|
|
267
|
+
upsertSession.run({
|
|
268
|
+
id: sessionId,
|
|
269
|
+
provider,
|
|
270
|
+
sourceSessionId: result.session.sourceSessionId,
|
|
271
|
+
importedAt: result.session.importedAt,
|
|
272
|
+
scope: result.session.scope ?? ""
|
|
273
|
+
});
|
|
274
|
+
deleteCitations.run(sessionId);
|
|
275
|
+
deleteEvents.run(sessionId);
|
|
276
|
+
result.events.forEach((event, i) => {
|
|
277
|
+
const info = insertEvent.run({
|
|
278
|
+
sessionId,
|
|
279
|
+
role: event.role,
|
|
280
|
+
content: event.content,
|
|
281
|
+
toolName: event.toolName ?? null,
|
|
282
|
+
ordinal: event.ordinal,
|
|
283
|
+
ts: event.timestamp
|
|
284
|
+
});
|
|
285
|
+
const citation = result.citations[i];
|
|
286
|
+
if (citation) {
|
|
287
|
+
insertCitation.run({
|
|
288
|
+
eventId: info.lastInsertRowid,
|
|
289
|
+
sourcePath: citation.sourcePath,
|
|
290
|
+
sourceLine: citation.sourceLine ?? null
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
run();
|
|
296
|
+
}
|
|
297
|
+
getSession(id) {
|
|
298
|
+
const row = this.db.prepare(`SELECT * FROM sessions WHERE id = ?`).get(id);
|
|
299
|
+
return row ? sessionFromRow(row) : null;
|
|
300
|
+
}
|
|
301
|
+
listSessions(options = {}) {
|
|
302
|
+
const clauses = [];
|
|
303
|
+
const params = [];
|
|
304
|
+
if (options.provider) {
|
|
305
|
+
clauses.push("provider = ?");
|
|
306
|
+
params.push(options.provider);
|
|
307
|
+
}
|
|
308
|
+
if (options.scope) {
|
|
309
|
+
clauses.push("scope = ?");
|
|
310
|
+
params.push(options.scope);
|
|
311
|
+
}
|
|
312
|
+
const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
313
|
+
const rows = this.db.prepare(`SELECT * FROM sessions ${where} ORDER BY imported_at DESC`).all(...params);
|
|
314
|
+
return rows.map(sessionFromRow);
|
|
315
|
+
}
|
|
316
|
+
listEvents(sessionId) {
|
|
317
|
+
const rows = this.db.prepare(`SELECT * FROM events WHERE session_id = ? ORDER BY ordinal ASC`).all(sessionId);
|
|
318
|
+
return rows.map(eventFromRow);
|
|
319
|
+
}
|
|
320
|
+
getEvent(id) {
|
|
321
|
+
const row = this.db.prepare(`SELECT * FROM events WHERE id = ?`).get(id);
|
|
322
|
+
return row ? eventFromRow(row) : null;
|
|
323
|
+
}
|
|
324
|
+
/** Events immediately before/after `id` within the same session, `id` included. */
|
|
325
|
+
getEventWindow(id, window) {
|
|
326
|
+
const center = this.getEvent(id);
|
|
327
|
+
if (!center)
|
|
328
|
+
return [];
|
|
329
|
+
const rows = this.db.prepare(`SELECT * FROM events
|
|
330
|
+
WHERE session_id = ? AND ordinal BETWEEN ? AND ?
|
|
331
|
+
ORDER BY ordinal ASC`).all(center.sessionId, center.ordinal - window, center.ordinal + window);
|
|
332
|
+
return rows.map(eventFromRow);
|
|
333
|
+
}
|
|
334
|
+
getCitation(eventId) {
|
|
335
|
+
const row = this.db.prepare(`SELECT * FROM citations WHERE event_id = ?`).get(eventId);
|
|
336
|
+
return row ? { eventId: row.event_id, sourcePath: row.source_path, sourceLine: row.source_line ?? void 0 } : null;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Full-text search over event content. Session-diverse by default: at most `groupLimit`
|
|
340
|
+
* hits per session so one large session can't crowd out the rest of the result page.
|
|
341
|
+
*
|
|
342
|
+
* Scoped by default in spirit — callers (CLI, MCP skills) are expected to pass `scope`
|
|
343
|
+
* for project-aware providers so an agent working in one project never sees another
|
|
344
|
+
* project's imported history. Omitting `scope` searches across every scope.
|
|
345
|
+
*/
|
|
346
|
+
search(query, options = {}) {
|
|
347
|
+
const limit = options.limit ?? 20;
|
|
348
|
+
const groupLimit = options.groupLimit ?? 3;
|
|
349
|
+
const clauses = ["fts.content MATCH @query"];
|
|
350
|
+
const params = { query };
|
|
351
|
+
if (options.sessionId) {
|
|
352
|
+
clauses.push("e.session_id = @sessionId");
|
|
353
|
+
params.sessionId = options.sessionId;
|
|
354
|
+
}
|
|
355
|
+
if (options.scope) {
|
|
356
|
+
clauses.push("s.scope = @scope");
|
|
357
|
+
params.scope = options.scope;
|
|
358
|
+
}
|
|
359
|
+
const rows = this.db.prepare(`SELECT e.*, rank AS score FROM events_fts fts
|
|
360
|
+
JOIN events e ON e.id = fts.rowid
|
|
361
|
+
JOIN sessions s ON s.id = e.session_id
|
|
362
|
+
WHERE ${clauses.join(" AND ")}
|
|
363
|
+
ORDER BY rank`).all(params);
|
|
364
|
+
const perSession = /* @__PURE__ */ new Map();
|
|
365
|
+
const sessionCache = /* @__PURE__ */ new Map();
|
|
366
|
+
const hits = [];
|
|
367
|
+
for (const row of rows) {
|
|
368
|
+
if (hits.length >= limit)
|
|
369
|
+
break;
|
|
370
|
+
const count = perSession.get(row.session_id) ?? 0;
|
|
371
|
+
if (count >= groupLimit)
|
|
372
|
+
continue;
|
|
373
|
+
let session = sessionCache.get(row.session_id);
|
|
374
|
+
if (session === void 0) {
|
|
375
|
+
session = this.getSession(row.session_id);
|
|
376
|
+
sessionCache.set(row.session_id, session);
|
|
377
|
+
}
|
|
378
|
+
if (!session)
|
|
379
|
+
continue;
|
|
380
|
+
perSession.set(row.session_id, count + 1);
|
|
381
|
+
hits.push({ event: eventFromRow(row), session, score: row.score });
|
|
382
|
+
}
|
|
383
|
+
return hits;
|
|
384
|
+
}
|
|
385
|
+
listCitations() {
|
|
386
|
+
const rows = this.db.prepare(`SELECT * FROM citations`).all();
|
|
387
|
+
return rows.map((r) => ({
|
|
388
|
+
eventId: r.event_id,
|
|
389
|
+
sourcePath: r.source_path,
|
|
390
|
+
sourceLine: r.source_line ?? void 0
|
|
391
|
+
}));
|
|
392
|
+
}
|
|
393
|
+
close() {
|
|
394
|
+
this.db.close();
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
// ../../packages/history/dist/skills.js
|
|
399
|
+
function defaultHistoryDbPath() {
|
|
400
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? ".";
|
|
401
|
+
return `${home}/.omni-ai/history.db`;
|
|
402
|
+
}
|
|
403
|
+
function encodeProjectScope(absPath) {
|
|
404
|
+
return absPath.replace(/[^a-zA-Z0-9]/g, "-");
|
|
405
|
+
}
|
|
406
|
+
var searchHistorySkill = {
|
|
407
|
+
name: "search-history",
|
|
408
|
+
description: "Full-text search over already-imported third-party AI agent history (run `omni history import` first to populate it). Scoped to the caller's own project by default \u2014 pass allProjects:true to search every imported project.",
|
|
409
|
+
async execute(input) {
|
|
410
|
+
const scope = input.allProjects ? void 0 : input.scope ?? encodeProjectScope(process.cwd());
|
|
411
|
+
const store = new HistoryStore({ path: defaultHistoryDbPath() });
|
|
412
|
+
try {
|
|
413
|
+
const hits = store.search(input.query, { scope, limit: input.limit });
|
|
414
|
+
return { scope: scope ?? null, hits };
|
|
415
|
+
} finally {
|
|
416
|
+
store.close();
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
var showEventSkill = {
|
|
421
|
+
name: "show-history-event",
|
|
422
|
+
description: "Show one imported history event with N events of surrounding context. Refuses events that belong to a different project scope than the caller's, unless allProjects:true is passed.",
|
|
423
|
+
async execute(input) {
|
|
424
|
+
const store = new HistoryStore({ path: defaultHistoryDbPath() });
|
|
425
|
+
try {
|
|
426
|
+
const center = store.getEvent(input.eventId);
|
|
427
|
+
if (!center)
|
|
428
|
+
return { events: [] };
|
|
429
|
+
if (!input.allProjects) {
|
|
430
|
+
const session = store.getSession(center.sessionId);
|
|
431
|
+
const callerScope = encodeProjectScope(process.cwd());
|
|
432
|
+
if (session?.scope && session.scope !== callerScope) {
|
|
433
|
+
throw new Error(`Event ${input.eventId} belongs to a different project scope ("${session.scope}"). Pass allProjects:true to override.`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return { events: store.getEventWindow(input.eventId, input.window ?? 3) };
|
|
437
|
+
} finally {
|
|
438
|
+
store.close();
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
// ../../packages/history/dist/parsers/codex.js
|
|
444
|
+
function defaultCodexRoot() {
|
|
445
|
+
return join2(homedir2(), ".codex", "sessions");
|
|
446
|
+
}
|
|
447
|
+
var CodexHistoryParser = class {
|
|
448
|
+
provider = "codex";
|
|
449
|
+
root;
|
|
450
|
+
constructor(options = {}) {
|
|
451
|
+
this.root = options.root ?? defaultCodexRoot();
|
|
452
|
+
}
|
|
453
|
+
async discover() {
|
|
454
|
+
const id = `${this.provider}:${this.root}`;
|
|
455
|
+
const base = {
|
|
456
|
+
id,
|
|
457
|
+
provider: this.provider,
|
|
458
|
+
path: this.root,
|
|
459
|
+
nativeImport: true
|
|
460
|
+
};
|
|
461
|
+
try {
|
|
462
|
+
const info = await stat2(this.root);
|
|
463
|
+
if (!info.isDirectory()) {
|
|
464
|
+
return [{ ...base, importable: false, reason: "path is not a directory" }];
|
|
465
|
+
}
|
|
466
|
+
} catch {
|
|
467
|
+
return [{ ...base, importable: false, reason: "history directory not found" }];
|
|
468
|
+
}
|
|
469
|
+
const files = await glob2("*.json", { cwd: this.root });
|
|
470
|
+
if (files.length === 0) {
|
|
471
|
+
return [{ ...base, importable: false, reason: "no session state files found" }];
|
|
472
|
+
}
|
|
473
|
+
return [{ ...base, importable: true }];
|
|
474
|
+
}
|
|
475
|
+
async import(source) {
|
|
476
|
+
const files = await glob2("*.json", { cwd: source.path, absolute: true });
|
|
477
|
+
const results = [];
|
|
478
|
+
for (const file of files) {
|
|
479
|
+
let parsed;
|
|
480
|
+
try {
|
|
481
|
+
parsed = JSON.parse(await readFile2(file, "utf-8"));
|
|
482
|
+
} catch {
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
const messages = Array.isArray(parsed.messages) ? parsed.messages : [];
|
|
486
|
+
const events = [];
|
|
487
|
+
const citations = [];
|
|
488
|
+
let ordinal = 0;
|
|
489
|
+
for (const m of messages) {
|
|
490
|
+
if (!m || typeof m.content !== "string" || !m.content)
|
|
491
|
+
continue;
|
|
492
|
+
const timestamp = typeof m.timestamp === "string" ? Date.parse(m.timestamp) : Number.NaN;
|
|
493
|
+
events.push({
|
|
494
|
+
role: m.role ?? "user",
|
|
495
|
+
content: m.content,
|
|
496
|
+
toolName: m.toolName,
|
|
497
|
+
ordinal: ordinal++,
|
|
498
|
+
timestamp: Number.isNaN(timestamp) ? Date.now() : timestamp
|
|
499
|
+
});
|
|
500
|
+
citations.push({ sourcePath: file });
|
|
501
|
+
}
|
|
502
|
+
if (events.length === 0)
|
|
503
|
+
continue;
|
|
504
|
+
const sourceSessionId = parsed.id ?? file.replace(/\.json$/, "").split(/[/\\]/).pop() ?? file;
|
|
505
|
+
const scope = parsed.cwd ? encodeProjectScope(parsed.cwd) : void 0;
|
|
506
|
+
results.push({
|
|
507
|
+
session: {
|
|
508
|
+
id: `${source.provider}:${scope ?? ""}:${sourceSessionId}`,
|
|
509
|
+
provider: source.provider,
|
|
510
|
+
sourceSessionId,
|
|
511
|
+
importedAt: Date.now(),
|
|
512
|
+
scope
|
|
513
|
+
},
|
|
514
|
+
events,
|
|
515
|
+
citations
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
return results;
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
// ../../packages/history/dist/registry.js
|
|
523
|
+
var HistoryParserRegistry = class {
|
|
524
|
+
parsers = /* @__PURE__ */ new Map();
|
|
525
|
+
register(parser) {
|
|
526
|
+
this.parsers.set(parser.provider, parser);
|
|
527
|
+
}
|
|
528
|
+
get(provider) {
|
|
529
|
+
return this.parsers.get(provider);
|
|
530
|
+
}
|
|
531
|
+
list() {
|
|
532
|
+
return [...this.parsers.values()];
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
export {
|
|
536
|
+
ClaudeCodeHistoryParser,
|
|
537
|
+
CodexHistoryParser,
|
|
538
|
+
HistoryParserRegistry,
|
|
539
|
+
HistoryStore,
|
|
540
|
+
defaultClaudeCodeRoot,
|
|
541
|
+
defaultCodexRoot,
|
|
542
|
+
encodeProjectScope,
|
|
543
|
+
searchHistorySkill,
|
|
544
|
+
showEventSkill
|
|
545
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ import '@omni-ai/skills/backend';
|
|
|
10
10
|
import '@omni-ai/skills/frontend';
|
|
11
11
|
import '@omni-ai/skills/qa';
|
|
12
12
|
import '@omni-ai/memory';
|
|
13
|
+
import '@omni-ai/history';
|
|
13
14
|
import '@omni-ai/mcp';
|
|
14
15
|
import '@omni-ai/provider-anthropic';
|
|
15
16
|
import '@omni-ai/provider-openai';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ftarganski/omni-ai",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Provider-agnostic AI agents and skills framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -52,6 +52,10 @@
|
|
|
52
52
|
"import": "./dist/memory.js",
|
|
53
53
|
"types": "./dist/memory.d.ts"
|
|
54
54
|
},
|
|
55
|
+
"./history": {
|
|
56
|
+
"import": "./dist/history.js",
|
|
57
|
+
"types": "./dist/history.d.ts"
|
|
58
|
+
},
|
|
55
59
|
"./mcp": {
|
|
56
60
|
"import": "./dist/mcp.js",
|
|
57
61
|
"types": "./dist/mcp.d.ts"
|
|
@@ -89,14 +93,15 @@
|
|
|
89
93
|
"glob": "^11.0.0",
|
|
90
94
|
"yaml": "^2.7.0",
|
|
91
95
|
"zod": "^3.24.0",
|
|
92
|
-
"@omni-ai/core": "0.1.0",
|
|
93
|
-
"@omni-ai/memory": "0.1.0",
|
|
94
|
-
"@omni-ai/mcp": "0.1.0",
|
|
95
96
|
"@omni-ai/cli": "0.1.0",
|
|
97
|
+
"@omni-ai/history": "0.1.0",
|
|
96
98
|
"@omni-ai/provider-anthropic": "0.1.0",
|
|
97
99
|
"@omni-ai/provider-google": "0.1.0",
|
|
100
|
+
"@omni-ai/core": "0.1.0",
|
|
101
|
+
"@omni-ai/mcp": "0.1.0",
|
|
102
|
+
"@omni-ai/skills": "0.1.0",
|
|
98
103
|
"@omni-ai/provider-openai": "0.1.0",
|
|
99
|
-
"@omni-ai/
|
|
104
|
+
"@omni-ai/memory": "0.1.0"
|
|
100
105
|
},
|
|
101
106
|
"peerDependencies": {
|
|
102
107
|
"@anthropic-ai/sdk": "^0.39.0",
|