lembas -l / --local: everything in the project, nothing written elsewhere
Config, data and state are rooted at <project>/.agent/local/lembas (or .lembas/), kept out of git; a local directory the repository carries is refused. The user's logins and connections.yaml are read, never written; the user's config.yaml is not read. The TUI is started with TMPDIR inside it (the compiled binary unpacks its native library there) and OpenTUI's cache goes there too; the agent's commands keep the user's TMPDIR. An instance serves models, chat, speech and the balance only: the link, /remote, the service and the hub are off, deletes are not sent, and the server library, search, fetch, embeddings and personalization are not used. Self-update and uninstall are refused. hardline_disable is ignored, and tools treat the local directory as outside the project. The smoke test runs a whole local session in the compiled binary and checks that HOME, the XDG directories and TMPDIR gained nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
27 files changed
+636
-105
No files matched your search
+53
-4
@@ -6,8 +6,10 @@ import { createApp } from "./app.ts"
|
||||
import { attachmentsFor } from "./project/attach.ts"
|
||||
import { ConfigError, loadConfig } from "./config/load.ts"
|
||||
import { asMode, EFFORTS, MODES, type Effort, type Mode } from "./config/schema.ts"
|
||||
import { isLocal, localRoot } from "./config/paths.ts"
|
||||
import { SettingError } from "./config/settings.ts"
|
||||
import { renderJson, renderPlain, terminalAsker } from "./headless.ts"
|
||||
import { enterLocal, localFor, LocalError, localRefusal, localTmp, LOCAL_FLAGS } from "./project/local.ts"
|
||||
import { findProject, setTrust, trustOf } from "./project/root.ts"
|
||||
import { effortsFor } from "./provider/effort.ts"
|
||||
import { clientFor, ModelError, resolveModel } from "./provider/index.ts"
|
||||
@@ -18,7 +20,7 @@ import { VERSION } from "./version.ts"
|
||||
const HELP = `lembas ${VERSION} — terminal coding agent and project manager
|
||||
|
||||
Usage:
|
||||
lembas [-m model] [--mode M] [-c | -s id]
|
||||
lembas [-l] [-m model] [--mode M] [-c | -s id]
|
||||
the TUI (-c continues the last session here, -s resumes one)
|
||||
lembas run [options] "…" one prompt, headless
|
||||
lembas models [--discover] list configured models
|
||||
@@ -61,6 +63,21 @@ Usage:
|
||||
lembas --version
|
||||
lembas --licenses the licences of everything this binary contains
|
||||
|
||||
Local mode:
|
||||
lembas -l | --local [command …]
|
||||
keep everything in this project: config, sessions, memory, skills,
|
||||
the library, snapshots, worktrees, caches and trust live in
|
||||
.agent/local/lembas/ (git-ignored), and nothing is written outside
|
||||
it. Your logins and connections.yaml are read, never written; your
|
||||
config.yaml is not read (the local one is used; lembas -l config set
|
||||
… writes it). A LLeMbas instance is used only for its models, chat,
|
||||
speech in and out (/v1/audio) and the balance: no link, /remote or
|
||||
service, no sessions or deletes sent to it, no server library,
|
||||
search, fetch or embeddings, no account personalization, and no
|
||||
self-update. The instance still records the usage of each request,
|
||||
as any API does. The project is not trusted by -l itself: the first
|
||||
start asks, and the answer is kept in the local directory.
|
||||
|
||||
Options for run:
|
||||
-m, --model conn/model model to use (default: config.yaml \`model\`)
|
||||
--mode MODE manual | edit | auto | plan
|
||||
@@ -134,8 +151,33 @@ async function loginCommand(cmd: "login" | "logout", args: string[]) {
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const argv = process.argv.slice(2)
|
||||
// The environment as it was given, before local mode sets anything in it.
|
||||
const startEnv = { ...process.env }
|
||||
let argv = process.argv.slice(2)
|
||||
// -l / --local: before the command, or anywhere among the TUI's options.
|
||||
let local = false
|
||||
while (LOCAL_FLAGS.includes(argv[0] ?? "")) {
|
||||
local = true
|
||||
argv = argv.slice(1)
|
||||
}
|
||||
if ((!argv[0] || argv[0].startsWith("-")) && argv.some((a) => LOCAL_FLAGS.includes(a))) {
|
||||
local = true
|
||||
argv = argv.filter((a) => !LOCAL_FLAGS.includes(a))
|
||||
}
|
||||
const cmd = argv[0]
|
||||
if (local && (cmd === "-h" || cmd === "--help" || cmd === "help"))
|
||||
return void process.stdout.write(`Local mode here keeps everything in ${localFor(process.cwd()).root}\n\n${HELP}`)
|
||||
if (local && !["-v", "--version", "--licenses", "licenses", "-h", "--help", "help"].includes(cmd ?? "")) {
|
||||
try {
|
||||
for (const note of enterLocal(process.cwd()).notes) process.stderr.write(`${note}\n`)
|
||||
} catch (e) {
|
||||
if (e instanceof LocalError) fail(e.message)
|
||||
throw e
|
||||
}
|
||||
// What local mode turns off, said in one line.
|
||||
const off: Record<string, string> = { service: "lembas service", update: "lembas update", uninstall: "lembas uninstall (run it without -l)", remote: "lembas remote" }
|
||||
if (cmd && off[cmd]) fail(localRefusal(off[cmd]))
|
||||
}
|
||||
// install.sh asks where the three directories are before it writes anything into them.
|
||||
// Internal, so not in --help; it prints where each one is, and install.sh writes where it says.
|
||||
if (cmd === "__paths") {
|
||||
@@ -157,6 +199,12 @@ async function main() {
|
||||
},
|
||||
})
|
||||
if (!process.stdin.isTTY || !process.stdout.isTTY) fail('the TUI needs a terminal; use: lembas run "…"', 2)
|
||||
// Local mode: the TUI starts again with TMPDIR inside the project, which the runtime reads
|
||||
// only as it starts (the native library it unpacks goes there). The user's TMPDIR is passed on
|
||||
// for the commands the agent runs.
|
||||
const root = localRoot()
|
||||
if (root && process.env.TMPDIR !== localTmp(root))
|
||||
await (await import("./reload.ts")).restart(["-l", ...argv], { ...startEnv, TMPDIR: localTmp(root), LEMBAS_OUTER_TMPDIR: startEnv.TMPDIR ?? "" })
|
||||
if (values.mode && !asMode(values.mode)) fail(`--mode must be one of ${MODES.join(", ")}`, 2)
|
||||
await (await import("./lembas/webui.ts")).refreshWebui({ timeoutMs: 3000 })
|
||||
const { runTui } = await import("./tui/index.tsx")
|
||||
@@ -399,15 +447,16 @@ async function main() {
|
||||
if (key === "model" || key === "small_model") resolveModel(loadConfig(), String(v))
|
||||
},
|
||||
})
|
||||
const label = (src: string) => (src === "global" && isLocal() ? "local" : src)
|
||||
if (argv[1] === "list") {
|
||||
for (const e of settings.list()) console.log(`${e.def.key} = ${show(e.value) || `(${e.def.fallback ?? "unset"})`}${e.source === "default" ? "" : ` [${e.source}]`}${e.note ? ` (${e.note})` : ""}`)
|
||||
for (const e of settings.list()) console.log(`${e.def.key} = ${show(e.value) || `(${e.def.fallback ?? "unset"})`}${e.source === "default" ? "" : ` [${label(e.source)}]`}${e.note ? ` (${e.note})` : ""}`)
|
||||
return
|
||||
}
|
||||
const [key, ...rest] = args
|
||||
if (!key) fail(`usage: lembas config ${argv[1]} <key>${argv[1] === "set" ? " <value> [--project]" : ""}`, 2)
|
||||
if (argv[1] === "get") {
|
||||
const e = settings.get(key)
|
||||
console.log(`${key} = ${show(e.value) || `(${e.def.fallback ?? "unset"})`}${e.source === "default" ? "" : ` [${e.source}]`}`)
|
||||
console.log(`${key} = ${show(e.value) || `(${e.def.fallback ?? "unset"})`}${e.source === "default" ? "" : ` [${label(e.source)}]`}`)
|
||||
if (e.note) console.log(`⚠ ${e.note}`)
|
||||
const d = describe(e.def)
|
||||
if (d) console.log(d)
|
||||
|
||||
Reference in new issue
Block a user