repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
public Clawd ADK gateway launch mirror
stars
latest
clone command
git clone gitlawb://did:key:z6Mkq5mY...iFZ5/my-project-publ...git clone gitlawb://did:key:z6Mkq5mY.../my-project-publ...2fa351d6docs: add automaton and perps launch sources16d ago| #1 | /** |
| #2 | * Conway API Client |
| #3 | * |
| #4 | * Communicates with Conway's control plane for sandbox management, |
| #5 | * credits, and infrastructure operations. |
| #6 | * Adapted from @aiws/sdk patterns. |
| #7 | */ |
| #8 | |
| #9 | import type { |
| #10 | ConwayClient, |
| #11 | ExecResult, |
| #12 | PortInfo, |
| #13 | CreateSandboxOptions, |
| #14 | SandboxInfo, |
| #15 | PricingTier, |
| #16 | CreditTransferResult, |
| #17 | DomainSearchResult, |
| #18 | DomainRegistration, |
| #19 | DnsRecord, |
| #20 | ModelInfo, |
| #21 | } from "../types.js"; |
| #22 | |
| #23 | interface ConwayClientOptions { |
| #24 | apiUrl: string; |
| #25 | apiKey: string; |
| #26 | sandboxId: string; |
| #27 | } |
| #28 | |
| #29 | export function createConwayClient( |
| #30 | options: ConwayClientOptions, |
| #31 | ): ConwayClient { |
| #32 | const { apiUrl, apiKey, sandboxId } = options; |
| #33 | |
| #34 | async function request( |
| #35 | method: string, |
| #36 | path: string, |
| #37 | body?: unknown, |
| #38 | ): Promise<any> { |
| #39 | const resp = await fetch(`${apiUrl}${path}`, { |
| #40 | method, |
| #41 | headers: { |
| #42 | "Content-Type": "application/json", |
| #43 | Authorization: apiKey, |
| #44 | }, |
| #45 | body: body ? JSON.stringify(body) : undefined, |
| #46 | }); |
| #47 | |
| #48 | if (!resp.ok) { |
| #49 | const text = await resp.text(); |
| #50 | throw new Error( |
| #51 | `Conway API error: ${method} ${path} -> ${resp.status}: ${text}`, |
| #52 | ); |
| #53 | } |
| #54 | |
| #55 | const contentType = resp.headers.get("content-type"); |
| #56 | if (contentType?.includes("application/json")) { |
| #57 | return resp.json(); |
| #58 | } |
| #59 | return resp.text(); |
| #60 | } |
| #61 | |
| #62 | // ─── Sandbox Operations (own sandbox) ──────────────────────── |
| #63 | |
| #64 | const exec = async ( |
| #65 | command: string, |
| #66 | timeout?: number, |
| #67 | ): Promise<ExecResult> => { |
| #68 | const result = await request( |
| #69 | "POST", |
| #70 | `/v1/sandboxes/${sandboxId}/exec`, |
| #71 | { command, timeout }, |
| #72 | ); |
| #73 | return { |
| #74 | stdout: result.stdout || "", |
| #75 | stderr: result.stderr || "", |
| #76 | exitCode: result.exit_code ?? result.exitCode ?? 0, |
| #77 | }; |
| #78 | }; |
| #79 | |
| #80 | const writeFile = async ( |
| #81 | path: string, |
| #82 | content: string, |
| #83 | ): Promise<void> => { |
| #84 | await request( |
| #85 | "POST", |
| #86 | `/v1/sandboxes/${sandboxId}/files/upload/json`, |
| #87 | { path, content }, |
| #88 | ); |
| #89 | }; |
| #90 | |
| #91 | const readFile = async (filePath: string): Promise<string> => { |
| #92 | const result = await request( |
| #93 | "GET", |
| #94 | `/v1/sandboxes/${sandboxId}/files/read?path=${encodeURIComponent(filePath)}`, |
| #95 | ); |
| #96 | return typeof result === "string" ? result : result.content || ""; |
| #97 | }; |
| #98 | |
| #99 | const exposePort = async (port: number): Promise<PortInfo> => { |
| #100 | const result = await request( |
| #101 | "POST", |
| #102 | `/v1/sandboxes/${sandboxId}/ports/expose`, |
| #103 | { port }, |
| #104 | ); |
| #105 | return { |
| #106 | port: result.port, |
| #107 | publicUrl: result.public_url || result.publicUrl || result.url, |
| #108 | sandboxId, |
| #109 | }; |
| #110 | }; |
| #111 | |
| #112 | const removePort = async (port: number): Promise<void> => { |
| #113 | await request( |
| #114 | "DELETE", |
| #115 | `/v1/sandboxes/${sandboxId}/ports/${port}`, |
| #116 | ); |
| #117 | }; |
| #118 | |
| #119 | // ─── Sandbox Management (other sandboxes) ──────────────────── |
| #120 | |
| #121 | const createSandbox = async ( |
| #122 | options: CreateSandboxOptions, |
| #123 | ): Promise<SandboxInfo> => { |
| #124 | const result = await request("POST", "/v1/sandboxes", { |
| #125 | name: options.name, |
| #126 | vcpu: options.vcpu || 1, |
| #127 | memory_mb: options.memoryMb || 512, |
| #128 | disk_gb: options.diskGb || 5, |
| #129 | region: options.region, |
| #130 | }); |
| #131 | return { |
| #132 | id: result.id || result.sandbox_id, |
| #133 | status: result.status || "running", |
| #134 | region: result.region || "", |
| #135 | vcpu: result.vcpu || options.vcpu || 1, |
| #136 | memoryMb: result.memory_mb || options.memoryMb || 512, |
| #137 | diskGb: result.disk_gb || options.diskGb || 5, |
| #138 | terminalUrl: result.terminal_url, |
| #139 | createdAt: result.created_at || new Date().toISOString(), |
| #140 | }; |
| #141 | }; |
| #142 | |
| #143 | const deleteSandbox = async (targetId: string): Promise<void> => { |
| #144 | await request("DELETE", `/v1/sandboxes/${targetId}`); |
| #145 | }; |
| #146 | |
| #147 | const listSandboxes = async (): Promise<SandboxInfo[]> => { |
| #148 | const result = await request("GET", "/v1/sandboxes"); |
| #149 | const sandboxes = Array.isArray(result) |
| #150 | ? result |
| #151 | : result.sandboxes || []; |
| #152 | return sandboxes.map((s: any) => ({ |
| #153 | id: s.id || s.sandbox_id, |
| #154 | status: s.status || "unknown", |
| #155 | region: s.region || "", |
| #156 | vcpu: s.vcpu || 0, |
| #157 | memoryMb: s.memory_mb || 0, |
| #158 | diskGb: s.disk_gb || 0, |
| #159 | terminalUrl: s.terminal_url, |
| #160 | createdAt: s.created_at || "", |
| #161 | })); |
| #162 | }; |
| #163 | |
| #164 | // ─── Credits ───────────────────────────────────────────────── |
| #165 | |
| #166 | const getCreditsBalance = async (): Promise<number> => { |
| #167 | const result = await request("GET", "/v1/credits/balance"); |
| #168 | return result.balance_cents ?? result.credits_cents ?? 0; |
| #169 | }; |
| #170 | |
| #171 | const getCreditsPricing = async (): Promise<PricingTier[]> => { |
| #172 | const result = await request("GET", "/v1/credits/pricing"); |
| #173 | const tiers = result.tiers || result.pricing || []; |
| #174 | return tiers.map((t: any) => ({ |
| #175 | name: t.name || "", |
| #176 | vcpu: t.vcpu || 0, |
| #177 | memoryMb: t.memory_mb || 0, |
| #178 | diskGb: t.disk_gb || 0, |
| #179 | monthlyCents: t.monthly_cents || 0, |
| #180 | })); |
| #181 | }; |
| #182 | |
| #183 | const transferCredits = async ( |
| #184 | toAddress: string, |
| #185 | amountCents: number, |
| #186 | note?: string, |
| #187 | ): Promise<CreditTransferResult> => { |
| #188 | const payload = { |
| #189 | to_address: toAddress, |
| #190 | amount_cents: amountCents, |
| #191 | note, |
| #192 | }; |
| #193 | |
| #194 | const paths = [ |
| #195 | "/v1/credits/transfer", |
| #196 | "/v1/credits/transfers", |
| #197 | ]; |
| #198 | |
| #199 | let lastError = "Unknown transfer error"; |
| #200 | |
| #201 | for (const path of paths) { |
| #202 | const resp = await fetch(`${apiUrl}${path}`, { |
| #203 | method: "POST", |
| #204 | headers: { |
| #205 | "Content-Type": "application/json", |
| #206 | Authorization: apiKey, |
| #207 | }, |
| #208 | body: JSON.stringify(payload), |
| #209 | }); |
| #210 | |
| #211 | if (!resp.ok) { |
| #212 | const text = await resp.text(); |
| #213 | lastError = `${resp.status}: ${text}`; |
| #214 | // Try next known endpoint shape before failing. |
| #215 | if (resp.status === 404) continue; |
| #216 | throw new Error(`Conway API error: POST ${path} -> ${lastError}`); |
| #217 | } |
| #218 | |
| #219 | const data = await resp.json().catch(() => ({} as any)); |
| #220 | return { |
| #221 | transferId: data.transfer_id || data.id || "", |
| #222 | status: data.status || "submitted", |
| #223 | toAddress: data.to_address || toAddress, |
| #224 | amountCents: data.amount_cents ?? amountCents, |
| #225 | balanceAfterCents: |
| #226 | data.balance_after_cents ?? data.new_balance_cents ?? undefined, |
| #227 | }; |
| #228 | } |
| #229 | |
| #230 | throw new Error( |
| #231 | `Conway API error: POST /v1/credits/transfer -> ${lastError}`, |
| #232 | ); |
| #233 | }; |
| #234 | |
| #235 | // ─── Domains ────────────────────────────────────────────────── |
| #236 | |
| #237 | const searchDomains = async ( |
| #238 | query: string, |
| #239 | tlds?: string, |
| #240 | ): Promise<DomainSearchResult[]> => { |
| #241 | const params = new URLSearchParams({ query }); |
| #242 | if (tlds) params.set("tlds", tlds); |
| #243 | const result = await request("GET", `/v1/domains/search?${params}`); |
| #244 | const results = result.results || result.domains || []; |
| #245 | return results.map((d: any) => ({ |
| #246 | domain: d.domain, |
| #247 | available: d.available ?? d.purchasable ?? false, |
| #248 | registrationPrice: d.registration_price ?? d.purchase_price, |
| #249 | renewalPrice: d.renewal_price, |
| #250 | currency: d.currency || "USD", |
| #251 | })); |
| #252 | }; |
| #253 | |
| #254 | const registerDomain = async ( |
| #255 | domain: string, |
| #256 | years: number = 1, |
| #257 | ): Promise<DomainRegistration> => { |
| #258 | const result = await request("POST", "/v1/domains/register", { |
| #259 | domain, |
| #260 | years, |
| #261 | }); |
| #262 | return { |
| #263 | domain: result.domain || domain, |
| #264 | status: result.status || "registered", |
| #265 | expiresAt: result.expires_at || result.expiry, |
| #266 | transactionId: result.transaction_id || result.id, |
| #267 | }; |
| #268 | }; |
| #269 | |
| #270 | const listDnsRecords = async (domain: string): Promise<DnsRecord[]> => { |
| #271 | const result = await request("GET", `/v1/domains/${encodeURIComponent(domain)}/dns`); |
| #272 | const records = result.records || result || []; |
| #273 | return (Array.isArray(records) ? records : []).map((r: any) => ({ |
| #274 | id: r.id || r.record_id || "", |
| #275 | type: r.type || "", |
| #276 | host: r.host || r.name || "", |
| #277 | value: r.value || r.answer || "", |
| #278 | ttl: r.ttl, |
| #279 | distance: r.distance ?? r.priority, |
| #280 | })); |
| #281 | }; |
| #282 | |
| #283 | const addDnsRecord = async ( |
| #284 | domain: string, |
| #285 | type: string, |
| #286 | host: string, |
| #287 | value: string, |
| #288 | ttl?: number, |
| #289 | ): Promise<DnsRecord> => { |
| #290 | const result = await request( |
| #291 | "POST", |
| #292 | `/v1/domains/${encodeURIComponent(domain)}/dns`, |
| #293 | { type, host, value, ttl: ttl || 3600 }, |
| #294 | ); |
| #295 | return { |
| #296 | id: result.id || result.record_id || "", |
| #297 | type: result.type || type, |
| #298 | host: result.host || host, |
| #299 | value: result.value || value, |
| #300 | ttl: result.ttl || ttl || 3600, |
| #301 | }; |
| #302 | }; |
| #303 | |
| #304 | const deleteDnsRecord = async ( |
| #305 | domain: string, |
| #306 | recordId: string, |
| #307 | ): Promise<void> => { |
| #308 | await request( |
| #309 | "DELETE", |
| #310 | `/v1/domains/${encodeURIComponent(domain)}/dns/${encodeURIComponent(recordId)}`, |
| #311 | ); |
| #312 | }; |
| #313 | |
| #314 | // ─── Model Discovery ─────────────────────────────────────────── |
| #315 | |
| #316 | const listModels = async (): Promise<ModelInfo[]> => { |
| #317 | // Try inference.conway.tech first (has availability info), fall back to control plane |
| #318 | const urls = ["https://inference.conway.tech/v1/models", `${apiUrl}/v1/models`]; |
| #319 | for (const url of urls) { |
| #320 | try { |
| #321 | const resp = await fetch(url, { |
| #322 | headers: { Authorization: apiKey }, |
| #323 | }); |
| #324 | if (!resp.ok) continue; |
| #325 | const result = await resp.json() as any; |
| #326 | const raw = result.data || result.models || []; |
| #327 | return raw |
| #328 | .filter((m: any) => m.available !== false) |
| #329 | .map((m: any) => ({ |
| #330 | id: m.id, |
| #331 | provider: m.provider || m.owned_by || "unknown", |
| #332 | pricing: { |
| #333 | inputPerMillion: m.pricing?.input_per_million ?? m.pricing?.input_per_1m_tokens_usd ?? 0, |
| #334 | outputPerMillion: m.pricing?.output_per_million ?? m.pricing?.output_per_1m_tokens_usd ?? 0, |
| #335 | }, |
| #336 | })); |
| #337 | } catch { |
| #338 | continue; |
| #339 | } |
| #340 | } |
| #341 | return []; |
| #342 | }; |
| #343 | |
| #344 | const client = { |
| #345 | exec, |
| #346 | writeFile, |
| #347 | readFile, |
| #348 | exposePort, |
| #349 | removePort, |
| #350 | createSandbox, |
| #351 | deleteSandbox, |
| #352 | listSandboxes, |
| #353 | getCreditsBalance, |
| #354 | getCreditsPricing, |
| #355 | transferCredits, |
| #356 | searchDomains, |
| #357 | registerDomain, |
| #358 | listDnsRecords, |
| #359 | addDnsRecord, |
| #360 | deleteDnsRecord, |
| #361 | listModels, |
| #362 | } as ConwayClient & { __apiUrl: string; __apiKey: string }; |
| #363 | |
| #364 | // Expose for child sandbox operations in replication module |
| #365 | client.__apiUrl = apiUrl; |
| #366 | client.__apiKey = apiKey; |
| #367 | |
| #368 | return client; |
| #369 | } |
| #370 |