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 +8 -0
- package/index.mjs +35 -2
- package/lib/chat.mjs +12 -2
- package/lib/conversation.mjs +60 -1
- package/lib/conversations-ui.mjs +572 -0
- package/package.json +3 -2
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)
|
package/lib/conversation.mjs
CHANGED
|
@@ -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, '&')
|
|
27
|
+
.replace(/</g, '<')
|
|
28
|
+
.replace(/>/g, '>')
|
|
29
|
+
.replace(/"/g, '"')
|
|
30
|
+
.replace(/'/g, ''')
|
|
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 <ID> "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.
|
|
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
|
}
|