Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ Correct, robust and recoverable, with the foundations other themes stand on.
for a file that moved or was renamed.
- Signed Windows builds (no SmartScreen warning), then in-place updates; settings an
administrator can fix for a managed install (no AI Assistant, plugins or web services).
- Extend the OS keychain secret store to service connections (database passwords, S3 keys and
AI provider keys can already be remembered).
- Extend the OS keychain secret store to service connections (database passwords, S3 keys,
AI provider keys and the ChatGPT sign-in's tokens are already kept there).
- Overlay tools beside Clip and Erase: intersect, union, identity and near _(open)_.
- Spatial Join by distance, closest, centre in and largest overlap, with merge rules (sum, mean,
first…) for one-to-one joins ([#851](https://github.com/opengeos/RustGIS/issues/851)).
Expand Down Expand Up @@ -233,7 +233,8 @@ Open questions rather than plans; discuss before building.
documentation built in.
- The AI Assistant's skills, subagents and ACP clients; MCP over HTTP, an audited loopback
relay and a file bridge, which let external AI clients drive the app. Database passwords, S3
keys and AI provider keys optionally remembered in the desktop OS keychain.
keys and AI provider keys optionally remembered in the desktop OS keychain, where the ChatGPT
sign-in keeps its tokens.
- Sandboxed WebAssembly plugins with a registry at plugins.rustgis.app; process plugins that
run as programs (with Python environments RustGIS makes), in the background with progress and
Cancel.
Expand Down
46 changes: 42 additions & 4 deletions apps/rustgis-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
//! [--provider anthropic|openai|chatgpt|gemini|openrouter|ollama|custom] [--model M] [--base-url URL] [--yes] [--save out.rgis] [--trace log.txt]
//! rustgis-cli ask --models [--provider P] # list the provider's models
//! rustgis-cli ask --accounts | --account CLIENT_ID # list or select a saved registration
//! rustgis-cli ask --remove-account CLIENT_ID # sign out and forget a saved registration
//! rustgis-cli ask --login [--another-account] # sign in with ChatGPT to use your ChatGPT plan
//! rustgis-cli ask --logout
//! rustgis-cli mcp [--sample | --project p.rgis | --data f.geojson …] [--yes] # an MCP server on stdio
Expand Down Expand Up @@ -42,7 +43,7 @@ use serde_json::{Value, json};

fn usage() -> ExitCode {
eprintln!(
"usage:\n rustgis-cli commands | tools\n rustgis-cli plugins [install <file|folder> | uninstall <id> | enable <id> | disable <id> | browse | get <id> | prepare <id>] [--registry URL]\n rustgis-cli skills [add <SKILL.md|folder|.zip> | remove <name> | enable <name> | disable <name>]\n rustgis-cli info <data>\n rustgis-cli logo <out.png|out.ico|out.bmp> [--size N] [--canvas WxH --at X,Y (.bmp only)]\n rustgis-cli render <data|project> <out.png> [--basemap NAME] [--tiles URL] [--width N] [--height N] [--dpi N] [--worldview auto|default|china]\n rustgis-cli run [--sample] [--project P] [--data F]… --cmd 'id={{json}}'… [--save P] [--worldview auto|default|china]\n rustgis-cli ask [--sample] [--project P] [--data F]… \"request\" [--provider P] [--model M] [--base-url URL] [--yes] [--save P]\n rustgis-cli ask --models [--provider P]\n rustgis-cli ask --login [--another-account] [--account CLIENT_ID] | --logout\n rustgis-cli ask --accounts | --account CLIENT_ID\n rustgis-cli mcp [--sample] [--project P] [--data F]… [--yes] [--attach [PORT] [--token-file FILE] [--timeout SECONDS (5-600)]]\n rustgis-cli mcp-register [--client claude] [--name NAME] [--port PORT]"
"usage:\n rustgis-cli commands | tools\n rustgis-cli plugins [install <file|folder> | uninstall <id> | enable <id> | disable <id> | browse | get <id> | prepare <id>] [--registry URL]\n rustgis-cli skills [add <SKILL.md|folder|.zip> | remove <name> | enable <name> | disable <name>]\n rustgis-cli info <data>\n rustgis-cli logo <out.png|out.ico|out.bmp> [--size N] [--canvas WxH --at X,Y (.bmp only)]\n rustgis-cli render <data|project> <out.png> [--basemap NAME] [--tiles URL] [--width N] [--height N] [--dpi N] [--worldview auto|default|china]\n rustgis-cli run [--sample] [--project P] [--data F]… --cmd 'id={{json}}'… [--save P] [--worldview auto|default|china]\n rustgis-cli ask [--sample] [--project P] [--data F]… \"request\" [--provider P] [--model M] [--base-url URL] [--yes] [--save P]\n rustgis-cli ask --models [--provider P]\n rustgis-cli ask --login [--another-account] [--account CLIENT_ID] | --logout\n rustgis-cli ask --accounts | --account CLIENT_ID | --remove-account CLIENT_ID\n rustgis-cli mcp [--sample] [--project P] [--data F]… [--yes] [--attach [PORT] [--token-file FILE] [--timeout SECONDS (5-600)]]\n rustgis-cli mcp-register [--client claude] [--name NAME] [--port PORT]"
);
ExitCode::from(2)
}
Expand Down Expand Up @@ -197,6 +198,7 @@ fn ask(s: &mut Session, args: &[String]) -> Result<(), String> {
"--registry",
"--worldview",
"--account",
"--remove-account",
];
let prompt = args
.iter()
Expand Down Expand Up @@ -235,7 +237,30 @@ fn ask(s: &mut Session, args: &[String]) -> Result<(), String> {
return Ok(());
}
}
if let Some(id) = opt(args, "--remove-account") {
use rustgis_assistant::chatgpt;
let accounts = chatgpt::accounts()?;
let matches: Vec<_> = accounts.iter().filter(|c| c.client_id == id).collect();
let [c] = matches.as_slice() else {
return Err("--remove-account needs an unambiguous client id from --accounts".into());
};
if let Some(r) = chatgpt::remove(&c.client_id, &c.subject)?
&& let Err(e) = chatgpt::revoke(&r)
{
eprintln!(
"removed on this computer, but OpenAI didn't confirm the sign-out ({e}); disconnect RustGIS in ChatGPT's settings if needed"
);
return Ok(());
}
eprintln!("removed {}", c.label());
return Ok(());
}
let mut cfg = Config::from_env(&provider);
if cfg.login()
&& let Some(c) = rustgis_assistant::chatgpt::load_any()
{
cfg.sign_in(&c);
}
if let Some(m) = opt(args, "--model").map(str::to_string).or_else(|| std::env::var("RUSTGIS_ASSISTANT_MODEL").ok()) {
cfg.model = m;
}
Expand All @@ -244,9 +269,19 @@ fn ask(s: &mut Session, args: &[String]) -> Result<(), String> {
}
if args.iter().any(|a| a == "--logout") {
use rustgis_assistant::chatgpt;
if let Some(r) = chatgpt::sign_out()?
&& let Err(e) = chatgpt::revoke(&r)
{
let out = chatgpt::sign_out()?;
if let Some(e) = &out.unrevoked {
eprintln!(
"signed out on this computer, but OpenAI wasn't asked to end the session ({e}); disconnect RustGIS in ChatGPT's settings if needed"
);
return Ok(());
}
let Some(r) = out.revocation else {
// No token here to end a session with (none, or a command-line sign-in that has ended).
eprintln!("signed out on this computer; no session could be ended at OpenAI from here");
return Ok(());
};
if let Err(e) = chatgpt::revoke(&r) {
eprintln!(
"signed out on this computer, but OpenAI didn't confirm it ({e}); disconnect RustGIS in ChatGPT's settings if needed"
);
Expand All @@ -270,6 +305,9 @@ fn ask(s: &mut Session, args: &[String]) -> Result<(), String> {
}
let who = if c.email.is_empty() { "your ChatGPT account" } else { c.email.as_str() };
eprintln!("signed in as {who}; RustGIS now uses your ChatGPT plan (manage usage at {})", chatgpt::USAGE_URL);
if c.kept == chatgpt::Kept::Session {
eprintln!("this sign-in isn't remembered: the command line can't use the system keychain, so it lasts for this command only");
}
return Ok(());
}
// A signed-in account's requests renew its tokens as needed.
Expand Down
2 changes: 2 additions & 0 deletions apps/rustgis/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ workspace = true
[dependencies]
rustgis-engine = { workspace = true, features = ["geolibre"] }
rustgis-ui-egui = { workspace = true }
# The ChatGPT sign-in, given the keychain at start-up.
rustgis-assistant = { workspace = true }
eframe = { workspace = true, features = ["clipboard", "default_fonts", "links", "wgpu", "accesskit", "wayland", "x11"] }
egui = { workspace = true }
serde_json = { workspace = true }
Expand Down
6 changes: 5 additions & 1 deletion apps/rustgis/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ mod prefs {
}

pub fn services() -> Services {
let secrets: std::sync::Arc<dyn rustgis_engine::data::secrets::SecretStore> =
std::sync::Arc::new(crate::keychain::Keychain::default());
// The ChatGPT sign-in's tokens go to the same keychain (its accounts stay in the file).
rustgis_assistant::chatgpt::set_secret_store(Some(secrets.clone()));
Services {
load_pref: Some(Box::new(|key: &str| read().get(key).and_then(Value::as_str).map(str::to_string))),
save_pref: Some(Box::new(|key: &str, value: &str| {
Expand All @@ -54,7 +58,7 @@ mod prefs {
log::warn!("could not save preferences: {e}");
}
})),
secrets: Some(std::sync::Arc::new(crate::keychain::Keychain::default())),
secrets: Some(secrets),
..Services::default()
}
}
Expand Down
Loading
Loading