@clawnch/clawtomaton 0.2.0 → 0.2.2

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.
@@ -1,23 +1,30 @@
1
1
  /**
2
- * Skill: conway — Conway Terminal integration for sandboxes, domains, and compute.
2
+ * Skill: conway — Full Conway Terminal integration.
3
3
  *
4
- * Conway Terminal provides:
5
- * - Cloud sandboxes (Linux VMs via Firecracker microVMs)
6
- * - Domain registration and DNS management
7
- * - x402 payment protocol for domain purchases
4
+ * Covers all Conway Terminal v2.0.9 MCP tools:
5
+ * - Sandbox lifecycle (create, list, get, delete)
6
+ * - Sandbox execution (exec, write_file, read_file)
7
+ * - Sandbox networking (expose_port, list_ports, remove_port, get_url)
8
+ * - Sandbox custom domains (add_domain, list_domains, remove_domain)
9
+ * - Sandbox monitoring (terminal_session, metrics, activity, commands)
10
+ * - PTY sessions (create, write, read, close, resize, list)
11
+ * - Wallet & x402 payments (wallet_info, wallet_networks, x402_discover, x402_check, x402_fetch)
12
+ * - Domain registration & DNS (search, list, info, register, renew, dns_*, pricing, check, privacy, nameservers)
13
+ * - Credits & billing (balance, history, pricing, topup)
14
+ * - Inference (chat_completions)
8
15
  *
9
16
  * Authentication:
10
- * - Sandbox/Cloud API: CONWAY_API_KEY (from ~/.conway/config.json or env)
11
- * - Domain API: SIWE via conway-terminal wallet (~/. conway/wallet.json)
17
+ * - Sandbox/Cloud/Credits/Inference API: CONWAY_API_KEY (from ~/.conway/config.json or env)
18
+ * - Domain API: SIWE via conway-terminal wallet (~/.conway/wallet.json)
19
+ * - x402 payments: via conway-terminal wallet + x402 protocol
12
20
  *
13
21
  * This is optional infrastructure — agents don't need it to function,
14
- * but it lets them build real web presence and run compute.
15
- */
16
- const CONWAY_API_URL = 'https://api.conway.tech';
17
- const CONWAY_DOMAIN_API_URL = 'https://api.conway.domains';
18
- /**
19
- * Make an authenticated request to the Conway Cloud/Sandbox API.
22
+ * but it lets them build real web presence, run compute, and pay for services.
20
23
  */
24
+ const CONWAY_API_URL = process.env.CONWAY_API_URL || 'https://api.conway.tech';
25
+ const CONWAY_DOMAIN_API_URL = process.env.CONWAY_DOMAIN_API_URL || 'https://api.conway.domains';
26
+ const CONWAY_PREVIEW_DOMAIN = process.env.CONWAY_PREVIEW_DOMAIN || 'life.conway.tech';
27
+ // ─── Conway Cloud/Sandbox API ────────────────────────────────────────────────
21
28
  async function conwayApiRequest(apiKey, method, path, body) {
22
29
  const response = await fetch(`${CONWAY_API_URL}${path}`, {
23
30
  method,
@@ -33,12 +40,17 @@ async function conwayApiRequest(apiKey, method, path, body) {
33
40
  }
34
41
  return response.json();
35
42
  }
43
+ // ─── Conway Domain API (SIWE auth) ──────────────────────────────────────────
44
+ let domainJwt = null;
45
+ let domainJwtExpiry = 0;
36
46
  /**
37
- * Authenticate with the Conway Domain API via SIWE and make a request.
38
- * Requires conway-terminal's wallet functions for SIWE signing.
47
+ * Get a JWT for the Conway Domain API via SIWE auth.
48
+ * Caches the token for 50 minutes (tokens expire at 60).
39
49
  */
40
- async function conwayDomainRequest(method, path, body) {
41
- // Domain API requires SIWE auth via conway-terminal wallet
50
+ async function getDomainJwt() {
51
+ if (domainJwt && Date.now() < domainJwtExpiry) {
52
+ return domainJwt;
53
+ }
42
54
  let conway;
43
55
  try {
44
56
  // @ts-ignore — conway-terminal is an optional dependency
@@ -48,14 +60,13 @@ async function conwayDomainRequest(method, path, body) {
48
60
  throw new Error('conway-terminal package not installed. Run: npm install conway-terminal\n' +
49
61
  'Then run: npx conway-terminal --init to create wallet + API key');
50
62
  }
51
- // Get or create Conway wallet
52
63
  const { account } = await conway.getWallet();
53
- // SIWE auth flow: nonce → sign → verify → JWT
64
+ // 1. Get nonce
54
65
  const nonceResp = await fetch(`${CONWAY_DOMAIN_API_URL}/auth/nonce`, { method: 'POST' });
55
66
  if (!nonceResp.ok)
56
67
  throw new Error(`Domain auth: failed to get nonce (${nonceResp.status})`);
57
68
  const { nonce } = (await nonceResp.json());
58
- // Dynamic import siwe (conway-terminal bundles it)
69
+ // 2. Sign SIWE message
59
70
  // @ts-expect-error — siwe is a transitive dependency of conway-terminal
60
71
  const { SiweMessage } = await import('siwe');
61
72
  const url = new URL(CONWAY_DOMAIN_API_URL);
@@ -71,6 +82,7 @@ async function conwayDomainRequest(method, path, body) {
71
82
  });
72
83
  const messageString = siweMessage.prepareMessage();
73
84
  const signature = await account.signMessage({ message: messageString });
85
+ // 3. Verify → get JWT
74
86
  const verifyResp = await fetch(`${CONWAY_DOMAIN_API_URL}/auth/verify`, {
75
87
  method: 'POST',
76
88
  headers: { 'Content-Type': 'application/json' },
@@ -81,11 +93,16 @@ async function conwayDomainRequest(method, path, body) {
81
93
  const { access_token } = (await verifyResp.json());
82
94
  if (!access_token)
83
95
  throw new Error('Domain auth: no access token returned');
84
- // Make the actual domain API request
96
+ domainJwt = access_token;
97
+ domainJwtExpiry = Date.now() + 50 * 60 * 1000;
98
+ return access_token;
99
+ }
100
+ async function conwayDomainRequest(method, path, body) {
101
+ const jwt = await getDomainJwt();
85
102
  const response = await fetch(`${CONWAY_DOMAIN_API_URL}${path}`, {
86
103
  method,
87
104
  headers: {
88
- Authorization: `Bearer ${access_token}`,
105
+ Authorization: `Bearer ${jwt}`,
89
106
  'Content-Type': 'application/json',
90
107
  },
91
108
  body: body ? JSON.stringify(body) : undefined,
@@ -96,35 +113,154 @@ async function conwayDomainRequest(method, path, body) {
96
113
  }
97
114
  return response.json();
98
115
  }
116
+ // ─── x402 helpers ────────────────────────────────────────────────────────────
117
+ async function getConwayModule() {
118
+ try {
119
+ // @ts-ignore — conway-terminal is an optional dependency
120
+ return await import('conway-terminal');
121
+ }
122
+ catch {
123
+ throw new Error('conway-terminal package not installed. Run: npm install conway-terminal\n' +
124
+ 'Then run: npx conway-terminal --init to create wallet + API key');
125
+ }
126
+ }
127
+ async function x402DomainFetch(path, method, body) {
128
+ const conway = await getConwayModule();
129
+ const { account } = await conway.getWallet();
130
+ const jwt = await getDomainJwt();
131
+ const url = `${CONWAY_DOMAIN_API_URL}${path}`;
132
+ const response = await conway.x402Fetch(account, url, {
133
+ method,
134
+ headers: {
135
+ Authorization: `Bearer ${jwt}`,
136
+ 'Content-Type': 'application/json',
137
+ },
138
+ body: body ? JSON.stringify(body) : undefined,
139
+ });
140
+ const data = await response.json();
141
+ return data;
142
+ }
143
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
144
+ function json(data) {
145
+ return JSON.stringify(data, null, 2);
146
+ }
147
+ function ok(data) {
148
+ return { callId: '', success: true, result: json(data) };
149
+ }
150
+ function fail(error) {
151
+ return { callId: '', success: false, result: null, error };
152
+ }
153
+ function requireApiKey() {
154
+ const apiKey = process.env.CONWAY_API_KEY;
155
+ if (!apiKey) {
156
+ throw new Error('CONWAY_API_KEY not set. Get one at https://app.conway.tech or run: npx conway-terminal --provision');
157
+ }
158
+ return apiKey;
159
+ }
160
+ function requireParam(params, name) {
161
+ const val = params[name];
162
+ if (!val && val !== 0)
163
+ throw new Error(`Required parameter "${name}" is missing`);
164
+ return String(val);
165
+ }
166
+ // ─── All actions by category ─────────────────────────────────────────────────
167
+ const ALL_ACTIONS = [
168
+ // Sandbox lifecycle
169
+ 'sandbox_create', 'sandbox_list', 'sandbox_get', 'sandbox_delete',
170
+ // Sandbox execution
171
+ 'sandbox_exec', 'sandbox_write_file', 'sandbox_read_file',
172
+ // Sandbox networking
173
+ 'sandbox_expose_port', 'sandbox_list_ports', 'sandbox_remove_port', 'sandbox_get_url',
174
+ // Sandbox custom domains
175
+ 'sandbox_add_domain', 'sandbox_list_domains', 'sandbox_remove_domain',
176
+ // Sandbox monitoring
177
+ 'sandbox_terminal_session', 'sandbox_metrics', 'sandbox_activity', 'sandbox_commands',
178
+ // PTY sessions
179
+ 'sandbox_pty_create', 'sandbox_pty_write', 'sandbox_pty_read',
180
+ 'sandbox_pty_close', 'sandbox_pty_resize', 'sandbox_pty_list',
181
+ // Wallet & x402
182
+ 'wallet_info', 'wallet_networks', 'x402_discover', 'x402_check', 'x402_fetch',
183
+ // Domains
184
+ 'domain_search', 'domain_list', 'domain_info', 'domain_register', 'domain_renew',
185
+ 'domain_dns_list', 'domain_dns_add', 'domain_dns_update', 'domain_dns_delete',
186
+ 'domain_pricing', 'domain_check', 'domain_privacy', 'domain_nameservers',
187
+ // Credits
188
+ 'credits_balance', 'credits_history', 'credits_pricing', 'credits_topup',
189
+ // Inference
190
+ 'chat_completions',
191
+ ];
192
+ // ─── Skill definition ────────────────────────────────────────────────────────
99
193
  export const conwaySkill = {
100
194
  name: 'conway',
101
- description: 'Conway Terminal: manage cloud sandboxes (Linux VMs), search/register domains, manage DNS. ' +
102
- 'Sandbox actions need CONWAY_API_KEY. Domain actions need conway-terminal package installed.',
195
+ description: 'Conway Terminal: full cloud infrastructure, compute, wallet, payments, domains, and deployment. ' +
196
+ 'Sandbox actions (sandbox_*) need CONWAY_API_KEY. Domain actions (domain_*) need conway-terminal installed. ' +
197
+ 'Wallet/x402 actions need conway-terminal installed. Credits actions need CONWAY_API_KEY. ' +
198
+ `Actions: ${ALL_ACTIONS.join(', ')}.`,
103
199
  parameters: [
104
200
  {
105
201
  name: 'action',
106
202
  type: 'string',
107
- description: 'Action: "sandbox_create", "sandbox_list", "sandbox_exec", "sandbox_delete", ' +
108
- '"domain_search", "domain_register", "domain_list", "domain_info", ' +
109
- '"domain_dns_list", "domain_dns_add", "domain_check", "credits_balance".',
203
+ description: `Action to perform. One of: ${ALL_ACTIONS.join(', ')}.`,
110
204
  required: true,
111
205
  },
112
206
  {
113
207
  name: 'sandbox_id',
114
208
  type: 'string',
115
- description: 'Sandbox ID (32-char hex). Required for sandbox_exec, sandbox_delete.',
209
+ description: 'Sandbox ID (32-char hex). Required for most sandbox_* and sandbox_pty_* actions.',
210
+ required: false,
211
+ },
212
+ {
213
+ name: 'session_id',
214
+ type: 'string',
215
+ description: 'PTY session ID. Required for sandbox_pty_write, sandbox_pty_read, sandbox_pty_close, sandbox_pty_resize.',
116
216
  required: false,
117
217
  },
118
218
  {
119
219
  name: 'command',
120
220
  type: 'string',
121
- description: 'Shell command (for sandbox_exec).',
221
+ description: 'Shell command (for sandbox_exec) or PTY command (for sandbox_pty_create, e.g. "bash", "python3").',
222
+ required: false,
223
+ },
224
+ {
225
+ name: 'path',
226
+ type: 'string',
227
+ description: 'Absolute file path in sandbox (for sandbox_write_file, sandbox_read_file).',
228
+ required: false,
229
+ },
230
+ {
231
+ name: 'content',
232
+ type: 'string',
233
+ description: 'File content (for sandbox_write_file). Text or base64-encoded binary.',
234
+ required: false,
235
+ },
236
+ {
237
+ name: 'encoding',
238
+ type: 'string',
239
+ description: 'Content encoding for sandbox_write_file: omit for text, "base64" for binary.',
240
+ required: false,
241
+ },
242
+ {
243
+ name: 'port',
244
+ type: 'string',
245
+ description: 'Port number (for sandbox_expose_port, sandbox_remove_port, sandbox_get_url).',
246
+ required: false,
247
+ },
248
+ {
249
+ name: 'visibility',
250
+ type: 'string',
251
+ description: 'Port visibility for sandbox_expose_port: "public" or "private" (default: "public").',
252
+ required: false,
253
+ },
254
+ {
255
+ name: 'subdomain',
256
+ type: 'string',
257
+ description: 'Custom subdomain for sandbox_expose_port (e.g. "my-app" -> my-app.life.conway.tech).',
122
258
  required: false,
123
259
  },
124
260
  {
125
261
  name: 'domain',
126
262
  type: 'string',
127
- description: 'Domain name (for domain actions).',
263
+ description: 'Domain name (for domain_* actions and sandbox_add_domain/sandbox_remove_domain).',
128
264
  required: false,
129
265
  },
130
266
  {
@@ -133,140 +269,551 @@ export const conwaySkill = {
133
269
  description: 'Search keyword (for domain_search).',
134
270
  required: false,
135
271
  },
272
+ {
273
+ name: 'tlds',
274
+ type: 'string',
275
+ description: 'Comma-separated TLDs for domain_search or domain_pricing (e.g. "com,io,ai").',
276
+ required: false,
277
+ },
278
+ {
279
+ name: 'domains',
280
+ type: 'string',
281
+ description: 'Comma-separated domain names for domain_check (e.g. "example.com,test.io").',
282
+ required: false,
283
+ },
284
+ {
285
+ name: 'years',
286
+ type: 'string',
287
+ description: 'Registration/renewal period in years (1-10) for domain_register, domain_renew.',
288
+ required: false,
289
+ },
290
+ {
291
+ name: 'privacy',
292
+ type: 'string',
293
+ description: 'WHOIS privacy: "true" or "false" (for domain_register, domain_privacy).',
294
+ required: false,
295
+ },
296
+ {
297
+ name: 'nameservers',
298
+ type: 'string',
299
+ description: 'Comma-separated nameservers for domain_nameservers (e.g. "ns1.example.com,ns2.example.com").',
300
+ required: false,
301
+ },
302
+ {
303
+ name: 'record_id',
304
+ type: 'string',
305
+ description: 'DNS record ID (for domain_dns_update, domain_dns_delete).',
306
+ required: false,
307
+ },
136
308
  {
137
309
  name: 'spec',
138
310
  type: 'string',
139
- description: 'JSON config for sandbox_create: {"name":"my-vm","vcpu":1,"memory_mb":512,"disk_gb":5,"region":"us-east"}. Or DNS record JSON for domain_dns_add: {"type":"A","name":"@","value":"1.2.3.4","ttl":3600}.',
311
+ description: 'JSON config. For sandbox_create: {"name":"my-vm","vcpu":1,"memory_mb":512,"disk_gb":5,"region":"us-east"}. ' +
312
+ 'For domain_dns_add: {"type":"A","host":"@","value":"1.2.3.4","ttl":3600}. ' +
313
+ 'For domain_dns_update: {"host":"www","value":"1.2.3.4","ttl":3600}. ' +
314
+ 'For chat_completions: {"model":"gpt-4o","messages":[{"role":"user","content":"Hello"}],"temperature":1,"max_tokens":1000}.',
315
+ required: false,
316
+ },
317
+ {
318
+ name: 'url',
319
+ type: 'string',
320
+ description: 'URL (for x402_discover, x402_check, x402_fetch).',
321
+ required: false,
322
+ },
323
+ {
324
+ name: 'method',
325
+ type: 'string',
326
+ description: 'HTTP method for x402_fetch (default: "GET").',
327
+ required: false,
328
+ },
329
+ {
330
+ name: 'headers',
331
+ type: 'string',
332
+ description: 'JSON headers for x402_fetch (e.g. {"X-Custom":"value"}).',
333
+ required: false,
334
+ },
335
+ {
336
+ name: 'body',
337
+ type: 'string',
338
+ description: 'Request body for x402_fetch (POST/PUT/PATCH).',
339
+ required: false,
340
+ },
341
+ {
342
+ name: 'network',
343
+ type: 'string',
344
+ description: 'Network ID for wallet_info: "eip155:8453" (Base) or "eip155:84532" (Base Sepolia). Default: "eip155:8453".',
345
+ required: false,
346
+ },
347
+ {
348
+ name: 'amount_usd',
349
+ type: 'string',
350
+ description: 'Credit top-up amount in USD tier units for credits_topup. Valid tiers: 5, 25, 100, 500, 1000, 2500.',
351
+ required: false,
352
+ },
353
+ {
354
+ name: 'address',
355
+ type: 'string',
356
+ description: 'Wallet address to credit for credits_topup. Defaults to local wallet.',
357
+ required: false,
358
+ },
359
+ {
360
+ name: 'limit',
361
+ type: 'string',
362
+ description: 'Max entries to return (for credits_history, sandbox_activity, sandbox_commands).',
363
+ required: false,
364
+ },
365
+ {
366
+ name: 'offset',
367
+ type: 'string',
368
+ description: 'Pagination offset (for credits_history).',
369
+ required: false,
370
+ },
371
+ {
372
+ name: 'timeout',
373
+ type: 'string',
374
+ description: 'Timeout in seconds for sandbox_exec (default: 30).',
375
+ required: false,
376
+ },
377
+ {
378
+ name: 'cols',
379
+ type: 'string',
380
+ description: 'Terminal columns for sandbox_pty_create or sandbox_pty_resize (default: 80).',
381
+ required: false,
382
+ },
383
+ {
384
+ name: 'rows',
385
+ type: 'string',
386
+ description: 'Terminal rows for sandbox_pty_create or sandbox_pty_resize (default: 24).',
387
+ required: false,
388
+ },
389
+ {
390
+ name: 'input',
391
+ type: 'string',
392
+ description: 'Input to send to PTY session (for sandbox_pty_write). Use "\\n" for Enter.',
393
+ required: false,
394
+ },
395
+ {
396
+ name: 'full',
397
+ type: 'string',
398
+ description: 'If "true", return full scrollback buffer for sandbox_pty_read.',
140
399
  required: false,
141
400
  },
142
401
  ],
143
- execute: async (params, _ctx) => {
402
+ execute: async (params) => {
144
403
  try {
145
404
  const action = params.action;
146
- // --- Sandbox actions (use CONWAY_API_KEY) ---
147
- if (action.startsWith('sandbox_') || action === 'credits_balance') {
148
- const apiKey = process.env.CONWAY_API_KEY;
149
- if (!apiKey) {
150
- return {
151
- callId: '',
152
- success: false,
153
- result: null,
154
- error: 'CONWAY_API_KEY not set. Get one at https://app.conway.tech or run: npx conway-terminal --init',
155
- };
405
+ // ─── Sandbox lifecycle ──────────────────────────────────────────
406
+ if (action === 'sandbox_create') {
407
+ const apiKey = requireApiKey();
408
+ const spec = params.spec ? JSON.parse(params.spec) : {};
409
+ const result = await conwayApiRequest(apiKey, 'POST', '/v1/sandboxes', {
410
+ name: spec.name || `sandbox-${Date.now()}`,
411
+ vcpu: spec.vcpu ?? 1,
412
+ memory_mb: spec.memory_mb ?? 512,
413
+ disk_gb: spec.disk_gb ?? 5,
414
+ region: spec.region ?? 'eu-north',
415
+ });
416
+ return ok(result);
417
+ }
418
+ if (action === 'sandbox_list') {
419
+ const apiKey = requireApiKey();
420
+ const result = await conwayApiRequest(apiKey, 'GET', '/v1/sandboxes');
421
+ return ok(result);
422
+ }
423
+ if (action === 'sandbox_get') {
424
+ const apiKey = requireApiKey();
425
+ const sandboxId = requireParam(params, 'sandbox_id');
426
+ const result = await conwayApiRequest(apiKey, 'GET', `/v1/sandboxes/${sandboxId}`);
427
+ return ok(result);
428
+ }
429
+ if (action === 'sandbox_delete') {
430
+ const apiKey = requireApiKey();
431
+ const sandboxId = requireParam(params, 'sandbox_id');
432
+ await conwayApiRequest(apiKey, 'DELETE', `/v1/sandboxes/${sandboxId}`);
433
+ return ok({ success: true, message: `Sandbox ${sandboxId} deleted` });
434
+ }
435
+ // ─── Sandbox execution ──────────────────────────────────────────
436
+ if (action === 'sandbox_exec') {
437
+ const apiKey = requireApiKey();
438
+ const sandboxId = requireParam(params, 'sandbox_id');
439
+ const command = requireParam(params, 'command');
440
+ const timeout = params.timeout ? parseInt(params.timeout) : 30;
441
+ const result = await conwayApiRequest(apiKey, 'POST', `/v1/sandboxes/${sandboxId}/exec`, {
442
+ command,
443
+ timeout,
444
+ });
445
+ return ok(result);
446
+ }
447
+ if (action === 'sandbox_write_file') {
448
+ const apiKey = requireApiKey();
449
+ const sandboxId = requireParam(params, 'sandbox_id');
450
+ const filePath = requireParam(params, 'path');
451
+ const content = requireParam(params, 'content');
452
+ const encoding = params.encoding;
453
+ const MAX_UPLOAD = 10 * 1024 * 1024; // 10MB
454
+ const contentBytes = encoding === 'base64'
455
+ ? Math.ceil(content.length * 3 / 4)
456
+ : new TextEncoder().encode(content).length;
457
+ if (contentBytes > MAX_UPLOAD) {
458
+ return fail(`Content too large: ${contentBytes} bytes exceeds ${MAX_UPLOAD} byte limit`);
459
+ }
460
+ const body = { path: filePath, content };
461
+ if (encoding)
462
+ body.encoding = encoding;
463
+ const result = await conwayApiRequest(apiKey, 'POST', `/v1/sandboxes/${sandboxId}/files/upload/json`, body);
464
+ return ok(result);
465
+ }
466
+ if (action === 'sandbox_read_file') {
467
+ const apiKey = requireApiKey();
468
+ const sandboxId = requireParam(params, 'sandbox_id');
469
+ const filePath = requireParam(params, 'path');
470
+ const result = (await conwayApiRequest(apiKey, 'POST', `/v1/sandboxes/${sandboxId}/exec`, {
471
+ command: `cat ${filePath}`,
472
+ timeout: 10,
473
+ }));
474
+ if (result.exit_code !== 0) {
475
+ return fail(`Failed to read file: ${result.stderr}`);
476
+ }
477
+ return ok({ content: result.stdout, path: filePath });
478
+ }
479
+ // ─── Sandbox networking ─────────────────────────────────────────
480
+ if (action === 'sandbox_expose_port') {
481
+ const apiKey = requireApiKey();
482
+ const sandboxId = requireParam(params, 'sandbox_id');
483
+ const port = parseInt(requireParam(params, 'port'));
484
+ const body = {
485
+ port,
486
+ visibility: params.visibility || 'public',
487
+ };
488
+ if (params.subdomain)
489
+ body.subdomain = params.subdomain;
490
+ const result = await conwayApiRequest(apiKey, 'POST', `/v1/sandboxes/${sandboxId}/ports/expose`, body);
491
+ return ok(result);
492
+ }
493
+ if (action === 'sandbox_list_ports') {
494
+ const apiKey = requireApiKey();
495
+ const sandboxId = requireParam(params, 'sandbox_id');
496
+ const result = await conwayApiRequest(apiKey, 'GET', `/v1/sandboxes/${sandboxId}/ports`);
497
+ return ok(result);
498
+ }
499
+ if (action === 'sandbox_remove_port') {
500
+ const apiKey = requireApiKey();
501
+ const sandboxId = requireParam(params, 'sandbox_id');
502
+ const port = requireParam(params, 'port');
503
+ const result = await conwayApiRequest(apiKey, 'DELETE', `/v1/sandboxes/${sandboxId}/ports/${port}`);
504
+ return ok(result);
505
+ }
506
+ if (action === 'sandbox_get_url') {
507
+ const sandboxId = requireParam(params, 'sandbox_id');
508
+ const port = requireParam(params, 'port');
509
+ const url = `https://${port}-${sandboxId}.${CONWAY_PREVIEW_DOMAIN}`;
510
+ return ok({ url });
511
+ }
512
+ // ─── Sandbox custom domains ─────────────────────────────────────
513
+ if (action === 'sandbox_add_domain') {
514
+ const apiKey = requireApiKey();
515
+ const sandboxId = requireParam(params, 'sandbox_id');
516
+ const domain = requireParam(params, 'domain');
517
+ const result = await conwayApiRequest(apiKey, 'POST', `/v1/sandboxes/${sandboxId}/domains`, { domain });
518
+ return ok(result);
519
+ }
520
+ if (action === 'sandbox_list_domains') {
521
+ const apiKey = requireApiKey();
522
+ const sandboxId = requireParam(params, 'sandbox_id');
523
+ const result = await conwayApiRequest(apiKey, 'GET', `/v1/sandboxes/${sandboxId}/domains`);
524
+ return ok(result);
525
+ }
526
+ if (action === 'sandbox_remove_domain') {
527
+ const apiKey = requireApiKey();
528
+ const sandboxId = requireParam(params, 'sandbox_id');
529
+ const domain = requireParam(params, 'domain');
530
+ const result = await conwayApiRequest(apiKey, 'DELETE', `/v1/sandboxes/${sandboxId}/domains/${encodeURIComponent(domain)}`);
531
+ return ok(result);
532
+ }
533
+ // ─── Sandbox monitoring ─────────────────────────────────────────
534
+ if (action === 'sandbox_terminal_session') {
535
+ const apiKey = requireApiKey();
536
+ const sandboxId = requireParam(params, 'sandbox_id');
537
+ const result = await conwayApiRequest(apiKey, 'POST', `/v1/sandboxes/${sandboxId}/terminal-session`);
538
+ return ok(result);
539
+ }
540
+ if (action === 'sandbox_metrics') {
541
+ const apiKey = requireApiKey();
542
+ const sandboxId = requireParam(params, 'sandbox_id');
543
+ const result = await conwayApiRequest(apiKey, 'GET', `/v1/sandboxes/${sandboxId}/metrics`);
544
+ return ok(result);
545
+ }
546
+ if (action === 'sandbox_activity') {
547
+ const apiKey = requireApiKey();
548
+ const sandboxId = requireParam(params, 'sandbox_id');
549
+ const limit = params.limit ? parseInt(params.limit) : 50;
550
+ const result = await conwayApiRequest(apiKey, 'GET', `/v1/sandboxes/${sandboxId}/activity?limit=${limit}`);
551
+ return ok(result);
552
+ }
553
+ if (action === 'sandbox_commands') {
554
+ const apiKey = requireApiKey();
555
+ const sandboxId = requireParam(params, 'sandbox_id');
556
+ const limit = params.limit ? parseInt(params.limit) : 50;
557
+ const result = await conwayApiRequest(apiKey, 'GET', `/v1/sandboxes/${sandboxId}/commands?limit=${limit}`);
558
+ return ok(result);
559
+ }
560
+ // ─── PTY sessions ──────────────────────────────────────────────
561
+ if (action === 'sandbox_pty_create') {
562
+ const apiKey = requireApiKey();
563
+ const sandboxId = requireParam(params, 'sandbox_id');
564
+ const command = requireParam(params, 'command');
565
+ const cols = params.cols ? parseInt(params.cols) : 80;
566
+ const rows = params.rows ? parseInt(params.rows) : 24;
567
+ const result = await conwayApiRequest(apiKey, 'POST', `/v1/sandboxes/${sandboxId}/pty`, {
568
+ command,
569
+ cols,
570
+ rows,
571
+ });
572
+ return ok(result);
573
+ }
574
+ if (action === 'sandbox_pty_write') {
575
+ const apiKey = requireApiKey();
576
+ const sandboxId = requireParam(params, 'sandbox_id');
577
+ const sessionId = requireParam(params, 'session_id');
578
+ const input = requireParam(params, 'input');
579
+ const result = await conwayApiRequest(apiKey, 'POST', `/v1/sandboxes/${sandboxId}/pty/${sessionId}/write`, { input });
580
+ return ok(result);
581
+ }
582
+ if (action === 'sandbox_pty_read') {
583
+ const apiKey = requireApiKey();
584
+ const sandboxId = requireParam(params, 'sandbox_id');
585
+ const sessionId = requireParam(params, 'session_id');
586
+ const full = params.full === 'true' ? '?full=true' : '';
587
+ const result = await conwayApiRequest(apiKey, 'GET', `/v1/sandboxes/${sandboxId}/pty/${sessionId}/read${full}`);
588
+ return ok(result);
589
+ }
590
+ if (action === 'sandbox_pty_close') {
591
+ const apiKey = requireApiKey();
592
+ const sandboxId = requireParam(params, 'sandbox_id');
593
+ const sessionId = requireParam(params, 'session_id');
594
+ const result = await conwayApiRequest(apiKey, 'DELETE', `/v1/sandboxes/${sandboxId}/pty/${sessionId}`);
595
+ return ok(result);
596
+ }
597
+ if (action === 'sandbox_pty_resize') {
598
+ const apiKey = requireApiKey();
599
+ const sandboxId = requireParam(params, 'sandbox_id');
600
+ const sessionId = requireParam(params, 'session_id');
601
+ const cols = parseInt(requireParam(params, 'cols'));
602
+ const rows = parseInt(requireParam(params, 'rows'));
603
+ const result = await conwayApiRequest(apiKey, 'POST', `/v1/sandboxes/${sandboxId}/pty/${sessionId}/resize`, { cols, rows });
604
+ return ok(result);
605
+ }
606
+ if (action === 'sandbox_pty_list') {
607
+ const apiKey = requireApiKey();
608
+ const sandboxId = requireParam(params, 'sandbox_id');
609
+ const result = await conwayApiRequest(apiKey, 'GET', `/v1/sandboxes/${sandboxId}/pty`);
610
+ return ok(result);
611
+ }
612
+ // ─── Wallet & x402 ─────────────────────────────────────────────
613
+ if (action === 'wallet_info') {
614
+ const conway = await getConwayModule();
615
+ const { account, isNew } = await conway.getWallet();
616
+ const network = params.network || 'eip155:8453';
617
+ try {
618
+ const balance = await conway.getUsdcBalance(account.address, network);
619
+ return ok({
620
+ address: account.address,
621
+ isNew,
622
+ network,
623
+ balance: { usdc: balance, formatted: `$${balance.toFixed(2)} USDC` },
624
+ configDir: conway.getConfigDir(),
625
+ });
626
+ }
627
+ catch (error) {
628
+ return ok({
629
+ address: account.address,
630
+ isNew,
631
+ network,
632
+ error: error.message || String(error),
633
+ configDir: conway.getConfigDir(),
634
+ });
635
+ }
636
+ }
637
+ if (action === 'wallet_networks') {
638
+ const conway = await getConwayModule();
639
+ const networks = Object.entries(conway.SUPPORTED_NETWORKS).map(([id, config]) => ({
640
+ id,
641
+ name: config.name,
642
+ chainId: config.chain.id,
643
+ usdcAddress: config.usdcAddress,
644
+ }));
645
+ return ok({ networks });
646
+ }
647
+ if (action === 'x402_discover') {
648
+ const conway = await getConwayModule();
649
+ const url = requireParam(params, 'url');
650
+ const result = await conway.discoverX402Endpoints(url);
651
+ return ok(result);
652
+ }
653
+ if (action === 'x402_check') {
654
+ const conway = await getConwayModule();
655
+ const url = requireParam(params, 'url');
656
+ const method = params.method || undefined;
657
+ const result = await conway.checkX402Endpoint(url, method);
658
+ return ok(result);
659
+ }
660
+ if (action === 'x402_fetch') {
661
+ const conway = await getConwayModule();
662
+ const { account } = await conway.getWallet();
663
+ const url = requireParam(params, 'url');
664
+ const fetchOptions = {};
665
+ if (params.method)
666
+ fetchOptions.method = params.method;
667
+ if (params.headers)
668
+ fetchOptions.headers = JSON.parse(params.headers);
669
+ if (params.body)
670
+ fetchOptions.body = params.body;
671
+ const response = await conway.x402Fetch(account, url, fetchOptions);
672
+ const responseBody = await response.text();
673
+ return ok({
674
+ status: response.status,
675
+ statusText: response.statusText,
676
+ body: responseBody,
677
+ });
678
+ }
679
+ // ─── Domains ────────────────────────────────────────────────────
680
+ if (action === 'domain_search') {
681
+ const query = requireParam(params, 'query');
682
+ const tlds = params.tlds || 'com,io,ai,xyz,net,org,dev';
683
+ const result = await conwayDomainRequest('GET', `/domains/search?q=${encodeURIComponent(query)}&tlds=${encodeURIComponent(tlds)}`);
684
+ return ok(result);
685
+ }
686
+ if (action === 'domain_list') {
687
+ const result = await conwayDomainRequest('GET', '/domains');
688
+ return ok(result);
689
+ }
690
+ if (action === 'domain_info') {
691
+ const domain = requireParam(params, 'domain');
692
+ const result = await conwayDomainRequest('GET', `/domains/${encodeURIComponent(domain)}`);
693
+ return ok(result);
694
+ }
695
+ if (action === 'domain_register') {
696
+ const domain = requireParam(params, 'domain');
697
+ const years = params.years ? parseInt(params.years) : 1;
698
+ const privacy = params.privacy !== 'false';
699
+ const result = await x402DomainFetch('/domains/register', 'POST', { domain, years, privacy });
700
+ return ok(result);
701
+ }
702
+ if (action === 'domain_renew') {
703
+ const domain = requireParam(params, 'domain');
704
+ const years = params.years ? parseInt(params.years) : 1;
705
+ const result = await x402DomainFetch(`/domains/${encodeURIComponent(domain)}/renew`, 'POST', { years });
706
+ return ok(result);
707
+ }
708
+ if (action === 'domain_dns_list') {
709
+ const domain = requireParam(params, 'domain');
710
+ const result = await conwayDomainRequest('GET', `/domains/${encodeURIComponent(domain)}/dns`);
711
+ return ok(result);
712
+ }
713
+ if (action === 'domain_dns_add') {
714
+ const domain = requireParam(params, 'domain');
715
+ const spec = params.spec ? JSON.parse(params.spec) : {};
716
+ const result = await conwayDomainRequest('POST', `/domains/${encodeURIComponent(domain)}/dns`, spec);
717
+ return ok(result);
718
+ }
719
+ if (action === 'domain_dns_update') {
720
+ const domain = requireParam(params, 'domain');
721
+ const recordId = requireParam(params, 'record_id');
722
+ const spec = params.spec ? JSON.parse(params.spec) : {};
723
+ const result = await conwayDomainRequest('PUT', `/domains/${encodeURIComponent(domain)}/dns/${encodeURIComponent(recordId)}`, spec);
724
+ return ok(result);
725
+ }
726
+ if (action === 'domain_dns_delete') {
727
+ const domain = requireParam(params, 'domain');
728
+ const recordId = requireParam(params, 'record_id');
729
+ const result = await conwayDomainRequest('DELETE', `/domains/${encodeURIComponent(domain)}/dns/${encodeURIComponent(recordId)}`);
730
+ return ok(result);
731
+ }
732
+ if (action === 'domain_pricing') {
733
+ const tlds = params.tlds ? `?tlds=${encodeURIComponent(params.tlds)}` : '';
734
+ const result = await conwayDomainRequest('GET', `/domains/pricing${tlds}`);
735
+ return ok(result);
736
+ }
737
+ if (action === 'domain_check') {
738
+ const domains = requireParam(params, 'domains');
739
+ const result = await conwayDomainRequest('GET', `/domains/check?domains=${encodeURIComponent(domains)}`);
740
+ return ok(result);
741
+ }
742
+ if (action === 'domain_privacy') {
743
+ const domain = requireParam(params, 'domain');
744
+ const enabled = params.privacy === 'true';
745
+ const result = await conwayDomainRequest('PUT', `/domains/${encodeURIComponent(domain)}/privacy`, { enabled });
746
+ return ok(result);
747
+ }
748
+ if (action === 'domain_nameservers') {
749
+ const domain = requireParam(params, 'domain');
750
+ const nsString = requireParam(params, 'nameservers');
751
+ const nameservers = nsString.split(',').map((s) => s.trim());
752
+ const result = await conwayDomainRequest('PUT', `/domains/${encodeURIComponent(domain)}/nameservers`, { nameservers });
753
+ return ok(result);
754
+ }
755
+ // ─── Credits ────────────────────────────────────────────────────
756
+ if (action === 'credits_balance') {
757
+ const apiKey = requireApiKey();
758
+ const result = await conwayApiRequest(apiKey, 'GET', '/v1/credits/balance');
759
+ return ok(result);
760
+ }
761
+ if (action === 'credits_history') {
762
+ const apiKey = requireApiKey();
763
+ const limit = params.limit ? parseInt(params.limit) : 20;
764
+ const offset = params.offset ? parseInt(params.offset) : 0;
765
+ const result = await conwayApiRequest(apiKey, 'GET', `/v1/credits/history?limit=${limit}&offset=${offset}`);
766
+ return ok(result);
767
+ }
768
+ if (action === 'credits_pricing') {
769
+ const apiKey = requireApiKey();
770
+ const result = await conwayApiRequest(apiKey, 'GET', '/v1/credits/pricing');
771
+ return ok(result);
772
+ }
773
+ if (action === 'credits_topup') {
774
+ const conway = await getConwayModule();
775
+ const { account } = await conway.getWallet();
776
+ const amountUsd = parseInt(requireParam(params, 'amount_usd'));
777
+ const validTiers = [5, 25, 100, 500, 1000, 2500];
778
+ if (!validTiers.includes(amountUsd)) {
779
+ return fail(`amount_usd must be one of: ${validTiers.join(', ')}`);
780
+ }
781
+ const recipientAddress = params.address || account.address;
782
+ const url = `${CONWAY_API_URL}/pay/${amountUsd}/${recipientAddress}`;
783
+ const response = await conway.x402Fetch(account, url, { method: 'GET' });
784
+ const raw = await response.text();
785
+ if (!response.ok) {
786
+ return fail(`Credits top-up failed (${response.status}): ${raw}`);
787
+ }
788
+ let parsed;
789
+ try {
790
+ parsed = JSON.parse(raw);
156
791
  }
157
- switch (action) {
158
- case 'sandbox_create': {
159
- const spec = params.spec ? JSON.parse(params.spec) : {};
160
- const result = await conwayApiRequest(apiKey, 'POST', '/v1/sandboxes', {
161
- name: spec.name,
162
- vcpu: spec.vcpu ?? 1,
163
- memory_mb: spec.memory_mb ?? 512,
164
- disk_gb: spec.disk_gb ?? 5,
165
- region: spec.region ?? 'us-east',
166
- });
167
- return { callId: '', success: true, result: JSON.stringify(result, null, 2) };
168
- }
169
- case 'sandbox_list': {
170
- const result = await conwayApiRequest(apiKey, 'GET', '/v1/sandboxes');
171
- return { callId: '', success: true, result: JSON.stringify(result, null, 2) };
172
- }
173
- case 'sandbox_exec': {
174
- const sandboxId = params.sandbox_id;
175
- const command = params.command;
176
- if (!sandboxId || !command) {
177
- return { callId: '', success: false, result: null, error: 'sandbox_id and command are required for sandbox_exec' };
178
- }
179
- const result = await conwayApiRequest(apiKey, 'POST', `/v1/sandboxes/${sandboxId}/exec`, {
180
- command,
181
- timeout: 30,
182
- });
183
- return { callId: '', success: true, result: JSON.stringify(result, null, 2) };
184
- }
185
- case 'sandbox_delete': {
186
- const sandboxId = params.sandbox_id;
187
- if (!sandboxId) {
188
- return { callId: '', success: false, result: null, error: 'sandbox_id is required for sandbox_delete' };
189
- }
190
- const result = await conwayApiRequest(apiKey, 'DELETE', `/v1/sandboxes/${sandboxId}`);
191
- return { callId: '', success: true, result: JSON.stringify(result, null, 2) };
192
- }
193
- case 'credits_balance': {
194
- const result = await conwayApiRequest(apiKey, 'GET', '/v1/credits/balance');
195
- return { callId: '', success: true, result: JSON.stringify(result, null, 2) };
196
- }
197
- default:
198
- return { callId: '', success: false, result: null, error: `Unknown sandbox action: "${action}"` };
792
+ catch {
793
+ parsed = { raw_response: raw };
199
794
  }
795
+ return ok({ ...parsed, amount_usd: amountUsd, recipient_address: recipientAddress });
200
796
  }
201
- // --- Domain actions (use SIWE via conway-terminal wallet) ---
202
- if (action.startsWith('domain_')) {
203
- switch (action) {
204
- case 'domain_search': {
205
- const query = params.query;
206
- if (!query) {
207
- return { callId: '', success: false, result: null, error: 'query is required for domain_search' };
208
- }
209
- const result = await conwayDomainRequest('GET', `/domains/search?q=${encodeURIComponent(query)}`);
210
- return { callId: '', success: true, result: JSON.stringify(result, null, 2) };
211
- }
212
- case 'domain_list': {
213
- const result = await conwayDomainRequest('GET', '/domains');
214
- return { callId: '', success: true, result: JSON.stringify(result, null, 2) };
215
- }
216
- case 'domain_info': {
217
- const domain = params.domain;
218
- if (!domain) {
219
- return { callId: '', success: false, result: null, error: 'domain is required for domain_info' };
220
- }
221
- const result = await conwayDomainRequest('GET', `/domains/${encodeURIComponent(domain)}`);
222
- return { callId: '', success: true, result: JSON.stringify(result, null, 2) };
223
- }
224
- case 'domain_check': {
225
- const domain = params.domain;
226
- if (!domain) {
227
- return { callId: '', success: false, result: null, error: 'domain is required for domain_check (comma-separated for multiple)' };
228
- }
229
- const result = await conwayDomainRequest('GET', `/domains/check?domains=${encodeURIComponent(domain)}`);
230
- return { callId: '', success: true, result: JSON.stringify(result, null, 2) };
231
- }
232
- case 'domain_register': {
233
- const domain = params.domain;
234
- if (!domain) {
235
- return { callId: '', success: false, result: null, error: 'domain is required for domain_register' };
236
- }
237
- // Domain registration uses x402 payment — requires USDC in Conway wallet
238
- // For now use the JWT-authenticated endpoint; the x402 payment is handled server-side
239
- const result = await conwayDomainRequest('POST', '/domains/register', { domain });
240
- return { callId: '', success: true, result: JSON.stringify(result, null, 2) };
241
- }
242
- case 'domain_dns_list': {
243
- const domain = params.domain;
244
- if (!domain) {
245
- return { callId: '', success: false, result: null, error: 'domain is required for domain_dns_list' };
246
- }
247
- const result = await conwayDomainRequest('GET', `/domains/${encodeURIComponent(domain)}/dns`);
248
- return { callId: '', success: true, result: JSON.stringify(result, null, 2) };
249
- }
250
- case 'domain_dns_add': {
251
- const domain = params.domain;
252
- const spec = params.spec;
253
- if (!domain || !spec) {
254
- return { callId: '', success: false, result: null, error: 'domain and spec (DNS record JSON) are required for domain_dns_add' };
255
- }
256
- const record = JSON.parse(spec);
257
- const result = await conwayDomainRequest('POST', `/domains/${encodeURIComponent(domain)}/dns`, record);
258
- return { callId: '', success: true, result: JSON.stringify(result, null, 2) };
259
- }
260
- default:
261
- return { callId: '', success: false, result: null, error: `Unknown domain action: "${action}". Valid domain actions: domain_search, domain_list, domain_info, domain_check, domain_register, domain_dns_list, domain_dns_add.` };
797
+ // ─── Inference ──────────────────────────────────────────────────
798
+ if (action === 'chat_completions') {
799
+ const apiKey = requireApiKey();
800
+ const spec = params.spec ? JSON.parse(params.spec) : {};
801
+ if (!spec.model || !spec.messages) {
802
+ return fail('chat_completions requires spec with "model" and "messages" fields');
262
803
  }
804
+ const body = {
805
+ model: spec.model,
806
+ messages: spec.messages,
807
+ };
808
+ if (spec.temperature !== undefined)
809
+ body.temperature = spec.temperature;
810
+ if (spec.max_tokens !== undefined)
811
+ body.max_tokens = spec.max_tokens;
812
+ const result = await conwayApiRequest(apiKey, 'POST', '/v1/chat/completions', body);
813
+ return ok(result);
263
814
  }
264
- return {
265
- callId: '',
266
- success: false,
267
- result: null,
268
- error: `Unknown Conway action: "${action}". Use sandbox_* or domain_* actions.`,
269
- };
815
+ // ─── Unknown action ─────────────────────────────────────────────
816
+ return fail(`Unknown Conway action: "${action}". Valid actions: ${ALL_ACTIONS.join(', ')}.`);
270
817
  }
271
818
  catch (err) {
272
819
  return { callId: '', success: false, result: null, error: err instanceof Error ? err.message : String(err) };