@peume/cli 0.0.0-stage → 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.
Files changed (3) hide show
  1. package/README.md +35 -2
  2. package/package.json +24 -4
  3. package/peume.mjs +196 -0
package/README.md CHANGED
@@ -1,3 +1,36 @@
1
- # Temporary Holding Version
1
+ # @peume/cli
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Put your app online from Claude Code or your terminal. No account, no GitHub. Free on `yourapp.peume.com`.
4
+
5
+ ## Claude Code
6
+
7
+ ```bash
8
+ claude mcp add peume -- npx -y @peume/cli mcp
9
+ ```
10
+
11
+ Then ask: **“Deploy this on Peume”**. Claude gets your link, and if the build fails it reads the logs and fixes the code.
12
+
13
+ ## Terminal
14
+
15
+ ```bash
16
+ npx @peume/cli deploy # deploy the current folder and follow the build
17
+ npx @peume/cli status # URL, state and last build
18
+ npx @peume/cli logs # runtime logs
19
+ npx @peume/cli env set KEY=value # set variables, then redeploy
20
+ ```
21
+
22
+ - Works with Node, Next.js, React/Vite, Python, plain HTML or a Dockerfile.
23
+ - Your app must listen on the `PORT` environment variable.
24
+ - Variables in `.env` are set on the app, never stored with the code.
25
+ - `node_modules`, `.git` and build caches are not uploaded.
26
+ - A new app stops after 24 h unless you claim it (free) with the link shown after the deploy.
27
+
28
+ ## Built in
29
+
30
+ Every app gets `PEUME_KEY` and `PEUME_API`:
31
+
32
+ - **Peume Data**: hundreds of data tools (scraping, search, maps, social…).
33
+ - **Peume Routing**: every AI model through the OpenAI SDK, no other account.
34
+ - **Payments**: get paid in your app, money goes to your own Stripe account.
35
+
36
+ Docs: https://cloud.peume.com/docs
package/package.json CHANGED
@@ -1,6 +1,26 @@
1
1
  {
2
2
  "name": "@peume/cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "Deploy your app on Peume from Claude Code or your terminal. No account, no GitHub.",
5
+ "type": "module",
6
+ "bin": {
7
+ "peume": "peume.mjs"
8
+ },
9
+ "files": [
10
+ "peume.mjs",
11
+ "README.md"
12
+ ],
13
+ "engines": {
14
+ "node": ">=20"
15
+ },
16
+ "license": "MIT",
17
+ "keywords": [
18
+ "deploy",
19
+ "hosting",
20
+ "mcp",
21
+ "claude-code",
22
+ "vibe-coding",
23
+ "paas"
24
+ ],
25
+ "homepage": "https://cloud.peume.com"
26
+ }
package/peume.mjs ADDED
@@ -0,0 +1,196 @@
1
+ #!/usr/bin/env node
2
+ // Peume CLI et serveur MCP (même fichier) : envoie le dossier courant sur Peume, suit le build, rend l'adresse de l'app.
3
+ // peume deploy [dossier] peume status peume logs peume env set CLE=valeur… peume mcp (pour Claude Code)
4
+ // Le nom de l'app est gardé dans <dossier>/.peume ; le jeton qui permet de la redéployer reste sur la machine (~/.peume/jetons.json).
5
+ import { spawn } from 'node:child_process';
6
+ import { readFile, writeFile, mkdir } from 'node:fs/promises';
7
+ import { join, resolve, basename } from 'node:path';
8
+ import { homedir } from 'node:os';
9
+ import { createInterface } from 'node:readline';
10
+
11
+ const PEUME = (process.env.PEUME_URL || 'https://cloud.peume.com').replace(/\/$/, '');
12
+ const JETONS = join(homedir(), '.peume', 'jetons.json');
13
+ const EXCLUS = ['node_modules', '.git', '.next', '.nuxt', '.svelte-kit', '.vercel', '.turbo', '.cache', '.DS_Store', '.peume', '__pycache__', '.venv', 'venv', '.idea', '.vscode', '*.log'];
14
+ const VERSION = '0.2.0';
15
+
16
+ const lireJSON = async (f, d) => { try { return JSON.parse(await readFile(f, 'utf8')); } catch { return d; } };
17
+ async function lien(dossier) {
18
+ const app = (await lireJSON(join(dossier, '.peume'), {})).app;
19
+ return { app, jeton: app ? (await lireJSON(JETONS, {}))[app] : undefined };
20
+ }
21
+ async function retenir(dossier, app, jeton) {
22
+ await writeFile(join(dossier, '.peume'), JSON.stringify({ app }, null, 2) + '\n');
23
+ if (jeton) {
24
+ const j = await lireJSON(JETONS, {}); j[app] = jeton;
25
+ await mkdir(join(homedir(), '.peume'), { recursive: true });
26
+ await writeFile(JETONS, JSON.stringify(j, null, 2), { mode: 0o600 });
27
+ }
28
+ }
29
+
30
+ // dossier → .tar.gz en mémoire (tar est présent sur macOS, Linux et Windows 10+)
31
+ function empaqueter(dossier) {
32
+ return new Promise((ok, ko) => {
33
+ const p = spawn('tar', ['-czf', '-', ...EXCLUS.map(e => `--exclude=${e}`), '-C', dossier, '.'], { env: { ...process.env, COPYFILE_DISABLE: '1' } });
34
+ const d = [], err = [];
35
+ p.stdout.on('data', x => d.push(x)); p.stderr.on('data', x => err.push(x));
36
+ p.on('error', e => ko(new Error(`tar is needed to pack the folder: ${e.message}`)));
37
+ p.on('close', c => c === 0 ? ok(Buffer.concat(d)) : ko(new Error(`Could not pack the folder: ${Buffer.concat(err).toString().slice(0, 300)}`)));
38
+ });
39
+ }
40
+
41
+ async function appel(chemin, { methode = 'GET', corps, entetes = {}, delai = 30000 } = {}) {
42
+ let r;
43
+ try { r = await fetch(`${PEUME}${chemin}`, { method: methode, body: corps, headers: { 'user-agent': `peume-cli/${VERSION}`, ...entetes }, signal: AbortSignal.timeout(delai) }); }
44
+ catch (e) { throw new Error(e.name === 'TimeoutError' ? 'Peume did not answer in time. Try again.' : `Could not reach Peume (${e.message}).`); }
45
+ const j = await r.json().catch(() => ({}));
46
+ if (!r.ok) throw new Error(j.error || `Peume answered ${r.status}.`);
47
+ return j;
48
+ }
49
+ const auth = jeton => (jeton ? { 'x-peume-jeton': jeton } : {});
50
+
51
+ // envoie le dossier puis suit le build (10 min max) ; surLigne reçoit chaque nouvelle ligne de log
52
+ export async function deployer(dossier, { nom, surLigne = () => {}, surEtape = () => {} } = {}) {
53
+ dossier = resolve(dossier);
54
+ const { app, jeton } = await lien(dossier);
55
+ surEtape('Packing the folder…');
56
+ const archive = await empaqueter(dossier);
57
+ if (archive.length > 50e6) throw new Error(`The folder is ${Math.round(archive.length / 1e6)} MB once packed (50 MB max). Move big files out of it.`);
58
+ surEtape(`Uploading ${Math.max(1, Math.round(archive.length / 1e3))} KB…`);
59
+ const r = await appel('/api/deploy', { methode: 'POST', corps: archive, delai: 180000, entetes: {
60
+ 'content-type': 'application/gzip', 'x-peume-nom': nom || basename(dossier), ...(app ? { 'x-peume-app': app } : {}), ...auth(jeton) } });
61
+ await retenir(dossier, r.id, r.jeton);
62
+ surEtape(`Building ${r.url}…`);
63
+ let vus = 0, e = r;
64
+ const fin = Date.now() + 10 * 60e3;
65
+ while (Date.now() < fin) {
66
+ await new Promise(t => setTimeout(t, 2000));
67
+ try { e = await appel(`/api/apps/${r.id}`, { entetes: auth(r.jeton || jeton) }); } catch { continue; }
68
+ const logs = e.deploiement?.logs || [];
69
+ for (const l of logs.slice(vus)) surLigne(l);
70
+ vus = logs.length;
71
+ if (['live', 'failed', 'cancelled'].includes(e.deploiement?.statut)) break;
72
+ }
73
+ return e;
74
+ }
75
+
76
+ const resume = e => {
77
+ const ok = e.deploiement?.statut === 'live';
78
+ const l = [ok ? `Live: ${e.url}` : e.deploiement?.statut === 'failed' ? `Build failed for ${e.url}.` : `Still building: ${e.url} (check again with "npx @peume/cli status").`];
79
+ if (!ok && e.deploiement?.logs?.length) l.push('', 'Last build lines:', ...e.deploiement.logs.slice(-40));
80
+ if (!e.reclamee && e.lienClaim) l.push('', `This app stops in 24 h unless you claim it (free): ${e.lienClaim}`);
81
+ return l.join('\n');
82
+ };
83
+
84
+ async function etat(dossier) {
85
+ const { app, jeton } = await lien(resolve(dossier));
86
+ if (!app) throw new Error('This folder is not deployed yet. Run "npx @peume/cli deploy" first.');
87
+ return { app, jeton, e: await appel(`/api/apps/${app}`, { entetes: auth(jeton) }) };
88
+ }
89
+ async function logs(dossier) {
90
+ const { app, jeton } = await lien(resolve(dossier));
91
+ if (!app) throw new Error('This folder is not deployed yet.');
92
+ return (await appel(`/api/apps/${app}/logs`, { entetes: auth(jeton) })).logs;
93
+ }
94
+ // Peume Data (outils de données via Peume, payés en crédits Peume) avec l'identité de l'app du dossier
95
+ async function data(dossier, chemin, corps) {
96
+ const { app, jeton } = await lien(resolve(dossier));
97
+ if (!app) throw new Error('Deploy this folder on Peume first (deploy tool), then claim the app to use Peume Data.');
98
+ return appel(`/api/v1/data${chemin}`, { methode: corps ? 'POST' : 'GET', corps: corps && JSON.stringify(corps), entetes: { 'content-type': 'application/json', 'x-peume-app': app, ...auth(jeton) }, delai: 60000 });
99
+ }
100
+ async function lancerData(dossier, args) {
101
+ let r = await data(dossier, '/run', { provider: args.provider, endpoint: args.endpoint, input: args.input || {} });
102
+ const fin = Date.now() + 120e3;
103
+ while (!['COMPLETED', 'FAILED', 'BLOCKED', 'STOPPED', 'TIMED_OUT'].includes(r.status) && Date.now() < fin) {
104
+ await new Promise(t => setTimeout(t, 5000));
105
+ r = await data(dossier, `/runs/${r.runId}`);
106
+ }
107
+ const sortie = JSON.stringify(r.output ?? r.error ?? null, null, 2);
108
+ return `Run ${r.runId}: ${r.status}${r.cost ? ` · $${r.cost.value.toFixed(4)}` : ''}\n${sortie.length > 20000 ? `${sortie.slice(0, 20000)}\n… (truncated)` : sortie}`;
109
+ }
110
+
111
+ async function variables(dossier, paires) {
112
+ const { app, jeton } = await lien(resolve(dossier));
113
+ if (!app) throw new Error('This folder is not deployed yet.');
114
+ const vars = Object.entries(paires).map(([key, value]) => ({ key, value: String(value) }));
115
+ await appel(`/api/apps/${app}/env`, { methode: 'PUT', corps: JSON.stringify({ vars, fusion: true }), entetes: { 'content-type': 'application/json', ...auth(jeton) } });
116
+ return appel(`/api/apps/${app}/redeploy`, { methode: 'POST', entetes: auth(jeton) });
117
+ }
118
+
119
+ // ——— serveur MCP (stdio) pour Claude Code : claude mcp add peume -- npx -y @peume/cli mcp
120
+ const OUTILS = [
121
+ { name: 'deploy', description: 'Deploy a project folder on Peume (free, no account needed) and wait for the build. Returns the live URL (name.peume.com), or the build logs if it failed so you can fix the code and deploy again. The app must listen on the PORT environment variable. Variables in .env are set on the app, never uploaded as files. The app also gets PEUME_KEY and PEUME_API for Peume Data and payments (see the services tool).',
122
+ inputSchema: { type: 'object', properties: { path: { type: 'string', description: 'Absolute path of the project folder.' }, name: { type: 'string', description: 'Wanted app name for the first deploy (becomes name.peume.com). Defaults to the folder name.' } }, required: ['path'] } },
123
+ { name: 'services', description: 'How the deployed app can use Peume services from its server code: Peume Data (hundreds of data tools: scraping, search, maps, social, companies), Peume Routing (every AI model through the OpenAI SDK, no other API key) and payments (Stripe checkout, money goes to the owner). Every Peume app gets PEUME_KEY and PEUME_API environment variables. Read this before adding data or payments to an app deployed on Peume.',
124
+ inputSchema: { type: 'object', properties: {} } },
125
+ { name: 'data_search', description: 'Search Peume Data tools for a need (e.g. "google maps reviews", "linkedin profiles"). Free. Returns provider, endpoint, description and price per call.',
126
+ inputSchema: { type: 'object', properties: { path: { type: 'string', description: 'Absolute path of the deployed project folder.' }, query: { type: 'string' } }, required: ['path', 'query'] } },
127
+ { name: 'data_inspect', description: 'Get the exact input schema (body, queryParams, pathParams) and price of a Peume Data tool. Free. Always inspect before running.',
128
+ inputSchema: { type: 'object', properties: { path: { type: 'string', description: 'Absolute path of the deployed project folder.' }, provider: { type: 'string' }, endpoint: { type: 'string' } }, required: ['path', 'provider', 'endpoint'] } },
129
+ { name: 'data_run', description: 'Run a Peume Data tool once (to test it while building the app) and wait up to 2 minutes for the result. Paid from the app owner\'s Peume credits: keep limits small (maxItems 5-10, one query).',
130
+ inputSchema: { type: 'object', properties: { path: { type: 'string', description: 'Absolute path of the deployed project folder.' }, provider: { type: 'string' }, endpoint: { type: 'string' }, input: { type: 'object', description: '{ body?, queryParams?, pathParams? } as given by data_inspect.' } }, required: ['path', 'provider', 'endpoint'] } },
131
+ { name: 'status', description: 'Status of the Peume app deployed from this folder: URL, state and the latest build logs.',
132
+ inputSchema: { type: 'object', properties: { path: { type: 'string', description: 'Absolute path of the project folder.' } }, required: ['path'] } },
133
+ { name: 'logs', description: 'Runtime logs of the running Peume app deployed from this folder.',
134
+ inputSchema: { type: 'object', properties: { path: { type: 'string', description: 'Absolute path of the project folder.' } }, required: ['path'] } },
135
+ { name: 'set_env', description: 'Set environment variables on the Peume app deployed from this folder, then redeploy it.',
136
+ inputSchema: { type: 'object', properties: { path: { type: 'string', description: 'Absolute path of the project folder.' }, vars: { type: 'object', additionalProperties: { type: 'string' }, description: 'Variables to set, e.g. {"API_KEY": "…"}.' } }, required: ['path', 'vars'] } },
137
+ ];
138
+ async function outil(nom, args = {}) {
139
+ if (nom === 'services') { const r = await fetch(`${PEUME}/docs.md`, { signal: AbortSignal.timeout(15000) }); return r.ok ? r.text() : 'Docs: https://cloud.peume.com/docs'; }
140
+ if (!args.path) throw new Error('path is required.');
141
+ if (nom === 'data_search') return JSON.stringify(await data(args.path, '/search', { query: args.query, limit: 10 }), null, 2);
142
+ if (nom === 'data_inspect') return JSON.stringify(await data(args.path, '/inspect', { provider: args.provider, endpoint: args.endpoint }), null, 2);
143
+ if (nom === 'data_run') return lancerData(args.path, args);
144
+ if (nom === 'deploy') return resume(await deployer(args.path, { nom: args.name }));
145
+ if (nom === 'status') { const { e } = await etat(args.path); return `${e.url} — ${e.statut}${e.deploiement ? ` (last build: ${e.deploiement.statut}, ${e.deploiement.commit})` : ''}\n${resume(e)}`; }
146
+ if (nom === 'logs') return (await logs(args.path)).join('\n') || 'No logs yet.';
147
+ if (nom === 'set_env') { const e = await variables(args.path, args.vars || {}); return `Variables set (${Object.keys(args.vars || {}).join(', ')}). Redeploying ${e.url}.`; }
148
+ throw new Error(`Unknown tool ${nom}.`);
149
+ }
150
+ function mcp() {
151
+ const envoyer = m => process.stdout.write(JSON.stringify({ jsonrpc: '2.0', ...m }) + '\n');
152
+ createInterface({ input: process.stdin }).on('line', async l => {
153
+ let m; try { m = JSON.parse(l); } catch { return; }
154
+ if (m.id === undefined) return; // notifications
155
+ try {
156
+ if (m.method === 'initialize') return envoyer({ id: m.id, result: { protocolVersion: m.params?.protocolVersion || '2025-06-18', capabilities: { tools: {} }, serverInfo: { name: 'peume', version: VERSION } } });
157
+ if (m.method === 'ping') return envoyer({ id: m.id, result: {} });
158
+ if (m.method === 'tools/list') return envoyer({ id: m.id, result: { tools: OUTILS } });
159
+ if (m.method === 'tools/call') {
160
+ try { return envoyer({ id: m.id, result: { content: [{ type: 'text', text: await outil(m.params?.name, m.params?.arguments) }] } }); }
161
+ catch (e) { return envoyer({ id: m.id, result: { content: [{ type: 'text', text: e.message }], isError: true } }); }
162
+ }
163
+ envoyer({ id: m.id, error: { code: -32601, message: `Unknown method ${m.method}` } });
164
+ } catch (e) { envoyer({ id: m.id, error: { code: -32603, message: e.message } }); }
165
+ });
166
+ }
167
+
168
+ // ——— ligne de commande
169
+ const AIDE = `Peume — deploy your app, no account, no GitHub.
170
+
171
+ npx @peume/cli deploy [folder] deploy the folder (default: current) and follow the build
172
+ npx @peume/cli status URL, state and last build
173
+ npx @peume/cli logs runtime logs
174
+ npx @peume/cli env set KEY=value… set variables, then redeploy
175
+ npx @peume/cli mcp MCP server for Claude Code:
176
+ claude mcp add peume -- npx -y @peume/cli mcp
177
+ `;
178
+ const gris = s => process.stdout.isTTY ? `\x1b[2m${s}\x1b[0m` : s;
179
+ async function principal([cmd = 'help', ...args]) {
180
+ if (cmd === 'mcp') return mcp();
181
+ try {
182
+ if (cmd === 'deploy') {
183
+ const e = await deployer(args.find(a => !a.startsWith('-')) || '.', { surEtape: s => console.log(s), surLigne: l => console.log(gris(` ${l}`)) });
184
+ console.log('\n' + resume({ ...e, deploiement: e.deploiement && { ...e.deploiement, logs: e.deploiement.statut === 'live' ? [] : e.deploiement.logs } }));
185
+ process.exitCode = e.deploiement?.statut === 'live' ? 0 : 1;
186
+ } else if (cmd === 'status') console.log(await outil('status', { path: '.' }));
187
+ else if (cmd === 'logs') console.log(await outil('logs', { path: '.' }));
188
+ else if (cmd === 'env' && args[0] === 'set') {
189
+ const vars = Object.fromEntries(args.slice(1).map(a => { const i = a.indexOf('='); return i > 0 ? [a.slice(0, i), a.slice(i + 1)] : null; }).filter(Boolean));
190
+ if (!Object.keys(vars).length) throw new Error('Usage: npx @peume/cli env set KEY=value');
191
+ console.log(await outil('set_env', { path: '.', vars }));
192
+ } else if (cmd === '--version' || cmd === '-v') console.log(VERSION);
193
+ else console.log(AIDE);
194
+ } catch (e) { console.error(e.message); process.exitCode = 1; }
195
+ }
196
+ principal(process.argv.slice(2));