ramwisp 0.1.2 → 0.1.4

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 CHANGED
@@ -1,12 +1,12 @@
1
1
  # ramwisp
2
2
 
3
- Subagentes do Claude Code e do Codex em máquinas próprias na nuvem, com a RAM que precisarem.
4
- Cada subagente roda numa AWS Nitro Enclave efêmera: antes de enviar qualquer coisa, este MCP confere a
5
- atestação do hardware (raiz AWS Nitro Enclaves G1 + PCR0 publicado) e cifra missão, projeto e credencial
6
- só para aquela enclave.
3
+ Claude Code and Codex subagents on their own cloud machines, with the RAM they need.
4
+ Every subagent runs inside an ephemeral AWS Nitro Enclave: before sending anything, this MCP verifies the
5
+ hardware attestation (AWS Nitro Enclaves Root G1 + published PCR0) and encrypts the task, the project and the
6
+ credential for that enclave only.
7
7
 
8
8
  ```bash
9
9
  claude mcp add --scope user ramwisp -- npx -y ramwisp@latest
10
10
  ```
11
11
 
12
- No primeiro uso ele abre o navegador para você entrar na sua conta. Mais em https://ramwisp.duckdns.org
12
+ On first use it opens your browser so you can sign in. More at https://ramwisp.duckdns.org
package/bin/wisp.js CHANGED
@@ -8,38 +8,38 @@ const [cmd, ...rest] = process.argv.slice(2);
8
8
  const flag = (name, d) => { const i = rest.indexOf(`--${name}`); return i >= 0 ? rest[i + 1] : d; };
9
9
  const out = (x) => console.log(JSON.stringify(x, null, 2));
10
10
 
11
- const HELP = `wisp — subagentes com RAM sob demanda (${API})
11
+ const HELP = `ramwisp — subagents with on-demand RAM (${API})
12
12
 
13
- wisp login conecta esta máquina à sua conta (navegador)
14
- wisp logout
15
- wisp spawn "missão" [--ram 2] [--engine claude|codex] [--model M] [--auth auto|login|key] [--workspace DIR] [--wait]
16
- wisp wait ID | result ID | kill ID
17
- wisp ls
18
- wisp setup mostra como adicionar o MCP ao Claude Code / Codex
13
+ ramwisp login connect this machine to your account (browser)
14
+ ramwisp logout
15
+ ramwisp spawn "task" [--ram 2] [--engine claude|codex] [--model M] [--auth auto|login|key] [--workspace DIR] [--wait]
16
+ ramwisp wait ID | result ID | kill ID
17
+ ramwisp ls
18
+ ramwisp setup how to add the MCP to Claude Code / Codex
19
19
 
20
- Sem argumentos roda o servidor MCP (stdio).`;
20
+ With no arguments it runs the MCP server (stdio).`;
21
21
 
22
22
  async function main() {
23
23
  switch (cmd) {
24
24
  case undefined: case "mcp": return serve();
25
25
  case "login": {
26
- if (getToken()) return console.log(`já conectado a ${API} (wisp logout para trocar)`);
26
+ if (getToken()) return console.log(`already connected to ${API} (ramwisp logout to switch)`);
27
27
  const l = await startLogin();
28
- console.log(`${l.opened ? "Abri o navegador. Se não abriu, acesse" : "Abra"}: ${l.verification_uri_complete}\nCódigo: ${l.user_code}`);
28
+ console.log(`${l.opened ? "Opened your browser. If it didn't open, visit" : "Open"}: ${l.verification_uri_complete}\nCode: ${l.user_code}`);
29
29
  await l.done;
30
- return console.log("conectado ✓");
30
+ return console.log("connected ✓");
31
31
  }
32
- case "logout": logout(); return console.log("desconectado");
32
+ case "logout": logout(); return console.log("disconnected");
33
33
  case "spawn": {
34
34
  const r = await spawnAgent({ mission: rest[0], ram_gb: Number(flag("ram", 2)), engine: flag("engine", "claude"),
35
35
  model: flag("model"), auth: flag("auth", "auto"), timeout_s: Number(flag("timeout", 1800)), label: flag("label"), workspace: flag("workspace") });
36
36
  out(r);
37
- console.error("esperando a máquina e a atestação para mandar a missão selada…");
37
+ console.error("waiting for the machine and its attestation to send the sealed task…");
38
38
  await sealed(r.id);
39
39
  const now = await result(r.id);
40
- if (now.erro || ["failed", "killed", "expired"].includes(now.status)) { out(now); process.exit(1); }
40
+ if (now.error || ["failed", "killed", "expired"].includes(now.status)) { out(now); process.exit(1); }
41
41
  if (rest.includes("--wait")) return out(await waitAgent(r.id, 3600));
42
- return console.error(`missão entregue. Recolha com: wisp wait ${r.id}`);
42
+ return console.error(`task delivered. Collect it with: ramwisp wait ${r.id}`);
43
43
  }
44
44
  case "wait": return out(await waitAgent(rest[0], Number(flag("max", 3600))));
45
45
  case "result": return out(await result(rest[0]));
@@ -52,4 +52,4 @@ async function main() {
52
52
  }
53
53
  }
54
54
 
55
- main().catch((e) => { console.error("erro:", e.message); process.exit(1); });
55
+ main().catch((e) => { console.error("error:", e.message); process.exit(1); });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ramwisp",
3
- "version": "0.1.2",
4
- "description": "ramwisp: subagentes do Claude Code e do Codex em máquinas próprias na nuvem (AWS Nitro Enclaves), com a sua assinatura. MCP + CLI.",
3
+ "version": "0.1.4",
4
+ "description": "ramwisp: Claude Code and Codex subagents on their own cloud machines (AWS Nitro Enclaves), with your own subscription. MCP + CLI.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "ramwisp": "bin/wisp.js",
package/src/account.js CHANGED
@@ -36,10 +36,14 @@ export function logout() {
36
36
  if (existsSync(CRED)) rmSync(CRED);
37
37
  }
38
38
 
39
+ let client = "cli";
40
+ /** Quem está usando o MCP (vem do clientInfo do initialize: "claude-code", "codex-mcp-client"…). */
41
+ export function setClient(name) { if (name) client = String(name).slice(0, 60); }
42
+
39
43
  export async function call(method, path, body, { token = getToken(), timeoutMs = 30_000 } = {}) {
40
44
  const r = await fetch(API + path, {
41
45
  method,
42
- headers: { "Content-Type": "application/json", ...(token ? { Authorization: `Bearer ${token}` } : {}) },
46
+ headers: { "Content-Type": "application/json", "X-Wisp-Client": client, ...(token ? { Authorization: `Bearer ${token}` } : {}) },
43
47
  body: body === undefined ? undefined : JSON.stringify(body),
44
48
  signal: AbortSignal.timeout(timeoutMs),
45
49
  });
package/src/attest.js CHANGED
@@ -22,44 +22,44 @@ export function allowedPcr0() {
22
22
  */
23
23
  export function verifyAttestation(docB64, expectedNonce, { devRootPem } = {}) {
24
24
  const doc = decode(Buffer.from(docB64, "base64"));
25
- if (!Array.isArray(doc) || doc.length !== 4) throw new Error("atestação: não é COSE_Sign1");
25
+ if (!Array.isArray(doc) || doc.length !== 4) throw new Error("attestation: not a COSE_Sign1");
26
26
  const [protectedBytes, , payloadBytes, signature] = doc;
27
27
  const prot = decode(protectedBytes);
28
- if (!(prot instanceof Map) || prot.get(1) !== -35) throw new Error("atestação: algoritmo não é ES384");
28
+ if (!(prot instanceof Map) || prot.get(1) !== -35) throw new Error("attestation: algorithm is not ES384");
29
29
  const p = decode(payloadBytes);
30
- const get = (k) => { const v = p.get(k); if (v === undefined || v === null) throw new Error(`atestação: falta ${k}`); return v; };
30
+ const get = (k) => { const v = p.get(k); if (v === undefined || v === null) throw new Error(`attestation: missing ${k}`); return v; };
31
31
 
32
32
  // cadeia: cabundle[0] é a raiz, depois intermediárias, por fim o certificado da enclave
33
33
  const bundle = get("cabundle").map((der) => new X509Certificate(der));
34
34
  const leaf = new X509Certificate(get("certificate"));
35
35
  const dev = !!devRootPem;
36
36
  const root = dev ? new X509Certificate(devRootPem) : NITRO_ROOT;
37
- if (!bundle.length || !bundle[0].raw.equals(root.raw)) throw new Error("atestação: raiz não é a AWS Nitro Enclaves");
37
+ if (!bundle.length || !bundle[0].raw.equals(root.raw)) throw new Error("attestation: root is not AWS Nitro Enclaves");
38
38
  const chain = [...bundle, leaf];
39
39
  const at = new Date();
40
40
  for (let i = 0; i < chain.length; i++) {
41
41
  const cert = chain[i];
42
42
  const issuer = i === 0 ? root : chain[i - 1];
43
- if (!cert.verify(issuer.publicKey)) throw new Error(`atestação: assinatura do certificado ${i} inválida`);
44
- if (at < new Date(cert.validFrom) || at > new Date(cert.validTo)) throw new Error(`atestação: certificado ${i} fora da validade`);
45
- if (i < chain.length - 1 && !cert.ca) throw new Error(`atestação: certificado ${i} não é CA`);
43
+ if (!cert.verify(issuer.publicKey)) throw new Error(`attestation: certificate ${i} signature invalid`);
44
+ if (at < new Date(cert.validFrom) || at > new Date(cert.validTo)) throw new Error(`attestation: certificate ${i} outside its validity`);
45
+ if (i < chain.length - 1 && !cert.ca) throw new Error(`attestation: certificate ${i} is not a CA`);
46
46
  }
47
47
  const ok = verify("sha384", encodeSigStructure(protectedBytes, payloadBytes),
48
48
  { key: leaf.publicKey, dsaEncoding: "ieee-p1363" }, signature);
49
- if (!ok) throw new Error("atestação: assinatura COSE inválida");
49
+ if (!ok) throw new Error("attestation: invalid COSE signature");
50
50
 
51
- if (get("digest") !== "SHA384") throw new Error("atestação: digest inesperado");
51
+ if (get("digest") !== "SHA384") throw new Error("attestation: unexpected digest");
52
52
  const ts = get("timestamp");
53
- if (Math.abs(Date.now() - ts) > MAX_AGE_MS) throw new Error("atestação: velha demais ou do futuro");
54
- if (!Buffer.from(get("nonce")).equals(expectedNonce)) throw new Error("atestação: nonce não confere (repetição?)");
53
+ if (Math.abs(Date.now() - ts) > MAX_AGE_MS) throw new Error("attestation: too old or from the future");
54
+ if (!Buffer.from(get("nonce")).equals(expectedNonce)) throw new Error("attestation: nonce mismatch (replay?)");
55
55
  const ud = p.get("user_data");
56
- if (!ud || Buffer.from(ud).toString() !== "wisp-v1") throw new Error("atestação: protocolo desconhecido");
56
+ if (!ud || Buffer.from(ud).toString() !== "wisp-v1") throw new Error("attestation: unknown protocol");
57
57
  const enclavePub = Buffer.from(get("public_key"));
58
- if (enclavePub.length !== 32) throw new Error("atestação: chave pública inválida");
58
+ if (enclavePub.length !== 32) throw new Error("attestation: invalid public key");
59
59
 
60
60
  const pcr0 = Buffer.from(get("pcrs").get(0)).toString("hex");
61
- if (/^0+$/.test(pcr0)) throw new Error("atestação: enclave em modo debug (PCR0 zerado) — recusado");
61
+ if (/^0+$/.test(pcr0)) throw new Error("attestation: enclave in debug mode (zero PCR0) — refused");
62
62
  const allowed = dev ? [DEV_PCR0] : allowedPcr0();
63
- if (!allowed.includes(pcr0)) throw new Error(`atestação: imagem não reconhecida (PCR0 ${pcr0.slice(0, 16)}…)`);
63
+ if (!allowed.includes(pcr0)) throw new Error(`attestation: unrecognized image (PCR0 ${pcr0.slice(0, 16)}…)`);
64
64
  return { enclavePub, pcr0, moduleId: get("module_id"), timestamp: ts, dev };
65
65
  }
package/src/client.js CHANGED
@@ -21,7 +21,7 @@ const SKIP_DIRS = new Set(["node_modules", ".git", ".venv", "venv", "__pycache__
21
21
  /** Copia do projeto: no git, o que ele rastreia + arquivos novos não ignorados (nunca o que está no .gitignore). */
22
22
  export function packWorkspace(dir) {
23
23
  const root = resolve(dir);
24
- if (!existsSync(root) || !statSync(root).isDirectory()) throw new Error(`workspace não é um diretório: ${root}`);
24
+ if (!existsSync(root) || !statSync(root).isDirectory()) throw new Error(`workspace is not a directory: ${root}`);
25
25
  let files;
26
26
  const git = spawnSync("git", ["-C", root, "ls-files", "-z", "-co", "--exclude-standard"], { maxBuffer: 256 * 1024 * 1024 });
27
27
  if (git.status === 0) {
@@ -41,8 +41,8 @@ export function packWorkspace(dir) {
41
41
  const tar = spawnSync("tar", ["-czf", "-", "-C", root, "--null", "-T", "-"], { input: files.join("\0"), maxBuffer: 256 * 1024 * 1024 });
42
42
  if (tar.status !== 0) throw new Error(`tar falhou: ${tar.stderr.toString().slice(0, 300)}`);
43
43
  if (tar.stdout.length > MAX_WORKSPACE) {
44
- throw new Error(`projeto grande demais (${(tar.stdout.length / 1048576).toFixed(1)} MB compactado, máximo 15 MB): ` +
45
- "aponte workspace para um subdiretório ou ignore arquivos pesados no .gitignore");
44
+ throw new Error(`project too large (${(tar.stdout.length / 1048576).toFixed(1)} MB compressed, max 15 MB): ` +
45
+ "point workspace at a subdirectory or ignore heavy files in .gitignore");
46
46
  }
47
47
  return { root, tgz: tar.stdout, files: files.length };
48
48
  }
@@ -56,7 +56,7 @@ export async function spawnAgent(o) {
56
56
  let note;
57
57
  if (cred.left && cred.left - 300 < timeout) {
58
58
  timeout = Math.max(60, Math.floor(cred.left - 300));
59
- note = `timeout reduzido para ${timeout}s (validade do login local)`;
59
+ note = `timeout lowered to ${timeout}s (local login validity)`;
60
60
  }
61
61
  const ws = o.workspace ? packWorkspace(o.workspace) : null;
62
62
  const priv = newClientKey();
@@ -72,7 +72,7 @@ export async function spawnAgent(o) {
72
72
  p.catch(() => {});
73
73
  return { id: job.id, status: job.status, engine, ram_gb: job.ram_gb, instance_type: job.instance_type,
74
74
  credential: cred.source, reserved_usd: +(job.hold_cents / 100).toFixed(4),
75
- ...(ws ? { workspace: `${ws.root} (${ws.files} arquivos, ${(ws.tgz.length / 1024).toFixed(0)} KB, cifrado)` } : {}),
75
+ ...(ws ? { workspace: `${ws.root} (${ws.files} files, ${(ws.tgz.length / 1024).toFixed(0)} KB, encrypted)` } : {}),
76
76
  ...(note ? { note } : {}) };
77
77
  }
78
78
 
@@ -98,7 +98,7 @@ async function sealWhenReady(id, priv, nonce, payload) {
98
98
  }
99
99
  await new Promise((r) => setTimeout(r, 2000));
100
100
  }
101
- throw new Error("a máquina não ficou pronta em 20 min");
101
+ throw new Error("the machine was not ready within 20 min");
102
102
  }
103
103
 
104
104
  /** Espera a selagem terminar (útil para o CLI, que sai logo depois). */
@@ -130,31 +130,31 @@ function parseOutput(stdout) {
130
130
  export async function result(id) {
131
131
  const j = await call("GET", `/api/jobs/${id}`);
132
132
  const meta = { id, status: j.status, ram_gb: j.ram_gb, peak_mem_mib: j.peak_mem_mib, cost_usd: j.cost_cents != null ? +(j.cost_cents / 100).toFixed(4) : null };
133
- if (sealErrors.has(id)) return { ...meta, status: "failed", erro: `recusado por segurança: ${sealErrors.get(id)}` };
133
+ if (sealErrors.has(id)) return { ...meta, status: "failed", error: `refused for security: ${sealErrors.get(id)}` };
134
134
  if (!FINAL.includes(j.status)) return { ...meta, mem_used_mib: j.mem_used_mib };
135
135
  if (j.status !== "done" || !j.output) {
136
136
  rmSync(keyFile(id), { force: true });
137
- return { ...meta, erro: j.error ?? (j.collected_at ? "resultado já recolhido antes" : j.status) };
137
+ return { ...meta, error: j.error ?? (j.collected_at ? "result was already collected" : j.status) };
138
138
  }
139
139
  const f = keyFile(id);
140
- if (!existsSync(f)) return { ...meta, erro: "a chave para abrir esse resultado não está nesta máquina" };
140
+ if (!existsSync(f)) return { ...meta, error: "the key to open this result is not on this machine" };
141
141
  const k = JSON.parse(readFileSync(f, "utf8"));
142
142
  const out = JSON.parse(openOutput(importKey(k.priv), Buffer.from(k.enclave_pub, "base64"), Buffer.from(k.nonce, "base64"), j.output).toString());
143
143
  await call("POST", `/api/jobs/${id}/collected`).catch(() => {});
144
144
  rmSync(f, { force: true });
145
145
  const parsed = parseOutput(out.stdout ?? "");
146
146
  const res = { ...parsed, ...meta, exit_code: out.exit_code, duration_s: out.duration_s };
147
- if (out.exit_code === 124) res.erro = "timeout";
147
+ if (out.exit_code === 124) res.error = "timeout";
148
148
  if (out.patch) {
149
149
  const f = join(ensureDir("patches"), `${id}.patch`);
150
150
  writeSecret(f, out.patch);
151
151
  res.patch_file = f;
152
152
  res.patch_stat = out.patch_stat;
153
- res.aplicar = k.workspace ? `git -C ${JSON.stringify(k.workspace)} apply ${JSON.stringify(f)}` : `git apply ${JSON.stringify(f)}`;
153
+ res.apply = k.workspace ? `git -C ${JSON.stringify(k.workspace)} apply ${JSON.stringify(f)}` : `git apply ${JSON.stringify(f)}`;
154
154
  } else if (out.patch === "") {
155
- res.patch_stat = "nenhuma mudança no projeto";
155
+ res.patch_stat = "no changes to the project";
156
156
  } else if (out.patch_error) {
157
- res.patch_erro = out.patch_error;
157
+ res.patch_error = out.patch_error;
158
158
  res.patch_stat = out.patch_stat;
159
159
  }
160
160
  if (out.exit_code !== 0 && out.stderr_tail) res.stderr_tail = out.stderr_tail.slice(-1500);
@@ -165,7 +165,7 @@ export async function waitAgent(id, maxWaitS = 900) {
165
165
  const until = Date.now() + maxWaitS * 1000;
166
166
  for (;;) {
167
167
  const r = await result(id);
168
- if (FINAL.includes(r.status) || r.erro || Date.now() > until) return r;
168
+ if (FINAL.includes(r.status) || r.error || Date.now() > until) return r;
169
169
  await new Promise((res) => setTimeout(res, 3000));
170
170
  }
171
171
  }
@@ -179,9 +179,9 @@ export async function killAgent(id) {
179
179
  export async function listAgents() {
180
180
  const [me, jobs] = await Promise.all([call("GET", "/api/me"), call("GET", "/api/jobs?limit=20")]);
181
181
  return {
182
- conta: me.email, saldo_usd: +(me.credit_cents / 100).toFixed(2),
183
- ram_disponivel_gb: me.tiers.filter((t) => t.available).map((t) => t.ram_gb),
184
- agentes: jobs.filter((j) => !FINAL.includes(j.status) || (j.status === "done" && !j.collected_at)).map((j) => ({
182
+ account: me.email, balance_usd: +(me.credit_cents / 100).toFixed(2),
183
+ ram_available_gb: me.tiers.filter((t) => t.available).map((t) => t.ram_gb),
184
+ agents: jobs.filter((j) => !FINAL.includes(j.status) || (j.status === "done" && !j.collected_at)).map((j) => ({
185
185
  id: j.id, status: j.status, engine: j.engine, ram_gb: j.ram_gb, mem_used_mib: j.mem_used_mib, label: j.label })),
186
186
  };
187
187
  }
package/src/creds.js CHANGED
@@ -24,8 +24,8 @@ function claudeLogin() {
24
24
 
25
25
  function claudeCredential(mode, needS) {
26
26
  const key = process.env.WISP_ANTHROPIC_API_KEY || process.env.ANTHROPIC_API_KEY;
27
- if (mode !== "login" && key) return { kind: "anthropic_key", value: key, left: null, source: "chave de API" };
28
- if (mode === "key") throw new Error("modo chave: defina ANTHROPIC_API_KEY (ou WISP_ANTHROPIC_API_KEY) no env do MCP");
27
+ if (mode !== "login" && key) return { kind: "anthropic_key", value: key, left: null, source: "API key" };
28
+ if (mode === "key") throw new Error("key mode: set ANTHROPIC_API_KEY (or WISP_ANTHROPIC_API_KEY) in the MCP env");
29
29
  if (process.env.CLAUDE_CODE_OAUTH_TOKEN) return { kind: "claude_oauth", value: process.env.CLAUDE_CODE_OAUTH_TOKEN, left: null, source: "CLAUDE_CODE_OAUTH_TOKEN" };
30
30
  let l = claudeLogin();
31
31
  if (l && l.exp - Date.now() / 1000 < Math.max(MIN_LEFT_S, needS)) {
@@ -33,30 +33,30 @@ function claudeCredential(mode, needS) {
33
33
  spawnSync("claude", ["-p", "ok", "--max-turns", "1"], { stdio: "ignore", timeout: 120_000 });
34
34
  l = claudeLogin();
35
35
  }
36
- if (!l) throw new Error("sem login do Claude Code nesta máquina: rode `claude` e faça login, ou use uma chave (ANTHROPIC_API_KEY)");
36
+ if (!l) throw new Error("no Claude Code login on this machine: run `claude` and sign in, or use a key (ANTHROPIC_API_KEY)");
37
37
  const left = l.exp - Date.now() / 1000;
38
- if (left < MIN_LEFT_S) throw new Error("o login local do Claude não renovou; abra o Claude Code uma vez e tente de novo");
39
- return { kind: "claude_oauth", value: l.token, left, source: "login do Claude Code" };
38
+ if (left < MIN_LEFT_S) throw new Error("the local Claude login did not refresh; open Claude Code once and try again");
39
+ return { kind: "claude_oauth", value: l.token, left, source: "Claude Code login" };
40
40
  }
41
41
 
42
42
  const jwtExp = (t) => JSON.parse(Buffer.from(t.split(".")[1], "base64url").toString()).exp;
43
43
 
44
44
  function codexCredential(mode) {
45
45
  const key = process.env.WISP_OPENAI_API_KEY || process.env.OPENAI_API_KEY;
46
- if (mode !== "login" && key) return { kind: "openai_key", value: key, left: null, source: "chave de API" };
46
+ if (mode !== "login" && key) return { kind: "openai_key", value: key, left: null, source: "API key" };
47
47
  const path = join(process.env.CODEX_HOME ?? join(homedir(), ".codex"), "auth.json");
48
48
  let d;
49
49
  try { d = JSON.parse(readFileSync(path, "utf8")); } catch {
50
- throw new Error(mode === "key" ? "modo chave: defina OPENAI_API_KEY no env do MCP" : "sem login do Codex nesta máquina: rode `codex login`");
50
+ throw new Error(mode === "key" ? "key mode: set OPENAI_API_KEY in the MCP env" : "no Codex login on this machine: run `codex login`");
51
51
  }
52
- if (mode !== "login" && d.OPENAI_API_KEY) return { kind: "openai_key", value: d.OPENAI_API_KEY, left: null, source: "chave do Codex" };
53
- if (mode === "key") throw new Error("modo chave: defina OPENAI_API_KEY no env do MCP");
52
+ if (mode !== "login" && d.OPENAI_API_KEY) return { kind: "openai_key", value: d.OPENAI_API_KEY, left: null, source: "Codex API key" };
53
+ if (mode === "key") throw new Error("key mode: set OPENAI_API_KEY in the MCP env");
54
54
  const t = d.tokens ?? {};
55
55
  const left = jwtExp(t.access_token) - Date.now() / 1000;
56
- if (left < MIN_LEFT_S) throw new Error("o login do Codex venceu; abra o Codex uma vez nesta máquina");
56
+ if (left < MIN_LEFT_S) throw new Error("the Codex login expired; open Codex once on this machine");
57
57
  const slim = { auth_mode: d.auth_mode ?? "chatgpt", OPENAI_API_KEY: null, last_refresh: new Date().toISOString(),
58
58
  tokens: { id_token: t.id_token, access_token: t.access_token, account_id: t.account_id, refresh_token: "" } };
59
- return { kind: "codex_auth", value: JSON.stringify(slim), left, source: "login do ChatGPT/Codex" };
59
+ return { kind: "codex_auth", value: JSON.stringify(slim), left, source: "ChatGPT/Codex login" };
60
60
  }
61
61
 
62
62
  /** mode: "auto" (chave se houver, senão login), "key" ou "login". */
package/src/mcp.js CHANGED
@@ -1,71 +1,71 @@
1
1
  // Servidor MCP (stdio, JSON-RPC por linha). Sem dependências.
2
2
  import { createInterface } from "node:readline";
3
- import { API, getToken, startLogin } from "./account.js";
3
+ import { API, getToken, setClient, startLogin } from "./account.js";
4
4
  import { LoginRequired, killAgent, listAgents, result, spawnAgent, waitAgent } from "./client.js";
5
5
 
6
6
  const PROTOCOLS = ["2025-06-18", "2025-03-26", "2024-11-05"];
7
- const VERSION = "0.1.2";
7
+ const VERSION = "0.1.4";
8
8
 
9
- const INSTRUCTIONS = `wisp roda subagentes Claude Code ou Codex em máquinas efêmeras na nuvem, com a RAM que você pedir,
10
- sem pesar esta máquina. Cada subagente nasce numa enclave isolada (AWS Nitro): antes de mandar qualquer coisa,
11
- este MCP confere criptograficamente a imagem da enclave e cifra a missão e o login do usuário só para ela.
12
- Nem o operador do wisp consegue ler a missão, a credencial ou o resultado. A máquina é destruída ao terminar.
9
+ const INSTRUCTIONS = `ramwisp runs Claude Code or Codex subagents on ephemeral cloud machines with the RAM you ask for,
10
+ without loading this machine. Each subagent starts inside an isolated enclave (AWS Nitro): before sending anything,
11
+ this MCP cryptographically verifies the enclave image and encrypts the task and the user's login for that enclave only.
12
+ Not even the ramwisp operator can read the task, the credential or the result. The machine is destroyed when it finishes.
13
13
 
14
- Quando usar: o usuário pede subagente remoto / "roda no wisp" / "sobe N subagentes", ou há tarefas
15
- independentes e pesadas (build, testes, pesquisa longa) que podem rodar em paralelo fora daqui.
14
+ When to use: the user asks for remote subagents / "run it on ramwisp" / "spin up N subagents", or there are
15
+ independent, heavy tasks (builds, test suites, long research) that can run in parallel elsewhere.
16
16
 
17
- Como usar bem:
18
- - Paralelo: chame spawn_agent para todas as missões primeiro, depois wait_agent para cada id.
19
- - Para trabalhar no código do usuário, passe workspace (ex.: o diretório do projeto): vai uma cópia cifrada,
20
- o subagente trabalha nela e as mudanças voltam como patch (aplique com o comando em "aplicar" depois de revisar).
21
- Sem workspace a máquina começa vazia: ponha todo o contexto na missão. Tem internet (HTTPS), mas não tem git/SSH do usuário.
22
- - A máquina leva ~2-3 min para subir; wait_agent espera até 15 min por chamada (chame de novo se voltar running).
23
- - Sempre recolha com wait_agent ou agent_result: a resposta só pode ser aberta nesta máquina.
24
- - Cada subagente consome crédito do wisp (máquina) e a assinatura/chave do usuário (modelo). Não dispare dezenas.
25
- - Se a resposta disser que é preciso entrar na conta, mostre o link ao usuário.`;
17
+ How to use it well:
18
+ - Parallel: call spawn_agent for every task first, then wait_agent for each id.
19
+ - To work on the user's code, pass workspace (e.g. the project directory): an encrypted copy is sent,
20
+ the subagent works on it and the changes come back as a patch (apply it with the "apply" command after reviewing).
21
+ Without workspace the machine starts empty: put all context in the mission. It has internet (HTTPS) but no git/SSH access of the user.
22
+ - A machine takes ~1-3 min to start; wait_agent waits up to 15 min per call (call it again if it returns running).
23
+ - Always collect with wait_agent or agent_result: the answer can only be decrypted on this machine.
24
+ - Each subagent uses ramwisp credit (the machine) and the user's subscription/key (the model). Don't launch dozens.
25
+ - If a response says the user needs to sign in, show them the link.`;
26
26
 
27
27
  const TOOLS = [
28
- { name: "spawn_agent", description: "Sobe um subagente efêmero numa máquina com a RAM pedida e devolve o id na hora.",
28
+ { name: "spawn_agent", description: "Launch an ephemeral subagent on a machine with the requested RAM and return its id right away.",
29
29
  inputSchema: { type: "object", required: ["mission"], properties: {
30
- mission: { type: "string", description: "Missão completa e autocontida, com todo o contexto necessário." },
30
+ mission: { type: "string", description: "Complete, self-contained task with all the context it needs." },
31
31
  engine: { type: "string", enum: ["claude", "codex"], default: "claude" },
32
- model: { type: "string", description: "Modelo do motor. Omitir = padrão dele." },
33
- ram_gb: { type: "integer", enum: [2, 4, 8, 16, 24], default: 2, description: "RAM do subagente." },
34
- max_turns: { type: "integer", default: 20, description: "Só vale para claude." },
32
+ model: { type: "string", description: "Model for the engine. Omit for its default." },
33
+ ram_gb: { type: "integer", enum: [2, 4, 8, 16, 24], default: 2, description: "Subagent RAM." },
34
+ max_turns: { type: "integer", default: 20, description: "Claude only." },
35
35
  timeout_s: { type: "integer", default: 1800, minimum: 60, maximum: 7200 },
36
36
  auth: { type: "string", enum: ["auto", "login", "key"], default: "auto",
37
- description: "login = assinatura do usuário nesta máquina; key = chave de API do env; auto = chave se houver." },
38
- workspace: { type: "string", description: "Caminho de um diretório/repositório DESTA máquina para mandar junto. Vai uma cópia cifrada (no git: arquivos rastreados + novos não ignorados; nunca o que está no .gitignore). O subagente trabalha em ~/work e as mudanças voltam como patch (patch_file + comando aplicar). Máx. 15 MB compactado." },
39
- label: { type: "string", description: "Rótulo curto VISÍVEL no painel (não coloque nada sensível)." } } } },
40
- { name: "wait_agent", description: "Espera o subagente terminar e devolve o resultado (campo result = resposta).",
37
+ description: "login = the user's subscription on this machine; key = API key from env; auto = key if present." },
38
+ workspace: { type: "string", description: "Path to a directory/repo ON THIS MACHINE to send along. An encrypted copy is sent (in git: tracked files + new non-ignored files; never anything in .gitignore). The subagent works in ~/work and changes come back as a patch (patch_file + apply command). Max 15 MB compressed." },
39
+ label: { type: "string", description: "Short label VISIBLE in the dashboard (don't put anything sensitive)." } } } },
40
+ { name: "wait_agent", description: "Wait for the subagent to finish and return the result (field result = the answer).",
41
41
  inputSchema: { type: "object", required: ["id"], properties: {
42
42
  id: { type: "string" }, max_wait_s: { type: "integer", default: 900, maximum: 1800 } } } },
43
- { name: "agent_result", description: "Resultado sem esperar: devolve a resposta ou o status atual (running, RAM em uso).",
43
+ { name: "agent_result", description: "Result without waiting: returns the answer or the current status (running, RAM in use).",
44
44
  inputSchema: { type: "object", required: ["id"], properties: { id: { type: "string" } } } },
45
- { name: "kill_agent", description: "Mata o subagente e destrói a máquina na hora.",
45
+ { name: "kill_agent", description: "Kill the subagent and destroy its machine immediately.",
46
46
  inputSchema: { type: "object", required: ["id"], properties: { id: { type: "string" } } } },
47
- { name: "list_agents", description: "Saldo, RAM disponível e subagentes vivos ou com resultado para recolher.",
47
+ { name: "list_agents", description: "Balance, available RAM sizes and subagents that are running or have results to collect.",
48
48
  inputSchema: { type: "object", properties: {} } },
49
- { name: "wisp_login", description: "Conecta este MCP à conta wisp do usuário (abre o navegador).",
49
+ { name: "wisp_login", description: "Connect this MCP to the user's ramwisp account (opens the browser).",
50
50
  inputSchema: { type: "object", properties: {} } },
51
51
  ];
52
52
 
53
53
  async function loginMessage() {
54
54
  const l = await startLogin();
55
- // com navegador aberto aqui, espera um pouco pela aprovação e segue sozinho
55
+ // with a browser open here, wait a bit for approval and continue on its own
56
56
  if (l.opened) {
57
57
  const ok = await Promise.race([l.done.then(() => true).catch(() => false), new Promise((r) => setTimeout(() => r(false), 120_000))]);
58
58
  if (ok) return null;
59
59
  }
60
- return `Para usar o wisp, conecte sua conta: abra ${l.verification_uri_complete} e confirme o código ${l.user_code}.\n` +
61
- `Não tem conta? Crie lá mesmo, com crédito grátis. Depois repita o pedido.`;
60
+ return `To use ramwisp, connect your account: open ${l.verification_uri_complete} and confirm the code ${l.user_code}.\n` +
61
+ `No account yet? Create one there, with free credit. Then repeat the request.`;
62
62
  }
63
63
 
64
64
  async function runTool(name, a) {
65
65
  if (name === "wisp_login") {
66
- if (getToken()) return { ok: true, msg: `já conectado a ${API}` };
66
+ if (getToken()) return { ok: true, msg: `already connected to ${API}` };
67
67
  const msg = await loginMessage();
68
- return msg ? { login_necessario: msg } : { ok: true, msg: "conta conectada" };
68
+ return msg ? { login_required: msg } : { ok: true, msg: "account connected" };
69
69
  }
70
70
  const attempt = () => {
71
71
  switch (name) {
@@ -74,7 +74,7 @@ async function runTool(name, a) {
74
74
  case "agent_result": return result(a.id);
75
75
  case "kill_agent": return killAgent(a.id);
76
76
  case "list_agents": return listAgents();
77
- default: throw new Error(`ferramenta desconhecida: ${name}`);
77
+ default: throw new Error(`unknown tool: ${name}`);
78
78
  }
79
79
  };
80
80
  try {
@@ -83,7 +83,7 @@ async function runTool(name, a) {
83
83
  } catch (e) {
84
84
  if (!(e instanceof LoginRequired) && e.status !== 401) throw e;
85
85
  const msg = await loginMessage();
86
- if (msg) return { login_necessario: msg };
86
+ if (msg) return { login_required: msg };
87
87
  return await attempt();
88
88
  }
89
89
  }
@@ -99,6 +99,7 @@ export function serve() {
99
99
  try {
100
100
  let res;
101
101
  if (method === "initialize") {
102
+ setClient(params?.clientInfo?.name ?? "mcp");
102
103
  const v = PROTOCOLS.includes(params?.protocolVersion) ? params.protocolVersion : PROTOCOLS[0];
103
104
  res = { protocolVersion: v, capabilities: { tools: {} }, serverInfo: { name: "ramwisp", version: VERSION }, instructions: INSTRUCTIONS };
104
105
  } else if (method === "tools/list") {
@@ -110,10 +111,10 @@ export function serve() {
110
111
  const out = await runTool(params.name, params.arguments ?? {});
111
112
  res = { content: [{ type: "text", text: typeof out === "string" ? out : JSON.stringify(out, null, 1) }] };
112
113
  } catch (e) {
113
- res = { content: [{ type: "text", text: `erro: ${e.message}` }], isError: true };
114
+ res = { content: [{ type: "text", text: `error: ${e.message}` }], isError: true };
114
115
  }
115
116
  } else {
116
- return send({ jsonrpc: "2.0", id, error: { code: -32601, message: `método desconhecido: ${method}` } });
117
+ return send({ jsonrpc: "2.0", id, error: { code: -32601, message: `unknown method: ${method}` } });
117
118
  }
118
119
  send({ jsonrpc: "2.0", id, result: res });
119
120
  } catch (e) {