forked from 1b-agent/fieldcraft-upstream
mcp, cli: list each command's parameters as JSON Schema
CommandSpec::listing gives id, label, the parameter doc and the schema; MCP list_commands, the control channel's engine.commands and the new fieldcraft-cli commands --json use it. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
217628ead4
commit
188f2d5fc4
7 files changed
+25
-6
No files matched your search
@@ -15,6 +15,7 @@ use serde_json::json;
|
||||
const USAGE: &str = "\
|
||||
usage:
|
||||
fieldcraft-cli commands list every command with its parameters
|
||||
fieldcraft-cli commands --json the same as JSON, each command's parameters as JSON Schema
|
||||
fieldcraft-cli run <script.json> run a script (a JSON array of {\"command\", \"params\"})
|
||||
and print the model summary
|
||||
fieldcraft-cli features print docs/features.md (every capability, ready or
|
||||
@@ -36,6 +37,10 @@ const MAX_SCRIPT_BYTES: u64 = 256 * 1024 * 1024;
|
||||
fn main() -> ExitCode {
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
let result = match args.first().map(String::as_str) {
|
||||
Some("commands") if args.get(1).map(String::as_str) == Some("--json") => {
|
||||
let all: Vec<_> = fieldcraft_engine::commands().iter().map(fieldcraft_engine::CommandSpec::listing).collect();
|
||||
serde_json::to_string_pretty(&all).map(|text| println!("{text}")).map_err(|e| e.to_string())
|
||||
}
|
||||
Some("commands") => {
|
||||
for c in fieldcraft_engine::commands() {
|
||||
println!("{:<24} {:<18} {}", c.id, c.label, c.params);
|
||||
|
||||
@@ -121,6 +121,19 @@ pub fn commands() -> &'static [CommandSpec] {
|
||||
cmd::ALL
|
||||
}
|
||||
|
||||
impl CommandSpec {
|
||||
/// The command as JSON for listings (`fieldcraft-cli commands --json`, the control channel's
|
||||
/// `engine.commands`, MCP `list_commands`): id, label, the parameter doc and, when described,
|
||||
/// the parameters as JSON Schema.
|
||||
pub fn listing(&self) -> Value {
|
||||
let mut v = serde_json::json!({"id": self.id, "label": self.label, "params": self.params});
|
||||
if let Some(form) = self.form {
|
||||
v["schema"] = schema::json_schema(form);
|
||||
}
|
||||
v
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_command(id: &str) -> Option<&'static CommandSpec> {
|
||||
commands().iter().find(|c| c.id == id)
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ impl Backend for Headless {
|
||||
}
|
||||
|
||||
fn commands(&mut self) -> Result<Value, String> {
|
||||
Ok(fieldcraft_engine::commands().iter().map(|c| json!({"id": c.id, "label": c.label, "params": c.params})).collect())
|
||||
Ok(fieldcraft_engine::commands().iter().map(fieldcraft_engine::CommandSpec::listing).collect())
|
||||
}
|
||||
|
||||
fn picture(&mut self, r: &PictureRequest) -> Result<Picture, String> {
|
||||
|
||||
@@ -243,6 +243,9 @@ fn a_batch_stops_at_the_first_error() {
|
||||
assert_eq!(ok(&mut s, "inspect_model", json!({}))["model"]["nodes"], 0);
|
||||
let listed = ok(&mut s, "list_commands", json!({"filter": "SOLVE."}));
|
||||
assert!(listed.as_array().unwrap().iter().all(|c| c["id"].as_str().unwrap().starts_with("solve.")));
|
||||
// Each command's parameters as JSON Schema, for agents to build valid calls.
|
||||
let statics = listed.as_array().unwrap().iter().find(|c| c["id"] == "solve.static").unwrap();
|
||||
assert_eq!(statics["schema"]["properties"]["threads"]["maximum"], 256);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -174,9 +174,7 @@ pub fn handle(app: &mut ModelApp, ctx: &egui::Context, req: &ControlRequest) ->
|
||||
let Some(steps) = p.get("commands") else { return err("missing `commands` (a list of {command, params})") };
|
||||
wrap(app.run_script(steps).map(|()| json!({"journal": app.session.journal.len()})))
|
||||
}
|
||||
"engine.commands" => {
|
||||
ok(Value::Array(fieldcraft_engine::commands().iter().map(|c| json!({"id": c.id, "label": c.label, "params": c.params})).collect()))
|
||||
}
|
||||
"engine.commands" => ok(Value::Array(fieldcraft_engine::commands().iter().map(fieldcraft_engine::CommandSpec::listing).collect())),
|
||||
"ui.inspect" => ok(json!({
|
||||
"ui": app.ui,
|
||||
"camera": app.cam,
|
||||
|
||||
@@ -26,7 +26,7 @@ checked and clamped (drag steps, wheel lines, window size); `ui.render` refuses
|
||||
|---|---|---|
|
||||
| `engine.execute` | `command`: command id, `params`: JSON object | the command's result |
|
||||
| `engine.script` | `commands`: `[{command, params}, …]` | stops at the first error; `journal`: commands that changed the model so far |
|
||||
| `engine.commands` | — | every command: `id`, `label`, `params` (the parameter documentation) |
|
||||
| `engine.commands` | — | every command: `id`, `label`, `params` (the parameter documentation) and `schema` (its parameters as JSON Schema) |
|
||||
|
||||
Commands never open forms or file pickers when run this way. `fieldcraft-cli commands` prints the
|
||||
same registry. `file.sample {name}` opens a sample (`parts`, `cantilever`, `impact`) and
|
||||
|
||||
+1
-1
@@ -91,7 +91,7 @@ software on the computer.
|
||||
|
||||
| Tool | Arguments | What it does |
|
||||
|---|---|---|
|
||||
| `list_commands` | `filter?` | Every command with its label and parameter documentation; `filter` matches id and label. |
|
||||
| `list_commands` | `filter?` | Every command with its label, parameter documentation and `schema` (its parameters as JSON Schema: types, units as `x-dimension`, defaults, required fields, one-of variants); `filter` matches id and label. |
|
||||
| `execute` | `command`, `params?` | Run any command (`list_commands`). A failure leaves the model unchanged and names what to fix. |
|
||||
| `batch` | `commands: [{command, params}]` | Run steps in order, stop at the first error; the reply has every result and the failing step (`failed.index`, `failed.command`, `failed.error`). Steps before it stay applied (`undo` takes them back). |
|
||||
| `inspect_model` | — | `model.info` (counts, units, definitions), `model.activity` (the step table) and `geometry.bodies` (bodies with measures and persistent face names, which sets name as `{body, face}`). |
|
||||
|
||||
Reference in new issue
Block a user