LLeMbas CLI 1.0.0
ci / check (push) Canceled after 0s

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:
HomerandClaude Opus 5.5 committed 2026-10-09 21:59:03 +00:00
commit f9bad01ed7
355 files changed
+47028

No files matched your search

+135
View File
@@ -0,0 +1,135 @@
// One entry per prompt: where the conversation and the working tree were before it and after it.
// /undo takes the files the turn changed back to how they were and drops the turn from the
// conversation; /redo reverses that. A new prompt clears what could be redone.
import type { Snapshots } from "../git/snapshot.ts"
import type { Message } from "../provider/types.ts"
import type { Engine, PromptMeta } from "./engine.ts"
import type { Store } from "./store.ts"
interface Turn {
prompt: string
/** engine.messages.length before the prompt. */
at: number
before?: string
after?: string
firstRow: number
lastRow: number
}
interface Undone {
turn: Turn
/** The working tree when it was undone — what /redo puts back. */
current?: string
messages: Message[]
}
export interface UndoResult {
prompt: string
files: string[]
}
export class Turns {
/** The prompt running now: where the tree was before it, and its turn id. */
current?: { before?: string; turnId?: string }
private done: Turn[] = []
private undone: Undone[] = []
/** Tree at the first prompt of the session: the base of /diff. */
base?: string
constructor(
private engine: Engine,
private snaps: Snapshots | undefined,
private store?: Store,
) {}
get canUndo() {
return this.done.length > 0
}
get canRedo() {
return this.undone.length > 0
}
/** Run a prompt with a snapshot either side. Every turn has an id: the one it was given
* (the web UI's message id), or a new one — carried on its `task` and `prompt` events. */
async prompt(text: string, extra: (string | import("../provider/types.ts").ImagePart)[] = [], shown = text, meta: PromptMeta = {}) {
this.undone = []
const before = this.snaps?.track()
this.base ??= before
const turnId = meta.turnId || crypto.randomUUID()
const turn: Turn = { prompt: text, at: this.engine.messages.length, before, firstRow: this.engine.lastRowId + 1, lastRow: 0 }
this.current = { ...turn, turnId }
this.engine.o.bus.emit({ type: "task", state: "start", turnId })
try {
return await this.engine.prompt(text, extra, shown, { ...meta, turnId })
} finally {
this.current = undefined
turn.after = this.snaps?.track()
turn.lastRow = this.engine.lastRowId
this.done.push(turn)
this.engine.o.bus.emit({ type: "task", state: "end", turnId })
}
}
/** A message the user sent while a task runs. False when no task runs: send it as a prompt
* instead. */
steer(text: string, id?: string): boolean {
if (!this.current) return false
this.engine.enqueue(text, id)
return true
}
/** What was sent during the task and never reached the model (queue mode, a turn that ended
* first, or Esc): for the caller to send next, or to give back to the user. */
takeInbox(): string[] {
return this.engine.takeInbox()
}
undo(): UndoResult | undefined {
const turn = this.done.pop()
if (!turn) return undefined
const current = this.snaps?.track()
let files: string[] = []
if (this.snaps && turn.before && current) {
files = this.snaps.changed(turn.before, current)
this.snaps.restore(turn.before, files)
}
const messages = this.engine.messages.splice(turn.at)
if (this.store && this.engine.sessionId && turn.lastRow >= turn.firstRow) this.store.setKind(this.engine.sessionId, turn.firstRow, turn.lastRow, "reverted")
this.undone.push({ turn, current, messages })
return { prompt: turn.prompt, files }
}
redo(): UndoResult | undefined {
const u = this.undone.pop()
if (!u) return undefined
let files: string[] = []
if (this.snaps && u.turn.before && u.current) {
files = this.snaps.changed(u.turn.before, u.current)
this.snaps.restore(u.current, files)
}
this.engine.messages.push(...u.messages)
if (this.store && this.engine.sessionId && u.turn.lastRow >= u.turn.firstRow) this.store.setKind(this.engine.sessionId, u.turn.firstRow, u.turn.lastRow, "message")
this.done.push(u.turn)
return { prompt: u.turn.prompt, files }
}
/** Everything changed since the session's first prompt, as a unified diff. */
diff(): string | undefined {
if (!this.snaps || !this.base) return undefined
const now = this.snaps.track()
return now ? this.snaps.diff(this.base, now) : undefined
}
/** Paths changed since the session's first prompt. */
touched(): string[] {
if (!this.snaps || !this.base) return []
const now = this.snaps.track()
return now ? this.snaps.changed(this.base, now) : []
}
reset() {
this.done = []
this.undone = []
this.base = undefined
}
}