@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.
- package/README.md +35 -2
- package/package.json +24 -4
- package/peume.mjs +196 -0
package/README.md
CHANGED
|
@@ -1,3 +1,36 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @peume/cli
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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));
|