Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/run-cloud-agent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kilocode/cli": minor
---

Run asynchronous Cloud Agent tasks with repository, model, mode, and organization defaults through `kilo cloud`. Add `--stream` to `kilo cloud start` to print admission output and then stream WebSocket events as JSONL until completion or inactivity ends the stream.
1 change: 1 addition & 0 deletions packages/kilo-docs/lychee.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ exclude = [
'^https?://vercel\.link/',
# API base URL, returns 404 when fetched directly
'^https?://api\.apertis\.ai/v1/?$',
'^https?://cloud-agent-next\.kilosessions\.ai/?$',
# Redirects to authenticated Google Cloud console
'^https?://console\.cloud\.google\.com',
# Google AI Studio API keys page redirects to Google sign-in
Expand Down
1 change: 1 addition & 0 deletions packages/kilo-docs/markdoc/partials/cli-commands-table.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

70 changes: 70 additions & 0 deletions packages/kilo-docs/pages/code-with-ai/platforms/cli-reference.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions packages/kilo-docs/source-links.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@
<!-- packages/opencode/src/provider/error.ts -->
- <https://cli.github.com/>
<!-- packages/kilo-vscode/src/agent-manager/WorktreeManager.ts -->
- <https://cloud-agent-next.kilosessions.ai>
<!-- packages/opencode/src/kilocode/cloud/origin.ts -->
- <https://cloud.digitalocean.com/v1/oauth/authorize>
<!-- packages/opencode/src/plugin/digitalocean.ts -->
- <https://cloudflare.com/cdn-cgi/trace>
Expand Down Expand Up @@ -106,6 +108,7 @@
<!-- packages/opencode/src/plugin/digitalocean.ts -->
- <https://kilo.ai>
<!-- packages/opencode/src/cli/cmd/github.handler.ts -->
<!-- packages/opencode/src/kilocode/cloud/origin.ts -->
<!-- packages/opencode/src/mcp/oauth-provider.ts -->
<!-- packages/opencode/src/session/network.ts -->
- <https://kilo.ai/>
Expand Down
134 changes: 134 additions & 0 deletions packages/opencode/src/kilocode/cli/cmd/cloud.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import type { Argv } from "yargs"
import { Effect } from "effect"
import { cmd } from "@/cli/cmd/cmd"
import { effectCmd } from "@/cli/effect-cmd"
import { CloudCommands } from "@/kilocode/cloud/commands"

export const CloudStartCommand = effectCmd({
command: "start",
describe: "start a Cloud Agent task",
builder: (yargs) =>
yargs
.option("prompt", {
type: "string",
demandOption: true,
describe: "prompt for the Cloud Agent",
})
.option("repo", {
type: "string",
describe: "repository shorthand or URL",
})
.option("repo-type", {
type: "string",
choices: ["github", "gitlab", "git"] as const,
describe: "repository provider type",
})
.option("branch", {
type: "string",
describe: "repository branch",
})
.option("model", {
type: "string",
describe: "Cloud Agent model",
})
.option("mode", {
type: "string",
describe: "Cloud Agent mode",
})
.option("org-id", {
type: "string",
describe: "Kilo organization ID",
})
.option("stream", {
type: "boolean",
describe: "connect to the WebSocket stream and print events as JSONL",
}),
handler: Effect.fn("Cli.cloud.start")(function* (args) {
yield* CloudCommands.start({
prompt: args.prompt,
...(args.repo === undefined ? {} : { repo: args.repo }),
...(args.repoType === undefined ? {} : { repoType: args.repoType }),
...(args.branch === undefined ? {} : { branch: args.branch }),
...(args.model === undefined ? {} : { model: args.model }),
...(args.mode === undefined ? {} : { mode: args.mode }),
...(args.orgId === undefined ? {} : { orgID: args.orgId }),
...(args.stream === undefined ? {} : { stream: args.stream }),
})
}),
})

export const CloudSendCommand = effectCmd({
command: "send",
describe: "send a follow-up prompt to a Cloud Agent task",
instance: false,
builder: (yargs) =>
yargs
.option("session-id", {
type: "string",
demandOption: true,
describe: "Cloud Agent session ID",
})
.option("prompt", {
type: "string",
demandOption: true,
describe: "follow-up prompt for the Cloud Agent",
}),
handler: Effect.fn("Cli.cloud.send")(function* (args) {
yield* CloudCommands.send({ sessionID: args.sessionId, prompt: args.prompt })
}),
})

export const CloudStatusCommand = effectCmd({
command: "status",
describe: "show Cloud Agent task status",
instance: false,
builder: (yargs) =>
yargs
.option("session-id", {
type: "string",
demandOption: true,
describe: "Cloud Agent session ID",
})
.option("message-id", {
type: "string",
demandOption: true,
describe: "Cloud Agent message ID",
}),
handler: Effect.fn("Cli.cloud.status")(function* (args) {
yield* CloudCommands.status({ sessionID: args.sessionId, messageID: args.messageId })
}),
})

export const CloudResultCommand = effectCmd({
command: "result",
describe: "show a Cloud Agent task result",
instance: false,
builder: (yargs) =>
yargs
.option("session-id", {
type: "string",
demandOption: true,
describe: "Cloud Agent session ID",
})
.option("message-id", {
type: "string",
demandOption: true,
describe: "Cloud Agent message ID",
}),
handler: Effect.fn("Cli.cloud.result")(function* (args) {
yield* CloudCommands.result({ sessionID: args.sessionId, messageID: args.messageId })
}),
})

export const CloudCommand = cmd({
command: "cloud",
describe: "run Cloud Agent tasks",
builder: (yargs: Argv) =>
yargs
.command(CloudStartCommand)
.command(CloudSendCommand)
.command(CloudStatusCommand)
.command(CloudResultCommand)
.demandCommand(),
async handler() {},
})
2 changes: 2 additions & 0 deletions packages/opencode/src/kilocode/cli/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { SessionExport } from "@/kilocode/session-export"
import { KiloShutdown } from "@/kilocode/cli/shutdown"
import { createHelpCommand } from "@/kilocode/help-command"
import { KiloConsoleCommand } from "@/kilocode/cli/cmd/console"
import { CloudCommand } from "@/kilocode/cli/cmd/cloud"
import { RollCallCommand } from "@/kilocode/cli/cmd/roll-call"
import { ProfileCommand } from "@/kilocode/cli/cmd/profile"
import { DaemonCommand } from "@/kilocode/cli/cmd/daemon"
Expand All @@ -32,6 +33,7 @@ export namespace KiloCli {
export function register<T>(cli: Argv<T>): Argv<T> {
cli
.command(KiloConsoleCommand)
.command(CloudCommand)
.command(RollCallCommand)
.command(ProfileCommand)
.command(RemoteCommand)
Expand Down
66 changes: 66 additions & 0 deletions packages/opencode/src/kilocode/cloud/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Auth } from "@/auth"
import { Effect, Redacted, Schema } from "effect"
import z from "zod"

export namespace CloudAuth {
export type Environment = Readonly<Record<string, string | undefined>>

export interface Input {
readonly orgID?: string
readonly env?: Environment
}

export interface Resolved {
readonly token: Redacted.Redacted
readonly organizationID?: string
}

export class ResolutionError extends Schema.TaggedErrorClass<ResolutionError>()("CloudAuthResolutionError", {
kind: Schema.Literals(["missing", "organization"]),
message: Schema.String,
}) {}

const Uuid = z.uuid()

const credentials = Effect.fn("CloudAuth.credentials")(function* (env: Environment) {
const service = yield* Auth.Service
const info = yield* service.get("kilo")
const stored = info?.type === "api" ? info.key.trim() : info?.type === "oauth" ? info.access.trim() : undefined
const fallback = env.KILO_API_KEY?.trim()
const value = stored || fallback
if (!value) {
return yield* Effect.fail(
new ResolutionError({
kind: "missing",
message: "Kilo credentials are required; run `kilo auth login`",
}),
)
}
return { token: Redacted.make(value), accountID: info?.type === "oauth" ? info.accountId : undefined }
})

export const token = Effect.fn("CloudAuth.token")(function* (env: Environment = process.env) {
return (yield* credentials(env)).token
})

export const resolve = Effect.fn("CloudAuth.resolve")(function* (input: Input = {}) {
const env = input.env ?? process.env
const auth = yield* credentials(env)
const explicit = input.orgID
const setting = env.KILO_ORG_ID?.trim()
const organizationID = explicit !== undefined ? explicit : setting || auth.accountID
if (organizationID !== undefined && !Uuid.safeParse(organizationID).success) {
return yield* Effect.fail(
new ResolutionError({
kind: "organization",
message: "Kilo organization ID must be a valid UUID",
}),
)
}

return {
token: auth.token,
...(organizationID ? { organizationID } : {}),
} satisfies Resolved
})
}
Loading
Loading