greed-compute-mcp 0.1.1 → 0.2.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/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,261 @@
1
+ #!/usr/bin/env node
2
+ import http from 'node:http';
3
+ import { execFileSync } from 'node:child_process';
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import { createInterface } from 'node:readline';
7
+ const BASE = 'https://compute.deep-ml.com';
8
+ const FRONTEND = 'https://greed-compute-ui.vercel.app';
9
+ const PORT = 19432;
10
+ // ── Helpers ──────────────────────────────────────────────────────────────────
11
+ const RESET = '\x1b[0m';
12
+ const BOLD = '\x1b[1m';
13
+ const DIM = '\x1b[2m';
14
+ const GREEN = '\x1b[32m';
15
+ const CYAN = '\x1b[36m';
16
+ const YELLOW = '\x1b[33m';
17
+ function log(msg) { console.log(msg); }
18
+ function accent(s) { return `${GREEN}${s}${RESET}`; }
19
+ function dim(s) { return `${DIM}${s}${RESET}`; }
20
+ function bold(s) { return `${BOLD}${s}${RESET}`; }
21
+ async function ask(question) {
22
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
23
+ return new Promise(resolve => {
24
+ rl.question(` ${CYAN}›${RESET} ${question} `, answer => {
25
+ rl.close();
26
+ resolve(answer.trim());
27
+ });
28
+ });
29
+ }
30
+ async function select(question, options) {
31
+ log(`\n ${question}`);
32
+ options.forEach((o, i) => log(` ${dim(`${i + 1})`)} ${o}`));
33
+ const answer = await ask(`Pick [1-${options.length}]:`);
34
+ const idx = parseInt(answer) - 1;
35
+ return idx >= 0 && idx < options.length ? idx : 0;
36
+ }
37
+ function openUrl(url) {
38
+ try {
39
+ const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
40
+ execFileSync(cmd, [url], { stdio: 'ignore' });
41
+ }
42
+ catch {
43
+ log(`\n Open this URL in your browser:\n ${accent(url)}`);
44
+ }
45
+ }
46
+ // ── Auth Flow ────────────────────────────────────────────────────────────────
47
+ function waitForAuth() {
48
+ return new Promise((resolve, reject) => {
49
+ const server = http.createServer((req, res) => {
50
+ const url = new URL(req.url || '', `http://localhost:${PORT}`);
51
+ if (url.pathname === '/callback') {
52
+ const key = url.searchParams.get('key') || '';
53
+ const login = url.searchParams.get('login') || '';
54
+ res.writeHead(200, { 'Content-Type': 'text/html' });
55
+ res.end(`
56
+ <html>
57
+ <body style="background:#0A0A08;color:#EFEFED;font-family:monospace;display:flex;align-items:center;justify-content:center;height:100vh;margin:0">
58
+ <div style="text-align:center">
59
+ <p style="color:#C8F135;font-size:24px;margin-bottom:8px">&#10003;</p>
60
+ <p style="font-size:14px">Logged in as <strong>@${login}</strong></p>
61
+ <p style="color:#A0A09C;font-size:12px;margin-top:16px">You can close this tab.</p>
62
+ </div>
63
+ </body>
64
+ </html>
65
+ `);
66
+ server.close();
67
+ resolve({ key, login });
68
+ }
69
+ });
70
+ server.listen(PORT, () => { });
71
+ server.on('error', reject);
72
+ setTimeout(() => { server.close(); reject(new Error('Auth timed out')); }, 120_000);
73
+ });
74
+ }
75
+ // ── Config Writers ───────────────────────────────────────────────────────────
76
+ function getClaudeDesktopConfigPath() {
77
+ if (process.platform === 'darwin') {
78
+ return path.join(process.env.HOME || '', 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
79
+ }
80
+ else if (process.platform === 'win32') {
81
+ return path.join(process.env.APPDATA || '', 'Claude', 'claude_desktop_config.json');
82
+ }
83
+ return path.join(process.env.HOME || '', '.config', 'claude', 'claude_desktop_config.json');
84
+ }
85
+ function writeClaudeDesktopConfig(apiKey) {
86
+ const configPath = getClaudeDesktopConfigPath();
87
+ let config = {};
88
+ try {
89
+ if (fs.existsSync(configPath)) {
90
+ config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
91
+ }
92
+ }
93
+ catch { }
94
+ const servers = (config.mcpServers || {});
95
+ servers['greed-compute'] = {
96
+ command: 'npx',
97
+ args: ['greed-compute-mcp'],
98
+ env: { GREED_API_KEY: apiKey },
99
+ };
100
+ config.mcpServers = servers;
101
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
102
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
103
+ return configPath;
104
+ }
105
+ function writeClaudeCodeConfig(apiKey) {
106
+ try {
107
+ execFileSync('claude', ['mcp', 'add', 'greed-compute', '-e', `GREED_API_KEY=${apiKey}`, '--', 'npx', 'greed-compute-mcp'], { stdio: 'ignore' });
108
+ return true;
109
+ }
110
+ catch {
111
+ return false;
112
+ }
113
+ }
114
+ function writeEnvFile(apiKey, template) {
115
+ const envPath = path.join(process.cwd(), '.env');
116
+ let content = '';
117
+ try {
118
+ content = fs.readFileSync(envPath, 'utf-8');
119
+ }
120
+ catch { }
121
+ if (content.includes('GREED_API_KEY')) {
122
+ content = content.replace(/GREED_API_KEY=.*/g, `GREED_API_KEY=${apiKey}`);
123
+ }
124
+ else {
125
+ content += `${content.length > 0 ? '\n' : ''}GREED_API_KEY=${apiKey}\n`;
126
+ }
127
+ if (!content.includes('GREED_DEFAULT_TEMPLATE')) {
128
+ content += `GREED_DEFAULT_TEMPLATE=${template}\n`;
129
+ }
130
+ fs.writeFileSync(envPath, content);
131
+ return envPath;
132
+ }
133
+ // ── Demo Run ─────────────────────────────────────────────────────────────────
134
+ async function runDemo(apiKey, template) {
135
+ log(`\n ${dim('running a quick demo...')}`);
136
+ const createRes = await fetch(`${BASE}/v1/session/create`, {
137
+ method: 'POST',
138
+ headers: { 'Content-Type': 'application/json', 'X-API-Key': apiKey },
139
+ body: JSON.stringify({ template }),
140
+ });
141
+ const session = await createRes.json();
142
+ if (!session.session_id) {
143
+ log(` ${YELLOW}Demo failed to create session${RESET}`);
144
+ return;
145
+ }
146
+ log(` ${dim('session:')} ${session.session_id.slice(0, 8)}...`);
147
+ const code = template === 'data-science'
148
+ ? 'import numpy as np; print(f"numpy {np.__version__} ready. random: {np.random.randn(3).round(2).tolist()}")'
149
+ : template === 'machine-learning'
150
+ ? 'import sklearn; print(f"scikit-learn {sklearn.__version__} ready")'
151
+ : 'print("hello from greed-compute")';
152
+ const execRes = await fetch(`${BASE}/v1/session/${session.session_id}/execute`, {
153
+ method: 'POST',
154
+ headers: { 'Content-Type': 'application/json', 'X-API-Key': apiKey },
155
+ body: JSON.stringify({ code }),
156
+ });
157
+ const result = await execRes.json();
158
+ if (result.stdout) {
159
+ log(` ${accent('→')} ${result.stdout.trim()} ${dim(`(${result.duration_ms}ms)`)}`);
160
+ }
161
+ if (result.error) {
162
+ log(` ${YELLOW}${result.error}${RESET}`);
163
+ }
164
+ // Clean up
165
+ await fetch(`${BASE}/v1/session/${session.session_id}`, {
166
+ method: 'DELETE',
167
+ headers: { 'X-API-Key': apiKey },
168
+ });
169
+ }
170
+ // ── Main ─────────────────────────────────────────────────────────────────────
171
+ async function main() {
172
+ log('');
173
+ log(` ${bold('greed')}${accent('.')}${bold('compute')}`);
174
+ log(` ${dim('stateful python for AI agents')}`);
175
+ log('');
176
+ // Step 1: Auth
177
+ log(` ${dim('step 1/3')} ${bold('authenticate')}`);
178
+ log('');
179
+ const authUrl = `${BASE}/v1/auth/github?redirect_uri=http://localhost:${PORT}/callback`;
180
+ log(` Opening GitHub login...`);
181
+ openUrl(authUrl);
182
+ log(` ${dim('waiting for auth...')}`);
183
+ let key;
184
+ let login;
185
+ try {
186
+ const result = await waitForAuth();
187
+ key = result.key;
188
+ login = result.login;
189
+ }
190
+ catch {
191
+ log(`\n ${YELLOW}Auth timed out.${RESET}`);
192
+ log(` Get your key at: ${accent(FRONTEND + '/login')}`);
193
+ const manual = await ask('Paste your API key:');
194
+ key = manual;
195
+ login = 'unknown';
196
+ }
197
+ log(` ${accent('✓')} Logged in as ${bold('@' + login)}`);
198
+ log(` ${dim('key: ' + key.slice(0, 12) + '...')}`);
199
+ // Step 2: Where are you using this?
200
+ log('');
201
+ log(` ${dim('step 2/3')} ${bold('configure')}`);
202
+ const target = await select('Where do you want to use greed-compute?', [
203
+ 'Claude Desktop',
204
+ 'Claude Code',
205
+ 'Both',
206
+ 'Just give me the key',
207
+ ]);
208
+ if (target === 0 || target === 2) {
209
+ const configPath = writeClaudeDesktopConfig(key);
210
+ log(` ${accent('✓')} Added to Claude Desktop`);
211
+ log(` ${dim(configPath)}`);
212
+ }
213
+ if (target === 1 || target === 2) {
214
+ const ok = writeClaudeCodeConfig(key);
215
+ if (ok) {
216
+ log(` ${accent('✓')} Added to Claude Code`);
217
+ }
218
+ else {
219
+ log(` ${YELLOW}Could not auto-configure Claude Code${RESET}`);
220
+ log(` ${dim('run: claude mcp add greed-compute -e GREED_API_KEY=' + key + ' -- npx greed-compute-mcp')}`);
221
+ }
222
+ }
223
+ if (target === 3) {
224
+ log(`\n Your API key: ${accent(key)}`);
225
+ }
226
+ // Step 3: Template
227
+ log('');
228
+ log(` ${dim('step 3/3')} ${bold('default template')}`);
229
+ log(` ${dim('your LLM uses this when creating sessions')}`);
230
+ const tmpl = await select('Pick a template:', [
231
+ 'blank — clean Python, nothing pre-installed',
232
+ 'data-science — numpy, pandas, sklearn, matplotlib',
233
+ 'machine-learning — torch, transformers, datasets',
234
+ 'web-scraping — requests, beautifulsoup4, lxml',
235
+ ]);
236
+ const templates = ['blank', 'data-science', 'machine-learning', 'web-scraping'];
237
+ const chosen = templates[tmpl];
238
+ log(` ${accent('✓')} Default template: ${bold(chosen)}`);
239
+ // Save .env
240
+ const envPath = writeEnvFile(key, chosen);
241
+ log(` ${accent('✓')} Saved to ${dim(envPath)}`);
242
+ // Demo run
243
+ await runDemo(key, chosen);
244
+ // Done
245
+ log('');
246
+ log(` ${accent('─'.repeat(45))}`);
247
+ log('');
248
+ log(` ${accent('✓')} ${bold("you're all set")}`);
249
+ log('');
250
+ log(` Ask your LLM to ${accent('"create a Python session and run some code"')}`);
251
+ log(` and it'll just work.`);
252
+ log('');
253
+ log(` ${dim('docs:')} ${FRONTEND}/docs`);
254
+ log(` ${dim('dash:')} ${FRONTEND}/dashboard`);
255
+ log('');
256
+ process.exit(0);
257
+ }
258
+ main().catch(err => {
259
+ console.error('Error:', err.message);
260
+ process.exit(1);
261
+ });
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "greed-compute-mcp",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "MCP server for greed-compute — gives LLMs stateful Python execution",
5
5
  "bin": {
6
- "greed-compute-mcp": "./dist/index.js"
6
+ "greed-compute-mcp": "./dist/index.js",
7
+ "greed-compute": "./dist/cli.js"
7
8
  },
8
9
  "main": "./dist/index.js",
9
10
  "type": "module",
@@ -25,7 +26,7 @@
25
26
  "@modelcontextprotocol/sdk": "^1.12.1"
26
27
  },
27
28
  "devDependencies": {
28
- "typescript": "^5.7.0",
29
- "@types/node": "^22.0.0"
29
+ "@types/node": "^22.0.0",
30
+ "typescript": "^5.7.0"
30
31
  }
31
32
  }
package/src/cli.ts ADDED
@@ -0,0 +1,307 @@
1
+ #!/usr/bin/env node
2
+
3
+ import http from 'node:http'
4
+ import { execFileSync } from 'node:child_process'
5
+ import fs from 'node:fs'
6
+ import path from 'node:path'
7
+ import { createInterface } from 'node:readline'
8
+
9
+ const BASE = 'https://compute.deep-ml.com'
10
+ const FRONTEND = 'https://greed-compute-ui.vercel.app'
11
+ const PORT = 19432
12
+
13
+ // ── Helpers ──────────────────────────────────────────────────────────────────
14
+
15
+ const RESET = '\x1b[0m'
16
+ const BOLD = '\x1b[1m'
17
+ const DIM = '\x1b[2m'
18
+ const GREEN = '\x1b[32m'
19
+ const CYAN = '\x1b[36m'
20
+ const YELLOW = '\x1b[33m'
21
+
22
+ function log(msg: string) { console.log(msg) }
23
+ function accent(s: string) { return `${GREEN}${s}${RESET}` }
24
+ function dim(s: string) { return `${DIM}${s}${RESET}` }
25
+ function bold(s: string) { return `${BOLD}${s}${RESET}` }
26
+
27
+ async function ask(question: string): Promise<string> {
28
+ const rl = createInterface({ input: process.stdin, output: process.stdout })
29
+ return new Promise(resolve => {
30
+ rl.question(` ${CYAN}›${RESET} ${question} `, answer => {
31
+ rl.close()
32
+ resolve(answer.trim())
33
+ })
34
+ })
35
+ }
36
+
37
+ async function select(question: string, options: string[]): Promise<number> {
38
+ log(`\n ${question}`)
39
+ options.forEach((o, i) => log(` ${dim(`${i + 1})`)} ${o}`))
40
+ const answer = await ask(`Pick [1-${options.length}]:`)
41
+ const idx = parseInt(answer) - 1
42
+ return idx >= 0 && idx < options.length ? idx : 0
43
+ }
44
+
45
+ function openUrl(url: string) {
46
+ try {
47
+ const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open'
48
+ execFileSync(cmd, [url], { stdio: 'ignore' })
49
+ } catch {
50
+ log(`\n Open this URL in your browser:\n ${accent(url)}`)
51
+ }
52
+ }
53
+
54
+ // ── Auth Flow ────────────────────────────────────────────────────────────────
55
+
56
+ function waitForAuth(): Promise<{ key: string; login: string }> {
57
+ return new Promise((resolve, reject) => {
58
+ const server = http.createServer((req, res) => {
59
+ const url = new URL(req.url || '', `http://localhost:${PORT}`)
60
+
61
+ if (url.pathname === '/callback') {
62
+ const key = url.searchParams.get('key') || ''
63
+ const login = url.searchParams.get('login') || ''
64
+
65
+ res.writeHead(200, { 'Content-Type': 'text/html' })
66
+ res.end(`
67
+ <html>
68
+ <body style="background:#0A0A08;color:#EFEFED;font-family:monospace;display:flex;align-items:center;justify-content:center;height:100vh;margin:0">
69
+ <div style="text-align:center">
70
+ <p style="color:#C8F135;font-size:24px;margin-bottom:8px">&#10003;</p>
71
+ <p style="font-size:14px">Logged in as <strong>@${login}</strong></p>
72
+ <p style="color:#A0A09C;font-size:12px;margin-top:16px">You can close this tab.</p>
73
+ </div>
74
+ </body>
75
+ </html>
76
+ `)
77
+
78
+ server.close()
79
+ resolve({ key, login })
80
+ }
81
+ })
82
+
83
+ server.listen(PORT, () => {})
84
+ server.on('error', reject)
85
+
86
+ setTimeout(() => { server.close(); reject(new Error('Auth timed out')) }, 120_000)
87
+ })
88
+ }
89
+
90
+ // ── Config Writers ───────────────────────────────────────────────────────────
91
+
92
+ function getClaudeDesktopConfigPath(): string {
93
+ if (process.platform === 'darwin') {
94
+ return path.join(process.env.HOME || '', 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json')
95
+ } else if (process.platform === 'win32') {
96
+ return path.join(process.env.APPDATA || '', 'Claude', 'claude_desktop_config.json')
97
+ }
98
+ return path.join(process.env.HOME || '', '.config', 'claude', 'claude_desktop_config.json')
99
+ }
100
+
101
+ function writeClaudeDesktopConfig(apiKey: string) {
102
+ const configPath = getClaudeDesktopConfigPath()
103
+ let config: Record<string, unknown> = {}
104
+
105
+ try {
106
+ if (fs.existsSync(configPath)) {
107
+ config = JSON.parse(fs.readFileSync(configPath, 'utf-8'))
108
+ }
109
+ } catch {}
110
+
111
+ const servers = (config.mcpServers || {}) as Record<string, unknown>
112
+ servers['greed-compute'] = {
113
+ command: 'npx',
114
+ args: ['greed-compute-mcp'],
115
+ env: { GREED_API_KEY: apiKey },
116
+ }
117
+ config.mcpServers = servers
118
+
119
+ fs.mkdirSync(path.dirname(configPath), { recursive: true })
120
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2))
121
+ return configPath
122
+ }
123
+
124
+ function writeClaudeCodeConfig(apiKey: string) {
125
+ try {
126
+ execFileSync('claude', ['mcp', 'add', 'greed-compute', '-e', `GREED_API_KEY=${apiKey}`, '--', 'npx', 'greed-compute-mcp'], { stdio: 'ignore' })
127
+ return true
128
+ } catch {
129
+ return false
130
+ }
131
+ }
132
+
133
+ function writeEnvFile(apiKey: string, template: string) {
134
+ const envPath = path.join(process.cwd(), '.env')
135
+ let content = ''
136
+ try { content = fs.readFileSync(envPath, 'utf-8') } catch {}
137
+
138
+ if (content.includes('GREED_API_KEY')) {
139
+ content = content.replace(/GREED_API_KEY=.*/g, `GREED_API_KEY=${apiKey}`)
140
+ } else {
141
+ content += `${content.length > 0 ? '\n' : ''}GREED_API_KEY=${apiKey}\n`
142
+ }
143
+
144
+ if (!content.includes('GREED_DEFAULT_TEMPLATE')) {
145
+ content += `GREED_DEFAULT_TEMPLATE=${template}\n`
146
+ }
147
+
148
+ fs.writeFileSync(envPath, content)
149
+ return envPath
150
+ }
151
+
152
+ // ── Demo Run ─────────────────────────────────────────────────────────────────
153
+
154
+ async function runDemo(apiKey: string, template: string) {
155
+ log(`\n ${dim('running a quick demo...')}`)
156
+
157
+ const createRes = await fetch(`${BASE}/v1/session/create`, {
158
+ method: 'POST',
159
+ headers: { 'Content-Type': 'application/json', 'X-API-Key': apiKey },
160
+ body: JSON.stringify({ template }),
161
+ })
162
+ const session = await createRes.json() as { session_id: string }
163
+
164
+ if (!session.session_id) {
165
+ log(` ${YELLOW}Demo failed to create session${RESET}`)
166
+ return
167
+ }
168
+
169
+ log(` ${dim('session:')} ${session.session_id.slice(0, 8)}...`)
170
+
171
+ const code = template === 'data-science'
172
+ ? 'import numpy as np; print(f"numpy {np.__version__} ready. random: {np.random.randn(3).round(2).tolist()}")'
173
+ : template === 'machine-learning'
174
+ ? 'import sklearn; print(f"scikit-learn {sklearn.__version__} ready")'
175
+ : 'print("hello from greed-compute")'
176
+
177
+ const execRes = await fetch(`${BASE}/v1/session/${session.session_id}/execute`, {
178
+ method: 'POST',
179
+ headers: { 'Content-Type': 'application/json', 'X-API-Key': apiKey },
180
+ body: JSON.stringify({ code }),
181
+ })
182
+ const result = await execRes.json() as { stdout?: string; duration_ms?: number; error?: string }
183
+
184
+ if (result.stdout) {
185
+ log(` ${accent('→')} ${result.stdout.trim()} ${dim(`(${result.duration_ms}ms)`)}`)
186
+ }
187
+ if (result.error) {
188
+ log(` ${YELLOW}${result.error}${RESET}`)
189
+ }
190
+
191
+ // Clean up
192
+ await fetch(`${BASE}/v1/session/${session.session_id}`, {
193
+ method: 'DELETE',
194
+ headers: { 'X-API-Key': apiKey },
195
+ })
196
+ }
197
+
198
+ // ── Main ─────────────────────────────────────────────────────────────────────
199
+
200
+ async function main() {
201
+ log('')
202
+ log(` ${bold('greed')}${accent('.')}${bold('compute')}`)
203
+ log(` ${dim('stateful python for AI agents')}`)
204
+ log('')
205
+
206
+ // Step 1: Auth
207
+ log(` ${dim('step 1/3')} ${bold('authenticate')}`)
208
+ log('')
209
+
210
+ const authUrl = `${BASE}/v1/auth/github?redirect_uri=http://localhost:${PORT}/callback`
211
+
212
+ log(` Opening GitHub login...`)
213
+ openUrl(authUrl)
214
+ log(` ${dim('waiting for auth...')}`)
215
+
216
+ let key: string
217
+ let login: string
218
+
219
+ try {
220
+ const result = await waitForAuth()
221
+ key = result.key
222
+ login = result.login
223
+ } catch {
224
+ log(`\n ${YELLOW}Auth timed out.${RESET}`)
225
+ log(` Get your key at: ${accent(FRONTEND + '/login')}`)
226
+ const manual = await ask('Paste your API key:')
227
+ key = manual
228
+ login = 'unknown'
229
+ }
230
+
231
+ log(` ${accent('✓')} Logged in as ${bold('@' + login)}`)
232
+ log(` ${dim('key: ' + key.slice(0, 12) + '...')}`)
233
+
234
+ // Step 2: Where are you using this?
235
+ log('')
236
+ log(` ${dim('step 2/3')} ${bold('configure')}`)
237
+
238
+ const target = await select('Where do you want to use greed-compute?', [
239
+ 'Claude Desktop',
240
+ 'Claude Code',
241
+ 'Both',
242
+ 'Just give me the key',
243
+ ])
244
+
245
+ if (target === 0 || target === 2) {
246
+ const configPath = writeClaudeDesktopConfig(key)
247
+ log(` ${accent('✓')} Added to Claude Desktop`)
248
+ log(` ${dim(configPath)}`)
249
+ }
250
+
251
+ if (target === 1 || target === 2) {
252
+ const ok = writeClaudeCodeConfig(key)
253
+ if (ok) {
254
+ log(` ${accent('✓')} Added to Claude Code`)
255
+ } else {
256
+ log(` ${YELLOW}Could not auto-configure Claude Code${RESET}`)
257
+ log(` ${dim('run: claude mcp add greed-compute -e GREED_API_KEY=' + key + ' -- npx greed-compute-mcp')}`)
258
+ }
259
+ }
260
+
261
+ if (target === 3) {
262
+ log(`\n Your API key: ${accent(key)}`)
263
+ }
264
+
265
+ // Step 3: Template
266
+ log('')
267
+ log(` ${dim('step 3/3')} ${bold('default template')}`)
268
+ log(` ${dim('your LLM uses this when creating sessions')}`)
269
+
270
+ const tmpl = await select('Pick a template:', [
271
+ 'blank — clean Python, nothing pre-installed',
272
+ 'data-science — numpy, pandas, sklearn, matplotlib',
273
+ 'machine-learning — torch, transformers, datasets',
274
+ 'web-scraping — requests, beautifulsoup4, lxml',
275
+ ])
276
+
277
+ const templates = ['blank', 'data-science', 'machine-learning', 'web-scraping']
278
+ const chosen = templates[tmpl]
279
+ log(` ${accent('✓')} Default template: ${bold(chosen)}`)
280
+
281
+ // Save .env
282
+ const envPath = writeEnvFile(key, chosen)
283
+ log(` ${accent('✓')} Saved to ${dim(envPath)}`)
284
+
285
+ // Demo run
286
+ await runDemo(key, chosen)
287
+
288
+ // Done
289
+ log('')
290
+ log(` ${accent('─'.repeat(45))}`)
291
+ log('')
292
+ log(` ${accent('✓')} ${bold("you're all set")}`)
293
+ log('')
294
+ log(` Ask your LLM to ${accent('"create a Python session and run some code"')}`)
295
+ log(` and it'll just work.`)
296
+ log('')
297
+ log(` ${dim('docs:')} ${FRONTEND}/docs`)
298
+ log(` ${dim('dash:')} ${FRONTEND}/dashboard`)
299
+ log('')
300
+
301
+ process.exit(0)
302
+ }
303
+
304
+ main().catch(err => {
305
+ console.error('Error:', err.message)
306
+ process.exit(1)
307
+ })