The first public release of LLeMbas CLI: a terminal coding agent and project manager for any LLM API, with permission modes, git snapshots, memory and skills, knowledge bases, MCP, voice, and a link to a LLeMbas instance whose web UI can work its sessions too. Signed Linux binaries for x64 and arm64. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
355 files changed
+47028
No files matched your search
@@ -0,0 +1,465 @@
|
||||
// /login and `lembas login`: sign this machine in to a LLeMbas instance, and point LLeMbas CLI at
|
||||
// it — one `webui` connection, named after the instance (example), and voice and web search through
|
||||
// it where the instance offers them.
|
||||
//
|
||||
// What is written is ordinary configuration, in the files rule 2 allows: one entry in the global
|
||||
// connections.yaml holding the address and the key as {file:} to a 0600 file beside it, never
|
||||
// pasted into the YAML. The models are not written anywhere: a webui connection reads them from the
|
||||
// instance at every start (webui.ts). `lembas.json` remembers which connections a login made, so
|
||||
// `login` again refreshes them and `logout` removes exactly them. Without a login nothing here is
|
||||
// read, and LLeMbas CLI runs on its hand-written config as before.
|
||||
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"
|
||||
import { join } from "node:path"
|
||||
import { parseDocument } from "yaml"
|
||||
import { paths } from "../config/paths.ts"
|
||||
import { awaitToken, discover, LembasError, signOut, spoken, startDevice, type DeviceStart, type Discovery, type InstanceInfo, type ServedModel, type Tls } from "./client.ts"
|
||||
import { dropCache, fetchWebui, isChatModel, readCache, specFor } from "./webui.ts"
|
||||
|
||||
export interface Instance {
|
||||
base_url: string
|
||||
connection: string
|
||||
email?: string
|
||||
name?: string
|
||||
/** The instance's own name (Example). */
|
||||
instance_name?: string
|
||||
ca?: string
|
||||
/** What the token may do, as the instance granted it: `library` in it makes the account's
|
||||
* library the default. An older login has none recorded until it is made again. */
|
||||
scope?: string
|
||||
/** The link protocols the instance listed when last asked: at login, and again at every
|
||||
* dial of the link, so an instance updated later is picked up. */
|
||||
protocols?: number[]
|
||||
logged_in_at: string
|
||||
}
|
||||
|
||||
interface Index {
|
||||
instances: Record<string, Instance>
|
||||
}
|
||||
|
||||
const indexFile = () => join(paths.config, "lembas.json")
|
||||
const keyDir = () => join(paths.config, "lembas")
|
||||
export const keyFile = (connection: string) => join(keyDir(), `${connection}.key`)
|
||||
|
||||
/** What a later look at the instance said it speaks, kept with the login. */
|
||||
export function rememberProtocols(connection: string, protocols: number[]) {
|
||||
const all = instances()
|
||||
const one = all[connection]
|
||||
if (!one || JSON.stringify(one.protocols) === JSON.stringify(protocols)) return
|
||||
saveIndex({ ...all, [connection]: { ...one, protocols } })
|
||||
}
|
||||
|
||||
export function instances(): Record<string, Instance> {
|
||||
try {
|
||||
const parsed = JSON.parse(readFileSync(indexFile(), "utf8")) as Index
|
||||
return parsed.instances ?? {}
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
function saveIndex(all: Record<string, Instance>) {
|
||||
mkdirSync(paths.config, { recursive: true })
|
||||
writeFileSync(indexFile(), JSON.stringify({ instances: all }, null, 2) + "\n", { mode: 0o600 })
|
||||
}
|
||||
|
||||
type Doc = ReturnType<typeof parseDocument>
|
||||
|
||||
function readYamlDoc(file: string): Doc {
|
||||
const doc = parseDocument(existsSync(file) ? readFileSync(file, "utf8") : "")
|
||||
if (!doc.contents) doc.contents = doc.createNode({}) as never
|
||||
return doc
|
||||
}
|
||||
|
||||
const connectionsFile = () => join(paths.config, "connections.yaml")
|
||||
const configFile = () => join(paths.config, "config.yaml")
|
||||
|
||||
function writeYamlDoc(file: string, doc: Doc, mode?: number) {
|
||||
mkdirSync(paths.config, { recursive: true })
|
||||
writeFileSync(file, doc.toString(), mode ? { mode } : undefined)
|
||||
if (mode) chmodSync(file, mode)
|
||||
}
|
||||
|
||||
/** What to call the instance's connection: the name it gives itself (`example`), or — from an
|
||||
* instance that gives none — its host's first label. Never one the user wrote by hand:
|
||||
* that gets `-lembas` after it. */
|
||||
export function connectionNameFor(baseUrl: string, preferred?: string): string {
|
||||
let name = (preferred ?? "").toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/^-+|-+$/g, "")
|
||||
if (!name) {
|
||||
const host = new URL(baseUrl).hostname
|
||||
const first = (/^\d+(\.\d+){3}$/.test(host) ? host.replace(/\./g, "-") : host.split(".")[0]!).toLowerCase()
|
||||
name = first.replace(/[^a-z0-9-]/g, "-").replace(/^-+|-+$/g, "") || "lembas"
|
||||
}
|
||||
const ours = Object.values(instances()).some((i) => i.connection === name && i.base_url === baseUrl)
|
||||
if (ours) return name
|
||||
return readYamlDoc(connectionsFile()).hasIn(["connections", name]) ? `${name}-lembas` : name
|
||||
}
|
||||
|
||||
export function writeConnection(connection: string, d: Discovery, tls: Tls | undefined) {
|
||||
const doc = readYamlDoc(connectionsFile())
|
||||
const entry: Record<string, unknown> = {
|
||||
type: "webui",
|
||||
url: d.base_url,
|
||||
api_key: `{file:${keyFile(connection)}}`,
|
||||
...(tls?.ca ? { tls: { ca: tls.ca } } : {}),
|
||||
}
|
||||
doc.setIn(["connections", connection], doc.createNode(entry))
|
||||
const node = doc.getIn(["connections", connection], true) as { commentBefore?: string } | undefined
|
||||
if (node)
|
||||
node.commentBefore = ` ${d.name ?? "LLeMbas"} at ${d.base_url}, written by \`lembas login\`. Its models and their settings come from the instance at every start; \`lembas logout ${connection}\` removes it.`
|
||||
writeYamlDoc(connectionsFile(), doc, 0o600)
|
||||
}
|
||||
|
||||
/** A model ref `old/x` as `new/x`; anything else unchanged. */
|
||||
function renamed(ref: unknown, from: string, to: string): unknown {
|
||||
return typeof ref === "string" && ref.startsWith(`${from}/`) ? `${to}/${ref.slice(from.length + 1)}` : ref
|
||||
}
|
||||
|
||||
/** An older kind of login named the connection after the host (`ai`) and copied every model
|
||||
* into it. Logging in again replaces it with the webui one: the old entry, key and cache go, and
|
||||
* config.yaml's model refs follow the new name. A start model that was only the instance's
|
||||
* default goes too, so the instance decides it from now on. Returns what changed, in words. */
|
||||
function replaceOld(old: string, now: string, instanceDefault: string | undefined): string[] {
|
||||
const said: string[] = []
|
||||
const conns = readYamlDoc(connectionsFile())
|
||||
if (conns.hasIn(["connections", old])) {
|
||||
conns.deleteIn(["connections", old])
|
||||
writeYamlDoc(connectionsFile(), conns, 0o600)
|
||||
}
|
||||
rmSync(keyFile(old), { force: true })
|
||||
dropCache(old)
|
||||
said.push(`the connection ${old} from an earlier login is now ${now}`)
|
||||
if (!existsSync(configFile())) return said
|
||||
const cfg = readYamlDoc(configFile())
|
||||
let changed = false
|
||||
const model = cfg.get("model")
|
||||
if (typeof model === "string" && model.startsWith(`${old}/`)) {
|
||||
if (instanceDefault && model === `${old}/${instanceDefault}`) {
|
||||
cfg.delete("model")
|
||||
said.push(`config.yaml no longer pins ${model}: sessions start on the instance's default`)
|
||||
} else {
|
||||
cfg.set("model", renamed(model, old, now))
|
||||
said.push(`config.yaml starts on ${renamed(model, old, now)}`)
|
||||
}
|
||||
changed = true
|
||||
}
|
||||
for (const key of ["small_model", "embedding"]) {
|
||||
const v = cfg.get(key)
|
||||
if (typeof v === "string" && v.startsWith(`${old}/`)) {
|
||||
cfg.set(key, renamed(v, old, now))
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
if (changed) writeYamlDoc(configFile(), cfg)
|
||||
return said
|
||||
}
|
||||
|
||||
/** Voice and web search through the instance, where it offers them and this machine does not
|
||||
* already use it: what the config had becomes the fallback (voice) or comes after it (search).
|
||||
* Returns what changed, in words. */
|
||||
export function useServices(connection: string, services: InstanceInfo["services"], several: boolean): string[] {
|
||||
if (!services) return []
|
||||
const said: string[] = []
|
||||
const cfg = readYamlDoc(configFile())
|
||||
const plain = (path: string[]) => {
|
||||
const v = cfg.getIn(path, true) as { toJSON?: () => unknown } | undefined
|
||||
return v && typeof v.toJSON === "function" ? (v.toJSON() as Record<string, unknown>) : undefined
|
||||
}
|
||||
const named = several ? { connection } : {}
|
||||
for (const [half, offered, words] of [
|
||||
["stt", services.stt, "voice input"],
|
||||
["tts", services.tts, "voice output"],
|
||||
] as const) {
|
||||
if (!offered) continue
|
||||
const now = plain(["voice", half])
|
||||
if (now?.provider === "webui") continue
|
||||
const { fallback: _, ...before } = now ?? {}
|
||||
cfg.setIn(["voice", half], cfg.createNode({ provider: "webui", ...named, ...(now ? { fallback: before } : {}) }))
|
||||
said.push(`${words} through ${connection}${now ? ` (what was set up stays as its fallback)` : ""}`)
|
||||
}
|
||||
if (services.search) {
|
||||
const search = plain(["search"]) ?? {}
|
||||
const order = Array.isArray(search.order) ? (search.order as string[]) : undefined
|
||||
if (!order?.includes("webui")) {
|
||||
// What was tried before is tried after the instance, in the order it was.
|
||||
const before = order ?? [...(search.searxng ? ["searxng"] : []), ...(search.firecrawl ? ["firecrawl"] : []), "ddg"]
|
||||
cfg.setIn(["search", "order"], ["webui", ...before.filter((p) => p !== "webui")])
|
||||
if (several) cfg.setIn(["search", "webui"], cfg.createNode({ connection }))
|
||||
said.push(`web search through ${connection} first (then ${before.join(", ")})`)
|
||||
}
|
||||
}
|
||||
if (said.length) writeYamlDoc(configFile(), cfg)
|
||||
return said
|
||||
}
|
||||
|
||||
/** Take a connection out of voice and search again: logout. */
|
||||
function dropServices(connection: string, several: boolean): void {
|
||||
if (!existsSync(configFile())) return
|
||||
const cfg = readYamlDoc(configFile())
|
||||
let changed = false
|
||||
const ours = (o: Record<string, unknown> | undefined) => o?.provider === "webui" && (o.connection === connection || (!o.connection && !several))
|
||||
for (const half of ["stt", "tts"]) {
|
||||
const node = cfg.getIn(["voice", half], true) as { toJSON?: () => unknown } | undefined
|
||||
const v = node && typeof node.toJSON === "function" ? (node.toJSON() as Record<string, unknown>) : undefined
|
||||
if (!ours(v)) continue
|
||||
if (v!.fallback) cfg.setIn(["voice", half], cfg.createNode(v!.fallback))
|
||||
else cfg.deleteIn(["voice", half])
|
||||
changed = true
|
||||
}
|
||||
const s = cfg.getIn(["search"], true) as { toJSON?: () => unknown } | undefined
|
||||
const search = s && typeof s.toJSON === "function" ? (s.toJSON() as Record<string, unknown>) : undefined
|
||||
const webui = search?.webui as { connection?: string } | undefined
|
||||
if (search && (webui?.connection === connection || (!webui?.connection && !several))) {
|
||||
if (Array.isArray(search.order) && search.order.includes("webui")) {
|
||||
cfg.setIn(["search", "order"], (search.order as string[]).filter((p) => p !== "webui"))
|
||||
changed = true
|
||||
}
|
||||
if (webui) {
|
||||
cfg.deleteIn(["search", "webui"])
|
||||
changed = true
|
||||
}
|
||||
if (search.fetch === "webui") {
|
||||
cfg.deleteIn(["search", "fetch"])
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
if (changed) writeYamlDoc(configFile(), cfg)
|
||||
}
|
||||
|
||||
/** The library's embedding model from the instance: with no `embedding:` in config.yaml,
|
||||
* the first model the instance serves as `kind: embedding`, as `<connection>/<served id>` — the
|
||||
* form the embedder resolves whatever the provider is called (library/embed.ts). An embedding the
|
||||
* person set — another instance's, or one of this machine's own — is never replaced: the vectors
|
||||
* already made with it are only found again with it. Returns what changed, in words. */
|
||||
export function useEmbedding(connection: string, models: ServedModel[]): string[] {
|
||||
const first = models.find((m) => m.kind === "embedding")
|
||||
if (!first) return []
|
||||
const cfg = readYamlDoc(configFile())
|
||||
const now = cfg.get("embedding")
|
||||
if (typeof now === "string" && now.trim()) return []
|
||||
const ref = `${connection}/${first.id}`
|
||||
cfg.set("embedding", ref)
|
||||
writeYamlDoc(configFile(), cfg)
|
||||
return [`the library embeds with ${ref} (embedding:)`]
|
||||
}
|
||||
|
||||
/** Logout: an `embedding:` on the connection that goes would only warn at every start. */
|
||||
function dropEmbedding(connection: string) {
|
||||
if (!existsSync(configFile())) return
|
||||
const cfg = readYamlDoc(configFile())
|
||||
const v = cfg.get("embedding")
|
||||
if (typeof v !== "string" || !v.startsWith(`${connection}/`)) return
|
||||
cfg.delete("embedding")
|
||||
writeYamlDoc(configFile(), cfg)
|
||||
}
|
||||
|
||||
function currentModel(): string | undefined {
|
||||
if (!existsSync(configFile())) return undefined
|
||||
const value = parseDocument(readFileSync(configFile(), "utf8")).get("model")
|
||||
return typeof value === "string" && value ? value : undefined
|
||||
}
|
||||
|
||||
/** Each served model's ref once this login is in place (by provider, `deepseek/x`; an
|
||||
* instance that does not speak protocol 2, or a provider a connection of the user's own is named after, keeps
|
||||
* `<connection>/<id>`). Read from the configuration as it now is. */
|
||||
async function refsOf(connection: string): Promise<Record<string, string>> {
|
||||
try {
|
||||
const { loadConfig } = await import("../config/load.ts")
|
||||
const loaded = loadConfig()
|
||||
return Object.fromEntries(loaded.refs.filter((r) => r.connection === connection).map((r) => [r.id, r.ref]))
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
export interface LoginResult {
|
||||
connection: string
|
||||
instance: Instance
|
||||
models: ServedModel[]
|
||||
/** The model a session starts on when config.yaml names none: the instance's default. */
|
||||
instanceDefault?: string
|
||||
/** The model ref config.yaml starts on, when login set it (--default). */
|
||||
defaultSet?: string
|
||||
/** What else changed — voice, search, an earlier login replaced — in words. */
|
||||
changes: string[]
|
||||
version: string
|
||||
/** Each served id's ref. */
|
||||
refs?: Record<string, string>
|
||||
}
|
||||
|
||||
export interface LoginOptions {
|
||||
/** The CA that signed the instance's certificate, when it is not one Bun trusts. */
|
||||
ca?: string
|
||||
/** Pin config.yaml's model to the instance's default (otherwise it follows the instance). */
|
||||
setDefault?: boolean
|
||||
/** Leave voice and web search as they are. */
|
||||
keepServices?: boolean
|
||||
signal?: AbortSignal
|
||||
/** Called once with the code to show and where to approve it. */
|
||||
onCode: (start: DeviceStart, d: Discovery) => void
|
||||
sleep?: (ms: number) => Promise<void>
|
||||
}
|
||||
|
||||
/** The two ways to approve a code, as the person is told them. */
|
||||
export function codeInstructions(start: DeviceStart, d: Discovery): string {
|
||||
const link = start.verification_uri_complete ?? start.verification_uri
|
||||
const name = d.name ? `${d.name} (LLeMbas ${d.version})` : `LLeMbas ${d.version}`
|
||||
return [
|
||||
`Found ${name} at ${d.base_url}. To sign this machine in:`,
|
||||
"",
|
||||
` 1. Click the link: ${link}`,
|
||||
` 2. Copy the code ${start.user_code} into User → Security → Devices`,
|
||||
"",
|
||||
`It lasts ${Math.round(start.expires_in / 60)} minutes. Waiting…`,
|
||||
].join("\n")
|
||||
}
|
||||
|
||||
/** Whether a login's connection is already the webui kind — not an older kind of entry, with
|
||||
* the models copied in, which a login again replaces. */
|
||||
export function isWebuiEntry(connection: string): boolean {
|
||||
const doc = readYamlDoc(connectionsFile()).toJSON() as { connections?: Record<string, { type?: string }> } | null
|
||||
return doc?.connections?.[connection]?.type === "webui"
|
||||
}
|
||||
|
||||
/** What a login or a refresh did, as lines for a terminal. */
|
||||
export function loginSummary(r: LoginResult, fresh: boolean): string[] {
|
||||
const out: string[] = []
|
||||
const who = r.instance.email ? ` as ${r.instance.email}` : ""
|
||||
const label = r.instance.instance_name ? `${r.instance.instance_name} (${r.instance.base_url})` : r.instance.base_url
|
||||
out.push(fresh ? `Signed in to ${label}${who}. Connection ${r.connection}: ${r.models.length} model${r.models.length === 1 ? "" : "s"}, read from the instance at every start.` : `${r.connection}: ${r.models.length} model${r.models.length === 1 ? "" : "s"} from ${label} (LLeMbas ${r.version}).`)
|
||||
const refOf = (id: string) => r.refs?.[id] ?? `${r.connection}/${id}`
|
||||
for (const m of r.models) out.push(` ${refOf(m.id)}${m.context ? ` ${Math.round(m.context / 1024)}k` : ""}${m.id === r.instanceDefault ? " (the instance's default)" : ""}`)
|
||||
const model = currentModel()
|
||||
if (r.defaultSet) out.push(`Sessions start on ${r.defaultSet}.`)
|
||||
else if (!model && r.instanceDefault) out.push(`Sessions start on the instance's default, ${refOf(r.instanceDefault)}.`)
|
||||
else if (model) out.push(`Sessions start on ${model} (config.yaml).`)
|
||||
for (const c of r.changes) out.push(`· ${c}`)
|
||||
if (r.instance.ca) out.push(`TLS: trusting ${r.instance.ca} for this connection.`)
|
||||
return out
|
||||
}
|
||||
|
||||
function webuiCount(): number {
|
||||
const doc = readYamlDoc(connectionsFile()).toJSON() as { connections?: Record<string, { type?: string }> } | null
|
||||
return Object.values(doc?.connections ?? {}).filter((c) => c?.type === "webui").length
|
||||
}
|
||||
|
||||
export async function login(address: string, o: LoginOptions): Promise<LoginResult> {
|
||||
const { discovery, tls } = await discover(address, o.ca ? { ca: o.ca } : undefined)
|
||||
const start = await startDevice(discovery, tls)
|
||||
o.onCode(start, discovery)
|
||||
const granted = await awaitToken(discovery, start, tls, { signal: o.signal, sleep: o.sleep })
|
||||
|
||||
const connection = connectionNameFor(discovery.base_url, discovery.connection)
|
||||
mkdirSync(keyDir(), { recursive: true, mode: 0o700 })
|
||||
writeFileSync(keyFile(connection), granted.access_token + "\n", { mode: 0o600 })
|
||||
chmodSync(keyFile(connection), 0o600)
|
||||
const fetched = await fetchWebui(connection, discovery.base_url, granted.access_token, tls)
|
||||
const instanceDefault = fetched.instance?.default_model ?? fetched.models.find((m) => m.default)?.id ?? undefined
|
||||
|
||||
const changes: string[] = []
|
||||
const known = instances()
|
||||
for (const [name, old] of Object.entries(known))
|
||||
if (name !== connection && old.base_url === discovery.base_url) {
|
||||
// The old token is signed out too: this machine holds one, the new one.
|
||||
if (existsSync(keyFile(name))) await signOut(old.base_url, readFileSync(keyFile(name), "utf8").trim(), old.ca ? { ca: old.ca } : undefined)
|
||||
changes.push(...replaceOld(name, connection, instanceDefault))
|
||||
delete known[name]
|
||||
}
|
||||
|
||||
writeConnection(connection, discovery, tls)
|
||||
const instance: Instance = {
|
||||
base_url: discovery.base_url,
|
||||
connection,
|
||||
email: granted.account?.email,
|
||||
name: granted.account?.name,
|
||||
...(discovery.name ? { instance_name: discovery.name } : {}),
|
||||
...(tls?.ca ? { ca: tls.ca } : {}),
|
||||
...(granted.scope ? { scope: granted.scope } : {}),
|
||||
protocols: spoken(discovery),
|
||||
logged_in_at: new Date().toISOString(),
|
||||
}
|
||||
saveIndex({ ...known, [connection]: instance })
|
||||
if (!o.keepServices) changes.push(...useServices(connection, fetched.instance?.services, webuiCount() > 1))
|
||||
changes.push(...useEmbedding(connection, fetched.models))
|
||||
const refs = await refsOf(connection)
|
||||
let defaultSet: string | undefined
|
||||
if (o.setDefault && instanceDefault) {
|
||||
const cfg = readYamlDoc(configFile())
|
||||
defaultSet = refs[instanceDefault] ?? `${connection}/${instanceDefault}`
|
||||
cfg.set("model", defaultSet)
|
||||
writeYamlDoc(configFile(), cfg)
|
||||
}
|
||||
// Chat models only: what the summary lists is what /model offers.
|
||||
return { connection, instance, models: fetched.models.filter(isChatModel), instanceDefault, defaultSet, changes, version: discovery.version, refs }
|
||||
}
|
||||
|
||||
function tokenOf(connection: string): string {
|
||||
try {
|
||||
return readFileSync(keyFile(connection), "utf8").trim()
|
||||
} catch {
|
||||
throw new LembasError(`no stored sign-in for ${connection} — run lembas login`)
|
||||
}
|
||||
}
|
||||
|
||||
/** Read the account's models again now (every start does this anyway). */
|
||||
export async function sync(connection: string, o: { setDefault?: boolean } = {}): Promise<LoginResult> {
|
||||
const instance = instances()[connection]
|
||||
if (!instance) throw new LembasError(`${connection} was not made by lembas login`)
|
||||
const { discovery, tls } = await discover(instance.base_url, instance.ca ? { ca: instance.ca } : undefined)
|
||||
const fetched = await fetchWebui(connection, discovery.base_url, tokenOf(connection), tls)
|
||||
const instanceDefault = fetched.instance?.default_model ?? fetched.models.find((m) => m.default)?.id ?? undefined
|
||||
// Logging in again reads the models again: an embedding model the instance has added since is
|
||||
// taken up the same way, still only where none is set.
|
||||
const changes = useEmbedding(connection, fetched.models)
|
||||
const refs = await refsOf(connection)
|
||||
let defaultSet: string | undefined
|
||||
if (o.setDefault && instanceDefault) {
|
||||
const cfg = readYamlDoc(configFile())
|
||||
defaultSet = refs[instanceDefault] ?? `${connection}/${instanceDefault}`
|
||||
cfg.set("model", defaultSet)
|
||||
writeYamlDoc(configFile(), cfg)
|
||||
}
|
||||
return { connection, instance, models: fetched.models.filter(isChatModel), instanceDefault, defaultSet, changes, version: discovery.version, refs }
|
||||
}
|
||||
|
||||
export interface LogoutResult {
|
||||
connection: string
|
||||
revoked: boolean
|
||||
/** config.yaml still names a model on the removed connection. */
|
||||
modelLeft?: string
|
||||
}
|
||||
|
||||
export async function logout(connection: string): Promise<LogoutResult> {
|
||||
const all = instances()
|
||||
const instance = all[connection]
|
||||
if (!instance) throw new LembasError(`${connection} was not made by lembas login${Object.keys(all).length ? ` — logged in: ${Object.keys(all).join(", ")}` : ""}`)
|
||||
// Whether config.yaml starts on one of its models — by provider (`deepseek/x`) the ref
|
||||
// no longer names the connection, so it is asked before the connection goes.
|
||||
let ours = false
|
||||
try {
|
||||
const { loadConfig } = await import("../config/load.ts")
|
||||
const { findRef } = await import("../provider/refs.ts")
|
||||
const loaded = loadConfig()
|
||||
const m = currentModel()
|
||||
ours = Boolean(m && findRef(loaded.connections, loaded.refs, m)?.connection === connection)
|
||||
} catch {}
|
||||
let revoked = false
|
||||
if (existsSync(keyFile(connection))) {
|
||||
revoked = await signOut(instance.base_url, tokenOf(connection), instance.ca ? { ca: instance.ca } : undefined)
|
||||
rmSync(keyFile(connection), { force: true })
|
||||
}
|
||||
const several = webuiCount() > 1
|
||||
const doc = readYamlDoc(connectionsFile())
|
||||
if (doc.hasIn(["connections", connection])) {
|
||||
doc.deleteIn(["connections", connection])
|
||||
writeYamlDoc(connectionsFile(), doc, 0o600)
|
||||
}
|
||||
dropCache(connection)
|
||||
dropServices(connection, several)
|
||||
dropEmbedding(connection)
|
||||
delete all[connection]
|
||||
saveIndex(all)
|
||||
const model = currentModel()
|
||||
return { connection, revoked, modelLeft: model && (ours || model.startsWith(`${connection}/`)) ? model : undefined }
|
||||
}
|
||||
|
||||
export { LembasError, readCache, specFor }
|
||||
Reference in new issue
Block a user