mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
feat(cli): add balance command
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": minor
|
||||
---
|
||||
|
||||
Add a `kilo balance` command for checking the active Kilo account or team balance.
|
||||
@@ -36,6 +36,7 @@ import { PrCommand } from "./cli/cmd/pr"
|
||||
import { SessionCommand } from "./cli/cmd/session"
|
||||
import { RemoteCommand } from "./cli/cmd/remote" // kilocode_change
|
||||
import { RollCallCommand } from "./kilocode/cli/cmd/roll-call" // kilocode_change
|
||||
import { BalanceCommand } from "./kilocode/cli/cmd/balance" // kilocode_change
|
||||
import { DevSetupCommand, DevAliasCommand } from "./kilocode/cli/dev-setup" // kilocode_change
|
||||
// kilocode_change start - Import telemetry, instance disposal, and legacy migration
|
||||
import { Telemetry } from "@kilocode/kilo-telemetry"
|
||||
@@ -231,6 +232,7 @@ let cli = yargs(args) // kilocode_change
|
||||
// .command(WebCommand) // kilocode_change (Disabled unsupported opencode web UI)
|
||||
.command(ModelsCommand)
|
||||
.command(RollCallCommand) // kilocode_change
|
||||
.command(BalanceCommand) // kilocode_change
|
||||
.command(StatsCommand)
|
||||
.command(ExportCommand)
|
||||
.command(ImportCommand)
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import type { Argv } from "yargs"
|
||||
import { cmd } from "../../../cli/cmd/cmd"
|
||||
import { UI } from "../../../cli/ui"
|
||||
import { Auth } from "../../../auth"
|
||||
import { fetchBalance, fetchProfile, type KilocodeBalance, type KilocodeProfile } from "@kilocode/kilo-gateway"
|
||||
|
||||
interface Info {
|
||||
email: string
|
||||
team: string
|
||||
organizationId: string | null
|
||||
balance: number
|
||||
}
|
||||
|
||||
export function payload(input: {
|
||||
profile: KilocodeProfile
|
||||
balance: KilocodeBalance | null
|
||||
organizationId?: string | null
|
||||
}): Info {
|
||||
const org = input.profile.organizations?.find((item) => item.id === input.organizationId)
|
||||
return {
|
||||
email: input.profile.email,
|
||||
team: org?.name ?? "Personal",
|
||||
organizationId: input.organizationId ?? null,
|
||||
balance: input.balance?.balance ?? 0,
|
||||
}
|
||||
}
|
||||
|
||||
export function format(info: Info): string {
|
||||
return [`Account: ${info.email}`, `Team: ${info.team}`, `Balance: $${info.balance.toFixed(2)}`].join("\n")
|
||||
}
|
||||
|
||||
export const BalanceCommand = cmd({
|
||||
command: "balance",
|
||||
describe: "show Kilo account balance",
|
||||
builder: (yargs: Argv) =>
|
||||
yargs.option("json", {
|
||||
describe: "output balance as JSON",
|
||||
type: "boolean",
|
||||
default: false,
|
||||
}),
|
||||
handler: async (args) => {
|
||||
const auth = await Auth.get("kilo")
|
||||
if (!auth || auth.type !== "oauth") {
|
||||
UI.error("Not authenticated with Kilo Gateway")
|
||||
process.exitCode = 1
|
||||
return
|
||||
}
|
||||
|
||||
const org = auth.accountId ?? null
|
||||
const [profile, balance] = await Promise.all([fetchProfile(auth.access), fetchBalance(auth.access, org ?? undefined)])
|
||||
const info = payload({ profile, balance, organizationId: org })
|
||||
|
||||
if (args.json) {
|
||||
console.log(JSON.stringify(info, null, 2))
|
||||
return
|
||||
}
|
||||
|
||||
UI.println(format(info))
|
||||
},
|
||||
})
|
||||
@@ -25,6 +25,7 @@ import { ConfigCommand as ConfigCLICommand } from "../cli/cmd/config"
|
||||
import { PluginCommand } from "../cli/cmd/plug"
|
||||
import { DevSetupCommand, DevAliasCommand } from "./cli/dev-setup"
|
||||
import { RollCallCommand } from "./cli/cmd/roll-call"
|
||||
import { BalanceCommand } from "./cli/cmd/balance"
|
||||
import { HelpCommand } from "./help-command"
|
||||
import { InstallationBuildKind } from "@opencode-ai/core/installation/version"
|
||||
|
||||
@@ -57,6 +58,7 @@ export const commands = [
|
||||
ServeCommand,
|
||||
ModelsCommand,
|
||||
RollCallCommand,
|
||||
BalanceCommand,
|
||||
StatsCommand,
|
||||
ExportCommand,
|
||||
ImportCommand,
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
|
||||
import { format, payload } from "../../../src/kilocode/cli/cmd/balance"
|
||||
|
||||
describe("balance CLI formatting", () => {
|
||||
test("formats personal balance for human output", () => {
|
||||
expect(
|
||||
format({
|
||||
email: "one@example.com",
|
||||
team: "Personal",
|
||||
organizationId: null,
|
||||
balance: 12.345,
|
||||
}),
|
||||
).toBe("Account: one@example.com\nTeam: Personal\nBalance: $12.35")
|
||||
})
|
||||
|
||||
test("formats team balance for human output", () => {
|
||||
expect(
|
||||
format({
|
||||
email: "one@example.com",
|
||||
team: "Team One",
|
||||
organizationId: "org-1",
|
||||
balance: 7,
|
||||
}),
|
||||
).toBe("Account: one@example.com\nTeam: Team One\nBalance: $7.00")
|
||||
})
|
||||
|
||||
test("creates JSON payload", () => {
|
||||
expect(
|
||||
payload({
|
||||
profile: {
|
||||
email: "one@example.com",
|
||||
organizations: [{ id: "org-1", name: "Team One", role: "admin" }],
|
||||
},
|
||||
balance: { balance: 3.5 },
|
||||
organizationId: "org-1",
|
||||
}),
|
||||
).toEqual({
|
||||
email: "one@example.com",
|
||||
team: "Team One",
|
||||
organizationId: "org-1",
|
||||
balance: 3.5,
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -23,6 +23,7 @@ import { ConfigCommand as ConfigCLICommand } from "../../src/cli/cmd/config"
|
||||
import { PluginCommand } from "../../src/cli/cmd/plug"
|
||||
import { DbCommand } from "../../src/cli/cmd/db"
|
||||
import { HelpCommand } from "../../src/kilocode/help-command"
|
||||
import { BalanceCommand } from "../../src/kilocode/cli/cmd/balance"
|
||||
|
||||
// Stand-in for TuiThreadCommand — the real one imports @opentui/solid which
|
||||
// doesn't resolve in the test environment. Only command/describe matter here.
|
||||
@@ -70,6 +71,7 @@ const commands = [
|
||||
DbCommand,
|
||||
ConfigCLICommand,
|
||||
PluginCommand,
|
||||
BalanceCommand,
|
||||
HelpCommand,
|
||||
CompletionStub,
|
||||
] as any[]
|
||||
@@ -77,7 +79,7 @@ const commands = [
|
||||
describe("kilo help --all (markdown)", () => {
|
||||
test("contains ## heading for each known top-level command", async () => {
|
||||
const output = await generateHelp({ all: true, format: "md", commands })
|
||||
for (const cmd of ["run", "auth", "debug", "mcp", "session", "agent"]) {
|
||||
for (const cmd of ["run", "auth", "debug", "mcp", "session", "agent", "balance"]) {
|
||||
expect(output).toContain(`## kilo ${cmd}`)
|
||||
}
|
||||
})
|
||||
@@ -99,7 +101,7 @@ describe("kilo help --all (text)", () => {
|
||||
|
||||
test("still contains each command name", async () => {
|
||||
const output = await generateHelp({ all: true, format: "text", commands })
|
||||
for (const cmd of ["run", "auth", "debug", "mcp", "session", "agent"]) {
|
||||
for (const cmd of ["run", "auth", "debug", "mcp", "session", "agent", "balance"]) {
|
||||
expect(output).toContain(`kilo ${cmd}`)
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user