use anyhow::Result;
use rusqlite::{params, Connection, OptionalExtension};
use serde::Serialize;
pub const SCHEMA: &str = r#"
CREATE TABLE IF NOT EXISTS claude_accounts (
id INTEGER PRIMARY KEY,
label TEXT NOT NULL,
created_at INTEGER NOT NULL,
used_at INTEGER NOT NULL DEFAULT 0
);
"#;
pub const COLUMNS_1_30: [&str; 3] = [
"ALTER TABLE desks ADD COLUMN account INTEGER NOT NULL DEFAULT 0",
"ALTER TABLE panes ADD COLUMN account INTEGER",
"ALTER TABLE panes_closed ADD COLUMN account INTEGER",
];
pub const DEFAULT: i64 = 0;
pub const LABEL_CHARS: usize = 40;
pub const TOKEN_VAR: &str = "CLAUDE_CODE_OAUTH_TOKEN";
pub const OUTRANKS: [&str; 2] = ["ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_API_KEY"];
pub const TOKEN_LIFE_SECS: i64 = 365 * 24 * 3600;
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct Account {
pub id: i64,
pub label: String,
pub created_at: i64,
pub used_at: i64,
}
pub fn effective(desk: i64, pane: Option<i64>) -> i64 {
pane.unwrap_or(desk)
}
pub fn clean_label(label: &str) -> Option<String> {
let one: String = label
.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
.chars()
.take(LABEL_CHARS)
.collect();
let one = one.trim().to_string();
(!one.is_empty()).then_some(one)
}
pub fn valid_token(token: &str) -> std::result::Result<(), &'static str> {
if token.starts_with("sk-ant-api") {
return Err("that is an API key: add it in Keys as ANTHROPIC_API_KEY instead");
}
if !token.starts_with("sk-ant-oat") {
return Err("a token from claude setup-token starts with sk-ant-oat");
}
let shape = token
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_');
if !shape || !(60..=300).contains(&token.len()) {
return Err("that does not look like a whole token: copy it again from claude setup-token");
}
Ok(())
}
pub fn list(conn: &Connection) -> Result<Vec<Account>> {
let mut stmt =
conn.prepare("SELECT id, label, created_at, used_at FROM claude_accounts ORDER BY id")?;
let rows = stmt
.query_map([], row_to_account)?
.collect::<rusqlite::Result<_>>()?;
Ok(rows)
}
pub fn get(conn: &Connection, id: i64) -> Result<Option<Account>> {
Ok(conn
.query_row(
"SELECT id, label, created_at, used_at FROM claude_accounts WHERE id = ?1",
params![id],
row_to_account,
)
.optional()?)
}
fn row_to_account(r: &rusqlite::Row) -> rusqlite::Result<Account> {
Ok(Account {
id: r.get(0)?,
label: r.get(1)?,
created_at: r.get(2)?,
used_at: r.get(3)?,
})
}
pub fn exists(conn: &Connection, id: i64) -> Result<bool> {
Ok(id == DEFAULT || get(conn, id)?.is_some())
}
pub fn add(conn: &Connection, label: &str, now: i64) -> Result<Account> {
conn.execute(
"INSERT INTO claude_accounts(label, created_at) VALUES (?1, ?2)",
params![label, now],
)?;
Ok(Account {
id: conn.last_insert_rowid(),
label: label.to_string(),
created_at: now,
used_at: 0,
})
}
pub fn rename(conn: &Connection, id: i64, label: &str) -> Result<bool> {
Ok(conn.execute(
"UPDATE claude_accounts SET label = ?2 WHERE id = ?1",
params![id, label],
)? > 0)
}
pub fn renewed(conn: &Connection, id: i64, now: i64) -> Result<bool> {
Ok(conn.execute(
"UPDATE claude_accounts SET created_at = ?2 WHERE id = ?1",
params![id, now],
)? > 0)
}
pub fn remove(conn: &mut Connection, id: i64) -> Result<bool> {
if id == DEFAULT {
return Ok(false);
}
let tx = conn.transaction()?;
let gone = tx.execute("DELETE FROM claude_accounts WHERE id = ?1", params![id])? > 0;
if gone {
tx.execute(
"UPDATE desks SET account = 0 WHERE account = ?1",
params![id],
)?;
tx.execute(
"UPDATE panes SET account = NULL WHERE account = ?1",
params![id],
)?;
tx.execute(
"UPDATE panes_closed SET account = NULL WHERE account = ?1",
params![id],
)?;
}
tx.commit()?;
Ok(gone)
}
pub fn set_desk(conn: &Connection, desk: i64, account: i64) -> Result<bool> {
Ok(conn.execute(
"UPDATE desks SET account = ?2 WHERE id = ?1 AND closed_at = 0",
params![desk, account],
)? > 0)
}
pub fn set_pane(conn: &Connection, pane: &str, account: Option<i64>) -> Result<bool> {
Ok(conn.execute(
"UPDATE panes SET account = ?2 WHERE id = ?1",
params![pane, account],
)? > 0)
}
pub fn following(conn: &Connection, desk: i64) -> Result<Vec<String>> {
let mut stmt =
conn.prepare("SELECT id FROM panes WHERE desk_id = ?1 AND account IS NULL ORDER BY slot")?;
let rows = stmt
.query_map(params![desk], |r| r.get(0))?
.collect::<rusqlite::Result<_>>()?;
Ok(rows)
}
pub fn touch(conn: &Connection, id: i64, now: i64) -> Result<()> {
conn.execute(
"UPDATE claude_accounts SET used_at = ?2 WHERE id = ?1",
params![id, now],
)?;
Ok(())
}
pub mod signin;
#[cfg(test)]
mod tests;