@agentic.artists/modelshortlist 0.2.1
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/.env.local.example +6 -0
- package/ATTRIBUTION.md +25 -0
- package/LICENSE +21 -0
- package/README.md +240 -0
- package/bin/modelshortlist.js +3 -0
- package/config/aliases.json +3 -0
- package/lib/artificial-analysis.js +104 -0
- package/lib/catalog.js +117 -0
- package/lib/http.js +68 -0
- package/lib/match.js +100 -0
- package/lib/openrouter.js +276 -0
- package/mcp/server.js +369 -0
- package/package.json +56 -0
- package/scripts/setup.js +179 -0
package/scripts/setup.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import process from 'node:process'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
|
|
6
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
7
|
+
const ENV_PATH = path.join(ROOT, '.env.local')
|
|
8
|
+
const MCP_PATH = path.join(ROOT, 'mcp', 'server.js')
|
|
9
|
+
|
|
10
|
+
function parseEnv(text) {
|
|
11
|
+
const values = {}
|
|
12
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
13
|
+
const line = rawLine.trim()
|
|
14
|
+
if (!line || line.startsWith('#')) continue
|
|
15
|
+
const equals = line.indexOf('=')
|
|
16
|
+
if (equals <= 0) continue
|
|
17
|
+
const key = line.slice(0, equals).trim()
|
|
18
|
+
let value = line.slice(equals + 1).trim()
|
|
19
|
+
if (
|
|
20
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
21
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
22
|
+
) {
|
|
23
|
+
value = value.slice(1, -1)
|
|
24
|
+
}
|
|
25
|
+
values[key] = value
|
|
26
|
+
}
|
|
27
|
+
return values
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function maskPrompt(label) {
|
|
31
|
+
if (!process.stdin.isTTY || typeof process.stdin.setRawMode !== 'function') {
|
|
32
|
+
throw new Error(
|
|
33
|
+
'Interactive setup requires a terminal. Create .env.local from .env.local.example instead.',
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
const stdin = process.stdin
|
|
39
|
+
const stdout = process.stdout
|
|
40
|
+
let value = ''
|
|
41
|
+
|
|
42
|
+
const finish = () => {
|
|
43
|
+
stdin.setRawMode(false)
|
|
44
|
+
stdin.pause()
|
|
45
|
+
stdin.removeListener('data', onData)
|
|
46
|
+
stdout.write('\n')
|
|
47
|
+
resolve(value.trim())
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const fail = (error) => {
|
|
51
|
+
try {
|
|
52
|
+
stdin.setRawMode(false)
|
|
53
|
+
} catch {}
|
|
54
|
+
stdin.pause()
|
|
55
|
+
stdin.removeListener('data', onData)
|
|
56
|
+
reject(error)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const onData = (chunk) => {
|
|
60
|
+
const text = String(chunk)
|
|
61
|
+
for (const char of text) {
|
|
62
|
+
if (char === '\u0003') {
|
|
63
|
+
stdout.write('\n')
|
|
64
|
+
fail(new Error('Setup cancelled'))
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
if (char === '\r' || char === '\n') {
|
|
68
|
+
finish()
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
if (char === '\u007f' || char === '\b') {
|
|
72
|
+
if (value.length > 0) {
|
|
73
|
+
value = value.slice(0, -1)
|
|
74
|
+
stdout.write('\b \b')
|
|
75
|
+
}
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
if (char < ' ') continue
|
|
79
|
+
value += char
|
|
80
|
+
stdout.write('*')
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
stdout.write(label)
|
|
85
|
+
stdin.setEncoding('utf8')
|
|
86
|
+
stdin.setRawMode(true)
|
|
87
|
+
stdin.resume()
|
|
88
|
+
stdin.on('data', onData)
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function envFile(values) {
|
|
93
|
+
return [
|
|
94
|
+
'# ModelShortlist local credentials',
|
|
95
|
+
'# This file is gitignored. Do not commit it.',
|
|
96
|
+
`ARTIFICIAL_ANALYSIS_API_KEY=${values.ARTIFICIAL_ANALYSIS_API_KEY}`,
|
|
97
|
+
`OPENROUTER_API_KEY=${values.OPENROUTER_API_KEY}`,
|
|
98
|
+
'',
|
|
99
|
+
'# Optional. Best-effort in-process cache (12 hours).',
|
|
100
|
+
`MODEL_SELECTOR_CACHE_TTL_MS=${values.MODEL_SELECTOR_CACHE_TTL_MS || '43200000'}`,
|
|
101
|
+
'',
|
|
102
|
+
].join('\n')
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function slashPath(value) {
|
|
106
|
+
return value.replaceAll('\\', '/')
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function printClientConfig() {
|
|
110
|
+
const nodePath = slashPath(process.execPath)
|
|
111
|
+
const serverPath = slashPath(MCP_PATH)
|
|
112
|
+
const standardConfig = {
|
|
113
|
+
mcpServers: {
|
|
114
|
+
modelshortlist: {
|
|
115
|
+
command: nodePath,
|
|
116
|
+
args: [serverPath],
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
}
|
|
120
|
+
const vscodeConfig = {
|
|
121
|
+
servers: {
|
|
122
|
+
modelshortlist: {
|
|
123
|
+
type: 'stdio',
|
|
124
|
+
command: nodePath,
|
|
125
|
+
args: [serverPath],
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
console.log('\nHermes Desktop / Cursor MCP config:')
|
|
131
|
+
console.log(JSON.stringify(standardConfig, null, 2))
|
|
132
|
+
console.log('\nVS Code / Copilot MCP config:')
|
|
133
|
+
console.log(JSON.stringify(vscodeConfig, null, 2))
|
|
134
|
+
console.log('\nThe generated config uses the exact Node executable running setup, so GUI clients do not need Node on their PATH.')
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function main() {
|
|
138
|
+
console.log('ModelShortlist setup')
|
|
139
|
+
console.log('Your API keys are stored only in .env.local on this machine.')
|
|
140
|
+
console.log('Input is masked and is not sent anywhere by this setup script.\n')
|
|
141
|
+
|
|
142
|
+
const existing = fs.existsSync(ENV_PATH)
|
|
143
|
+
? parseEnv(fs.readFileSync(ENV_PATH, 'utf8'))
|
|
144
|
+
: {}
|
|
145
|
+
|
|
146
|
+
const values = {
|
|
147
|
+
ARTIFICIAL_ANALYSIS_API_KEY: existing.ARTIFICIAL_ANALYSIS_API_KEY || '',
|
|
148
|
+
OPENROUTER_API_KEY: existing.OPENROUTER_API_KEY || '',
|
|
149
|
+
MODEL_SELECTOR_CACHE_TTL_MS: existing.MODEL_SELECTOR_CACHE_TTL_MS || '43200000',
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!values.ARTIFICIAL_ANALYSIS_API_KEY) {
|
|
153
|
+
values.ARTIFICIAL_ANALYSIS_API_KEY = await maskPrompt('Artificial Analysis API key: ')
|
|
154
|
+
} else {
|
|
155
|
+
console.log('Artificial Analysis API key: already configured')
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!values.OPENROUTER_API_KEY) {
|
|
159
|
+
values.OPENROUTER_API_KEY = await maskPrompt('OpenRouter API key: ')
|
|
160
|
+
} else {
|
|
161
|
+
console.log('OpenRouter API key: already configured')
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (!values.ARTIFICIAL_ANALYSIS_API_KEY || !values.OPENROUTER_API_KEY) {
|
|
165
|
+
throw new Error('Both API keys are required')
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
fs.writeFileSync(ENV_PATH, envFile(values), { encoding: 'utf8', mode: 0o600 })
|
|
169
|
+
|
|
170
|
+
console.log(`\nSaved local credentials to ${ENV_PATH}`)
|
|
171
|
+
printClientConfig()
|
|
172
|
+
console.log('\nNext: paste the appropriate config into your MCP client, then start a new chat.')
|
|
173
|
+
console.log('Try: "Use ModelShortlist to recommend a model for my workload."')
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
main().catch((error) => {
|
|
177
|
+
console.error(`\nSetup failed: ${error.message}`)
|
|
178
|
+
process.exitCode = 1
|
|
179
|
+
})
|