mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 14:41:19 +00:00
Merge remote-tracking branch 'upstream/main' into feat-4936-custom-dictionary
# Conflicts: # docs/gaps.md # docs/roadmap.md # docs/target-app-parity.md
This commit is contained in:
@@ -78,7 +78,7 @@ USAGE:
|
||||
Safe Mode is on. Plug-ins stay installed. `--safe-mode mcp` serves a headless session and
|
||||
does not take --connect.
|
||||
|
||||
vectorcraft-cli mcp [--connect ADDR | --headless] [--automation-read-root DIR]... [--automation-write-root DIR]...
|
||||
vectorcraft-cli mcp [--connect ADDR | --headless] [--automation-read-root DIR]... [--automation-write-root DIR]... [--allow-plugin-grants]
|
||||
Run the MCP server on stdio. Default: connect to a running app at 127.0.0.1:7979
|
||||
(vectorcraft --control 7979), falling back to a headless in-process session.
|
||||
--automation-read-root / --automation-write-root confine the files the agent's commands read
|
||||
@@ -86,8 +86,18 @@ USAGE:
|
||||
DIR, links followed; each may be given more than once, a path inside any of its folders being
|
||||
allowed, and a kind left out grants none of its access. They imply --headless: a running app
|
||||
enforces its own (vectorcraft --control PORT --automation-read-root DIR …).
|
||||
--allow-plugin-grants lets plugin.install's grant and allowHeavy take effect in this session,
|
||||
so whoever sends the commands can give a plug-in the permissions it lists, and a heavy budget,
|
||||
with nobody approving them. Without it they are refused, and a plug-in that lists permissions
|
||||
installs only when its hash is already recorded in grants.json, by a person's approval in the
|
||||
desktop app or by a grant accepted under this flag. A grant accepted under this flag stays
|
||||
recorded and counts as approved in later sessions. It implies --headless. It can't be used
|
||||
with --automation-read-root or --automation-write-root: a server confined to folders has no
|
||||
plug-in store, so it reads no grants.json, and a plug-in that lists permissions or asks for a
|
||||
heavy budget does not install there. With VECTORCRAFT_NO_PREFS set there is no store either,
|
||||
and a grant accepted under this flag lasts for that session only.
|
||||
|
||||
vectorcraft-cli run [--in FILE] [--cmd ID [--params JSON] | --commands FILE|-]... [--export FILE]... [--scale N] [--ppi N]
|
||||
vectorcraft-cli run [--in FILE] [--cmd ID [--params JSON] | --commands FILE|-]... [--export FILE]... [--scale N] [--ppi N] [--allow-plugin-grants]
|
||||
Headless batch: open FILE (any readable format) or start a new document, run commands in
|
||||
order, export each FILE in the format its extension picks (see Writable formats; a .ait FILE
|
||||
is a template, written as file.saveAsTemplate writes it). Raster exports use the document's
|
||||
@@ -97,6 +107,8 @@ USAGE:
|
||||
--cmd and --export steps: a JSON array of {\"command\": ID, \"params\": {...}} (or \"id\" for
|
||||
\"command\"), or one such object per line (JSON Lines), at most 64 MiB and 100000 steps.
|
||||
Errors name the step, counting from 0 as \"$N\" references do.
|
||||
--allow-plugin-grants lets plugin.install's grant and allowHeavy take effect in this run, as
|
||||
for mcp.
|
||||
|
||||
vectorcraft-cli commands
|
||||
Print the command catalogue as JSON.
|
||||
@@ -235,6 +247,14 @@ fn with_stores(mut h: Headless) -> Headless {
|
||||
h
|
||||
}
|
||||
|
||||
/// Makes `h`'s `plugin.install` accept `grant` and `allowHeavy` when `allow` (`--allow-plugin-grants`).
|
||||
fn accepting_grants(mut h: Headless, allow: bool) -> Headless {
|
||||
if allow {
|
||||
h.session.allow_plugin_grants_from_requests();
|
||||
}
|
||||
h
|
||||
}
|
||||
|
||||
/// `mcp`. With `--safe-mode` (`safe_mode`) it serves a headless session: a running app loads its own
|
||||
/// plug-ins, so `--connect` is refused and the default does not connect.
|
||||
fn mcp(args: &[String], safe_mode: bool) -> Result<(), String> {
|
||||
@@ -243,6 +263,7 @@ fn mcp(args: &[String], safe_mode: bool) -> Result<(), String> {
|
||||
}
|
||||
let mut connect: Option<String> = None;
|
||||
let mut headless = false;
|
||||
let mut allow_grants = false;
|
||||
let (mut read_roots, mut write_roots): (Vec<String>, Vec<String>) = (vec![], vec![]);
|
||||
let mut it = args.iter();
|
||||
while let Some(a) = it.next() {
|
||||
@@ -254,6 +275,7 @@ fn mcp(args: &[String], safe_mode: bool) -> Result<(), String> {
|
||||
match flag {
|
||||
"--connect" => connect = Some(it.next().cloned().ok_or("--connect needs an address")?),
|
||||
"--headless" => headless = true,
|
||||
"--allow-plugin-grants" => allow_grants = true,
|
||||
"--automation-read-root" | "--automation-write-root" => {
|
||||
let dir = inline.or_else(|| it.next().cloned()).ok_or_else(|| format!("{flag} needs a folder"))?;
|
||||
if flag == "--automation-read-root" { read_roots.push(dir) } else { write_roots.push(dir) }
|
||||
@@ -264,6 +286,12 @@ fn mcp(args: &[String], safe_mode: bool) -> Result<(), String> {
|
||||
if headless && connect.is_some() {
|
||||
return Err("use either --connect or --headless".into());
|
||||
}
|
||||
if allow_grants && connect.is_some() {
|
||||
return Err("--allow-plugin-grants applies to the headless server; in a running app a person approves each plug-in".into());
|
||||
}
|
||||
if allow_grants && !(read_roots.is_empty() && write_roots.is_empty()) {
|
||||
return Err("--allow-plugin-grants can't be used with --automation-read-root or --automation-write-root: a server confined to folders has no plug-in store to record a grant in".into());
|
||||
}
|
||||
if safe_mode && connect.is_some() {
|
||||
return Err("--safe-mode runs a headless session without plug-ins; a running app loads its own: start it with `vectorcraft --safe-mode --control <port>`".into());
|
||||
}
|
||||
@@ -278,9 +306,9 @@ fn mcp(args: &[String], safe_mode: bool) -> Result<(), String> {
|
||||
// Every thread of this process, not only the server's: nothing reaches past the roots.
|
||||
file_access::confine_process(roots.clone());
|
||||
eprintln!("vectorcraft-cli: files confined: {}", roots.describe());
|
||||
Box::new(Headless::with_document().with_automation_roots(Some(roots)))
|
||||
} else if headless || safe_mode {
|
||||
Box::new(with_stores(Headless::with_document()))
|
||||
Box::new(accepting_grants(Headless::with_document().with_automation_roots(Some(roots)), allow_grants))
|
||||
} else if headless || safe_mode || allow_grants {
|
||||
Box::new(accepting_grants(with_stores(Headless::with_document()), allow_grants))
|
||||
} else if let Some(addr) = connect {
|
||||
// Explicit address: fail loudly if the app isn't there.
|
||||
Box::new(Remote::connect(&addr).map_err(|e| format!("cannot connect to {addr}: {e}"))?)
|
||||
@@ -563,6 +591,7 @@ fn run(args: &[String]) -> Result<(), String> {
|
||||
let mut stdin_read = false;
|
||||
let mut scale = None::<f64>;
|
||||
let mut ppi = None::<f64>;
|
||||
let mut allow_grants = false;
|
||||
let mut it = args.iter();
|
||||
while let Some(a) = it.next() {
|
||||
let mut val = |what: &str| it.next().cloned().ok_or_else(|| format!("{a} needs {what}"));
|
||||
@@ -597,6 +626,7 @@ fn run(args: &[String]) -> Result<(), String> {
|
||||
steps.push(Step::Export(val("a file")?));
|
||||
params_for = None;
|
||||
}
|
||||
"--allow-plugin-grants" => allow_grants = true,
|
||||
"--scale" | "-s" => {
|
||||
let raw = val("a number")?;
|
||||
scale = Some(raw.parse::<f64>().map_err(|_| format!("--scale {raw}: not a number"))?);
|
||||
@@ -614,7 +644,7 @@ fn run(args: &[String]) -> Result<(), String> {
|
||||
}
|
||||
}
|
||||
|
||||
let mut h = with_stores(Headless::new());
|
||||
let mut h = accepting_grants(with_stores(Headless::new()), allow_grants);
|
||||
let mut out = std::io::stdout().lock();
|
||||
// A reader that stopped early (a closed pipe) isn't an error, and the batch goes on: its later
|
||||
// steps (an export…) still run, unprinted.
|
||||
|
||||
Reference in New Issue
Block a user