Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9499e93bc4
|
No files matched your search
+1
-1
@@ -324,5 +324,5 @@ say ""
|
||||
say "Done. Open a new shell (or: source your rc file), then run: lembas"
|
||||
[ -n "$extra" ] && say "Aliases: $(printf '%s' "$extra" | sed -n "s/^alias \([a-z]*\)=.*/\1/p" | paste -sd' ')"
|
||||
if [ "$starter" -eq 1 ] && [ -z "$(sed -n 's/^model: *//p' "$CONFIG/config.yaml")" ]; then
|
||||
say "Next: add a connection to $CONFIG/connections.yaml and set model: in config.yaml."
|
||||
say "Next: lembas login <your LLeMbas server address>"
|
||||
fi
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "lembas-cli",
|
||||
"version": "2.0.0-beta.3",
|
||||
"version": "2.0.0-beta.4",
|
||||
"description": "LLeMbas CLI: a terminal coding agent and project manager for a LLeMbas server — OpenTUI, memory, skills, MCP, git-native.",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
|
||||
+10
-1
@@ -98,7 +98,7 @@ function fail(msg: string, code = 1): never {
|
||||
}
|
||||
|
||||
async function loginCommand(cmd: "login" | "logout", args: string[]) {
|
||||
const { codeEvent, codeInstructions, doneEvent, instances, isWebuiEntry, login, loginSummary, loginWithToken, logout, sync, LembasError } = await import("./lembas/login.ts")
|
||||
const { codeEvent, codeInstructions, doneEvent, instances, isWebuiEntry, login, loginSummary, loginWithToken, logout, sync, LembasError, SignInRejected } = await import("./lembas/login.ts")
|
||||
const { values, positionals } = parseArgs({
|
||||
args,
|
||||
allowPositionals: true,
|
||||
@@ -144,10 +144,19 @@ async function loginCommand(cmd: "login" | "logout", args: string[]) {
|
||||
// `login` again with no address refreshes the one instance there is.
|
||||
const again = address ? Object.values(known).find((i) => sameBase(i.base_url, address)) : Object.keys(known).length === 1 ? Object.values(known)[0] : undefined
|
||||
if (again && !values.ca && isWebuiEntry(again.connection)) {
|
||||
try {
|
||||
const r = await sync(again.connection, { setDefault: values.default })
|
||||
if (json) emit(doneEvent(r, false))
|
||||
else console.log(loginSummary(r, false).join("\n"))
|
||||
return
|
||||
} catch (e) {
|
||||
// The token here was revoked or is unknown to the instance: refreshing it can never work,
|
||||
// and "run login again" would send the person straight back here. Sign in afresh instead;
|
||||
// the new login replaces the old one.
|
||||
if (!(e instanceof SignInRejected)) throw e
|
||||
if (!json) console.error("The instance no longer accepts the sign-in kept here: signing in again.")
|
||||
address = again.base_url
|
||||
}
|
||||
}
|
||||
// An older kind of login (the models copied into connections.yaml) is replaced by signing in
|
||||
// again, at the address it had.
|
||||
|
||||
@@ -27,6 +27,8 @@ export function protocolMismatch(base: string, d: Pick<Discovery, "protocol" | "
|
||||
}
|
||||
|
||||
export class LembasError extends Error {}
|
||||
/** The instance answered 401: the token this machine holds is revoked or unknown there. `login` signs in afresh. */
|
||||
export class SignInRejected extends LembasError {}
|
||||
/** The server and this CLI speak no link protocol in common: no login, and no dial gets further. */
|
||||
export class ProtocolError extends LembasError {}
|
||||
|
||||
@@ -268,7 +270,7 @@ export async function checkToken(d: Discovery, token: string, tls: Tls | undefin
|
||||
|
||||
export async function models(d: { api: { openai: string } }, token: string, tls: Tls | undefined, timeoutMs?: number): Promise<ServedModel[]> {
|
||||
const res = await call(`${d.api.openai}/models`, { headers: { authorization: `Bearer ${token}`, accept: "application/json" } }, tls, timeoutMs)
|
||||
if (res.status === 401) throw new LembasError("the instance no longer accepts this sign-in — run login again")
|
||||
if (res.status === 401) throw new SignInRejected("the instance no longer accepts this sign-in — run login again")
|
||||
if (res.status === 403) throw new LembasError("this account may not use the instance's models through the API (the “Use the API” permission)")
|
||||
if (!res.ok) throw new LembasError(`listing the models: ${res.status} ${res.statusText}`)
|
||||
const body = (await res.json()) as { data?: ServedModel[] }
|
||||
@@ -293,7 +295,7 @@ export interface InstanceInfo {
|
||||
export async function instanceInfo(base: string, token: string, tls: Tls | undefined, timeoutMs?: number): Promise<InstanceInfo | undefined> {
|
||||
const res = await call(`${base}/api/v1/instance`, { headers: { authorization: `Bearer ${token}`, accept: "application/json" } }, tls, timeoutMs)
|
||||
if (res.status === 404) return undefined
|
||||
if (res.status === 401) throw new LembasError("the instance no longer accepts this sign-in — run login again")
|
||||
if (res.status === 401) throw new SignInRejected("the instance no longer accepts this sign-in — run login again")
|
||||
if (!res.ok) throw new LembasError(`reading the instance: ${res.status} ${res.statusText}`)
|
||||
const body = (await res.json().catch(() => undefined)) as InstanceInfo | undefined
|
||||
return body && typeof body === "object" ? body : undefined
|
||||
@@ -341,7 +343,7 @@ export interface Usage {
|
||||
export async function usage(base: string, token: string, tls: Tls | undefined, timeoutMs = 5000): Promise<Usage | undefined> {
|
||||
const res = await call(`${base}/v1/usage`, { headers: { authorization: `Bearer ${token}`, accept: "application/json" } }, tls, timeoutMs)
|
||||
if (res.status === 404) return undefined
|
||||
if (res.status === 401) throw new LembasError("the instance no longer accepts this sign-in — run login again")
|
||||
if (res.status === 401) throw new SignInRejected("the instance no longer accepts this sign-in — run login again")
|
||||
if (!res.ok) throw new LembasError(`reading the usage: ${res.status} ${res.statusText}`)
|
||||
const body = (await res.json().catch(() => undefined)) as Usage | undefined
|
||||
return body && typeof body === "object" ? body : undefined
|
||||
@@ -369,7 +371,7 @@ function asPersonalization(b: unknown): Personalization | undefined {
|
||||
export async function personalization(base: string, token: string, tls: Tls | undefined, timeoutMs = 5000): Promise<Personalization | undefined> {
|
||||
const res = await call(`${base}/v1/me/personalization`, { headers: { authorization: `Bearer ${token}`, accept: "application/json" } }, tls, timeoutMs)
|
||||
if (res.status === 404) return undefined
|
||||
if (res.status === 401) throw new LembasError("the instance no longer accepts this sign-in — run login again")
|
||||
if (res.status === 401) throw new SignInRejected("the instance no longer accepts this sign-in — run login again")
|
||||
if (!res.ok) throw new LembasError(`reading the personalization: ${res.status} ${res.statusText}`)
|
||||
return asPersonalization(await res.json().catch(() => undefined))
|
||||
}
|
||||
@@ -393,7 +395,7 @@ export async function savePersonalization(base: string, token: string, tls: Tls
|
||||
if (res.status === 422) throw new LembasError(`the instance would not take that personalization${detail ? `: ${detail}` : ""}`)
|
||||
throw new LembasError(detail ? `the instance did not allow it: ${detail}` : "not allowed: the instance refused to save the personalization")
|
||||
}
|
||||
if (res.status === 401) throw new LembasError("the instance no longer accepts this sign-in — run login again")
|
||||
if (res.status === 401) throw new SignInRejected("the instance no longer accepts this sign-in — run login again")
|
||||
if (!res.ok) throw new LembasError(`saving the personalization: ${res.status} ${res.statusText}`)
|
||||
return asPersonalization(await res.json().catch(() => undefined))
|
||||
}
|
||||
+2
-2
@@ -13,7 +13,7 @@ import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync }
|
||||
import { join, resolve } from "node:path"
|
||||
import { parseDocument } from "yaml"
|
||||
import { expandHome, globalPaths, isLocal, paths } from "../config/paths.ts"
|
||||
import { awaitToken, checkToken, discover, LembasError, signOut, spoken, startDevice, type DeviceStart, type Discovery, type Granted, type InstanceInfo, type ServedModel, type Tls } from "./client.ts"
|
||||
import { awaitToken, checkToken, discover, LembasError, SignInRejected, signOut, spoken, startDevice, type DeviceStart, type Discovery, type Granted, type InstanceInfo, type ServedModel, type Tls } from "./client.ts"
|
||||
import { dropCache, fetchWebui, isChatModel, readCache, specFor } from "./webui.ts"
|
||||
|
||||
export interface Instance {
|
||||
@@ -557,4 +557,4 @@ export async function logout(connection: string): Promise<LogoutResult> {
|
||||
return { connection, revoked, modelLeft: model && (ours || model.startsWith(`${connection}/`)) ? model : undefined }
|
||||
}
|
||||
|
||||
export { LembasError, readCache, specFor }
|
||||
export { LembasError, readCache, SignInRejected, specFor }
|
||||
@@ -555,3 +555,17 @@ test("lembas login --token-stdin --server --json: the token from stdin, the devi
|
||||
expect((await cli(["login", "--token-stdin", fake.url], "lmb_secret")).code).toBe(2)
|
||||
expect((await cli(["login", "--server", fake.url], "lmb_secret")).code).toBe(2)
|
||||
})
|
||||
|
||||
test("lembas login over a sign-in the instance no longer accepts signs in afresh, not in a loop", async () => {
|
||||
fake = lembas()
|
||||
await login(fake.url, { ...quick, onCode: () => {} })
|
||||
// The instance forgot the token kept here (revoked, or another instance's database).
|
||||
fake.token = "lmb_fresh"
|
||||
fake.polls = 0
|
||||
const r = await cli(["login", fake.url, "--json"], "")
|
||||
expect(r.code).toBe(0)
|
||||
const lines = r.out.trim().split("\n").map((l) => JSON.parse(l))
|
||||
expect(lines[0]).toMatchObject({ event: "code" })
|
||||
expect(lines.at(-1)).toMatchObject({ event: "done", fresh: true, connection: "example" })
|
||||
expect(readFileSync(keyFile("example"), "utf8").trim()).toBe("lmb_fresh")
|
||||
})
|
||||
Reference in new issue
Block a user