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 +2 -0
- package/dist/cli.js +261 -0
- package/package.json +5 -4
- package/src/cli.ts +307 -0
package/dist/cli.d.ts
ADDED
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">✓</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.
|
|
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
|
-
"
|
|
29
|
-
"
|
|
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">✓</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
|
+
})
|