@flamecast/cli 0.1.0

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.
package/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # flamecast
2
+
3
+ Command-line client for [Flamecast](https://flamecast.dev). Manage cloud agents and sessions from your terminal.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ git clone https://github.com/smithery-ai/flamecast-cli
9
+ cd flamecast-cli
10
+ bun install
11
+ bun run build # compiles dist/flamecast
12
+ ```
13
+
14
+ Requires [Bun](https://bun.sh) for source builds. The compiled binary runs standalone.
15
+
16
+ ## Quickstart
17
+
18
+ ```bash
19
+ flamecast login # browser sign-in, stores an API key locally
20
+ flamecast whoami
21
+ flamecast agents
22
+ flamecast sessions
23
+ ```
24
+
25
+ ## Commands
26
+
27
+ | Command | What it does |
28
+ |---|---|
29
+ | `flamecast login` | Sign in via the browser. Stores an API key. |
30
+ | `flamecast logout` | Clear the local API key. |
31
+ | `flamecast whoami` | Print the currently authed identity. |
32
+ | `flamecast config` | Show config file path + base URL. |
33
+ | `flamecast agents` | List agents in your workspace. |
34
+ | `flamecast sessions` | List recent sessions. |
35
+ | `flamecast sessions create --input <text>` | Launch a Think session. |
36
+ | `flamecast sessions get <sessionId>` | Show one session. |
37
+ | `flamecast sessions events <sessionId>` | Dump the event log. |
38
+
39
+ ### Launching a session
40
+
41
+ ```bash
42
+ flamecast sessions create --input "summarize my open Linear tickets"
43
+ ```
44
+
45
+ By default this runs on Flamecast's free tier: the server fills in
46
+ `runtime.auth` from its own AI Gateway key. The free tier currently allows
47
+ `anthropic/claude-haiku-4-5` only, with a monthly cap per org (10 sessions
48
+ and $5 of gateway spend). Set `AI_GATEWAY_API_KEY` to bring your own key
49
+ and bypass the cap.
50
+
51
+ Flags on `sessions create`:
52
+
53
+ | Flag | Default | Notes |
54
+ |---|---|---|
55
+ | `--input <text>` | _(required)_ | First message to the agent. |
56
+ | `--model <id>` | `anthropic/claude-haiku-4-5` | Free tier supports this model only; BYOK keys can use any Think-supported model. |
57
+ | `--agent-id <id>` | _none_ | Launch a saved agent instead of an inline runtime. |
58
+ | `--async` | off | Return immediately; poll `sessions get` / `sessions events` for progress. |
59
+
60
+ ## Configuration
61
+
62
+ Credentials live at `~/.config/flamecast/config.json` (respects `$XDG_CONFIG_HOME`). Delete the file or run `flamecast logout` to clear.
63
+
64
+ | Env var | Default | Notes |
65
+ |---|---|---|
66
+ | `FLAMECAST_URL` | `https://flamecast.dev` | Point at a different worker. |
67
+ | `AI_GATEWAY_API_KEY` | _none_ | Optional. When set, `sessions create` uses BYOK and bypasses the free-tier cap. |
68
+
69
+ ## Development
70
+
71
+ ```bash
72
+ bun run bin/flamecast.ts <command> # run from source
73
+ bun run typecheck # type-check
74
+ ```
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env bun
2
+ import { dispatch } from "../src/commands.ts"
3
+
4
+ const code = await dispatch(process.argv.slice(2))
5
+ process.exit(code)
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@flamecast/cli",
3
+ "version": "0.1.0",
4
+ "description": "Command-line client for Flamecast. Drives the device-auth flow and wraps the public REST API.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "bin": {
8
+ "flamecast": "./bin/flamecast.ts"
9
+ },
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "files": [
14
+ "bin",
15
+ "src",
16
+ "README.md"
17
+ ],
18
+ "scripts": {
19
+ "dev": "bun run bin/flamecast.ts",
20
+ "build": "bun build bin/flamecast.ts --compile --outfile dist/flamecast",
21
+ "typecheck": "bun --bun tsc --noEmit"
22
+ },
23
+ "engines": {
24
+ "bun": ">=1.1"
25
+ },
26
+ "devDependencies": {
27
+ "@types/bun": "latest",
28
+ "typescript": "^5.5.0"
29
+ }
30
+ }
package/src/auth.ts ADDED
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Device-flow login. Mirrors the Smithery CLI handshake:
3
+ * POST /api/auth/cli/session → { sessionId, authUrl }
4
+ * GET /api/auth/cli/poll/:id → pending | success(apiKey, organization)
5
+ *
6
+ * Bun's built-in fetch + a 2s poll loop. No external deps.
7
+ */
8
+ import { configBaseUrl, writeConfig, type FlamecastConfig } from "./config.ts"
9
+
10
+ const POLL_INTERVAL_MS = 2000
11
+ const TIMEOUT_MS = 5 * 60 * 1000
12
+
13
+ interface SessionResponse {
14
+ sessionId: string
15
+ authUrl: string
16
+ }
17
+
18
+ type PollResponse =
19
+ | { status: "pending" }
20
+ | { status: "success"; apiKey: string; organization?: { id: string } }
21
+ | { status: "error"; message: string }
22
+
23
+ async function createSession(baseUrl: string): Promise<SessionResponse> {
24
+ const r = await fetch(`${baseUrl}/api/auth/cli/session`, {
25
+ method: "POST",
26
+ headers: { "content-type": "application/json" },
27
+ body: JSON.stringify({}),
28
+ })
29
+ if (!r.ok) throw new Error(`session create: ${r.status} ${r.statusText}`)
30
+ return (await r.json()) as SessionResponse
31
+ }
32
+
33
+ async function poll(baseUrl: string, sessionId: string): Promise<PollResponse> {
34
+ const r = await fetch(`${baseUrl}/api/auth/cli/poll/${sessionId}`)
35
+ if (r.status === 404) {
36
+ return { status: "error", message: "session expired" }
37
+ }
38
+ if (!r.ok) {
39
+ return { status: "error", message: `poll: ${r.status} ${r.statusText}` }
40
+ }
41
+ return (await r.json()) as PollResponse
42
+ }
43
+
44
+ async function openBrowser(url: string): Promise<void> {
45
+ const platform = process.platform
46
+ const cmd =
47
+ platform === "darwin" ? ["open", url] :
48
+ platform === "win32" ? ["cmd", "/c", "start", "", url] :
49
+ ["xdg-open", url]
50
+ try {
51
+ const proc = Bun.spawn(cmd, { stdout: "ignore", stderr: "ignore" })
52
+ await proc.exited
53
+ } catch {
54
+ // Browser open is best-effort; URL is already printed.
55
+ }
56
+ }
57
+
58
+ export async function login(): Promise<FlamecastConfig> {
59
+ const baseUrl = configBaseUrl()
60
+ process.stderr.write("Preparing authentication…\n")
61
+ const session = await createSession(baseUrl)
62
+ process.stderr.write("\nSign in to authorize the Flamecast CLI:\n")
63
+ process.stderr.write(` ${session.authUrl}\n\n`)
64
+ await openBrowser(session.authUrl)
65
+
66
+ const deadline = Date.now() + TIMEOUT_MS
67
+ let dots = 0
68
+ while (Date.now() < deadline) {
69
+ const result = await poll(baseUrl, session.sessionId)
70
+ if (result.status === "success") {
71
+ process.stderr.write("\nAuthorized.\n")
72
+ const config: FlamecastConfig = {
73
+ baseUrl,
74
+ apiKey: result.apiKey,
75
+ organization: result.organization,
76
+ updatedAt: new Date().toISOString(),
77
+ }
78
+ await writeConfig(config)
79
+ return config
80
+ }
81
+ if (result.status === "error") {
82
+ throw new Error(`Authentication failed: ${result.message}`)
83
+ }
84
+ dots = (dots + 1) % 4
85
+ process.stderr.write(`\rWaiting for authorization${".".repeat(dots).padEnd(3)}`)
86
+ await Bun.sleep(POLL_INTERVAL_MS)
87
+ }
88
+ throw new Error("Authentication timed out after 5 minutes. Re-run `flamecast login`.")
89
+ }
@@ -0,0 +1,97 @@
1
+ import { login } from "./auth.ts"
2
+ import { clearConfig, configBaseUrl, configPath, readConfig } from "./config.ts"
3
+ import { sessions } from "./sessions.ts"
4
+ import { whoami } from "./whoami.ts"
5
+
6
+ const USAGE = `flamecast — programmable cloud agents
7
+
8
+ Usage:
9
+ flamecast login Sign in via your browser, store an API key
10
+ flamecast logout Clear the local API key
11
+ flamecast whoami Print the currently authed identity
12
+ flamecast config Show the local config file path + base URL
13
+ flamecast agents List agents in your workspace
14
+ flamecast sessions List recent sessions
15
+ flamecast sessions create --input <text> Launch a Think session
16
+ flamecast sessions get <sessionId> Show one session
17
+ flamecast sessions events <sessionId> Dump the event log
18
+
19
+ Environment:
20
+ FLAMECAST_URL Override the API base URL (default: https://flamecast.dev)
21
+ AI_GATEWAY_API_KEY Optional. BYOK key to bypass the free-tier cap
22
+ `
23
+
24
+ async function requireAuth() {
25
+ const config = await readConfig()
26
+ if (!config?.apiKey) {
27
+ process.stderr.write("Not signed in. Run `flamecast login` first.\n")
28
+ process.exit(1)
29
+ }
30
+ return config
31
+ }
32
+
33
+ export async function dispatch(argv: string[]): Promise<number> {
34
+ const cmd = argv[0]
35
+ switch (cmd) {
36
+ case undefined:
37
+ case "help":
38
+ case "--help":
39
+ case "-h":
40
+ process.stdout.write(USAGE)
41
+ return 0
42
+
43
+ case "login": {
44
+ await login()
45
+ return 0
46
+ }
47
+
48
+ case "logout": {
49
+ const removed = await clearConfig()
50
+ process.stderr.write(
51
+ removed ? "Signed out.\n" : "No active session.\n",
52
+ )
53
+ return removed ? 0 : 1
54
+ }
55
+
56
+ case "whoami": {
57
+ const me = await whoami()
58
+ if (!me.authed) {
59
+ process.stderr.write("Not signed in.\n")
60
+ return 1
61
+ }
62
+ process.stdout.write(`${me.email ?? me.name ?? me.workosUserId}\n`)
63
+ if (me.orgId) process.stdout.write(`org: ${me.orgId}\n`)
64
+ return 0
65
+ }
66
+
67
+ case "config": {
68
+ process.stdout.write(`baseUrl: ${configBaseUrl()}\n`)
69
+ process.stdout.write(`config: ${configPath()}\n`)
70
+ const config = await readConfig()
71
+ process.stdout.write(`status: ${config?.apiKey ? "signed in" : "signed out"}\n`)
72
+ return 0
73
+ }
74
+
75
+ case "agents": {
76
+ const config = await requireAuth()
77
+ const r = await fetch(`${config.baseUrl}/agents`, {
78
+ headers: { authorization: `Bearer ${config.apiKey}` },
79
+ })
80
+ if (!r.ok) {
81
+ process.stderr.write(`list agents: ${r.status} ${r.statusText}\n`)
82
+ return 1
83
+ }
84
+ process.stdout.write(`${JSON.stringify(await r.json(), null, 2)}\n`)
85
+ return 0
86
+ }
87
+
88
+ case "sessions": {
89
+ return sessions(argv.slice(1))
90
+ }
91
+
92
+ default: {
93
+ process.stderr.write(`Unknown command: ${cmd}\n\n${USAGE}`)
94
+ return 2
95
+ }
96
+ }
97
+ }
package/src/config.ts ADDED
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Local credential store. Plain JSON at $XDG_CONFIG_HOME/flamecast/config.json
3
+ * (falling back to ~/.config/flamecast/config.json). Same shape Stripe and
4
+ * gh use — a single file with the active profile, easy to inspect and rm.
5
+ */
6
+ import { mkdir } from "node:fs/promises"
7
+ import { homedir } from "node:os"
8
+ import { dirname, join } from "node:path"
9
+
10
+ export interface FlamecastConfig {
11
+ baseUrl: string
12
+ apiKey?: string
13
+ organization?: { id: string }
14
+ updatedAt: string
15
+ }
16
+
17
+ const DEFAULT_BASE_URL = "https://flamecast.dev"
18
+
19
+ export function configBaseUrl(): string {
20
+ return process.env.FLAMECAST_URL ?? DEFAULT_BASE_URL
21
+ }
22
+
23
+ export function configPath(): string {
24
+ const xdg = process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config")
25
+ return join(xdg, "flamecast", "config.json")
26
+ }
27
+
28
+ export async function readConfig(): Promise<FlamecastConfig | null> {
29
+ const path = configPath()
30
+ const file = Bun.file(path)
31
+ if (!(await file.exists())) return null
32
+ try {
33
+ return (await file.json()) as FlamecastConfig
34
+ } catch {
35
+ return null
36
+ }
37
+ }
38
+
39
+ export async function writeConfig(config: FlamecastConfig): Promise<void> {
40
+ const path = configPath()
41
+ await mkdir(dirname(path), { recursive: true })
42
+ await Bun.write(path, `${JSON.stringify(config, null, 2)}\n`)
43
+ }
44
+
45
+ export async function clearConfig(): Promise<boolean> {
46
+ const path = configPath()
47
+ const file = Bun.file(path)
48
+ if (!(await file.exists())) return false
49
+ await Bun.write(path, "")
50
+ // Bun.write with empty string truncates but leaves the file. Remove
51
+ // fully so a stale empty config can't confuse readConfig.
52
+ const fs = await import("node:fs/promises")
53
+ await fs.unlink(path).catch(() => undefined)
54
+ return true
55
+ }
@@ -0,0 +1,179 @@
1
+ /**
2
+ * `flamecast sessions` subcommands: list, create, get, events.
3
+ */
4
+ import { readConfig } from "./config.ts"
5
+
6
+ interface ParsedFlags {
7
+ positional: string[]
8
+ flags: Record<string, string | boolean>
9
+ }
10
+
11
+ function parseFlags(argv: string[]): ParsedFlags {
12
+ const positional: string[] = []
13
+ const flags: Record<string, string | boolean> = {}
14
+ for (let i = 0; i < argv.length; i++) {
15
+ const a = argv[i]
16
+ if (a.startsWith("--")) {
17
+ const key = a.slice(2)
18
+ const next = argv[i + 1]
19
+ if (next && !next.startsWith("--")) {
20
+ flags[key] = next
21
+ i++
22
+ } else {
23
+ flags[key] = true
24
+ }
25
+ } else {
26
+ positional.push(a)
27
+ }
28
+ }
29
+ return { positional, flags }
30
+ }
31
+
32
+ async function authed() {
33
+ const config = await readConfig()
34
+ if (!config?.apiKey) {
35
+ process.stderr.write("Not signed in. Run `flamecast login` first.\n")
36
+ process.exit(1)
37
+ }
38
+ return config
39
+ }
40
+
41
+ function printJson(data: unknown) {
42
+ process.stdout.write(`${JSON.stringify(data, null, 2)}\n`)
43
+ }
44
+
45
+ async function list(): Promise<number> {
46
+ const config = await authed()
47
+ const r = await fetch(`${config.baseUrl}/sessions`, {
48
+ headers: { authorization: `Bearer ${config.apiKey}` },
49
+ })
50
+ if (!r.ok) {
51
+ process.stderr.write(`list sessions: ${r.status} ${r.statusText}\n`)
52
+ return 1
53
+ }
54
+ printJson(await r.json())
55
+ return 0
56
+ }
57
+
58
+ async function get(id: string | undefined): Promise<number> {
59
+ if (!id) {
60
+ process.stderr.write("usage: flamecast sessions get <sessionId>\n")
61
+ return 2
62
+ }
63
+ const config = await authed()
64
+ const r = await fetch(`${config.baseUrl}/sessions/${id}`, {
65
+ headers: { authorization: `Bearer ${config.apiKey}` },
66
+ })
67
+ if (r.status === 404) {
68
+ process.stderr.write("session not found\n")
69
+ return 1
70
+ }
71
+ if (!r.ok) {
72
+ process.stderr.write(`get session: ${r.status} ${r.statusText}\n`)
73
+ return 1
74
+ }
75
+ printJson(await r.json())
76
+ return 0
77
+ }
78
+
79
+ async function events(id: string | undefined): Promise<number> {
80
+ if (!id) {
81
+ process.stderr.write("usage: flamecast sessions events <sessionId>\n")
82
+ return 2
83
+ }
84
+ const config = await authed()
85
+ const r = await fetch(
86
+ `${config.baseUrl}/sessions/${id}/events?limit=500`,
87
+ { headers: { authorization: `Bearer ${config.apiKey}` } },
88
+ )
89
+ if (!r.ok) {
90
+ process.stderr.write(`events: ${r.status} ${r.statusText}\n`)
91
+ return 1
92
+ }
93
+ printJson(await r.json())
94
+ return 0
95
+ }
96
+
97
+ async function create(flags: Record<string, string | boolean>): Promise<number> {
98
+ const input = typeof flags.input === "string" ? flags.input : undefined
99
+ if (!input) {
100
+ process.stderr.write(
101
+ "usage: flamecast sessions create --input <text> [--model <id>] [--agent-id <id>] [--async]\n",
102
+ )
103
+ return 2
104
+ }
105
+ const model = typeof flags.model === "string" ? flags.model : "anthropic/claude-haiku-4-5"
106
+ const agentId = typeof flags["agent-id"] === "string" ? flags["agent-id"] : undefined
107
+ const asyncFlag = flags.async === true || flags.async === "true"
108
+ const gatewayKey = process.env.AI_GATEWAY_API_KEY
109
+ const config = await authed()
110
+
111
+ const body: Record<string, unknown> = { input, async: asyncFlag }
112
+ if (agentId) {
113
+ body.agentId = agentId
114
+ } else {
115
+ // runtime.auth is optional: the server fills it from its free-tier key
116
+ // (subject to monthly caps) when omitted and the model is on the free
117
+ // allowlist. Pass AI_GATEWAY_API_KEY to bypass the cap with BYOK.
118
+ const runtime: Record<string, unknown> = {
119
+ id: "think",
120
+ config: { model, toolMode: "mcp" },
121
+ }
122
+ if (gatewayKey) {
123
+ runtime.auth = { type: "api_key", key: gatewayKey }
124
+ }
125
+ body.agent = { runtime }
126
+ }
127
+
128
+ const r = await fetch(`${config.baseUrl}/sessions`, {
129
+ method: "POST",
130
+ headers: {
131
+ authorization: `Bearer ${config.apiKey}`,
132
+ "content-type": "application/json",
133
+ },
134
+ body: JSON.stringify(body),
135
+ })
136
+ if (!r.ok) {
137
+ const text = await r.text().catch(() => r.statusText)
138
+ process.stderr.write(`create session: ${r.status} ${text}\n`)
139
+ return 1
140
+ }
141
+ printJson(await r.json())
142
+ return 0
143
+ }
144
+
145
+ const SESSIONS_USAGE = `flamecast sessions <verb>
146
+
147
+ Verbs:
148
+ list List recent sessions
149
+ create --input <text> [--model M] Launch a Think session
150
+ [--agent-id ID] [--async]
151
+ get <sessionId> Show one session
152
+ events <sessionId> Dump the event log
153
+
154
+ Env:
155
+ AI_GATEWAY_API_KEY Optional. BYOK key to bypass the free-tier cap.
156
+ `
157
+
158
+ export async function sessions(argv: string[]): Promise<number> {
159
+ const { positional, flags } = parseFlags(argv)
160
+ const verb = positional[0] ?? "list"
161
+ switch (verb) {
162
+ case "list":
163
+ return list()
164
+ case "create":
165
+ return create(flags)
166
+ case "get":
167
+ return get(positional[1])
168
+ case "events":
169
+ return events(positional[1])
170
+ case "help":
171
+ case "--help":
172
+ case "-h":
173
+ process.stdout.write(SESSIONS_USAGE)
174
+ return 0
175
+ default:
176
+ process.stderr.write(`Unknown sessions verb: ${verb}\n\n${SESSIONS_USAGE}`)
177
+ return 2
178
+ }
179
+ }
package/src/whoami.ts ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Calls /auth/me on the configured base URL using the stored API key.
3
+ * Used by `flamecast whoami` and as a probe before mutating commands.
4
+ */
5
+ import { readConfig } from "./config.ts"
6
+
7
+ export interface WhoamiResult {
8
+ authed: boolean
9
+ email?: string | null
10
+ name?: string | null
11
+ workosUserId?: string
12
+ orgId?: string
13
+ }
14
+
15
+ export async function whoami(): Promise<WhoamiResult> {
16
+ const config = await readConfig()
17
+ if (!config?.apiKey) return { authed: false }
18
+ const r = await fetch(`${config.baseUrl}/auth/me`, {
19
+ headers: { authorization: `Bearer ${config.apiKey}` },
20
+ })
21
+ if (!r.ok) return { authed: false }
22
+ return (await r.json()) as WhoamiResult
23
+ }