mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-04-28 03:49:31 +00:00
feat: show cloud prices as lead indicator (#2347)
* feat: show cloud prices as lead indicator, default OpenClaw to Kimi K2.5 - Add `price` field to all clouds in manifest.json - Show price as lead indicator in cloud picker hints, cloud listings, cloud info, and dry-run preview - Change OpenClaw default model from openrouter/auto to moonshotai/kimi-k2.5 (top used model by OpenClaw users) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: add defensive guards for undefined cloud price in cached manifests When users upgrade CLI but have cached manifests from before the price field was added, c.price is undefined. Add ?? "" fallbacks and an if-guard to prevent runtime crashes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: A <258483684+la14-1@users.noreply.github.com>
This commit is contained in:
parent
c61852d689
commit
24a3c7328d
17 changed files with 61 additions and 11 deletions
|
|
@ -57,7 +57,7 @@
|
|||
"interactive_prompts": {
|
||||
"model_id": {
|
||||
"prompt": "Enter model ID",
|
||||
"default": "openrouter/auto"
|
||||
"default": "moonshotai/kimi-k2.5"
|
||||
}
|
||||
},
|
||||
"icon": "https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/assets/agents/openclaw.png",
|
||||
|
|
@ -249,6 +249,7 @@
|
|||
"clouds": {
|
||||
"local": {
|
||||
"name": "Local Machine",
|
||||
"price": "Free",
|
||||
"description": "Your computer — no account or payment needed",
|
||||
"url": "https://github.com/OpenRouterTeam/spawn",
|
||||
"type": "local",
|
||||
|
|
@ -260,7 +261,8 @@
|
|||
},
|
||||
"hetzner": {
|
||||
"name": "Hetzner Cloud",
|
||||
"description": "European cloud servers from ~€3/mo (account required)",
|
||||
"price": "~€3/mo",
|
||||
"description": "European cloud servers (account required)",
|
||||
"url": "https://www.hetzner.com/cloud/",
|
||||
"type": "api",
|
||||
"auth": "HCLOUD_TOKEN",
|
||||
|
|
@ -276,7 +278,8 @@
|
|||
},
|
||||
"aws": {
|
||||
"name": "AWS Lightsail",
|
||||
"description": "Amazon cloud servers from $3.50/mo (AWS account required)",
|
||||
"price": "$3.50/mo",
|
||||
"description": "Amazon cloud servers (AWS account required)",
|
||||
"url": "https://aws.amazon.com/lightsail/",
|
||||
"type": "cli",
|
||||
"auth": "AWS_ACCESS_KEY_ID+AWS_SECRET_ACCESS_KEY",
|
||||
|
|
@ -293,7 +296,8 @@
|
|||
},
|
||||
"digitalocean": {
|
||||
"name": "DigitalOcean",
|
||||
"description": "Cloud servers from $4/mo (account + payment method required)",
|
||||
"price": "$4/mo",
|
||||
"description": "Cloud servers (account + payment method required)",
|
||||
"url": "https://www.digitalocean.com/",
|
||||
"type": "api",
|
||||
"auth": "DO_API_TOKEN",
|
||||
|
|
@ -309,6 +313,7 @@
|
|||
},
|
||||
"gcp": {
|
||||
"name": "GCP Compute Engine",
|
||||
"price": "$7/mo",
|
||||
"description": "Google cloud servers — $300 free trial (Google account required)",
|
||||
"url": "https://cloud.google.com/compute",
|
||||
"type": "cli",
|
||||
|
|
@ -326,6 +331,7 @@
|
|||
},
|
||||
"sprite": {
|
||||
"name": "Sprite",
|
||||
"price": "Free tier",
|
||||
"description": "Managed cloud servers — one command to deploy",
|
||||
"url": "https://sprites.dev",
|
||||
"type": "cli",
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ function createManifest(): Manifest {
|
|||
sprite: {
|
||||
name: "Sprite",
|
||||
description: "Lightweight VMs",
|
||||
price: "test",
|
||||
url: "https://sprite.sh",
|
||||
type: "vm",
|
||||
auth: "SPRITE_TOKEN",
|
||||
|
|
@ -61,6 +62,7 @@ function createManifest(): Manifest {
|
|||
hetzner: {
|
||||
name: "Hetzner Cloud",
|
||||
description: "European cloud provider",
|
||||
price: "test",
|
||||
url: "https://hetzner.com",
|
||||
type: "cloud",
|
||||
auth: "HCLOUD_TOKEN",
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ function createTestManifest(): Manifest {
|
|||
sprite: {
|
||||
name: "Sprite",
|
||||
description: "Lightweight VMs",
|
||||
price: "test",
|
||||
url: "https://sprite.sh",
|
||||
type: "vm",
|
||||
auth: "SPRITE_TOKEN",
|
||||
|
|
@ -69,6 +70,7 @@ function createTestManifest(): Manifest {
|
|||
hetzner: {
|
||||
name: "Hetzner Cloud",
|
||||
description: "European cloud provider",
|
||||
price: "test",
|
||||
url: "https://hetzner.com",
|
||||
type: "cloud",
|
||||
auth: "HCLOUD_TOKEN",
|
||||
|
|
@ -79,6 +81,7 @@ function createTestManifest(): Manifest {
|
|||
vultr: {
|
||||
name: "Vultr",
|
||||
description: "Cloud compute",
|
||||
price: "test",
|
||||
url: "https://vultr.com",
|
||||
type: "cloud",
|
||||
auth: "VULTR_API_KEY",
|
||||
|
|
@ -377,6 +380,7 @@ describe("checkEntity", () => {
|
|||
"local-cloud": {
|
||||
name: "Local Cloud",
|
||||
description: "Local cloud provider",
|
||||
price: "test",
|
||||
url: "",
|
||||
type: "local",
|
||||
auth: "none",
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ const smallManifest: Manifest = {
|
|||
sprite: {
|
||||
name: "Sprite",
|
||||
description: "Lightweight VMs",
|
||||
price: "test",
|
||||
url: "https://sprite.sh",
|
||||
type: "vm",
|
||||
auth: "SPRITE_TOKEN",
|
||||
|
|
@ -67,6 +68,7 @@ const smallManifest: Manifest = {
|
|||
hetzner: {
|
||||
name: "Hetzner Cloud",
|
||||
description: "European cloud provider",
|
||||
price: "test",
|
||||
url: "https://hetzner.com",
|
||||
type: "cloud",
|
||||
auth: "HCLOUD_TOKEN",
|
||||
|
|
@ -116,6 +118,7 @@ const multiTypeManifest: Manifest = {
|
|||
local: {
|
||||
name: "Local Machine",
|
||||
description: "Run agents on your own machine",
|
||||
price: "test",
|
||||
url: "",
|
||||
type: "local",
|
||||
auth: "none",
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ const manifestWithNotes = {
|
|||
emptycloud: {
|
||||
name: "Empty Cloud",
|
||||
description: "Cloud with no agents",
|
||||
price: "test",
|
||||
url: "https://empty.cloud",
|
||||
type: "vm",
|
||||
auth: "token",
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ const manyCloudManifest = {
|
|||
vultr: {
|
||||
name: "Vultr",
|
||||
description: "Cloud compute",
|
||||
price: "test",
|
||||
url: "https://vultr.com",
|
||||
type: "cloud",
|
||||
auth: "token",
|
||||
|
|
@ -50,6 +51,7 @@ const manyCloudManifest = {
|
|||
linode: {
|
||||
name: "Linode",
|
||||
description: "Cloud hosting",
|
||||
price: "test",
|
||||
url: "https://linode.com",
|
||||
type: "cloud",
|
||||
auth: "token",
|
||||
|
|
@ -60,6 +62,7 @@ const manyCloudManifest = {
|
|||
digitalocean: {
|
||||
name: "DigitalOcean",
|
||||
description: "Cloud infrastructure",
|
||||
price: "test",
|
||||
url: "https://digitalocean.com",
|
||||
type: "cloud",
|
||||
auth: "token",
|
||||
|
|
|
|||
|
|
@ -218,6 +218,7 @@ describe("getImplementedAgents", () => {
|
|||
newcloud: {
|
||||
name: "New Cloud",
|
||||
description: "Test",
|
||||
price: "test",
|
||||
url: "",
|
||||
type: "vm",
|
||||
auth: "token",
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ const manifestWithDistinctNames = {
|
|||
sp: {
|
||||
name: "Sprite Cloud",
|
||||
description: "Lightweight VMs",
|
||||
price: "test",
|
||||
url: "https://sprite.sh",
|
||||
type: "vm",
|
||||
auth: "token",
|
||||
|
|
@ -70,6 +71,7 @@ const manifestWithDistinctNames = {
|
|||
hz: {
|
||||
name: "Hetzner Cloud",
|
||||
description: "European cloud provider",
|
||||
price: "test",
|
||||
url: "https://hetzner.com",
|
||||
type: "cloud",
|
||||
auth: "token",
|
||||
|
|
@ -80,6 +82,7 @@ const manifestWithDistinctNames = {
|
|||
dc: {
|
||||
name: "DigitalOcean",
|
||||
description: "Cloud infrastructure",
|
||||
price: "test",
|
||||
url: "https://digitalocean.com",
|
||||
type: "cloud",
|
||||
auth: "token",
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ const manyCloudManifest = {
|
|||
sprite: {
|
||||
name: "Sprite",
|
||||
description: "Lightweight VMs",
|
||||
price: "test",
|
||||
url: "https://sprite.sh",
|
||||
type: "vm",
|
||||
auth: "token",
|
||||
|
|
@ -54,6 +55,7 @@ const manyCloudManifest = {
|
|||
hetzner: {
|
||||
name: "Hetzner Cloud",
|
||||
description: "European cloud provider",
|
||||
price: "test",
|
||||
url: "https://hetzner.com",
|
||||
type: "cloud",
|
||||
auth: "token",
|
||||
|
|
@ -64,6 +66,7 @@ const manyCloudManifest = {
|
|||
vultr: {
|
||||
name: "Vultr",
|
||||
description: "Cloud compute",
|
||||
price: "test",
|
||||
url: "https://vultr.com",
|
||||
type: "cloud",
|
||||
auth: "token",
|
||||
|
|
@ -74,6 +77,7 @@ const manyCloudManifest = {
|
|||
linode: {
|
||||
name: "Linode",
|
||||
description: "Cloud hosting",
|
||||
price: "test",
|
||||
url: "https://linode.com",
|
||||
type: "cloud",
|
||||
auth: "token",
|
||||
|
|
@ -84,6 +88,7 @@ const manyCloudManifest = {
|
|||
digitalocean: {
|
||||
name: "DigitalOcean",
|
||||
description: "Cloud infrastructure",
|
||||
price: "test",
|
||||
url: "https://digitalocean.com",
|
||||
type: "cloud",
|
||||
auth: "token",
|
||||
|
|
|
|||
|
|
@ -149,6 +149,11 @@ describe("Cloud required field types", () => {
|
|||
expect(cloud.description.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("price should be a non-empty string", () => {
|
||||
expect(typeof cloud.price).toBe("string");
|
||||
expect(cloud.price.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("url should be a valid URL string", () => {
|
||||
expect(typeof cloud.url).toBe("string");
|
||||
expect(cloud.url).toMatch(/^https?:\/\//);
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ function makeManifest(cloudAuth: string): Manifest {
|
|||
testcloud: {
|
||||
name: "Test Cloud",
|
||||
description: "A test cloud",
|
||||
price: "test",
|
||||
url: "https://test.cloud",
|
||||
type: "vps",
|
||||
auth: cloudAuth,
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ function makeManifest(overrides?: Partial<Manifest>): Manifest {
|
|||
hetzner: {
|
||||
name: "Hetzner Cloud",
|
||||
description: "German cloud provider",
|
||||
price: "test",
|
||||
url: "https://hetzner.cloud",
|
||||
type: "api",
|
||||
auth: "HCLOUD_TOKEN",
|
||||
|
|
@ -53,6 +54,7 @@ function makeManifest(overrides?: Partial<Manifest>): Manifest {
|
|||
sprite: {
|
||||
name: "Sprite",
|
||||
description: "Instant cloud dev environments",
|
||||
price: "test",
|
||||
url: "https://sprite.dev",
|
||||
type: "cli",
|
||||
auth: "sprite login",
|
||||
|
|
@ -63,6 +65,7 @@ function makeManifest(overrides?: Partial<Manifest>): Manifest {
|
|||
digitalocean: {
|
||||
name: "DigitalOcean",
|
||||
description: "Simple cloud hosting",
|
||||
price: "test",
|
||||
url: "https://digitalocean.com",
|
||||
type: "api",
|
||||
auth: "DO_API_TOKEN",
|
||||
|
|
@ -73,6 +76,7 @@ function makeManifest(overrides?: Partial<Manifest>): Manifest {
|
|||
upcloud: {
|
||||
name: "UpCloud",
|
||||
description: "European cloud provider",
|
||||
price: "test",
|
||||
url: "https://upcloud.com",
|
||||
type: "api",
|
||||
auth: "UPCLOUD_USERNAME + UPCLOUD_PASSWORD",
|
||||
|
|
@ -83,6 +87,7 @@ function makeManifest(overrides?: Partial<Manifest>): Manifest {
|
|||
localcloud: {
|
||||
name: "Local Machine",
|
||||
description: "Run locally",
|
||||
price: "test",
|
||||
url: "",
|
||||
type: "local",
|
||||
auth: "none",
|
||||
|
|
@ -165,7 +170,7 @@ describe("prioritizeCloudsByCredentials", () => {
|
|||
|
||||
expect(result.sortedClouds).toEqual(clouds);
|
||||
expect(result.credCount).toBe(0);
|
||||
expect(Object.keys(result.hintOverrides)).toHaveLength(0);
|
||||
expect(Object.keys(result.hintOverrides).length).toBeGreaterThanOrEqual(clouds.length);
|
||||
});
|
||||
|
||||
it("should move clouds with credentials to front", () => {
|
||||
|
|
@ -211,8 +216,8 @@ describe("prioritizeCloudsByCredentials", () => {
|
|||
const result = prioritizeCloudsByCredentials(clouds, manifest);
|
||||
|
||||
expect(result.hintOverrides["hetzner"]).toContain("credentials detected");
|
||||
expect(result.hintOverrides["hetzner"]).toContain("German cloud provider");
|
||||
expect(result.hintOverrides["digitalocean"]).toBeUndefined();
|
||||
expect(result.hintOverrides["hetzner"]).toContain("test");
|
||||
expect(result.hintOverrides["digitalocean"]).toBeDefined();
|
||||
});
|
||||
|
||||
it("should handle multi-var auth (both vars must be set)", () => {
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ export const createMockManifest = (): Manifest => ({
|
|||
sprite: {
|
||||
name: "Sprite",
|
||||
description: "Lightweight VMs",
|
||||
price: "test",
|
||||
url: "https://sprite.sh",
|
||||
type: "vm",
|
||||
auth: "token",
|
||||
|
|
@ -44,6 +45,7 @@ export const createMockManifest = (): Manifest => ({
|
|||
hetzner: {
|
||||
name: "Hetzner Cloud",
|
||||
description: "European cloud provider",
|
||||
price: "test",
|
||||
url: "https://hetzner.com",
|
||||
type: "cloud",
|
||||
auth: "token",
|
||||
|
|
|
|||
|
|
@ -251,7 +251,7 @@ export async function cmdClouds(): Promise<void> {
|
|||
}
|
||||
const credIndicator = formatCredentialIndicatorLocal(c.auth);
|
||||
console.log(
|
||||
` ${pc.green(key.padEnd(NAME_COLUMN_WIDTH))} ${c.name.padEnd(NAME_COLUMN_WIDTH)} ${pc.dim(`${countStr.padEnd(6)} ${c.description}`)}${credIndicator}`,
|
||||
` ${pc.green(key.padEnd(NAME_COLUMN_WIDTH))} ${c.name.padEnd(NAME_COLUMN_WIDTH)} ${pc.bold((c.price ?? "").padEnd(16))} ${pc.dim(`${countStr.padEnd(6)} ${c.description}`)}${credIndicator}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -372,6 +372,9 @@ export async function cmdCloudInfo(cloud: string, preloadedManifest?: Manifest):
|
|||
|
||||
const c = manifest.clouds[cloudKey];
|
||||
printInfoHeader(c);
|
||||
if (c.price) {
|
||||
console.log(` ${pc.bold(c.price)}`);
|
||||
}
|
||||
const credStatus = hasCloudCredentials(c.auth) ? pc.green("credentials detected") : pc.dim("no credentials set");
|
||||
console.log(pc.dim(` Type: ${c.type} | Auth: ${c.auth} | `) + credStatus);
|
||||
|
||||
|
|
|
|||
|
|
@ -116,11 +116,13 @@ function buildAgentLines(agentInfo: {
|
|||
|
||||
function buildCloudLines(cloudInfo: {
|
||||
name: string;
|
||||
price: string;
|
||||
description: string;
|
||||
defaults?: Record<string, string>;
|
||||
}): string[] {
|
||||
const lines = [
|
||||
` Name: ${cloudInfo.name}`,
|
||||
` Price: ${cloudInfo.price}`,
|
||||
` Description: ${cloudInfo.description}`,
|
||||
];
|
||||
if (cloudInfo.defaults) {
|
||||
|
|
|
|||
|
|
@ -425,13 +425,16 @@ export function prioritizeCloudsByCredentials(
|
|||
|
||||
const hintOverrides: Record<string, string> = {};
|
||||
for (const c of withCreds) {
|
||||
hintOverrides[c] = `credentials detected -- ${manifest.clouds[c].description}`;
|
||||
hintOverrides[c] = `${manifest.clouds[c].price ?? ""} — credentials detected`;
|
||||
}
|
||||
for (const c of featured) {
|
||||
hintOverrides[c] = `recommended -- ${manifest.clouds[c].description}`;
|
||||
hintOverrides[c] = `${manifest.clouds[c].price ?? ""} — recommended`;
|
||||
}
|
||||
for (const c of withCli) {
|
||||
hintOverrides[c] = `CLI installed -- ${manifest.clouds[c].description}`;
|
||||
hintOverrides[c] = `${manifest.clouds[c].price ?? ""} — CLI installed`;
|
||||
}
|
||||
for (const c of rest) {
|
||||
hintOverrides[c] = `${manifest.clouds[c].price ?? ""} — ${manifest.clouds[c].description}`;
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ export interface AgentDef {
|
|||
export interface CloudDef {
|
||||
name: string;
|
||||
description: string;
|
||||
price: string;
|
||||
url: string;
|
||||
type: string;
|
||||
auth: string;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue