import { r as __exportAll } from "./chunk-D_gEzPfs.js";
import { g as resolveStateDir } from "./paths-VdmjGGyc.js";
import { n as isRich, r as theme } from "./theme-C5EwWDWE.js";
import { b as sleep } from "./utils-D019Z2NU.js";
import { t as VERSION } from "./version-BKRSzdyx.js";
import { o as loadConfig, u as writeConfigFile } from "./config-ZjEd8561.js";
import { h as GATEWAY_CLIENT_NAMES, m as GATEWAY_CLIENT_MODES } from "./message-channel-CaDAAMe0.js";
import { r as loadOrCreateDeviceIdentity } from "./device-identity-CpMBJtRJ.js";
import { Ot as PROTOCOL_VERSION, n as GatewayClient } from "./client-BM6xyPUD.js";
import { t as ensureLocalGatewayRunning } from "./ensure-local-gateway-CWMjpIBa.js";
import { existsSync } from "node:fs";
import path, { join } from "node:path";
import os, { homedir, hostname } from "node:os";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { spawn } from "node:child_process";
import { createHash, randomBytes } from "node:crypto";

//#region src/cli/spinner.ts
/**
* @module cli/spinner
*
* A tiny, dependency-free animated spinner + structured step printer for CLI
* flows (login / onboarding). TTY-aware: in a rich interactive terminal it
* animates a braille spinner on a single rewritten line; in a plain/piped
* stream it degrades to clean static lines (no escape soup in captured logs).
*
* Visual language (shared across the CLI so flows feel consistent):
*   ◆  section header (accent)
*   ⠙… animated while a step runs (accent)
*   ✓  success (green)   ⚠ warning (amber)   ✗ failure (red)   ›  hint (muted)
*/
const FRAMES = [
	"⠋",
	"⠙",
	"⠹",
	"⠸",
	"⠼",
	"⠴",
	"⠦",
	"⠧",
	"⠇",
	"⠏"
];
const FRAME_MS = 80;
const richTty = () => isRich() && Boolean(process.stdout.isTTY);
/** Print a section header — groups related steps under a labelled heading. */
function section(label) {
	const bullet = theme.accent("◆");
	process.stdout.write(`\n  ${bullet} ${theme.heading(label)}\n`);
}
/** Print a static, already-resolved line (no animation). */
function line(icon, text) {
	process.stdout.write(`    ${icon} ${text}\n`);
}
/** A muted, indented hint/sub-line under a step. */
function hint(text) {
	process.stdout.write(`      ${theme.muted(text)}\n`);
}
/** A live step: animates while running, then resolves to ✓ / ⚠ / ✗. */
var Step = class {
	constructor(label) {
		this.timer = null;
		this.frame = 0;
		this.rich = richTty();
		this.done = false;
		this.label = label;
	}
	start() {
		if (!this.rich) {
			process.stdout.write(`    ${theme.accentBright("›")} ${this.label}…\n`);
			return this;
		}
		process.stdout.write("\x1B[?25l");
		this.render();
		this.timer = setInterval(() => {
			this.frame = (this.frame + 1) % FRAMES.length;
			this.render();
		}, FRAME_MS);
		return this;
	}
	/** Update the running label in place. */
	update(label) {
		this.label = label;
		if (!this.rich) process.stdout.write(`    ${theme.accentBright("›")} ${label}…\n`);
	}
	render() {
		const spin = theme.accentBright(FRAMES[this.frame]);
		process.stdout.write(`\r\x1b[K    ${spin} ${theme.info(this.label)}…`);
	}
	finish(icon, text) {
		if (this.done) return;
		this.done = true;
		if (this.timer) {
			clearInterval(this.timer);
			this.timer = null;
		}
		if (this.rich) {
			process.stdout.write(`\r\x1b[K    ${icon} ${text}\n`);
			process.stdout.write("\x1B[?25h");
		} else process.stdout.write(`    ${icon} ${text}\n`);
	}
	succeed(text = this.label) {
		this.finish(theme.success("✓"), text);
	}
	warn(text = this.label) {
		this.finish(theme.warn("⚠"), theme.warn(text));
	}
	fail(text = this.label) {
		this.finish(theme.error("✗"), theme.error(text));
	}
	/** Stop the animation and erase the line, printing nothing — for when the
	* caller will render its own terminal message. */
	clear() {
		if (this.done) return;
		this.done = true;
		if (this.timer) {
			clearInterval(this.timer);
			this.timer = null;
		}
		if (this.rich) process.stdout.write("\r\x1B[K\x1B[?25h");
	}
};
/** Start an animated step. */
function step(label) {
	return new Step(label).start();
}
/** Briefly hold so a fast step still reads as "work happening" (rich TTY only). */
async function settle(ms = 280) {
	if (richTty()) await sleep(ms);
}

//#endregion
//#region src/commands/request-device-permissions.ts
/**
* @module commands/request-device-permissions
*
* After a device is claimed (koi login), surface the OS-level permissions the koi
* needs ALL AT ONCE — instead of the user hitting "screenshot unavailable" or a
* surprise prompt mid-task later.
*
* macOS is the one that actually gates these (Screen Recording, Accessibility,
* Camera, Microphone via TCC). We register this binary with the Screen Recording
* service (one capture attempt) so it appears as a toggle, then open the Privacy
* pane so the user can grant everything in one sitting. Windows only gates camera
* + microphone; Linux gates none of these.
*/
function runDetached(cmd, args) {
	return new Promise((resolve) => {
		try {
			const child = spawn(cmd, args, {
				stdio: "ignore",
				timeout: 9e3
			});
			child.on("close", () => resolve());
			child.on("error", () => resolve());
		} catch {
			resolve();
		}
	});
}
async function requestDevicePermissions() {
	try {
		if (process.platform === "darwin") await requestMacPermissions();
		else if (process.platform === "win32") await requestWindowsPermissions();
	} catch {}
}
async function requestMacPermissions() {
	await runDetached(["/usr/sbin/screencapture", "/usr/bin/screencapture"].find((p) => {
		try {
			return existsSync(p);
		} catch {
			return false;
		}
	}) ?? "screencapture", ["-x", path.join(os.tmpdir(), "skykoi-perm-register.png")]);
	await runDetached("open", ["x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture"]);
	await runDetached("open", ["x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles"]);
	section("Permissions");
	line(theme.accentBright("›"), "Turn these ON in the System Settings · Privacy windows that just opened:");
	line(theme.muted("•"), `${theme.info("Full Disk Access")}     ${theme.muted("— so it can get to know you (messages, mail, files)")}`);
	line(theme.muted("•"), `${theme.info("Screen Recording")}     ${theme.muted("— so your koi can see your screen")}`);
	line(theme.muted("•"), `${theme.info("Accessibility")}        ${theme.muted("— so it can click & type for you")}`);
	line(theme.muted("•"), `${theme.info("Camera + Microphone")}  ${theme.muted("— for vision & calls")}`);
	hint("SkyKoi appears as \"node\" in the list — grant now and you won't be interrupted later.");
}
async function requestWindowsPermissions() {
	await runDetached("cmd", [
		"/c",
		"start",
		"",
		"ms-settings:privacy-webcam"
	]);
	await runDetached("cmd", [
		"/c",
		"start",
		"",
		"ms-settings:privacy-microphone"
	]);
	section("Permissions");
	line(theme.accentBright("›"), "Make sure Camera & Microphone access are ON for apps (the Settings windows just opened).");
	hint("Screen capture needs no prompt on Windows.");
}

//#endregion
//#region src/commands/login.ts
/**
* `skykoi login` — authenticate the CLI with a SkyKoi account.
*
* Two modes:
*
*   skykoi login                  # browser device flow
*   skykoi login --token <token>  # paste an existing CLI token
*
* The device flow:
*   1. Generate a 32-byte random secret S; compute H = sha256(S).
*   2. POST H to /api/cli/auth/init; get back a verification URL.
*   3. Open the URL in the user's browser AND poll /api/cli/auth/poll
*      every 2s with raw S.
*   4. Once the user clicks "Approve" on the web page, poll returns
*      { status: "approved", token, userId, orgId }.
*   5. Write ~/.skykoi/auth.json with the token + scope metadata.
*
* The token (raw "skk_…" string) is what the CLI uses for all
* subsequent platform API calls — passed as `Authorization: Bearer
* <token>`. The platform's requireRoleFromRequest detects the `skk_`
* prefix and resolves the auth context from the CliToken row.
*/
var login_exports = /* @__PURE__ */ __exportAll({
	loginCommand: () => loginCommand,
	readAuthFile: () => readAuthFile
});
/** Mark onboarding done (matches the TUI's `~/.skykoi/onboarded` marker) so the
* chat surface doesn't ALSO kick off its own onboarding pass after login. */
async function markOnboardingComplete() {
	try {
		const dir = resolveStateDir();
		await mkdir(dir, { recursive: true });
		await writeFile(join(dir, "onboarded"), `${VERSION}\t${(/* @__PURE__ */ new Date()).toISOString()}\n`);
	} catch {}
}
/**
* Instruction that proactively starts the device koi's deep onboarding pass. The
* koi already knows HOW (its system prompt's "Onboarding & user dossier"
* section) — this just triggers it right after login, in a side session, so the
* dossier is built BEFORE the user's first message and the koi knows them from
* its very first reply (no investigating mid-conversation).
*/
const ONBOARDING_KICKOFF = "[setup] Run your full onboarding NOW, silently in the background, per your Onboarding & user dossier instructions. Investigate this computer and build the complete USER.md + profile.json: identity; the people who matter (rank by real two-way message volume, then READ each top person's recent thread); the user's voice/writing style (global + per-person); their business/ventures and team; and their current headspace from recent browser/search activity. Do NOT message me about it — just build the dossier so you already know me when I first message you. This is the initial setup pass; keep refining it each session.";
/** A short, human "currently doing" phrase for an onboarding tool call. */
function onboardingActivity(tool) {
	switch (tool) {
		case "exec":
		case "run_code":
		case "bash": return "reading your computer";
		case "email": return "skimming your inbox";
		case "memory_get":
		case "memory_search": return "organizing what I learn";
		default: return "getting the picture";
	}
}
/**
* Run the onboarding pass and BLOCK until it finishes (capped), so the chat opens
* already knowing the user. The koi must NEVER investigate DURING the first
* message — that made a simple "hi" crawl while the onboarding run competed with
* it for the model. We show a clear, live indicator the whole time.
*/
async function runOnboardingBeforeChat(wsUrl, token) {
	const onb = step("Getting to know you");
	let client = null;
	let runId = "";
	let sent = false;
	let succeeded = false;
	try {
		await new Promise((resolve) => {
			let settled = false;
			const finish = (ok) => {
				if (settled) return;
				settled = true;
				succeeded = ok;
				resolve();
			};
			client = new GatewayClient({
				url: wsUrl,
				token,
				instanceId: randomBytes(8).toString("hex"),
				clientName: GATEWAY_CLIENT_NAMES.CLI,
				clientVersion: "login",
				mode: GATEWAY_CLIENT_MODES.CLI,
				role: "operator",
				scopes: ["operator.admin"],
				deviceIdentity: loadOrCreateDeviceIdentity(),
				minProtocol: PROTOCOL_VERSION,
				maxProtocol: PROTOCOL_VERSION,
				onHelloOk: async () => {
					if (sent) return;
					sent = true;
					try {
						runId = (await client.request("chat.send", {
							sessionKey: "koi:setup:onboarding",
							message: ONBOARDING_KICKOFF,
							deliver: false,
							idempotencyKey: randomBytes(12).toString("hex")
						}))?.runId ?? "";
					} catch {
						finish(false);
					}
				},
				onEvent: (evt) => {
					const payload = evt.payload;
					if (!payload || !runId || payload.runId !== runId) return;
					if (evt.event === "chat") {
						const state = payload.state;
						if (state === "final") finish(true);
						else if (state === "error" || state === "aborted") finish(false);
					} else if (evt.event === "koi" && payload.stream === "tool") {
						const data = payload.data;
						if (data?.phase === "start" && typeof data.name === "string") onb.update(`Getting to know you · ${onboardingActivity(data.name)}`);
					}
				}
			});
			client.start();
			setTimeout(() => finish(false), 360 * 1e3).unref?.();
		});
		if (succeeded) {
			await markOnboardingComplete();
			onb.succeed("Got to know you — your people, your calendar, and what's live");
		} else onb.warn("Couldn't finish getting to know you yet — I'll pick it up next time you open koi");
	} catch {
		onb.warn("Couldn't finish getting to know you yet — I'll pick it up next time you open koi");
	} finally {
		try {
			client?.stop();
		} catch {}
	}
}
const DEFAULT_PLATFORM_URL = "https://www.skykoi.com";
function resolvePlatformUrl(opt) {
	return (opt || process.env.SKYKOI_PLATFORM_URL || DEFAULT_PLATFORM_URL).replace(/[\r\n\t]/g, "").replace(/\/$/, "").replace(/^https?:\/\/skykoi\.com$/i, "https://www.skykoi.com");
}
function authFilePath() {
	return join(homedir(), ".skykoi", "auth.json");
}
async function readAuthFile() {
	try {
		const buf = await readFile(authFilePath(), "utf-8");
		const parsed = JSON.parse(buf);
		if (typeof parsed?.token !== "string") return null;
		return parsed;
	} catch {
		return null;
	}
}
async function writeAuthFile(data) {
	const path = authFilePath();
	await mkdir(join(homedir(), ".skykoi"), { recursive: true });
	await writeFile(path, JSON.stringify(data, null, 2) + "\n", {
		encoding: "utf-8",
		mode: 384
	});
	return path;
}
async function openBrowser(url) {
	const { spawn } = await import("node:child_process");
	const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
	const args = process.platform === "win32" ? [
		"/c",
		"start",
		"",
		url
	] : [url];
	try {
		spawn(cmd, args, {
			stdio: "ignore",
			detached: true
		}).unref();
	} catch {}
}
/**
* Claim THIS device to the user's koi: resolve their default koi + its gateway
* token from the platform, then write the device's gateway identity so it
* heartbeats (registering its public/tunnel URL) under that koi. After this the
* hosted UI can reach this device through the platform's gateway registry.
*/
async function claimDeviceToKoi(platformUrl, token) {
	const koisRes = await fetch(`${platformUrl}/api/koi?pageSize=1&sortBy=createdAt&sortOrder=asc`, { headers: { Authorization: `Bearer ${token}` } }).catch(() => null);
	if (!koisRes || !koisRes.ok) return { error: `couldn't list your kois (${koisRes?.status ?? "network error"})` };
	let koi = (await koisRes.json().catch(() => ({}))).data?.[0];
	if (!koi?.id) {
		const createRes = await fetch(`${platformUrl}/api/koi`, {
			method: "POST",
			headers: {
				Authorization: `Bearer ${token}`,
				"Content-Type": "application/json"
			},
			body: JSON.stringify({
				name: "My Koi",
				type: "CUSTOM"
			})
		}).catch(() => null);
		if (!createRes || !createRes.ok) return { error: `couldn't create a koi for your account (${createRes?.status ?? "network error"})` };
		koi = (await createRes.json().catch(() => ({}))).data;
		if (!koi?.id) return { error: "created a koi but the platform didn't return its id" };
	}
	const ciRes = await fetch(`${platformUrl}/api/koi/${encodeURIComponent(koi.id)}/connect-info`, { headers: { Authorization: `Bearer ${token}` } }).catch(() => null);
	if (!ciRes || !ciRes.ok) return { error: `couldn't fetch your koi's connection info (${ciRes?.status ?? "network error"})` };
	const info = await ciRes.json().catch(() => ({}));
	const gatewayToken = info.data?.token;
	if (!gatewayToken) return { error: "the platform didn't return a gateway token for your koi" };
	const koiPlatformUrl = String(info.data?.platformUrl || platformUrl).trim().replace(/\/+$/, "");
	const recallApiKey = info.data?.recallApiKey?.trim();
	if (recallApiKey) await persistSkykoiEnvVar("RECALL_AI_API_KEY", recallApiKey).catch(() => {});
	const cfg = loadConfig();
	await writeConfigFile({
		...cfg,
		gateway: {
			...cfg.gateway,
			mesh: {
				...cfg.gateway?.mesh,
				koiId: koi.id,
				platformUrl: koiPlatformUrl
			},
			auth: {
				...cfg.gateway?.auth,
				mode: "token",
				token: gatewayToken
			}
		},
		models: {
			...cfg.models,
			providers: {
				...cfg.models?.providers,
				skykoi: {
					baseUrl: `${koiPlatformUrl}/api/ai/anthropic`,
					api: "anthropic-messages",
					apiKey: `${koi.id}.${gatewayToken}`,
					models: [FABLE_MODEL_DEF]
				}
			}
		},
		kois: {
			...cfg.kois,
			defaults: {
				...cfg.kois?.defaults,
				model: {
					...cfg.kois?.defaults?.model,
					primary: "skykoi/claude-fable-5"
				},
				thinkingDefault: cfg.kois?.defaults?.thinkingDefault ?? "high"
			}
		}
	});
	return {
		koiId: koi.id,
		koiName: koi.name
	};
}
/**
* Fable 5 model definition for the device's proxy-backed `skykoi` provider. A
* custom provider name (not the built-in `anthropic`) is required so pi uses our
* `baseUrl` (the proxy) instead of the catalog's `api.anthropic.com`.
*/
const FABLE_MODEL_DEF = {
	id: "claude-fable-5",
	name: "Claude Fable 5",
	reasoning: true,
	input: ["text", "image"],
	cost: {
		input: 10,
		output: 50,
		cacheRead: 1,
		cacheWrite: 12.5
	},
	contextWindow: 1e6,
	maxTokens: 64e3
};
/** Persist a key into ~/.skykoi/.env (create/update in place), so the value is
* present for the gateway on every start, not just this process. */
async function persistSkykoiEnvVar(name, value) {
	try {
		const { homedir } = await import("node:os");
		const { join } = await import("node:path");
		const { readFile, writeFile, mkdir } = await import("node:fs/promises");
		const dir = join(homedir(), ".skykoi");
		await mkdir(dir, { recursive: true });
		const path = join(dir, ".env");
		let content = "";
		try {
			content = await readFile(path, "utf-8");
		} catch {}
		const line = `${name}=${value}`;
		const re = new RegExp(`^${name}=.*$`, "m");
		content = re.test(content) ? content.replace(re, line) : `${content.replace(/\n?$/, "\n")}${line}\n`;
		await writeFile(path, content, { mode: 384 });
	} catch {}
}
/** Best-effort: bring the user's terminal back to the front once they approve in
* the browser, so they don't have to switch back manually. macOS only; no-op if
* the terminal app isn't recognized. */
async function focusTerminal() {
	if (process.platform !== "darwin") return;
	const termProgram = process.env.TERM_PROGRAM ?? "";
	const app = {
		Apple_Terminal: "Terminal",
		"iTerm.app": "iTerm",
		vscode: "Visual Studio Code",
		Hyper: "Hyper",
		WarpTerminal: "Warp",
		Tabby: "Tabby",
		WezTerm: "WezTerm",
		ghostty: "Ghostty"
	}[termProgram];
	if (!app) return;
	let tty = "";
	try {
		const { execFileSync } = await import("node:child_process");
		tty = execFileSync("tty", [], { stdio: [
			"inherit",
			"pipe",
			"ignore"
		] }).toString().trim();
	} catch {}
	let script = `tell application "${app}" to activate`;
	if (tty && termProgram === "Apple_Terminal") script = `tell application "Terminal"
  activate
  repeat with w in windows
    repeat with t in tabs of w
      if tty of t is "${tty}" then
        set index of w to 1
        set selected of t to true
      end if
    end repeat
  end repeat
end tell`;
	else if (tty && termProgram === "iTerm.app") script = `tell application "iTerm"
  activate
  repeat with w in windows
    repeat with t in tabs of w
      repeat with s in sessions of t
        if tty of s is "${tty}" then
          select w
          select t
        end if
      end repeat
    end repeat
  end repeat
end tell`;
	try {
		const { execFile } = await import("node:child_process");
		await new Promise((resolve) => {
			execFile("osascript", ["-e", script], () => resolve());
		});
	} catch {}
}
/** Shared tail for both login modes: optionally claim this device to the user's
* koi, restart the running gateway, and shape the result. */
async function claimAndFinish(platformUrl, token, base, doClaim) {
	if (!doClaim) return {
		ok: true,
		...base
	};
	section("Your koi");
	const connecting = step("Connecting this device");
	const claim = await claimDeviceToKoi(platformUrl, token);
	if ("error" in claim) {
		connecting.warn("Signed in, but couldn't connect this device to your koi");
		hint(claim.error);
		return {
			ok: true,
			...base
		};
	}
	const koiLabel = claim.koiName ? `"${claim.koiName}"` : "your koi";
	await settle();
	connecting.succeed(`Connected to ${koiLabel}`);
	if (!process.env.SKYKOI_TUNNEL) process.env.SKYKOI_TUNNEL = "cloudflare";
	await persistSkykoiEnvVar("SKYKOI_TUNNEL", "cloudflare");
	const online = step("Bringing it online");
	const ensure = await ensureLocalGatewayRunning();
	if (ensure.ok) {
		online.succeed(`Online — reachable from your phone and ${theme.command("koi.skykoi.com")}`);
		await requestDevicePermissions();
		await runOnboardingBeforeChat(ensure.wsUrl, ensure.token);
	} else if (ensure.wsUrl) {
		online.warn("Started — opening your chat; it'll connect as your koi finishes coming online");
		hint(ensure.error);
	} else {
		online.warn("Couldn't auto-start your koi");
		hint(ensure.error);
		hint(`Run ${theme.command("koi onboard")} to finish bringing it online.`);
	}
	const gatewayCoords = ensure.ok || ensure.wsUrl ? {
		gatewayUrl: ensure.wsUrl,
		gatewayToken: ensure.token
	} : {};
	return {
		ok: true,
		...base,
		koiId: claim.koiId,
		koiName: claim.koiName,
		...gatewayCoords
	};
}
async function loginCommand(opts = {}) {
	const platformUrl = resolvePlatformUrl(opts.platformUrl);
	if (opts.token && opts.token.trim()) {
		const token = opts.token.trim();
		if (!token.startsWith("skk_")) return {
			ok: false,
			error: "Token must start with `skk_`. Run `skykoi login` to generate one in the browser."
		};
		const res = await fetch(`${platformUrl}/api/auth/me`, { headers: { Authorization: `Bearer ${token}` } }).catch((err) => ({
			ok: false,
			status: 0,
			statusText: err?.message ?? "network error",
			json: async () => ({})
		}));
		if (!("ok" in res) || !res.ok) return {
			ok: false,
			error: `Token verification failed (${res?.status ?? "?"})`
		};
		const me = await res.json().catch(() => ({}));
		if (!me.userId || !me.orgId) return {
			ok: false,
			error: "Token validated but no user/org returned. Token may be malformed."
		};
		const path = await writeAuthFile({
			token,
			userId: me.userId,
			orgId: me.orgId,
			platformUrl,
			savedAt: (/* @__PURE__ */ new Date()).toISOString()
		});
		return await claimAndFinish(platformUrl, token, {
			userId: me.userId,
			orgId: me.orgId,
			authFile: path
		}, opts.claim !== false);
	}
	const secret = randomBytes(32).toString("base64url");
	const requestHash = createHash("sha256").update(secret).digest("hex");
	const host = (opts.hostname || hostname() || "this device").slice(0, 80);
	const verificationUrl = `${platformUrl.replace(/\/+$/, "")}/cli?req=${requestHash}`;
	section("Sign in");
	line(theme.accentBright("›"), "Approve this device in your browser:");
	process.stdout.write(`      ${theme.command(verificationUrl)}\n`);
	hint(opts.noBrowser ? "copy the link above into your browser" : "opening automatically — if it doesn't, copy the link above");
	if (!opts.noBrowser) await openBrowser(verificationUrl);
	const initRes = await fetch(`${platformUrl}/api/cli/auth/init`, {
		method: "POST",
		headers: { "Content-Type": "application/json" },
		body: JSON.stringify({
			requestHash,
			hostname: host,
			userAgent: `skykoi-cli node-${process.version} ${process.platform}`
		})
	}).catch((err) => ({
		ok: false,
		status: 0,
		statusText: err?.message ?? "network error",
		json: async () => ({})
	}));
	if (!("ok" in initRes) || !initRes.ok) return {
		ok: false,
		error: `Failed to start login (${initRes?.status ?? "?"}). Check your network and SKYKOI_PLATFORM_URL.`
	};
	const init = await initRes.json();
	const pollIntervalMs = Math.min(1e3, Math.max(500, (init.pollIntervalSec ?? .6) * 1e3));
	const signin = step("Waiting for your approval");
	const deadline = Date.now() + (opts.timeoutMs ?? 600 * 1e3);
	let firstPoll = true;
	while (Date.now() < deadline) {
		if (firstPoll) firstPoll = false;
		else await new Promise((r) => setTimeout(r, pollIntervalMs));
		let pollRes;
		try {
			pollRes = await fetch(`${platformUrl}/api/cli/auth/poll`, {
				method: "POST",
				headers: { "Content-Type": "application/json" },
				body: JSON.stringify({ requestSecret: secret })
			});
		} catch {
			continue;
		}
		if (!pollRes.ok) continue;
		const data = await pollRes.json().catch(() => ({}));
		if (data.status === "approved" && data.token && data.userId && data.orgId) {
			focusTerminal();
			const path = await writeAuthFile({
				token: data.token,
				userId: data.userId,
				orgId: data.orgId,
				platformUrl,
				savedAt: (/* @__PURE__ */ new Date()).toISOString()
			});
			signin.succeed("Signed in");
			hint(`token saved to ${path}`);
			return await claimAndFinish(platformUrl, data.token, {
				userId: data.userId,
				orgId: data.orgId,
				authFile: path
			}, opts.claim !== false);
		}
		if (data.status === "denied") {
			signin.clear();
			return {
				ok: false,
				error: "Login denied in the browser."
			};
		}
		if (data.status === "expired") {
			signin.clear();
			return {
				ok: false,
				error: "Login request expired. Run `koi login` again."
			};
		}
	}
	signin.clear();
	return {
		ok: false,
		error: "Timed out waiting for browser approval."
	};
}

//#endregion
export { login_exports as n, loginCommand as t };