M1.4: persistent libraries for headless MCP (lightcraft-cli mcp --library DIR); bump render cache version

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas 2026-10-01 00:15:32 -04:00
parent fa77faeb6b
commit 55c5c6f0c6
4 changed files with 27 additions and 5 deletions

View File

@ -24,6 +24,7 @@ USAGE:
given files imported. Options:
--connect [ADDR] drive a running app instead (`lightcraft --control 7980`; default 127.0.0.1:7980)
--demo headless: start with the procedurally generated demo library
--library DIR headless: open (or create) a persistent LightCraft library; edits are saved
--compact list only the helper tools (every command stays reachable via run_command)
lightcraft-cli render <IN> -o <OUT> [OPTIONS]
Develop one file and write the result (.png, .jpg, .tif or .webp by extension). Options:
@ -66,6 +67,7 @@ fn main() -> ExitCode {
fn mcp(args: &[String]) -> Result<(), String> {
let mut connect: Option<String> = None;
let mut demo = false;
let mut library: Option<String> = None;
let mut compact = false;
let mut files = Vec::new();
let mut i = 0;
@ -85,6 +87,7 @@ fn mcp(args: &[String]) -> Result<(), String> {
"--headless" => connect = None,
"--demo" => demo = true,
"--compact" => compact = true,
"--library" => library = Some(take_value(args, &mut i, "--library")?.to_string()),
a if a.starts_with("--") => return Err(format!("unknown option `{a}`")),
f => files.push(f.to_string()),
}
@ -92,8 +95,8 @@ fn mcp(args: &[String]) -> Result<(), String> {
}
let backend: Box<dyn Backend> = match connect {
Some(addr) => {
if !files.is_empty() || demo {
return Err("FILES and --demo apply to headless mode only (import through the `import` tool instead)".into());
if !files.is_empty() || demo || library.is_some() {
return Err("FILES, --demo and --library apply to headless mode only (import through the `import` tool instead)".into());
}
match Remote::connect(&addr) {
Ok(r) => {
@ -107,7 +110,16 @@ fn mcp(args: &[String]) -> Result<(), String> {
}
}
None => {
let mut h = if demo { Headless::demo() } else { Headless::default() };
let mut h = match &library {
Some(dir) => {
let mut h = Headless::default();
let r = h.session.open_library(dir, demo).map_err(|e| e.to_string())?;
eprintln!("lightcraft-cli mcp: opened library {dir} ({r:?})");
h
}
None if demo => Headless::demo(),
None => Headless::default(),
};
if !files.is_empty() {
let paths = expand_paths(&files);
let r = h.session.execute("library.import", &json!({"paths": paths})).map_err(|e| e.to_string())?;

View File

@ -20,7 +20,7 @@ use lightcraft_raster::{Histogram, Rgb32f};
use serde::{Deserialize, Serialize};
/// Bump when the pipeline's output changes, to invalidate cached thumbnails.
pub const RENDER_CACHE_VERSION: u64 = 1;
pub const RENDER_CACHE_VERSION: u64 = 2;
/// Thumbnails render at one of these long edges (so window/cell size changes reuse the cache).
pub const THUMB_SIZES: [usize; 4] = [128, 256, 384, 512];

View File

@ -19,6 +19,13 @@ pub struct Headless {
pub session: Session,
}
impl Drop for Headless {
/// Snapshot a persistent library on exit (a no-op for in-memory sessions).
fn drop(&mut self) {
let _ = self.session.close_library();
}
}
impl Default for Headless {
fn default() -> Self {
Self::new(Session::new().with_fs())

View File

@ -13,7 +13,10 @@ It runs in one of two modes:
| **Headless** (default) | `lightcraft-cli mcp [--demo] [FILES/FOLDERS…]` | An in-process engine `Session`. Develop, render and export without a window. |
| **Connect** | `lightcraft-cli mcp --connect [127.0.0.1:7980]` | A running desktop app started with `lightcraft --control 7980`, through its loopback JSON-lines control channel ([control-protocol.md](control-protocol.md)). Adds the UI tools (screenshot, clicks, keys, pointer gestures). |
Options: `--demo` starts the headless session with the procedurally generated demo library;
Options: `--library DIR` opens (or creates) a persistent LightCraft library — the same crash-safe
format the desktop app uses (`~/Pictures/LightCraft Library` by default there) — so ratings, edits and albums
survive between sessions (with `--demo`, a new library is seeded with the demo photos);
`--demo` starts the headless session with the procedurally generated demo library;
`--compact` lists only the helper tools (see below). In connect mode the server starts even when
the app is not running yet and connects on the first call (and reconnects if the app restarts).