@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 +74 -0
- package/bin/flamecast.ts +5 -0
- package/package.json +30 -0
- package/src/auth.ts +89 -0
- package/src/commands.ts +97 -0
- package/src/config.ts +55 -0
- package/src/sessions.ts +179 -0
- package/src/whoami.ts +23 -0
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
|
+
```
|
package/bin/flamecast.ts
ADDED
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
|
+
}
|
package/src/commands.ts
ADDED
|
@@ -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
|
+
}
|
package/src/sessions.ts
ADDED
|
@@ -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
|
+
}
|