askimo 1.2.0 → 1.3.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 CHANGED
@@ -66,6 +66,7 @@ askimo "what's happening today?" -x # Use xAI Grok
66
66
  ```bash
67
67
  askimo "tell me more" -c 1 # Continue last conversation
68
68
  askimo "go deeper" -c 2 # Continue second-to-last
69
+ askimo "explain more" --cid abc123 # Continue by conversation ID
69
70
  ```
70
71
 
71
72
  ### JSON output
@@ -95,6 +96,7 @@ askimo chat # Start new chat
95
96
  askimo chat -o # Chat with OpenAI
96
97
  askimo chat -x # Chat with xAI Grok
97
98
  askimo chat -c 1 # Continue last conversation
99
+ askimo chat --cid abc123 # Continue by conversation ID
98
100
  ```
99
101
 
100
102
  Type `exit` or `Ctrl+C` to quit.
@@ -107,6 +109,12 @@ askimo models -p # Perplexity only
107
109
  askimo models -x # xAI only
108
110
  ```
109
111
 
112
+ ### View conversation history
113
+
114
+ ```bash
115
+ askimo conversations # Opens browser with all conversations
116
+ ```
117
+
110
118
  ---
111
119
 
112
120
  ## ✨ Features
package/index.mjs CHANGED
@@ -3,7 +3,8 @@
3
3
  import { Command } from 'commander'
4
4
  import { startChat } from './lib/chat.mjs'
5
5
  import { ensureDirectories, loadConfig } from './lib/config.mjs'
6
- import { createConversation, loadConversation, saveConversation } from './lib/conversation.mjs'
6
+ import { createConversation, loadConversation, loadConversationById, saveConversation } from './lib/conversation.mjs'
7
+ import { showConversationsInBrowser } from './lib/conversations-ui.mjs'
7
8
  import { buildMessage, readFile, readStdin } from './lib/input.mjs'
8
9
  import { DEFAULT_MODELS, determineProvider, getProvider, listModels } from './lib/providers.mjs'
9
10
  import { generateResponse, outputJson, streamResponse } from './lib/stream.mjs'
@@ -23,6 +24,7 @@ program
23
24
  .option('-x, --xai', 'Use xAI Grok')
24
25
  .option('-j, --json', 'Output as JSON instead of streaming')
25
26
  .option('-c, --continue <n>', 'Continue conversation N (1=last, 2=second-to-last)', Number.parseInt)
27
+ .option('--cid <id>', 'Continue conversation by ID')
26
28
  .option('-f, --file <path>', 'Read content from file')
27
29
  .action(async (question, options) => {
28
30
  try {
@@ -51,11 +53,24 @@ program
51
53
  let conversation
52
54
  let existingPath = null
53
55
 
56
+ if (options.continue && options.cid) {
57
+ console.error('Error: Cannot use both -c and --cid flags')
58
+ process.exit(1)
59
+ }
60
+
54
61
  if (options.continue) {
55
62
  const loaded = await loadConversation(options.continue)
56
63
  conversation = loaded.conversation
57
64
  existingPath = loaded.filePath
58
65
 
66
+ if (conversation.provider !== name) {
67
+ console.error(`Warning: Continuing ${conversation.provider} conversation with ${name}`)
68
+ }
69
+ } else if (options.cid) {
70
+ const loaded = await loadConversationById(options.cid)
71
+ conversation = loaded.conversation
72
+ existingPath = loaded.filePath
73
+
59
74
  if (conversation.provider !== name) {
60
75
  console.error(`Warning: Continuing ${conversation.provider} conversation with ${name}`)
61
76
  }
@@ -101,15 +116,21 @@ program
101
116
  .option('-a, --anthropic', 'Use Anthropic Claude')
102
117
  .option('-x, --xai', 'Use xAI Grok')
103
118
  .option('-c, --continue <n>', 'Continue conversation N (1=last, 2=second-to-last)', Number.parseInt)
119
+ .option('--cid <id>', 'Continue conversation by ID')
104
120
  .action(async (options) => {
105
121
  try {
122
+ if (options.continue && options.cid) {
123
+ console.error('Error: Cannot use both -c and --cid flags')
124
+ process.exit(1)
125
+ }
126
+
106
127
  const config = await loadConfig()
107
128
  await ensureDirectories()
108
129
 
109
130
  const providerName = determineProvider(options, config)
110
131
  const { model, name, modelName } = getProvider(providerName, config)
111
132
 
112
- await startChat(model, name, modelName, options.continue)
133
+ await startChat(model, name, modelName, options.continue, options.cid)
113
134
  } catch (err) {
114
135
  console.error('Error:', err.message)
115
136
  process.exit(1)
@@ -163,4 +184,16 @@ program
163
184
  }
164
185
  })
165
186
 
187
+ program
188
+ .command('conversations')
189
+ .description('View all conversations in browser')
190
+ .action(async () => {
191
+ try {
192
+ await showConversationsInBrowser()
193
+ } catch (err) {
194
+ console.error('Error:', err.message)
195
+ process.exit(1)
196
+ }
197
+ })
198
+
166
199
  program.parse()
package/lib/chat.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  import input from '@inquirer/input'
2
- import { createConversation, loadConversation, saveConversation } from './conversation.mjs'
2
+ import { createConversation, loadConversation, loadConversationById, saveConversation } from './conversation.mjs'
3
3
  import { streamResponse } from './stream.mjs'
4
4
 
5
- async function startChat(model, providerName, modelName, continueN = null) {
5
+ async function startChat(model, providerName, modelName, continueN = null, conversationId = null) {
6
6
  let conversation
7
7
  let existingPath = null
8
8
 
@@ -15,6 +15,16 @@ async function startChat(model, providerName, modelName, continueN = null) {
15
15
  console.error(`Warning: Continuing ${conversation.provider} conversation with ${providerName}`)
16
16
  }
17
17
 
18
+ console.log(`Continuing conversation ${conversation.id} (${conversation.messages.length} messages)`)
19
+ } else if (conversationId) {
20
+ const loaded = await loadConversationById(conversationId)
21
+ conversation = loaded.conversation
22
+ existingPath = loaded.filePath
23
+
24
+ if (conversation.provider !== providerName) {
25
+ console.error(`Warning: Continuing ${conversation.provider} conversation with ${providerName}`)
26
+ }
27
+
18
28
  console.log(`Continuing conversation ${conversation.id} (${conversation.messages.length} messages)`)
19
29
  } else {
20
30
  conversation = createConversation(providerName, modelName)
@@ -52,6 +52,65 @@ async function loadConversation(n) {
52
52
  }
53
53
  }
54
54
 
55
+ async function loadConversationById(conversationId) {
56
+ await ensureDirectories()
57
+
58
+ try {
59
+ const files = await readdir(CONVERSATIONS_DIR)
60
+ const jsonFiles = files.filter((f) => f.endsWith('.json'))
61
+
62
+ const matchingFile = jsonFiles.find((f) => {
63
+ const nameWithoutExt = f.slice(0, -5)
64
+ return nameWithoutExt.endsWith(`-${conversationId}`)
65
+ })
66
+
67
+ if (!matchingFile) {
68
+ console.error(`Conversation with ID "${conversationId}" not found`)
69
+ process.exit(1)
70
+ }
71
+
72
+ const filePath = join(CONVERSATIONS_DIR, matchingFile)
73
+ const content = await readFile(filePath, 'utf8')
74
+ return {
75
+ conversation: JSON.parse(content),
76
+ filePath
77
+ }
78
+ } catch (err) {
79
+ if (err.code !== 'ENOENT') {
80
+ throw err
81
+ }
82
+ console.error('No conversations directory found')
83
+ process.exit(1)
84
+ }
85
+ }
86
+
87
+ async function getAllConversations() {
88
+ await ensureDirectories()
89
+
90
+ try {
91
+ const files = await readdir(CONVERSATIONS_DIR)
92
+ const jsonFiles = files
93
+ .filter((f) => f.endsWith('.json'))
94
+ .sort()
95
+ .reverse()
96
+
97
+ const conversations = await Promise.all(
98
+ jsonFiles.map(async (filename) => {
99
+ const filePath = join(CONVERSATIONS_DIR, filename)
100
+ const content = await readFile(filePath, 'utf8')
101
+ return JSON.parse(content)
102
+ })
103
+ )
104
+
105
+ return conversations
106
+ } catch (err) {
107
+ if (err.code !== 'ENOENT') {
108
+ throw err
109
+ }
110
+ return []
111
+ }
112
+ }
113
+
55
114
  async function saveConversation(conversation, existingPath = null) {
56
115
  await ensureDirectories()
57
116
 
@@ -68,4 +127,4 @@ async function saveConversation(conversation, existingPath = null) {
68
127
  return filePath
69
128
  }
70
129
 
71
- export { createConversation, loadConversation, saveConversation }
130
+ export { createConversation, loadConversation, loadConversationById, getAllConversations, saveConversation }
@@ -0,0 +1,572 @@
1
+ import hcat from 'hcat'
2
+ import { getAllConversations } from './conversation.mjs'
3
+
4
+ function formatDate(isoString) {
5
+ const date = new Date(isoString)
6
+ return date.toLocaleDateString('en-US', {
7
+ year: 'numeric',
8
+ month: 'short',
9
+ day: 'numeric',
10
+ hour: '2-digit',
11
+ minute: '2-digit'
12
+ })
13
+ }
14
+
15
+ function getPreview(messages, maxLength = 100) {
16
+ const firstUserMessage = messages.find((m) => m.role === 'user')
17
+ if (!firstUserMessage) return 'No messages'
18
+
19
+ const content = firstUserMessage.content
20
+ if (content.length <= maxLength) return content
21
+ return content.slice(0, maxLength) + '...'
22
+ }
23
+
24
+ function escapeHtml(text) {
25
+ return text
26
+ .replace(/&/g, '&amp;')
27
+ .replace(/</g, '&lt;')
28
+ .replace(/>/g, '&gt;')
29
+ .replace(/"/g, '&quot;')
30
+ .replace(/'/g, '&#039;')
31
+ }
32
+
33
+ const ITEMS_PER_PAGE = 20
34
+
35
+ function generateHtml(conversations) {
36
+ // Prepare conversation data for client-side rendering
37
+ const conversationsJson = JSON.stringify(
38
+ conversations.map((conv) => ({
39
+ id: conv.id,
40
+ provider: conv.provider,
41
+ model: conv.model,
42
+ createdAt: conv.createdAt,
43
+ messageCount: conv.messages.length,
44
+ preview: getPreview(conv.messages),
45
+ messages: conv.messages
46
+ }))
47
+ )
48
+
49
+ return `<!DOCTYPE html>
50
+ <html lang="en">
51
+ <head>
52
+ <meta charset="UTF-8">
53
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
54
+ <title>Askimo Conversations</title>
55
+ <style>
56
+ * {
57
+ box-sizing: border-box;
58
+ margin: 0;
59
+ padding: 0;
60
+ }
61
+
62
+ body {
63
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
64
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
65
+ min-height: 100vh;
66
+ color: #e0e0e0;
67
+ padding: 2rem;
68
+ }
69
+
70
+ .container {
71
+ max-width: 1400px;
72
+ margin: 0 auto;
73
+ }
74
+
75
+ header {
76
+ text-align: center;
77
+ margin-bottom: 2rem;
78
+ }
79
+
80
+ h1 {
81
+ font-size: 2.5rem;
82
+ font-weight: 700;
83
+ color: #fff;
84
+ margin-bottom: 0.5rem;
85
+ }
86
+
87
+ .subtitle {
88
+ color: #888;
89
+ font-size: 1rem;
90
+ }
91
+
92
+ .stats {
93
+ display: flex;
94
+ justify-content: center;
95
+ gap: 2rem;
96
+ margin-bottom: 2rem;
97
+ }
98
+
99
+ .stat {
100
+ background: rgba(255, 255, 255, 0.05);
101
+ padding: 1rem 2rem;
102
+ border-radius: 8px;
103
+ text-align: center;
104
+ }
105
+
106
+ .stat-value {
107
+ font-size: 2rem;
108
+ font-weight: 700;
109
+ color: #4fc3f7;
110
+ }
111
+
112
+ .stat-label {
113
+ font-size: 0.85rem;
114
+ color: #888;
115
+ text-transform: uppercase;
116
+ letter-spacing: 0.5px;
117
+ }
118
+
119
+ table {
120
+ width: 100%;
121
+ border-collapse: collapse;
122
+ background: rgba(255, 255, 255, 0.03);
123
+ border-radius: 12px;
124
+ overflow: hidden;
125
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
126
+ }
127
+
128
+ th {
129
+ background: rgba(79, 195, 247, 0.1);
130
+ padding: 1rem;
131
+ text-align: left;
132
+ font-weight: 600;
133
+ color: #4fc3f7;
134
+ text-transform: uppercase;
135
+ font-size: 0.75rem;
136
+ letter-spacing: 1px;
137
+ }
138
+
139
+ td {
140
+ padding: 1rem;
141
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
142
+ }
143
+
144
+ .conversation-row {
145
+ cursor: pointer;
146
+ transition: background 0.2s;
147
+ }
148
+
149
+ .conversation-row:hover {
150
+ background: rgba(255, 255, 255, 0.05);
151
+ }
152
+
153
+ .conversation-row.expanded {
154
+ background: rgba(79, 195, 247, 0.1);
155
+ }
156
+
157
+ .conversation-detail {
158
+ display: none;
159
+ }
160
+
161
+ .conversation-detail.expanded {
162
+ display: table-row;
163
+ }
164
+
165
+ .conversation-detail td {
166
+ padding: 0;
167
+ background: rgba(0, 0, 0, 0.2);
168
+ }
169
+
170
+ .detail-content {
171
+ padding: 1.5rem;
172
+ max-height: 500px;
173
+ overflow-y: auto;
174
+ }
175
+
176
+ .message {
177
+ margin-bottom: 1rem;
178
+ padding: 1rem;
179
+ border-radius: 8px;
180
+ }
181
+
182
+ .message:last-child {
183
+ margin-bottom: 0;
184
+ }
185
+
186
+ .message.user {
187
+ background: rgba(79, 195, 247, 0.1);
188
+ border-left: 3px solid #4fc3f7;
189
+ }
190
+
191
+ .message.assistant {
192
+ background: rgba(129, 199, 132, 0.1);
193
+ border-left: 3px solid #81c784;
194
+ }
195
+
196
+ .message-role {
197
+ font-size: 0.75rem;
198
+ font-weight: 600;
199
+ text-transform: uppercase;
200
+ letter-spacing: 0.5px;
201
+ margin-bottom: 0.5rem;
202
+ color: #888;
203
+ }
204
+
205
+ .message.user .message-role {
206
+ color: #4fc3f7;
207
+ }
208
+
209
+ .message.assistant .message-role {
210
+ color: #81c784;
211
+ }
212
+
213
+ .message-content {
214
+ white-space: pre-wrap;
215
+ word-break: break-word;
216
+ line-height: 1.6;
217
+ }
218
+
219
+ .id {
220
+ font-family: 'SF Mono', Monaco, 'Courier New', monospace;
221
+ font-size: 0.85rem;
222
+ color: #81c784;
223
+ }
224
+
225
+ .id-text {
226
+ margin-right: 0.5rem;
227
+ }
228
+
229
+ .copy-btn {
230
+ background: none;
231
+ border: none;
232
+ color: #666;
233
+ cursor: pointer;
234
+ padding: 0.25rem;
235
+ border-radius: 4px;
236
+ transition: all 0.2s;
237
+ vertical-align: middle;
238
+ }
239
+
240
+ .copy-btn:hover {
241
+ color: #4fc3f7;
242
+ background: rgba(79, 195, 247, 0.1);
243
+ }
244
+
245
+ .copy-btn.copied {
246
+ color: #81c784;
247
+ }
248
+
249
+ .provider {
250
+ text-transform: capitalize;
251
+ font-weight: 500;
252
+ }
253
+
254
+ .model {
255
+ font-family: 'SF Mono', Monaco, 'Courier New', monospace;
256
+ font-size: 0.85rem;
257
+ color: #ffb74d;
258
+ }
259
+
260
+ .date {
261
+ color: #888;
262
+ font-size: 0.9rem;
263
+ white-space: nowrap;
264
+ }
265
+
266
+ .messages {
267
+ text-align: center;
268
+ font-weight: 600;
269
+ }
270
+
271
+ .preview {
272
+ color: #aaa;
273
+ font-size: 0.9rem;
274
+ max-width: 400px;
275
+ overflow: hidden;
276
+ text-overflow: ellipsis;
277
+ white-space: nowrap;
278
+ }
279
+
280
+ .empty {
281
+ text-align: center;
282
+ padding: 4rem;
283
+ color: #666;
284
+ }
285
+
286
+ .usage {
287
+ margin-top: 2rem;
288
+ text-align: center;
289
+ color: #666;
290
+ font-size: 0.9rem;
291
+ }
292
+
293
+ code {
294
+ background: rgba(255, 255, 255, 0.1);
295
+ padding: 0.2rem 0.5rem;
296
+ border-radius: 4px;
297
+ font-family: 'SF Mono', Monaco, 'Courier New', monospace;
298
+ }
299
+
300
+ /* Pagination */
301
+ .pagination {
302
+ display: flex;
303
+ justify-content: center;
304
+ align-items: center;
305
+ gap: 0.5rem;
306
+ margin-top: 1.5rem;
307
+ padding: 1rem;
308
+ }
309
+
310
+ .page-btn {
311
+ background: rgba(255, 255, 255, 0.05);
312
+ border: 1px solid rgba(255, 255, 255, 0.1);
313
+ color: #e0e0e0;
314
+ padding: 0.5rem 0.75rem;
315
+ border-radius: 6px;
316
+ cursor: pointer;
317
+ transition: all 0.2s;
318
+ display: flex;
319
+ align-items: center;
320
+ justify-content: center;
321
+ }
322
+
323
+ .page-btn:hover:not(:disabled) {
324
+ background: rgba(79, 195, 247, 0.2);
325
+ border-color: rgba(79, 195, 247, 0.3);
326
+ color: #4fc3f7;
327
+ }
328
+
329
+ .page-btn:disabled {
330
+ opacity: 0.3;
331
+ cursor: not-allowed;
332
+ }
333
+
334
+ .page-info {
335
+ color: #888;
336
+ font-size: 0.9rem;
337
+ padding: 0 1rem;
338
+ }
339
+
340
+ /* Scrollbar styling */
341
+ .detail-content::-webkit-scrollbar {
342
+ width: 8px;
343
+ }
344
+
345
+ .detail-content::-webkit-scrollbar-track {
346
+ background: rgba(255, 255, 255, 0.05);
347
+ border-radius: 4px;
348
+ }
349
+
350
+ .detail-content::-webkit-scrollbar-thumb {
351
+ background: rgba(255, 255, 255, 0.2);
352
+ border-radius: 4px;
353
+ }
354
+
355
+ .detail-content::-webkit-scrollbar-thumb:hover {
356
+ background: rgba(255, 255, 255, 0.3);
357
+ }
358
+ </style>
359
+ </head>
360
+ <body>
361
+ <div class="container">
362
+ <header>
363
+ <h1>Askimo Conversations</h1>
364
+ <p class="subtitle">Your AI conversation history</p>
365
+ </header>
366
+
367
+ <div class="stats">
368
+ <div class="stat">
369
+ <div class="stat-value">${conversations.length}</div>
370
+ <div class="stat-label">Conversations</div>
371
+ </div>
372
+ <div class="stat">
373
+ <div class="stat-value">${conversations.reduce((sum, c) => sum + c.messages.length, 0)}</div>
374
+ <div class="stat-label">Total Messages</div>
375
+ </div>
376
+ </div>
377
+
378
+ <div id="conversations-container">
379
+ <!-- Rendered by JavaScript -->
380
+ </div>
381
+
382
+ <div class="usage">
383
+ <p>Continue a conversation: <code>askimo --cid &lt;ID&gt; "your follow-up"</code></p>
384
+ </div>
385
+ </div>
386
+
387
+ <script>
388
+ const conversations = ${conversationsJson};
389
+ const ITEMS_PER_PAGE = ${ITEMS_PER_PAGE};
390
+ let currentPage = 1;
391
+ const totalPages = Math.ceil(conversations.length / ITEMS_PER_PAGE);
392
+
393
+ function escapeHtml(text) {
394
+ const div = document.createElement('div');
395
+ div.textContent = text;
396
+ return div.innerHTML;
397
+ }
398
+
399
+ function formatDate(isoString) {
400
+ const date = new Date(isoString);
401
+ return date.toLocaleDateString('en-US', {
402
+ year: 'numeric',
403
+ month: 'short',
404
+ day: 'numeric',
405
+ hour: '2-digit',
406
+ minute: '2-digit'
407
+ });
408
+ }
409
+
410
+ function renderMessages(messages) {
411
+ return messages.map(msg => \`
412
+ <div class="message \${msg.role}">
413
+ <div class="message-role">\${msg.role === 'user' ? 'You' : 'Assistant'}</div>
414
+ <div class="message-content">\${escapeHtml(msg.content)}</div>
415
+ </div>
416
+ \`).join('');
417
+ }
418
+
419
+ function renderPage(page) {
420
+ currentPage = page;
421
+ const container = document.getElementById('conversations-container');
422
+ const start = (page - 1) * ITEMS_PER_PAGE;
423
+ const end = Math.min(start + ITEMS_PER_PAGE, conversations.length);
424
+ const pageConversations = conversations.slice(start, end);
425
+
426
+ if (conversations.length === 0) {
427
+ container.innerHTML = \`
428
+ <div class="empty">
429
+ <p>No conversations found.</p>
430
+ <p>Start a conversation with: <code>askimo "your question"</code></p>
431
+ </div>
432
+ \`;
433
+ return;
434
+ }
435
+
436
+ const rows = pageConversations.map((conv, idx) => {
437
+ const globalIndex = start + idx;
438
+ return \`
439
+ <tr class="conversation-row" data-index="\${globalIndex}">
440
+ <td class="id">
441
+ <span class="id-text">\${escapeHtml(conv.id)}</span>
442
+ <button class="copy-btn" data-id="\${escapeHtml(conv.id)}" title="Copy ID">
443
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
444
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
445
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
446
+ </svg>
447
+ </button>
448
+ </td>
449
+ <td class="provider">\${escapeHtml(conv.provider)}</td>
450
+ <td class="model">\${escapeHtml(conv.model)}</td>
451
+ <td class="date">\${formatDate(conv.createdAt)}</td>
452
+ <td class="messages">\${conv.messageCount}</td>
453
+ <td class="preview">\${escapeHtml(conv.preview)}</td>
454
+ </tr>
455
+ <tr class="conversation-detail" data-index="\${globalIndex}">
456
+ <td colspan="6">
457
+ <div class="detail-content">
458
+ \${renderMessages(conv.messages)}
459
+ </div>
460
+ </td>
461
+ </tr>
462
+ \`;
463
+ }).join('');
464
+
465
+ const pagination = totalPages > 1 ? \`
466
+ <div class="pagination">
467
+ <button class="page-btn" onclick="renderPage(1)" \${currentPage === 1 ? 'disabled' : ''}>
468
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
469
+ <polyline points="11 17 6 12 11 7"></polyline>
470
+ <polyline points="18 17 13 12 18 7"></polyline>
471
+ </svg>
472
+ </button>
473
+ <button class="page-btn" onclick="renderPage(\${currentPage - 1})" \${currentPage === 1 ? 'disabled' : ''}>
474
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
475
+ <polyline points="15 18 9 12 15 6"></polyline>
476
+ </svg>
477
+ </button>
478
+ <span class="page-info">Page \${currentPage} of \${totalPages}</span>
479
+ <button class="page-btn" onclick="renderPage(\${currentPage + 1})" \${currentPage === totalPages ? 'disabled' : ''}>
480
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
481
+ <polyline points="9 18 15 12 9 6"></polyline>
482
+ </svg>
483
+ </button>
484
+ <button class="page-btn" onclick="renderPage(\${totalPages})" \${currentPage === totalPages ? 'disabled' : ''}>
485
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
486
+ <polyline points="13 17 18 12 13 7"></polyline>
487
+ <polyline points="6 17 11 12 6 7"></polyline>
488
+ </svg>
489
+ </button>
490
+ </div>
491
+ \` : '';
492
+
493
+ container.innerHTML = \`
494
+ <table>
495
+ <thead>
496
+ <tr>
497
+ <th>ID</th>
498
+ <th>Provider</th>
499
+ <th>Model</th>
500
+ <th>Date</th>
501
+ <th>Messages</th>
502
+ <th>Preview</th>
503
+ </tr>
504
+ </thead>
505
+ <tbody>
506
+ \${rows}
507
+ </tbody>
508
+ </table>
509
+ \${pagination}
510
+ \`;
511
+
512
+ attachEventListeners();
513
+ }
514
+
515
+ function attachEventListeners() {
516
+ // Accordion behavior
517
+ document.querySelectorAll('.conversation-row').forEach(row => {
518
+ row.addEventListener('click', (e) => {
519
+ if (e.target.closest('.copy-btn')) return;
520
+
521
+ const index = row.dataset.index;
522
+ const detailRow = document.querySelector('.conversation-detail[data-index="' + index + '"]');
523
+ const isExpanded = row.classList.contains('expanded');
524
+
525
+ document.querySelectorAll('.conversation-row.expanded').forEach(r => r.classList.remove('expanded'));
526
+ document.querySelectorAll('.conversation-detail.expanded').forEach(d => d.classList.remove('expanded'));
527
+
528
+ if (!isExpanded) {
529
+ row.classList.add('expanded');
530
+ detailRow.classList.add('expanded');
531
+ }
532
+ });
533
+ });
534
+
535
+ // Copy button functionality
536
+ document.querySelectorAll('.copy-btn').forEach(btn => {
537
+ btn.addEventListener('click', async (e) => {
538
+ e.stopPropagation();
539
+ const id = btn.dataset.id;
540
+
541
+ try {
542
+ await navigator.clipboard.writeText(id);
543
+ btn.classList.add('copied');
544
+ btn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"></polyline></svg>';
545
+
546
+ setTimeout(() => {
547
+ btn.classList.remove('copied');
548
+ btn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>';
549
+ }, 2000);
550
+ } catch (err) {
551
+ console.error('Failed to copy:', err);
552
+ }
553
+ });
554
+ });
555
+ }
556
+
557
+ // Initial render
558
+ renderPage(1);
559
+ </script>
560
+ </body>
561
+ </html>`
562
+ }
563
+
564
+ async function showConversationsInBrowser() {
565
+ const conversations = await getAllConversations()
566
+ const html = generateHtml(conversations)
567
+
568
+ console.log('Opening conversations in browser...')
569
+ hcat(html, { port: 0 })
570
+ }
571
+
572
+ export { showConversationsInBrowser, generateHtml }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "askimo",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "A CLI tool for communicating with AI providers (Perplexity, OpenAI, Anthropic)",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Amit Tal",
@@ -42,6 +42,7 @@
42
42
  "@ai-sdk/xai": "^2.0.40",
43
43
  "@inquirer/input": "^5.0.2",
44
44
  "ai": "^5.0.106",
45
- "commander": "^14.0.2"
45
+ "commander": "^14.0.2",
46
+ "hcat": "^2.2.1"
46
47
  }
47
48
  }