@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.
@@ -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
+ })