omni-ai-cli 1.6.0 → 1.8.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/omni_ai/history.py +108 -15
- package/package.json +1 -1
package/omni_ai/history.py
CHANGED
|
@@ -135,31 +135,124 @@ def _read_codex():
|
|
|
135
135
|
'cwd': cwd, 'messages': messages}
|
|
136
136
|
|
|
137
137
|
|
|
138
|
+
# ─── Cursor ──────────────────────────────────────────────────────
|
|
139
|
+
# Cursor keeps chats in a SQLite "state" DB: composerData:<id> holds a conversation's metadata and the
|
|
140
|
+
# ordered bubble headers; bubbleId:<composerId>:<bubbleId> holds each message (type 1=user, 2=assistant).
|
|
141
|
+
# Listing reads only composerData (fast); the bubbles are fetched only when one conversation is opened.
|
|
142
|
+
|
|
143
|
+
def _cursor_db():
|
|
144
|
+
env = os.environ.get('CURSOR_STATE_DB')
|
|
145
|
+
if env:
|
|
146
|
+
return Path(env).expanduser()
|
|
147
|
+
for candidate in ('~/Library/Application Support/Cursor/User/globalStorage/state.vscdb',
|
|
148
|
+
'~/.config/Cursor/User/globalStorage/state.vscdb',
|
|
149
|
+
'~/AppData/Roaming/Cursor/User/globalStorage/state.vscdb'):
|
|
150
|
+
path = Path(candidate).expanduser()
|
|
151
|
+
if path.is_file():
|
|
152
|
+
return path
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _cursor_connect():
|
|
157
|
+
import sqlite3
|
|
158
|
+
db = _cursor_db()
|
|
159
|
+
if not db or not db.is_file():
|
|
160
|
+
return None
|
|
161
|
+
try:
|
|
162
|
+
return sqlite3.connect(f'file:{db}?mode=ro', uri=True)
|
|
163
|
+
except Exception:
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _cursor_sessions():
|
|
168
|
+
con = _cursor_connect()
|
|
169
|
+
if not con:
|
|
170
|
+
return
|
|
171
|
+
try:
|
|
172
|
+
for (value,) in con.execute("SELECT value FROM cursorDiskKV WHERE key LIKE 'composerData:%'"):
|
|
173
|
+
try:
|
|
174
|
+
d = json.loads(value)
|
|
175
|
+
except ValueError:
|
|
176
|
+
continue
|
|
177
|
+
heads = d.get('fullConversationHeadersOnly') or []
|
|
178
|
+
name = d.get('name') or (d.get('text') or '').strip()
|
|
179
|
+
if not name or not heads:
|
|
180
|
+
continue
|
|
181
|
+
yield {'id': 'cursor:' + d.get('composerId', ''), 'source': 'cursor',
|
|
182
|
+
'title': _first_line(name), 'updated': (d.get('lastUpdatedAt') or d.get('createdAt') or 0) / 1000,
|
|
183
|
+
'cwd': '', 'count': len(heads)}
|
|
184
|
+
finally:
|
|
185
|
+
con.close()
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _cursor_load(conv_id):
|
|
189
|
+
cid = conv_id.split(':', 1)[1]
|
|
190
|
+
con = _cursor_connect()
|
|
191
|
+
if not con:
|
|
192
|
+
return None
|
|
193
|
+
try:
|
|
194
|
+
row = con.execute("SELECT value FROM cursorDiskKV WHERE key=?", (f'composerData:{cid}',)).fetchone()
|
|
195
|
+
if not row:
|
|
196
|
+
return None
|
|
197
|
+
d = json.loads(row[0])
|
|
198
|
+
messages = []
|
|
199
|
+
for head in d.get('fullConversationHeadersOnly') or []:
|
|
200
|
+
bid = head.get('bubbleId')
|
|
201
|
+
br = con.execute("SELECT value FROM cursorDiskKV WHERE key=?", (f'bubbleId:{cid}:{bid}',)).fetchone()
|
|
202
|
+
if not br:
|
|
203
|
+
continue
|
|
204
|
+
try:
|
|
205
|
+
b = json.loads(br[0])
|
|
206
|
+
except ValueError:
|
|
207
|
+
continue
|
|
208
|
+
text = b.get('text') or ''
|
|
209
|
+
if text.strip():
|
|
210
|
+
messages.append({'role': 'assistant' if b.get('type') == 2 else 'user', 'content': text})
|
|
211
|
+
if not messages:
|
|
212
|
+
return None
|
|
213
|
+
return {'id': conv_id, 'source': 'cursor', 'title': _first_line(d.get('name') or messages[0]['content']),
|
|
214
|
+
'updated': (d.get('lastUpdatedAt') or d.get('createdAt') or 0) / 1000, 'cwd': '', 'messages': messages}
|
|
215
|
+
finally:
|
|
216
|
+
con.close()
|
|
217
|
+
|
|
218
|
+
|
|
138
219
|
READERS = {'omni': _read_omni, 'claude': _read_claude, 'codex': _read_codex}
|
|
220
|
+
# Sources with a fast metadata list separate from the heavy per-conversation load.
|
|
221
|
+
LAZY = {'cursor': (_cursor_sessions, _cursor_load)}
|
|
139
222
|
|
|
140
223
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
224
|
+
ALL_SOURCES = list(READERS) + list(LAZY)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def sessions(sources=None):
|
|
228
|
+
"""Metadata only (no messages), newest first — for a fast list across all sources."""
|
|
229
|
+
picked = sources or ALL_SOURCES
|
|
144
230
|
items = []
|
|
145
231
|
for name in picked:
|
|
146
|
-
reader = READERS.get(name)
|
|
147
|
-
if not reader:
|
|
148
|
-
continue
|
|
149
232
|
try:
|
|
150
|
-
|
|
151
|
-
|
|
233
|
+
if name in READERS:
|
|
234
|
+
items += [{k: v for k, v in c.items() if k != 'messages'} | {'count': len(c['messages'])}
|
|
235
|
+
for c in READERS[name]()]
|
|
236
|
+
elif name in LAZY:
|
|
237
|
+
items += list(LAZY[name][0]())
|
|
238
|
+
except Exception: # a broken tool store must not sink the rest
|
|
152
239
|
continue
|
|
153
240
|
items.sort(key=lambda c: c.get('updated') or 0, reverse=True)
|
|
154
241
|
return items
|
|
155
242
|
|
|
156
243
|
|
|
157
|
-
def sessions(sources=None):
|
|
158
|
-
"""Metadata only (no messages), newest first — for a fast list."""
|
|
159
|
-
return [{k: v for k, v in c.items() if k != 'messages'} | {'count': len(c['messages'])}
|
|
160
|
-
for c in conversations(sources)]
|
|
161
|
-
|
|
162
|
-
|
|
163
244
|
def conversation(conv_id):
|
|
164
245
|
"""One conversation in full, by its id, or None."""
|
|
165
|
-
|
|
246
|
+
source = conv_id.split(':', 1)[0]
|
|
247
|
+
if source in LAZY:
|
|
248
|
+
try:
|
|
249
|
+
return LAZY[source][1](conv_id)
|
|
250
|
+
except Exception:
|
|
251
|
+
return None
|
|
252
|
+
reader = READERS.get(source)
|
|
253
|
+
if not reader:
|
|
254
|
+
return None
|
|
255
|
+
try:
|
|
256
|
+
return next((c for c in reader() if c['id'] == conv_id), None)
|
|
257
|
+
except Exception:
|
|
258
|
+
return None
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omni-ai-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "One chat for Claude Code, Codex, Antigravity and OpenRouter, with automatic routing, account fallback, API keys, permission modes and MCP/skill sync.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"ai": "bin/ai.js",
|