askimo 1.1.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 +27 -13
- package/index.mjs +43 -6
- package/lib/chat.mjs +13 -3
- package/lib/conversation.mjs +60 -1
- package/lib/conversations-ui.mjs +572 -0
- package/lib/providers.mjs +35 -2
- package/lib/stream.mjs +21 -6
- package/package.json +16 -2
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
A CLI tool for communicating with AI providers.
|
|
9
9
|
|
|
10
|
-
**Supported providers:** Perplexity · OpenAI · Anthropic
|
|
10
|
+
**Supported providers:** Perplexity · OpenAI · Anthropic · xAI (Grok)
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
@@ -26,12 +26,14 @@ Create a config file at `~/.askimo/config`:
|
|
|
26
26
|
PERPLEXITY_API_KEY=your-perplexity-key
|
|
27
27
|
OPENAI_API_KEY=your-openai-key
|
|
28
28
|
ANTHROPIC_API_KEY=your-anthropic-key
|
|
29
|
+
XAI_API_KEY=your-xai-key
|
|
29
30
|
|
|
30
31
|
# Optional settings
|
|
31
32
|
DEFAULT_PROVIDER=perplexity
|
|
32
33
|
PERPLEXITY_MODEL=sonar
|
|
33
34
|
OPENAI_MODEL=gpt-4o
|
|
34
35
|
ANTHROPIC_MODEL=claude-sonnet-4-20250514
|
|
36
|
+
XAI_MODEL=grok-4
|
|
35
37
|
```
|
|
36
38
|
|
|
37
39
|
---
|
|
@@ -46,15 +48,17 @@ askimo "What is the capital of France?"
|
|
|
46
48
|
|
|
47
49
|
### Choose a provider
|
|
48
50
|
|
|
49
|
-
| Flag | Provider
|
|
50
|
-
|
|
51
|
+
| Flag | Provider |
|
|
52
|
+
|------|----------------------|
|
|
51
53
|
| `-p` | Perplexity (default) |
|
|
52
|
-
| `-o` | OpenAI
|
|
53
|
-
| `-a` | Anthropic
|
|
54
|
+
| `-o` | OpenAI |
|
|
55
|
+
| `-a` | Anthropic |
|
|
56
|
+
| `-x` | xAI (Grok) |
|
|
54
57
|
|
|
55
58
|
```bash
|
|
56
59
|
askimo "explain quantum computing" -o # Use OpenAI
|
|
57
60
|
askimo "write a haiku" -a # Use Anthropic
|
|
61
|
+
askimo "what's happening today?" -x # Use xAI Grok
|
|
58
62
|
```
|
|
59
63
|
|
|
60
64
|
### Continue a conversation
|
|
@@ -62,6 +66,7 @@ askimo "write a haiku" -a # Use Anthropic
|
|
|
62
66
|
```bash
|
|
63
67
|
askimo "tell me more" -c 1 # Continue last conversation
|
|
64
68
|
askimo "go deeper" -c 2 # Continue second-to-last
|
|
69
|
+
askimo "explain more" --cid abc123 # Continue by conversation ID
|
|
65
70
|
```
|
|
66
71
|
|
|
67
72
|
### JSON output
|
|
@@ -89,7 +94,9 @@ askimo -f error.log "find the bug"
|
|
|
89
94
|
```bash
|
|
90
95
|
askimo chat # Start new chat
|
|
91
96
|
askimo chat -o # Chat with OpenAI
|
|
97
|
+
askimo chat -x # Chat with xAI Grok
|
|
92
98
|
askimo chat -c 1 # Continue last conversation
|
|
99
|
+
askimo chat --cid abc123 # Continue by conversation ID
|
|
93
100
|
```
|
|
94
101
|
|
|
95
102
|
Type `exit` or `Ctrl+C` to quit.
|
|
@@ -99,20 +106,27 @@ Type `exit` or `Ctrl+C` to quit.
|
|
|
99
106
|
```bash
|
|
100
107
|
askimo models # All providers
|
|
101
108
|
askimo models -p # Perplexity only
|
|
109
|
+
askimo models -x # xAI only
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### View conversation history
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
askimo conversations # Opens browser with all conversations
|
|
102
116
|
```
|
|
103
117
|
|
|
104
118
|
---
|
|
105
119
|
|
|
106
120
|
## ✨ Features
|
|
107
121
|
|
|
108
|
-
| Feature
|
|
109
|
-
|
|
110
|
-
| Streaming
|
|
111
|
-
| Piping
|
|
112
|
-
| File input
|
|
113
|
-
| Citations
|
|
114
|
-
| History
|
|
115
|
-
| Multi-provider | Switch between AI providers easily
|
|
122
|
+
| Feature | Description |
|
|
123
|
+
|----------------|---------------------------------------------------|
|
|
124
|
+
| Streaming | Real-time response output |
|
|
125
|
+
| Piping | Pipe content via stdin |
|
|
126
|
+
| File input | Read content from files with `-f` |
|
|
127
|
+
| Citations | Source links with Perplexity |
|
|
128
|
+
| History | Conversations saved to `~/.askimo/conversations/` |
|
|
129
|
+
| Multi-provider | Switch between AI providers easily |
|
|
116
130
|
|
|
117
131
|
---
|
|
118
132
|
|
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'
|
|
@@ -20,8 +21,10 @@ program
|
|
|
20
21
|
.option('-p, --perplexity', 'Use Perplexity AI (default)')
|
|
21
22
|
.option('-o, --openai', 'Use OpenAI')
|
|
22
23
|
.option('-a, --anthropic', 'Use Anthropic Claude')
|
|
24
|
+
.option('-x, --xai', 'Use xAI Grok')
|
|
23
25
|
.option('-j, --json', 'Output as JSON instead of streaming')
|
|
24
26
|
.option('-c, --continue <n>', 'Continue conversation N (1=last, 2=second-to-last)', Number.parseInt)
|
|
27
|
+
.option('--cid <id>', 'Continue conversation by ID')
|
|
25
28
|
.option('-f, --file <path>', 'Read content from file')
|
|
26
29
|
.action(async (question, options) => {
|
|
27
30
|
try {
|
|
@@ -50,11 +53,24 @@ program
|
|
|
50
53
|
let conversation
|
|
51
54
|
let existingPath = null
|
|
52
55
|
|
|
56
|
+
if (options.continue && options.cid) {
|
|
57
|
+
console.error('Error: Cannot use both -c and --cid flags')
|
|
58
|
+
process.exit(1)
|
|
59
|
+
}
|
|
60
|
+
|
|
53
61
|
if (options.continue) {
|
|
54
62
|
const loaded = await loadConversation(options.continue)
|
|
55
63
|
conversation = loaded.conversation
|
|
56
64
|
existingPath = loaded.filePath
|
|
57
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
|
+
|
|
58
74
|
if (conversation.provider !== name) {
|
|
59
75
|
console.error(`Warning: Continuing ${conversation.provider} conversation with ${name}`)
|
|
60
76
|
}
|
|
@@ -70,16 +86,16 @@ program
|
|
|
70
86
|
let responseText
|
|
71
87
|
|
|
72
88
|
if (options.json) {
|
|
73
|
-
const { text, sources } = await generateResponse(model, conversation.messages)
|
|
89
|
+
const { text, sources, duration } = await generateResponse(model, conversation.messages)
|
|
74
90
|
responseText = text
|
|
75
91
|
conversation.messages.push({
|
|
76
92
|
role: 'assistant',
|
|
77
93
|
content: responseText
|
|
78
94
|
})
|
|
79
95
|
await saveConversation(conversation, existingPath)
|
|
80
|
-
outputJson(conversation, responseText, sources)
|
|
96
|
+
outputJson(conversation, responseText, sources, duration)
|
|
81
97
|
} else {
|
|
82
|
-
responseText = await streamResponse(model, conversation.messages)
|
|
98
|
+
responseText = await streamResponse(model, conversation.messages, modelName)
|
|
83
99
|
conversation.messages.push({
|
|
84
100
|
role: 'assistant',
|
|
85
101
|
content: responseText
|
|
@@ -98,16 +114,23 @@ program
|
|
|
98
114
|
.option('-p, --perplexity', 'Use Perplexity AI (default)')
|
|
99
115
|
.option('-o, --openai', 'Use OpenAI')
|
|
100
116
|
.option('-a, --anthropic', 'Use Anthropic Claude')
|
|
117
|
+
.option('-x, --xai', 'Use xAI Grok')
|
|
101
118
|
.option('-c, --continue <n>', 'Continue conversation N (1=last, 2=second-to-last)', Number.parseInt)
|
|
119
|
+
.option('--cid <id>', 'Continue conversation by ID')
|
|
102
120
|
.action(async (options) => {
|
|
103
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
|
+
|
|
104
127
|
const config = await loadConfig()
|
|
105
128
|
await ensureDirectories()
|
|
106
129
|
|
|
107
130
|
const providerName = determineProvider(options, config)
|
|
108
131
|
const { model, name, modelName } = getProvider(providerName, config)
|
|
109
132
|
|
|
110
|
-
await startChat(model, name, modelName, options.continue)
|
|
133
|
+
await startChat(model, name, modelName, options.continue, options.cid)
|
|
111
134
|
} catch (err) {
|
|
112
135
|
console.error('Error:', err.message)
|
|
113
136
|
process.exit(1)
|
|
@@ -120,6 +143,7 @@ program
|
|
|
120
143
|
.option('-p, --perplexity', 'Show only Perplexity models')
|
|
121
144
|
.option('-o, --openai', 'Show only OpenAI models')
|
|
122
145
|
.option('-a, --anthropic', 'Show only Anthropic models')
|
|
146
|
+
.option('-x, --xai', 'Show only xAI models')
|
|
123
147
|
.action(async (options) => {
|
|
124
148
|
try {
|
|
125
149
|
const config = await loadConfig()
|
|
@@ -128,8 +152,9 @@ program
|
|
|
128
152
|
if (options.perplexity) providers.push('perplexity')
|
|
129
153
|
if (options.openai) providers.push('openai')
|
|
130
154
|
if (options.anthropic) providers.push('anthropic')
|
|
155
|
+
if (options.xai) providers.push('xai')
|
|
131
156
|
|
|
132
|
-
const toShow = providers.length === 0 ? ['perplexity', 'openai', 'anthropic'] : providers
|
|
157
|
+
const toShow = providers.length === 0 ? ['perplexity', 'openai', 'anthropic', 'xai'] : providers
|
|
133
158
|
|
|
134
159
|
const results = await Promise.all(
|
|
135
160
|
toShow.map(async (provider) => ({
|
|
@@ -159,4 +184,16 @@ program
|
|
|
159
184
|
}
|
|
160
185
|
})
|
|
161
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
|
+
|
|
162
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)
|
|
@@ -42,7 +52,7 @@ async function startChat(model, providerName, modelName, continueN = null) {
|
|
|
42
52
|
})
|
|
43
53
|
|
|
44
54
|
console.log('')
|
|
45
|
-
const responseText = await streamResponse(model, conversation.messages)
|
|
55
|
+
const responseText = await streamResponse(model, conversation.messages, modelName)
|
|
46
56
|
|
|
47
57
|
conversation.messages.push({
|
|
48
58
|
role: 'assistant',
|
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/lib/providers.mjs
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { createAnthropic } from '@ai-sdk/anthropic'
|
|
2
2
|
import { createOpenAI } from '@ai-sdk/openai'
|
|
3
3
|
import { createPerplexity } from '@ai-sdk/perplexity'
|
|
4
|
+
import { createXai } from '@ai-sdk/xai'
|
|
4
5
|
|
|
5
6
|
const DEFAULT_MODELS = {
|
|
6
7
|
perplexity: 'sonar',
|
|
7
8
|
openai: 'gpt-4o',
|
|
8
|
-
anthropic: 'claude-sonnet-4-20250514'
|
|
9
|
+
anthropic: 'claude-sonnet-4-20250514',
|
|
10
|
+
xai: 'grok-4'
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
// Perplexity doesn't have a models list API, so we hardcode these
|
|
@@ -17,6 +19,20 @@ const PERPLEXITY_MODELS = [
|
|
|
17
19
|
{ id: 'sonar-deep-research', description: 'Deep research sessions' }
|
|
18
20
|
]
|
|
19
21
|
|
|
22
|
+
// xAI doesn't have a public models list API, so we hardcode these
|
|
23
|
+
const XAI_MODELS = [
|
|
24
|
+
{ id: 'grok-4-1-fast-reasoning', description: 'Grok 4.1 fast with reasoning' },
|
|
25
|
+
{ id: 'grok-4-1-fast-non-reasoning', description: 'Grok 4.1 fast without reasoning' },
|
|
26
|
+
{ id: 'grok-code-fast-1', description: 'Grok optimized for code' },
|
|
27
|
+
{ id: 'grok-4-fast-reasoning', description: 'Grok 4 fast with reasoning' },
|
|
28
|
+
{ id: 'grok-4-fast-non-reasoning', description: 'Grok 4 fast without reasoning' },
|
|
29
|
+
{ id: 'grok-4-0709', description: 'Grok 4 flagship model' },
|
|
30
|
+
{ id: 'grok-3-mini', description: 'Lightweight Grok 3 model' },
|
|
31
|
+
{ id: 'grok-3', description: 'Grok 3 base model' },
|
|
32
|
+
{ id: 'grok-2-vision-1212', description: 'Grok 2 with vision capabilities' },
|
|
33
|
+
{ id: 'grok-2-image-1212', description: 'Image generation model' }
|
|
34
|
+
]
|
|
35
|
+
|
|
20
36
|
async function fetchOpenAiModels(apiKey) {
|
|
21
37
|
const response = await fetch('https://api.openai.com/v1/models', {
|
|
22
38
|
// biome-ignore lint/style/useNamingConvention: headers use standard capitalization
|
|
@@ -71,6 +87,9 @@ async function listModels(provider, config) {
|
|
|
71
87
|
return fetchAnthropicModels(apiKey)
|
|
72
88
|
}
|
|
73
89
|
|
|
90
|
+
case 'xai':
|
|
91
|
+
return XAI_MODELS
|
|
92
|
+
|
|
74
93
|
default:
|
|
75
94
|
throw new Error(`Unknown provider: ${provider}`)
|
|
76
95
|
}
|
|
@@ -117,6 +136,19 @@ function getProvider(providerName, config) {
|
|
|
117
136
|
modelName
|
|
118
137
|
}
|
|
119
138
|
}
|
|
139
|
+
case 'xai': {
|
|
140
|
+
const apiKey = config.XAI_API_KEY
|
|
141
|
+
if (!apiKey) {
|
|
142
|
+
throw new Error('XAI_API_KEY not found in config')
|
|
143
|
+
}
|
|
144
|
+
const modelName = config.XAI_MODEL || DEFAULT_MODELS.xai
|
|
145
|
+
const xai = createXai({ apiKey })
|
|
146
|
+
return {
|
|
147
|
+
model: xai(modelName),
|
|
148
|
+
name: 'xai',
|
|
149
|
+
modelName
|
|
150
|
+
}
|
|
151
|
+
}
|
|
120
152
|
default:
|
|
121
153
|
throw new Error(`Unknown provider: ${providerName}`)
|
|
122
154
|
}
|
|
@@ -126,9 +158,10 @@ function determineProvider(options, config = {}) {
|
|
|
126
158
|
if (options.openai) return 'openai'
|
|
127
159
|
if (options.anthropic) return 'anthropic'
|
|
128
160
|
if (options.perplexity) return 'perplexity'
|
|
161
|
+
if (options.xai) return 'xai'
|
|
129
162
|
|
|
130
163
|
const defaultProvider = config.DEFAULT_PROVIDER?.toLowerCase()
|
|
131
|
-
if (defaultProvider && ['perplexity', 'openai', 'anthropic'].includes(defaultProvider)) {
|
|
164
|
+
if (defaultProvider && ['perplexity', 'openai', 'anthropic', 'xai'].includes(defaultProvider)) {
|
|
132
165
|
return defaultProvider
|
|
133
166
|
}
|
|
134
167
|
|
package/lib/stream.mjs
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { generateText, streamText } from 'ai'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
function formatDuration(ms) {
|
|
4
|
+
if (ms < 1000) return `${ms}ms`
|
|
5
|
+
return `${(ms / 1000).toFixed(1)}s`
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async function streamResponse(model, messages, modelName) {
|
|
9
|
+
const startTime = Date.now()
|
|
10
|
+
|
|
4
11
|
const result = streamText({
|
|
5
12
|
model,
|
|
6
13
|
messages
|
|
@@ -29,19 +36,26 @@ async function streamResponse(model, messages) {
|
|
|
29
36
|
})
|
|
30
37
|
}
|
|
31
38
|
|
|
39
|
+
// Display status line
|
|
40
|
+
const duration = Date.now() - startTime
|
|
41
|
+
process.stdout.write(`\n\x1b[2m${modelName} · ${formatDuration(duration)}\x1b[0m\n`)
|
|
42
|
+
|
|
32
43
|
return fullText
|
|
33
44
|
}
|
|
34
45
|
|
|
35
46
|
async function generateResponse(model, messages) {
|
|
47
|
+
const startTime = Date.now()
|
|
48
|
+
|
|
36
49
|
const { text, sources } = await generateText({
|
|
37
50
|
model,
|
|
38
51
|
messages
|
|
39
52
|
})
|
|
40
53
|
|
|
41
|
-
|
|
54
|
+
const duration = Date.now() - startTime
|
|
55
|
+
return { text, sources, duration }
|
|
42
56
|
}
|
|
43
57
|
|
|
44
|
-
function buildJsonOutput(conversation, response, sources) {
|
|
58
|
+
function buildJsonOutput(conversation, response, sources, duration) {
|
|
45
59
|
const lastUserMessage = conversation.messages.findLast((m) => m.role === 'user')
|
|
46
60
|
const output = {
|
|
47
61
|
provider: conversation.provider,
|
|
@@ -49,7 +63,8 @@ function buildJsonOutput(conversation, response, sources) {
|
|
|
49
63
|
question: lastUserMessage?.content || '',
|
|
50
64
|
response,
|
|
51
65
|
conversationId: conversation.id,
|
|
52
|
-
messageCount: conversation.messages.length + 1
|
|
66
|
+
messageCount: conversation.messages.length + 1,
|
|
67
|
+
durationMs: duration
|
|
53
68
|
}
|
|
54
69
|
|
|
55
70
|
if (sources?.length > 0) {
|
|
@@ -59,8 +74,8 @@ function buildJsonOutput(conversation, response, sources) {
|
|
|
59
74
|
return output
|
|
60
75
|
}
|
|
61
76
|
|
|
62
|
-
function outputJson(conversation, response, sources) {
|
|
63
|
-
const output = buildJsonOutput(conversation, response, sources)
|
|
77
|
+
function outputJson(conversation, response, sources, duration) {
|
|
78
|
+
const output = buildJsonOutput(conversation, response, sources, duration)
|
|
64
79
|
console.log(JSON.stringify(output, null, 2))
|
|
65
80
|
}
|
|
66
81
|
|
package/package.json
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
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",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"cli",
|
|
9
|
+
"ai",
|
|
10
|
+
"llm",
|
|
11
|
+
"perplexity",
|
|
12
|
+
"openai",
|
|
13
|
+
"anthropic",
|
|
14
|
+
"claude",
|
|
15
|
+
"gpt",
|
|
16
|
+
"chatbot",
|
|
17
|
+
"terminal"
|
|
18
|
+
],
|
|
7
19
|
"type": "module",
|
|
8
20
|
"bin": {
|
|
9
21
|
"askimo": "./index.mjs"
|
|
@@ -27,8 +39,10 @@
|
|
|
27
39
|
"@ai-sdk/anthropic": "^2.0.53",
|
|
28
40
|
"@ai-sdk/openai": "^2.0.76",
|
|
29
41
|
"@ai-sdk/perplexity": "^2.0.21",
|
|
42
|
+
"@ai-sdk/xai": "^2.0.40",
|
|
30
43
|
"@inquirer/input": "^5.0.2",
|
|
31
44
|
"ai": "^5.0.106",
|
|
32
|
-
"commander": "^14.0.2"
|
|
45
|
+
"commander": "^14.0.2",
|
|
46
|
+
"hcat": "^2.2.1"
|
|
33
47
|
}
|
|
34
48
|
}
|