refactor(auth-broker): migrated OAuth flow from pi-ai CLI to AuthStorage
- Migrated OAuth provider authentication from standalone `pi-ai` CLI to in-process `AuthStorage.login()` flow in coding-agent. - Made provider argument optional for `login` and `logout` commands with interactive provider picker when omitted. - Added `list` command to enumerate registered OAuth providers with optional `--json` output format. - Removed `pi-ai` CLI binary and `bin` entry from @oh-my-pi/ai package; library API remains unchanged. - Updated documentation and examples to reflect new `omp auth-broker` command interface and in-process OAuth flow.
This commit is contained in:
@@ -47,8 +47,9 @@ The broker is the only writer of OAuth refresh tokens. Clients (including the ga
|
||||
```
|
||||
omp auth-broker serve [--bind=host:port] # boot the broker
|
||||
omp auth-broker token [--regenerate] [--json] # print or rotate the bearer token
|
||||
omp auth-broker login <provider> [--via=user@host] [--dry-run]
|
||||
omp auth-broker logout <provider>
|
||||
omp auth-broker login [<provider>] [--via=user@host] [--dry-run]
|
||||
omp auth-broker logout [<provider>]
|
||||
omp auth-broker list [--json]
|
||||
omp auth-broker import <file|dir> [--provider=<id>] [--include-disabled] [--dry-run] [--json]
|
||||
omp auth-broker migrate --from-local [--dry-run] [--json]
|
||||
omp auth-broker status [--json]
|
||||
@@ -56,8 +57,9 @@ omp auth-broker status [--json]
|
||||
|
||||
- `serve` opens the local SQLite store at `getAgentDbPath()` and binds an HTTP listener (default `127.0.0.1:8765`). On startup a token is ensured at `<config-dir>/auth-broker.token` (mode `0600`, `0700` parent dir). The background refresher refreshes any OAuth credential whose `expires - Date.now() < refreshSkewMs` (default 5 min) every `refreshIntervalMs` (default 60 s).
|
||||
- `token` prints the cached bearer or generates a new one. `--regenerate` rotates it.
|
||||
- `login <provider>` runs the per-provider OAuth flow locally, or — with `--via=user@host` — `ssh -L <callback-port>:127.0.0.1:<callback-port> user@host omp auth-broker login <provider>` so the OAuth callback hits the local browser but the credential is written on the broker host. Built-in callback ports: `anthropic:54545`, `openai-codex:1455`, `google-gemini-cli:8085`, `google-antigravity:51121`, `gitlab-duo:8080`.
|
||||
- `logout <provider>` deletes every credential row for `<provider>`.
|
||||
- `login [<provider>]` runs the per-provider OAuth flow locally — when no provider is supplied, it falls back to an interactive numbered picker. With `--via=user@host` it shells out `ssh -L <callback-port>:127.0.0.1:<callback-port> user@host omp auth-broker login <provider>` so the OAuth callback hits the local browser but the credential is written on the broker host (`--via` requires `<provider>`). Built-in callback ports: `anthropic:54545`, `openai-codex:1455`, `google-gemini-cli:8085`, `google-antigravity:51121`, `gitlab-duo:8080`. The OAuth dance is driven in-process via `AuthStorage.login()` — there is no longer a `pi-ai` bin to spawn.
|
||||
- `logout [<provider>]` deletes every credential row for `<provider>`. With no argument it shows an interactive numbered picker of currently-stored providers.
|
||||
- `list` enumerates every registered OAuth provider id/name (the union of built-ins + `registerOAuthProvider` custom providers). `--json` emits a machine-readable array.
|
||||
- `import <file|dir>` imports CLIProxyAPI-style JSON credentials into the local SQLite store. Maps `type` field → omp provider (`claude → anthropic`, `codex → openai-codex`, `gemini → google-gemini-cli`, `antigravity → google-antigravity`, `gemini-cli → google-gemini-cli`).
|
||||
- `migrate --from-local` walks the local SQLite store + env-derived credentials and idempotently uploads them to the configured broker (`POST /v1/credential`).
|
||||
- `status` health-pings the configured remote broker.
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed the `pi-ai` CLI binary (`packages/ai/src/cli.ts`) and its `bin` entry. Use the in-process equivalent in the omp coding-agent CLI: `omp auth-broker login [provider]`, `omp auth-broker logout [provider]`, and `omp auth-broker list`. The library API (`AuthStorage.login()`, `getOAuthProviders()`, etc.) is unchanged.
|
||||
|
||||
## [15.4.3] - 2026-05-26
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -1057,13 +1057,14 @@ Official docs: [Application Default Credentials](https://cloud.google.com/docs/a
|
||||
|
||||
### CLI Login
|
||||
|
||||
The quickest way to authenticate:
|
||||
Authenticate via the [`omp`](https://omp.sh) coding-agent CLI, which drives this library's OAuth/API-key flows in-process and persists into `agent.db`:
|
||||
|
||||
```bash
|
||||
bunx @oh-my-pi/pi-ai login # interactive provider selection
|
||||
bunx @oh-my-pi/pi-ai login anthropic # login to specific provider
|
||||
bunx @oh-my-pi/pi-ai login vllm # store vLLM API key (or placeholder for local no-auth)
|
||||
bunx @oh-my-pi/pi-ai list # list available providers
|
||||
omp auth-broker login # interactive provider selection
|
||||
omp auth-broker login anthropic # login to a specific provider
|
||||
omp auth-broker login vllm # store vLLM API key (or placeholder for local no-auth)
|
||||
omp auth-broker list # list supported providers
|
||||
omp auth-broker logout # interactive — pick a stored credential to remove
|
||||
```
|
||||
|
||||
Credentials are saved to `agent.db` in the agent directory. `/login qianfan` opens the Qianfan console and stores the pasted API key.
|
||||
|
||||
@@ -28,9 +28,6 @@
|
||||
],
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"bin": {
|
||||
"pi-ai": "./src/cli.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"check": "biome check . && bun run check:types",
|
||||
"check:types": "tsgo -p tsconfig.json --noEmit",
|
||||
|
||||
@@ -1,262 +0,0 @@
|
||||
#!/usr/bin/env bun
|
||||
import * as readline from "node:readline";
|
||||
import { AuthStorage, SqliteAuthCredentialStore } from "./auth-storage";
|
||||
import { getOAuthProviders } from "./utils/oauth";
|
||||
import type { OAuthProvider } from "./utils/oauth/types";
|
||||
|
||||
const PROVIDERS = getOAuthProviders();
|
||||
|
||||
function prompt(rl: readline.Interface, question: string): Promise<string> {
|
||||
const { promise, resolve, reject } = Promise.withResolvers<string>();
|
||||
const input = process.stdin as NodeJS.ReadStream;
|
||||
const supportsRawMode = input.isTTY && typeof input.setRawMode === "function";
|
||||
const wasRaw = supportsRawMode ? input.isRaw : false;
|
||||
let settled = false;
|
||||
|
||||
const cleanup = () => {
|
||||
rl.off("SIGINT", onSigint);
|
||||
if (supportsRawMode) {
|
||||
input.off("keypress", onKeypress);
|
||||
input.setRawMode?.(wasRaw);
|
||||
}
|
||||
};
|
||||
|
||||
const finish = (result: () => void) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
cleanup();
|
||||
result();
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
finish(() => reject(new Error("Login cancelled")));
|
||||
};
|
||||
|
||||
const onSigint = () => {
|
||||
cancel();
|
||||
};
|
||||
|
||||
const onKeypress = (_str: string, key: readline.Key) => {
|
||||
if (key.name === "escape" || (key.ctrl && key.name === "c")) {
|
||||
cancel();
|
||||
rl.close();
|
||||
}
|
||||
};
|
||||
|
||||
if (supportsRawMode) {
|
||||
readline.emitKeypressEvents(input, rl);
|
||||
input.setRawMode(true);
|
||||
input.on("keypress", onKeypress);
|
||||
}
|
||||
|
||||
rl.once("SIGINT", onSigint);
|
||||
rl.question(question, answer => {
|
||||
finish(() => resolve(answer));
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
|
||||
async function login(provider: OAuthProvider): Promise<void> {
|
||||
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
||||
const promptFn = (msg: string) => prompt(rl, `${msg} `);
|
||||
const store = await SqliteAuthCredentialStore.open();
|
||||
const storage = new AuthStorage(store);
|
||||
await storage.reload();
|
||||
|
||||
try {
|
||||
await storage.login(provider, {
|
||||
onAuth(info) {
|
||||
const { url, instructions } = info;
|
||||
console.log(`\nOpen this URL in your browser:\n${url}`);
|
||||
if (instructions) console.log(instructions);
|
||||
console.log();
|
||||
},
|
||||
onProgress(message) {
|
||||
console.log(message);
|
||||
},
|
||||
onPrompt(p) {
|
||||
return promptFn(`${p.message}${p.placeholder ? ` (${p.placeholder})` : ""}:`);
|
||||
},
|
||||
});
|
||||
console.log(`\nCredentials saved to ~/.omp/agent/agent.db`);
|
||||
} finally {
|
||||
store.close();
|
||||
rl.close();
|
||||
}
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const args = process.argv.slice(2);
|
||||
const command = args[0];
|
||||
|
||||
if (!command || command === "help" || command === "--help" || command === "-h") {
|
||||
console.log(`Usage: bunx @oh-my-pi/pi-ai <command> [provider]
|
||||
|
||||
Commands:
|
||||
login [provider] Login to a provider
|
||||
logout [provider] Logout from a provider
|
||||
status Show logged-in providers
|
||||
list List available providers
|
||||
|
||||
Providers:
|
||||
anthropic Anthropic (Claude Pro/Max)
|
||||
github-copilot GitHub Copilot
|
||||
google-gemini-cli Google Gemini CLI
|
||||
google-antigravity Antigravity (Gemini 3, Claude, GPT-OSS)
|
||||
openai-codex OpenAI Codex (ChatGPT Plus/Pro)
|
||||
kimi-code Kimi Code
|
||||
kilo Kilo Gateway
|
||||
kagi Kagi
|
||||
tavily Tavily
|
||||
zai Z.AI (GLM Coding Plan)
|
||||
deepseek DeepSeek
|
||||
nanogpt NanoGPT
|
||||
minimax-code MiniMax Coding Plan (International)
|
||||
minimax-code-cn MiniMax Coding Plan (China)
|
||||
cursor Cursor (Claude, GPT, etc.)
|
||||
zenmux ZenMux
|
||||
ollama-cloud Ollama Cloud
|
||||
|
||||
Examples:
|
||||
bunx @oh-my-pi/pi-ai login # interactive provider selection
|
||||
bunx @oh-my-pi/pi-ai login anthropic # login to specific provider
|
||||
bunx @oh-my-pi/pi-ai logout anthropic # logout from specific provider
|
||||
bunx @oh-my-pi/pi-ai status # show logged-in providers
|
||||
bunx @oh-my-pi/pi-ai list # list providers
|
||||
`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (command === "status") {
|
||||
const storage = await SqliteAuthCredentialStore.open();
|
||||
try {
|
||||
const providers = storage.listProviders();
|
||||
if (providers.length === 0) {
|
||||
console.log("No credentials stored.");
|
||||
console.log(`Use 'bunx @oh-my-pi/pi-ai login' to authenticate.`);
|
||||
} else {
|
||||
console.log("Logged-in providers:\n");
|
||||
for (const provider of providers) {
|
||||
const oauth = storage.getOAuth(provider);
|
||||
if (oauth) {
|
||||
const expires = new Date(oauth.expires);
|
||||
const expired = Date.now() >= oauth.expires;
|
||||
const status = expired ? "(expired)" : `(expires ${expires.toLocaleString()})`;
|
||||
console.log(` ${provider.padEnd(20)} ${status}`);
|
||||
continue;
|
||||
}
|
||||
const apiKey = storage.getApiKey(provider);
|
||||
if (apiKey) {
|
||||
console.log(` ${provider.padEnd(20)} (api key)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
storage.close();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (command === "list") {
|
||||
console.log("Available providers:\n");
|
||||
for (const p of PROVIDERS) {
|
||||
console.log(` ${p.id.padEnd(20)} ${p.name}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (command === "logout") {
|
||||
let provider = args[1] as OAuthProvider | undefined;
|
||||
const storage = await SqliteAuthCredentialStore.open();
|
||||
|
||||
try {
|
||||
if (!provider) {
|
||||
const providers = storage.listProviders();
|
||||
if (providers.length === 0) {
|
||||
console.log("No credentials stored.");
|
||||
return;
|
||||
}
|
||||
|
||||
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
||||
console.log("Select a provider to logout:\n");
|
||||
for (let i = 0; i < providers.length; i++) {
|
||||
console.log(` ${i + 1}. ${providers[i]}`);
|
||||
}
|
||||
console.log();
|
||||
|
||||
const choice = await prompt(rl, `Enter number (1-${providers.length}): `);
|
||||
rl.close();
|
||||
|
||||
const index = parseInt(choice, 10) - 1;
|
||||
if (index < 0 || index >= providers.length) {
|
||||
console.error("Invalid selection");
|
||||
process.exit(1);
|
||||
}
|
||||
provider = providers[index] as OAuthProvider;
|
||||
}
|
||||
if (!provider) {
|
||||
console.error("No provider selected");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const oauth = storage.getOAuth(provider);
|
||||
const apiKey = storage.getApiKey(provider);
|
||||
if (!oauth && !apiKey) {
|
||||
console.error(`Not logged in to ${provider}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
storage.deleteProvider(provider);
|
||||
console.log(`Logged out from ${provider}`);
|
||||
} finally {
|
||||
storage.close();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (command === "login") {
|
||||
let provider = args[1] as OAuthProvider | undefined;
|
||||
|
||||
if (!provider) {
|
||||
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
||||
console.log("Select a provider:\n");
|
||||
for (let i = 0; i < PROVIDERS.length; i++) {
|
||||
console.log(` ${i + 1}. ${PROVIDERS[i].name}`);
|
||||
}
|
||||
console.log();
|
||||
|
||||
const choice = await prompt(rl, `Enter number (1-${PROVIDERS.length}): `);
|
||||
rl.close();
|
||||
|
||||
const index = parseInt(choice, 10) - 1;
|
||||
if (index < 0 || index >= PROVIDERS.length) {
|
||||
console.error("Invalid selection");
|
||||
process.exit(1);
|
||||
}
|
||||
provider = PROVIDERS[index].id as OAuthProvider;
|
||||
}
|
||||
if (!provider) {
|
||||
console.error("No provider selected");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!PROVIDERS.some(p => p.id === provider)) {
|
||||
console.error(`Unknown provider: ${provider}`);
|
||||
console.error(`Use 'bunx @oh-my-pi/pi-ai list' to see available providers`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`Logging in to ${provider}…`);
|
||||
await login(provider);
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(`Unknown command: ${command}`);
|
||||
console.error(`Use 'bunx @oh-my-pi/pi-ai --help' for usage`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error("Error:", err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,6 +1,19 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
|
||||
- Added interactive provider selection to `omp auth-broker logout` when no provider argument is supplied
|
||||
- Added `--json` flag to `omp auth-broker list` for machine-readable output
|
||||
- Added `omp auth-broker list` to enumerate supported OAuth providers (replaces `bunx @oh-my-pi/pi-ai list`).
|
||||
- Added interactive provider selection to `omp auth-broker login` and `omp auth-broker logout` when no provider argument is supplied (replaces `bunx @oh-my-pi/pi-ai login` / `logout` interactive flows).
|
||||
|
||||
### Changed
|
||||
|
||||
- Changed `omp auth-broker login` to support interactive provider selection when invoked without a provider argument
|
||||
- Changed `omp auth-broker logout` to support interactive provider selection from stored credentials when invoked without a provider argument
|
||||
- Changed `omp auth-broker login` to drive the per-provider OAuth/API-key flow in-process via `AuthStorage.login()` instead of spawning the `pi-ai` CLI subprocess. The pi-ai bin is being removed; the same login surface now lives entirely inside `omp`.
|
||||
- Changed default per-line truncation cap for search/grep output (`DEFAULT_MAX_COLUMN`) from `1024` to `512` characters.
|
||||
|
||||
## [15.4.3] - 2026-05-26
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import * as crypto from "node:crypto";
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import * as readline from "node:readline";
|
||||
import {
|
||||
AuthBrokerClient,
|
||||
type AuthCredential,
|
||||
@@ -28,6 +29,7 @@ import {
|
||||
listProvidersWithEnvKey,
|
||||
type OAuthCredential,
|
||||
type OAuthProvider,
|
||||
type OAuthProviderInfo,
|
||||
SqliteAuthCredentialStore,
|
||||
startAuthBroker,
|
||||
} from "@oh-my-pi/pi-ai";
|
||||
@@ -36,7 +38,7 @@ import { $ } from "bun";
|
||||
import chalk from "chalk";
|
||||
import { resolveAuthBrokerConfig } from "../session/auth-broker-config";
|
||||
|
||||
export type AuthBrokerAction = "serve" | "token" | "login" | "logout" | "status" | "import" | "migrate";
|
||||
export type AuthBrokerAction = "serve" | "token" | "login" | "logout" | "status" | "import" | "migrate" | "list";
|
||||
|
||||
export interface AuthBrokerCommandArgs {
|
||||
action: AuthBrokerAction;
|
||||
@@ -60,7 +62,16 @@ export interface AuthBrokerCommandArgs {
|
||||
};
|
||||
}
|
||||
|
||||
const ACTIONS: readonly AuthBrokerAction[] = ["serve", "token", "login", "logout", "import", "migrate", "status"];
|
||||
const ACTIONS: readonly AuthBrokerAction[] = [
|
||||
"serve",
|
||||
"token",
|
||||
"login",
|
||||
"logout",
|
||||
"import",
|
||||
"migrate",
|
||||
"status",
|
||||
"list",
|
||||
];
|
||||
|
||||
/** Callback ports baked from the per-provider OAuth flow modules. */
|
||||
const CALLBACK_PORTS: Record<string, number> = {
|
||||
@@ -168,13 +179,23 @@ async function runToken(flags: AuthBrokerCommandArgs["flags"]): Promise<void> {
|
||||
}
|
||||
|
||||
async function runLogin(flags: AuthBrokerCommandArgs["flags"]): Promise<void> {
|
||||
const providerArg = flags.provider;
|
||||
const providers = getOAuthProviders();
|
||||
let providerArg = flags.provider;
|
||||
if (!providerArg) {
|
||||
throw new Error("Usage: omp auth-broker login <provider> [--via=user@host]");
|
||||
if (flags.via) {
|
||||
throw new Error(
|
||||
"Usage: omp auth-broker login <provider> --via=user@host (provider required for remote login)",
|
||||
);
|
||||
}
|
||||
providerArg = await pickProviderInteractively(providers);
|
||||
}
|
||||
const oauthProviders = new Set<string>(getOAuthProviders().map(p => p.id));
|
||||
if (!oauthProviders.has(providerArg)) {
|
||||
throw new Error(`Unknown OAuth provider '${providerArg}'. Known: ${[...oauthProviders].sort().join(", ")}`);
|
||||
if (!providers.some(p => p.id === providerArg)) {
|
||||
throw new Error(
|
||||
`Unknown OAuth provider '${providerArg}'. Known: ${providers
|
||||
.map(p => p.id)
|
||||
.sort()
|
||||
.join(", ")}`,
|
||||
);
|
||||
}
|
||||
if (flags.via) {
|
||||
await runRemoteLogin(providerArg, flags.via, flags.dryRun ?? false);
|
||||
@@ -184,18 +205,107 @@ async function runLogin(flags: AuthBrokerCommandArgs["flags"]): Promise<void> {
|
||||
}
|
||||
|
||||
async function runLocalLogin(provider: OAuthProvider): Promise<void> {
|
||||
// Spawn the pi-ai CLI in-process — it handles the per-provider OAuth dance
|
||||
// and persists into the same SQLite store the broker uses.
|
||||
const piAiCli = Bun.fileURLToPath(import.meta.resolve("@oh-my-pi/pi-ai/cli"));
|
||||
const proc = Bun.spawn({
|
||||
cmd: [process.execPath, piAiCli, "login", provider],
|
||||
stdin: "inherit",
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
// Drive the per-provider OAuth dance in-process. Persists into the same
|
||||
// SQLite store the broker uses.
|
||||
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
||||
const ask = (msg: string) => promptLine(rl, `${msg} `);
|
||||
const store = await SqliteAuthCredentialStore.open(getAgentDbPath());
|
||||
const storage = new AuthStorage(store);
|
||||
await storage.reload();
|
||||
try {
|
||||
await storage.login(provider, {
|
||||
onAuth({ url, instructions }) {
|
||||
process.stdout.write(`\nOpen this URL in your browser:\n${url}\n`);
|
||||
if (instructions) process.stdout.write(`${instructions}\n`);
|
||||
process.stdout.write("\n");
|
||||
},
|
||||
onProgress(message) {
|
||||
process.stdout.write(`${message}\n`);
|
||||
},
|
||||
onPrompt(p) {
|
||||
return ask(`${p.message}${p.placeholder ? ` (${p.placeholder})` : ""}:`);
|
||||
},
|
||||
});
|
||||
process.stdout.write(`\nCredentials saved to ${getAgentDbPath()}\n`);
|
||||
} finally {
|
||||
store.close();
|
||||
rl.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interactive `readline` prompt that cleanly tears down on Ctrl-C / Escape so
|
||||
* cancelling a half-finished login flow doesn't leave the terminal in raw mode.
|
||||
*/
|
||||
function promptLine(rl: readline.Interface, question: string): Promise<string> {
|
||||
const { promise, resolve, reject } = Promise.withResolvers<string>();
|
||||
const input = process.stdin as NodeJS.ReadStream;
|
||||
const supportsRawMode = input.isTTY && typeof input.setRawMode === "function";
|
||||
const wasRaw = supportsRawMode ? input.isRaw : false;
|
||||
let settled = false;
|
||||
|
||||
const cleanup = () => {
|
||||
rl.off("SIGINT", onSigint);
|
||||
if (supportsRawMode) {
|
||||
input.off("keypress", onKeypress);
|
||||
input.setRawMode?.(wasRaw);
|
||||
}
|
||||
};
|
||||
|
||||
const finish = (result: () => void) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
cleanup();
|
||||
result();
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
finish(() => reject(new Error("Login cancelled")));
|
||||
};
|
||||
|
||||
const onSigint = () => {
|
||||
cancel();
|
||||
};
|
||||
|
||||
const onKeypress = (_str: string, key: readline.Key) => {
|
||||
if (key.name === "escape" || (key.ctrl && key.name === "c")) {
|
||||
cancel();
|
||||
rl.close();
|
||||
}
|
||||
};
|
||||
|
||||
if (supportsRawMode) {
|
||||
readline.emitKeypressEvents(input, rl);
|
||||
input.setRawMode(true);
|
||||
input.on("keypress", onKeypress);
|
||||
}
|
||||
|
||||
rl.once("SIGINT", onSigint);
|
||||
rl.question(question, answer => {
|
||||
finish(() => resolve(answer));
|
||||
});
|
||||
const exitCode = await proc.exited;
|
||||
if (exitCode !== 0) {
|
||||
throw new Error(`pi-ai login exited with code ${exitCode}`);
|
||||
return promise;
|
||||
}
|
||||
|
||||
async function pickProviderInteractively(providers: readonly OAuthProviderInfo[]): Promise<string> {
|
||||
if (providers.length === 0) {
|
||||
throw new Error("No OAuth providers registered");
|
||||
}
|
||||
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
||||
try {
|
||||
process.stdout.write("Select a provider:\n\n");
|
||||
for (let i = 0; i < providers.length; i++) {
|
||||
process.stdout.write(` ${i + 1}. ${providers[i].name}\n`);
|
||||
}
|
||||
process.stdout.write("\n");
|
||||
const choice = await promptLine(rl, `Enter number (1-${providers.length}): `);
|
||||
const index = Number.parseInt(choice, 10) - 1;
|
||||
if (Number.isNaN(index) || index < 0 || index >= providers.length) {
|
||||
throw new Error(`Invalid selection: ${choice}`);
|
||||
}
|
||||
return providers[index].id;
|
||||
} finally {
|
||||
rl.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,12 +345,17 @@ async function runRemoteLogin(provider: string, via: string, dryRun: boolean): P
|
||||
}
|
||||
|
||||
async function runLogout(flags: AuthBrokerCommandArgs["flags"]): Promise<void> {
|
||||
const providerArg = flags.provider;
|
||||
if (!providerArg) {
|
||||
throw new Error("Usage: omp auth-broker logout <provider>");
|
||||
}
|
||||
let providerArg = flags.provider;
|
||||
const store = await SqliteAuthCredentialStore.open(getAgentDbPath());
|
||||
try {
|
||||
if (!providerArg) {
|
||||
const stored = store.listProviders();
|
||||
if (stored.length === 0) {
|
||||
process.stdout.write("No credentials stored.\n");
|
||||
return;
|
||||
}
|
||||
providerArg = await pickStoredProviderInteractively(stored);
|
||||
}
|
||||
store.deleteAuthCredentialsForProvider(providerArg, "logged out by user");
|
||||
process.stdout.write(`Logged out of ${providerArg}\n`);
|
||||
} finally {
|
||||
@@ -248,6 +363,37 @@ async function runLogout(flags: AuthBrokerCommandArgs["flags"]): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function pickStoredProviderInteractively(providers: string[]): Promise<string> {
|
||||
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
||||
try {
|
||||
process.stdout.write("Select a provider to logout:\n\n");
|
||||
for (let i = 0; i < providers.length; i++) {
|
||||
process.stdout.write(` ${i + 1}. ${providers[i]}\n`);
|
||||
}
|
||||
process.stdout.write("\n");
|
||||
const choice = await promptLine(rl, `Enter number (1-${providers.length}): `);
|
||||
const index = Number.parseInt(choice, 10) - 1;
|
||||
if (Number.isNaN(index) || index < 0 || index >= providers.length) {
|
||||
throw new Error(`Invalid selection: ${choice}`);
|
||||
}
|
||||
return providers[index];
|
||||
} finally {
|
||||
rl.close();
|
||||
}
|
||||
}
|
||||
|
||||
async function runList(flags: AuthBrokerCommandArgs["flags"]): Promise<void> {
|
||||
const providers = getOAuthProviders();
|
||||
if (flags.json) {
|
||||
process.stdout.write(`${JSON.stringify(providers.map(p => ({ id: p.id, name: p.name })))}\n`);
|
||||
return;
|
||||
}
|
||||
process.stdout.write("Available providers:\n\n");
|
||||
for (const p of providers) {
|
||||
process.stdout.write(` ${p.id.padEnd(20)} ${p.name}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── CLIProxyAPI import ─────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -732,6 +878,9 @@ export async function runAuthBrokerCommand(cmd: AuthBrokerCommandArgs): Promise<
|
||||
case "status":
|
||||
await runStatus(cmd.flags);
|
||||
return;
|
||||
case "list":
|
||||
await runList(cmd.flags);
|
||||
return;
|
||||
default: {
|
||||
// Exhaustive check.
|
||||
const _exhaustive: never = cmd.action;
|
||||
|
||||
@@ -56,8 +56,11 @@ export default class AuthBroker extends Command {
|
||||
"# Boot on a non-default port\n omp auth-broker serve --bind=127.0.0.1:9000",
|
||||
"# Print the bearer token\n omp auth-broker token",
|
||||
"# Rotate the bearer token\n omp auth-broker token --regenerate",
|
||||
"# List supported OAuth providers\n omp auth-broker list",
|
||||
"# Local login (run on the broker host)\n omp auth-broker login anthropic",
|
||||
"# Interactive provider selection\n omp auth-broker login",
|
||||
"# Remote login over SSH tunnel\n omp auth-broker login anthropic --via=user@broker",
|
||||
"# Log out of a provider (interactive without provider arg)\n omp auth-broker logout anthropic",
|
||||
"# Import a CLIProxyAPI auth dump\n omp auth-broker import ~/.cliproxy/auth",
|
||||
"# Import a single CLIProxyAPI JSON, overriding the provider mapping\n omp auth-broker import ~/.cliproxy/auth/claude-foo.json --provider anthropic",
|
||||
"# Preview a migration from local store + env vars to the configured broker\n omp auth-broker migrate --from-local --include-env --dry-run",
|
||||
|
||||
Reference in New Issue
Block a user